1.類的默認成員函數
默認成員函數就是用戶沒有顯示實現,編譯器會自動生成的成員函數稱為默認成員函數。一個類我們不寫的情況下編譯器會默認生成以下的6個默認成員函數。
(1)構造函數:主要完成初始化的工作
(2)析構函數:主要完成銷毀清理的工作
(3)拷貝構造函數:使用同類對象初始化創建對象
(4)賦值重載:主要是把一個對象賦值給另一個對象
(5)普通對象取地址重載:主要用于特殊情況,如安全敏感的應用、自定義內存管理或調試目的。在大多數日常編程中,不需要重載這個運算符。
(6)const對象取地址重載:和普通對象取地址重載一樣。日常編程中不需要重載這個運算符。
以上六種默認成員函數我們主要學習前四種。
1.1構造函數
構造函數是特殊的成員函數,構造函數雖然名稱是構造,但是構造函數的主要任務是實例化時初始化對象,并不是開空間創造對象(我們常使用的局部對象是棧幀創建時,空間就開好了)。
構造函數的語法特點如下
(1)函數名可以和類名相同
class MyClass {
public:MyClass() { // 構造函數與類名相同// 初始化代碼}
};
(2)無返回值,無返回類型。甚至不能使用void(C++規定是這樣,不用糾結)
(3)對象實例化時系統會自動調用對應的構造函數
MyClass obj; // 構造函數自動調用
(4)構造函數可以重載
一個類可以有多個構造函數,只要它們的參數列表不同。
class MyClass {
public:MyClass() { // 默認構造函數// 初始化代碼}MyClass(int value) { // 帶參數的構造函數// 使用value初始化}MyClass(int a, double b) { // 另一個重載// 使用a和b初始化}
};
(5)可以有默認參數
class MyClass {
public:MyClass(int a = 0, double b = 0.0) {// 使用默認參數或提供的值初始化}
};
(6)如果類中沒有顯式定義構造函數,則C++編譯器會自動生成?個無參的默認構造函數,?旦用戶顯式定義編譯器將不再生成。
#include<iostream>
using namespace std;
class Stack {
public:Stack()//無參的默認構造函數{}
private:int* _date;int _top;int _capacity;
};
int main()
{Stack s1;return 0;
}
這里我們來調試觀察s1
會發現s1會被編譯器自動生成默認構造函數
1.2析構函數
析構函數與構造函數功能相反,析構函數不是完成對對象本身的銷毀,比如局部對象是存在棧幀的,函數結束棧幀銷毀,他就釋放了,不需要我們管,C++規定對象在銷毀時會自動調用析構函數,完成對象中資源的清理釋放工作。
析構函數的特點如下:
(1)在類名前加~
class MyClass {
public:~MyClass() { // 析構函數// 清理代碼}
};
(2)無參數,無返回值也,不能被重載。一個類只能有一個析構函數?(這里跟構造類似,也不需要加void)
class MyClass {
public:~MyClass() { // 正確// 清理代碼}// ~MyClass(int x) { } // 錯誤:析構函數不能有參數
};
(3)當對象離開其作用域或被顯示刪除時,析構函數會自動調用。這里跟構造函數類似,我們不寫編譯器自動生0成的析構函數對內置類型成員不做處理,自定類型成員會調用他的析構函數。
{MyClass obj; // 對象創建// 使用對象...
} // 對象離開作用域,析構函數自動調用MyClass* ptr = new MyClass();
delete ptr; // 析構函數被調用
(4)還需要注意的是我們顯示寫析構函數,對于自定義類型成員也會調用他的析構,也就是說自定義類型成員無論什么情況都會自動調用析構函數。
(5)對于繼承層次中的對象,析構函數的調用順序與構造函數相反:派生類的析構函數 成員對象的析構函數(按聲明逆序)基類的析構函數
class Base {
public:~Base() { std::cout << "Base destructor" << std::endl; }
};class Member {
public:~Member() { std::cout << "Member destructor" << std::endl; }
};class Derived : public Base {
private:Member member;
public:~Derived() { std::cout << "Derived destructor" << std::endl; }
};// 創建Derived對象然后銷毀
// 輸出順序:
// Derived destructor
// Member destructor
// Base destructor
1.3拷貝構造函數
拷貝構造是一種特殊的構造函數,用于創建一個新對象來當已存在對象的副本。以下是拷貝構造的主要特點。
(1)函數名與類名相同,沒有返回類型。參數必須是同類型對象的引用,通常為const引用。如果不引用可能會出現無限遞歸的現象。
class MyClass {
public:// 正確的拷貝構造函數MyClass(const MyClass& other) {// 復制邏輯}// 錯誤的拷貝構造函數示例// MyClass(MyClass other) { } // 錯誤:值傳遞會導致無限遞歸// MyClass(MyClass* other) { } // 錯誤:這不是拷貝構造函數
};
如果不用引用導致遞歸,編譯器會報錯
(2)C++規定自定義類型對象進行拷貝行為必須調用拷貝構造,所以自定義類型傳值傳參和傳值返回都會調用拷貝構造完成。
MyClass obj1;
MyClass obj2 = obj1; // 調用拷貝構造函數
MyClass obj3(obj1); // 調用拷貝構造函數void func(MyClass param); // 值傳遞參數時調用拷貝構造函數
func(obj1); // 調用拷貝構造函數MyClass func2() {MyClass local;return local; // 可能調用拷貝構造函數(取決于編譯器優化)
}
(3)若未顯式定義拷貝構造,編譯器會生成自動生成拷貝構造函數。自動生成的拷貝構造對內置類型成員變量會完成值拷貝/淺拷貝(?個字節?個字節的拷貝),對自定義類型成員變量會調用他的拷貝構造。
默認拷貝構造函數的陷阱。淺拷貝構造
這對于簡單類足夠使用,但是遇到指針類型就會出現雙重釋放的問題
class ShallowCopyExample {
private:int* data;public:ShallowCopyExample(int value) {data = new int(value);}// 默認拷貝構造函數執行淺拷貝:// ShallowCopyExample(const ShallowCopyExample& other) : data(other.data) {}~ShallowCopyExample() {delete data;}
};// 使用示例
int main()
{ShallowCopyExample obj1(42);ShallowCopyExample obj2 = obj1; // 淺拷貝:兩個對象的data指向同一內存return 0;
}
// 問題:obj2析構時會釋放內存,然后obj1析構時再次釋放同一內存 → 雙重釋放錯誤!
因為淺拷貝構造的指針指向了同一塊內存,我們用編譯器調試或者轉到反匯編就可以清楚的看到
那么我們遇到帶有指針的成員變量,就需要用自定義拷貝構造來實現深拷貝。
#include <iostream>
#include <cstdlib> // 包含 malloc 和 free
#include <cstring> // 包含 strcpyclass StringWithMalloc
{
private:char* data;size_t length;public:// 構造函數 - 使用 malloc 分配內存StringWithMalloc(const char* str = ""){length = strlen(str);data = (char*)malloc(length + 1); // 使用 malloc 分配內存if (data != nullptr) {strcpy(data, str);}else {length = 0;}std::cout << "構造函數: " << (data ? data : "NULL")<< " (地址: " << (void*)data << ")" << std::endl;}// 自定義拷貝構造函數 - 使用 malloc 實現深拷貝StringWithMalloc(const StringWithMalloc& other){length = other.length;data = (char*)malloc(length + 1); // 使用 malloc 分配新內存if (data != nullptr) {strcpy(data, other.data);}else {length = 0;}std::cout << "拷貝構造函數: " << (data ? data : "NULL")<< " (新地址: " << (void*)data << ")" << std::endl;}// 析構函數 - 使用 free 釋放內存~StringWithMalloc(){std::cout << "析構函數: " << (data ? data : "NULL")<< " (地址: " << (void*)data << ")" << std::endl;free(data); // 使用 free 釋放內存data = nullptr; // 防止懸空指針}
};
(4)傳值返回會產生?個臨時對象調用拷貝構造,傳值引用返回,返回的是返回對象的別名(引用),沒有產生拷貝。但是如果返回對象是?個當前函數局部域的局部對象,函數結束就銷毀了,那么使用引用返回是有問題的,這時的引用相當于?個野引用,類似?個野指針?樣。傳引用返回可以減少拷貝,但是?定要確保返回對象,在當前函數結束后還在,才能用引用返。
1.4賦值運算符重載
1.4.1運算符重載
Class Data//定義一個類
{//......
}
int main()
{Data d1;Data d2;d1-d2//這里編譯器沒有對應的運算符重載會報錯return 0;
}
(2)運算符重載是具有特殊名字的函數,他的名字是由operator和后面要定義的運算符共同構成。和其他函數?樣,它也具有其返回類型和參數列表以及函數體。
基本語法如下
返回類型 operator操作符(參數列表) {// 函數體
}
(3)重載運算符函數的參數個數和該運算符作用的運算對象數量?樣多。?元運算符有?個參數,?元運算符有兩個參數,?元運算符的左側運算對象傳給第?個參數,右側運算對象傳給第?個參數。
(4)如果?個重載運算符函數是成員函數,則它的第?個運算對象默認傳給隱式的this指針,因此運算符重載作為成員函數時,參數比運算對象少?個。
(5)運算符重載以后,其優先級和結合性與對應的內置類型運算符保持?致。
(6)不能通過連接語法中沒有的符號來創建新的操作符:比如operator@。
(7).*? ?::? ? sizeof? ? ?:? ? . 注意以上5個運算符不能重載。
(8)重載操作符至少有?個類類型參數,不能通過運算符重載改變內置類型對象的含義
int operator+(int x, int y)//里面沒有自定義的類,都是內置類型不可重載
1.4.2賦值運算符重載的特點
*this
),以支持連續賦值(如 a = b = c)