解鎖高效內(nèi)存管理:C++智能指針的用法
C++ 的世界里,內(nèi)存管理一直是程序員們需要高度關(guān)注的核心問題之一。一個(gè)小小的內(nèi)存泄漏或者懸空指針錯(cuò)誤,都可能引發(fā)程序的崩潰或性能的嚴(yán)重下降。而智能指針,作為 C++ 中一種強(qiáng)大的內(nèi)存管理工具,其重要性不言而喻。它不僅能夠自動(dòng)處理內(nèi)存的分配與釋放,還能有效避免許多常見的內(nèi)存管理錯(cuò)誤,讓我們的代碼更加健壯和可靠。接下來,就讓我們一起深入解析智能指針的奧秘。
一、智能指針簡(jiǎn)介
1.1 概述
在 C++ 編程中,內(nèi)存管理是至關(guān)重要的一環(huán)。C++ 語言賦予了程序員對(duì)內(nèi)存的高度控制權(quán),但這也意味著需要謹(jǐn)慎地處理內(nèi)存的分配與釋放,否則很容易陷入諸如內(nèi)存泄漏、懸空指針等棘手問題的泥沼。
當(dāng)我們使用new操作符手動(dòng)分配內(nèi)存時(shí),必須時(shí)刻牢記在適當(dāng)?shù)臅r(shí)機(jī)使用delete來釋放內(nèi)存,稍有疏忽就可能導(dǎo)致內(nèi)存泄漏,使程序占用的內(nèi)存不斷增加,最終耗盡系統(tǒng)資源。而如果對(duì)已經(jīng)釋放的內(nèi)存進(jìn)行訪問,就會(huì)產(chǎn)生懸空指針,這可能引發(fā)程序崩潰或出現(xiàn)不可預(yù)測(cè)的行為。
為了幫助程序員更安全、高效地管理內(nèi)存,C++ 引入了智能指針這一強(qiáng)大的工具。智能指針能夠自動(dòng)管理所指向?qū)ο蟮纳芷?,在很大程度上減輕了程序員手動(dòng)管理內(nèi)存的負(fù)擔(dān),降低了內(nèi)存相關(guān)錯(cuò)誤的發(fā)生概率,使得 C++ 編程更加穩(wěn)健、可靠,讓我們能夠?qū)⒏嗟木劢褂诔绦虻臉I(yè)務(wù)邏輯實(shí)現(xiàn)上,而無需為繁瑣的內(nèi)存管理細(xì)節(jié)而憂心忡忡。接下來,就讓我們深入探究 C++ 智能指針的奧秘。
智能指針主要用于管理在堆上分配的內(nèi)存,它將普通的指針封裝為一個(gè)棧對(duì)象。當(dāng)棧對(duì)象的生存周期結(jié)束后,會(huì)在析構(gòu)函數(shù)中釋放掉申請(qǐng)的內(nèi)存,從而防止內(nèi)存泄漏。簡(jiǎn)要的說,智能指針利用了 C++ 的 RAII 機(jī)制,在智能指針對(duì)象作用域結(jié)束后,會(huì)自動(dòng)做內(nèi)存釋放的相關(guān)操作,不需要我們?cè)偈謩?dòng)去操作內(nèi)存。
C++ 中有四種智能指針:
- auto_ptr:已經(jīng)廢棄
- unique_ptr:獨(dú)占式指針,同一時(shí)刻只能有一個(gè)指針指向同一個(gè)對(duì)象
- shared_ptr:共享式指針,同一時(shí)刻可以有多個(gè)指針指向同一個(gè)對(duì)象
- weak_ptr:用來解決shared_ptr相互引用導(dǎo)致的死鎖問題
1.2 誕生背景
在 C++ 中,內(nèi)存管理的復(fù)雜性常常讓開發(fā)者頭疼不已。當(dāng)我們使用new操作符在堆上分配內(nèi)存時(shí),必須謹(jǐn)慎地使用delete操作符來釋放內(nèi)存,否則就可能出現(xiàn)內(nèi)存泄漏的問題。例如:
void memoryLeakExample() {
int* ptr = new int(42);
// 這里如果忘記釋放內(nèi)存,就會(huì)導(dǎo)致內(nèi)存泄漏
}
在上述代碼中,如果memoryLeakExample函數(shù)執(zhí)行完畢后沒有對(duì)ptr指向的內(nèi)存進(jìn)行釋放,那么這塊內(nèi)存就會(huì)一直被占用,無法被系統(tǒng)回收,從而造成內(nèi)存泄漏。隨著程序的運(yùn)行,這種泄漏的內(nèi)存會(huì)不斷累積,最終可能導(dǎo)致系統(tǒng)內(nèi)存耗盡,程序崩潰。
除了內(nèi)存泄漏,懸空指針也是一個(gè)棘手的問題。當(dāng)一個(gè)指針?biāo)赶虻膬?nèi)存已經(jīng)被釋放,但指針仍然存在并被錯(cuò)誤地使用時(shí),就會(huì)出現(xiàn)懸空指針的情況。例如:
int* danglingPointerExample() {
int* ptr = new int(10);
delete ptr;
// 此時(shí)ptr成為懸空指針,下面的返回語句是不安全的
return ptr;
}
在這個(gè)例子中,ptr所指向的內(nèi)存已經(jīng)被釋放,但函數(shù)仍然返回了這個(gè)指針,這就導(dǎo)致了懸空指針的產(chǎn)生。如果在其他地方使用了這個(gè)返回的指針,就可能引發(fā)未定義的行為,如程序崩潰或數(shù)據(jù)錯(cuò)誤。
為了解決這些問題,C++ 引入了智能指針。智能指針利用了 RAII(Resource Acquisition Is Initialization,資源獲取即初始化)機(jī)制,將內(nèi)存管理的責(zé)任交給對(duì)象的生命周期來處理。當(dāng)智能指針對(duì)象被創(chuàng)建時(shí),它獲取資源(即指向的內(nèi)存),而在智能指針對(duì)象銷毀時(shí),它會(huì)自動(dòng)釋放所管理的資源,從而確保內(nèi)存的正確釋放,避免了手動(dòng)管理內(nèi)存時(shí)容易出現(xiàn)的錯(cuò)誤,大大提高了程序的穩(wěn)定性和可靠性。
二、智能指針的核心原理
2.1 RAII 機(jī)制
RAII(Resource Acquisition Is Initialization),即資源獲取即初始化,是智能指針實(shí)現(xiàn)自動(dòng)內(nèi)存管理的基石。其核心思想是將資源的獲取與對(duì)象的初始化緊密綁定,而資源的釋放則與對(duì)象的析構(gòu)函數(shù)關(guān)聯(lián)。當(dāng)一個(gè)對(duì)象被創(chuàng)建時(shí),它會(huì)獲取所需的資源(例如動(dòng)態(tài)分配的內(nèi)存),并在對(duì)象的生命周期內(nèi)持有這些資源。一旦對(duì)象的生命周期結(jié)束,無論是因?yàn)楹瘮?shù)執(zhí)行完畢導(dǎo)致局部對(duì)象超出作用域,還是因?yàn)閷?duì)象被顯式銷毀,其析構(gòu)函數(shù)都會(huì)被自動(dòng)調(diào)用,從而確保資源被正確釋放,避免了因程序員疏忽而導(dǎo)致的資源泄漏問題。
以下是一個(gè)簡(jiǎn)單的示例代碼,展示了如何通過 RAII 機(jī)制實(shí)現(xiàn)一個(gè)簡(jiǎn)單的智能指針:
template<typename T>
class MySmartPtr {
public:
// 構(gòu)造函數(shù)獲取資源
MySmartPtr(T* ptr) : m_ptr(ptr) {}
// 析構(gòu)函數(shù)釋放資源
~MySmartPtr() {
delete m_ptr;
}
// 重載解引用運(yùn)算符,使其行為類似于普通指針
T& operator*() {
return *m_ptr;
}
// 重載箭頭運(yùn)算符,使其行為類似于普通指針
T* operator->() {
return m_ptr;
}
private:
T* m_ptr;
};
在上述代碼中,MySmartPtr類模板實(shí)現(xiàn)了一個(gè)基本的智能指針功能。構(gòu)造函數(shù)接受一個(gè)指針類型的參數(shù),將其賦值給成員變量m_ptr,從而獲取資源。而析構(gòu)函數(shù)則在對(duì)象銷毀時(shí),使用delete操作符釋放m_ptr指向的內(nèi)存資源,確保資源的正確回收。通過這種方式,我們將資源的管理封裝在了類中,利用對(duì)象的生命周期來自動(dòng)管理資源,遵循了 RAII 機(jī)制的原則。
2.2 引用計(jì)數(shù)技術(shù)
引用計(jì)數(shù)是智能指針實(shí)現(xiàn)資源共享和自動(dòng)釋放的關(guān)鍵技術(shù)之一,尤其是在std::shared_ptr中得到了廣泛應(yīng)用。其原理是為每個(gè)被管理的資源維護(hù)一個(gè)引用計(jì)數(shù)變量,用于記錄當(dāng)前有多少個(gè)智能指針對(duì)象正在引用該資源。
當(dāng)一個(gè)新的std::shared_ptr對(duì)象被創(chuàng)建并指向某一資源時(shí),該資源的引用計(jì)數(shù)會(huì)增加。例如:
#include <memory>
#include <iostream>
int main() {
// 創(chuàng)建一個(gè)shared_ptr,此時(shí)資源的引用計(jì)數(shù)為1
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::cout << "ptr1引用計(jì)數(shù): " << ptr1.use_count() << std::endl;
// 拷貝構(gòu)造一個(gè)新的shared_ptr,引用計(jì)數(shù)增加為2
std::shared_ptr<int> ptr2 = ptr1;
std::cout << "ptr2引用計(jì)數(shù): " << ptr2.use_count() << std::endl;
// 賦值操作,引用計(jì)數(shù)不變(先減少左邊的引用計(jì)數(shù),再增加右邊的引用計(jì)數(shù))
std::shared_ptr<int> ptr3;
ptr3 = ptr2;
std::cout << "ptr3引用計(jì)數(shù): " << ptr3.use_count() << std::endl;
// 當(dāng)一個(gè)shared_ptr超出作用域,引用計(jì)數(shù)減少
{
std::shared_ptr<int> ptr4 = ptr3;
std::cout << "ptr4引用計(jì)數(shù): " << ptr4.use_count() << std::endl;
}
std::cout << "ptr3引用計(jì)數(shù)(ptr4超出作用域后): " << ptr3.use_count() << std::endl;
return 0;
}
在上述代碼中,通過std::make_shared創(chuàng)建了一個(gè)std::shared_ptr<int>對(duì)象ptr1,此時(shí)資源的引用計(jì)數(shù)為 1。接著通過拷貝構(gòu)造和賦值操作創(chuàng)建了ptr2和ptr3,每次操作都會(huì)使引用計(jì)數(shù)相應(yīng)增加。當(dāng)ptr4超出其作用域時(shí),其析構(gòu)函數(shù)被調(diào)用,引用計(jì)數(shù)減少。
當(dāng)引用計(jì)數(shù)變?yōu)?0 時(shí),表示沒有任何智能指針再引用該資源,此時(shí)資源會(huì)被自動(dòng)釋放。這種機(jī)制確保了資源在不再被使用時(shí)能夠及時(shí)、正確地被回收,避免了內(nèi)存泄漏的發(fā)生,同時(shí)也支持了多個(gè)智能指針安全地共享同一資源,提高了資源的利用率和程序的靈活性。
三、C++智能指針家族成員
3.1 std::auto_ptr
創(chuàng)建auto_ptr對(duì)象的三種方式:
#include <iostream>
#include <memory>
using namespace std;
class A
{
public:
void fun()
{
cout << this << "A::fun()" << endl;
}
};
int main()
{
auto_ptr<A> p1(new A() );
auto_ptr<A> p2;
p2.reset(new A());
auto_ptr<A> p3;
p3 = p1; //把p1空間的歸屬權(quán)交給p3,后面不能再用p1
return 0;
}
使用對(duì)象:
p3.get()->fun();
p3->fun();
auto_ptr 存在的問題:將 p1 賦值給 p3 ,會(huì)將 p1 的資源轉(zhuǎn)交給 p3,而不是復(fù)制,此時(shí)再調(diào)用 p1會(huì)出現(xiàn)空指針問題:
auto_ptr<A> p3;
p3 = p1;
p1->fun(); //error
因此在 C++11 中被棄用。
3.2 std::unique_ptr
(1)獨(dú)占所有權(quán)
圖片
std::unique_ptr 有著獨(dú)占所有權(quán)的特性,這意味著在同一時(shí)間內(nèi),只有一個(gè)std::unique_ptr指針能夠擁有對(duì)象的所有權(quán)。它從設(shè)計(jì)上就禁止了拷貝構(gòu)造和賦值操作,原因在于如果允許拷貝構(gòu)造和賦值,就可能出現(xiàn)多個(gè)std::unique_ptr指向同一個(gè)對(duì)象,這樣在對(duì)象銷毀時(shí)就會(huì)出現(xiàn)多次釋放同一塊內(nèi)存等錯(cuò)誤情況,破壞了獨(dú)占所有權(quán)的語義。其禁止拷貝構(gòu)造和賦值操作是通過在類定義中,將拷貝構(gòu)造函數(shù)和賦值運(yùn)算符函數(shù)聲明為delete來實(shí)現(xiàn)的。例如:
class MyClass {
// 其他成員等定義
};
std::unique_ptr<MyClass> ptr1(new MyClass());
// 下面這行代碼會(huì)編譯報(bào)錯(cuò),因?yàn)閡nique_ptr禁止拷貝構(gòu)造
// std::unique_ptr<MyClass> ptr2 = ptr1;
std::unique_ptr<MyClass> ptr3;
// 下面這行代碼也會(huì)編譯報(bào)錯(cuò),因?yàn)閡nique_ptr禁止賦值操作
// ptr3 = ptr1;
// 可以通過移動(dòng)語義來轉(zhuǎn)移所有權(quán)
std::unique_ptr<MyClass> ptr4 = std::move(ptr1);
而創(chuàng)建std::unique_ptr對(duì)象有多種方式,比如可以直接使用new關(guān)鍵字來創(chuàng)建,像std::unique_ptr<int> ptr(new int(10));。另外,更推薦的方式是使用std::make_unique函數(shù)來創(chuàng)建,如auto ptr = std::make_unique<int>();。當(dāng)std::unique_ptr對(duì)象超出其作用域時(shí),它會(huì)自動(dòng)調(diào)用所管理對(duì)象的析構(gòu)函數(shù),釋放對(duì)應(yīng)的內(nèi)存資源,實(shí)現(xiàn)了對(duì)資源生命周期的有效管理。
⑵資源轉(zhuǎn)移
std::unique_ptr 通過移動(dòng)語義來實(shí)現(xiàn)資源的所有權(quán)轉(zhuǎn)移。關(guān)鍵在于std::move函數(shù)的使用,std::move函數(shù)并不會(huì)真正地移動(dòng)內(nèi)存中的數(shù)據(jù),而是將對(duì)象的所有權(quán)進(jìn)行轉(zhuǎn)移,把源對(duì)象的狀態(tài)標(biāo)記為 “可析構(gòu)”,目標(biāo)對(duì)象獲取到對(duì)資源的所有權(quán)。例如:
#include <iostream>
#include <memory>
class MyClass {
public:
MyClass() { std::cout << "MyClass Constructor" << std::endl; }
~MyClass() { std::cout << "MyClass Destructor" << std::endl; }
void display() const { std::cout << "Displaying MyClass" << std::endl; }
};
int main() {
std::unique_ptr<MyClass> ptr1(new MyClass());
// 使用std::move轉(zhuǎn)移所有權(quán)
std::unique_ptr<MyClass> ptr2 = std::move(ptr1);
if (!ptr1) {
std::cout << "ptr1 is now nullptr" << std::endl;
}
ptr2->display();
return 0;
}
在上述代碼中,通過std::move(ptr1)將ptr1對(duì)MyClass對(duì)象的所有權(quán)轉(zhuǎn)移給了ptr2,轉(zhuǎn)移后ptr1變?yōu)閚ullptr,而ptr2獲得了管理該對(duì)象的權(quán)限,可以正常調(diào)用對(duì)象的成員函數(shù)等進(jìn)行操作。
在函數(shù)返回值和容器中使用std::unique_ptr管理資源有著明顯的優(yōu)勢(shì)。比如在函數(shù)返回值方面,如果函數(shù)內(nèi)部創(chuàng)建了一個(gè)動(dòng)態(tài)分配的對(duì)象,使用std::unique_ptr來管理并返回,就可以將對(duì)象的所有權(quán)順利地傳遞給函數(shù)的調(diào)用者,避免了內(nèi)存泄漏的風(fēng)險(xiǎn),同時(shí)調(diào)用者也無需擔(dān)心資源釋放的問題,因?yàn)楫?dāng)對(duì)應(yīng)的std::unique_ptr超出作用域時(shí)會(huì)自動(dòng)釋放資源。在容器中,像std::vector<std::unique_ptr<MyClass>>這樣的定義,可以方便地存儲(chǔ)多個(gè)獨(dú)占資源的智能指針,容器會(huì)自動(dòng)管理這些std::unique_ptr的生命周期,進(jìn)而管理其所指向?qū)ο蟮纳芷?,使得代碼對(duì)資源管理更加清晰和安全。
應(yīng)用場(chǎng)景
適合使用std::unique_ptr的場(chǎng)景有很多。比如管理單個(gè)對(duì)象的生命周期,當(dāng)某個(gè)對(duì)象只在程序的某一部分有意義,并且在這部分結(jié)束后就應(yīng)該被銷毀時(shí),使用std::unique_ptr是很好的選擇。例如在一個(gè)函數(shù)內(nèi)部創(chuàng)建了一個(gè)臨時(shí)的文件讀取對(duì)象,當(dāng)函數(shù)執(zhí)行完畢,這個(gè)對(duì)象就應(yīng)該被釋放,就可以用std::unique_ptr來管理它。再比如在函數(shù)中返回獨(dú)占資源的情況,下面是一個(gè)簡(jiǎn)單示例:
#include <memory>
#include <iostream>
class Resource {
public:
Resource() { std::cout << "Resource created" << std::endl; }
~Resource() { std::cout << "Resource destroyed" << std::endl; }
void use() { std::cout << "Using the resource" << std::endl; }
};
std::unique_ptr<Resource> createResource() {
return std::make_unique<Resource>();
}
int main() {
auto ptr = createResource();
ptr->use();
return 0;
}
在上述代碼中,createResource函數(shù)創(chuàng)建并返回一個(gè)獨(dú)占資源的std::unique_ptr,在main函數(shù)中獲取后可以正常使用該資源,當(dāng)main函數(shù)結(jié)束時(shí),ptr超出作用域,對(duì)應(yīng)的資源會(huì)自動(dòng)被銷毀。通過這樣的方式,利用std::unique_ptr的獨(dú)占所有權(quán)特性,清晰且安全地管理了資源的生命周期,避免了內(nèi)存泄漏等問題。
3.3 std::shared_ptr
基本特性
std::shared_ptr 具有共享所有權(quán)的特性,允許多個(gè) std::shared_ptr 指針指向同一個(gè)對(duì)象,它們共同管理這個(gè)對(duì)象的生命周期。其核心的引用計(jì)數(shù)機(jī)制發(fā)揮著關(guān)鍵作用,引用計(jì)數(shù)用于記錄當(dāng)前有多少個(gè)智能指針對(duì)象正在引用該資源。
圖片
例如,以下是創(chuàng)建、拷貝、賦值和析構(gòu)過程中引用計(jì)數(shù)變化的示例:
#include <memory>
#include <iostream>
int main() {
// 創(chuàng)建一個(gè)shared_ptr,此時(shí)資源的引用計(jì)數(shù)為1
std::shared_ptr<int> ptr1 = std::make_shared<int>(42);
std::cout << "ptr1創(chuàng)建后引用計(jì)數(shù): " << ptr1.use_count() << std::endl;
// 拷貝構(gòu)造一個(gè)新的shared_ptr,引用計(jì)數(shù)增加為2
std::shared_ptr<int> ptr2 = ptr1;
std::cout << "ptr2拷貝構(gòu)造后引用計(jì)數(shù): " << ptr2.use_count() << std::endl;
// 賦值操作,引用計(jì)數(shù)不變(先減少左邊的引用計(jì)數(shù),再增加右邊的引用計(jì)數(shù))
std::shared_ptr<int> ptr3;
ptr3 = ptr2;
std::cout << "ptr3賦值后引用計(jì)數(shù): " << ptr3.use_count() << std::endl;
// 當(dāng)一個(gè)shared_ptr超出作用域,引用計(jì)數(shù)減少
{
std::shared_ptr<int> ptr4 = ptr3;
std::cout << "ptr4創(chuàng)建后引用計(jì)數(shù): " << ptr4.use_count() << std::endl;
}
std::cout << "ptr3引用計(jì)數(shù)(ptr4超出作用域后): " << ptr3.use_count() << std::endl;
return 0;
}
在上述代碼中,首先通過std::make_shared創(chuàng)建了ptr1,此時(shí)引用計(jì)數(shù)為 1。接著ptr2通過拷貝構(gòu)造ptr1,引用計(jì)數(shù)變?yōu)?2。ptr3通過賦值操作獲取ptr2管理的資源,由于賦值操作的機(jī)制,整體引用計(jì)數(shù)依然是 2(先對(duì)ptr3原指向資源引用計(jì)數(shù)減 1,若為 0 則釋放,再將其指向ptr2指向的資源并對(duì)該資源引用計(jì)數(shù)加 1)。當(dāng)ptr4超出其作用域時(shí),其析構(gòu)函數(shù)被調(diào)用,對(duì)應(yīng)的資源引用計(jì)數(shù)減 1,變回 1。當(dāng)引用計(jì)數(shù)最終變?yōu)?0 時(shí),表示沒有任何智能指針再引用該資源,此時(shí)資源會(huì)被自動(dòng)釋放,這就確保了資源在不再被使用時(shí)能夠及時(shí)、正確地被回收,避免了內(nèi)存泄漏的發(fā)生,同時(shí)支持多個(gè)智能指針安全地共享同一資源,提高了資源的利用率和程序的靈活性。
內(nèi)存管理
std::shared_ptr 能夠自動(dòng)管理所指向?qū)ο蟮膬?nèi)存。在對(duì)象創(chuàng)建方面,像前面提到的可以通過std::make_shared函數(shù)方便地創(chuàng)建并初始化一個(gè)std::shared_ptr對(duì)象,同時(shí)初始化其引用計(jì)數(shù)為 1。例如std::shared_ptr<int> ptr = std::make_shared<int>(10);,這樣就在堆上創(chuàng)建了一個(gè)int類型的對(duì)象,并由ptr進(jìn)行管理。
當(dāng)涉及到對(duì)象的銷毀以及內(nèi)存釋放時(shí)機(jī),是基于引用計(jì)數(shù)來決定的。只要有一個(gè)std::shared_ptr對(duì)象引用著該資源,資源對(duì)應(yīng)的內(nèi)存就會(huì)保持有效。只有當(dāng)最后一個(gè)引用該資源的std::shared_ptr被銷毀(比如超出作用域或者被手動(dòng)重置等情況),使得引用計(jì)數(shù)變?yōu)?0 時(shí),才會(huì)自動(dòng)調(diào)用對(duì)象的析構(gòu)函數(shù)來釋放其所占內(nèi)存。
然而,在多線程環(huán)境下,引用計(jì)數(shù)的操作就需要考慮線程安全性問題了。因?yàn)槎鄠€(gè)線程可能同時(shí)對(duì)同一個(gè)std::shared_ptr對(duì)象進(jìn)行拷貝、賦值或者析構(gòu)等操作,這就可能導(dǎo)致引用計(jì)數(shù)出現(xiàn)不一致的情況。幸運(yùn)的是,C++ 標(biāo)準(zhǔn)庫(kù)保證了std::shared_ptr的引用計(jì)數(shù)操作在常見的平臺(tái)實(shí)現(xiàn)上是原子性的,也就是在多線程環(huán)境下是線程安全的,無需我們額外去加鎖等進(jìn)行復(fù)雜的處理,就能確保多個(gè)線程對(duì)共享資源通過std::shared_ptr管理時(shí)不會(huì)因?yàn)椴l(fā)訪問引用計(jì)數(shù)而出錯(cuò)。
循環(huán)引用問題
std::shared_ptr 存在一種容易導(dǎo)致內(nèi)存泄漏的循環(huán)引用問題。例如下面這種場(chǎng)景:
class ClassA;
class ClassB;
class ClassA {
public:
std::shared_ptr<ClassB> ptrB;
};
class ClassB {
public:
std::shared_ptr<ClassA> ptrA;
};
int main() {
auto a = std::make_shared<ClassA>();
auto b = std::make_shared<ClassB>();
// 形成循環(huán)引用
a->ptrB = b;
b->ptrA = a;
// a和b離開作用域,但因?yàn)檠h(huán)引用,它們不會(huì)被銷毀
return 0;
}
在上述代碼中,ClassA的對(duì)象a中有一個(gè)std::shared_ptr指向ClassB的對(duì)象b,而ClassB的對(duì)象b又有一個(gè)std::shared_ptr指向ClassA的對(duì)象a,這樣就形成了一個(gè)閉環(huán)。當(dāng)a和b所在的作用域結(jié)束時(shí)(比如main函數(shù)結(jié)束),由于它們互相引用,各自的引用計(jì)數(shù)都是 2(初始化時(shí)為 1,互相賦值引用后加 1),在離開作用域時(shí)引用計(jì)數(shù)減 1,但最終都變?yōu)?1 而不是 0,所以它們的析構(gòu)函數(shù)都不會(huì)被調(diào)用,對(duì)應(yīng)的對(duì)象也就不會(huì)被銷毀,從而導(dǎo)致內(nèi)存泄漏。
為了解決這個(gè)問題,可以使用std::weak_ptr。std::weak_ptr是一種不增加引用計(jì)數(shù)的智能指針,它持有一個(gè)非擁有(non-owning)的引用。在上述例子中,我們可以將一個(gè)方向的std::shared_ptr替換為std::weak_ptr,比如將ClassB中的std::shared_ptr<ClassA> ptrA;修改為std::weak_ptr<ClassA> ptrA;,這樣ClassB對(duì)ClassA的引用就不會(huì)增加ClassA對(duì)象的引用計(jì)數(shù),當(dāng)ClassA對(duì)應(yīng)的外部std::shared_ptr(如a)超出作用域后,其引用計(jì)數(shù)能正常減為 0 并被銷毀,而ClassB中std::weak_ptr所關(guān)聯(lián)的ClassA對(duì)象即使不存在了也不會(huì)影響ClassB自身的銷毀,從而打破了循環(huán)引用,避免了內(nèi)存泄漏的發(fā)生。
3.4 std::weak_ptr
弱引用特性
std::weak_ptr 具有弱引用的特性,它不增加對(duì)象的引用計(jì)數(shù),僅僅是用于觀察對(duì)象的狀態(tài)。與std::shared_ptr不同,std::weak_ptr并不對(duì)對(duì)象的生命周期有控制權(quán),它更像是一個(gè)旁觀者。
圖片
例如,創(chuàng)建和使用std::weak_ptr的方法如下:
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sp = std::make_shared<int>(10);
std::weak_ptr<int> wp = sp; // 創(chuàng)建weak_ptr
// 檢查weak_ptr是否有效
if (auto temp_sp = wp.lock()) {
std::cout << "通過weak_ptr獲取到關(guān)聯(lián)的shared_ptr,值為: " << *temp_sp << std::endl;
} else {
std::cout << "The object has been destroyed." << std::endl;
}
return 0;
}
在上述代碼中,先創(chuàng)建了一個(gè)std::shared_ptr對(duì)象sp,然后通過它來初始化std::weak_ptr對(duì)象wp,wp并不改變sp所指向?qū)ο蟮囊糜?jì)數(shù)。接著通過wp.lock()嘗試獲取對(duì)應(yīng)的std::shared_ptr,如果對(duì)象還存在(也就是對(duì)應(yīng)的std::shared_ptr還沒被銷毀,引用計(jì)數(shù)不為 0),就能獲取到并進(jìn)行后續(xù)操作,否則返回nullptr。通過這樣的機(jī)制,std::weak_ptr可以在不影響對(duì)象生命周期的前提下,對(duì)對(duì)象的存在狀態(tài)進(jìn)行監(jiān)測(cè)。
解決循環(huán)引用
std::weak_ptr在解決std::shared_ptr之間的循環(huán)引用問題上有著重要作用。例如之前提到的循環(huán)引用的代碼示例:
class ClassA;
class ClassB;
class ClassA {
public:
std::shared_ptr<ClassB> ptrB;
};
class ClassB {
public:
std::weak_ptr<ClassA> ptrA; // 修改為weak_ptr
};
int main() {
auto a = std::make_shared<ClassA>();
auto b = std::make_shared<ClassB>();
a->ptrB = b;
b->ptrA = a;
// 當(dāng)main函數(shù)結(jié)束時(shí),A和B對(duì)象會(huì)被正確銷毀,因?yàn)闆]有循環(huán)引用
return 0;
}
在沒有使用std::weak_ptr之前,ClassA和ClassB互相用std::shared_ptr引用對(duì)方,導(dǎo)致循環(huán)引用,對(duì)象無法正常銷毀。而將ClassB中對(duì)ClassA的引用改為std::weak_ptr后,b->ptrA這個(gè)弱引用并不會(huì)增加a所指向ClassA對(duì)象的引用計(jì)數(shù),當(dāng)main函數(shù)結(jié)束,a對(duì)應(yīng)的std::shared_ptr超出作用域,其引用計(jì)數(shù)能正常減為 0,ClassA對(duì)象被銷毀,然后b中std::weak_ptr雖然還關(guān)聯(lián)著之前ClassA對(duì)象的位置,但它不會(huì)阻止資源釋放,之后b對(duì)應(yīng)的std::shared_ptr也能正常被銷毀,從而打破了循環(huán)引用,保證了對(duì)象能被正確地釋放,避免了內(nèi)存泄漏。在復(fù)雜的數(shù)據(jù)結(jié)構(gòu)中,比如存在對(duì)象之間相互關(guān)聯(lián)且可能出現(xiàn)類似循環(huán)依賴的情況時(shí),合理使用std::weak_ptr就能有效避免這種因?yàn)檠h(huán)引用導(dǎo)致的內(nèi)存管理問題,讓整個(gè)程序的內(nèi)存使用更加健康、穩(wěn)定。
有效期檢查
std::weak_ptr可以通過expired函數(shù)和lock函數(shù)來檢查所指向?qū)ο蟮挠行?。expired函數(shù)用于判斷所觀測(cè)的資源是否已經(jīng)被釋放,它返回一個(gè)bool值,如果返回true表示資源已經(jīng)不存在了(對(duì)應(yīng)的std::shared_ptr已經(jīng)被銷毀,引用計(jì)數(shù)為 0 了),如果返回false則表示資源還存在。例如:
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sp(new int(10));
std::weak_ptr<int> wp(sp);
std::cout << "1. wp " << (wp.expired()? "is" : "is not ") << "expired" << std::endl;
sp.reset();
std::cout << "2. wp " << (wp.expired()? "is" : "is not ") << "expired" << std::endl;
return 0;
}
在上述代碼中,先創(chuàng)建sp并關(guān)聯(lián)wp,開始時(shí)wp.expired()返回false,當(dāng)通過sp.reset()釋放了sp管理的資源后,再次調(diào)用wp.expired()就返回true了。
而lock函數(shù)則是用于獲取管理所監(jiān)測(cè)資源的std::shared_ptr對(duì)象,如果資源還存在,就返回對(duì)應(yīng)的非空std::shared_ptr,可以接著進(jìn)行對(duì)資源的操作;如果資源已經(jīng)不存在了,就返回nullptr。例如:
#include <iostream>
#include <memory>
int main() {
std::shared_ptr<int> sp = std::make_shared<int>(10);
std::weak_ptr<int> wp = sp;
if (auto valid_sp = wp.lock()) {
std::cout << *valid_sp << std::endl; // 輸出10,能正常訪問對(duì)象
}
sp.reset();
if (auto valid_sp = wp.lock()) {
std::cout << *valid_sp << std::endl; // 不會(huì)執(zhí)行,因?yàn)閷?duì)象已銷毀,獲取到的是空shared_ptr
}
return 0;
}
通過合理使用expired函數(shù)和lock函數(shù),就能在代碼中安全地利用std::weak_ptr來處理可能已經(jīng)銷毀或者還存在的對(duì)象,避免出現(xiàn)訪問非法內(nèi)存等問題,尤其在復(fù)雜的數(shù)據(jù)結(jié)構(gòu)或者涉及到對(duì)象生命周期不確定的場(chǎng)景中非常有用。
3.5定制刪除器
#define _CRT_SECURE_NO_WARNINGS 1
// 上述簡(jiǎn)單實(shí)現(xiàn)的 unique_ptr / shared_ptr / weak_ptr 是存在缺陷的
// 一個(gè)最大的缺陷就是釋放資源只能是默認(rèn)的 delete 處理
// 所以我們需要定制刪除器,可以通過仿函數(shù)或者lambda實(shí)現(xiàn)
#include <iostream>
// 定制刪除器
template<class T>
struct DeleteArray
{
void operator()(const T* ptr)
{
delete[] ptr;
}
};
int main()
{
std::shared_ptr<int> sp1(new int[10], DeleteArray<int>());
std::shared_ptr<std::string> sp2(new std::string[10], DeleteArray<std::string>());
std::shared_ptr<FILE> sp3(fopen("Test.cpp", "w"), [](FILE* ptr) {fclose(ptr); });
}
四、智能指針的使用技巧
4.1 選擇合適的智能指針類型
在實(shí)際編程中,選擇合適的智能指針類型至關(guān)重要,它直接關(guān)系到程序的性能、資源管理的有效性以及代碼的穩(wěn)定性。
當(dāng)我們需要獨(dú)占某個(gè)對(duì)象的所有權(quán),確保在對(duì)象的生命周期內(nèi)只有一個(gè)指針能夠訪問和管理它時(shí),std::unique_ptr是不二之選。例如,在一個(gè)函數(shù)內(nèi)部創(chuàng)建的對(duì)象,只在該函數(shù)內(nèi)部使用,并且不需要將其所有權(quán)傳遞給其他部分的代碼,就可以使用std::unique_ptr。像下面這樣的代碼場(chǎng)景:
#include <iostream>
#include <memory>
void processResource() {
// 使用std::unique_ptr獨(dú)占管理一個(gè)Resource對(duì)象
std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::cout << *ptr << std::endl;
// 函數(shù)結(jié)束時(shí),ptr自動(dòng)析構(gòu),所管理的int對(duì)象也被釋放
}
int main() {
processResource();
return 0;
}
在上述代碼中,processResource函數(shù)內(nèi)部創(chuàng)建的int對(duì)象通過std::unique_ptr進(jìn)行管理,當(dāng)函數(shù)執(zhí)行完畢,ptr超出作用域,其析構(gòu)函數(shù)會(huì)自動(dòng)釋放所指向的int對(duì)象,保證了資源的正確回收,同時(shí)避免了其他部分代碼對(duì)該對(duì)象的意外訪問和修改。
而當(dāng)多個(gè)對(duì)象需要共享同一塊內(nèi)存資源時(shí),std::shared_ptr就派上用場(chǎng)了。比如在一個(gè)多線程環(huán)境下,多個(gè)線程可能同時(shí)訪問和操作同一個(gè)對(duì)象,此時(shí)使用std::shared_ptr可以方便地實(shí)現(xiàn)資源的共享,并且保證對(duì)象在所有引用它的指針都銷毀后才被釋放。例如:
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
class SharedResource {
public:
SharedResource() {
std::cout << "SharedResource constructed." << std::endl;
}
~SharedResource() {
std::cout << "SharedResource destroyed." << std::endl;
}
void doSomething() {
std::cout << "Doing something with the shared resource." << std::endl;
}
};
void threadFunction(std::shared_ptr<SharedResource> ptr) {
ptr->doSomething();
}
int main() {
// 創(chuàng)建一個(gè)指向SharedResource對(duì)象的shared_ptr
std::shared_ptr<SharedResource> sharedPtr = std::make_shared<SharedResource>();
std::vector<std::thread> threads;
// 創(chuàng)建多個(gè)線程,每個(gè)線程都傳入共享的shared_ptr
for (int i = 0; i < 5; ++i) {
threads.push_back(std::thread(threadFunction, sharedPtr));
}
// 等待所有線程完成
for (auto& th : threads) {
th.join();
}
return 0;
}
在上述代碼中,SharedResource對(duì)象通過std::shared_ptr進(jìn)行管理,在多個(gè)線程中都可以安全地訪問和操作這個(gè)共享對(duì)象。每個(gè)線程函數(shù)threadFunction都接受一個(gè)std::shared_ptr作為參數(shù),這樣多個(gè)線程就可以共享同一個(gè)SharedResource對(duì)象,而對(duì)象的生命周期由std::shared_ptr的引用計(jì)數(shù)機(jī)制來自動(dòng)管理,當(dāng)所有線程都結(jié)束,不再有std::shared_ptr指向該對(duì)象時(shí),對(duì)象會(huì)被自動(dòng)銷毀。
然而,正如前面所提到的,std::shared_ptr在使用過程中可能會(huì)出現(xiàn)循環(huán)引用的問題。為了避免這種情況,當(dāng)我們遇到對(duì)象之間存在相互引用,但又不希望因?yàn)檫@種引用關(guān)系導(dǎo)致內(nèi)存泄漏時(shí),就需要引入std::weak_ptr。例如在一個(gè)樹形數(shù)據(jù)結(jié)構(gòu)中,節(jié)點(diǎn)之間可能存在父子節(jié)點(diǎn)的相互引用,如果使用std::shared_ptr來管理節(jié)點(diǎn),就很容易出現(xiàn)循環(huán)引用,導(dǎo)致節(jié)點(diǎn)無法正常釋放。此時(shí),我們可以將父節(jié)點(diǎn)對(duì)子節(jié)點(diǎn)的引用使用std::shared_ptr,而子節(jié)點(diǎn)對(duì)父節(jié)點(diǎn)的引用使用std::weak_ptr,這樣就可以打破循環(huán)引用,保證對(duì)象能夠在合適的時(shí)候被正確銷毀。
4.2 選擇合適的智能指針類型
在實(shí)際編程中,選擇合適的智能指針類型至關(guān)重要,它直接關(guān)系到程序的性能、資源管理的有效性以及代碼的穩(wěn)定性。
當(dāng)我們需要獨(dú)占某個(gè)對(duì)象的所有權(quán),確保在對(duì)象的生命周期內(nèi)只有一個(gè)指針能夠訪問和管理它時(shí),std::unique_ptr是不二之選。例如,在一個(gè)函數(shù)內(nèi)部創(chuàng)建的對(duì)象,只在該函數(shù)內(nèi)部使用,并且不需要將其所有權(quán)傳遞給其他部分的代碼,就可以使用std::unique_ptr。像下面這樣的代碼場(chǎng)景:
#include <iostream>
#include <memory>
void processResource() {
// 使用std::unique_ptr獨(dú)占管理一個(gè)Resource對(duì)象
std::unique_ptr<int> ptr = std::make_unique<int>(42);
std::cout << *ptr << std::endl;
// 函數(shù)結(jié)束時(shí),ptr自動(dòng)析構(gòu),所管理的int對(duì)象也被釋放
}
int main() {
processResource();
return 0;
}
在上述代碼中,processResource函數(shù)內(nèi)部創(chuàng)建的int對(duì)象通過std::unique_ptr進(jìn)行管理,當(dāng)函數(shù)執(zhí)行完畢,ptr超出作用域,其析構(gòu)函數(shù)會(huì)自動(dòng)釋放所指向的int對(duì)象,保證了資源的正確回收,同時(shí)避免了其他部分代碼對(duì)該對(duì)象的意外訪問和修改。
而當(dāng)多個(gè)對(duì)象需要共享同一塊內(nèi)存資源時(shí),std::shared_ptr就派上用場(chǎng)了。比如在一個(gè)多線程環(huán)境下,多個(gè)線程可能同時(shí)訪問和操作同一個(gè)對(duì)象,此時(shí)使用std::shared_ptr可以方便地實(shí)現(xiàn)資源的共享,并且保證對(duì)象在所有引用它的指針都銷毀后才被釋放。例如:
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
class SharedResource {
public:
SharedResource() {
std::cout << "SharedResource constructed." << std::endl;
}
~SharedResource() {
std::cout << "SharedResource destroyed." << std::endl;
}
void doSomething() {
std::cout << "Doing something with the shared resource." << std::endl;
}
};
void threadFunction(std::shared_ptr<SharedResource> ptr) {
ptr->doSomething();
}
int main() {
// 創(chuàng)建一個(gè)指向SharedResource對(duì)象的shared_ptr
std::shared_ptr<SharedResource> sharedPtr = std::make_shared<SharedResource>();
std::vector<std::thread> threads;
// 創(chuàng)建多個(gè)線程,每個(gè)線程都傳入共享的shared_ptr
for (int i = 0; i < 5; ++i) {
threads.push_back(std::thread(threadFunction, sharedPtr));
}
// 等待所有線程完成
for (auto& th : threads) {
th.join();
}
return 0;
}
在上述代碼中,SharedResource對(duì)象通過std::shared_ptr進(jìn)行管理,在多個(gè)線程中都可以安全地訪問和操作這個(gè)共享對(duì)象。每個(gè)線程函數(shù)threadFunction都接受一個(gè)std::shared_ptr作為參數(shù),這樣多個(gè)線程就可以共享同一個(gè)SharedResource對(duì)象,而對(duì)象的生命周期由std::shared_ptr的引用計(jì)數(shù)機(jī)制來自動(dòng)管理,當(dāng)所有線程都結(jié)束,不再有std::shared_ptr指向該對(duì)象時(shí),對(duì)象會(huì)被自動(dòng)銷毀。
然而,正如前面所提到的,std::shared_ptr在使用過程中可能會(huì)出現(xiàn)循環(huán)引用的問題。為了避免這種情況,當(dāng)我們遇到對(duì)象之間存在相互引用,但又不希望因?yàn)檫@種引用關(guān)系導(dǎo)致內(nèi)存泄漏時(shí),就需要引入std::weak_ptr。例如在一個(gè)樹形數(shù)據(jù)結(jié)構(gòu)中,節(jié)點(diǎn)之間可能存在父子節(jié)點(diǎn)的相互引用,如果使用std::shared_ptr來管理節(jié)點(diǎn),就很容易出現(xiàn)循環(huán)引用,導(dǎo)致節(jié)點(diǎn)無法正常釋放。此時(shí),我們可以將父節(jié)點(diǎn)對(duì)子節(jié)點(diǎn)的引用使用std::shared_ptr,而子節(jié)點(diǎn)對(duì)父節(jié)點(diǎn)的引用使用std::weak_ptr,這樣就可以打破循環(huán)引用,保證對(duì)象能夠在合適的時(shí)候被正確銷毀。
4.3 與容器的結(jié)合使用
智能指針與 C++ 標(biāo)準(zhǔn)容器的結(jié)合使用,為我們?cè)诠芾韺?duì)象集合時(shí)提供了極大的便利,同時(shí)也能有效地避免內(nèi)存泄漏和懸空指針等問題。
在容器中存儲(chǔ)智能指針時(shí),我們可以像存儲(chǔ)普通對(duì)象一樣將智能指針放入容器中。例如,使用std::vector來存儲(chǔ)std::unique_ptr指向的對(duì)象:
#include <iostream>
#include <memory>
#include <vector>
class MyClass {
public:
MyClass(int num) : num_(num) {
std::cout << "MyClass " << num_ << " constructed." << std::endl;
}
~MyClass() {
std::cout << "MyClass " << num_ << " destroyed." << std::endl;
}
void print() const {
std::cout << "MyClass " << num_ << std::endl;
}
private:
int num_;
};
int main() {
std::vector<std::unique_ptr<MyClass>> vec;
// 創(chuàng)建多個(gè)MyClass對(duì)象,并通過unique_ptr管理,放入向量中
for (int i = 0; i < 5; ++i) {
vec.push_back(std::make_unique<MyClass>(i));
}
// 遍歷向量,調(diào)用每個(gè)對(duì)象的print函數(shù)
for (const auto& ptr : vec) {
ptr->print();
}
return 0;
}
在上述代碼中,std::vector存儲(chǔ)了std::unique_ptr<MyClass>類型的元素,每個(gè)std::unique_ptr都獨(dú)占管理一個(gè)MyClass對(duì)象。通過這種方式,我們可以方便地管理一組對(duì)象,并且不用擔(dān)心對(duì)象的生命周期問題,因?yàn)楫?dāng)std::unique_ptr超出作用域時(shí)(例如從容器中移除或者容器本身被銷毀),它所管理的對(duì)象會(huì)自動(dòng)被析構(gòu),從而避免了內(nèi)存泄漏。
當(dāng)使用std::shared_ptr與容器結(jié)合時(shí),同樣可以實(shí)現(xiàn)對(duì)象的共享管理。例如,在一個(gè)std::list中存儲(chǔ)std::shared_ptr指向的對(duì)象:
#include <iostream>
#include <memory>
#include <list>
class SharedResource {
public:
SharedResource() {
std::cout << "SharedResource constructed." << std::endl;
}
~SharedResource() {
std::cout << "SharedResource destroyed." << std::endl;
}
void doSomething() {
std::cout << "Doing something with the shared resource." << std::endl;
}
};
int main() {
std::list<std::shared_ptr<SharedResource>> myList;
// 創(chuàng)建一個(gè)SharedResource對(duì)象,并通過shared_ptr管理,放入列表中
std::shared_ptr<SharedResource> sharedPtr = std::make_shared<SharedResource>();
myList.push_back(sharedPtr);
// 從列表中取出shared_ptr,并調(diào)用對(duì)象的方法
for (const auto& ptr : myList) {
ptr->doSomething();
}
return 0;
}
在這個(gè)例子中,std::list中的多個(gè)元素可以共享同一個(gè)SharedResource對(duì)象,通過std::shared_ptr的引用計(jì)數(shù)機(jī)制來確保對(duì)象在所有引用它的指針都被銷毀后才被釋放,保證了資源的正確管理。
需要注意的是,在使用容器存儲(chǔ)智能指針時(shí),要避免一些可能導(dǎo)致問題的操作。例如,不要在容器中存儲(chǔ)已經(jīng)被析構(gòu)的智能指針,否則可能會(huì)導(dǎo)致未定義行為。同時(shí),當(dāng)對(duì)容器進(jìn)行插入、刪除或者修改操作時(shí),要確保智能指針的生命周期仍然在有效的控制范圍內(nèi),以防止出現(xiàn)懸空指針或者內(nèi)存泄漏的情況。
五、智能指針的性能分析
5.1 內(nèi)存開銷
在分析智能指針的內(nèi)存開銷時(shí),我們需要考慮多個(gè)因素,包括引用計(jì)數(shù)的存儲(chǔ)、控制塊的大小等。
std::shared_ptr的內(nèi)存占用相對(duì)較大。它除了要存儲(chǔ)指向?qū)ο蟮闹羔樛?,還需要維護(hù)一個(gè)引用計(jì)數(shù),以及一個(gè)包含引用計(jì)數(shù)、弱引用計(jì)數(shù)、刪除器、分配器等信息的控制塊。在常見的編譯器和運(yùn)行環(huán)境下,一個(gè)std::shared_ptr對(duì)象的大小通常是裸指針大小的兩倍。例如,在 64 位系統(tǒng)中,裸指針的大小為 8 字節(jié),而std::shared_ptr的大小可能達(dá)到 16 字節(jié)左右。這是因?yàn)樗枰~外的空間來存儲(chǔ)引用計(jì)數(shù)和控制塊信息,以實(shí)現(xiàn)資源的共享和生命周期的管理。
以下是一個(gè)簡(jiǎn)單的代碼示例,用于展示std::shared_ptr的內(nèi)存占用情況:
#include <iostream>
#include <memory>
class MyClass {
public:
int data;
};
int main() {
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
std::cout << "Size of std::shared_ptr: " << sizeof(ptr) << " bytes" << std::endl;
std::cout << "Size of raw pointer: " << sizeof(MyClass*) << " bytes" << std::endl;
return 0;
}
在上述代碼中,通過sizeof運(yùn)算符可以大致了解std::shared_ptr和裸指針的內(nèi)存占用情況。
相比之下,std::unique_ptr的內(nèi)存開銷則較小。它只需要存儲(chǔ)指向?qū)ο蟮闹羔槪恍枰~外的引用計(jì)數(shù)和控制塊,因此其大小與裸指針基本相同。在 64 位系統(tǒng)中,std::unique_ptr的大小通常也為 8 字節(jié),與指向相同類型對(duì)象的裸指針大小一致。例如:
#include <iostream>
#include <memory>
class MyClass {
public:
int data;
};
int main() {
std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
std::cout << "Size of std::unique_ptr: " << sizeof(ptr) << " bytes" << std::endl;
std::cout << "Size of raw pointer: " << sizeof(MyClass*) << " bytes" << std::endl;
return 0;
}
在對(duì)內(nèi)存敏感的場(chǎng)景中,如嵌入式系統(tǒng)開發(fā)或者對(duì)內(nèi)存使用要求極為嚴(yán)格的高性能計(jì)算場(chǎng)景,如果不需要資源的共享,應(yīng)優(yōu)先考慮使用std::unique_ptr,以減少不必要的內(nèi)存開銷。
5.2 運(yùn)行時(shí)效率
在運(yùn)行時(shí)效率方面,智能指針的不同操作會(huì)帶來不同程度的開銷。
std::shared_ptr的拷貝和賦值操作相對(duì)較為復(fù)雜,因?yàn)樗鼈冃枰乱糜?jì)數(shù),這涉及到原子操作(在多線程環(huán)境下)或者簡(jiǎn)單的計(jì)數(shù)增減(在單線程環(huán)境下),會(huì)帶來一定的性能開銷。例如,在一個(gè)頻繁進(jìn)行對(duì)象拷貝和賦值的場(chǎng)景中,如果使用std::shared_ptr,可能會(huì)導(dǎo)致程序的執(zhí)行速度變慢。
#include <iostream>
#include <memory>
#include <vector>
class MyClass {
public:
MyClass() {}
~MyClass() {}
};
int main() {
std::vector<std::shared_ptr<MyClass>> vec;
for (int i = 0; i < 1000000; ++i) {
// 頻繁創(chuàng)建和拷貝shared_ptr
std::shared_ptr<MyClass> ptr = std::make_shared<MyClass>();
vec.push_back(ptr);
}
return 0;
}
在上述代碼中,創(chuàng)建了大量的std::shared_ptr并進(jìn)行拷貝操作,會(huì)消耗一定的時(shí)間和資源來維護(hù)引用計(jì)數(shù)。
std::unique_ptr的移動(dòng)操作則相對(duì)高效,因?yàn)樗皇呛?jiǎn)單地轉(zhuǎn)移了對(duì)象的所有權(quán),不需要進(jìn)行復(fù)雜的計(jì)數(shù)操作,類似于將一個(gè)指針賦值給另一個(gè)指針,開銷較小。例如:
#include <iostream>
#include <memory>
#include <vector>
class MyClass {
public:
MyClass() {}
~MyClass() {}
};
int main() {
std::vector<std::unique_ptr<MyClass>> vec;
for (int i = 0; i < 1000000; ++i) {
// 頻繁創(chuàng)建和移動(dòng)unique_ptr
std::unique_ptr<MyClass> ptr = std::make_unique<MyClass>();
vec.push_back(std::move(ptr));
}
return 0;
}
在多線程環(huán)境下,std::shared_ptr的引用計(jì)數(shù)操作是原子性的,這保證了在多個(gè)線程同時(shí)對(duì)同一個(gè)std::shared_ptr進(jìn)行拷貝、賦值或者析構(gòu)等操作時(shí),引用計(jì)數(shù)的正確性,避免了數(shù)據(jù)競(jìng)爭(zhēng)和內(nèi)存泄漏等問題。但原子操作本身會(huì)帶來一定的性能開銷,相比之下,std::unique_ptr在多線程環(huán)境下,如果不需要共享資源,其獨(dú)占所有權(quán)的特性使得它在并發(fā)場(chǎng)景中更加高效,不需要額外的同步機(jī)制來保證引用計(jì)數(shù)的正確性。
為了優(yōu)化智能指針的性能,可以考慮以下幾點(diǎn):
- 在不需要共享資源的情況下,盡量使用std::unique_ptr,避免std::shared_ptr的引用計(jì)數(shù)開銷。
- 對(duì)于std::shared_ptr,盡量減少不必要的拷貝和賦值操作,可以通過合理的對(duì)象設(shè)計(jì)和編程邏輯,減少對(duì)象的生命周期交叉,從而降低引用計(jì)數(shù)的更新頻率。
- 在多線程環(huán)境下,如果使用std::shared_ptr,要注意避免頻繁的線程切換和競(jìng)爭(zhēng),盡量將共享資源的訪問和操作集中在一個(gè)線程或者通過合適的同步機(jī)制進(jìn)行協(xié)調(diào),以減少原子操作的開銷。
通過實(shí)際的性能測(cè)試數(shù)據(jù)可以更直觀地了解智能指針的性能差異。例如,使用專業(yè)的性能測(cè)試工具,對(duì)不同智能指針在相同操作場(chǎng)景下的執(zhí)行時(shí)間、內(nèi)存使用情況等指標(biāo)進(jìn)行測(cè)量,可以發(fā)現(xiàn)std::unique_ptr在簡(jiǎn)單的對(duì)象生命周期管理場(chǎng)景中,執(zhí)行速度通常比std::shared_ptr快,尤其是在對(duì)象頻繁創(chuàng)建和銷毀的情況下。而std::shared_ptr在需要資源共享的場(chǎng)景中,雖然存在一定的性能開銷,但它提供的共享機(jī)制是std::unique_ptr無法替代的,在實(shí)際應(yīng)用中需要根據(jù)具體的需求來權(quán)衡選擇合適的智能指針類型,并結(jié)合適當(dāng)?shù)膬?yōu)化策略,以達(dá)到最佳的性能表現(xiàn)。
六、全文總結(jié)
C++ 智能指針作為現(xiàn)代 C++ 編程中不可或缺的一部分,為我們解決了長(zhǎng)期以來困擾程序員的內(nèi)存管理難題。通過 RAII 機(jī)制和引用計(jì)數(shù)等核心技術(shù),智能指針實(shí)現(xiàn)了對(duì)象生命周期的自動(dòng)化管理,大大減少了因手動(dòng)內(nèi)存管理而導(dǎo)致的內(nèi)存泄漏、懸空指針等問題,提高了程序的穩(wěn)定性和可靠性。
在 C++11 中引入的std::shared_ptr、std::unique_ptr和std::weak_ptr三種智能指針類型,各有其獨(dú)特的特性和適用場(chǎng)景。std::shared_ptr通過引用計(jì)數(shù)實(shí)現(xiàn)資源的共享,允許多個(gè)指針指向同一對(duì)象,但需要注意循環(huán)引用的問題;std::unique_ptr則強(qiáng)調(diào)獨(dú)占所有權(quán),具有高效、安全的特點(diǎn),適用于大多數(shù)只需要單一所有者的對(duì)象管理場(chǎng)景;std::weak_ptr作為std::shared_ptr的補(bǔ)充,用于解決循環(huán)引用問題,并提供了對(duì)對(duì)象的弱引用訪問,使得我們能夠更加靈活地處理對(duì)象之間的關(guān)系。
在實(shí)際使用智能指針時(shí),我們需要根據(jù)具體的需求選擇合適的智能指針類型,并遵循一些最佳實(shí)踐和技巧,如避免常見的陷阱、合理與容器結(jié)合使用等,以充分發(fā)揮智能指針的優(yōu)勢(shì),同時(shí)避免可能出現(xiàn)的問題。
雖然智能指針在一定程度上增加了一些內(nèi)存開銷和運(yùn)行時(shí)的性能成本,但與它所帶來的好處相比,這些代價(jià)是值得的。而且,隨著 C++ 語言的不斷發(fā)展,智能指針也在持續(xù)優(yōu)化和改進(jìn),未來我們有理由期待它在性能和功能上會(huì)有更好的表現(xiàn)。