7、類
7.1、類聲明
前置聲明:聲明一個將稍后在此作用域定義的類類型。直到定義出現前,此類名具有不完整類型。當代碼僅僅需要用到類的指針或引用時,就可以采用前置聲明,無需包含完整的類定義。
前置聲明有以下幾個作用:
- 降低編譯時的依賴性,加快編譯速度
- 避免出現循環引用
// 前置聲明
class MyClass;void func(MyClass* obj); // 可以使用指針
void func2(const MyClass& obj); // 可以使用引用
MyClass* createObj(); // 返回指針或引用是可行的
使用前置聲明也會有一些限制,除了不能訪問類成員、不能創建類對象、不能使用類的大小信息外,還不能繼承自該類。
7.2、局部類
局部類(Local Class)是指定義在函數內部的類,其作用域僅限于該函數。它們無法被函數外部訪問。局部類可以訪問函數的靜態變量、外部全局變量和枚舉。局部類的非靜態成員函數不能訪問外部函數的非靜態局部變量。
int globalVar = 100;void func() {static int staticVar = 200;int localVar = 300; // 局部變量(非靜態)class Local {public:void print() {std::cout << globalVar << std::endl; // 合法:訪問全局變量std::cout << staticVar << std::endl; // 合法:訪問靜態變量// std::cout << localVar << std::endl; // 錯誤:無法訪問非靜態局部變量}};Local().print();
}
局部類一般用于封裝臨時性算法,實現函數內部的策略模式。
7.3、聯合體聲明
聯合體(Union)是C++中一種特殊的類類型,它允許在相同的內存位置存儲不同類型的數據。與結構體(struct)和類(class)相比,聯合體的主要特點是所有成員共享同一塊內存,因此在同一時刻只能存儲一個成員的值。
聯合體有以下特性:
- 默認成員訪問是public。
- 修改一個成員會覆蓋其他成員的值。
- 聯合體的對齊方式通常由其最大成員的對齊要求決定
- 聯合體的大小至少要能容納其最大的數據成員,并且可能因對齊需求而增加額外空間。
union Example {char c; // 1字節int i; // 4字節(假設int為4字節)double d; // 8字節(假設double為8字節)
}; // 8字節
匿名聯合體是沒有名稱的聯合體,其成員直接成為包含它的作用域的成員:
struct Employee {enum class Type { MANAGER, ENGINEER };Type type;union {char* department; // 當type為MANAGER時使用int engineerId; // 當type為ENGINEER時使用}; // 匿名聯合體// 訪問方式:直接通過Employee對象訪問department或engineerId
};// 使用示例
Employee e;
e.type = Employee::Type::MANAGER;
e.department = "HR";
7.4、this指針
this指針在特殊場景中的行為:
- 在析構函數中使用this:析構函數執行時,對象的成員變量仍在內存中,但已進入銷毀流程,析構函數執行完畢后,對象占用的內存會被回收。析構函數中不要訪問已釋放的成員變量,僅執行必要的資源釋放操作,避免復雜邏輯,應避免調用其他成員函數。
class Resource {
public:~Resource() {// 合法:析構函數執行時對象尚未完全銷毀std::cout << "Destroying resource at " << this << std::endl;// 但不能調用非析構的成員函數,可能訪問已釋放內存}
};
- delete this的危險用法
class Dangerous {
public:void selfDestruct() {std::cout << "Before delete: " << this << std::endl;delete this; // 調用后當前對象被銷毀// this->data = 42; // 災難!訪問已釋放的內存// std::cout << data; // 同樣危險// 應該立即返回}
private:int data;
};Dangerous* obj = new Dangerous;
obj->selfDestruct();
// 從這里開始,obj已經成為懸空指針
// 任何對obj的使用都是未定義行為
這種模式有時用于引用計數對象的自我銷毀
- 多態場景下的this指針,在虛函數中,this指針始終指向實際對象(而非靜態類型)
class Base {
public:virtual void printAddress() {std::cout << "Base: " << this << std::endl;}
};class Derived : public Base {
public:void printAddress() override {Base::printAddress(); // 輸出基類視角的this(與派生類相同)std::cout << "Derived: " << this << std::endl;}
};// 輸出:Base和Derived的this指針地址相同,指向同一對象
7.5、static成員
在類定義中,關鍵詞static聲明不綁定到類實例的成員
static成員變量是具有靜態存儲期的獨立變量
class X { static int n; }; // 聲明(用 'static')
int X::n = 1; // 定義(不用 'static')
static成員函數不依賴于對象實例(沒有this指針),只能訪問static成員變量,可以直接用類名調用。
7.6、嵌套類
嵌套類(Nested Class)是指在一個類(稱為外圍類/外部類)內部定義的另一個類。嵌套類的作用域受外圍類限制,但可以訪問外圍類的成員(包括私有成員),嵌套類被視為外部類的 “朋友”。。
嵌套類可以訪問外部類的所有成員(包括私有、受保護和公共成員)以及成員函數,不過訪問非靜態成員時,必須借助外部類的實例來實現。對于外部類的靜態成員,嵌套類可以直接訪問,無需外部類的實例。
外部類無法直接訪問嵌套類的私有和受保護成員,如需訪問要將將外部類聲明為友元類。
外部類和嵌套類之間成員的相互使用無需理會聲明順序:
class enclose
{
public:void printNest() {nested1 ne;ne.num = 10;}private:class nested1 {public:void print() {enclose en;en.num = 10;}private:friend class enclose;int num;};int num;
};
嵌套類可以前置聲明并在之后定義,在外圍類的體內或體外均可:
class enclose
{class nested1; // 前置聲明class nested2; // 前置聲明class nested1 {}; // 嵌套類的定義
};class enclose::nested2 { }; // 嵌套類的定義
嵌套類不影響外圍類的大小。
7.7、派生類
在基類子句中列出的類是直接基類,直接基類的基類被稱為間接基類。同一個類不能多次被指定為直接基類,但是可以既是直接基類又是間接基類。
基類子對象的構造函數被派生類的構造函數所調用:可以在成員初始化器列表中向這些構造函數提供實參。
繼承時,如果省略訪問說明符,那么它對以類關鍵詞struct聲明的類默認為public,對以類關鍵詞class聲明的類為private。
虛基類:虛繼承是為了解決菱形繼承問題而引入的。當使用虛繼承時,無論通過多少條路徑繼承同一個虛基類,最終派生對象中只會包含該虛基類的一個實例。
struct B { int n; };
class X : public virtual B {};
class Y : virtual public B {};
class Z : public B {};// 每個 AA 類型對象擁有一個 X,一個 Y,一個 Z 和兩個 B:
// 一個是 Z 的基類,另一個由 X 與 Y 所共享
struct AA : X, Y, Z
{void f(){X::n = 1; // 修改虛 B 子對象的成員Y::n = 2; // 修改同一虛 B 子對象的成員Z::n = 3; // 修改非虛 B 子對象的成員std::cout << X::n << Y::n << Z::n << '\n'; // 打印 223}
};
上述例子雖然能運行,但是n的定義是不明確的!!
私有繼承時,子類不對外表現出is-a的關系,但是可以在子類內部使用is-a,在類外部可以通過指針強轉進行虛函數調用。
7.8、using 聲明
在命名空間和塊作用域中:using聲明將另一命名空間的成員引入到當前命名空間或塊作用域中。
在類定義中:using聲明可以將別處定義的名字引入到此using聲明所在的聲明區中,例如將基類的受保護成員暴露為派生類的公開成員。這有幾個優點:
7.8.1、實現接口擴展與兼容性
當你設計一個類的繼承體系時,可能基類出于封裝和安全性的考慮,將某些成員設置為受保護的。但在派生類的使用場景中,這些成員可能需要被外部更方便地訪問,以滿足特定的接口需求。通過 using 聲明將基類的受保護成員提升為派生類的公開成員,可以在不破壞基類原有封裝的前提下,為派生類提供更廣泛的接口。
// 基類
class Base {
protected:void protectedFunction() {std::cout << "Base::protectedFunction() called" << std::endl;}
};// 派生類
class Derived : public Base {
public:using Base::protectedFunction; // 將基類的受保護成員暴露為公開成員
};int main() {Derived d;d.protectedFunction(); // 可以直接調用,無需通過其他接口return 0;
}
引入有作用域枚舉項:除了另一命名空間的成員和基類的成員,using聲明也能將枚舉的枚舉項引入命名空間、塊和類作用域。
7.8.2、繼承構造函數
當在派生類中寫下using Base::Base; 時,編譯器會:
- 隱式生成派生類的構造函數:這些構造函數與基類的構造函數具有相同的參數列表。
- 將基類構造函數納入重載決議:當創建派生類對象時,編譯器會考慮基類的構造函數。
class Base {
public:Base(int a) { cout << "Base int " << endl; }Base(int a, double b) { cout << "Base int double " << endl; }Base(int a, double b, char c) { cout << "Base int double char " << endl; }
};class Derived : public Base {
public:Derived(int a, double b) : Base(a, b){cout << "Derived int double " << endl;}
private:using Base::Base; // 讓Base的構造函數在Derived中可見
};int main() {Derived a(1);Derived b(1, 1.1);Derived c(1, 2, 'c');return 0;
}
當創建派生類對象時,編譯器會:
- 優先考慮派生類自身定義的構造函數。
- 如果沒有匹配的構造函數,則考慮通過 using Base::Base; 繼承的基類構造函數。
- 繼承的構造函數只會初始化基類部分,派生類的新增成員需通過默認初始化(如果有默認構造函數)或保持未初始化狀態。
訪問控制規則:
- 基類構造函數的訪問權限不變:如果基類的某個構造函數是 protected,繼承后在派生類中仍然是 protected。
- 派生類無法繼承基類的私有構造函數:因為私有成員對派生類不可見。
構造函數繼承的限制:
- 無法初始化派生類成員
- 無法繼承默認 / 拷貝 / 移動構造函數
- 沖突的構造函數會被刪除:如果派生類已經定義了與基類構造函數參數列表相同的構造函數,繼承的版本會被刪除。
7.9、空基類優化
為保證同一類型的不同對象地址始終有別,要求任何對象或成員子對象的大小至少為1,即使該類型是空的類類型(即沒有非靜態數據成員的類或結構體),否則兩個對象的大小為0,它們的內存地址可能相同,導致指針無法區分它們。
C++標準允許編譯器將空基類子對象的大小優化為0字節,即使普通對象必須至少1字節
class Empty {}; // 空基類class Derived : public Empty {int x; // 只有一個數據成員
};// 通常 sizeof(int) 為 4 字節
// 由于 EBCO,Empty 基類子對象的大小被優化為 0
// 因此 sizeof(Derived) == 4,而非 5!