一:背景
在玩 C 的時候,經常會用 void*
來指向一段內存地址開端,然后再將其強轉成尺度更小的 char*
或 int*
來丈量一段內存,參考如下代碼:
int?main()
{void*?ptr?=?malloc(sizeof(int)?*?10);int*?int_ptr?=?(int*)ptr;char*?char_ptr?=?(char*)ptr;
}
由于 C 的自由度比較大,想怎么玩就怎么玩,帶來的弊端就是容易隱藏著一些不易發現的bug,歸根到底還是程序員的功底不扎實,C++ 設計者覺得不能把程序員想的太厲害,應該要力所能及的幫助程序員避掉一些不必要的潛在 bug,并且還要盡最大努力的避免對性能有過多的傷害,所以就出現了 4 個強制類型轉換運算符。
const_cast
reinterpret_cast
dynamic_cast
static_cast
既然 C++ 做了歸類,必然就有其各自用途,接下來我們逐一和大家聊一下。
二:理解四大運算符
1. const_cast
這是四個運算符中最好理解的,玩過 C++ 的都知道,默認情況下是不能修改一個 const 變量,比如下面這樣:
int?main()
{const?int?i?=?10;i?=?12;
}
這段代碼肯定是要報錯的,那如果我一定要實現這個功能,如何做呢?這就需要用到 const_cast
去掉它的常量符號,然后對 i 進行操作即可,所以修改代碼如下:
int?main()
{const?int?i?=?10;auto?j?=?const_cast<int*>(&i);*(j)?=?12;
}

2. reinterpret_cast
從名字上看就是一個 重新解釋轉換
,很顯然這個非常底層,如果大家玩過 windbg ,應該知道用 dt
命令可以將指定的內存地址按照某一個結構體丈量出來,比如說 C# 的 CLR 在觸發 GC 時,會有 gc_mechanisms
結構,參考代碼如下:
0:000>?dt?WKS::gc_mechanisms?0x7ffb6ba96e60
coreclr!WKS::gc_mechanisms+0x000?gc_index?????????:?1+0x008?condemned_generation?:?0n0+0x00c?promotion????????:?0n0+0x010?compaction???????:?0n1+0x014?loh_compaction???:?0n0+0x018?heap_expansion???:?0n0+0x01c?concurrent???????:?0+0x020?demotion?????????:?0n0+0x024?card_bundles?????:?0n1+0x028?gen0_reduction_count?:?0n0+0x02c?should_lock_elevation?:?0n0+0x030?elevation_locked_count?:?0n0+0x034?elevation_reduced?:?0n0+0x038?minimal_gc???????:?0n0+0x03c?reason???????????:?0?(?reason_alloc_soh?)+0x040?pause_mode???????:?1?(?pause_interactive?)+0x044?found_finalizers?:?0n0+0x048?background_p?????:?0n0+0x04c?b_state??????????:?0?(?bgc_not_in_process?)+0x050?allocations_allowed?:?0n1+0x054?stress_induced???:?0n0+0x058?entry_memory_load?:?0+0x05c?exit_memory_load?:?0
其實 reinterpret_cast 大概也是干這個事的,參考代碼如下:
typedef?struct?_Point?{int?x;int?y;
}?Point;int?main()
{Point?point?=?{?10,11?};//內存地址void*?ptr?=?&point;//根據內存地址?丈量出??PointPoint*?ptr_point?=?reinterpret_cast<Point*>(ptr);printf("x=%d",?ptr_point->x);
}
從代碼看,我直接根據 ptr
地址丈量出了 Point
結構,說實話這個和 C 玩法就比較類似了。
3. dynamic_cast
在多態場景下,有時候會遇到這樣的一個問題,一個父類有多個子類,我現在手擁一個父類,我不知道能不能將它轉換為其中一個子類,要試探一下看看,那怎么去試探呢?類似 C# 中的 as
運算符,在 C++ 中就需要用 dynamic_cast
?來做這件事情,參考如下:
//點
class?Point?{
public:Point(int?x,?int?y)?:x(x),?y(y)?{}virtual?void?show()?{}
public:int?x;int?y;
};//矩形
class?Rectangle?:public?Point?{
public:Rectangle(int?x,?int?y,?int?w,?int?h)?:?Point(x,?y),?w(w),?h(h)?{}
public:int?w;int?h;
};//三角形
class?Triangle?:public?Point?{
public:Triangle(int?x,?int?y,?int?z)?:Point(x,?y),?z(z)?{}
public:int?z;
};int?main()
{Point*?p1?=?new?Rectangle(10,?20,?100,?200);Point*?p2?=?new?Triangle(4,?5,?6);//將??p1?轉成?子類?Triangle?會報錯的Triangle*?t1?=?dynamic_cast<Triangle*>(p1);if?(t1?==?nullptr)?{printf("p1?不能轉成?Triangle");}
}

對,場景就是這個,p1 其實是 Rectangle
轉上去的, 這時候你肯定是不能將它向下轉成 Triangle
, 問題就在這里,很多時候你并不知道此時的 p1 是哪一個子類。
接下來的一個問題是,C++ 并不像C# 有元數據,那它是如何鑒別呢?其實這用了 RTTI 技術,哪里能看出來呢?哈哈,看匯編啦。
Triangle*?t1?=?dynamic_cast<Triangle*>(p1);
00831D57??push????????0??
00831D59??push????????offset?Triangle?`RTTI?Type?Descriptor'?(083C150h)??
00831D5E??push????????offset?Point?`RTTI?Type?Descriptor'?(083C138h)??
00831D63??push????????0??
00831D65??mov?????????eax,dword?ptr?[p1]??
00831D68??push????????eax??
00831D69??call????????___RTDynamicCast?(083104Bh)??
00831D6E??add?????????esp,14h??
00831D71??mov?????????dword?ptr?[t1],eax
從匯編可以看到編譯器這是帶夾私貨了,在底層偷偷的調用了一個 ___RTDynamicCast
函數在運行時幫忙檢測的,根據 cdcel
調用協定,參數是從右到左,恢復成代碼大概是這樣。
___RTDynamicCast(&p1,?0,?&Point,?&Triangle,0)
3. static_cast
從名字上就能看出,這個強轉具有 static 語義,也就是 編譯階段
就生成好了,具體安全不安全,它就不管了,就拿上面的例子,將 dynamic_cast 改成 static_cast 看看有什么微妙的變化。
int?main()
{Point*?p1?=?new?Rectangle(10,?20,?100,?200);Point*?p2?=?new?Triangle(4,?5,?6);Triangle*?t1?=?static_cast<Triangle*>(p1);printf("x=%d,?y=%d,z=%d",?t1->x,?t1->y,?t1->z);
}

我們發現居然轉成功了,而且 Triangle
的值也是莫名奇怪,直接取了 Rectangle
的前三個值,如果這是生產代碼,肯定要挨批了。。。
接下來簡單看下匯編代碼:
Triangle*?t1?=?static_cast<Triangle*>(p1);
00DF5B17??mov?????????eax,dword?ptr?[p1]??
00DF5B1A??mov?????????dword?ptr?[t1],eax??printf("x=%d,?y=%d,z=%d",?t1->x,?t1->y,?t1->z);
00DF5B1D??mov?????????eax,dword?ptr?[t1]??
00DF5B20??mov?????????ecx,dword?ptr?[eax+0Ch]??
00DF5B23??push????????ecx??
00DF5B24??mov?????????edx,dword?ptr?[t1]??
00DF5B27??mov?????????eax,dword?ptr?[edx+8]??
00DF5B2A??push????????eax??
00DF5B2B??mov?????????ecx,dword?ptr?[t1]??
00DF5B2E??mov?????????edx,dword?ptr?[ecx+4]??
00DF5B31??push????????edx??
00DF5B32??push????????offset?string?"x=%d,?y=%d,z=%d"?(0DF8C80h)??
00DF5B37??call????????_printf?(0DF145Bh)??
00DF5B3C??add?????????esp,10h
從代碼中看,它其實就是將 p1 的首地址給了 t1,然后依次把copy偏移值 +4,+8,+0C
, 除了轉換這個,還可以做一些 int ,long ,double 之間的強轉,當然也是一樣,編譯時匯編代碼就已經生成好了。
好了,本篇就說這么多,希望對你有幫助。