這篇介紹C++的4種類型轉換 dynamic_cast, static_cast, reinterpret_cast和const_cast。
1 dynamic_cast
從名字可以看出這種轉換是動態發生的,其轉化結果是可靠的;這種轉換通常用在父類和子類之間的轉化。
- 如果從父類到子類,那么要求父類必須有虛函數。
- 如果從子類到父類,dynamic_cast實際的功能和static_cast是一樣的,因而也不需要類的定義有虛函數。
所謂動態就是體現在虛函數表上,因為在轉換時需要從虛函數表動態獲取類的信息。
2 static_cast
相對動態轉換而言,所謂靜態轉換,是指轉換發生在編譯器編譯的時刻,而不是程序運行的時刻,那么這種轉化不需要類的虛函數表,因而轉換結果也是不可靠的,需要程序員自己保證轉換后是否有效。
- 和動態轉換一樣,靜態轉換也能用來在父類和子類之間。
- 從子類到父類的轉換是安全的
實際上dyanmic_cast在處理子類到父類的轉換時使用的就是static_cast的邏輯,即這兩者是一樣的,這就說明即使是dynamic_cast在處理這類轉換時也不需要類有虛函數表了。
- 從子類到父類的轉換是安全的
- 而從父類到子類的轉換時不安全的
相比較dynamic_cast在這類轉換時安全的,即如果轉換成功則返回一個有效指針,而轉換不成功則返回空指針,程序員可以依據返回值判斷轉換是否成功;而static_cast則沒有這個功能,返回值均不為空,這樣就無法保證轉換過程是否正確,必須由程序員來保障,好處是這類轉化不需要基類有虛函數表,比如當你的類繼承關系中沒有虛函數表時而又需要做類型轉換時使用。
- 此外靜態轉換還能處理基本數據類型之間的轉換
比如char, int, float, double等
int i = 1;
double d = static_cast<double>(1);
再次討論下動態轉換和靜態轉換。
動態轉換發生在程序運行時刻,而靜態轉換發生在編譯時刻,編譯時只能進行靜態檢查,無法確定真實運行時刻的信息,我們看下匯編代碼就很清楚:
#include <string>
#include <typeinfo>
class A {
public:
virtual ~A() {}
};
class B : public A {
public:
virtual ~B() {}
};
void foo(A * a) {
B * b1 = dynamic_cast<B *>(a);
B * b2 = static_cast<B *>(a);
}
int main(int argc, char * argv[]) {
B * b = new B();
foo(b);
return 0;
函數foo代碼把參數A *a轉換成B *,一次使用動態轉換,一次使用靜態轉換;生成的foo函數匯編代碼如下:
pushq %rbp
movq %rsp, %rbp
subq $32, %rsp
movq %rdi, -24(%rbp)
movq -24(%rbp), %rax
testq %rax, %rax
jne .L16
movl $0, %eax
jmp .L17
.L16:
movl $0, %ecx
movl $_ZTI1B, %edx
movl $_ZTI1A, %esi
movq %rax, %rdi
call __dynamic_cast
.L17:
movq %rax, -16(%rbp)
movq -24(%rbp), %rax
movq %rax, -8(%rbp)
leave
ret
對于動態轉換,首先檢查參數A * a是否為null,如果為null則無法繼續轉換,返回null表示轉換失敗,如果不為null則調用C++系統庫函數__dynamic_cast完成轉換;而對于靜態準換,編譯器直接把A * a的指針move給了B * b2的指針,沒有做任何檢查和轉換,只在編譯的時候編譯器檢查兩者的聲明類型是否匹配能否轉換。
當然如果是兩個不相關的類型,那么也是不能使用static_cast進行轉換的,因為編譯器在編譯的時候檢查了兩者類型是否合適,比如:
class A {
public:
virtual ~A() {}
};
class B {
public:
virtual ~B() {}
};
void foo(A * a) {
B * b = static_cast<B *>(a);
}
上述A和B沒有任何關系,編譯器在編譯時就報錯:
t.cpp: In function 'void foo(A*)':
t.cpp:<line>: error: invalid static_cast from type 'A*' to type 'B*'
因為A和B根本就是兩個風牛馬不相及的類;但是另一個reinterpret_cast卻可以做到。
3 reinterpret_cast
這類轉換很好理解,就是重新解析了內存比特位,不管你的內存內容是什么,全部按照目標類的內存聲明是套。
可想而知,這種轉換是一點安全也沒有的,任意轉換,任意解析,連編譯器靜態檢查都沒有,必須由程序員完成控制。如果原類型和目標類型的內存機構一樣轉換是沒有問題的,如果稍有不一樣,后果是不可預知的,程序crash就很容易出現。舉一個例子:
#include <stdio.h>
#include <string>
#include <typeinfo>
class A {
public:
int i;
int j;
public:
void foo() { printf("A::foo(): i=%d,j=%d\n", i, j); }
};
class B {
public:
int j;
int i;
public:
void foo() { printf("B::foo(): i=%d,j=%d\n", i, j); }
};
void foo(A * a) {
a->foo();
B * b = reinterpret_cast<B *>(a);
b->foo();
}
int main(int argc, char * argv[]) {
A * a = new A();
a->i = 100;
a->j = 200;
foo(a);
return 0;
}
類A和類B沒有任何繼承關系,A和B都定義了兩個變量i 和j,區別就是A先定義i,再定義j;而B相反先定義j,再定義i,我們運行結果:
A::foo(): i=100,j=200
B::foo(): i=200,j=100
在B:foo輸出里面顛倒了A中定義的i和j,為什么呢,因為B的定義中內存布局是先j在i的,當我們把同樣的一塊A內存重新解析成了B內容,前四個字節就是存儲j的值,后四個字節存儲i的值。
4 const_cast
最后一種轉換,很簡單就是去除const屬性:
#include <stdio.h>
#include <string>
#include <typeinfo>
class A {
};
class B {
};
void foo() {
A * a1 = new A();
const A * a2 = new A();
a1 = a2;
}
int main(int argc, char * argv[]) {
foo();
return 0;
}
編譯器報錯:
t.cpp: In function 'void foo()':
t.cpp:<line>: error: invalid conversion from 'const A*' to 'A*'
改正辦法就是使用const_cast,去除一個變量的const屬性。
a1 = const_cast<A *>(a2);
考慮另外一個問題,const_cast能不能增加const屬性呢?還是上面例子:
void foo() {
A * a1 = new A();
const A * a2 = new A();
a2 = a1;
a2 = const_cast<A *>(a1);
}
編譯沒有報任何錯誤,可見const_cast也可以用來增加const屬性,但其實這個作用沒什么用處,a2 = a1就可以實現,編譯器也沒有報錯;試想把一個不可更改對象改成一個可更改對象是有風險的,對象的屬性會發生變化,而反過來把一個可變對象改成一個不可變對象其實是沒有風險的,不會對對象本身發生變化。
另外補充一點,除了dynamic_cast以外,其他的轉換都是靜態轉換,也就是由編譯器在編譯時刻完成轉換,在編譯器生成的匯編代碼里已經沒有任何轉換信息了;編譯器只在編譯時根據靜態聲明類型判斷能夠完成轉換,如果編譯器認為可以完成轉換,那么通常會把原對象地址直接move到目標對象使用。