【普通類/模板類的繼承 + 父類&子類的轉換 + 繼承的作用域 + 子類的默認構造函數】目錄
- 前言:
- ------------------------
- 一、繼承的定義和使用
- 1. 什么使繼承?
- 2. 為什么要引入繼承?
- 3. 怎么使用繼承?
- ① 父類(基類)
- ② 子類(派生類)
- ③ 繼承方式
- 4. 使用繼承需要注意什么?
- ------------------------
- 二、模板類的繼承
- 1. 模板類的繼承有哪些?
- ① 模板類繼承普通類
- ② 模板類繼承另一個模板類
- 2. 模板類之間的繼承需要注意什么?
- ------------------------
- 三、父類與子類之間的轉換
- 1. 什么是父類與子類之間的轉換?
- 2. 父類與子類之間的轉換有哪些類型?
- ① 向上轉型(子類轉父類)
- ② 向下轉型(父類轉子類)
- ------------------------
- 四、繼承中的作用域
- 1. 什么是隱藏?
- 2. 隱藏有哪些?
- ① 變量隱藏
- ② 函數隱藏
- 3. 關于繼承作用域的相關習題
- ------------------------
- 五、子類的默認成員函數
- 1. 默認構造函數
- ① 介紹
- ② 使用
- ③ 初始化
- ④ 禁用
- 2. 拷貝構造函數
- ① 介紹
- ② 使用
- ③ 禁用
- 3. 拷貝賦值運算符重載函數
- ① 介紹
- ② 使用
- ③ 禁用
- ④ 拷貝構造 vs 拷貝賦值
- 4. 析構函數
- ① 注意事項
- ② 析構順序
- 子類中常見的四大默認成員函數的大總結:
往期《C++初階》回顧:
《C++初階》目錄導航
前言:
hi~ 小伙伴們大家好呀(ノ′ヮ)ノ*: ・゚!今天可是末伏哦?(。???-)?
這意味著三伏天的前兩伏已經悄悄溜走啦🏃?♂?💨(°▽°)/,從今天起我們就正式進入最后一伏啦~ 末伏固定是 10 天📅(??????)??,也就是說,十天之后,夏天的暑氣就會慢慢消散咯(?′ω?)。
那今天呢,我們要開啟《C++ 進階》的第一課啦🎉(≧?≦)/:
【普通類 / 模板類的繼承 + 父類 & 子類的轉換 + 繼承的作用域 + 子類的默認成員函數】 📚💡(。・ω・。)ノ?
內容是關于 “繼承” 的知識點哦🧬,新的知識帶來新的開始?(ノ?ヮ?)ノ*:・゚?
讓我們一起加油學習吧🚀!沖鴨💪🔥(? ??_??)?
------------------------
一、繼承的定義和使用
1. 什么使繼承?
繼承(Inheritance)
:是面向對象編程(OOP)中的核心概念之一,它允許一個類(稱為子類或派生類)直接擁有另一個類(稱為父類或基類)的屬性和方法,并可以在此基礎上擴展新的功能或修改原有實現。
- 通過繼承,子類能復用父類的代碼,減少重復開發,同時形成類之間的層次關系,體現
“is-a”(屬于)
的邏輯關系。
2. 為什么要引入繼承?
現在大家試想一下,假如說現在要求你設計兩個類:學生類、教師類,具體需求如下:
- 學生類
- 成員變量:需包含學生的姓名、年齡、電話、地址、學號
- 成員函數:需實現學生的身份認證功能,以及進行學習的行為
- 教師類
- 成員變量:需包含教師的姓名、年齡、電話、地址、職稱
- 成員函數:需實現教師的身份認證功能,以及進行授課的行為
小伙伴們,面對這樣的需求,你會如何設計這兩個類呢?
我相信有不少的小伙伴們是像下面這樣進行設計的。
#include <iostream>
using namespace std;/*--------------------------定義“學生類”--------------------------*/
class Student
{
public:/*----------------成員函數(學生的行為)----------------*///1.實現:“身份的驗證邏輯”的函數void identity(){// 實現身份驗證邏輯(如:調用二維碼掃描接口)// ...}//2.實現:“進行學習”的函數void studing(){// 實現學習邏輯(如:記錄學習時間、課程等)// ...}protected:/*----------------成員變量(學生的屬性)----------------*/string _name;int _age;string _tel;string _address;int _stuid; // 學號(唯一標識學生身份)
};/*--------------------------定義“教師類”--------------------------*/
class Teacher
{
public:/*----------------成員函數(教師的行為)----------------*///1.實現:“身份的驗證邏輯”的函數void identity(){// 實現教師身份驗證邏輯// ...}//2.實現:“進行授課”的函數void teaching(){// 實現授課邏輯(如:記錄課程內容、學生出勤等)// ...}protected:/*----------------成員變量(教師的屬性)----------------*/string _name;int _age;string _tel;string _address;string _title; // 職稱(如"教授"、"副教授"等)
};int main()
{return 0;
}
從小伙伴們設計的
Student
(學生類)和Teacher
(教師類)可以發現,這兩個類存在不少共性:
- 都有姓名、年齡、電話、地址這些成員變量
- 也都有
identity
(身份認證)這樣的成員函數而這些共性內容在兩個類里重復定義,造成了代碼冗余。
同時,它們也存在各自的差異:
- 成員變量方面,學生獨有 “學號”,老師獨有 “職稱”。
- 成員函數方面,學生有專屬的 “學習” 函數,老師有專屬的 “授課” 函數 。
所以:我們自然會想到一個問題:就是有沒有一種方法,既能避免上述代碼冗余問題,又能兼容它們各自的差異呢?
有,哈哈,沒錯這種方法就是繼承
我們可以將兩個類的公共成員(如:姓名、年齡、電話、地址、
identity
身份認證函數等)提取到一個基類Person
(人)中然后讓
Student
(學生類)和Teacher
(教師類)繼承Person
這樣一來,它們既能復用基類的公共成員,又能各自添加獨有的成員變量(如:學號、職稱)和成員函數(如:學習、授課),完美解決代碼冗余問題。
#include <iostream>
#include <string>
using namespace std;/*--------------------------定義“基類:人類”--------------------------*/
class Person
{
public:/*----------------成員函數(人的行為)----------------*///1.實現:“身份的驗證邏輯”的函數void identity(){cout << "void identity()"<< endl;}protected:/*----------------成員函數(人的屬性)----------------*/string _name;int _age;string _tel;string _address;
};/*--------------------------定義“派生類:學生類”--------------------------*/
class Student : public Person
{
public:/*----------------成員函數(學生的行為)----------------*///1.實現:“進行學習”的函數void studing(){// 實現學習邏輯(如:記錄學習時間、課程等)// ...}protected:/*----------------成員變量(學生的屬性)----------------*/int _stuid; // 學號(唯一標識學生身份)
};/*--------------------------定義“派生類:教師類”--------------------------*/
class Teacher : public Person
{
public:/*----------------成員函數(教師的行為)----------------*///1.實現:“進行授課”的函數void teaching(){// 實現授課邏輯(如:記錄課程內容、學生出勤等)// ...}protected:/*----------------成員變量(教師的屬性)----------------*/string _title; // 職稱(如"教授"、"副教授"等)
};int main()
{//1.創建學生和教師對象Student s;Teacher t;//2.調用繼承自Person類的身份認證方法s.identity();t.identity();return 0;
}
3. 怎么使用繼承?
想要使用繼承的話,我們首先要知道繼承的格式是什么樣的?
① 父類(基類)
父類(基類)
:被繼承的類,包含子類共有的屬性和方法。
- 例如:定義 “動物” 類作為父類,包含 “呼吸”“移動” 等通用方法。
② 子類(派生類)
子類(派生類)
:繼承父類的類,除了擁有父類的成員,還可以添加新成員或重寫父類方法。
- 例如:“狗” 類作為子類繼承 “動物” 類,新增 “吠叫” 方法,并重寫 “移動” 方法(如:“四條腿跑”)
③ 繼承方式
繼承的類型
- 公有繼承:子類保留父類成員的訪問權限(public 成員在子類中仍為 public,protected 仍為 protected)
- 保護繼承:父類的 public 和 protected 成員在子類中變為 protected
- 私有繼承:父類的 public 和 protected 成員在子類中變為 private(子類內部可訪問,但無法繼續繼承)
4. 使用繼承需要注意什么?
基類中的私有成員,無論派生類以何種方式繼承,在派生類中都是不可見的。
- 這里的
“不可見”
指的是:基類私有成員雖然會被繼承到派生類對象的內存空間中,但語法上禁止派生類在類內或類外訪問它們。
另外需要注意:
使用關鍵字
class
定義派生類時,默認繼承方式為private
使用
struct
時,默認繼承方式為public
不過,為了代碼清晰性,最好顯式寫出繼承方式。
注意:在實際開發中,通常只使用
public繼承
,幾乎不推薦使用protected
或private
繼承。
(。・ω・。)ノ? 看到上面的內容,估計大家都會有點懵吧~(???ω???)💦
畢竟我們之前學的訪問權限,一旦和繼承關聯起來,就會變得異常復雜(這可以說是 C++ 設計中不太好的一點啦),超級容易踩坑(╥﹏╥)!不過沒關系喲~ 博主經過無數次踩坑,還綜合了很多人的經驗,總結出了下面的表格,希望這次能一次性解決大家的心頭之痛呀~(≧?≦)ノ?
如果能幫到你,那博主真的會超開心的哦(。???。)?, (?′ω`?) 偷偷舉手~要是能順便關注博主一下,那就更更更開心啦!
------------------------
二、模板類的繼承
前面我們看到的繼承都是
普通類的繼承
,其實繼承也可以是模板類的繼承
,那如何實現模板類的繼承呢?
1. 模板類的繼承有哪些?
模板類的繼承的基本形式:
模板類繼承普通類
模板類繼承另一個模板類
① 模板類繼承普通類
代碼示例:實現“模板類繼承普通類”
#include <iostream>
using namespace std;class Base
{
public:void commonMethod(){cout << "Base method" << endl;}
};
template <typename T>
class Derived : public Base // 類模板繼承普通類
{
public:T value;void templateMethod(){cout << "Template method: " << value << endl;}
};int main()
{// 使用示例Derived<int> d;d.commonMethod(); // 繼承自Based.templateMethod(); // 模板特有的方法return 0;
}
② 模板類繼承另一個模板類
代碼示例:實現“模板類繼承另一個模板類”
#include <iostream>
#include <string>
using namespace std;template <typename T>
class BaseTemplate
{
public:T data;void print(){cout << "Base data: " << data << endl;}
};template <typename T, typename U>
class DerivedTemplate : public BaseTemplate<T> // 繼承時需指定Base的模板參數
{
public:U extraData;void extendedPrint(){cout << "Derived data: " << this->data << ", " << extraData << endl;}
};int main()
{// 使用示例DerivedTemplate<int, string> dt;dt.data = 42; // 繼承自BaseTemplatedt.extraData = "text";// 派生模板特有的屬性dt.print(); // 繼承自BaseTemplatedt.extendedPrint(); // 派生模板特有的方法return 0;
}
2. 模板類之間的繼承需要注意什么?
1. 繼承時需顯式指定基類模板的參數
派生模板必須為基類模板提供類型參數(如:
BaseTemplate<T>
),可以是:
派生模板自身的類型參數
(如:T
)具體類型
(如:int
)其他模板參數
(如:U
)示例:
template <typename T> class Base {};template <typename T, typename U> class Derived : public Base<T>// 使用派生模板的參數T { // ... };template <typename T> class Derived2 : public Base<int> // 使用具體類型 { // ... };
2. 基類模板的成員訪問
派生模板中訪問基類模板的成員時,需通過this->或顯式指定類域,避免編譯錯誤。(如:
Base<T>::member
)template <typename T> class Base { public:T value; };template <typename T> class Derived : public Base<T> { public:void setValue(const T& val) {this->value = val; // 必須使用this->或Base<T>::value} };
代碼示例:實現“stack模板類繼承vector類模板”
#include <iostream>
#include <vector>
using namespace std;namespace mySpace
{//stack類模板:基于vector實現的適配器容器template<class T>class stack : public std::vector<T>{public://1.實現“入棧操作:將元素添加到棧頂”void push(const T& x){ vector<T>::push_back(x); //注意:我們這里繼承的是“類模板”,這里一定要顯示的指定類域// 等價于:this->push_back(x); 或 std::vector<T>::push_back(x);/* 注意事項:* 基類是類模板時,需顯式指定類域(vector<T>::)* 原因:模板實例化是"按需進行"的* 當stack<int>實例化時,vector<int>的框架被實例化* 但vector<int>的成員函數(如:push_back)尚未實例化* 因此直接調用push_back會導致編譯錯誤(找不到標識符)*/}//2.實現:“出棧操作:移除棧頂元素”void pop(){vector<T>::pop_back();}//3.實現:“獲取棧頂元素引用”(只讀)const T& top(){return vector<T>::back();}//4.實現:“判斷棧是否為空”bool empty(){return vector<T>::empty();}};
}int main()
{//1.創建一個存儲int類型的棧對象mySpace::stack<int> stk;//2.壓入元素:1, 2, 3(棧頂為3)stk.push(1);stk.push(2);stk.push(3);//3.后進先出(LIFO)順序彈出元素while (!stk.empty()){cout << stk.top() << " ";stk.pop();}return 0;
}
------------------------
三、父類與子類之間的轉換
1. 什么是父類與子類之間的轉換?
在 C++ 的繼承體系中,父類(基類)與子類(派生類)之間的轉換:是指不同類型對象或指針、引用之間的賦值或強制類型轉換,其核心遵循 賦值兼容規則(Liskov 替換原則)
- 父類(基類)與子類(派生類)之間的轉換是面向對象編程中處理類繼承關系的重要機制,主要涉及
指針
、引用
和對象
三種形式的轉換。
2. 父類與子類之間的轉換有哪些類型?
根據轉換方向可以分為兩類:
向上轉型(子類轉父類)
和向下轉型(父類轉子類)
,二者的規則和使用場景差異顯著。
① 向上轉型(子類轉父類)
向上轉型(子類轉父類)
:將子類
對象或指針/引用轉換為父類
對象或指針/引用特點:
隱式轉換
:向上轉型是隱式的,不需要顯式地進行類型轉換,因為子類對象在邏輯上是父類對象的一種特例。
安全轉換
:向上轉型總是安全的,因為子類對象包含父類的所有屬性和方法。
向上轉型之對象轉換:對象直接賦值
public繼承的子類,若直接使用子類對象給父類對象賦值,子類中獨有的成員會被 “截斷”,僅保留從父類繼承的部分,這種情況的我們稱為是 對象切片
示例:
class Parent { int x; };class Child : public Parent { int y; };Parent p; Child c; p = c; // 切片:c 的 y 成員被丟棄,p 僅保留從 Parent 繼承的 x
向上轉型之指針/引用轉換:子類指針/引用可直接賦值給父類指針/引用,指向子類對象的父類部分。
示例:
Child c;Parent* p = &c; // 合法,p 指向 c 的父類部分 Parent& ref = c; // 合法,ref 是 c 的父類部分的別名
② 向下轉型(父類轉子類)
向下轉型(父類轉子類)
:將父類
對象或指針/引用轉換為子類
對象或指針/引用特點:
顯式轉換
:向下轉型是顯式的,無法自動轉換,需使用static_cast
、dynamic_cast
等強制類型轉換運算符,因為父類對象可能不包含子類的特有屬性和方法。存在風險
:向下轉型可能會導致運行時錯誤,因為父類對象可能不包含子類的特有成員。
向下轉型的兩種方式:
static_cast
(非多態場景):用于非多態類型的轉換,編譯期完成,不檢查轉換的有效性。Parent* p = new Child(); // 父類指針指向子類對象 Child* c1 = static_cast<Child*>(p); // 合法(正確轉換)// 危險!需確保父類指針實際指向子類對象 Parent p_obj; Child* c2 = static_cast<Child*>(&p_obj); // 非法(p_obj 是純父類對象,轉換后訪問子類成員會崩潰)
dynamic_cast
(多態場景):用于多態類型(父類含虛函數),運行時檢查轉換的有效性。
- 若轉換成功,返回子類指針/引用。
- 若失敗,指針返回
nullptr
,引用拋出std::bad_cast
異常。class Parent {virtual void func() {} // 含虛函數,支持多態 };class Child : public Parent {};Parent* p = new Child(); Child* c = dynamic_cast<Child*>(p); // 成功,c 非空Parent p_obj; Child& ref = dynamic_cast<Child&>(p_obj); // 拋出 std::bad_cast 異常(p_obj 非子類對象)
代碼示例:向下轉型(父類轉子類)的錯誤使用案例
#include <iostream>
using namespace std;class Person
{
protected:string _name; string _sex; int _age;
};class Student : public Person
{
public:int _No; // 學號(Student獨有成員)
};int main()
{Student sobj; // 創建子類對象//1.子類對象可以賦值給父類的指針/引用(向上轉型,合法)Person* pp = &sobj; // 父類指針指向子類對象(指向子類中的父類部分)Person& rp = sobj; // 父類引用綁定到子類對象(引用子類中的父類部分)Person pobj = sobj; // 子類對象賦值給父類對象(發生對象切片,僅復制父類部分)// 注意:此處通過調用Person類的拷貝構造函數完成賦值,// 僅復制_name、_sex、_age,Student的_No成員被截斷//2. 父類對象不能賦值給子類對象(向下轉型,非法)sobj = pobj; // 編譯錯誤!父類對象無法自動轉換為子類對象// 原因:父類對象不包含子類的獨有成員(如:_No),// 若允許賦值,會導致子類的_No成員未被初始化return 0;
}
------------------------
四、繼承中的作用域
1. 什么是隱藏?
隱藏
:是指 派生類中的同名成員(函數
或變量
)覆蓋了基類中的同名成員 ,導致基類成員在派生類作用域內不可直接訪問的現象。
2. 隱藏有哪些?
隱藏規則主要分為以下兩種情況:
- 變量隱藏:派生類變量覆蓋基類變量
- 函數隱藏:派生類函數覆蓋基類同名函數
① 變量隱藏
變量隱藏
:當派生類定義了與基類同名的變量時,基類變量會被隱藏,派生類對象默認訪問自身的變量。
- 需要注意的是只需要變量名相同就可以構成隱藏。
代碼示例1:變量隱藏
#include <iostream>
using namespace std;class Base
{
protected:int x = 10;
};class Derived : public Base
{
private:int x = 20; // 隱藏基類的x
public:void print(){cout <<"x=" << x << endl; // 輸出20(派生類的x)cout << "Base::x=" << Base::x; // 顯式訪問基類的x(輸出10)}
};int main()
{Derived d;d.print(); return 0;
}
代碼示例2:變量隱藏
#include <iostream>
using namespace std;/*-----------------------定義基類:“Person類”表示通用的個人信息-----------------------*/class Person
{
protected:string _name = "張三"; // 姓名int _num = 111; // 身份證號(基類成員)
};/*-----------------------定義派生類:“Student類”繼承自Person,新增學號信息-----------------------*/class Student : public Person
{
public:void Print(){//1.訪問從Person繼承的姓名cout << "姓名:" << _name << endl;//2.由于“身份證號”被同名的“學號”隱藏了,所以通過類域顯式指定,訪問基類的_num(身份證號)cout << "身份證號:" << Person::_num << endl;//3.直接訪問_num,默認使用派生類隱藏的成員(學號)cout << "學號:" << _num << endl;}
protected:int _num = 999; // 學號,注意:這里派生類的成員變量和基類的成員變量“同名”了(所以:派生類成員,隱藏基類的_num)
};int main()
{Student s1;s1.Print();return 0;
}
② 函數隱藏
函數隱藏
:當派生類定義了與基類同名但參數列表不同的函數時,基類的所有同名函數會被隱藏,即使參數不同也無法直接調用。
- 需要注意的是只需要函數名相同就可以構成隱藏。
#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:void func(double x) // 隱藏基類的func()和func(int){cout << "Derived::func(double)" << endl;}
};int main()
{Derived d;d.func(3.14); // 合法,調用Derived::func(double)//d.func(); // 錯誤!基類的func()被隱藏d.func(10); // 錯誤!基類的func(int)被隱藏d.Base::func(); // 合法,顯式調用基類函數return 0;
}
3. 關于繼承作用域的相關習題
#include <iostream>
using namespace std;/*-----------------------定義基類:“A類”-----------------------*/
class A
{
public:void fun(){cout << "A::func()" << endl; }
};/*-----------------------定義派生類:“B類”-----------------------*/
class B : public A
{
public:void fun(int i) {cout << "B::func(int i): " << i << endl;}
};int main()
{B b;b.fun(10); b.fun(); return 0;
}
問題 1:A 類和 B 類中的兩個
func
構成什么關系?( )A. 重載 B. 隱藏 C. 沒關系
問題 2:上面的代碼編譯運行結果是什么?( )
A. 編譯報錯 B. 運行報錯 C. 正常運行
答案【B. 隱藏】
分析:
我相信一定會有一部分的小伙伴們回選擇A.重載,他們應該是這么想的:A類中的fun函數是:
void fun()
,B類中的fun函數是:void fun(int i)
,咦……這兩個函數不是滿足函數的重載的要求嘛,ok這道題就選A選項了。一對答案,啊,這道題為什么選B. 隱藏 啊!!!
解析:
基類
A
的fun()
與派生類B
的fun(int)
同名且參數列表不同(標準的函數重載的要求),但是這兩個函數位于不同作用域(基類與派生類)重載要求函數在同一作用域內,而此處屬于繼承體系中的跨作用域同名函數,因此不構成重載
在 C++ 中,派生類的同名函數會隱藏基類的所有同名函數(無論參數是否相同),這種現象稱為隱藏(Name Hiding)
答案【A. 編譯報錯】
解析:
- 在
main
函數中,b.fun();
會觸發編譯錯誤
- 派生類
B
的fun(int)
隱藏了基類A
的fun()
,編譯器在B
的作用域內找不到無參的fun()
,因此報錯。- 若想調用基類的
fun()
,需顯式指定類域:b.A::fun();
------------------------
五、子類的默認成員函數
在 C++ 中,當定義一個子類(派生類)時,編譯器會自動生成以下默認成員函數(與基類的默認成員函數行為相關)
- 這些默認函數的生成規則和行為與基類的構造函數、析構函數、賦值運算符等密切相關。
子類會繼承基類的成員
變量
和函數
,但 不會繼承基類的構造函數、析構函數和賦值運算符。編譯器會為子類自動生成以下默認成員函數(若未手動定義):
默認構造函數
析構函數
拷貝構造函數
拷貝賦值運算符重載函數
移動構造函數
(C++11 新增)移動賦值運算符重載函數
(C++11 新增)
1. 默認構造函數
① 介紹
在 C++ 中,子類的默認構造函數的行為與普通類有所不同,因為它需要正確處理基類子對象和成員變量的初始化
子類的默認構造函數
:當子類沒有顯式定義任何構造函數
時,編譯器會自動生成一個隱式的默認構造函數。子類的默認構造函數的主要職責是:
- 調用基類的默認構造函數
- 初始化子類的成員變量(調用它們的默認構造函數)
② 使用
情況一:
- 父類中有默認構造函數
- 如果子類沒有顯式定義構造函數
編譯器會生成一個默認構造函數。
#include <iostream>
#include <string>
using namespace std;/*---------------------------定義:“基類:Base類”---------------------------*/
class Base
{
public:Base() // 基類有默認構造函數{cout << "Base()\n";}
};/*---------------------------定義:“派生類:Derived類”---------------------------*/
class Derived : public Base
{
public:int x; // 內置類型不初始化string s; // 成員有默認構造函數// 編譯器自動生成:Derived() : Base(), s() {}
};int main()
{Derived d; // 輸出 Base(),x是未定義值,s為空字符串
}
情況二:
- 父類中沒有默認構造函數(如:只有帶參構造函數)
- 如果子類沒有顯式定義構造函數
編譯器會生成一個默認構造函數,但是:子類的默認構造函數會編譯失敗,需
手動定義子類構造函數
并在初始化列表中顯式調用基類構造函數
#include <iostream>
#include <string>
using namespace std;/*---------------------------定義:“基類:Base類”---------------------------*/
class Base
{
public:Base(int val){std::cout << "Base(int)\n";}
};/*---------------------------定義:“派生類:Derived類”---------------------------*/
class Derived : public Base
{
public:Derived() : Base(42) // 必須顯式調用基類構造函數{}
};int main()
{Derived d; // 輸出 Base(int)
}
③ 初始化
子類自身成員的初始化
內置類型成員
:不會自動初始化(如:int、指針,其值未定義)類類型成員
:調用其默認構造函數(如:std::string
會初始化為空字符串)
#include <iostream>
#include <string>
using namespace std;/*---------------------------定義:“基類:Base類”---------------------------*/
class Base
{
public:Base(int val){cout << "Base(int)\n";}
};/*---------------------------定義:“派生類:Derived類”---------------------------*/
class Derived : public Base
{
public:int x = 10; // C++11 成員默認值Derived() : Base(42), x(5) // 構造函數初始化列表優先{}
};int main()
{Derived d; // 輸出 Base(int)
}
④ 禁用
如果希望禁止子類的默認構造,可以:
- 將基類或子類的默認構造函數聲明為
= delete
- 將基類構造函數設為
private
/*------------------------禁用案例1:聲明為=delete------------------------*/#include <iostream>
#include <string>
using namespace std;class Base
{
public:Base() = delete; // 禁用默認構造
};class Derived : public Base
{
public:Derived() // 錯誤:無法調用 Base(){}
};int main()
{Derived d; // 輸出 Base(int)
}/*------------------------禁用案例2:修改為private------------------------*/#include <iostream>
#include <string>
using namespace std;class Base
{
private:Base(){} // 禁用默認構造
};class Derived : public Base
{
public:};int main()
{Derived d; // 輸出 Base(int)
}
2. 拷貝構造函數
① 介紹
在 C++ 中,子類的拷貝構造函數的行為與普通類不同,因為它需要正確處理基類部分和派生類新增成員的拷貝
子類的默認拷貝構造函數
:如果子類未定義拷貝構造函數
,編譯器會生成一個隱式的拷貝構造函數。子類的默認拷貝構造函數的行為是:
- 調用基類的拷貝構造函數(拷貝基類部分)
- 對派生類的新增成員逐成員拷貝(調用各自的拷貝構造函數)
#include <iostream>
#include <string>
using namespace std;class Base
{
public:Base() {}Base(const Base&){cout << "Base拷貝構造\n";}
};class Derived : public Base
{
public:string s; // 類類型成員// 編譯器生成://1.子類的默認構造函數//Derived() {}//2.子類的拷貝構造函數//Derived(const Derived& other)// : Base(other)// , s(other.s) //{}
};int main()
{Derived d1;Derived d2 = d1; // 輸出:Base拷貝構造return 0;
}
② 使用
注意一:
子類拷貝構造函數的基本形式:子類的拷貝構造函數必須顯式調用基類的拷貝構造函數,否則基類部分將被默認構造而非拷貝構造
#include <iostream>
using namespace std; class Base
{
public:Base() // 默認構造函數(無參){cout << "Base默認構造\n"; }Base(const Base& other) // 拷貝構造函數(參數為基類對象的引用){cout << "Base拷貝構造\n"; }
};class Derived : public Base
{
public:Derived() // 子類默認構造函數(無參){cout << "Derived默認構造\n"; //注意:隱式調用基類的默認構造函數Base()}Derived(const Derived& other) // 子類拷貝構造函數(參數為子類對象的引用): Base(other) {cout << "Derived拷貝構造\n"; //注意:顯式調用基類的拷貝構造函數,傳入子類對象other(向上轉型為Base&)}
};int main()
{Derived d1; //創建Derived對象d1,觸發以下構造順序:// 1.調用基類Base的默認構造函數 → 輸出"Base默認構造"// 2.調用子類Derived的默認構造函數 → 輸出"Derived默認構造"Derived d2(d1); //使用d1拷貝構造d2,觸發以下構造順序:// 1.調用基類Base的拷貝構造函數(傳入d1的Base部分)→ 輸出"Base拷貝構造"// 2.調用子類Derived的拷貝構造函數 → 輸出"Derived拷貝構造"return 0;
}
注意二:
深拷貝與淺拷貝問題:當派生類包含指針成員時,需手動實現深拷貝
#include <iostream>
using namespace std;class Base
{
public:int* data;//1.實現:“默認構造函數”Base() : data(new int(0)) //注意:初始化data指針并分配內存{}//2.實現:“拷貝構造函數”(實現深拷貝)Base(const Base& other) : data(new int(*other.data)) //注意:為新對象分配獨立內存并復制原對象的值{}//3.實現:“析構函數”~Base(){delete data; //釋放動態分配的內存}
};class Derived : public Base
{
public:int* more_data;//1.實現:“默認構造函數”Derived() : more_data(new int(0)) //注意:初始化基類部分并為more_data分配內存{}//2.實現:“拷貝構造函數”(實現深拷貝)Derived(const Derived& other): Base(other),more_data(new int(*other.more_data)){}//注意事項:// 1.調用基類拷貝構造函數處理基類部分// 2.為more_data分配新內存并復制原對象的值//3.實現:“析構函數”~Derived() //注意:基類析構函數會被自動調用{delete more_data; //釋放派生類部分的動態內存}
};int main()
{/*--------------測試準備階段:創建一個派生類的對象d1--------------*///1.創建第一個派生類對象Derived d1;//2.設置基類部分的值*d1.data = 10;//3.設置派生類擴展部分的值*d1.more_data = 20;/*--------------測試準備階段:使用拷貝構造函數創建第二個派生類的對象d2--------------*/Derived d2(d1); //期望d2是d1的獨立副本/*--------------驗證拷貝結果:輸出d2的值--------------*/cout << "d2.data: " << *d2.data << endl; cout << "d2.more_data: " << *d2.more_data << endl; /*--------------驗證拷貝結果:修改原始對象的值--------------*/*d1.data = 100; //注意:深拷貝保證這不會影響d2*d1.more_data = 200; //注意:深拷貝保證這不會影響d2/*--------------驗證深拷貝:d2的值應保持不變--------------*/cout << "After modification:" << endl;cout << "d2.data: " << *d2.data << endl; cout << "d2.more_data: " << *d2.more_data << endl; // 對象離開作用域時,析構函數會自動釋放內存// 不會發生內存泄漏return 0;
}
③ 禁用
必須顯式調用基類拷貝構造的情況:
- 如果基類的拷貝構造函數不可訪問(如:
= delete
或private
)- 子類無法生成或使用隱式拷貝構造
注意:若基類沒有可訪問的拷貝構造函數(如:
刪除
或私有
),子類的默認拷貝構造函數會被隱式刪除。
/*------------------------禁用案例1:刪除------------------------*/#include <iostream>
using namespace std;class Base
{
public:Base() //默認的構造函數{}Base(const Base&) = delete; // 禁止拷貝
};class Derived : public Base
{
public:Derived(){}//Derived(const Derived&) // 錯誤:無法調用基類拷貝構造//{}
};int main()
{Derived d1;Derived d2(d1); // 編譯錯誤:嘗試使用已刪除的拷貝構造函數cout << "拷貝構造已被禁用,無法創建對象副本。" << endl;return 0;
}/*------------------------禁用案例2:私有------------------------*/#include <iostream>
using namespace std;class Base
{
public:Base() //默認的構造函數{}
private:Base(const Base&) // 禁止拷貝{}
};class Derived : public Base
{
public:Derived(){}//Derived(const Derived&) // 錯誤:無法調用基類拷貝構造//{}
};int main()
{Derived d1;Derived d2(d1); // 編譯錯誤:嘗試使用已刪除的拷貝構造函數cout << "拷貝構造已被禁用,無法創建對象副本。" << endl;return 0;
}
3. 拷貝賦值運算符重載函數
① 介紹
在 C++ 中,子類的拷貝賦值運算符 的行為比普通類更復雜,因為它需要正確處理基類部分和派生類新增成員的賦值
子類的默認拷貝賦值運算符重載函數
:如果子類沒有自定義operator=
,編譯器會生成一個隱式的拷貝賦值運算符重載函數。子類的拷貝賦值運算符重載函數的行為是:
- 調用基類的operator=(賦值基類部分)
- 對派生類的新增成員逐成員賦值(調用各自的 operator=)
#include <iostream>
#include <string>
using namespace std;/*---------------------定義:“基類:Base類”---------------------*/
class Base
{
public://1.實現:“拷貝賦值運算符”Base& operator=(const Base&) {cout << "Base::operator=\n"; // 打印賦值操作信息return *this; // 返回當前對象的引用}
};/*---------------------定義:“派生類:Derived類”---------------------*/
class Derived : public Base
{string s; // 類類型成員變量//注意:編譯器會自動生成如下拷貝賦值運算符:// Derived& operator=(const Derived& other) // {// Base::operator=(other); // 調用基類的拷貝賦值// // s = other.s; // 拷貝派生類成員// return *this; // 返回當前對象引用// }
};int main()
{//1.創建兩個Derived對象Derived d1, d2; //2.執行拷貝賦值操作d1 = d2; //將調用編譯器自動生成的拷貝賦值運算符return 0;
}
② 使用
注意一:
子類拷貝賦值運算符的基本形式:子類的operator= 必須 顯式調用基類的operator=,否則基類部分不會被正確賦值(只會被默認構造,而不會拷貝)
- 原因:派生類的
operator=
會隱藏基類的operator=
- 因此:若要在派生類中顯式調用基類的
operator=
,需要通過指定基類作用域來實現
#include <iostream>
#include <string>
using namespace std;/*---------------------定義:“基類:Base類”---------------------*/
class Base
{
public://1.實現:基類的“拷貝賦值運算符”Base& operator=(const Base& other) {cout << "Base::operator=\n"; // 打印調試信息return *this; // 返回當前對象的引用}
};/*---------------------定義:“派生類:Derived類”---------------------*/
/*
* 任務:
* 1.這個類展示了派生類中如何正確實現拷貝賦值運算符,
* 2.必須顯式調用基類的拷貝賦值運算符,
* 3.否則基類部分不會被賦值。
*/
class Derived : public Base
{
public:/** 實現要點:* 1. 必須顯式調用基類的operator=* 2. 然后處理派生類特有的成員拷貝* 3. 返回*this以支持鏈式賦值*///1.實現:派生類的“拷貝賦值運算符”Derived& operator=(const Derived& other) {cout << "開始Derived拷貝賦值操作...\n";//1.首先調用基類的拷貝賦值運算符Base::operator=(other); //注意:必須顯式調用基類賦值//2.然后處理派生類特有的成員賦值// 如果有成員變量需要拷貝,應該在這里處理// 例如:this->member = other.member;cout << "完成Derived特有成員的賦值\n";//3.返回當前對象的引用return *this; }
};int main()
{//1.創建兩個派生類的對象cout << "創建Derived對象d1和d2...\n";Derived d1, d2;//2.進行派生類的賦值操作cout << "\n執行拷貝賦值操作(d1 = d2)...\n";d1 = d2; //調用Derived::operator=cout << "\n程序正常結束\n";return 0;
}
注意二:
深拷貝與淺拷貝問題:如果派生類包含 指針或動態資源,必須手動管理深拷貝
#include <iostream>
using namespace std;/*---------------------定義:“基類:Base類”---------------------*/
class Base
{
public:int* data;//1.實現:“默認構造函數”---> 初始化并分配內存Base() : data(new int(0)) {}//2.實現:“深拷貝賦值運算符”---> 防止內存泄漏和懸掛指針Base& operator=(const Base& other) {//1.防止自賦值(如 a = a)if (this != &other) { //2.釋放當前資源delete data; //3.創建新資源并復制值data = new int(*other.data); }return *this; // 返回引用以支持鏈式賦值(如:a = b = c)}//3.實現:“析構函數”---> 釋放動態分配的內存~Base() { delete data; }
};/*---------------------定義:“派生類:Derived類”---------------------*/
class Derived : public Base
{
public:int* more_data;//1.實現:“默認構造函數”---> 初始化基類和派生類資源Derived() : more_data(new int(0)) {}//2.實現:“深拷貝賦值運算符”---> 先處理基類部分,再處理自身Derived& operator=(const Derived& other) {//1.防止自賦值if (this != &other) { //2.調用基類的賦值運算符處理基類資源Base::operator=(other); //3.釋放當前派生類資源delete more_data; //4.創建新資源并復制值more_data = new int(*other.more_data); }return *this; // 返回引用以支持鏈式賦值}//3.實現:“析構函數”---> 釋放派生類資源(基類析構函數自動調用)~Derived() { delete more_data; }
};int main()
{//1.創建兩個派生類的對象并進行賦值操作Derived d1, d2;*d1.data = 10; // 設置基類部分的值*d1.more_data = 20; // 設置派生類部分的值//2.調用派生類的賦值運算符d2 = d1; //3.輸出“修改前”派生類對象d2的成員變量的值cout << "修改前 d2.data: " << *d2.data << endl; cout << "修改前 d2.more_data: " << *d2.more_data << endl; //4.修改派生類的對象d1,驗證深拷貝:不會影響 d2*d1.data = 100;*d1.more_data = 200;//5.輸出“修改后”派生類對象d2的成員變量的值cout << "修改后 d2.data: " << *d2.data << endl; // 輸出: 10cout << "修改后 d2.more_data: " << *d2.more_data << endl; // 輸出: 20return 0;
}
③ 禁用
必須顯式調用基類
operator=
的情況
- 如果基類的
operator=
不可訪問(如:= delete
或private
)- 子類無法使用隱式拷貝賦值運算符
/*------------------------禁用案例1:刪除------------------------*/#include <iostream>
using namespace std; /*---------------------定義:“基類:Base類”---------------------*/
class Base
{
public:Base& operator=(const Base&) = delete; // C++11特性:顯式刪除拷貝賦值,通過將拷貝賦值運算符聲明為delete來禁止拷貝賦值操作/** 效果:* 1.禁止Base類的拷貝賦值(Base b1; Base b2; b1 = b2; 會編譯失敗)* 2.任何嘗試繼承Base并實現拷貝賦值的派生類也會失敗*/
};/*---------------------定義:“派生類:Derived類”---------------------*/
class Derived : public Base //注意:派生類Derived,繼承自不可拷貝賦值的Base,由于基類禁止拷貝賦值,這個類也無法實現有效的拷貝賦值運算符
{
public://嘗試實現拷貝賦值運算符會導致編譯錯誤:Derived& operator=(const Derived& other) {//無法調用Base::operator=,因為基類的拷貝賦值已被刪除return *this;}
};int main()
{cout << "創建Base對象..." << endl;Base b1;Base b2;// 以下代碼如果取消注釋會導致編譯錯誤://b1 = b2; // 錯誤:Base::operator=已被刪除cout << "創建Derived對象..." << endl;Derived d1;Derived d2;// 以下代碼如果取消注釋會導致編譯錯誤:d1 = d2; //注意:無法使用隱式生成的拷貝賦值運算符return 0;
}/*----------------------------禁用案例1:私有----------------------------*/#include <iostream>
using namespace std; /*---------------------定義:“基類:Base類”---------------------*/
class Base
{
private:Base& operator=(const Base&){return *this;}};/*---------------------定義:“派生類:Derived類”---------------------*/
class Derived : public Base
{
public:Derived& operator=(const Derived& other) {//無法調用Base::operator=,因為基類的拷貝賦值已被私有return *this;}
};int main()
{cout << "創建Base對象..." << endl;Base b1;Base b2;b1 = b2; cout << "創建Derived對象..." << endl;Derived d1;Derived d2;// 以下代碼如果取消注釋會導致編譯錯誤:d1 = d2; //注意:無法使用隱式生成的拷貝賦值運算符return 0;
}
④ 拷貝構造 vs 拷貝賦值
特性 | 拷貝構造函數 | 拷貝賦值運算符 |
---|---|---|
調用時機 | 創建新對象時(如 Derived d2 = d1; ) | 已存在對象賦值時(如 d2 = d1; ) |
基類處理 | 在初始化列表調用基類拷貝構造 | 顯式調用 Base::operator= |
資源管理 | 直接構造新資源 | 需先釋放舊資源,再分配新資源 |
自賦值檢查 | 不需要(因為是新對象) | 必須檢查(if (this != &other) ) |
4. 析構函數
在 C++ 中,子類的析構函數 的行為與普通析構函數有所不同,因為它涉及繼承關系和多態銷毀的問題
子類的默認析構函數
:如果子類沒有顯式定義析構函數
,編譯器會自動生成一個 隱式的析構函數
① 注意事項
當基類指針刪除派生類對象時,如果基類析構函數 不是 virtual,通過基類指針刪除派生類對象會導致 未定義行為
- 通常只調用基類析構函數,派生類部分內存泄漏。
注意:基類析構函數必須為 virtual(多態場景)
#include <iostream>
using namespace std; class Base
{
public:~Base(){cout << "~Base()\n";}/** 任務:演示非虛析構函數的問題* * 1.這個類展示了一個關鍵問題:當基類析構函數不是虛函數時* 2.通過基類指針刪除派生類對象會導致派生類析構函數不被調用*/
};class Derived : public Base
{
public:~Derived(){cout << "~Derived()\n";}/** 注意事項:** 1.如果通過基類指針刪除對象,且基類析構函數不是虛函數,* 2.這個析構函數將不會被調用,導致派生類特有的資源泄漏。*/
};int main()
{//1.創建派生類對象,但通過基類指針持有Base* ptr = new Derived();//2.刪除對象 delete ptr; // 僅輸出 ~Base(),Derived 部分泄漏!return 0;
}
解決方案:基類虛析構
思考與探究:
現在的大家可以試著想一想:
為什么通過基類指針刪除派生類對象會導致派生類析構函數不被調用呢?
原因:在多態場景中,析構函數需要構成
重寫
(覆蓋),而重寫的條件之一是函數名相同(這一點將在多態章節中詳細講解)所以:編譯器會對析構函數名進行特殊處理(統一處理為
destructor()
),因此若基類析構函數未聲明為virtual
的情況下,派生類析構函數與基類析構函數會構成隱藏
關系(而非重寫)
② 析構順序
析構函數的調用順序(與構造函數相反):
- 先執行 子類的析構函數體
- 然后按 聲明順序逆序 析構子類的成員變量
- 最后自動調用 基類的析構函數(從最底層派生類向頂層基類回溯)
注:派生類的析構函數在執行完畢后,會自動調用基類的析構函數來清理基類成員。
這是為了確保派生類對象遵循
“先清理派生類成員,再清理基類成員”
的順序。
#include <iostream>
using namespace std; class Member
{
public:~Member() //注意:當包含它的類對象被銷毀時自動調用{cout << "~Member()\n";}
};class Base
{
public:virtual ~Base(){cout << "~Base()\n";}
};/** 注意事項:* 1.派生類 Derived,繼承自 Base* 2.包含 Member 成員對象,演示完整析構順序*/
class Derived : public Base
{
public://1.Member的成員對象Member m; //2.派生類的析構函數~Derived() {cout << "~Derived()\n";}
};int main()
{Derived d;// 對象離開作用域時,自動析構順序:// 1. 調用 ~Derived() 函數體// 2. 析構成員對象 m(調用 ~Member())(成員逆序析構)// 3. 調用基類析構函數 ~Base()return 0;
}
子類中常見的四大默認成員函數的大總結:
#include <iostream>
#include <string>
using namespace std;class Person
{
public://1.實現:“構造函數”---> 初始化 _name,默認值為 "peter"Person(const char* name = "peter"): _name(name){cout << "Person()" << endl;}//2.實現:“拷貝構造函數”Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}//3.實現:“賦值運算符重載”Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p){_name = p._name;}return *this;}//4.實現:“析構函數”~Person(){cout << "~Person()" << endl;}protected:string _name; // 姓名
};class Student : public Person
{
public://1.實現:“構造函數”---> 調用基類構造函數初始化 _name,再初始化 _numStudent(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}//2.實現:“拷貝構造函數”---> 調用基類拷貝構造函數,再拷貝 _numStudent(const Student& s): Person(s), _num(s._num){cout << "Student(const Student& s)" << endl;}//3.實現:“賦值運算符重載”Student& operator=(const Student& s){cout << "Student& operator=(const Student& s)" << endl;if (this != &s){Person::operator=(s); //注意:基類的賦值運算符被隱藏,需顯式調用基類作用域的賦值運算符_num = s._num;}return *this;}//4.實現:“析構函數”~Student(){cout << "~Student()" << endl;}protected:int _num; // 學號
};int main()
{/*--------------測試1:子類的拷貝構造函數--------------*/cout << "-------測試1:子類的拷貝構造函數-------" << endl;cout << "創建第一個 Student 對象 s1" << endl;Student s1("jack", 18);cout << "使用拷貝構造創建 s2" << endl;Student s2(s1);/*--------------測試2:子類的拷貝賦值運算符重載函數--------------*/cout << "-------測試2:子類的拷貝賦值運算符重載函數-------" << endl;cout << "創建第三個 Student 對象 s3" << endl;//3. Student s3("rose", 17);cout << "使用拷貝賦值運算符重載函數為對象s1進行賦值" << endl;//4. s1 = s3;return 0;
}