一、繼承
1. 繼承概念
繼承機制是面向?qū)ο蟪绦蛟O計使代碼可以復用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產(chǎn)生新的類,稱派生類/子類。繼承呈現(xiàn)了面向?qū)ο蟪绦蛟O計的層次結(jié)構(gòu),體現(xiàn)了由簡單到復雜的認知過程。以前我們接觸的復用都是函數(shù)復用,繼承是類設計層次的復用。
我們先簡單看一下繼承的使用,如以下代碼:
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "Mike";
int _age = 18;
};
// 繼承 Person 父類
class Student : public Person
{
protected:
string s_id;
};
// 繼承 Person 父類
class Tercher : public Person
{
protected:
string t_id;
};
繼承后,父類的 Person 的成員(成員函數(shù)+成員變量)都會變成子類的一部分。這里體現(xiàn)出了 Student 和 Teacher 復用了 Person 的成員。下面我們使用監(jiān)視窗口查看 Student 和 Teacher 對象,可以看到變量的復用;調(diào)用 Print 可以看到成員函數(shù)的復用;如下代碼:
int main()
{
Person p;
Student s;
Tercher t;
p.Print();
s.Print();
t.Print();
return 0;
}
我們先觀察監(jiān)視窗口:
我們可以看到,s 和 t 都繼承了父類的成員變量;再嘗試調(diào)用父類的成員函數(shù):
2. 繼承定義
(1)繼承的格式定義
繼承的定義格式如下,Person 是父類/基類,Student 是子類/派生類,public 為繼承方式:
(2)繼承父類成員訪問方式的變化
繼承父類成員訪問方式的變化如下表:
其中以 public 繼承方式我們稱父類和子類是一種 is-a 關系,也就是“我是一個你”。
總結(jié):
- 父類 private 成員在子類中無論以什么方式繼承都是不可見的。這里的不可見是指父類的私有成員還是被繼承到了派生類對象中,但是語法上限制子類對象不管在類里面還是類外面都不能去訪問它。
- 父類 private 成員在子類中是不能被訪問,如果父類成員不想在類外直接被訪問,但需要在子類中能訪問,就定義為 protected??梢钥闯霰Wo成員限定符是因繼承才出現(xiàn)的。
- 父類的私有成員在子類都是不可見。父類的其他成員在子類的訪問方式 == min(成員在基類的訪問限定符,繼承方式),其中比較規(guī)則:public > protected > private.
- 使用關鍵字 class 時默認的繼承方式是 private,使用 struct 時默認的繼承方式是 public,不過最好顯示的寫出繼承方式。
- 在實際運用中一般使用都是 public 繼承,幾乎很少使用 protetced/private 繼承,也不提倡使用 protetced/private 繼承,因為 protetced/private 繼承下來的成員都只能在派生類的類里面使用,實際中擴展維護性不強。
下面演示 public 繼承關系下父類成員的各類型成員訪問關系的變化 :
// Print() 函數(shù)在父類中是 pubilc 成員
class Person
{
public:
void Print()
{
cout << _name << endl;
}
protected:
string _name = "Mike"; // 姓名
};
// public 繼承父類
class Student : public Person
{
public:
Student()
:_stuid(000)
{
Print();
}
protected:
int _stuid; // 學號
};
int main()
{
Student s;
s.Print();
return 0;
}
如上,Print() 函數(shù)在父類中是 pubilc 成員,子類繼承方式也是 public,所以此時 Print() 在類內(nèi)類外都可訪問,如下:
當我們將 Print() 的訪問限定符改為 protected 后,如下:
// Print() 函數(shù)在父類中是 protected 成員
class Person
{
protected:
void Print()
{
cout << _name << endl;
}
protected:
string _name = "Mike"; // 姓名
};
那么此時 Print() 函數(shù)只能在子類中使用,在類外不可使用:
3. 父類和子類對象賦值轉(zhuǎn)換
- 子類對象可以賦值給父類的對象 / 父類的指針 / 父類的引用。這里有個形象的說法叫切片或者切割。寓意把子類中父類那部分切來賦值過去,如下圖所示:
- 父類對象不能賦值給子類對象,因為子類就是繼承父類下來的,父類有的子類也有。
如下示例:
class Person
{
protected:
string _name;
int _age;
};
class Student : public Person
{
public:
string s_id;
};
int main()
{
Student s;
// 1.子類對象可以賦值給父類對象/指針/引用
Person p = s;
Person* pp = &s;
Person& rp = s;
// 2.基類的指針可以通過強制類型轉(zhuǎn)換賦值給派生類的指針
pp = &s;
Student* ps1 = (Student*)pp; // 這種情況轉(zhuǎn)換時可以
ps1->s_id = 7;
pp = &p;
Student* ps2 = (Student*)pp; // 這種情況轉(zhuǎn)換時雖然可以,但是會存在越界訪問
ps2->s_id = 3;
return 0;
}
4. 繼承中的作用域
- 在繼承體系中父類和子類都有獨立的作用域。
-
父類和子類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。(在子類成員函數(shù)中,可以使用
父類::基類成員
顯示訪問) - 需要注意的是如果是成員函數(shù)的隱藏,只需要函數(shù)名相同就構(gòu)成隱藏。
- 注意在實際中在繼承體系里面最好不要定義同名的成員。
例如以下示例:
class A
{
public:
void fun()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
A::fun(); // 可顯示調(diào)用 A 類的 fun 函數(shù)
cout << "func(int i)->" << i << endl;
}
};
int main()
{
B b;
b.fun(10); // 默認調(diào)的是 B 類的 fun 函數(shù)
b.A::fun(); // 可顯示調(diào)用 A 類的 fun 函數(shù)
return 0;
}
B 中的 fun 和 A 中的 fun 不是構(gòu)成重載,因為不是在同一作用域,而是構(gòu)成隱藏,成員函數(shù)滿足函數(shù)名相同就構(gòu)成隱藏。
5. 子類的默認成員函數(shù)
6個默認成員函數(shù),“默認”的意思就是指我們不寫,編譯器會變我們自動生成一個,那么在子類中,這幾個成員函數(shù)是如何生成的呢?
-
子類的構(gòu)造函數(shù)必須調(diào)用父類的構(gòu)造函數(shù)初始化父類的那一部分成員。如果父類沒有默認的構(gòu)造函數(shù),則必須在子類構(gòu)造函數(shù)的初始化列表階段顯示調(diào)用。
-
子類的拷貝構(gòu)造函數(shù)必須調(diào)用父類的拷貝構(gòu)造完成父類的拷貝初始化。
-
子類的 operator= 必須要調(diào)用父類的 operator= 完成基類的復制。
-
子類的析構(gòu)函數(shù)會在被調(diào)用完成后自動調(diào)用父類的析構(gòu)函數(shù)清理父類成員。因為這樣才能保證子類對象先清理子類成員再清理父類成員的順序。
-
子類對象初始化先調(diào)用父類構(gòu)造再調(diào)子類構(gòu)造。
-
子類對象析構(gòu)清理先調(diào)用子類析構(gòu)再調(diào)父類的析構(gòu)。
-
因為后續(xù)一些場景析構(gòu)函數(shù)需要構(gòu)成重寫,重寫的條件之一是函數(shù)名相同(后面會講解)。那么編譯器會對析構(gòu)函數(shù)名進行特殊處理,處理成 destrutor(),所以父類析構(gòu)函數(shù)不加 virtual 的情況下,子類析構(gòu)函數(shù)和父類析構(gòu)函數(shù)構(gòu)成隱藏關系。
下面演示繼承中構(gòu)造函數(shù)和析構(gòu)函數(shù)的調(diào)用情況:
class A
{
public:
A() { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
private:
int _a;
};
class B : public A
{
public:
B() { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
};
上述代碼中,我們實例化B對象,編譯器會先調(diào)用父類的構(gòu)造,即先調(diào)A的構(gòu)造,然后再構(gòu)造子類B,析構(gòu)順序按照先子后父,例如下圖:
6. 繼承與友元
友元關系不能繼承,也就是說父類友元不能訪問子類私有和保護成員。簡單一句話說就是,父類的友元并不是子類的友元,所以不能繼承下來。
7. 繼承與靜態(tài)成員
父類定義了 static 靜態(tài)成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個子類,都只有一個 static 成員實例。
8. 復雜的菱形繼承及菱形虛擬繼承
(1)繼承類型
1. 單繼承:一個子類只有一個直接父類時稱這個繼承關系為單繼承。
2. 多繼承:一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承。
3. 菱形繼承:菱形繼承是多繼承的一種特殊情況。
菱形繼承的問題:從下面的對象成員模型構(gòu)造,可以看出菱形繼承有數(shù)據(jù)冗余和二義性的問題。以上圖為例,在 D 的對象中 A 成員會有兩份,如下圖:
用代碼來說明如下代碼為四個類以以上的方式繼承:
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
我們嘗試實例化一個D類對象d,先嘗試修改d對象中的 _b 值:
如上圖是沒有問題的,我們再嘗試修改d對象中的 _a 值:
如上圖,可以看出編譯不通過,理由就是對 _a 的訪問不明確,就是因為存在二義性,在 d 中存有兩份 _a,編譯器不知道我們訪問的是哪一個;
但是我們可以指定訪問域,如下圖:
因為在 B 中和 C 中各有一份 _a,所以我們可以指定 B 中的 _a,或者 C 中的 _a 進行指定操作,這樣就解決二義性問題了;但是數(shù)據(jù)冗余還沒有解決。
但是有一種方法可以既解決二義性,也解決數(shù)據(jù)冗余問題,就是虛擬繼承。虛擬繼承可以解決菱形繼承的二義性和數(shù)據(jù)冗余的問題。 如上面的繼承關系,在 B 和 C 繼承 A 時使用虛擬繼承,即可解決問題。需要注意的是,虛擬繼承不要在其他地方去使用。
我們用代碼演示證明一下,首先我們在 B 和 C 繼承 A 時加上 virtual 關鍵字,說明是虛擬繼承:
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
接下來我們直接實例化 D 對象,并訪問 _a,如下圖:
如上二義性問題就解決了,那么我們看看 d 對象中 B 和 C 中的 _a 的值 :
我們觀察可知,d 對象中繼承下來的類里面的 _a 都是 1,也就是說,這個 _a 在這個 d 對象中,只有一份!那就說明虛擬繼承也解決了數(shù)據(jù)冗余問題了!
(2)虛擬繼承解決數(shù)據(jù)冗余和二義性的原理
首先我們先通過調(diào)試觀察一下普通菱形繼承中,d 對象中的內(nèi)存分布,如下圖所示:
我們可以觀察到 d 對象中有兩份 _a ,明顯的數(shù)據(jù)冗余。
接下來我們加上虛擬繼承,繼續(xù)觀察 d 對象中的內(nèi)存分布,如下圖:
上圖是菱形虛擬繼承的內(nèi)存對象成員模型:這里可以分析出 D 對象中將 A 放到的了對象組成的最下面,這個 A 同時屬于 B 和 C,那么 B 和 C 如何去找到公共的 A 呢?這就和 B 和 C 中多了兩個地址有關系了,這兩個地址是什么呢?我們可以取它們的地址到內(nèi)存窗口去觀察一下:
這里是通過了 B 和 C 的兩個指針,指向的一張表。這兩個指針叫虛基表指針,這兩個表叫虛基表。虛基表中存的是偏移量,通過偏移量可以找到下面的 A。例如上圖中 B 中的指針指向的虛基表中,它的偏移量是十六進制的 14 字節(jié),化作十進制就是 20 字節(jié),那么從虛基表指針開始,往下 5 個單位就是 A,每個單位 4 個字節(jié),那么就剛好是 20 個字節(jié)就能找到 A.
9. 繼承的總結(jié)
繼承和組合
-
public 繼承是一種 is-a 的關系。也就是說每個子類對象都是一個父類對象。
-
組合是一種 has-a 的關系。假設 B 組合了 A,每個 B 對象中都有一個 A 對象。
什么是組合呢?如下代碼就是組合,其中是 B 組合了 A:class A { public: int _a; }; class B { public: int _b; A a; };
-
優(yōu)先使用對象組合,而不是類繼承 。
-
繼承允許我們根據(jù)父類的實現(xiàn)來定義子類的實現(xiàn)。這種通過生成子類的復用通常被稱為白箱復用(white-box reuse)。術語 “白箱” 是相對可視性而言:在繼承方式中,父類的內(nèi)部細節(jié)對子類可見 。繼承一定程度破壞了父類的封裝,父類的改變,對子類有很大的影響。子類和父類間的依賴關系很強,耦合度高。
-
對象組合是類繼承之外的另一種復用選擇。新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種復用風格被稱為黑箱復用(black-box reuse),因為對象的內(nèi)部細節(jié)是不可見的。對象只以 “黑箱” 的形式出現(xiàn)。組合類之間沒有很強的依賴關系,耦合度低。 優(yōu)先使用對象組合有助于我們保持每個類被封裝。
-
實際盡量多去用組合。組合的耦合度低,代碼維護性好。 不過繼承也有用武之地的,有些關系就適合繼承那就用繼承,另外要實現(xiàn)多態(tài),也必須要繼承。類之間的關系可以用繼承,可以用組合,就用組合。
二、多態(tài)
1. 多態(tài)的概念
多態(tài)的概念: 通俗來說,就是多種形態(tài),具體點就是去完成某個行為,當不同的對象去完成時會產(chǎn)生出不同的狀態(tài)。
比如買票這個行為,當普通人買票時,是全價買票;學生買票時,是半價買票;軍人買票時是優(yōu)先買票。
2. 多態(tài)的定義及實現(xiàn)
多態(tài)是在不同繼承關系的類對象,去調(diào)用同一函數(shù),產(chǎn)生了不同的行為。比如 Student 繼承了 Person。Person 對象買票全價,Student 對象買票半價。
(1)多態(tài)的構(gòu)成條件
那么在繼承中要構(gòu)成多態(tài)還有兩個條件:
- 必須通過父類的指針或者引用調(diào)用虛函數(shù);
- 被調(diào)用的函數(shù)必須是虛函數(shù),且子類必須對父類的虛函數(shù)進行重寫;
我們先簡單看一下多態(tài)的使用,如以下代碼:
class Person
{
// 讓 BuyTicket 成為虛函數(shù)
public:
virtual void BuyTicket()
{
cout << "買票-全價" << endl;
}
};
class Student : public Person
{
// 對 BuyTicket 進行重寫
public:
virtual void BuyTicket()
{
cout << "買票-半價" << endl;
}
};
// 父類的指針或者引用調(diào)用
//void func(Person& p)
void func(Person* pa)
{
pa->BuyTicket();
}
下面開始多態(tài)的調(diào)用,如下結(jié)果:
如上就是多態(tài)的簡單使用,下面開始詳細介紹多態(tài)的條件。
(2)虛函數(shù)
虛函數(shù): 即被 virtual 修飾的類成員函數(shù)稱為虛函數(shù)。
如下代碼中的 BuyTicket 函數(shù):
class Person
{
public:
virtual void BuyTicket()
{
cout << "買票-全價" << endl;
}
};
(3)虛函數(shù)的重寫
虛函數(shù)的重寫(覆蓋):子類中有一個跟父類完全相同的虛函數(shù)(即子類虛函數(shù)與父類虛函數(shù)的返回值類型、函數(shù)名字、參數(shù)列表完全相同),稱子類的虛函數(shù)重寫了父類的虛函數(shù)。
如下段代碼中,子類 Student 類對 BuyTicket 函數(shù)完成了重寫:
class Person
{
public:
virtual void BuyTicket()
{
cout << "買票-全價" << endl;
}
};
class Student : public Person
{
public:
// void BuyTicket() // 子類對 BuyTicket 進行重寫時,也可以不加 virtual
virtual void BuyTicket()
{
cout << "買票-半價" << endl;
}
};
在重寫父類虛函數(shù)時,子類的虛函數(shù)在不加 virtual 關鍵字時,雖然也可以構(gòu)成重寫(因為繼承后基類的虛函數(shù)被繼承下來了在派生類依舊保持虛函數(shù)屬性),但是該種寫法不是很規(guī)范,不建議。這也和下面要說的析構(gòu)函數(shù)的重寫有一定的關聯(lián)。
虛函數(shù)重寫的兩個例外:
- 協(xié)變(父類與子類虛函數(shù)返回值類型不同)
子類重寫父類虛函數(shù)時,與父類虛函數(shù)返回值類型不同。即父類虛函數(shù)返回父類對象的指針或者引用,子類虛函數(shù)返回子類對象的指針或者引用時,稱為協(xié)變。(了解即可)
如以下這段代碼中,也構(gòu)成多態(tài):
class Person
{
public:
virtual Person* BuyTicket()
{
cout << "買票-全價" << endl;
return nullptr;
}
};
class Student : public Person
{
public:
virtual Student* BuyTicket()
{
cout << "買票-半價" << endl;
return nullptr;
}
};
- 析構(gòu)函數(shù)的重寫(父類與子類析構(gòu)函數(shù)的名字不同)
如果父類的析構(gòu)函數(shù)為虛函數(shù),此時子類析構(gòu)函數(shù)只要定義,無論是否加 virtual 關鍵字,都與父類的析構(gòu)函數(shù)構(gòu)成重寫,雖然父類與子類析構(gòu)函數(shù)名字不同。這樣看起來違背了重寫的規(guī)則,其實不然,這里可以理解為編譯器對析構(gòu)函數(shù)的名稱做了特殊處理,編譯后析構(gòu)函數(shù)的名稱統(tǒng)一處理成 destructor .
如果在父類中沒有加上 virtual 即析構(gòu)函數(shù)不構(gòu)成多態(tài),當下面這種情景時,不能正確調(diào)用析構(gòu)函數(shù):
// 析構(gòu)函數(shù)不構(gòu)成多態(tài)
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
};
如上圖,當我們 new 的是一個子類對象,將它賦值兼容給父類,會發(fā)生切片操作,此時我們釋放 p 指針,由于析構(gòu)函數(shù)不構(gòu)成多態(tài),因此只會調(diào)用父類的析構(gòu)函數(shù),此時子類部分的空間就沒有被釋放,就會發(fā)生內(nèi)存泄漏。
當我們在父類的析構(gòu)函數(shù)加上 virtual,此時就構(gòu)成多態(tài)了,子類的析構(gòu)加不加 virtual 都無所謂,就是為了防止這種情況,我們在子類中忘記對析構(gòu)函數(shù)進行重寫,所以才會有上面的例外,在子類中進行重寫時可以不加 virtual;所以我們在父類中加上 virtual 即可構(gòu)成多態(tài),如下段代碼:
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
};
(3)override 和 final
從上面可以看出,C++ 對函數(shù)重寫的要求比較嚴格,但是有些情況下由于疏忽,可能會導致函數(shù)名字母次序?qū)懛炊鵁o法構(gòu)成重載,而這種錯誤在編譯期間是不會報出的,只有在程序運行時沒有得到預期結(jié)果才來 debug 會得不償失,因此:C++11提供了 override 和 final 兩個關鍵字,可以幫助用戶檢測是否重寫。
- final:修飾虛函數(shù),表示該虛函數(shù)不能再被重寫;
如下段代碼:
- override:檢查子類虛函數(shù)是否重寫了父類某個虛函數(shù),如果沒有重寫編譯報錯。
如下段代碼:
此時將B類中的func函數(shù)的返回類型改為 void 即可通過編譯。
(4)重載、覆蓋(重寫)、隱藏(重定義)
重載、覆蓋(重寫)、隱藏(重定義)的對比如下圖所示:
3. 抽象類
(1)概念
在虛函數(shù)的后面寫上 =0 ,則這個函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也叫接口類),抽象類不能實例化出對象。子類繼承后也不能實例化出對象,只有重寫純虛函數(shù),子類才能實例化出對象。純虛函數(shù)規(guī)范了子類必須重寫,另外純虛函數(shù)更體現(xiàn)出了接口繼承。
class A
{
public:
virtual void func() = 0
{
cout << "A::func()" << endl;
}
};
class B : public A
{
public:
virtual void func()
{
cout << "B::func()" << endl;
}
};
class C : public A
{
public:
virtual void func()
{
cout << "C::func()" << endl;
}
};
如上段代碼,A類是抽象類,不能實例化出對象,B類和C類繼承了A類,并完成重寫 func 函數(shù),所以B類和C類可以實例化對象;下面我們簡單使用一下:
(2)接口繼承和實現(xiàn)繼承
普通函數(shù)的繼承是一種實現(xiàn)繼承,子類繼承了父類函數(shù),可以使用函數(shù),繼承的是函數(shù)的實現(xiàn)。
虛函數(shù)的繼承是一種接口繼承,子類繼承的是父類虛函數(shù)的接口,目的是為了重寫,達成多態(tài),繼承的是接口。所以如果不實現(xiàn)多態(tài),不要把函數(shù)定義成虛函數(shù)。
4. 多態(tài)的原理
(1)虛函數(shù)表
我們先看一下下面這個類的大小是多少?
class A
{
public:
virtual void Func()
{
cout << "Func()" << endl;
}
private:
int _a = 3;
};
根據(jù)我們以前學的知識,成員函數(shù)不占A類的空間,而是放在公共代碼區(qū),而成員變量則根據(jù)內(nèi)存對齊計算大小,所以 sizeof(A) = 4
,這樣算對嗎?我們看一下結(jié)果:
如上圖,答案是 8,為什么會是 8 呢?我們實例化一個對象出來觀察它里面到底有什么:
通過上圖我們發(fā)現(xiàn),除了 _a 成員,還多一個 __vfptr 放在對象的前面(注意有些平臺可能會放到對象的最后面,這個跟平臺有關),對象中的這個指針我們叫做虛函數(shù)表指針(v代表virtual,f代表function)。一個含有虛函數(shù)的類中都至少都有一個虛函數(shù)表指針,因為虛函數(shù)的地址要被放到虛函數(shù)表中,虛函數(shù)表也簡稱虛表。那么子類中這個表放了些什么呢?我們接著往下分析;
我們寫一個 Base 類,Derive 類繼承Base,Base 類中Func1 和 Func2 是虛函數(shù),F(xiàn)unc3 不是虛函數(shù); Derive 中重寫 Func1函數(shù),如下段代碼:
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
下面我們實例化 b 對象和 d 對象:
int main()
{
Base b;
Derive d;
return 0;
}
然后我們通過調(diào)試窗口觀察對象中的內(nèi)存:
我們看到,兩個對象的虛函數(shù)指針是不一樣;我們再進一步觀察虛函數(shù)指針中虛表的內(nèi)容:
通過觀察和測試,我們發(fā)現(xiàn)了以下幾點問題:
- 子類對象 d 中也有一個虛表指針,d 對象由兩部分構(gòu)成,一部分是父類繼承下來的成員和虛表指針,另一部分是自己的成員。
- 父類 b 對象和子類 d 對象虛表是不一樣的,這里我們發(fā)現(xiàn) Func1完成了重寫,所以 d 的虛表中存的是重寫的 Derive::Func1 的地址,所以虛函數(shù)的重寫也叫作覆蓋,覆蓋就是指虛表中虛函數(shù)地址的覆蓋。重寫是語法的叫法,覆蓋是原理層的叫法。
- 另外 Func2 繼承下來后是虛函數(shù),所以放進了虛表,Func3 也繼承下來了,但是不是虛函數(shù),所以不會放進虛表。
- 虛函數(shù)表本質(zhì)是一個存虛函數(shù)指針的指針數(shù)組,一般情況這個數(shù)組最后面放了一個nullptr。
- 總結(jié)一下子類的虛表生成:
a.先將父類中的虛表內(nèi)容拷貝一份到子類虛表中;
b.如果子類重寫了父類中某個虛函數(shù),用子類自己的虛函數(shù)覆蓋虛表中父類的虛函數(shù) ;
c.子類自己新增加的虛函數(shù)按其在子類中的聲明次序增加到子類虛表的最后。
(2)多態(tài)的原理
有了上面的基礎,我們就可以分析一下多態(tài)的原理了;
首先我們有以下兩個類實現(xiàn)的多態(tài):
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
private:
int _b = 1;
};
class Derive : public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
void Test(Base* pa)
{
pa->Func1();
}
接下來我們分別使用Base和Derive實例化出對象 b 和 d,再調(diào)用 Test 函數(shù):
int main()
{
Base b;
Test(&b);
Derive d;
Test(&d);
return 0;
}
下面我們從匯編的角度分析多態(tài)的原理,如下圖:
從上圖可以看出,多態(tài)調(diào)用是運行時,去虛表里面找到函數(shù)地址,確定地址再調(diào)用。
但如果是對象調(diào)用,如下代碼,就不構(gòu)成多態(tài),我們同樣在匯編角度分析:
void Test(Base pa)
{
pa.Func1();
}
int main()
{
Base b;
Test(b);
Derive d;
Test(d);
return 0;
}
從上圖可以看出,普通調(diào)用是編譯鏈接時確定地址。
所以,我們要達到多態(tài),有兩個條件,一個是虛函數(shù)覆蓋,一個是對象的指針或引用調(diào)用虛函數(shù);其中虛函數(shù)覆蓋就不多說了;
父類的指針或引用接收對象的指針或引用調(diào)用虛函數(shù),本質(zhì)就是父類指針指向子類對象中切割出來父類的那一部分,然后再取虛函數(shù)指針找到虛函數(shù)的地址。
那么我們使用對象調(diào)用為什么不能實現(xiàn)多態(tài)呢?其實使用子類對象賦值給父類對象,會切割出子類對象中父類那一部分成員拷貝給父類,但是不會拷貝虛函數(shù)指針。為什么呢?我們假設:子類對象會拷貝虛函數(shù)表指針給父類對象,我們看一下以下代碼:
Person* p = new Person;
Student s;
*p = s;
delete p;
此時出現(xiàn)的問題就是,多態(tài)調(diào)用指向父類,調(diào)用的不一定是父類的虛函數(shù)了,因為我們中間將 s 賦給了 *p,現(xiàn)在 p 中的虛函數(shù)指針是 s 的;所以這種方法不可取。
(3)動態(tài)綁定與靜態(tài)綁定
- 靜態(tài)綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態(tài)多態(tài),比如:函數(shù)重載;
- 動態(tài)綁定又稱后期綁定(晚綁定),是在程序運行期間,根據(jù)具體拿到的類型確定程序的具體行為,調(diào)用具體的函數(shù),也稱為動態(tài)多態(tài)。
5. 虛函數(shù)和虛表存在于哪里?
虛函數(shù)和虛表存在于哪里?有人會說虛函數(shù)存在虛表,虛表存在對象中。但是上面的回答的錯誤的。
想要知道虛函數(shù)和虛表存在于哪里,我們可以打出各個區(qū)域的地址觀察,觀察虛函數(shù)和虛表離哪個區(qū)域比較近,下面我們開始驗證,如下段代碼:
class Base
{
public:
virtual void func1()
{
cout << "Base::func1" << endl;
}
virtual void func2()
{
cout << "Base::func2" << endl;
}
private:
int a;
};
void func()
{
cout << "void func()" << endl;
}
int main()
{
Base b1;
static int a = 0;
int b = 0;
int* p1 = new int;
const char* p2 = "hello";
printf("靜態(tài)區(qū):%p\n", &a);
printf("棧:%p\n", &b);
printf("堆:%p\n", p1);
printf("代碼段/常量區(qū):%p\n", p2);
printf("虛表:%p\n", *((int*)&b1));
printf("虛函數(shù)地址:%p\n", &Base::func1);
printf("普通函數(shù)地址:%p\n", func);
return 0;
}
運行結(jié)果如下:
我們可以看到,在 vs2019 中,虛表和虛函數(shù)的地址都是離代碼段/常量區(qū)最近的,所以我們認為,在 vs2019 中,它們是在代碼段中的。
6. 單繼承中的虛函數(shù)表
需要注意的是在單繼承和多繼承關系中,下面我們?nèi)リP注的是子類對象的虛表模型,因為父類的虛表模型前面我們已經(jīng)看過了,沒什么需要特別研究的。我們這里就只看單繼承的中的虛函數(shù)表。
例如有以下代碼:
class Base
{
public:
virtual void func1()
{
cout << "Base::func1" << endl;
}
virtual void func2()
{
cout << "Base::func2" << endl;
}
private:
int a;
};
class Derive :public Base
{
public:
virtual void func1()
{
cout << "Derive::func1" << endl;
}
virtual void func3()
{
cout << "Derive::func3" << endl;
}
virtual void func4()
{
cout << "Derive::func4" << endl;
}
private:
int b;
};
int main()
{
Base b;
Derive d;
return 0;
}
我們通過監(jiān)視窗口觀察子類對象中的虛表模型:
觀察上圖中的監(jiān)視窗口中我們發(fā)現(xiàn)看不見 func3 和 func4。這里是編譯器的監(jiān)視窗口故意隱藏了這兩個函數(shù),也可以認為是他的一個小 bug。那么我們?nèi)绾尾榭?d 的虛表呢?下面我們使用代碼打印出虛表中的函數(shù)。
class Base
{
public:
virtual void func1()
{
cout << "Base::func1" << endl;
}
virtual void func2()
{
cout << "Base::func2" << endl;
}
private:
int a;
};
class Derive : public Base
{
public:
virtual void func1()
{
cout << "Derive::func1" << endl;
}
virtual void func3()
{
cout << "Derive::func3" << endl;
}
virtual void func4()
{
cout << "Derive::func4" << endl;
}
private:
int b;
};
// typedef 函數(shù)指針類型
typedef void (*VFUNC)();
void PrintVFT(VFUNC* a)
{
for (size_t i = 0; a[i] != 0; i++)
{
printf("[%d]:%p->", i, a[i]);
VFUNC f = a[i];
f(); // 相當于調(diào)(*f)(); 調(diào)函數(shù)指針對應的函數(shù)
}
}
int main()
{
Base b;
PrintVFT((VFUNC*)(*((int*)&b)));
cout << endl;
Derive d;
PrintVFT((VFUNC*)(*((int*)&d)));
return 0;
}
打印的結(jié)果如下:
此時我們就可以看到 d 對象中的虛函數(shù)了。
7. 多繼承中的虛函數(shù)表
假設一個類是多繼承下來的,這個類有幾個父類,如果父類都有虛函數(shù),則就會有幾張?zhí)摫恚@個類自身不會產(chǎn)生多余的虛表;如果這個類自身也有虛函數(shù)呢?那么這個虛函數(shù)將會放到第一個父類的虛表中的最后,其他父類的虛表中不需要存儲這個類的虛函數(shù),因為存儲了也不能調(diào)用。
例如我們有以下三個類,A,B,C,其中 C 繼承 A 和 B,屬于多繼承,A 和 B 中都有虛函數(shù),C 中也對 A 、B 中的虛函數(shù)進行重寫,C 中再增加自己的虛函數(shù);如下三個類:
class A
{
public:
virtual void func1()
{
cout << "A" << endl;
}
};
class B
{
public:
virtual void func1()
{
cout << "B" << endl;
}
};
class C : public A, public B
{
public:
virtual void func1()
{
cout << "C" << endl;
}
virtual void func2() { ; }
};
此時我們觀察它們的虛表,探究它們的關系;先定義三個類的對象:
int main()
{
A a;
B b;
C c;
return 0;
}
我們進入調(diào)式模式觀察每個對象中的內(nèi)存:
先觀察a、b 對象中的內(nèi)存,它們都分別有一個虛函數(shù)指針,對應的虛函數(shù)指針中都有它們各自的虛函數(shù)的地址:
然后觀察 c 對象中的內(nèi)存,c對象中有兩個虛函數(shù)指針,即有兩個虛函數(shù)表,我們分別觀察這兩個虛函數(shù)表中的內(nèi)容:
文章來源:http://www.zghlxwxcb.cn/news/detail-735205.html
通過上面我們可以得出結(jié)論,c 對象中自己定義的虛函數(shù)最終會放在第一個繼承的父類中的虛函數(shù)表的最后,而不會放在其它父類的虛函數(shù)表中。文章來源地址http://www.zghlxwxcb.cn/news/detail-735205.html
到了這里,關于【C++】繼承和多態(tài)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!