C++繼承和多態(tài)的用法解讀
繼承
繼承的概念
繼承機(jī)制是面向?qū)ο蟪绦蛟O(shè)計(jì)使代碼可以復(fù)用的最重要的手段,它允許程序員在保 持原有類特性的基礎(chǔ)上進(jìn)行擴(kuò)展,增加功能,這樣產(chǎn)生新的類,稱派生類。
繼承呈現(xiàn)了面向?qū)ο?程序設(shè)計(jì)的層次結(jié)構(gòu),體現(xiàn)了由簡單到復(fù)雜的認(rèn)知過程。以前我們接觸的復(fù)用都是函數(shù)復(fù)用,繼承是類設(shè)計(jì)層次的復(fù)用。
繼承的定義
定義格式

繼承關(guān)系和訪問限定符


繼承基類成員訪問方式的變化

基類是 private ,這種情況下,無論以何種方式繼承,對于基類私有的部分在子類中都是不可見的,這里的不可見指的是無法訪問,但仍然繼承下來了,只是無法使用而已。但是可以通過父類提供的方法進(jìn)行間接的訪問和使用。
如圖:(Print 方法在上圖父類中有寫)如圖:

對于父類私有成員,子類無論哪種繼承都無法訪問,對于父類其他成員,繼承后被哪種訪問限定符修飾取決于父類成員訪問限定符和繼承方式中權(quán)限小的那一個(gè)。
從繼承這里我們也可以看出 protect 的意義:正是因?yàn)橛辛死^承,protect 才有意義,對于父類不想讓外界訪問也不想讓子類訪問的成員,父類可以用 private 修飾,對于父類不想讓外界訪問,但想讓子類訪問的成員可以用 protect 修飾。
使?關(guān)鍵字 class 時(shí)默認(rèn)的繼承?式是private,使? struct 時(shí)默認(rèn)的繼承?式是public
基類和派生類對象賦值轉(zhuǎn)換
派生類對象可以賦值給基類的對象 / 基類的指針 / 基類的引用。這里有個(gè)形象的說法叫切片 或者切割。寓意把派生類中父類那部分切來賦值過去。
基類對象不能賦值給派生類對象。
基類的指針或者引用可以通過強(qiáng)制類型轉(zhuǎn)換賦值給派生類的指針或者引用。但是必須是基類 的指針是指向派生類對象時(shí)才是安全的。
子賦值給父:向上轉(zhuǎn)換(可以)
父賦值給子:向下轉(zhuǎn)換(不可以)
不會產(chǎn)生臨時(shí)變量,將子類中父親的切片出來拷貝給父類
不能向下轉(zhuǎn)換原因
根本問題:基類指針/引用可能并不實(shí)際指向派生類對象
派生類通常比基類有更多成員,錯(cuò)誤向下轉(zhuǎn)型會導(dǎo)致訪問不存在的數(shù)據(jù):

基本繼承示例代碼
#include<iostream>
using namespace std;
// 基類 Person
class Person
{
public:
void Print()
{
cout << "name:" << _name << endl;
cout << "age:" << _age << endl;
}
protected:
string _name = "peter"; // 姓名
int _age = 18; // 年齡
};
// 公有繼承 Person
class Student : public Person
{
protected:
int _stuid; // 學(xué)號
};
// 公有繼承 Person
class Teacher : public Person
{
protected:
int _jobid; // 工號
};
int main()
{
Person p;
Student s;
// 賦值兼容轉(zhuǎn)換(切割/切片)
Person p1 = s; // 派生類對象賦值給基類對象
Person& rp = s; // 基類引用引用派生類對象
rp._name = "張三"; // 通過引用修改派生類中的基類部分
Person* ptrp = &s; // 基類指針指向派生類對象
ptrp->_name = "李四"; // 通過指針修改派生類中的基類部分
return 0;
}
學(xué)生和教師類繼承了人這個(gè)類,人這個(gè)類中所有的東西在學(xué)生和教師類里已經(jīng)都具有了,需要注意的是成員變量雖然繼承過來了,但各自的對象都獨(dú)立有這樣一份成員變量,使用起來互不影響,而對于繼承過來的成員方法都使用同一份(構(gòu)造函數(shù)除外)。
繼承中的作用域
1. 在繼承體系中基類和派生類都有獨(dú)立的作用域。
2. 子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏, 也叫重定義。(在子類成員函數(shù)中,可以使用 基類::基類成員 顯示訪問)
3. 需要注意的是如果是成員函數(shù)的隱藏,只需要函數(shù)名相同就構(gòu)成隱藏。
4. 注意在實(shí)際中在繼承體系里面最好不要定義同名的成員。
- 隱藏/重定義:子類和父類有同名成員,子類隱藏父類成員(就近原則)
- 重載:同一個(gè)作用域
- 隱藏:父子類域中函數(shù)名相同
- 派生類不能直接在初始化列表中初始化基類的成員變量,必須通過基類的構(gòu)造函數(shù)來初始化基類成員
成員隱藏(重定義)示例
class Person
{
public:
void fun()
{
cout << "Person::func()" << endl;
}
protected:
string _name = "小李子"; // 姓名
int _num = 111; // 身份證號
};
// 派生類 Student
class Student : public Person
{
public:
// 隱藏了父類的 fun() 函數(shù)
void fun()
{
cout << "Student::func()" << endl;
}
void Print()
{
cout << "姓名:" << _name << endl;
cout << _num << endl; // 訪問派生類的 _num
cout << Person::_num << endl; // 訪問基類的 _num
}
protected:
int _num = 999; // 學(xué)號 (隱藏了基類的 _num)
};
int main()
{
Student s;
s.Print();
s.fun(); // 調(diào)用派生類的 fun
s.Person::fun(); // 顯式調(diào)用基類的 fun
return 0;
}
派生類的默認(rèn)成員函數(shù)
6個(gè)默認(rèn)成員函數(shù),“默認(rèn)”的意思就是指我們不寫,編譯器會變我們自動(dòng)生成一個(gè),那么在派生類 中,這幾個(gè)成員函數(shù)是如何生成的呢?
1. 派生類的構(gòu)造函數(shù)必須調(diào)用基類的構(gòu)造函數(shù)初始化基類的那一部分成員。如果基類沒有默認(rèn) 的構(gòu)造函數(shù),則必須在派生類構(gòu)造函數(shù)的初始化列表階段顯示調(diào)用。
class person
{
public:
person()
:_name("張三")
{}
void Print()
{
cout << "person" << _name << endl;
}
protected:
int _age;
string _name;
};
class student :public person
{
public:
student()
:person()
,_num(0)
{}
void Print()
{
cout << "student" << _name <<_num<< endl;
}
protected:
int _num;
};class Base {
protected:
int _x;
public:
Base(int x) : _x(x) {} // 基類構(gòu)造函數(shù)初始化 _x
};
class Derived : public Base {
public:
// 通過 Base(x) 初始化基類成員 _x
Derived(int x) : Base(x) {}
// 錯(cuò)誤:不能在派生類初始化列表直接初始化 _x
// Derived(int x) : _x(x) {}
};2. 派生類的拷貝構(gòu)造函數(shù)必須調(diào)用基類的拷貝構(gòu)造完成基類的拷貝初始化。
3. 派生類的operator=必須要調(diào)用基類的operator=完成基類的復(fù)制。
4. 派生類的析構(gòu)函數(shù)會在被調(diào)用完成后自動(dòng)調(diào)用基類的析構(gòu)函數(shù)清理基類成員。因?yàn)檫@樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
5. 派生類對象初始化先調(diào)用基類構(gòu)造再調(diào)派生類構(gòu)造。
6. 派生類對象析構(gòu)清理先調(diào)用派生類析構(gòu)再調(diào)基類的析構(gòu)。
7. 因?yàn)楹罄m(xù)一些場景析構(gòu)函數(shù)需要構(gòu)成重寫,重寫的條件之一是函數(shù)名相同(這個(gè)我們后面會講 解)。那么編譯器會對析構(gòu)函數(shù)名進(jìn)行特殊處理,處理成destrutor(),所以父類析構(gòu)函數(shù)不加 virtual的情況下,子類析構(gòu)函數(shù)和父類析構(gòu)函數(shù)構(gòu)成隱藏關(guān)系。


成員函數(shù)示例
class Person
{
public:
Person(const char* name)
: _name(name)
{
cout << "Person()" << endl;
}
Person(const Person& p)
: _name(p._name)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
cout << "Person operator=(const Person& p)" << endl;
if (this != &p)
_name = p._name;
return *this;
}
~Person()
{
cout << "~Person()" << endl;
delete _pstr;
}
protected:
string _name; // 姓名
string* _pstr = new string("111111111");
};
class Student : public Person
{
public:
// 先調(diào)用基類構(gòu)造函數(shù),再初始化派生類成員
Student(const char* name = "張三", int id = 0)
:Person(name)
,_id(id)
{}
// 拷貝構(gòu)造
Student(const Student& s)
:Person(s) // 調(diào)用基類拷貝構(gòu)造
,_id(s._id)
{}
// 賦值運(yùn)算符
Student& operator=(const Student& s)
{
if (this != &s)
{
Person::operator=(s); // 調(diào)用基類賦值運(yùn)算符
_id = s._id;
}
return *this;
}
~Student()
{
// 子類析構(gòu)完成后會自動(dòng)調(diào)用父類析構(gòu)
cout << *_pstr << endl;
delete _ptr;
}
protected:
int _id;
int* _ptr = new int;
};
int main()
{
Student s1;
Student s2(s1); // 調(diào)用拷貝構(gòu)造
Student s3("李四", 1);
s1 = s3; // 調(diào)用賦值運(yùn)算符
return 0;
}- Student s2(s1); // 調(diào)用拷貝構(gòu)造,沒寫拷貝構(gòu)造默認(rèn)掉默認(rèn)構(gòu)造
- 派生類只用析構(gòu)自己的就可以了
- 構(gòu)造子類后編譯器自動(dòng)析構(gòu)父類(子可以用父,父不可以用子)
- 由于后邊多態(tài)的問題析構(gòu)函數(shù)函數(shù)名被特殊處理了,統(tǒng)一處理成destructer
繼承與友元
友元關(guān)系不能繼承,也就是說基類友元不能訪問子類私有和保護(hù)成員
友元函數(shù)示例
class Student;
class Person
{
public:
friend void Display(const Person& p, const Student& s);
protected:
string _name; // 姓名
};
class Student : public Person
{
friend void Display(const Person& p, const Student& s);
protected:
int _stuNum; // 學(xué)號
};
// 友元函數(shù)可以訪問兩個(gè)類的私有和保護(hù)成員
void Display(const Person& p, const Student& s)
{
cout << p._name << endl;
cout << s._stuNum << endl;
}
int main()
{
Person p;
Student s;
Display(p, s);
return 0;
}

前置聲明
| 關(guān)鍵點(diǎn) | 說明 |
|---|---|
| 前置聲明的作用 | 告訴編譯器某個(gè)名稱是類類型,具體定義稍后出現(xiàn)。 |
| 何時(shí)需要前置聲明 | 當(dāng)類的名稱在完整定義之前被使用(如友元聲明、函數(shù)參數(shù))。 |
| 何時(shí)需要完整定義 | 當(dāng)需要實(shí)例化對象、訪問成員、繼承或計(jì)算類大小時(shí)。 |
| 友元函數(shù)的特殊性 | 友元聲明中用到未定義的類時(shí),必須前置聲明該類。 |
繼承與靜態(tài)成員
基類定義了static靜態(tài)成員,則整個(gè)繼承體系里面只有一個(gè)這樣的成員。無論派生出多少個(gè)子類,都只有一個(gè)static成員實(shí)例 。
靜態(tài)成員屬于父類和派生類,派生類中不會單獨(dú)拷貝一份,繼承的是使用權(quán)
- 靜態(tài)成員屬于類本身,不屬于對象
- 無論是否涉及繼承,靜態(tài)成員(static 變量/函數(shù))都是類的全局共享成員,不屬于任何一個(gè)對象。所有對象(包括基類和派生類的對象)訪問的是同一份靜態(tài)成員。
- 派生類不會單獨(dú)拷貝靜態(tài)成員
- 靜態(tài)成員不會被派生類復(fù)制,而是直接繼承訪問權(quán)。
- 基類和派生類共享同一個(gè)靜態(tài)成員。
靜態(tài)成員繼承示例
class Person
{
public:
Person() { ++_count; }
protected:
string _name; // 姓名
public:
static int _count; // 統(tǒng)計(jì)人的個(gè)數(shù)
};
int Person::_count = 0;
class Student : public Person
{
protected:
int _stuNum; // 學(xué)號
};
class Graduate : public Student
{
protected:
string _seminarCourse; // 研究科目
};
int main()
{
Person p;
Student s1;
Student s2;
cout << Person::_count << endl; // 輸出3,因?yàn)閯?chuàng)建了3個(gè)對象
return 0;
}
復(fù)雜的菱形繼承與菱形虛擬繼承
單繼承:一個(gè)子類只有一個(gè)直接父類時(shí)稱這個(gè)繼承關(guān)系為單繼承

多繼承:一個(gè)子類有兩個(gè)或以上直接父類時(shí)稱這個(gè)繼承關(guān)系為多繼承

菱形繼承:菱形繼承是多繼承的一種特殊情況。

菱形繼承的問題:從下面的對象成員模型構(gòu)造,可以看出菱形繼承有數(shù)據(jù)冗余和二義性的問題。 在Assistant的對象中Person成員會有兩份。

菱形繼承代碼

主要問題
1. 數(shù)據(jù)冗余問題
Assistant對象會包含 兩個(gè)Person子對象:- 一個(gè)來自
Student繼承路徑 - 一個(gè)來自
Teacher繼承路徑 - 這意味著
_name和_age會被存儲兩次,造成內(nèi)存浪費(fèi)
2. 二義性問題(編譯錯(cuò)誤)
當(dāng)嘗試直接訪問 as._name 時(shí),編譯器無法確定應(yīng)該使用哪個(gè)路徑的 _name:
as._name = "張三"; // 錯(cuò)誤:對成員'_name'的訪問不明確
3. 必須明確指定訪問路徑
要解決二義性問題,必須指定訪問路徑:
as.Student::_name = "張三"; // 通過Student路徑訪問 // 或 as.Teacher::_name = "張三"; // 通過Teacher路徑訪問
虛繼承(解決菱形繼承問題)
通過virtual關(guān)鍵字來實(shí)現(xiàn)虛繼承解決菱形繼承問題
Person成為虛基類(Virtual Base Class)。Assistant對象只包含一份Person子對象,Student和Teacher共享它。_age和_name不再冗余,所有訪問都指向同一個(gè)內(nèi)存位置。
virtual要加在第一個(gè)會引起數(shù)據(jù)冗余的類上
class Person
{
public:
string _name; // 姓名
int _age;
};
class Student : virtual public Person
{
protected:
int _num; //學(xué)號
};
class Teacher : virtual public Person
{
protected:
int _id; // 職工編號
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修課程
};
int main()
{
Assistant as;
as.Student::_age = 18;
as.Teacher::_age = 30;
as._age = 19;
return 0;
}虛擬繼承解決數(shù)據(jù)冗余和二義性的原理
菱形繼承

菱形繼承有數(shù)據(jù)冗余問題
菱形虛擬繼承



虛繼承構(gòu)造函數(shù)調(diào)用順序
#include<iostream>
using namespace std;
class A {
public:
A(const char* s) {
cout << s << endl; // 打印構(gòu)造信息
}
~A() {}
};
class B : virtual public A // 虛繼承A
{
public:
B(const char* sa, const char* sb)
: A(sa) { // 初始化虛基類A
cout << sb << endl;
}
};
class C : virtual public A // 虛繼承A
{
public:
C(const char* sa, const char* sb)
: A(sa) { // 初始化虛基類A
cout << sb << endl;
}
};
class D : public B, public C
{
public:
// 注意:虛基類A的初始化由D直接負(fù)責(zé)
D(const char* sa, const char* sb, const char* sc, const char* sd)
: A(sa), // 顯式初始化虛基類(實(shí)際最先執(zhí)行)
B(sa, sb), // 初始化B(此時(shí)不會重復(fù)構(gòu)造A)
C(sa, sc) { // 初始化C(此時(shí)不會重復(fù)構(gòu)造A)
cout << sd << endl;
}
};
int main() {
// 場景1:構(gòu)造D對象(菱形繼承)
D* p = new D("class A", "class B", "class C", "class D");
/* 輸出順序:
class A (虛基類A的構(gòu)造)
class B (B的構(gòu)造)
class C (C的構(gòu)造)
class D (D的構(gòu)造)
*/
delete p;
// 場景2:單獨(dú)構(gòu)造B對象(單繼承)
B b("class A", "class B");
/* 輸出順序:
class A (虛基類A的構(gòu)造)
class B (B的構(gòu)造)
*/
return 0;
}在虛繼承中,構(gòu)造順序遵循:
- 虛基類最先構(gòu)造(無論它在初始化列表中的位置)
- 非虛基類按聲明順序構(gòu)造(
class D : public B, public C則先B后C) - 最后構(gòu)造派生類自身
初始化列表順序僅決定參數(shù)傳遞
如果交換 B 和 C 的參數(shù)(如 C(sa, sc) 寫在 B(sa, sb) 前面),參數(shù)會正常傳遞,但構(gòu)造順序不變
虛繼承內(nèi)存布局示例
class A
{
public:
int _a;
};
// 虛繼承 A
class B : virtual public A
{
public:
int _b;
};
// 虛繼承 A
class C : virtual public A
{
public:
int _c;
};
// 多重繼承
class D : public B, public C
{
public:
int _d;
};
int main()
{
/* D d;
d.B::_a = 1;
d.C::_a = 2;
d._b = 3;
d._c = 4;
d._d = 5;*/
D d;
d._a = 1; // 虛繼承后只有一個(gè)_a
B b;
b._a = 2;
b._b = 3;
B* ptr = &b;
ptr->_a++; // 通過基類指針訪問
ptr = &d;
ptr->_a++; // 通過基類指針訪問派生類對象
return 0;
}- 每個(gè)虛繼承類存儲一個(gè)虛基表指針,指向其虛基表。
- 虛基表存的是偏移量不是A的地址(用來動(dòng)態(tài)計(jì)算虛基類的位置)
- 偏移量在切割和切片中需要
- 虛基表中偏移量里第一個(gè)為其他值進(jìn)行了預(yù)留
- d1和d2都直接指向這個(gè)數(shù)據(jù),不需要在內(nèi)部開額外空間存重復(fù)數(shù)據(jù)
- ptr->a不知道是B還是D的,B和D中間可能擱這好幾部分,先通過虛基表指針找到虛基表,再憑借偏移量可以找到A。

繼承和組合
public繼承是一種is-a的關(guān)系。也就是說每個(gè)派生類對象都是一個(gè)基類對象。
組合是一種has-a的關(guān)系。假設(shè)B組合了A,每個(gè)B對象中都有一個(gè)A對象。
繼承允許你根據(jù)基類的實(shí)現(xiàn)來定義派生類的實(shí)現(xiàn)。這種通過生成派生類的復(fù)用通常被稱 為白箱復(fù)用(white-box reuse)。術(shù)語“白箱”是相對可視性而言:在繼承方式中,基類的內(nèi)部細(xì)節(jié)對子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很 大的影響。派生類和基類間的依賴關(guān)系很強(qiáng),耦合度高。
對象組合是類繼承之外的另一種復(fù)用選擇。新的更復(fù)雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種復(fù)用風(fēng)格被稱為黑箱復(fù) 用(black-box reuse),因?yàn)閷ο蟮膬?nèi)部細(xì)節(jié)是不可見的。對象只以“黑箱”的形式出現(xiàn)。 組合類之間沒有很強(qiáng)的依賴關(guān)系,耦合度低。優(yōu)先使用對象組合有助于你保持每個(gè)類被封裝。
實(shí)際盡量多去用組合。組合的耦合度低,代碼維護(hù)性好。不過繼承也有用武之地的,有些關(guān)系就適合繼承那就用繼承,另外要實(shí)現(xiàn)多態(tài),也必須要繼承。類之間的關(guān)系可以用繼承,可以用組合,就用組合。
| 特性 | 繼承 | 組合 |
|---|---|---|
| 關(guān)系 | is-a | has-a |
| 耦合度 | 高 | 低 |
| 靈活性 | 低(編譯時(shí)確定) | 高(運(yùn)行時(shí)可替換) |
| 代碼復(fù)用 | 白盒復(fù)用(了解實(shí)現(xiàn)) | 黑盒復(fù)用(只使用接口) |
| 多態(tài)支持 | 支持 | 間接支持(通過接口) |
| 基類/組件修改影響 | 影響所有派生類 | 影響范圍有限 |
| 適合場景 | 需要多態(tài)/接口擴(kuò)展 | 代碼復(fù)用/功能組合 |
//繼承
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
//組合
class A
{
public:
int _a;
};
class B :
{
public:
A a();
int _b;
};
多態(tài)
注意:
- 只有成員函數(shù)才能+virtual
- 對象里不存成員函數(shù),只有成員變量
- 不同對象傳過去調(diào)用不同函數(shù)
- 多態(tài)調(diào)用看的指向的對象
- 普通對象看當(dāng)前類型
- 派生類的重寫虛函數(shù)可以不+virtual
- 協(xié)變:返回值可以不同,但返回值必須是父子關(guān)系指針和引用
- 虛函數(shù)三同(函數(shù)名,參數(shù)列表,返回值類型)
多態(tài)的概念
多態(tài)是面向?qū)ο缶幊痰娜筇匦灾唬ǚ庋b、繼承、多態(tài)),它允許不同類的對象對同一消息做出不同的響應(yīng)
多態(tài)分為:
- 編譯時(shí)多態(tài)(靜態(tài)多態(tài)):通過函數(shù)重載和模板實(shí)現(xiàn)
- 運(yùn)行時(shí)多態(tài)(動(dòng)態(tài)多態(tài)):通過虛函數(shù)和繼承實(shí)現(xiàn)
我們主要了解運(yùn)?時(shí)多態(tài)。要實(shí)現(xiàn)運(yùn)行時(shí)多態(tài),必須滿足以下條件:
- 繼承關(guān)系:存在基類和派生類
- 虛函數(shù):基類中使用
virtual聲明函數(shù) - 指針/引用:通過基類指針或引用調(diào)用虛函數(shù)
多態(tài)的定義和實(shí)現(xiàn)
多態(tài)的構(gòu)成條件
多態(tài)是在不同繼承關(guān)系的類對象,去調(diào)用同一函數(shù),產(chǎn)生了不同的行為。比如Student繼承了 Person。Person對象買票全價(jià),Student對象買票半價(jià)。
- 1.調(diào)用函數(shù)是重寫的虛函數(shù)
- 2.基類指針或引用調(diào)用(子類指針只能指向子類,虛表只有子類虛函數(shù),父類沒有子類成員變量或方法會引發(fā)內(nèi)存錯(cuò)誤)
基礎(chǔ)多態(tài)代碼:
#include <iostream>
using namespace std;
// 基類
class Person {
public:
virtual void BuyTicket() {
cout << "買票-全價(jià)" << endl;
}
};
// 派生類
class Student : public Person {
public:
virtual void BuyTicket() override {
cout << "買票-半價(jià)" << endl;
}
};
// 多態(tài)調(diào)用函數(shù)
void Func(Person& p) {
p.BuyTicket(); // 多態(tài)調(diào)用
}
int main() {
cout << "----- 基礎(chǔ)多態(tài)演示 -----" << endl;
Person Mike;
Student Johnson;
Func(Mike); // 輸出: 買票-全價(jià)
Func(Johnson); // 輸出: 買票-半價(jià)
return 0;
}“調(diào)用函數(shù)是重寫的虛函數(shù)”
- 體現(xiàn)在
Student類中virtual void BuyTicket() override,它重寫了基類Person的虛函數(shù)BuyTicket()。 - 運(yùn)行時(shí)通過
Person& p調(diào)用p.BuyTicket()時(shí),會根據(jù)實(shí)際對象類型(Person或Student)決定調(diào)用哪個(gè)版本(多態(tài))。
“基類指針或引用調(diào)用”
- 體現(xiàn)在
Func(Person& p)的參數(shù)是基類引用,且p.BuyTicket()通過該引用調(diào)用虛函數(shù)。 - 如果改為
Func(Person p)(傳值而非引用),則失去多態(tài),始終調(diào)用Person::BuyTicket()。
虛函數(shù)
虛函數(shù):即被virtual修飾的類成員函數(shù)稱為虛函數(shù)。
virtual void BuyTicket() {
cout << "買票-全價(jià)" << endl;
}虛函數(shù)的重寫(覆蓋)
派生類中有一個(gè)跟基類完全相同的虛函數(shù)(即派生類虛函數(shù)與基類虛函數(shù)的 返回值類型、函數(shù)名字、參數(shù)列表完全相同),稱子類的虛函數(shù)重寫了基類的虛函數(shù)。(派生類中virtual可以不寫,但是父類必須寫,因?yàn)槿绻割惒粚戇@個(gè)函數(shù)就不是虛函數(shù)了)
// 派生類
class Student : public Person {
public:
virtual void BuyTicket() override {
cout << "買票-半價(jià)" << endl;
}
};虛函數(shù)重寫的兩個(gè)例外
協(xié)變
基類與派生類虛函數(shù)返回值類型不同
派生類重寫基類虛函數(shù)時(shí),與基類虛函數(shù)返回值類型不同。即基類虛函數(shù)返回基類對象的指 針或者引用,派生類虛函數(shù)返回派生類對象的指針或者引用時(shí),稱為協(xié)變。
協(xié)變僅適用于指針或引用類型
Student::f() 返回 int*不合法,int* 與 A* 無繼承關(guān)系,不滿足協(xié)變規(guī)則。
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
析構(gòu)函數(shù)的重寫
- 基類與派生類析構(gòu)函數(shù)的名字不同
- 如果基類的析構(gòu)函數(shù)為虛函數(shù),此時(shí)派生類析構(gòu)函數(shù)只要定義,無論是否加virtual關(guān)鍵字, 都與基類的析構(gòu)函數(shù)構(gòu)成重寫,雖然基類與派生類析構(gòu)函數(shù)名字不同。雖然函數(shù)名不相同, 看起來違背了重寫的規(guī)則,其實(shí)不然,這里可以理解為編譯器對析構(gòu)函數(shù)的名稱做了特殊處 理,編譯后析構(gòu)函數(shù)的名稱統(tǒng)一處理成destructor。
確保通過基類指針刪除派生類對象時(shí)正確調(diào)用派生類析構(gòu)
內(nèi)存釋放過程:
通過虛表找到 Student::~Student()
執(zhí)行派生類析構(gòu):
- 輸出
~Student() - 釋放
ptr指向的數(shù)組
自動(dòng)調(diào)用基類析構(gòu) Person::~Person()
若不使用虛析構(gòu)函數(shù):
delete p只會調(diào)用Person::~Person()- 導(dǎo)致
ptr內(nèi)存泄漏(約40字節(jié))
防止基類new派生類,析構(gòu)基類


class Person {
public:
virtual void BuyTicket() { cout << "買票-全價(jià)" << endl; }
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "買票-半價(jià)" << endl; }
~Student() {
cout << "~Student()" << endl;
delete[] ptr;
}
protected:
int* ptr = new int[10];
};
int main()
{
//Person p;
//Student s;
Person* p = new Person;
p->BuyTicket();
delete p;
p = new Student;
p->BuyTicket();
delete p; // p->destructor() + operator delete(p)
// 這里我們期望p->destructor()是一個(gè)多態(tài)調(diào)用,而不是普通調(diào)用
return 0;
}
禁止在派生類中釋放基類資源
- 基類析構(gòu)函數(shù)已經(jīng)負(fù)責(zé)其自身資源的釋放,派生類不應(yīng)干涉。
層級化資源管理
- 基類管理基類的資源
- 派生類管理派生類新增的資源
- 像堆疊的俄羅斯套娃,各層管好自己的部分
C++11 override 和 ?nal
C++對函數(shù)重寫的要求比較嚴(yán)格,但是有些情況下由于疏忽,可能會導(dǎo)致函數(shù) 名字母次序?qū)懛炊鵁o法構(gòu)成重載,而這種錯(cuò)誤在編譯期間是不會報(bào)出的,只有在程序運(yùn)行時(shí)沒有 得到預(yù)期結(jié)果才來debug會得不償失,因此:C++11提供了override和?nal兩個(gè)關(guān)鍵字,可以幫 助用戶檢測是否重寫。
?nal:修飾虛函數(shù),表示該虛函數(shù)不能再被重寫
final 的作用:
當(dāng)用于虛函數(shù)時(shí)(如 virtual void Drive() final),表示禁止派生類重寫該函數(shù)。
當(dāng)用于類時(shí)(如 class Benz final),表示禁止其他類繼承 Benz。
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒適" << endl;}
};
override: 檢查派生類虛函數(shù)是否重寫了基類某個(gè)虛函數(shù),如果沒有重寫編譯報(bào)錯(cuò)。
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒適" << endl;}
};設(shè)計(jì)不希望被繼承類
1.基類構(gòu)造函數(shù)私有 (C++98)
1)通過將構(gòu)造函數(shù)設(shè)為私有,阻止派生類實(shí)例化(因?yàn)榕缮悩?gòu)造時(shí)需要調(diào)用基類構(gòu)造函數(shù))。

2)派生類析構(gòu)時(shí)需要調(diào)用基類析構(gòu)函數(shù),私有化析構(gòu)函數(shù)可阻止繼承。

2.基類加一個(gè)final (C++11)

重載、覆蓋(重寫)、隱藏(重定義)的對比

抽象類
概念
在虛函數(shù)的后面寫上 =0 ,則這個(gè)函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也叫接口類),抽象類不能實(shí)例化出對象。派生類繼承后也不能實(shí)例化出對象,只有重寫純虛函數(shù),派生類才能實(shí)例化出對象。純虛函數(shù)規(guī)范了派生類必須重寫,另外純虛函數(shù)更體現(xiàn)出了接口繼承。
純虛函數(shù)和抽象類
func 為純虛函數(shù),所以 A 為抽象類。抽象類不允許實(shí)例化對象。
class A
{
public:
virtual void fun() = 0;
};雖然抽象類不可以實(shí)例化出對象,但是我們可以寫一個(gè)類來繼承它,并重寫里面的純虛函數(shù),這時(shí)這個(gè)子類是可以實(shí)例化出對象并可以調(diào)用重寫后的方法的。
class A
{
public:
virtual void fun() = 0;
};
class B:public A
{
public:
virtual void fun()
{
cout << " " << endl;
}
};多態(tài)的原理
練習(xí)
class A
{
public:
virtual void func(int val = 1) { cout << "A->" << val << endl; }
virtual void test() { func(); }//隱含this指針
};
class B : public A
{
public:
virtual void func(int val = 0) { cout << "B->" << val << endl; }
};
int main()
{
B* p = new B;
p->test();
return 0;
}- p 指針調(diào) test 方法的時(shí)候會去 A 類中調(diào)用,A 類的 test 方法中又調(diào)用了 func 方法,類的成員函數(shù)是有一個(gè)隱含的 this 指針的,這個(gè) func 方法就是通過這個(gè) this 指針調(diào)用的,這個(gè) this 指針類型是 A*(基類指針)func 方法是重寫虛函數(shù),滿足了多態(tài)的條件。
test()是從A繼承的,調(diào)用func()時(shí)使用A的默認(rèn)參數(shù)1- 但實(shí)際調(diào)用的是B類的
func()實(shí)現(xiàn),因?yàn)閜指向B對象

多態(tài)的條件
- 父類的指針和引用
- 虛函數(shù)的重寫
- 為什么不能是子類指針或引用,為什么不能是父類對象:
- 子類賦值給父類對象切片,不會拷貝虛表,如果拷貝虛表那么父類對象虛表中時(shí)父類虛函數(shù)還是子類就不確定了
- 派生類虛表先將父類拷貝一份再將修改的進(jìn)行覆蓋
多態(tài)的底層

虛函數(shù)表(vtable):
Student 對象會包含一個(gè)虛函數(shù)表,包含:
- 重寫的
BuyTicket()(半價(jià)版本) - 繼承的
Func1()、Func2() - 新增的
Func3()(應(yīng)該有的,編譯器優(yōu)化了)
訪問控制:
Func3()是private,無法通過基類指針調(diào)用,但仍在虛表中存在。(vs優(yōu)化了)_a在基類中是public(建議改為protected,避免直接暴露)。
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;
};
int main()
{
Base b;
Derive d;
return 0;
}1.派生類對象d中也有一個(gè)虛表指針,d對象由兩部分構(gòu)成,一部分是父類繼承下來的成員,虛表指針也就是存在部分的另一部分是自己的成員。
2. 基類b對象和派生類d對象虛表是不一樣的,這里我們發(fā)現(xiàn)Func1完成了重寫,所以d的虛表中存的是重寫的Derive::Func1,所以虛函數(shù)的重寫也叫作覆蓋,覆蓋就是指虛表中虛函數(shù)的覆蓋。重寫是語法的叫法,覆蓋是原理層的叫法。
3. 另外Func2繼承下來后是虛函數(shù),所以放進(jìn)了虛表,F(xiàn)unc3也繼承下來了,但是不是虛函 數(shù),所以不會放進(jìn)虛表。
4. 虛函數(shù)表本質(zhì)是一個(gè)存虛函數(shù)指針的指針數(shù)組,一般情況這個(gè)數(shù)組最后面放了一個(gè)nullptr。
5. 總結(jié)一下派生類的虛表生成:
- a.先將基類中的虛表內(nèi)容拷貝一份到派生類虛表中
- b.如果派生類重寫了基類中某個(gè)虛函數(shù),用派生類自己的虛函數(shù)覆蓋虛表中基類的虛函數(shù)
- c.派生類自己新增加的虛函數(shù)按其在派生類中的聲明次序增加到派生類虛表的最后。
6. 虛函數(shù)存在哪的?虛表存在哪的? 答:虛函數(shù)存在虛表,虛表存在對象中。
注意上面的回答的錯(cuò)的。虛表存的是虛函數(shù)指針,不是虛函數(shù),虛函數(shù)和普通函數(shù)一樣的,都是存在代碼段的,只是他的指針又存到了虛表中。另外對象中存的不是虛表,存的是虛表指針。vs下虛表存在代碼段
內(nèi)存分布
內(nèi)存問題(虛函數(shù)表指針)
- 虛函數(shù)表也簡稱虛表
- 虛函數(shù)本質(zhì)放在代碼段
- 虛表里存的是虛函數(shù)的地址

虛函數(shù)表指針 (vptr)
- 當(dāng)類包含虛函數(shù)時(shí),編譯器會自動(dòng)添加一個(gè) 虛函數(shù)表指針 (vptr),指向該類的虛函數(shù)表 (vtable)。
- 在 64 位系統(tǒng) 下,指針的大小是 8 字節(jié)。
- 因此,
Base類至少占用 8 字節(jié)(用于存儲vptr)。
成員變量 _b
_b是一個(gè)char類型,占用 1 字節(jié)。- 但由于 內(nèi)存對齊(alignment),編譯器會在
_b后面填充 7 字節(jié),使得vptr和_b整體對齊到 8 字節(jié) 邊界(優(yōu)化訪問速度)。
| 組成部分 | 大?。?4 位) | 大小(32 位) |
|---|---|---|
| 虛函數(shù)表指針 | 8 字節(jié) | 4 字節(jié) |
| char _b | 1 字節(jié) | 1 字節(jié) |
| 填充字節(jié) | 7 字節(jié) | 3 字節(jié) |
| 總大小 | 16 字節(jié) | 8 字節(jié) |
為什么不是9字節(jié)
- 如果
sizeof(Base)是 9 字節(jié)(vptr8 +_b1),那么當(dāng)Base對象存儲在數(shù)組中時(shí),第二個(gè)對象的vptr會錯(cuò)位(起始地址不是 8 的倍數(shù)),導(dǎo)致 性能下降 或 崩潰(某些 CPU 架構(gòu)要求指針地址對齊)。 - 因此,編譯器會自動(dòng)填充字節(jié),使類的大小是 最大對齊單位(8 字節(jié))的整數(shù)倍。

多態(tài)實(shí)現(xiàn)指向父類調(diào)父類指向子類調(diào)子類 :
指向父類在父類虛函數(shù)表中找到父類地址,找父類虛表
指向子類在子類虛函數(shù)表中找到子類地址(切片后看到的還是父類對象,但是是子類里的父類,他的虛表已經(jīng)被覆蓋了,找到的是子類地址)
- 函數(shù)調(diào)用棧幀才會去開空間
- 同類型對象共用虛表,虛表不在棧上
- 沒有獨(dú)立函數(shù)不建立自己的虛表
C++ 的多態(tài)機(jī)制、對象內(nèi)存布局和繼承關(guān)系
class Person {
public:
virtual void BuyTicket() { cout << "買票-全價(jià)" << endl; }
virtual void Func1()
{
cout << "Person::Func1()" << endl;
}
virtual void Func2()
{
cout << "Person::Func2()" << endl;
}
//protected:
int _a = 0;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "買票-半價(jià)" << endl; }
private:
virtual void Func3()
{
//_b++;
cout << "Student::Func3()" << endl;
}
protected:
int _b = 1;
};
void Func(Person& p)
{
p.BuyTicket();
}
void test()
{
Person ps1;
Student st1;
}
int main()
{
Person ps;
Student st;
st._a = 10;
ps = st;
Person* ptr = &st;
Person& ref = st;
test();
return 0;
}


由表可推斷虛表是儲存在常量區(qū)的。
x86環(huán)境運(yùn)行



多繼承
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
int main()
{
Derive d;
Base1* ptr1 = &d;
ptr1->func1();
Base2* ptr2 = &d;
ptr2->func1();
Derive* ptr3 = &d;
ptr3->func1();
return 0;
}對象內(nèi)存分布和虛函數(shù)表布局

base1
vfptr: 指向Derive類為Base1部分維護(hù)的虛表(地址0x00007ff7179bbd30)
[0]: 重寫的Derive::func1()(地址0x00007ff7179b1348)[1]: 繼承的Base1::func2()(地址0x00007ff7179b1334)
b1: 未初始化的int成員(值-858993460是Debug模式的填充值0xCCCCCCCC)
base2
vfptr: 指向Derive類為Base2部分維護(hù)的虛表(地址0x00007ff7179bbd38)
[0]: Thunk函數(shù)(地址0x00007ff7179b1230),用于調(diào)整this指針并跳轉(zhuǎn)到Derive::func1()[1]: 繼承的Base2::func2()(地址0x00007ff7179b10eb)
b2: 同樣未初始化的int成員
總結(jié):
多重繼承的虛表:
- 每個(gè)基類(
Base1/Base2)有獨(dú)立的虛表指針 Derive重寫的func1()在Base1虛表中直接替換,在Base2虛表中通過Thunk調(diào)用
Thunk函數(shù):
- 當(dāng)通過
Base2*調(diào)用func1()時(shí),需要調(diào)整this指針(指向Base2子對象的起始位置) - Thunk會先修正
this指針(減去Base2在Derive中的偏移量),再跳轉(zhuǎn)到Derive::func1()
未初始化值:
-858993460(即0xCCCCCCCC)是Debug模式的填充值,用于標(biāo)記未初始化的棧內(nèi)存
兩種多態(tài)
靜態(tài)(編譯時(shí))的多態(tài),函數(shù)重載
動(dòng)態(tài)(運(yùn)行時(shí))的多態(tài),繼承,虛函數(shù)重寫,實(shí)現(xiàn)的多態(tài)
int main()
{
//靜態(tài)多態(tài)(編譯時(shí)多態(tài))
int i = 1;
double d = 1.1;
cout << i << endl;
cout << d << endl;
//動(dòng)態(tài)多態(tài)(運(yùn)行時(shí)多態(tài))
Person ps;
Person* ptr = &ps;
ps.BuyTicket();
ptr->BuyTicket();
return 0;
}
靜態(tài)
實(shí)現(xiàn)方式:函數(shù)重載
operator<<針對不同的參數(shù)類型(int和double)有不同的實(shí)現(xiàn)- 在編譯時(shí)就能確定調(diào)用哪個(gè)版本的函數(shù)
特點(diǎn):
- 通過函數(shù)重載實(shí)現(xiàn)
- 編譯時(shí)確定具體調(diào)用哪個(gè)函數(shù)
- 不需要虛函數(shù)或繼承關(guān)系
- 效率高(無運(yùn)行時(shí)開銷)
動(dòng)態(tài)
實(shí)現(xiàn)方式:
- 繼承關(guān)系
- 虛函數(shù)重寫
- 通過基類指針或引用調(diào)用
特點(diǎn):
- 通過虛函數(shù)表(vtable)實(shí)現(xiàn)
- 運(yùn)行時(shí)確定調(diào)用哪個(gè)函數(shù)
- 需要繼承和虛函數(shù)
- 有一定的運(yùn)行時(shí)開銷(查虛函數(shù)表)
兩種多態(tài)的關(guān)鍵區(qū)別
| 特性 | 靜態(tài)多態(tài) | 動(dòng)態(tài)多態(tài) |
|---|---|---|
| 實(shí)現(xiàn)方式 | 函數(shù)重載、模板 | 虛函數(shù)、繼承 |
| 確定時(shí)機(jī) | 編譯時(shí) | 運(yùn)行時(shí) |
| 性能 | 高效(無額外開銷) | 有一定開銷(查虛函數(shù)表) |
| 靈活性 | 較低(編譯時(shí)確定) | 高(運(yùn)行時(shí)可改變行為) |
| 典型應(yīng)用 | 運(yùn)算符重載、函數(shù)重載 | 接口設(shè)計(jì)、多態(tài)對象處理 |
虛基表和虛函數(shù)表區(qū)別
| 特性 | 虛基表 (Virtual Base Table) | 虛函數(shù)表 (Virtual Function Table, vtable) |
|---|---|---|
| 用途 | 解決虛繼承中的共享基類偏移問題 | 實(shí)現(xiàn)運(yùn)行時(shí)多態(tài),管理虛函數(shù)調(diào)用 |
| 觸發(fā)條件 | 當(dāng)類使用virtual繼承時(shí)(如class D : virtual public B) | 當(dāng)類包含virtual成員函數(shù)時(shí) |
| 存儲內(nèi)容 | 存儲虛基類相對于當(dāng)前對象的偏移量 | 存儲虛函數(shù)的地址(指向?qū)嶋H實(shí)現(xiàn)的函數(shù)指針) |
| 指針名稱 | vbptr(虛基表指針) | vfptr(虛函數(shù)表指針) |
| 內(nèi)存位置 | 位于對象內(nèi)存布局的起始或相關(guān)位置 | 通常位于對象內(nèi)存布局的起始位置 |
| 編譯器生成邏輯 | 確保多個(gè)派生類共享同一虛基類實(shí)例時(shí)能正確訪問基類成員 | 確保通過基類指針/引用調(diào)用時(shí)能正確跳轉(zhuǎn)到派生類實(shí)現(xiàn) |
| 是否依賴運(yùn)行時(shí) | 是(運(yùn)行時(shí)計(jì)算偏移量) | 是(運(yùn)行時(shí)查表確定函數(shù)地址) |
| 典型場景 | 菱形繼承(如B ← D1 ← D和B ← D2 ← D,B為虛基類) | 基類定義虛函數(shù),派生類重寫(如Shape::draw()) |
| 訪問開銷 | 額外間接尋址(通過vbptr找到偏移量再訪問基類) | 一次指針解引用(通過vfptr跳轉(zhuǎn)到函數(shù)地址) |
| 調(diào)試查看方式 | 在調(diào)試器中觀察vbptr和偏移量 | 在調(diào)試器中觀察vfptr和函數(shù)地址列表 |
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
c語言實(shí)現(xiàn)數(shù)組循環(huán)左移m位
這篇文章主要介紹了c語言實(shí)現(xiàn)數(shù)組循環(huán)左移m位,具有很好的參考價(jià)值,希望對大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-07-07
C語言使用DP動(dòng)態(tài)規(guī)劃思想解最大K乘積與乘積最大問題
Dynamic Programming動(dòng)態(tài)規(guī)劃方法采用最優(yōu)原則來建立用于計(jì)算最優(yōu)解的遞歸式,并且考察每個(gè)最優(yōu)決策序列中是否包含一個(gè)最優(yōu)子序列,這里我們就來展示C語言使用DP動(dòng)態(tài)規(guī)劃思想解最大K乘積與乘積最大問題2016-06-06
QML中動(dòng)態(tài)與靜態(tài)模型應(yīng)用詳解
QML是一種描述性的腳本語言,文件格式以.qml結(jié)尾。語法格式非常像CSS(參考后文具體例子),但又支持javascript形式的編程控制。QtDesigner可以設(shè)計(jì)出·ui界面文件,但是不支持和Qt原生C++代碼的交互2022-08-08
C++在多線程中使用condition_variable實(shí)現(xiàn)wait
這篇文章主要介紹了C++中的condition_variable中在多線程中的使用,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)吧2022-09-09
Qt+FFMPEG實(shí)現(xiàn)循環(huán)解碼詳解

