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

