天天看點

C++ RTTI和類型轉換運算符

目錄

一、RTTI

1、dynamic_cast運算符

  2、dynamic_cast實作原理

3、typeid 運算符和type_info類

4、typeid 實作原理

二、類型轉換運算符

1、static_cast 運算符

2、const_cast

3、reinterpret_cast

一、RTTI

     RTTI是Runtime Type Identification的縮寫,即運作時類型識别,主要用于運作時能根據基類的指針或引用來獲得該指針或引用所指的對象的實際類型,進而調用實際類型的特定方法。C++在編譯器層面提供了typeid和dynamic_cast兩個運算符來支援RTTI。

1、dynamic_cast運算符

     dynamic_cast不能擷取某個基類指針或者引用指向或者引用的實際類型,但是能夠判斷該基類指針或者引用能否安全的轉換為某個實際類型,如果能夠轉換則傳回該實際類型的指針或者引用,如果不能夠轉換則傳回空指針,因為沒有空引用是以這種情況下會抛出bad_cast異常。注意使用dynamic_cast要求基類必須提供虛方法,否則直接報錯源類型不是多态的。

利用dynamic_cast對基類指針做轉換的示例如下:

#include <iostream>

using std::cout;

class ClassA {
public:
	virtual ~ClassA() {
	}
	;
	virtual void say() {
		cout << "ClassA\n";
	}
	;
};

class ClassB: public ClassA {
public:
	void say() {
		cout << "ClassB\n";
	}
	;
	void sayB() {
		cout << "sayB\n";
	}
	;
};

class ClassC: public ClassB {
public:
	void say() {
		cout << "ClassC\n";
	}
	;
	void sayB() {
		cout << "ClassC sayB\n";
	}
	;
	void sayC() {
		cout << "sayC\n";
	}
	;
};

int main() {
	//a的指針類型是ClassA,無法調用a的實際類型ClassC的特定方法
	ClassA * a = new ClassC;
//	ClassA * a = new ClassB;
	//如果ClassB不包含虛函數,則直接報錯源類型不是多态的
	ClassB * b = dynamic_cast<ClassB*>(a);
	//如果不能轉換,b是空指針,if為false
	if (b) {
		//say是虛方法,依然按照a實際指向的類型ClassC調用其say方法
		b->say();
		//sayB不是虛方法,按照b的指針類型ClassB調用其sayB方法
		b->sayB();
	}
	ClassC * c = dynamic_cast<ClassC*>(a);
	if (c) {
		c->say();
		c->sayC();
	}

	return 0;
}
           

 利用dynamic_cast對基類引用做轉換的示例如下:

#include <iostream>
#include <typeinfo>

using std::cout;
using std::bad_cast;

class ClassA {
public:
	virtual ~ClassA() {
	}
	;
	virtual void say() {
		cout << "ClassA\n";
	}
	;
};

class ClassB: public ClassA {
public:
	void say() {
		cout << "ClassB\n";
	}
	;
	void sayB() {
		cout << "sayB\n";
	}
	;
};

class ClassC: public ClassB {
public:
	void say() {
		cout << "ClassC\n";
	}
	;
	void sayB() {
		cout << "ClassC sayB\n";
	}
	;
	void sayC() {
		cout << "sayC\n";
	}
	;
};

int main() {
	//模拟方法調用中的将子類執行個體傳給基類引用參數
	ClassB cb;
	ClassA & a = cb;
	try {
		ClassB & b = dynamic_cast<ClassB &>(a);
		b.say();
		b.sayB();
	} catch (bad_cast & e) {
		cout << "cast error,errmsg->" << e.what() << "\n";
	}
	try {
		ClassC & c = dynamic_cast<ClassC &>(a);
		c.say();
		c.sayC();
	} catch (bad_cast & e) {
		cout << "cast error,errmsg->" << e.what() << "\n";
	}

	return 0;
}
           

  2、dynamic_cast實作原理

       dynamic_cast要求基類必須提供虛函數,是以其實作應該跟虛函數表有關,反彙編第一個測試用例,執行ClassB * b = dynamic_cast<ClassB*>(a);時的彙編代碼如下:

 <main()+34>: mov    -0x18(%rbp),%rax   将ClassA指針a的位址拷貝到rax中

 <main()+38>: test   %rax,%rax        對rax中的值判斷是否為空,即a是否是空指針

 <main()+41>: jne    0x40098f    <main()+50>  如果不是空指針,則跳轉到main+50的代碼處

 <main()+43>: mov    $0x0,%eax

 <main()+48>: jmp    0x4009a6    <main()+73>

 <main()+50>: mov    $0x0,%ecx    準備__dynamic_cast調用的四個參數

 <main()+55>: mov    $0x400e30,%edx

 <main()+60>: mov    $0x400e50,%esi

 <main()+65>: mov    %rax,%rdi

 <main()+68>: callq  0x400850    <[email protected]>  調用__dynamic_cast函數

 <main()+73>: mov    %rax,-0x20(%rbp)  将調用結果從rax拷貝到棧幀中

__dynamic_cast函數是底層C++ lib包中提供的實作,由C++ Itanium ABI定義,0x400e30和0x400e50兩個是編譯期确認的兩個全局變量,分别是ClassA 和ClassB的類型資訊。其大緻實作原理跟虛函數調用一樣,在虛函數表索引為-1處通過type_info類儲存了變量的實際類型資訊,根據該實際類型和目标類型的繼承關系判斷類型轉換是否安全,但是該類型資訊無法通過info vbl檢視,如下圖:

C++ RTTI和類型轉換運算符

3、typeid 運算符和type_info類

     typeid運算符的入參可以是一個類型名或一個結果為對象的表達式,對象可以是任意類型,注意判斷某個基類指針實際指向的類型時必須對該指針變量解引用。該運算符傳回一個類型為type_info的對象的const引用,type_info類在頭檔案typeinfo中定義,其重載了==和!=運算符,可以借此對類型進行比較,type_info類還有一個name()方法可以傳回類型資訊,不同編譯器廠商的實作不一緻。注意typeid的入參為空指針時會抛出bad_typeid異常。

基于上述第二個測試用例,main方法修改如下:

int main() {
	ClassA * a = new ClassC;
//	ClassA * a = new ClassB;
    cout<<"typeid(a).name()-->"<<typeid(*a).name()<<"\n";
	if(typeid(ClassC)==typeid(*a)){
		cout<<"is ClassC,name->"<<typeid(ClassC).name()<<"\n";
	}
	if(typeid(ClassB)==typeid(*a)){
		cout<<"is ClassB,name->"<<typeid(ClassB).name()<<"\n";
	}

	return 0;
}
           

執行結果如下:

C++ RTTI和類型轉換運算符

上述示例中ClassA包含了虛函數,typeid準确的識别出了變量a的真實類型,如果是普通的不包含虛函數的ClassA了?将該用例的virtual關鍵字去掉,執行結果如下:

C++ RTTI和類型轉換運算符

 說明typeid同dynamic_cast一樣,隻有在類包含虛函數的情況下才能正确識别出基類指針或者引用實際指向或者引用的類型,與dynamic_cast不同的是,如果基類沒有包含虛函數, typeid不會編譯報錯,而是傳回目标變量在編譯期确認的類型資訊。

4、typeid 實作原理

     在ClassA是虛函數下反彙編cout<<"typeid(a).name()-->"<<typeid(*a).name()<<"\n";,其中typeid操作符相關彙編代碼如下:

cmpq   $0x0,-0x18(%rbp)  判斷指針a是否非空,如果是空的則執行main()+111

je     0x400a0c    <main()+111>

mov    -0x18(%rbp),%rax    将指針a的位址拷貝到rax中

mov    (%rax),%rax   将rax中的位址處的後8個位元組拷貝到rax中,即擷取虛函數表的位址

mov    -0x8(%rax),%rax  将虛函數表位址前8個位元組拷貝到rax中,即type_info對象的位址

mov    %rax,%rdi   将type_info 對象的位址拷貝到rdi中,準備函數調用

callq  0x400b24    <std::type_info::name()    const>

     在ClassA是非虛函數下反彙編的代碼如下:

C++ RTTI和類型轉換運算符

非虛函數下,表示type_info類執行個體的位址變成一個編譯期确認的全局變量了。    

      綜上可知編譯器在編譯時為每個類都生成了一個記錄其類型資訊相關的全局type_info執行個體,如果是包含虛函數的類,該執行個體的位址儲存在虛函數表索引為-1處,可通過虛函數表擷取類型資訊。可以用代碼模拟上述行為,如下所示:

int main() {
	using std::type_info;
	typedef void (*FUNC)();
	ClassC a;
	ClassA * b = &a;
	long *vp = (long *) (*(long*) &a);
	const type_info & ti = typeid(*b);
	type_info * f = (type_info *) vp[-1];
	cout << "name1->" << ti.name() << "\n";
	cout << "name2->" << f->name() << "\n";
	cout << "is true:" << (f == &ti) << "\n";
	cout << "end\n";

	return 0;
}
           

執行結果如下:

C++ RTTI和類型轉換運算符

參考:C++對象模型之RTTI的實作原理

           C/C++雜記:運作時類型識别(RTTI)與動态類型轉換原理

二、類型轉換運算符

     C中對指針變量的類型轉換沒有限制,這可以讓C輕松擷取或者修改任意資料結構在記憶體中的資料,非常靈活強大,如上一節中用代碼擷取虛函數表中的type_info執行個體,但是這樣也導緻了記憶體安全問題。同時因為代碼編譯層面無法對上述轉換做校驗也導緻了很多潛在的類型轉換Bug,這些bug不一定導緻程式崩潰,其行為是不可預知的。如下示例:

int main() {
	//在ClassA是虛函數下
//	ClassA * a = new ClassC;
	ClassA * a = new ClassA;
	ClassB * b=(ClassB*)a;
	b->sayB();

	return 0;
}
           

上述代碼在理論上應該報錯,基類執行個體無法向下做類型轉換,但是編譯正常,運作結果也正常,如下:

C++ RTTI和類型轉換運算符

sayB方法中未使用類屬性,如果使用了擷取到的類屬性的值就是未知的,有可能是非法記憶體通路而導緻程式崩潰,也可能是其他變量占用的記憶體擷取的值未知。因為類似這種指針變量的強制類型轉換在C中是非常普遍的,也是C強大的特性之一,C++作為C的超集,必須相容這種特性,是以就産生上述問題。

    C++為了在代碼編譯層面對指針類型轉換做校驗,盡可能暴露類型轉換可能導緻的問題,添加了4個類型轉換運算符,分别是dynamic_cast,static_cast,const_cast,reinterpret_cast,dynamic_cast在上一節中已經清楚了,再看另外3個的用法,注意這4個都隻适用于指針或者引用類型變量。

1、static_cast 運算符

      static_cast的用法同dynamic_cast,dynamic_cast是在運作期檢查源類型變量能否轉換成目标類型變量,要求源類型必須提供虛函數,并且轉換完成後調用虛函數時依然使用變量的實際類型的虛函數實作;static_cast是在編譯期檢查源類型變量能否轉換成目标類型變量,要求源類型與目标類型有繼承關系或者是内置的預設類型轉換,如double轉換成int,不要求源類型提供虛函數,如果不符合要求編譯報錯,轉換完成後調用虛函數時使用目标類型的實作,如下示例:

int main() {
	//在ClassA是虛函數下
	ClassA * a = new ClassC;
//	ClassA * a = new ClassA;
	ClassB * b=static_cast<ClassB*>(a);
	b->sayB();
	//編譯不報錯
	int * i=(int *)a;
	//編譯不報錯
//    int * s=static_cast<int *>(a);

	return 0;
}
           

2、const_cast

      const_cast用于去掉原變量的const/volitale限制,注意dynamic_cast/static_cast對const變量做類型轉換時目标變量也必須帶上const,否則編譯報錯,如下示例:

int main() {

	const ClassA * ca=new ClassC;
	//兩處的const都是必須的,否則編譯報錯
	const ClassB * cb=dynamic_cast<const ClassB *>(ca);
	//編譯報錯,轉換成const以後不能調用類執行個體方法,因為這可能修改類屬性,隻能調用類靜态方法
//	cb->sayB();

	const int a = 10;
	//編譯報錯,要求pt必須是const int *
//	int* pt = &a;
	const int* pt = &a;
	//編譯報錯,pt對應的變量不可修改
//	*pt=12;
	int* b = const_cast<int*>(pt);
	*b = 20;
	cout << "b = " << *b << endl;
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "&a = " << &a << endl;

	//編譯報錯,int不是指針或者引用類型
//	int b2 = const_cast<int>(a);
//	b2=13;

	return 0;
}
           

執行結果如下:

C++ RTTI和類型轉換運算符

有趣的是這裡列印a的值并未改變,但是*b的值卻改變了,b也确實指向a,反彙編找答案。列印*b的彙編代碼如下:

C++ RTTI和類型轉換運算符

mov    (%rax),%ebx表示将rax位址指向的記憶體的後4個位元組拷貝到rbx中,即讀取變量a的值。

列印變量a的代碼如下:

C++ RTTI和類型轉換運算符

0xa是十六進制的10,即這裡列印a是将a作為一個字元常量處理了,編譯完成就寫死成10了,而沒有從a的記憶體位址讀取實際的值,這應該是編譯器自己的優化了。為了規避上述優化,将測試代碼調整如下:

int main() {

	cout<<"請輸入一個數:"<<endl;
	int input;
	cin>>input;

	const int a = input;
	const int* pt = &a;
	int* b = const_cast<int*>(pt);
	*b = 20;
	cout << "b = " << *b << endl;
	cout << "a = " << a << endl;
	cout << "b = " << b << endl;
	cout << "&a = " << &a << endl;

	return 0;
}
           

 執行結果如下:

C++ RTTI和類型轉換運算符

這回變量a的值變了,反彙編列印a的代碼如下:

C++ RTTI和類型轉換運算符

 mov    %ebx,%esi就是将變量a的值拷貝到esi中,因為a是在運作期确認的,是以這裡無法将其視為字元常量處理,必須在棧中為其配置設定記憶體。

3、reinterpret_cast

    reinterpret_cast做的校驗非常有限,例如不能将指針變量強轉成4位元組的int變量,不能将指向函數的指針轉換成指向數字的指針,一般情況下跟C中指針強轉的效果一樣,是以需要謹慎使用,如下示例:

#include <iostream>

using std::cout;
using std::endl;

class ClassA {
private:
	int a=1;
public:
	void say(){cout<<"ClassA";};
};


int main() {

	ClassA * a = new ClassA;
	int * b = reinterpret_cast<int *>(a);
    cout<<*b<<endl;

    //跟reinterpret_cast效果等價
    int * c=(int *)a;
    cout<<*c<<endl;

    //報錯損失精度,因為指針是8位元組,int是4位元組
    //	int b2 = reinterpret_cast<int>(a);
    long b2=reinterpret_cast<long>(a);
    cout<<b2<<endl;

    ClassA a2;
    //報錯類型轉換無效
//    int b2 = reinterpret_cast<int>(a2);

	return 0;
}
           

    參考:C++的類型轉換運算符總結

繼續閱讀