C++ 繼承從語法陷阱到內(nèi)存底層的終極復(fù)習(xí)
C++繼承全解——從基礎(chǔ)語法到對(duì)象模型暗坑全吃透
你以為繼承只是簡(jiǎn)單的代碼復(fù)用?
當(dāng)你遇到對(duì)象切片、名字隱藏、菱形繼承數(shù)據(jù)冗余、虛基表編譯報(bào)錯(cuò)、多態(tài)析構(gòu)內(nèi)存泄漏時(shí),才會(huì)明白:
C++ 繼承的本質(zhì),是對(duì)對(duì)象模型、內(nèi)存布局、訪問控制的終極考驗(yàn)。
本文帶你從零開始,吃透 C++ 繼承的所有語法、底層原理與暗坑。
一、繼承的本質(zhì) —— 從代碼冗余到層次化復(fù)用
1.1 為什么需要繼承?
描述 Student 和 Teacher 時(shí),都會(huì)用到姓名、年齡等公共屬性。不使用繼承,會(huì)出現(xiàn)大量重復(fù)代碼,維護(hù)成本極高。
無繼承:代碼冗余,難以維護(hù)
#include <iostream>
#include <string>
using namespace std;
class Student {
public:
string name;
int age;
string studentId;
void study() { cout << name << " 正在學(xué)習(xí)" << endl; }
};
class Teacher {
public:
string name;
int age;
string course;
void teach() { cout << name << " 正在授課" << endl; }
};繼承的解決方案:
把公共成員抽取到基類(父類),Student/Teacher 作為**派生類(子類)**復(fù)用基類成員。
// 繼承:代碼復(fù)用 + 層次清晰
class Person {
public:
string name;
int age;
};
// 派生類 : 繼承方式 基類
class Student : public Person {
public:
string studentId;
void study() { cout << name << " 正在學(xué)習(xí)" << endl; }
};
class Teacher : public Person {
public:
string course;
void teach() { cout << name << " 正在授課" << endl; }
};1.2 繼承基本語法
class 派生類名 : 繼承方式 基類名 {
// 派生類新增成員
};
二、訪問控制 —— 權(quán)限的「木桶效應(yīng)」
繼承的核心難點(diǎn):基類成員在派生類中的最終權(quán)限,不是繼承方式說了算,也不是原權(quán)限說了算,而是取更小的那個(gè)。
2.1 核心公式(背會(huì)這一條就夠)
最終權(quán)限 = min(基類成員權(quán)限, 繼承方式)
權(quán)限強(qiáng)度:public > protected > private
| 繼承方式 | 基類 public | 基類 protected | 基類 private |
|---|---|---|---|
| public | public | protected | 不可訪問 |
| protected | protected | protected | 不可訪問 |
| private | private | private | 不可訪問 |
兩個(gè)關(guān)鍵理解【面試高頻】
- 不可訪問 ≠ 不存在
基類private成員依然會(huì)被派生類對(duì)象包含(占內(nèi)存),只是語法上禁止直接訪問。 - protected 是為繼承而生
private:自己能用,子類不能用;protected:自己能用,子類能用,外部不能用。
2.2 默認(rèn)繼承規(guī)則
class派生:默認(rèn) private 繼承struct派生:默認(rèn) public 繼承
2.3 可運(yùn)行代碼示例
#include <iostream>
using namespace std;
class Base {
public:
int pub = 1;
protected:
int pro = 2;
private:
int pri = 3; // 子類不可直接訪問
};
// public 繼承
class PubDerive : public Base {
public:
void test() {
cout << pub << endl; // ok
cout << pro << endl; // ok
// cout << pri << endl; // error
}
};
// protected 繼承
class ProDerive : protected Base {
public:
void test() {
cout << pub << endl; // ok,內(nèi)部可訪問
cout << pro << endl; // ok
}
};
// private 繼承
class PriDerive : private Base {
public:
void test() {
cout << pub << endl; // ok,內(nèi)部可訪問
cout << pro << endl; // ok
}
};
int main() {
PubDerive p;
p.pub; // ok
// p.pro; // error
ProDerive pr;
// pr.pub; // error,外部無法訪問
PriDerive pv;
// pv.pub; // error,外部無法訪問
return 0;
}三、內(nèi)存視角:對(duì)象切片(最容易踩的暗坑)
3.1 向上轉(zhuǎn)型
派生類對(duì)象可以賦值/初始化給:
- 基類對(duì)象
- 基類指針
- 基類引用
這叫向上轉(zhuǎn)型(安全、隱式)。
#include <string>
using namespace std;
class Person {
public:
int _age;
string _name;
};
class Student : public Person {
public:
string _studentId;
};
int main() {
Student s;
// 賦值給基類對(duì)象 → 發(fā)生切片
Person p = s;
// 指針/引用 → 不切片
Person* pp = &s;
Person& rp = s;
return 0;
}3.2 底層真相:切片是什么?
派生類對(duì)象內(nèi)存布局:
基類子對(duì)象在前,派生類新增成員在后
Student:[ Person(_name, _age) ][ _studentId ]
Person p = s:只拷貝基類部分,切掉子類獨(dú)有成員 → 對(duì)象切片- 切片后,
p徹底變成Person,無法訪問子類任何成員
【致命暗坑】
多態(tài)必須用指針/引用,絕對(duì)不能用對(duì)象賦值,否則切片會(huì)破壞多態(tài)。
四、作用域隱藏:同名就藏,不看參數(shù)
4.1 核心規(guī)則
基類和派生類是兩個(gè)獨(dú)立作用域。
只要名字相同,派生類成員會(huì)隱藏基類所有同名成員。
?? 不是重載!不是覆蓋!是隱藏!
- 重載要求:同一作用域 + 同名不同參;
- 隱藏要求:不同作用域 + 同名即可。
4.2 變量隱藏
#include <iostream>
using namespace std;
class Base {
public:
int num = 100;
};
class Derived : public Base {
public:
int num = 200; // 隱藏基類 num
void test() {
cout << num << endl; // 200(優(yōu)先子類)
cout << Base::num << endl; // 100(指定作用域)
}
};
int main() {
Derived d;
d.test();
return 0;
}4.3 函數(shù)隱藏
只要函數(shù)名相同,無論參數(shù)是否一致,基類所有重載函數(shù)都會(huì)被隱藏。
#include <iostream>
using namespace std;
class Base {
public:
void func() { cout << "Base func" << endl; }
void func(int x) { cout << "Base func(int)" << endl; }
};
class Derived : public Base {
public:
// 隱藏基類兩個(gè) func
void func() { cout << "Derived func" << endl; }
};
int main() {
Derived d;
d.func(); // ok
// d.func(10); // error!基類func(int)被隱藏
d.Base::func(10); // ok,指定作用域
return 0;
}【面試題】
基類有多個(gè)重載函數(shù)fun(),子類只要定義一個(gè)同名函數(shù),基類所有重載版本全部被隱藏。
4.4 隱藏 / 重寫 / 重載 極簡(jiǎn)對(duì)比
| 特性 | 重載 | 隱藏 | 重寫(多態(tài)) |
|---|---|---|---|
| 作用域 | 同一作用域 | 不同作用域 | 不同作用域 |
| 函數(shù)名 | 相同 | 相同 | 相同 |
| 參數(shù) | 不同 | 可同可不同 | 必須相同 |
| virtual | 不需要 | 不需要 | 必須有 |
| 綁定類型 | 靜態(tài)綁定 | 靜態(tài)綁定 | 動(dòng)態(tài)綁定 |
五、派生類生命周期:構(gòu)造 & 析構(gòu)
5.1 構(gòu)造函數(shù)規(guī)則
- 派生類必須初始化基類子對(duì)象
- 若不顯示調(diào)用基類構(gòu)造,編譯器會(huì)隱式調(diào)用基類默認(rèn)構(gòu)造
- 基類無默認(rèn)構(gòu)造 → 直接編譯報(bào)錯(cuò)
#include <string>
using namespace std;
class Person {
public:
// 無默認(rèn)構(gòu)造函數(shù)
Person(const string& name) : _name(name) {}
private:
string _name;
};
class Student : public Person {
public:
// 正確:必須顯式調(diào)用基類構(gòu)造
Student(const string& name, const string& id)
: Person(name), _id(id) {}
private:
string _id;
};【暗坑】
基類構(gòu)造調(diào)用順序 = 繼承列表順序,與初始化列表順序無關(guān)。
5.2 析構(gòu)函數(shù):內(nèi)存泄漏的重災(zāi)區(qū)
- 調(diào)用順序:先析構(gòu)子類 → 再析構(gòu)父類
- 普通析構(gòu):不構(gòu)成多態(tài),基類指針刪除子類對(duì)象 → 只調(diào)用基類析構(gòu) → 內(nèi)存泄漏
錯(cuò)誤示例
#include <iostream>
using namespace std;
class Base {
public:
~Base() { cout << "基類析構(gòu)" << endl; }
};
class Derived : public Base {
public:
~Derived() { cout << "子類析構(gòu)" << endl; }
};
int main() {
Base* ptr = new Derived;
delete ptr;
// 輸出:僅 基類析構(gòu) → 子類資源泄漏!
return 0;
}正確寫法
基類析構(gòu)函數(shù)必須加 virtual
class Base {
public:
virtual ~Base() { cout << "基類析構(gòu)" << endl; }
};【鐵律】
一個(gè)類只要可能被繼承,析構(gòu)函數(shù)就必須是 virtual。
六、多繼承與菱形繼承:底層災(zāi)難與虛繼承
6.1 多繼承語法
class Derived : public Base1, public Base2 {};
6.2 菱形繼承的模型圖
Person
/ \
Student Teacher
\ /
Assistant
#include <string>
using namespace std;
class Person { public: string _name; };
class Student : public Person {};
class Teacher : public Person {};
class Assistant : public Student, public Teacher {};兩大致命問題
- 數(shù)據(jù)冗余:
Assistant包含兩份Person - 訪問二義性:
a._name編譯報(bào)錯(cuò),不知道用哪一份
6.3 虛繼承:解決菱形繼承
在中間派生類加上 virtual:
class Student : virtual public Person {};
class Teacher : virtual public Person {};
class Assistant : public Student, public Teacher {};
底層原理
- 增加 vbptr(虛基表指針)
- 指向 vbtable(虛基表)
- 表中存放:當(dāng)前指針到唯一虛基類子對(duì)象的偏移
最終:整個(gè)繼承體系中只有一份 Person
工程建議
盡量避免菱形繼承,邏輯復(fù)雜、性能略低、可讀性差。
七、類模板繼承
7.1.類模板繼承需要指定類域
我們以stack繼承vector為例子
代碼實(shí)現(xiàn):
#include<iostream>
#include<vector>
using namespace std;
template<class T>
class Mystack:pubilc vector<T>{
void push(const T& data){
//push_back(data) error:找不到push_back,因?yàn)榫幾g器在沒有實(shí)例化之前是不會(huì)進(jìn)入到類模板中的,即訪問不到vector內(nèi)部的函數(shù)和成員,需要指定類域,要求編譯器去vector里面尋找
this->push_back(data);
//或
vector<T>::push_back(data)
}
};
7.2.模板與虛函數(shù)
模板函數(shù)是不允許繼承的,因?yàn)樘摵瘮?shù)在繼承之前就需要計(jì)算虛函數(shù)表的大小,而模板導(dǎo)致無法計(jì)算
八、繼承 vs 組合
8.1.定義
- 繼承(is-a):Derived是一種Base,例如狗是一種動(dòng)物
- 組合(has-a):Class包含另一個(gè)類作為成員,例如車有引擎
8.2.優(yōu)先使用組合
- 白箱 vs 黑箱:繼承是一種白箱調(diào)用,派生類能夠訪問基類的public成員,甚至protected成員,導(dǎo)致基類的實(shí)現(xiàn)細(xì)節(jié)暴露,基類一變動(dòng),可能影響所有的派生類,而組合是一種黑箱調(diào)用,只能通過特點(diǎn)的函數(shù)接口訪問成員變量,屏蔽了底層細(xì)節(jié),耦合度較低
- 繼承存在濫用:某些類的關(guān)系可能并不符合is-a的情況,比如stack和vector,stack并不支持insert等等接口,可能會(huì)破壞類的特點(diǎn)
九、如何終結(jié)繼承
有些類可能并不期望被繼承,那么應(yīng)該怎么做呢?
9.1.C++11方案:final
在不期望被繼承的類和函數(shù)后面加上final關(guān)鍵字
class Nonderived final{
//
}
// class Bad : public NonDerived {}; // ? 編譯錯(cuò)誤
final也能修飾函數(shù),讓派生類無法重寫
示例代碼:
class Base {
public:
virtual void show() final { // 虛函數(shù) + final
cout << "Base" << endl;
}
};
class Derived : public Base {
public:
// 錯(cuò)誤!無法重寫被 final 修飾的虛函數(shù)
void show() override {
cout << "Derived" << endl;
}
};9.2.C++98方案:構(gòu)造函數(shù)私有
原理:派生類在構(gòu)造時(shí)必定要調(diào)用基類的構(gòu)造函數(shù),基類的構(gòu)造函數(shù)私有化就導(dǎo)致派生類構(gòu)造不了,這樣也就沒辦法繼承了,不如final直觀,較為麻煩
class Base{
private:
Base(){}
public:
//讓外面能夠?qū)嵗?
static Base* create(){
return new Base();
}
}
// Base b; error
// Base d = Base::create();ok
// class D = Base(); error結(jié)語:給讀者的總結(jié)
繼承不只是把代碼抄一遍,它涉及到了:
- 作用域屏蔽(隱藏規(guī)則)
- 構(gòu)造析構(gòu)順序(先父后子,相反析構(gòu))
- 內(nèi)存布局與切片(對(duì)象賦值丟失派生部分)
- 多態(tài)與虛表(virtual 的神奇之處)
- 菱形繼承與虛基表(偏移量尋址)
- 模板繼承中的查找問題(依賴型名稱)
掌握了這些底層原理,你才能在面對(duì)C++復(fù)雜的對(duì)象模型時(shí)游刃有余,寫出健壯、高效、易維護(hù)的面向?qū)ο蟪绦颍?/p>
到此這篇關(guān)于C++ 繼承從語法陷阱到內(nèi)存底層的終極復(fù)習(xí)的文章就介紹到這了,更多相關(guān)C++ 繼承語法內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語言編程中實(shí)現(xiàn)二分查找的簡(jiǎn)單入門實(shí)例
這篇文章主要介紹了C語言編程中實(shí)現(xiàn)二分查找的簡(jiǎn)單入門實(shí)例,需要的朋友可以參考下2015-12-12
C++實(shí)現(xiàn)掃雷小游戲(控制臺(tái)版)
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)控制臺(tái)版的掃雷小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-03-03
c語言printf實(shí)現(xiàn)同一位置打印輸出的實(shí)例
下面小編就為大家?guī)硪黄猚語言printf實(shí)現(xiàn)同一位置打印輸出的實(shí)例。小編覺得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2017-09-09
opencv實(shí)現(xiàn)機(jī)器視覺檢測(cè)和計(jì)數(shù)的方法
在機(jī)器視覺中,有時(shí)需要對(duì)產(chǎn)品進(jìn)行檢測(cè)和計(jì)數(shù)。其難點(diǎn)無非是對(duì)于產(chǎn)品的圖像分割。本文就來介紹一下機(jī)器視覺檢測(cè)和計(jì)數(shù)的實(shí)現(xiàn),感興趣的可以參考一下2021-05-05
深入剖析C語言中qsort函數(shù)的實(shí)現(xiàn)原理
這篇文章主要介紹了C語言中qsort函數(shù)的實(shí)現(xiàn)原理,本文將從回調(diào)函數(shù),qsort函數(shù)的應(yīng)用,qsort函數(shù)的實(shí)現(xiàn)原理三個(gè)方面進(jìn)行講解,并通過代碼示例講解的非常詳細(xì),需要的朋友可以參考下2024-03-03
C語言實(shí)現(xiàn)簡(jiǎn)單航班管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語言實(shí)現(xiàn)簡(jiǎn)單航班管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2019-12-12
C++ DLL動(dòng)態(tài)庫的創(chuàng)建與調(diào)用(類庫,隱式調(diào)用)
本文主要介紹了C++ DLL動(dòng)態(tài)庫的創(chuàng)建與調(diào)用(類庫,隱式調(diào)用),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-05-05
C++11 模板參數(shù)的“右值引用”是轉(zhuǎn)發(fā)引用嗎
這篇文章主要介紹了C++11 模板參數(shù)的“右值引用”是轉(zhuǎn)發(fā)引用嗎,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-05-05

