C++對象模型復習
本文寫於2017-02-24,從老賬號遷移到本賬號,原文地址:https://i.cnblogs.com/EditPosts.aspx?postid=6440685
一:對象模型
C++面向對象的實現,相對於C耗費成本是由virutal引起的。包括
- virtual function機制,用來支持執行期綁定。
- virutal base class 虛基類機制,以實現共享虛基類的subobject
此外還有多重繼承下,發生在其第二或後繼派生類之間的轉換。
C++對象模型,所有非靜態數據成員存儲在對象本身中,所有的靜態數據成員,成員函數(包括靜態與非靜態)都置於對象之外。虛函數的支持是在每個class內部維護一個vptr指向vtbl,vtbl存放所有虛函數指針。vptr的設定和重置都由每一個class的構造函數/析構函數/拷貝分配函數自動完成。每一個typeinfo也放在vtbl之中(用來支持RTTI),vtbl之中還有virtual base subobject的offset值。
主要有三種方法支持多態:(1)基類使用引用或指針接收派生類。(2)可調用虛函數觸發多態。(3)經由dynamic_cast和typeid運算符。
當一個基類對象被直接初始化為一個派生類對象時,派生類對象就會被切割,僅剩基類類型大小的內存。
總結:
總而言之,多態是一種威力強大的設計機制,允許通過將派生類的指針賦給基類指針,讓基類可以根據賦值給它的子類的特性以不同的方式運作。需要付出的代價就是額外的間接性,包括虛表和RTTI兩方面。
二:構造函數語意學
四種情況下編譯器自動合成trival構造函數:
1.包含帶有默認構造函數的對象成員的類 2. 繼承自帶有默認構造函數的基類的類 3.帶有虛函數的類 4.帶有一個虛基類的類
拷貝構造函數同理,否則執行默認位逐次拷貝。
NRV優化不必說。
成員初始化列表必須使用的情況:1.有const成員 2.有引用類型成員 3.有沒有默認構造函數的成員對象 4.基類對象沒有默認構造函數
三:Data語意學
影響C++類的大小的三個因素:
- 支持特殊功能所帶來的額外負擔(對各種virtual的支持)
- 編譯器對特殊情況的優化處理(空class)
- 內存對齊
一般對象的布局順序為:vptr、基類成員(按聲明順序)、自身數據成員、虛基類數據成員(按聲明順序)
四:Function語意學
C++支持三種類型成員函數,分別是static、nonstatic、virtual。
非靜態成員函數:和非成員函數由相同的效率,因為編譯器內部會將其轉換為非靜態成員函數,加上this指針作為額外參數。此外還要額外對函數的名稱進行處理。
虛擬成員函數p->function轉換為(*p->vptr[1])(p),引入虛表下表。
靜態成員函數實際上相當於全局函數。不能夠存取類中非靜態成員,不能調用非靜態成員函數。不能夠聲明為const、voliatle或virtual。它不需要經由對象調用,當然,通過對象調用也被允許。
消極多態與積極多態:
Point ptr = new Point3d //Point3d繼承自Point
在這種情況下,多態可以在編譯期完成(虛基類情況除外),因此被稱作消極多態(沒有進行虛函數的調用)。相對於消極多態,則有積極多態--指向的對象類型需要在執行期才能決定。積極多態的例子如虛函數和RTTI:
//例1,虛函數的調用
ptr->z();
//例2,RTTI的應用
if(Point3d *p = dynamic_cast<Point3d*>(ptr))
return p->z();
虛基類子對象為什麽是運行時確定的,而不是編譯器? 因為虛基類的構造函數在繼承體系中被編譯器壓制到了最派生類才構造,所以集成體系中間的類不能再編譯器確定虛基類子對象的位置。因為它下面可能還有多層繼承。所以要引入一個間接性,為虛繼承體系中每個class引入一個虛基類子對象offset,在運行時填充該offset。
在多重繼承中支持虛函數,其復雜度圍繞在第二個或後繼基類身上,以及“必須調整this指針這一點”。多重繼承中調整this指針(註意沒說虛函數):比如對於base *pbase = new derived(這裏沒有多態),新的derived對象以指向base subobject。如果沒有這樣的調整,指針的任何非多態運用都將失敗,向base->data_base,存取操作會失敗!或者delete base,這個操作執行時又必須把this指針指到頭部,因為要析構的大小是derived對象大小。這些操作必須在執行期完成,也就是this指針跳躍的這些offset必須指到,其實會存於vtbl之中。如果base是派生類的最基類,那麽this無需調整,因為他們起始位置是一樣的。
有三種情況,第二或後繼的基類會影響對虛函數的支持:
- 通過一個指向第二基類的指針,調用派生類虛函數(指針必須後移)。
- 通過一個派生類的指針,調用第二個基類中一個繼承而來的virtual function,此情形派生類指針必須再次調整,以指向第二個基類子對象。
- 第三種情況發生於一個語言擴充性質之下,允許一個虛函數返回值類型發生變化,可能是基類類型,或者派生類類型。比如base* pb=pb->clone(),該函數返回值為derived*類型,所以此處必須調整offset。
五
六
七
1.為什麽catch字句的異常聲明通常被聲明為引用?
- 可以避免由異常對象到catch字句中的對象的拷貝,特別是對象比較大時。
- 能確保catch字句對異常對象的修改能再次拋出
能確保正確地調用與異常類型相關聯的虛擬函數,避免對象切割
異常對象的聲明周期?
- 產生:throw className()時產生
- 銷毀:該異常最後一個catch字句退出時銷毀
註意:因為一場可能在catch子句中重新被拋出,所以在到達最後一個處理該異常的catch子句之前,異常對象是不能銷毀的。
RTTI:
- RTTI只支持多態類,也就是說沒有定義虛函數的類是不能進行RTTI的。
- 對指針的dynamic_cast失敗時會返回NULL,對引用的話,失敗會拋出bad_cast_exception.
- typeid可以放回const type_info&,用以獲取類型信息。
關於1是因為RTTI的實現時通過vptr來獲取存儲在虛函數表中的type_info*,事實上為非多態類提供RTTI,也沒有多大意義。2的原因在於指針可以被賦值為0,以表示no object,但是引用不行。關於3,UI然第一點指出RTTI只支持多肽類,但typeid和typeinfo同樣可適用於內建類型及所有非多態類。與多態類的差別在於,非多態類的type_info對象是靜態取得,**所以不能叫“執行期類型識”。
dynamic_cast主要用於類層次之間的上行裝換和下行轉換,還可以用於類之間的交叉轉換。上行轉換時,static_cast是安全的。在進行下行轉換時,dynamic_cast有類型檢查的功能,比static_cast更安全:
#include <iostream>
#include <assert.h>
class base {
public:
virtual void fun() { std::cout<<"base"<<std::endl; } //去掉virtual dynamic_cast會編譯失敗
};
class derived : public base {
public:
char* str[100];
};
void test(base* b)
{
derived* d1 = static_cast<derived*>(b);
//d1->str[1] = "hello"; //使用static_cast不會類型檢查,如果b指針真得是derived*類型此處就沒問題,如果b實際上就是base類型,此處段錯誤
derived* d2 = dynamic_cast<derived*>(b);
assert(d2 == NULL); //使用dynamic_cast,如果b只是base*類型,上一句強轉稱為derived*類型會失敗,d2會返回NULL。
}
int main()
{
base *b;
test(b);
return 0;
}
另外,dynamic_cast還支持交叉轉化(cross cast),也就是繼承體系中統一層級的對象之間的轉換。而使用static_cast這種無關連的類的轉換會編譯不通過。
#include <iostream>
class A
{
public:
int m_iNum;
virtual void f(){}
};
class B:public A
{
};
class D:public A
{
};
void foo()
{
B* pb = new B;
pb->m_iNum = 100;
//D* pd1 = static_cast<D*>(pb); //compile error
//D* pd2 = dynamic_cast<D*>(pb);//pd2isNULL
delete pb;
}
int main()
{
foo();
}
C++對象模型復習