一、首先我們先來學(xué)習(xí)下:《Effect C++》學(xué)習(xí)------條款05:了解C++默默編寫并調(diào)用哪些函數(shù)蚜印。
如果你在類聲明時(shí)沒有聲明拷貝構(gòu)造函數(shù)莺禁、拷貝類型操作符、析構(gòu)函數(shù)晒哄、構(gòu)造函數(shù)睁宰。編譯器會(huì)自動(dòng)為你提供一份聲明,惟有這些函數(shù)在被需要(調(diào)用時(shí))寝凌,他們才會(huì)被編譯器創(chuàng)造出來柒傻。
默認(rèn)構(gòu)造函數(shù)和析構(gòu)函數(shù)主要是給編譯器一個(gè)地方用來放置“藏身幕后”的代碼,像是調(diào)用基類和非靜態(tài)成員變量的構(gòu)造函數(shù)和析構(gòu)函數(shù)较木;至于拷貝構(gòu)造函數(shù)和拷貝賦值操作符红符,編譯器創(chuàng)建的版本只是單純地將來源對象的每一個(gè)非靜態(tài)成員變量拷貝到目標(biāo)對象。
- 空類伐债,聲明時(shí)編譯器不會(huì)生成任何成員函數(shù): 對于空類预侯,編譯器不會(huì)生成任何的成員函數(shù),只會(huì)生成1個(gè)字節(jié)的占位符峰锁。
有時(shí)可能會(huì)以為編譯器會(huì)為空類生成默認(rèn)構(gòu)造函數(shù)等萎馅,事實(shí)上是不會(huì)的,編譯器只會(huì)在需要的時(shí)候生成6個(gè)成員函數(shù):一個(gè)缺省的構(gòu)造函數(shù)虹蒋、一個(gè)拷貝構(gòu)造函數(shù)糜芳、一個(gè)析構(gòu)函數(shù)、一個(gè)賦值運(yùn)算符魄衅、一對取址運(yùn)算符和一個(gè)this指針峭竣。
class A
{
};
class B
{
/// 聲明一個(gè)虛函數(shù)
virtual bool compare(int a, int b) = 0;
};
class C :public A, public B
{
};
class D :public A, public B
{
/// 聲明一個(gè)虛函數(shù)
virtual bool compare(int a, int b) = 0;
};
class E :virtual A, virtual B
{
};
class F :virtual A, virtual B
{
virtual bool compare(int a, int b) = 0;
};
int main()
{
cout << "A zize:" << sizeof(A) << endl;//1
cout << "B zize:" << sizeof(B) << endl;//4
cout << "C zize:" << sizeof(C) << endl;//4
cout << "D zize:" << sizeof(D) << endl;//4
cout << "E zize:" << sizeof(E) << endl;//8
cout << "F zize:" << sizeof(F) << endl;//8
system("pause");
return 0;
}
分析:
類A是空類,但空類同樣可以被實(shí)例化晃虫,而每個(gè)實(shí)例在內(nèi)存中都有一個(gè)獨(dú)一無二的地址皆撩,為了達(dá)到這個(gè)目的,編譯器往往會(huì)給一個(gè)空類隱含的加一個(gè)字節(jié)哲银,這樣空類在實(shí)例化后在內(nèi)存得到了獨(dú)一無二的地址扛吞,所以sizeof(A)的大小為1。
類B里面因有一個(gè)純虛函數(shù)荆责,故有一個(gè)指向虛函數(shù)的指針(vptr)喻粹,32位系統(tǒng)分配給指針的大小為4個(gè)字節(jié),所以sizeof(B)的大小為4草巡。類C繼承于A和B,編譯器取消A的占位符,保留一虛函數(shù)表山憨,故大小為4查乒。類D繼承于A和B,派生類基類共享一個(gè)虛表,故大小為4。類E虛繼承A和B,含有一個(gè)指向基類的指針(vftr)和一個(gè)指向虛函數(shù)的指針郁竟。類F虛繼承A和B,含有一個(gè)指向基類的指針(vftr)和一個(gè)指向虛函數(shù)的指針玛迄。
2、空類棚亩,定義時(shí)會(huì)生成6個(gè)成員函數(shù)
class Empty
{
};
等價(jià)于
class Empty
{
public:
Empty(); // 缺省構(gòu)造函數(shù)
Empty(const Empty &rhs); // 拷貝構(gòu)造函數(shù)
~Empty(); // 析構(gòu)函數(shù)
Empty& operator=(const Empty &rhs); //賦值運(yùn)算符
Empty* operator&(); // 取址運(yùn)算符
const Empty* operator&()const; //取址運(yùn)算符(const版本)
};
使用時(shí)的情況
Empty *e = new Empty();// 缺省構(gòu)造函數(shù)蓖议;
delete e; // 析構(gòu)函數(shù)
Empty e1; // 缺省構(gòu)造函數(shù);
Empty e2(e1); // 拷貝構(gòu)造函數(shù)
Empty e3 = e2; // 賦值運(yùn)算符
Empty *pe1 = &e1; // 取址運(yùn)算符
const Empty *pe2 = &e2; //取址運(yùn)算符(const)
編譯器對這些函數(shù)的實(shí)現(xiàn)如下:
inline Empty::Empty() //缺省構(gòu)造函數(shù)
{
}
inline Empty::~Empty() //析構(gòu)函數(shù)
{
}
inline Empty *Empty::operator&() //取址運(yùn)算符(非const)
{
return this;
}
inline const Empty *Empty::operator&() const //取址運(yùn)算符(const)
{
return this;
}
inline Empty::Empty(const Empty &rhs) //拷貝構(gòu)造函數(shù)
{
//對類的非靜態(tài)數(shù)據(jù)成員進(jìn)行以"成員為單位"逐一拷貝構(gòu)造
//固定類型的對象拷貝構(gòu)造是從源對象到目標(biāo)對象的"逐位"拷貝
}
inline Empty& Empty::operator=(const Empty &rhs) //賦值運(yùn)算符
{
//對類的非靜態(tài)數(shù)據(jù)成員進(jìn)行以"成員為單位"逐一賦值 //固定類型的對象賦值是從源對象到目標(biāo)對象的"逐位"賦值讥蟆。
}
二勒虾、類大小計(jì)算
1、空類大腥惩:我們都知道空類大小為1修然,為什么呢?C++標(biāo)準(zhǔn)指出质况,不允許一個(gè)對象(當(dāng)然包括類對象)的大小為0愕宋,不同的對象不能具有相同的地址。這是由于:new需要分配不同的內(nèi)存地址结榄,不能分配內(nèi)存大小為0的空間避免除以 sizeof(T)時(shí)得到除以0錯(cuò)誤中贝,故使用一個(gè)字節(jié)來區(qū)分空類。
有兩種情況需要我們注意下:
A臼朗、第一種情況邻寿,涉及到空類的繼承。 當(dāng)派生類繼承空類后依溯,派生類如果有自己的數(shù)據(jù)成員老厌,而空基類的一個(gè)字節(jié)并不會(huì)加到派生類中去。如下:sizeof(DerEmpty)為4黎炉。
class Empty{};
class DerEmpty:public Empty
{
public:
int a;
}
B枝秤、第二中情況,一個(gè)類包含一個(gè)空類對象數(shù)據(jù)成員慷嗜。
class Empty {};
class HoldsAnInt {
int x;
Empty e;
};
sizeof(HoldsAnInt)為8淀弹。 因?yàn)樵谶@種情況下,空類的1字節(jié)是會(huì)被計(jì)算進(jìn)去的庆械。而又由于字節(jié)對齊的原則薇溃,所以結(jié)果為4+4=8。繼承空類的派生類缭乘,如果派生類也為空類沐序,大小也都為1。
2、含有虛函數(shù)成員:
在上文中策幼,我們也簡要分析了含有虛函數(shù)時(shí)類的大小邑时,這里舉例子詳細(xì)說明。
首先特姐,要介紹一下虛函數(shù)的工作原理:虛函數(shù)(Virtual Function)是通過一張?zhí)摵瘮?shù)表(Virtual Table)來實(shí)現(xiàn)的晶丘。編譯器必須要保證虛函數(shù)表的指針存放于對象實(shí)例中最前面的位置(這是為了確保正確取到虛函數(shù)的偏移量)。
每當(dāng)創(chuàng)建一個(gè)包含有虛函數(shù)的類或從包含有虛函數(shù)的類派生一個(gè)類時(shí)唐含,編譯器就會(huì)為這個(gè)類創(chuàng)建一個(gè)虛函數(shù)表(VTABLE)保存該類所有虛函數(shù)的地址浅浮,其實(shí)這個(gè)VTABLE的作用就是保存自己類中所有虛函數(shù)的地址,可以把VTABLE形象地看成一個(gè)函數(shù)指針數(shù)組捷枯,這個(gè)數(shù)組的每個(gè)元素存放的就是虛函數(shù)的地址滚秩。在每個(gè)帶有虛函數(shù)的類 中,編譯器秘密地置入一指針铜靶,稱為v p o i n t e r(縮寫為V P T R)叔遂,指向這個(gè)對象的V TA B L E。 當(dāng)構(gòu)造該派生類對象時(shí)争剿,其成員VPTR被初始化指向該派生類的VTABLE已艰。所以可以認(rèn)為VTABLE是該類的所有對象共有的,在定義該類時(shí)被初始化蚕苇;而VPTR則是每個(gè)類對象都有獨(dú)立一份的哩掺,且在該類對象被構(gòu)造時(shí)被初始化。
假設(shè)我們有這樣的一個(gè)類:
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
當(dāng)我們定義一個(gè)這個(gè)類的實(shí)例涩笤,Base b時(shí)嚼吞,其b中成員的存放如下:
指向虛函數(shù)表的指針在對象b的最前面。虛函數(shù)表的最后多加了一個(gè)結(jié)點(diǎn)蹬碧,這是虛函數(shù)表的結(jié)束結(jié)點(diǎn)舱禽,就像字符串的結(jié)束符”\0”一樣,其標(biāo)志了虛函數(shù)表的結(jié)束恩沽。這個(gè)結(jié)束標(biāo)志的值在不同的編譯器下是不同的誊稚。在vs下,這個(gè)值是NULL罗心。而在linux下里伯,這個(gè)值是如果1,表示還有下一個(gè)虛函數(shù)表渤闷,如果值是0疾瓮,表示是最后一個(gè)虛函數(shù)表。 因?yàn)閷ο骲中多了一個(gè)指向虛函數(shù)表的指針飒箭,而指針的sizeof是4狼电,因此含有虛函數(shù)的類或?qū)嵗詈蟮膕izeof是實(shí)際的數(shù)據(jù)成員的sizeof加4蜒灰。sizeof(Base)為4。
下面將討論針對基類含有虛函數(shù)的繼承討論:
(1)在派生類中不對基類的虛函數(shù)進(jìn)行覆蓋肩碟,同時(shí)派生類中還擁有自己的虛函數(shù)卷员,比如有如下的派生類:
class Derived: public Base
{
public:
virtual void f1() { cout << "Derived::f1" << endl; }
virtual void g1() { cout << "Derived::g1" << endl; }
virtual void h1() { cout << "Derived::h1" << endl; }
};
基類和派生類的關(guān)系如下:
當(dāng)定義一個(gè)Derived的對象d后,其成員的存放如下:
可以發(fā)現(xiàn):
1)虛函數(shù)按照其聲明順序放于表中腾务。
2)基類的虛函數(shù)在派生類的虛函數(shù)前面。
此時(shí)基類和派生類的sizeof都是數(shù)據(jù)成員的大小+指針的大小4削饵。
(2)在派生類中對基類的虛函數(shù)進(jìn)行覆蓋岩瘦,假設(shè)有如下的派生類:
class Derived: public Base
{
public:
virtual void f() { cout << "Derived::f" << endl; }
virtual void g1() { cout << "Derived::g1" << endl; }
virtual void h1() { cout << "Derived::h1" << endl; }
};
基類和派生類之間的關(guān)系:其中基類的虛函數(shù)f在派生類中被覆蓋了
當(dāng)我們定義一個(gè)派生類對象d后,其d的成員存放為:
可以發(fā)現(xiàn):
1)覆蓋的f()函數(shù)被放到了虛表中原來基類虛函數(shù)的位置窿撬。
2)沒有被覆蓋的函數(shù)依舊启昧。_
派生類的大小仍是基類和派生類的非靜態(tài)數(shù)據(jù)成員的大小+一個(gè)vptr指針的大小,這樣劈伴,我們就可以看到對于下面這樣的程序:
Base *b = new Derive();
b->f();
由b所指的內(nèi)存中的虛函數(shù)表的f()的位置已經(jīng)被Derive::f()函數(shù)地址所取代密末,于是在實(shí)際調(diào)用發(fā)生時(shí),是Derive::f()被調(diào)用了跛璧。這就實(shí)現(xiàn)了多態(tài)严里。
(3)多繼承:無虛函數(shù)覆蓋
假設(shè)基類和派生類之間有如下關(guān)系:
對于派生類實(shí)例中的虛函數(shù)表,是下面這個(gè)樣子:
我們可以看到:
1) 每個(gè)基類都有自己的虛表追城。
2) 派生類的成員函數(shù)被放到了第一個(gè)基類的表中刹碾。(所謂的第一個(gè)基類是按照聲明順序來判斷的)
由于每個(gè)基類都需要一個(gè)指針來指向其虛函數(shù)表,因此d的sizeof等于d的數(shù)據(jù)成員加上三個(gè)指針的大小座柱。
(4)多重繼承迷帜,含虛函數(shù)覆蓋
假設(shè),基類和派生類又如下關(guān)系:派生類中覆蓋了基類的虛函數(shù)f
下面是對于派生類實(shí)例中的虛函數(shù)表的圖:
我們可以看見色洞,三個(gè)基類虛函數(shù)表中的f()的位置被替換成了派生類的函數(shù)指針戏锹。這樣,我們就可以任一靜態(tài)類型的基類類來指向派生類火诸,并調(diào)用派生類的f()了锦针。如:
Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
此情況派生類的大小也是類的所有非靜態(tài)數(shù)據(jù)成員的大小+三個(gè)指針的大小。
class A
{
};
class B
{
char ch;
virtual void func0() { }
};
class C
{
char ch1;
char ch2;
virtual void func() { }
virtual void func1() { }
};
class D: public A, public C
{
int d;
virtual void func() { }
virtual void func1() { }
};
class E: public B, public C
{
int e;
virtual void func0() { }
virtual void func1() { }
};
int main(void)
{
cout << "A=" << sizeof(A) << endl; //result=1
cout << "B=" << sizeof(B) << endl; //result=8
cout << "C=" << sizeof(C) << endl; //result=8
cout << "D=" << sizeof(D) << endl; //result=12
cout << "E=" << sizeof(E) << endl; //result=20
return 0;
}
結(jié)果分析:
1.A為空類惭蹂,所以大小為1
2.B的大小為char數(shù)據(jù)成員大小+vptr指針大小伞插。由于字節(jié)對齊,大小為4+4=8
三、虛函數(shù)表的打印
class A1
{
public:
A1(int _a1 = 1) : a1(_a1) { }
virtual void f() { cout << "A1::f" << endl; }
virtual void g() { cout << "A1::g" << endl; }
virtual void h() { cout << "A1::h" << endl; }
~A1() {}
private:
int a1;
};
class C : public A1
{
public:
C(int _a1 = 1, int _c = 4) :A1(_a1), c(_c) { }
virtual void f() { cout << "C::f" << endl; }
virtual void g() { cout << "C::g" << endl; }
virtual void h() { cout << "C::h" << endl; }
private:
int c;
};
如果類C中重寫了A類中的函數(shù),那么就會(huì)覆蓋A類的虛函數(shù)盾碗,重寫一部分就會(huì)覆蓋一部分媚污,重寫全部就會(huì)覆蓋全部。如果C中重新寫了一些別的虛函數(shù)廷雅,那么這些虛函數(shù)將排在父類的后面耗美,這里編譯器無法顯示京髓,可以通過打印虛表來進(jìn)行。打印的過程比較簡單商架,通過訪問類C的前8字節(jié)(64位編譯器)找到虛函數(shù)表堰怨,再一次遍歷虛函數(shù)表即可。虛函數(shù)表最后一項(xiàng)用的是0蛇摸,代表虛函數(shù)表結(jié)束备图。
class Base
{
public:
int base_data;
Base() { base_data = 1; }
virtual void func1() { cout << "base_func1" << endl; }
virtual void func2() { cout << "base_func2" << endl; }
virtual void func3() { cout << "base_func3" << endl; }
};
class Derive : public Base
{
public:
int derive_data;
Derive() { derive_data = 2; }
virtual void func1() { cout << "derive_func1" << endl; }
virtual void func2() { cout << "derive_func2" << endl; }
};
typedef void(*func)();
int test()
{
Base base;
cout << "&base: " << &base << endl;
cout << "&base.base_data: " << &base.base_data << endl;
cout << "----------------------------------------" << endl;
Derive derive;
cout << "&derive: " << &derive << endl;
cout << "&derive.base_data: " << &derive.base_data << endl;
cout << "&derive.derive_data: " << &derive.derive_data << endl;
cout << "----------------------------------------" << endl;
for (int i = 0; i<3; i++)
{
// &base : base首地址
// (unsigned long*)&base : base的首地址,vptr的地址
// (*(unsigned long*)&base) : vptr的內(nèi)容赶袄,即vtable的地址揽涮,指向第一個(gè)虛函數(shù)的slot的地址
// (unsigned long*)(*(unsigned long*)&base) : vtable的地址,指向第一個(gè)虛函數(shù)的slot的地址
// vtbl : 指向虛函數(shù)slot的地址
// *vtbl : 虛函數(shù)的地址
unsigned long* vtbl = (unsigned long*)(*(unsigned long*)&base) + i;
cout << "slot address: " << vtbl << endl;
cout << "func address: " << *vtbl << endl;
func pfunc = (func)*(vtbl);
pfunc();
}
cout << "----------------------------------------" << endl;
for(int i=0; i<3; i++)
{
unsigned long* vtbl = (unsigned long*)(*(unsigned long*)&derive) + i;
cout << "slot address: " << vtbl << endl;
cout << "func address: " << *vtbl << endl;
func pfunc = (func)*(vtbl); pfunc();
}
cout << "----------------------------------------" << endl;
return 1;
}
四饿肺、類繼承與C++多態(tài)
1蒋困、類繼承:C++是一種面向?qū)ο蟮恼Z言,最重要的一個(gè)目的就是——提供可重用的代碼敬辣,而類繼承就是C++提供來擴(kuò)展和修改類的方法雪标。類繼承就是從已有的類中派生出新的類,派生類繼承了基類的特性溉跃,同時(shí)可以添加自己的特性村刨。實(shí)際上,類與類之間的關(guān)系分為三種:代理喊积、組合和繼承烹困。
以下是三種關(guān)系的圖解:(為了更好的理解)
基類可以派生出派生類,基類也叫做“父類”乾吻,派生類也稱為“子類”髓梅。 那么,派生類從基類中繼承了哪些東西呢绎签?分為兩個(gè)方面:
1). 變量——派生類繼承了基類中所有的成員變量枯饿,并從基類中繼承了基類作用域,即使子類中的變量和父類中的同名诡必,有了作用域奢方,兩者也不沖突。
2).方法——派生類繼承了基類中除去構(gòu)造函數(shù)爸舒、析構(gòu)函數(shù)以外的所有方法蟋字。
2、繼承方式和訪問限定符
繼承方式有三種——public扭勉、protected和private鹊奖,不同的繼承方式對繼承到派生類中的基類成員有什么影響?見下圖:
總的來說涂炎,父類成員的訪問限定符通過繼承派生到子類中之后忠聚,訪問限定符的權(quán)限小于设哗、等于原權(quán)限。其中两蟀,父類中的private成員只有父類本身及其友元可以訪問网梢,通過其他方式都不能進(jìn)行訪問,當(dāng)然就包括繼承赂毯。protected多用于繼承當(dāng)中战虏,如果對父類成員的要求是——子類可訪問而外部不可訪問,則可以選擇protected繼承方式党涕。
3活烙、派生類對象的構(gòu)造方式
前面也提到,派生類將基類中除去構(gòu)造函數(shù)和析構(gòu)函數(shù)的其他方法繼承了過來遣鼓,那么對于派生類對象中自己的成員變量和來自基類的成員變量,它們的構(gòu)造方式是怎樣的呢重贺?
答案是:
1).先調(diào)用基類構(gòu)造函數(shù)骑祟,構(gòu)造基類部分成員變量,再調(diào)用派生類構(gòu)造函數(shù)構(gòu)造派生類部分的成員變量气笙。
2).基類部分成員的初始化方式在派生類構(gòu)造函數(shù)的初始化列表中指定次企。
3).若基類中還有成員對象,則先調(diào)用成員對象的構(gòu)造函數(shù)潜圃,再調(diào)用基類構(gòu)造函數(shù)缸棵,最后是派生類構(gòu)造函數(shù)。析構(gòu)順序和構(gòu)造順序相反谭期。見下:
class Test
{
public: Test()
{
cout<<"Test::Test()"<<endl;
}
private:
int mc;
};
class Base
{
public:
Base(int a)
{
ma = a; cout<<"Base::base()"<<endl;
}
~Base()
{
cout<<"Base::~base()"<<endl;
}
private:
int ma;
Test t;
};
class Derive : public Base
{
public:
Derive(int b) :Base(b)
{
mb = b;
cout << "Derive::derive()" << endl;
}
~Derive()
{
cout << "Derive::~derive()" << endl;
}
private:
int mb;
};
結(jié)果如下:
4堵第、基類和派生類中同名成員的關(guān)系
派生類從基類中繼承過來的成員(函數(shù)、變量)可能和派生類部分成員(函數(shù)隧出、變量)重名踏志。
1).前面提到,派生類從基類中繼承了基類作用域胀瞪,所以同成員名變量可以靠作用域區(qū)分開(隱藏)针余。
2).同名成員函數(shù)則有三種關(guān)系:重載、隱藏和覆蓋凄诞。
(1)重載overload
函數(shù)重載有三個(gè)條件圆雁,一函數(shù)名相同,二形參類型帆谍、個(gè)數(shù)伪朽、順序不同,三相同作用域既忆。根據(jù)第三個(gè)條件驱负,可知函數(shù)重載只可能發(fā)生在一個(gè)類中嗦玖,見下:
終于進(jìn)入正題,我們先來看下C++官方對于多態(tài)的描述:多態(tài)是指通過基類的指針或者引用跃脊,在運(yùn)行時(shí)動(dòng)態(tài)調(diào)用實(shí)際綁定對象函數(shù)的行為宇挫。與之相對應(yīng)的編譯時(shí)綁定函數(shù)稱為靜態(tài)綁定。多態(tài)是面向?qū)ο缶幊痰暮诵乃枷胫焕沂酰虼宋覀冇斜匾钊胩剿饕幌滤膶?shí)現(xiàn)原理器瘪。理解了原理才能更好的使用。
C++按照實(shí)現(xiàn)的時(shí)機(jī)分為編譯時(shí)多態(tài)和運(yùn)行時(shí)多態(tài)
1.編譯時(shí)多態(tài)也成為靜態(tài)連編绘雁,是指程序在編譯的時(shí)候就確定了多態(tài)性橡疼,通過重載機(jī)制實(shí)現(xiàn)
2運(yùn)行時(shí)多態(tài)又稱為動(dòng)態(tài)聯(lián)編,是指必須在運(yùn)行中才能確定的多態(tài)性庐舟,通過繼承和虛函數(shù)實(shí)現(xiàn)