1 空的class並非不佔空間
它佔有一個字節的空間,據說這是爲了區別對象,編譯器爲空類默默的安插進了一個char到空對象內。既然這樣,那麼我門就來看一下,這個對像能否被繼承到子對象中呢?
- #include <iostream>
- using namespace std;
- class base
- {
- };
- class derived:public base
- {
- };
- int main()
- {
- cout << sizeof(derived) << endl;
- return 0;
- }
輸出結果 1 根據輸出結果,還是輸出了一個空類的大小,但是若基類中被安插的哪個char能被繼承到派生類中的話,那麼派生類的大小應改爲2纔對啊。由此可見這個char並不能被繼承或是使用,僅僅是作爲一個區別標誌而已。那麼我們再看一下下面這個程序:
- #include <iostream>
- using namespace std;
- class base
- {
- };
- class derived:public base
- {
- private:
- int x;
- base t;
- };
- int main()
- {
- cout << sizeof(derived) << endl;
- return 0;
- }
大家可以猜測一下輸出結果。輸出結果爲8.也許很出乎你的意料吧。其實這裏編譯器還是吧空對象的大小當成1了。然後在派生類中根據字節對齊,結果就爲8了。
2 類中不佔空間的成員
類中並不是所有的成員都佔空間的。哪那些不佔用空間呢?其實當我們知道那些佔用空間,剩下的就是不佔用空間的了。佔用空間的有:非靜態數據成員,虛函數。基本上我就知道這兩個。也就是說靜態數據成員,靜態函數,枚舉,非虛函數,typedef這些東西都是不佔用空間的。我們帶着這些知識來看一個程序:
- #include<iostream>
- using namespace std;
- class DS
- {
- int i;
- short int j;
- union u1{double p; };
- private:
- enum e1{i1}a;
- };
- int main()
- {
- cout<<sizeof(DS)<<endl;
- return 0;
- }
輸出結果爲12 。爲什麼爲12呢?這裏也許很多同學想不通了。。我們一起來看一下:int 4個字節,short int 2 個字節,字節對齊補充爲4個字節,union不佔空間。爲什麼不佔空間呢?union其實也屬於我們上面所說的不佔空間的類成員中的一種。enum不佔空間。那現在才8個字節啊,爲什麼結果是12呢?其實大家仔細看一下就知道了。enum這裏定義了一個對象a。所以它這裏就佔用空間了。若是我們這裏僅僅是enum{//這裏寫多少爲所謂};它不佔用空間。所以問題就出在這個a上。a佔用幾個字節呢?我們都知道enum定義出來的對象本質上就是一個int型。故它佔4個字節,這不12了字節了嗎?
對於上面這個程序,大家可以自己試試,無論你往裏面加多少個非虛函數,多少個靜態成員,多少個類似這樣的enum{//這裏寫多少爲所謂};枚舉和類似 union u1{double p; };這樣的聯合體。類對象的大小都不會增加。那麼他們佔用的空間在那裏呢?輸出的類對象的大小是屬於內存中的那一部分空間呢?這個問題留給大家補充吧,我也不清楚。對於上述說明我們可以看下面程序證明下:
- #include<iostream>
- using namespace std;
- class DS
- {
- public:
- union u1{double p; };
- //enum e1{i1}a;
- static a;
- static b;
- void f(){}
- void g(){}
- };
- int main()
- {
- DS t;
- cout<<sizeof(t)<<endl;
- return 0;
- }
輸出結果爲1.說明此刻編譯器仍把類當做一個空類來處理。噢,我的天啊。不過這樣的類還真是有用處。我們可以試想一下。我們若是想在一個類繼承層次裏面。若是一個基類僅僅是爲派生類提供一個接口,而並提供數據。不就是用的這樣的類嗎?當然我們這裏有靜態成員的數據。我們可以定義一個僅提供接口的類。STL裏好像就使用了好多這樣的所謂的空類。
3 虛繼承
我們來看一下下面這個程序:
- #include<iostream>
- using namespace std;
- class base
- {
- public:
- virtual void f()=0;
- };
- class derived1:public base
- {
- };
- class derived2:virtual public base
- {
- };
- int main()
- {
- cout<<sizeof(derived1)<<endl;
- cout<<sizeof(derived2)<<endl;
- return 0;
- }
輸出結果爲4,8.大喫一驚吧。呵呵。第一個結果應該是正常的,我想大家對這個不會有異議。因爲derived1繼承了基類的虛函數。虛函數是佔4個字節的。故結果爲4.那第二個爲什麼是8呢。也不應該是4嗎?其實這時編譯器好像是往虛函數表裏又增加了一個指針。具體也不是很清楚,求知中。。
4 我們可以爲純虛函數定義
這點我以前確實不知道。今天才在書上看到。原來我們也可以爲純虛函數定義,我們看下面這個程序:
- #include<iostream>
- using namespace std;
- class base
- {
- public:
- virtual void f()=0;
- };
- void base::f()
- {
- cout << "hello world!" << endl;
- }
- class derived1:public base
- {
- void f()
- {
- base::f();
- cout << "derived1::f()" << endl;
- }
- };
- class derived2:virtual public base
- {
- void f()
- {
- base::f();
- cout << "derived2::f()" << endl;
- }
- };
- int main()
- {
- base *t = new derived1();
- t->f();
- base *d = new derived2();
- d->f();
- return 0;
- }
我們爲基類的純虛函數做了定義。然後我們又在兩個派生類進行了重定義,並在定義體裏調用了基類的虛函數。呵呵。。具體結果是什麼自己試試吧。但是若我們不在派生類中進行重定義的話,派生類也會被編譯器默認爲虛類的。即無法進行實例化。對於這樣的用法又有什麼用呢?我們可以設想這樣一種情況,即我們不想讓客戶對我們的基類進行實例化,而我們又像我們的客戶在派生類中重定義自己的函數時引用我們基類中的函數部分。這樣若是我們將我們的基類中的函數定義爲虛函數的話,可以試現我們的後一部分,但可以還是可以實例化,這樣我們就可以用我們這個方法,用戶不可以進行實例化了,又可以通過調用基類的哪個我們定義過的純虛函數來引用基類部分。
5 析構函數也可以被繼承?
我們都知道我們若是若是準備讓一個類作爲基類來用的話,一般都會吧它的析構函數定義爲虛函數的。爲的是多態性,即讓我們用一個基類的指針指向一個派生類對象時。我們用delete刪除這個基類指針時,可以動態的調用派生類的析構函數來析構派生類部分。否則就會產生內存泄露。我們來看下面這個程序:
- #include<iostream>
- using namespace std;
- class base
- {
- public:
- virtual ~base()
- {
- cout << "base::~base()" << endl;
- }
- };
- class derived:public base
- {
- };
- int main()
- {
- base *t = new derived();
- delete t;
- return 0;
- }
結果僅僅調用了基類的析構函數,具體派生類的析構函數是否調用了。我們無法看出來。若我們重定義一下,那肯定就調用了。。呵呵。但是在這種情況下到底調用了嗎?一個謎底。只有寫編譯器,定義C++標準的人才能告訴我們正確的答案,因爲規則使他們定的。但我們根據我們對C++的學習也是可以猜出答案的。
可是我們仔細分析一下你估計就會迷惑了!現在derived對象的大小爲多少。1對吧。也就是編譯器爲我們安插進去的那個char。這個char是屬於base還是derived的呢?不得而知。。暫且不管他吧。。我們看一下析構函數,析構函數是用來幹什麼的?釋放空間。對吧。現在我們的base指針t目前僅僅有一個字節的空間,而我們這裏卻調用了兩次析構函數,這又怎麼解釋。一個字節的空間需要兩次釋放?不不。我們換個方式思考,我們看下面這個程序:
- #include<iostream>
- using namespace std;
- class base
- {
- public:
- ~base()
- {
- cout << "base::~base()" << endl;
- }
- };
- class derived:public base
- {
- public:
- ~derived()
- {
- cout << "derived::~derived()" << endl;
- }
- };
- int main()
- {
- cout << sizeof(derived) << endl;
- derived t;
- return 0;
- }
這個程序中明確輸出了派生類有一個字節的空間。然而當我們的程序結束時卻調用了兩次析構函數。我們都知道當我們一個派生類對象的生命期結束時會調用派生類的析構函數析構派生類部分,然後調用基類的構造函數調用基類部分。然而我們這個派生類中僅有一個字節的空間啊。到底它是怎麼析構的呢。迷。。迷。。。。
我們再看一下我們這個題目,析構函數也可以被繼承?爲什麼這麼問呢?它確實會令人產生聯想。設想多態行爲。是有一個基類對象的指針或引用指向派生類對象,然後調用虛函數產生的。被稱爲執行期行爲,即只有到執行期才能確定掉用的具體對象。然而我們的析構函數的多態行爲,和這個很是類似。這又是怎麼回事?我們的析構函數該是不能被繼承啊。。畢竟他在每個類中的名字都不一樣。然而這樣的多態行爲是怎麼產生的?僅僅是一個規則,還是有章可循的?。。