C++六大默認成員函數
- 默認構造函數
- 默認析構函數
- RAII技術
- RAII的核心思想
- 優點
- 示例
- 應用場景
- 默認拷貝構造
- 深拷貝和淺拷貝
- 默認拷貝賦值運算符
- 移動構造函數(C++11起)
- 默認移動賦值運算符(C++11起)
- 取地址及const取地址操作符重載
- 取地址操作符重載
- 常量取地址操作符重載
- 示例代碼
- 輸出結果
- 注意事項
- 擴展:前置++和后置++重載
- 重載規則
C++中的六大默認成員函數是編譯器在特定條件下自動生成的成員函數,用于管理對象的生命周期和資源操作。它們分別是:
默認構造函數
-
作用:初始化對象,當類沒有顯式定義任何構造函數時生成。
-
生成條件:用戶未定義任何構造函數。
-
注意:若類有其他構造函數(如帶參數的構造函數),需顯式使用 = default 聲明默認構造函數。
class Person
{
public://Person()//{//} 不寫的話默認自動生成void GetAge(){std::cout << _age << std::endl;}
private:int _age;
};int main()
{Person p;p.GetAge();
}
其特征如下:
- 函數名與類名相同。
- 無返回值。
- 對象實例化時編譯器自動調用對應的構造函數。
- 構造函數可以重載。
- 如果類中沒有顯式定義構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦用戶顯式定義編譯器將不再生成。
- 編譯器生成默認的構造函數會對自定類型成員調用的它的默認成員
函數。C++11 中針對內置類型成員不初始化的缺陷,又打了補丁,即:內置類型成員變量在類中聲明時可以給默認值。
默認析構函數
-
作用:釋放對象資源,默認析構函數調用成員變量的析構函數。
-
生成條件:用戶未定義析構函數。
-
注意:若類管理動態資源(如堆內存),需自定義析構函數以避免內存泄漏
class Person
{
public://Person()//{//} 不寫的話默認自動生成void GetAge(){std::cout << _age << std::endl;}~Person(){}
private:int _age;
};int main()
{Person p;p.GetAge();
}
RAII技術
RAII(Resource Acquisition Is Initialization,資源獲取即初始化)是C++中一種管理資源的編程技術。它通過將資源的生命周期與對象的生命周期綁定在一起,利用C++的構造函數和析構函數來自動管理資源,從而避免了手動分配和釋放資源可能帶來的問題,如內存泄漏、資源未正確釋放等。
RAII的核心思想
- 資源在對象構造時獲取:當一個對象被創建時,它的構造函數負責獲取所需的資源(例如,動態內存分配、文件打開、網絡連接等)。
- 資源在對象銷毀時釋放:當對象離開作用域或被顯式刪除時,其析構函數會自動釋放之前獲取的資源。
優點
- 異常安全性:由于資源管理由構造和析構函數自動處理,即使程序中拋出了異常,也能確保資源得到正確釋放。
- 簡化代碼:開發者不需要手動跟蹤每個資源的狀態,并且可以在不使用顯式的
try-finally
塊的情況下保證資源的釋放。 - 防止資源泄露:只要對象被正確地創建并最終銷毀,資源就會被正確釋放。
示例
以下是一個簡單的例子,展示了如何使用RAII來管理動態分配的內存:
#include <iostream>class ResourceHandler {
private:int* data;
public:// 構造函數:資源獲取ResourceHandler() {data = new int(10); // 分配資源std::cout << "Resource acquired." << std::endl;}// 析構函數:資源釋放~ResourceHandler() {delete data; // 釋放資源std::cout << "Resource released." << std::endl;}void showData() const {std::cout << "Data: " << *data << std::endl;}
};void useResource() {ResourceHandler handler;handler.showData();// 不需要手動釋放資源,handler離開作用域時會自動調用析構函數
}int main() {useResource();return 0;
}
在這個例子中,ResourceHandler
類負責管理一個整數類型的動態分配內存。構造函數在對象創建時分配資源,而析構函數在對象銷毀時釋放這些資源。這樣就確保了無論函數useResource
如何退出(正常結束或因異常退出),資源都會被正確釋放。
應用場景
RAII不僅限于內存管理,還可以應用于其他資源類型,如文件句柄、網絡套接字、數據庫連接等。標準庫中的智能指針(如std::unique_ptr
、std::shared_ptr
)、鎖機制(如std::lock_guard
、std::unique_lock
)都是RAII原則的實際應用案例。通過使用這些工具,可以有效地減少資源管理錯誤,提高代碼的安全性和可靠性。
默認拷貝構造
-
聲明形式:ClassName(const ClassName&)
-
作用:通過已有對象初始化新對象,默認執行淺拷貝。
-
生成條件:用戶未定義拷貝構造函數。
-
注意:若類包含指針或動態資源,需自定義深拷貝防止重復釋放。
class Person
{
public:Person(){}Person(const Person& person){this->_age = person._age;}~Person(){}void GetAge(){std::cout << _age << std::endl;}
private:int _age;
};int main()
{Person p;p.GetAge();
}
深拷貝和淺拷貝
class Stack
{
public://初始化Stack(){_array = new int[20];}//默認生成拷貝構造//析構~Stack(){delete[] _array;}
private:int* _array;size_t _size;size_t _capacity;
};int main()
{Stack s1;Stack s2(s1);
}
這里我沒有寫實際的拷貝構造函數,這里s2調用的默認的拷貝構造,所以s2_array的地址就是s1中_array的地址,這就叫淺拷貝:
這樣代碼就會有問題,因為一個地址會被析構兩次:
正確的方法應該是給s2的array開辟一塊新的空間:
class Stack
{
public://初始化Stack(){_array = new int[20];}Stack(const Stack& st){_array = new int[10];_size = st._size;_capacity = st._capacity;}//析構~Stack(){delete[] _array;}
private:int* _array;size_t _size;size_t _capacity;
};int main()
{Stack s1;Stack s2(s1);
}
這樣的拷貝我們稱為深拷貝,再次運行程序:
默認拷貝賦值運算符
-
聲明形式:ClassName& operator=(const ClassName&)
-
作用:將已有對象的值賦給另一個對象,默認淺拷貝。
-
生成條件:用戶未定義拷貝賦值運算符。
-
注意:需處理自賦值問題,并在資源管理時實現深拷貝。
class Stack
{
public://初始化Stack(){_array = new int[20];}Stack(const Stack& st){_array = new int[10];_size = st._size;_capacity = st._capacity;}Stack& operator=(const Stack& st){if (this != &st){_array = new int[10];_size = st._size;_capacity = st._capacity;}return *this;}//析構~Stack(){delete[] _array;}
private:int* _array;size_t _size;size_t _capacity;
};int main()
{Stack s1;Stack s2;s2 = s1;
}
移動構造函數(C++11起)
-
聲明形式:ClassName(ClassName&&)
-
作用:通過右值引用“竊取”資源,避免深拷貝開銷。
-
生成條件:用戶未定義拷貝操作、移動操作或析構函數。
-
注意:移動后源對象應處于有效但未定義狀態(如空指針)。
class Stack
{
public://初始化Stack(){_array = new int[20];}Stack(const Stack& st){_array = new int[10];_size = st._size;_capacity = st._capacity;}Stack& operator=(const Stack& st){if (this != &st){_array = new int[10];_size = st._size;_capacity = st._capacity;}return *this;}void swap(Stack& st){std::swap(_array, st._array);std::swap(_size, st._size);std::swap(_capacity, st._capacity);}//移動構造函數Stack(Stack&& st):_array(nullptr), _size(0), _capacity(0){swap(st);}//析構~Stack(){delete[] _array;}
private:int* _array;size_t _size;size_t _capacity;
};int main()
{Stack s1;Stack s2(std::move(s1));
}
默認移動賦值運算符(C++11起)
-
聲明形式:ClassName& operator=(ClassName&&)
-
作用:通過右值引用轉移資源所有權。
-
生成條件:同移動構造函數。
-
注意:需正確處理自移動賦值。
class Stack
{
public://初始化Stack(){_array = new int[20];}Stack(const Stack& st){_array = new int[10];_size = st._size;_capacity = st._capacity;}Stack& operator=(const Stack& st){if (this != &st){_array = new int[10];_size = st._size;_capacity = st._capacity;}return *this;}void swap(Stack& st){std::swap(_array, st._array);std::swap(_size, st._size);std::swap(_capacity, st._capacity);}//移動構造函數Stack(Stack&& st):_array(nullptr), _size(0), _capacity(0){swap(st);}//移動復制構造Stack& operator=(Stack&& st){swap(st);st._array = nullptr;st._size = 0;st._capacity = 0;return *this;}//析構~Stack(){delete[] _array;}
private:int* _array;size_t _size;size_t _capacity;
};int main()
{Stack s1;Stack s2;s2 = std::move(s1);
}
在C++中,前置++
和后置++
運算符可以通過成員函數或非成員函數的形式進行重載。兩者的主要區別在于參數列表和返回值:
- 前置
++
:增加對象的值,并返回增加后的對象引用。 - 后置
++
:首先保存當前對象的狀態,然后增加對象的值,最后返回之前保存的對象的副本。
取地址及const取地址操作符重載
在C++中,取地址操作符(&
)和常量取地址操作符(const &
)通常不需要顯式地重載,因為編譯器提供了默認的實現,它們分別返回對象或常量對象的內存地址。然而,在某些特定情況下,你可能想要自定義這些操作符的行為。
取地址操作符重載
當你重載取地址操作符時,你通常是為了改變其默認行為,例如返回一個代理對象的地址而不是原始對象的地址。不過這種情況非常少見,大多數時候并不需要這樣做。
常量取地址操作符重載
類似地,重載常量版本的取地址操作符也是為了提供特定的行為,但同樣,這并不是常見的需求。
示例代碼
盡管不常見,這里還是給出如何重載這兩種操作符的基本示例:
#include <iostream>class MyClass {
private:int value;
public:MyClass(int val) : value(val) {}// 重載取地址操作符int* operator&() {std::cout << "非const取地址操作符被調用" << std::endl;return &value;}// 重載const取地址操作符const int* operator&() const {std::cout << "const取地址操作符被調用" << std::endl;return &value;}
};int main() {MyClass obj(10);const MyClass constObj(20);int* addr = &obj; // 調用非const版本const int* constAddr = &constObj; // 調用const版本std::cout << "*addr: " << *addr << std::endl;std::cout << "*constAddr: " << *constAddr << std::endl;return 0;
}
輸出結果
非const取地址操作符被調用
const取地址操作符被調用
*addr: 10
*constAddr: 20
在這個例子中,我們為MyClass
類重載了取地址操作符和常量取地址操作符。當通過非常量對象調用operator&()
時,會調用非常量版本的操作符,并打印一條消息。而當通過常量對象調用operator&()
時,則調用常量版本的操作符,并打印另一條不同的消息。
注意事項
- 謹慎使用:一般情況下,不需要也不建議重載這兩個操作符,除非有特別的需求。這是因為它們改變了標準語義,可能會導致混淆或者不可預期的行為。
- 保持一致性:如果你決定重載這些操作符,請確保它們的行為符合邏輯且一致,避免引入錯誤。
- 理解限制:需要注意的是,即使你重載了取地址操作符,也無法阻止使用內置的取地址操作來獲取對象的實際地址。例如,
&obj
總是可以獲得obj
的實際地址,除非你完全隱藏了對象的訪問方式。
總的來說,重載取地址操作符和常量取地址操作符是一種高級技巧,適用于特定場景下的特殊需求。在大多數日常編程任務中,這種重載是不必要的。
擴展:前置++和后置++重載
重載規則
- 前置
++
只需要一個參數(即調用該運算符的對象本身),并且通常返回一個指向修改后的對象的引用。 - 后置
++
需要兩個參數:第一個是調用該運算符的對象本身,第二個是一個int類型的占位參數,用于區分前置和后置形式。后置++
返回的是操作前對象的一個副本(通常是通過值返回)。
#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;class Count
{
public://重載后置++Count operator++(){++_count;return *this;}//后置++Count operator++(int){Count temp = *this;++_count;return temp;}int getCount() const {return _count;}
private:int _count = 0;
};int main()
{Count myCounter;std::cout << "Initial count: " << myCounter.getCount() << std::endl;++myCounter; // 調用前置++std::cout << "After prefix increment: " << myCounter.getCount() << std::endl;Count d;d = myCounter++; // 調用后置++std::cout << "After postfix increment: " << d.getCount() << std::endl;return 0;
}