總覽
一提到面向?qū)ο笏孤担蜁岬矫嫦驅(qū)ο蟮娜齻€特征,封裝,繼承和多態(tài)羡微,雖然這種概括可能顯得比較籠統(tǒng)谷饿,但是都這里還是從這三個角度來進行cpp面向?qū)ο蟮姆治觥?/p>
封裝
所謂的封裝一般指的就是,隱藏自身的一些屬性和方法妈倔,對外暴露出一些抽象的有具體的使用目的的方法博投,這樣做的一個顯著的好處就是在類的屬性需要修改的時候,使用方不需要做相應(yīng)的改動盯蝴。使用方不需要關(guān)注類的內(nèi)部具體的實現(xiàn)細節(jié)毅哗,只需要關(guān)注特定的功能,在一定程度上實現(xiàn)了解耦捧挺,同時提供了很多靈活性虑绵。
多態(tài)
提到多態(tài)呢,顧名思義闽烙,就是說有一樣東西他會有幾種不同的形態(tài)翅睛,具體從c++的角度來說,可以分為編譯時多態(tài)和運行時多.
編譯時多態(tài)一般就指的是函數(shù)的重載黑竞;運行時多態(tài)一般特指父類的指針去指向子類的對象捕发。關(guān)于這部分內(nèi)容可以看看另一篇隨筆:http://www.reibang.com/p/b2fb9943aaf0
繼承
一般說到繼承,離不開以下幾點很魂,繼承方式:公有(public), 私有(private),保護(protected)扎酷,友元函數(shù)。
- 1 公有繼承時遏匆,基類的公用成員和保護成員在派生類中保持原有的訪問屬性法挨,其私有成員仍為基類私有,即在派生類中不能訪問幅聘,在類外也不能訪問坷剧。私有成員體現(xiàn)了數(shù)據(jù)的封裝性,如果基類的私有成員可以被派生類所訪問喊暖,即破壞了基類的封裝性,這就會失去C++的一個重要特性撕瞧。
- 2 保護繼承(protected)的特點是基類的所有公有成員和保護成員都成為派生類的保護成員陵叽,并且只能被它的派生類成員函數(shù)或友元訪問,基類的私有成員仍然是私有的丛版。對派生類而言巩掺,保護成員類似于公有成員,但對于外部而言页畦,保護成員與私有成員類似胖替。
- 3 私有繼承即所有基類成員均變成派生類的私有成員,基類的私有成員仍然不能在派生類中訪問。(基類的保護成員在子類中也會變成私有成員)
小結(jié):公有和保護可以被子類繼承独令,私有不可以端朵。父類成員變量的權(quán)限和子類繼承父類的方式會以一種兩者比較取較低的方式存在于此類中。
對象
從下面的代碼我們演示了一個先有父親的基類燃箭,然后三個子類分別以公有冲呢,私有,保護的方式繼承這個父類招狸,發(fā)生的現(xiàn)象敬拓。
首先講一下繼承類的構(gòu)造方式:首先會調(diào)用父類的構(gòu)造函數(shù),然后去調(diào)用自身的構(gòu)造函數(shù)裙戏;析構(gòu)的時候是相反的乘凸,首先析構(gòu)自身,然后再去析構(gòu)父類對象累榜。
#include <string>
#include <iostream>
using namespace std;
class Father {
private:
string name = "Tom";
int age = 40;
int height = 175;
public:
string gender = "male";
string eyeColor = "blue";
string getName() {
cout << "father's name is " << name << endl;
return name;
}
int getAge () {
cout << "father's age is " << age << endl;
return age;
}
protected:
string address = "Utopia";
};
class SonA : public Father {
};
class SonB : protected Father {
};
class SonC : private Father {
};
cout << "sizeof father is " << sizeof(Father) << endl;
cout << "sizeof sonA is " << sizeof(SonA) << endl;
cout << "sizeof sonB is " << sizeof(SonB) << endl;
cout << "sizeof sonC is " << sizeof(SonC) << endl;
結(jié)果:
sizeof father is 104
sizeof sonA is 104
sizeof sonB is 104
sizeof sonC is 104
我們首先去看一下营勤,以不同方式繼承父類的子類的size會不會不同,實驗發(fā)size都是一樣的信柿,說明不同的繼承方式不影響父類在子類內(nèi)部的存儲冀偶。
下面如果在子類A中重寫父類的一個成員變量,看看會發(fā)生什么渔嚷?
class SonA : public Father {
public:
string gender = "male";
};
sizeof(SonA) = 128
這說明子類新的成員變量并沒有把父類的成員變量覆蓋掉进鸠。同樣,要遵守內(nèi)存對齊形病。
友元
如果從現(xiàn)實的角度去分析父類客年,子類,友元之間的關(guān)系漠吻,友元是你家的生死之交量瓜,雖然跟你家沒有什么血緣關(guān)系,但是他有著你家的鑰匙途乃,跟你爹的關(guān)系可能比你跟你爹關(guān)系還要親绍傲。下面給出一個正式一點的說法:
類對數(shù)據(jù)進行了隱藏和封裝后,類的數(shù)據(jù)成員一般都定義為私有成員耍共,成員函數(shù)一般都定義為公有的烫饼,以此提供類與外界的通訊接口。但是试读,有時需要定義一些函數(shù)杠纵,這些函數(shù)不是類的一部分颅筋,但又需要頻繁地訪問類的數(shù)據(jù)成員歹鱼,這時可以將這些函數(shù)定義為該函數(shù)的友元函數(shù)。除了友元函數(shù)外贡这,還有友元類,兩者統(tǒng)稱為友元银亲。 友元函數(shù)是類外的函數(shù)慢叨,所以它的聲明可以放在類的私有段或公有段且沒有區(qū)別。友元類的所有成員函數(shù)都是另一個類的友元函數(shù)群凶,都可以訪問另一個類中的隱藏信息(包括私有成員和保護成員)插爹。
下面來看一個例子:友元函數(shù)getheight
class Father {
private:
string name = "Tom";
int age = 40;
int height = 175;
friend int getHeight(Father father);
public:
string gender = "male";
string eyeColor = "blue";
string getName() {
cout << "father's name is " << name << endl;
return name;
}
int getAge () {
cout << "father's age is " << age << endl;
return age;
}
protected:
string address = "Utopia";
};
如果在棧上定義會提示成員私有,無法獲取请梢。但是友元可以獲取到類的私有信息:
int getHeight(Father father) {
return father.height;
}
int main()
{
Father father;
cout << getHeight(father);
return 0;
}
結(jié)果:
175
菱形繼承
在多重繼承時可能會導致菱形繼承(鉆石繼承的問題)赠尾。
- 多重繼承的情況下,嚴格按照派生類定義時從左到右的順序來調(diào)用構(gòu)造函數(shù)毅弧,析構(gòu)函數(shù)與之相反气嫁。但是如果基類(基類,父類够坐,超類是指被繼承的類寸宵,派生類,子類是指繼承于基類的類.)中有虛基類的話則構(gòu)造函數(shù)的調(diào)用順序如下:
(1) 虛基類的構(gòu)造函數(shù)在非虛基類的構(gòu)造函數(shù)之前調(diào)用元咙;
(2) 若同一層次中包含多個虛基類梯影,這些虛基類的構(gòu)造函數(shù)按照他們的說明順序調(diào)用;
(3) 若虛基類由非虛基類派生而來庶香,則任然先調(diào)用基類構(gòu)造函數(shù)甲棍,再調(diào)用派生誒,在調(diào)用派生類的構(gòu)造函數(shù)赶掖。 - 鉆石繼承:B繼承A感猛,C繼承A,D多重繼承B,C奢赂,則A會在D中存在兩份拷貝陪白,而且調(diào)用的時候不知道調(diào)用誰,不過可以通過D.B::f來指定調(diào)用膳灶。
解決這個問題的方式是使用虛繼承咱士,虛繼承的原理是通過虛基類表指針指向虛基類表,虛表中記錄了虛基類與本類的偏移地址轧钓;通過偏移地址司致,這樣就找到了虛基類成員,而虛繼承也不用像普通多繼承那樣維持著公共基類(虛基類)的兩份同樣的拷貝聋迎,節(jié)省了存儲空間。
可以通過下面的代碼看一下:
class Father {
private:
string name = "Tom";
int age = 40;
int height = 175;
friend int getHeight(Father father);
public:
Father() {
cout << "construct Father" << endl;
}
string gender = "male";
string eyeColor = "blue";
string getName() {
cout << "father's name is " << name << endl;
return name;
}
int getAge () {
cout << "father's age is " << age << endl;
return age;
}
protected:
string address = "Utopia";
};
class SonA : public Father {
public:
SonA() {
cout << "contruct SonA" << endl;
}
string gender = "male";
};
class SonD : public Father {
public:
SonD() {
cout << "contruct SonB" << endl;
}
};
class Grandson : public SonA , public SonD {
public:
Grandson() {
cout << "construct GrandSon" << endl;
}
};
在構(gòu)造GrandSon時會調(diào)用分別調(diào)用兩次Father的構(gòu)造函數(shù):
int main(void) {
Grandson* grandson = new Grandson;
//grandson->getName();
return 0;
}
結(jié)果:
construct Father
contruct SonA
construct Father
contruct SonB
construct GrandSon
在調(diào)用getName方式時枣耀,編譯器會提示:這就是所謂的鉆石繼承霉晕。也可以顯式的指明調(diào)用的是哪個函數(shù):
int main(void) {
Grandson* grandson = new Grandson;
grandson->SonA::getName();
return 0;
}
結(jié)果:
construct Father
contruct SonA
construct Father
contruct SonB
construct GrandSon
father's name is Tom
但這樣其實還有一個問題庭再,我們只需要一個Father,但是現(xiàn)在卻有了兩個:
sizeof(*grandson) = 232
這種問題可以通過虛繼承來避免:
using namespace std;
class Father {
private:
string name = "Tom";
int age = 40;
int height = 175;
friend int getHeight(Father father);
public:
Father() {
cout << "construct Father" << endl;
}
string gender = "male";
string eyeColor = "blue";
string getName() {
cout << "father's name is " << name << endl;
return name;
}
int getAge () {
cout << "father's age is " << age << endl;
return age;
}
protected:
string address = "Utopia";
};
class SonA : virtual public Father {
public:
SonA() {
cout << "contruct SonA" << endl;
}
string gender = "male";
};
class SonD : virtual public Father {
public:
SonD() {
cout << "contruct SonB" << endl;
}
};
class Grandson : public SonA , public SonD {
public:
Grandson() {
cout << "construct GrandSon" << endl;
}
};
看一下效果:
using namespace std;
int main(void) {
Grandson* grandson = new Grandson;
cout << sizeof(* grandson) << endl;
grandson->getName();
return 0;
}
結(jié)果:
construct Father
contruct SonA
contruct SonB
construct GrandSon
144
father's name is Tom
編譯器不再困惑牺堰,對象占用的空間也少了拄轻。減到了 144.
注意一點,虛繼承要在繼承Father時添加伟葫,如果另GrandSon虛繼承SonA,SonD的話就沒有效果了恨搓,反而會因為引入虛指針增加對象大小。
class Father {
private:
string name = "Tom";
int age = 40;
int height = 175;
friend int getHeight(Father father);
public:
Father() {
cout << "construct Father" << endl;
}
string gender = "male";
string eyeColor = "blue";
string getName() {
cout << "father's name is " << name << endl;
return name;
}
int getAge () {
cout << "father's age is " << age << endl;
return age;
}
protected:
string address = "Utopia";
};
class SonA : public Father {
public:
SonA() {
cout << "contruct SonA" << endl;
}
string gender = "male";
};
class SonD : public Father {
public:
SonD() {
cout << "contruct SonB" << endl;
}
};
class Grandson : virtual public SonA , virtual public SonD {
public:
Grandson() {
cout << "construct GrandSon" << endl;
}
};
看一下效果:
int main(void) {
Grandson* grandson = new Grandson;
cout << "sizeof grandson is " << sizeof(* grandson) << endl;
return 0;
}
結(jié)果:
construct Father
contruct SonA
construct Father
contruct SonB
construct GrandSon
sizeof grandson is 240
可以看到size 反而增加了筏养,分析一下:
從結(jié)果上看:被虛繼承的類斧抱,在發(fā)生多重繼承時,只會被實例化一次渐溶。
在發(fā)生虛繼承時辉浦,子類實例中會有一個虛指針指向虛表,虛表中存著一個地址或者是一個偏移量茎辐,發(fā)生了多重繼承時宪郊,不同的對象指向的父類地址是一個。
虛表的內(nèi)存布局
上面說過拖陆,對象的第一個位置有一個虛指針指向該類的虛表弛槐,在發(fā)生多重繼承時,其實是有多個指針的:
class A{
virtual void play() {
}
};
class B{
virtual void plays() {
}
};
class C: public A, public B {
};
sizeof(C) = 16;
說明有兩個虛指針指向兩個虛表依啰,虛指針并非指向表頭乎串,而是直接指到了虛函數(shù)的位置。虛表的內(nèi)存布局是什么樣的呢:
- 首先是top_offset:非多重繼承時都為0孔飒,在多重繼承時為第二個被繼承的偏移值灌闺。
- 其次是子類的type_info。
- 之后是虛函數(shù)坏瞄。