繼承
一、繼承概述
1、為什么需要繼承
如下示例,Person 類、Student 類、Teacher 類有大量重復的代碼,造成代碼冗余,降低開發效率。
我們可以通過繼承來解決這一問題。在面向對象的編程語言中,繼承是一個核心概念。主要作用將重復的代碼統一定義在父類中,子類從父類繼承,同時繼承也是實現多態的重要條件。
2、什么是繼承
繼承就是一個新類從現有類派生的過程。新類稱之為派生類或子類,原有的類稱之為基類或父類;子類可以繼承父類中的成員,從而可以提高代碼的可重用性。
繼承關系下,子類和父類存在 is a 的 關系。例如,狗是動物,貓是動物,老虎是一個動物等等。那么可以說動物類是一個父類,老虎、貓、狗都是動物類的子類。
在繼承關系下父類更通用,子類更具體。也就是說父類擁有子類的共同特性,子類可以具備獨有的特性。
二、繼承的實現
C++ 中類實現繼承的形式如下:
class 派生類名:[繼承方式]基類名 //默認是private繼承方式
{
}
繼承方式有 3 種類型,分別為共有型 (public),保護型 (protected) 和私有型 (private);: 表示基類和派生類之間的繼承關系的符號。
示例:
Person 類
Person 類作為父類,其包含了 public 修飾的屬性:
#pragma once
#include <string>
class Person
{
public:
std::string name;
int age;
};
Student 類
繼承了 Person 類,子類從父類繼承 public 成員
Student.h
#pragma once
#include "Person.h"
class Student: public Person
public:
void show();
};
Student.cpp
#include "Student.h"
#include <iostream>
using namespace std;
void Student::show()
{
cout << "name:" << name << endl;
cout << "age:" << age << endl;
}
Main.cpp
#include <iostream>
#include "Student.h"
int main()
Student s;
s.name = "張三"; //從父類繼承的成員
s.age = 20;
s.show();
}
三、派生類的訪問控制
在 C++ 中,類成員的訪問權限分為 public (公共)、protected (受保護) 或 private (私有) 3 種。其中父類的 public 和 protected 成員允許子類繼承,private 成員不能被繼承。
以 public 繼承模式為例,訪問控制權限如下:
訪問 | public | protected | private |
---|---|---|---|
同一個類 | yes | yes | yes |
派生類 | yes | yes | no |
外部的類 | yes | no | no |
類 A 的定義:
#pragma once
class A
{
public:
int num_public; //公有的成員任何類都可以訪問
protected:
int num_protected; //受保護的成員可以在當前類和子類中訪問
private:
int num_private; //私有的成員只能在當前類中訪問
};
類 B 繼承于類 A:
#pragma once
#include "A.h"
class B: public A
public:
B();
B(int a, int b);
void print();
};
類 B 中訪問父類中的成員:
#include "B.h"
#include <iostream>
using namespace std;
B::B() {}
B::B(int a, int b)
{
this->num_public = a;
this->num_protected = b;
}
void B::print()
{
cout << "public:" << num_public << endl;
cout << "protected:" << num_protected << endl;
//cout << "private:" << num_private << endl; //編譯錯誤,私有成員,不能被子類繼承
}
四、繼承類型
C++ 支持三種繼承類型,分別是 public、protected 及 private 類型,這些繼承類型影響著基類成員在派生類中的訪問權限。
1、訪問權限變化總覽
基類成員權限 public繼承 protected繼承 private繼承
---------------------------------------------------------------------------
public成員 ───? public(外部可訪問) protected(外部不可) private(外部不可)
protected成員 ───? protected(外部不可) protected(外部不可) private(外部不可)
private成員 ───? 不可訪問 不可訪問 不可訪問
- 繼承方式只會影響基類 public / protected 成員在派生類中的可見性,不會影響派生類對自己新成員的訪問控制。
private
成員無論哪種繼承方式,子類都不能直接訪問。
直觀理解:
- public繼承:原汁原味 —— public 還是 public,protected 還是 protected。
- protected繼承:降一級 —— public 變 protected,protected 不變。
- private繼承:全收進屋 —— public 和 protected 全變 private。
2、基類定義
#pragma once
#include <iostream>
using namespace std;class Base
{
public:void func_public() { cout << "Base::func_public()" << endl; }protected:void func_protected() { cout << "Base::func_protected()" << endl; }private:void func_private() { cout << "Base::func_private()" << endl; }
};
3、公有繼承 (Public Inheritance)
規則
- 基類
public
成員 → 派生類public
- 基類
protected
成員 → 派生類protected
- 外部依然可以訪問繼承的
public
成員
代碼示例
SubPublic.h
#pragma once
#include "Base.h"
class SubPublic : public Base
{
public:void func();
};
SubPublic.cpp
#include "SubPublic.h"
#include <iostream>
using namespace std;void SubPublic::func()
{cout << "[public繼承] 子類內部可以訪問父類 public + protected 成員" << endl;func_public(); // ?func_protected(); // ?
}
測試
SubPublic pub;
pub.func();
pub.func_public(); // ? 外部可訪問
4、私有繼承 (Private Inheritance)
規則
- 基類
public
成員 → 派生類private
- 基類
protected
成員 → 派生類private
- 外部無法訪問這些繼承的成員
代碼示例
SubPrivate.h
#pragma once
#include "Base.h"class SubPrivate : private Base
{
public:void func();
};
SubPrivate.cpp
#include "SubPrivate.h"
#include <iostream>
using namespace std;void SubPrivate::func()
{cout << "[private繼承] 子類內部可以訪問父類 public + protected 成員" << endl;func_public(); // ?func_protected(); // ?
}
測試
SubPrivate pri;
pri.func();
// pri.func_public(); // ? 外部不可訪問
5、保護繼承 (Protected Inheritance)
規則
- 基類
public
成員 → 派生類protected
- 基類
protected
成員 → 派生類protected
- 外部無法直接訪問,但派生類的子類可以訪問
代碼示例
SubProtected.h
pragma once
#include "Base.h"class SubProtected : protected Base
{
public:void func();
};
SubProtected.cpp
#include "SubProtected.h"
#include <iostream>
using namespace std;void SubProtected::func()
{cout << "[protected繼承] 子類內部可以訪問父類 public + protected 成員" << endl;func_public(); // ?func_protected(); // ?
}
6、保護繼承的子類
Subclass.h
#pragma once
#include "SubProtected.h"class Subclass : public SubProtected
{
public:void test();
};
Subclass.cpp
#include "Subclass.h"
#include <iostream>
using namespace std;void Subclass::test()
{cout << "[保護繼承的子類] 仍然可以訪問父類的 public + protected 成員" << endl;func_public(); // ?func_protected(); // ?
}
測試
Subclass subc;
subc.test();
// subc.func_public(); // ? 外部不可訪問
五、繼承中的構造函數與析構函數
基類中的構造函數、析構函數和拷貝構造函數不能被派生類繼承。
1、構造函數和析構函數的執行順序
繼承關系下:
- 當派生類對象被創建時,先調用基類的構造函數,然后再調用派生類的構造函數。
- 析構時順序相反,先調用派生類的析構函數,再調用基類的析構函數。
- 基類的構造函數、析構函數以及拷貝構造函數不會被繼承到派生類。
#include <iostream>
using namespace std;class A
{
public:A(){cout << "A類構造函數" << endl;}~A(){cout << "A類析構函數" << endl;}
};class B : public A
{
public:B(){cout << "B類構造函數" << endl;}~B(){cout << "B類析構函數" << endl;}
};class C : public B
{
public:C(){cout << "C類構造函數" << endl;}~C(){cout << "C類析構函數" << endl;}
};int main()
{C c; // 創建C類對象return 0;
}
程序輸出
A類構造函數
B類構造函數
C類構造函數C類析構函數
B類析構函數
A類析構函數
- 當你定義(創建)一個對象時,系統會自動調用該對象所屬類的構造函數,用來完成對象的初始化。
- 當對象的生命周期結束時,系統會自動調用對應類的析構函數,用來完成清理工作(比如釋放內存、關閉文件等)。
2、子類中調用父類構造
- 當基類只提供帶參數的構造函數且沒有無參構造函數時,派生類必須在其初始化列表中顯式調用基類的有參構造函數,否則編譯會報錯。
- 如果基類有無參構造函數,則派生類會默認調用基類的無參構造函數。
基類 Person
示例
Person.h
#pragma once
#include <string>class Person
{
public:Person(std::string name);std::string getName();private:std::string name;
};
Person.cpp
#include "Person.h"Person::Person(std::string name) : name(name)
{
}std::string Person::getName()
{return name;
}
派生類 Student
示例
Student.h
#pragma once
#include "Person.h"class Student : public Person
{
public:Student();Student(std::string name);
};
Student.cpp
#include "Student.h"// 當基類無默認構造時,派生類必須顯示調用基類有參構造函數
Student::Student() : Person("")
{
}Student::Student(std::string name) : Person(name)
{
}
測試 main.cpp
#include <iostream>
#include "Student.h"
using namespace std;int main()
{Student s("張三");cout << s.getName() << endl; // 輸出:張三return 0;
}
Student::Student() : Person("") { }
這是 Student
的無參構造函數,寫法表示:
- 當創建
Student
對象時,先調用基類Person
的構造函數,傳入空字符串""
初始化Person
部分。 - 然后執行
Student
自己的構造函數體(這里為空)。
Student::Student(std::string name) : Person(name) { }
這是帶參數的構造函數,表示:
- 創建
Student
對象時,先調用基類Person
的構造函數,傳入參數name
。 - 然后執行
Student
自己的構造函數體(這里為空)。
如果基類沒有無參構造函數,編譯器就不知道用什么參數去初始化基類部分,編譯會失敗。 所以派生類構造函數中必須用初始化列表顯示調用基類構造函數,告訴它該怎么初始化基類。
3、調用順序原因
一、構造函數調用順序:先基類后派生類
- 當你創建一個派生類對象時,派生類通常會用到基類的成員(包括數據和方法)。
- 如果基類還沒初始化,派生類就無法安全使用基類的內容。
- 所以必須先調用基類的構造函數,完成基類部分的初始化,再調用派生類構造函數來初始化派生類自己新增的成員。
這樣做保證了派生類擁有一個“完整且有效”的基類部分,避免使用未初始化數據帶來的錯誤。
二、析構函數調用順序:先派生類后基類
- 對象銷毀時,派生類先清理自己新增的資源(比如動態申請的內存、打開的文件等)。
- 清理完派生類資源后,再去銷毀基類成員。
- 如果先銷毀基類,派生類成員還沒清理完,就會出現訪問已銷毀資源的錯誤。
所以析構時先調用派生類析構函數釋放派生類資源,再調用基類析構函數釋放基類資源,符合“從內到外”的釋放原則。
三、簡單比喻 ---- 把對象想象成建房子
構造(建房子):
蓋房子的時候,先打好地基(基類),確保基礎穩固,
然后再蓋樓層(派生類),一層一層往上建。
先有地基,樓層才能安全搭建。
析構(拆房子):
拆房子時,先拆樓層(派生類),再拆地基(基類),
這樣避免樓層倒塌砸到地基,也保證拆除順序安全有序。