C++ 智能指針內存泄漏問題

shared_ptr相互嵌套導致循環引用

代碼示例

#include <iostream>
#include <memory>
using namespace std;class B;class A {
public:std::shared_ptr<B> b_ptr;~A() { std::cout << "A destroyed\n"; }
};class B {
public:std::shared_ptr<A> a_ptr;~B() { std::cout << "B destroyed\n"; }
};int main() {// 創建 shared_ptr 對象auto a = std::make_shared<A>();auto b = std::make_shared<B>();// 相互引用a->b_ptr = b;b->a_ptr = a;cout<<"use_count of a:"<<a.use_count()<<endl;cout<<"use_count of b:"<<b.use_count()<<endl;return 0;
}

解釋說明

  1. 創建了兩個?std::shared_ptr?對象?a?和?b
  2. a?持有?b?的?shared_ptrb?持有?a?的?shared_ptr
  3. 當?main?函數結束時,a?和?b?的引用計數不會減少到零,因此它們的析構函數不會被調用。
  4. 導致內存泄漏,因為對象?A?和?B?的內存不會被釋放。

?解決方法

為了避免這種循環引用的問題,可以使用 std::weak_ptrstd::weak_ptr 是一種弱智能指針,它不會增加對象的引用計數。它可以用來打破循環引用,從而防止內存泄漏。

#include <iostream>
#include <memory>
using namespace std;
class B;  // 先聲明類 B,使得 A 和 B 可以互相引用。class A {
public:std::shared_ptr<B> b_ptr; // A 擁有 B 的強引用~A() { std::cout << "A destroyed\n"; }
};class B {
public:std::weak_ptr<A> a_ptr; // B 擁有 A 的弱引用~B() { std::cout << "B destroyed\n"; }void safeAccess() {// 嘗試鎖定 a_ptr 獲取 shared_ptrif (auto a_shared = a_ptr.lock()) {// 安全訪問 a_shared 對象std::cout << "Accessing A from B\n";} else {std::cout << "A is already destroyed, cannot access A from B\n";}}
};int main() {// 創建 shared_ptr 對象auto a = std::make_shared<A>();auto b = std::make_shared<B>();// 互相引用a->b_ptr = b;b->a_ptr = a;// 安全訪問b->safeAccess();cout<<"use_count of a:"<<a.use_count()<<endl;cout<<"use_count of b:"<<b.use_count()<<endl;return 0; // 在這里,a 和 b 的引用計數將會正確地減少到零,并且它們將會被銷毀。
}

shared_ptr的層次使用沒有導致循環引用

shared_ptr<vector<shared_ptr<pair<string, shared_ptr<string>>>>> jsFiles;

這個聲明表示 jsFiles 是一個 std::shared_ptr,它指向一個 std::vector,向量中的每個元素是一個 std::shared_ptr,指向一個 std::pair 對象,而這個 std::pair 對象中包含一個 std::string 和一個 std::shared_ptr<std::string>。它們之間只是層次結構,沒有跨層次的相互引用?。也就是說沒有內存泄漏的問題。證明如下:

#include <iostream>
#include <vector>
#include <memory>
#include <string>using namespace std;
// 自定義 String 類,模擬 std::string
class MyString {
public:std::string data;MyString(const std::string& str) : data(str) {std::cout << "MyString created: " << data << std::endl;}~MyString() {std::cout << "MyString destroyed: " << data << std::endl;}// 添加輸出操作符重載friend std::ostream& operator<<(std::ostream& os, const MyString& myStr) {os << myStr.data;return os;}
};// 自定義 Pair 類,模擬 std::pair
template<typename K, typename V>
class MyPair {
public:K first;V second;MyPair(const K& key, const V& value) : first(key), second(value) {std::cout << "MyPair created: {" << first << ", " << *second << "}" << std::endl;}~MyPair() {std::cout << "MyPair destroyed: {" << first << ", " << *second << "}" << std::endl;}
};int main() {// 創建 jsFiles,它是一個 shared_ptr,指向 vectorauto jsFiles = std::make_shared<std::vector<std::shared_ptr<MyPair<std::string, std::shared_ptr<MyString>>>>>();// 添加元素auto innerPair1 = std::make_shared<MyPair<std::string, std::shared_ptr<MyString>>>("file1", std::make_shared<MyString>("content of file1"));auto innerPair2 = std::make_shared<MyPair<std::string, std::shared_ptr<MyString>>>("file2", std::make_shared<MyString>("content of file2"));jsFiles->push_back(innerPair1);jsFiles->push_back(innerPair2);// 訪問元素for (const auto& pairPtr : *jsFiles) {std::cout << "Filename: " << pairPtr->first << ", Content: " << *pairPtr->second << std::endl;}// 離開作用域時,智能指針會自動銷毀它們管理的對象return 0;
}

同時也證明了一個結論,構造函數和析構函數的調用順序是相反的。?

回調函數中的循環引用問題

值捕獲

#include <iostream>
#include <memory>
#include <functional>class MyClass {
public:MyClass() { std::cout << "MyClass created" << std::endl; }~MyClass() { std::cout << "MyClass destroyed" << std::endl; }void setCallback(std::function<void()> cb) {callback_ = cb;}void executeCallback() {if (callback_) {callback_();}}private:std::function<void()> callback_;
};void createNoLeak() {auto myObject = std::make_shared<MyClass>();myObject->setCallback([=]() {std::cout << "Callback executed, myObject use count: " << myObject.use_count() << std::endl;});myObject->executeCallback();}int main() {createNoLeak();std::cout << "End of program" << std::endl;return 0;
}

可以看出myObject最后沒有調用析構函數,是shared_ptr循環引用了。

引用捕獲

如果換為引用捕獲,則不會造成?shared_ptr循環引用。雖然這種方式不會增加引用計數,但需要特別注意捕獲對象的生命周期,防止在 lambda 被調用時,對象已經被銷毀,從而導致未定義行為。

如何解決?

#include <iostream>
#include <memory>
#include <functional>class MyClass : public std::enable_shared_from_this<MyClass> {
public:MyClass() { std::cout << "MyClass created" << std::endl; }~MyClass() { std::cout << "MyClass destroyed" << std::endl; }void setCallback(std::function<void()> cb) {callback_ = cb;}void executeCallback() {if (callback_) {callback_();}}private:std::function<void()> callback_;
};void createNoLeak() {auto myObject = std::make_shared<MyClass>();std::weak_ptr<MyClass> weakPtr = myObject;myObject->setCallback([weakPtr]() {if (auto sharedPtr = weakPtr.lock()) {std::cout << "Callback executed, object is valid" << std::endl;} else {std::cout << "Object already destroyed" << std::endl;}});myObject->executeCallback();// 這里 myObject 是按 weak_ptr 捕獲,當 createNoLeak() 結束時,myObject 的生命周期也就結束了,并且引用計數=0
}int main() {createNoLeak();std::cout << "End of program" << std::endl;return 0;
}

  • weakPtr.lock()?的使用:持有 std::weak_ptr,并且需要檢查或者使用其管理的對象。如果對象仍然存在(即它的 shared_ptr 引用計數大于零),我們希望獲取一個 shared_ptr 來安全地使用該對象。否則,weak_ptr.lock() 返回一個空的 shared_ptr
  • std::enable_shared_from_this 是一個非常有用的標準庫模板類,用于解決一個特定的問題: 當一個類的成員函數需要創建一個指向自己(this)的 std::shared_ptr 時,這類問題如何安全地實現。

std::enable_shared_from_this

背景問題

在使用 std::shared_ptr 管理對象時,有時會遇到需要在類的成員函數中獲取該對象的 shared_ptr 的情況。例如,在一個類的成員函數中,如果想要得到一個指向該對象的 shared_ptr,不能簡單地使用 std::shared_ptr<MyClass>(this),因為這會創建一個新的 shared_ptr,而不是增加現有的 shared_ptr 的引用計數。這可能導致對象被提前銷毀或者多次銷毀。

std::enable_shared_from_this?的作用

通過繼承 std::enable_shared_from_this,類就能夠安全地使用 shared_from_this 方法,從而獲取一個 shared_ptr,該 shared_ptr 與其他 shared_ptr 共享所有權,而不會重復增加引用計數。

使用示例

#include <iostream>
#include <memory>// 定義 MyClass 繼承 std::enable_shared_from_this<MyClass>
class MyClass : public std::enable_shared_from_this<MyClass> {
public:MyClass() { std::cout << "MyClass created" << std::endl; }~MyClass() { std::cout << "MyClass destroyed" << std::endl; }// 一個成員函數,它需要返回一個指向自身的 shared_ptrstd::shared_ptr<MyClass> getSharedPtr() {// 使用 shared_from_this 返回一個 shared_ptrreturn shared_from_this();}void doSomething() {auto ptr = shared_from_this(); // 獲取 shared_ptrstd::cout << "Doing something with MyClass instance, ref count: " << ptr.use_count() << std::endl;}
};void exampleFunction() {// 創建 MyClass 對象的 shared_ptrauto myObject = std::make_shared<MyClass>();// 調用成員函數獲取 shared_ptrauto mySharedPtr = myObject->getSharedPtr();std::cout << "Reference count after getSharedPtr: " << mySharedPtr.use_count() << std::endl;myObject->doSomething();
}int main() {exampleFunction();return 0;
}

注意

1.創建對象:
只有通過?std::shared_ptr?創建或管理的對象,才能安全地使用?shared_from_this

2. 保護避免使用 new 操作符:
直接使用 new 操作符創建的對象不能正確使用 shared_from_this,這樣做可能會導致未定義行為(例如崩潰)。

為什么?std::enable_shared_from_this?是必要的?

std::enable_shared_from_this 內部維護了一個弱引用(std::weak_ptr)指向當前對象。這個弱引用確保不會增加引用計數,同時允許 shared_from_this 方法安全地獲取 std::shared_ptr,從而真正共享管理的對象,避免不安全的重復引用計數增加。

通過這樣做,C++ STL 提供了一種方便而安全的方式來管理對象的生命周期,特別是在需要從對象內部生成 shared_ptr的情境下。

總結

通過繼承 std::enable_shared_from_thisMyClass 能夠安全地在其成員函數中創建返回指向自身的 std::shared_ptr,避免不必要的重復引用計數,從而有效地管理和共享對象生命周期。這樣既提升了代碼的安全性,也使得對象生命周期管理變得更加簡潔和直觀。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/39800.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/39800.shtml
英文地址,請注明出處:http://en.pswp.cn/web/39800.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

數據結構 1.1 數據結構的基本概念

本章總覽&#xff1a; 一.什么是數據 1.數據 數據是信息的載體&#xff0c;是描述客觀事物屬性的數、字符及所有能輸入到計算機中并被計算機程 序識別和處理的符號的集合。數據是計算機程序加工的原料。 早期計算機只能處理純數值的問題&#xff0c;如世界第一題計算機ENI…

轉讓北京文化傳媒公司帶營業性演出經紀許可證

影視文化傳播倡導將健康的影視文化有效傳播給觀眾&#xff0c;從而構建觀眾與電影制作者的良 性溝通與互動&#xff0c;是溝通電影制作者與電影受眾的重要橋梁。影視文化泛指以電影&#xff0c;電視方式所進行的全部文化創造&#xff0c;即體現為電影&#xff0c;電視全部的存在…

Java-List集合堆內存溢出

Java-List集合堆內存溢出 情況一情況二對照分析對照規定堆內存 情況一 往List<Object>的集合中不斷插入元素&#xff0c;集合底層的數組會不斷擴容&#xff0c;從0 -> 10 -> 10 10>>1…。最終出現堆內存溢出&#xff0c;是在擴容數組大小的時候。這里的過程…

【應屆應知應會】SQL常用知識點50道

SueWakeup 個人主頁&#xff1a;SueWakeup 系列專欄&#xff1a;借他一雙眼&#xff0c;愿這盛世如先生所愿 個性簽名&#xff1a;人生乏味啊&#xff0c;我欲令之光怪陸離 本文封面由 凌七七~? 友情提供 目錄 數據庫的概念 (什么是數據庫) RDBMS NOSQL 數據庫的分類 …

Qt涂鴉板

Qt版本&#xff1a;Qt6 具體代碼&#xff1a; 頭文件 dialog.h #ifndef DIALOG_H #define DIALOG_H#include <QDialog>QT_BEGIN_NAMESPACE namespace Ui { class Dialog; } QT_END_NAMESPACEclass Dialog : public QDialog {Q_OBJECTpublic:Dialog(QWidget *parent n…

0145__contain_of的原理與實現

contain_of的原理與實現_contain of-CSDN博客

從零開始!Jupyter Notebook的安裝教程

引言 Jupyter Notebook作為一種交互式的開發環境&#xff0c;已經成為數據科學和機器學習領域中不可或缺的工具之一。它能夠將代碼、文本、圖像和數據結合在一個靈活的文檔中&#xff0c;使得數據分析和可視化變得更加直觀和高效。 本文將詳細介紹Jupyter Notebook的安裝過程…

深入理解 Git `git add -p` 命令中的交互選項

個人名片 &#x1f393;作者簡介&#xff1a;java領域優質創作者 &#x1f310;個人主頁&#xff1a;碼農阿豪 &#x1f4de;工作室&#xff1a;新空間代碼工作室&#xff08;提供各種軟件服務&#xff09; &#x1f48c;個人郵箱&#xff1a;[2435024119qq.com] &#x1f4f1…

500mA、低壓差、低噪聲、超快、無需旁路電容的CMOS LDO穩壓器RT9013

一般描述 RT9013 SOT23-5封裝的外觀和絲印 RT9013 是一款高性能的 500mA LDO 穩壓器&#xff0c;具有極高的 PSRR 和超低壓差。非常適合具有苛刻性能和空間要求的便攜式射頻和無線應用。 RT9013的靜態電流低至25μA&#xff0c;進一步延長了電池的使用壽命。RT9013 也適用于低…

mysql在部署時的問題

1.遠程連接是否開放問題 DataGrip遠程連接Ubuntu Linux MySQL服務器報錯DBMS: MySQL (no ver.)-CSDN博客 【MySQL】DataGrip遠程連接MySQL_datagrip連接遠程mysql數據庫-CSDN博客 一定要把對應端口規則打開 2.遠程連接不適用3306作為默認運行端口 打開mysql的配置文件&…

音樂發行平臺無加密開源源碼

適用于唱片公司&#xff0c;用于接收物料&#xff0c;下載物料功能&#xff1a;個人或機構認證&#xff0c;上傳專輯和歌曲&#xff0c;版稅結算環境要求php7.4Nginx 1、導入數據庫 2、/inc/conn.php里填寫數據庫密碼等后臺路徑/admin&#xff08;可自行修改任意入口名稱&…

AI在軟件開發中的角色:助手還是取代者?

目錄 前言 一、AI工具現狀&#xff1a;高效助手的崛起 二、AI對開發者的影響&#xff1a;新技能與競爭力的重塑 三、AI開發的未來&#xff1a;共生而非取代 寫在最后 前言 隨著科技的飛速發展&#xff0c;生成式人工智能&#xff08;AIGC&#xff09;在軟件開發領域的應用日…

【JS】過濾數組中空值——arr.filter(Boolean)

前言&#xff1a;過濾數組中的空值&#xff0c;包括 &#xff08;undefined、null、“”、0、false、NaN&#xff09; Boolean函數可以將一個值轉換為布爾值&#xff0c;空值會被轉換為false&#xff0c;非空值會被轉換為true 方法&#xff1a; const arr [1, 2, ""…

【SQL常用日期函數(一)】

SQL 常用日期函數-基于impala 引擎 當前日期&#xff08;YYYY-MM-DD&#xff09; SELECT CURRENT_DATE(); -- 2024-06-30昨天 SELECT CURRENT_DATE(); -- 2024-06-30 SELECT CAST( DAYS_ADD(TO_DATE( CURRENT_DATE() ), -1 ) AS VARCHAR(10) ); -- 2024-06-29 SELECT CAST( …

Linux-頁表如何對物理內存進行映射

1.1 頁框和頁幀 我們知道通過頁表可以將虛擬內存映射到對應的物理內存&#xff0c;而操作系統對于物理內存的管理并不是以字節為單位的&#xff0c;而是將物理內存分為許多大小為4KB的塊&#xff0c;稱為頁框或頁幀&#xff0c;這就是為什么我們在創建共享內存是建議將大小設定…

LTSPICE仿真電路:(十九)磁珠的一些簡單仿真

1.作用 簡單來說就是用來濾波的&#xff0c;將高頻信號轉化為熱量濾除掉&#xff0c;低頻有用信號正常通過 2.參數 上圖幾個參數比較簡單&#xff0c;就是字面上的意思&#xff0c;更重要的就是頻率阻抗圖 不同曲線代表不同型號的磁珠&#xff0c;實際上除了額定電流外&#…

基于springboot+vue+uniapp的語言課學習系統小程序

開發語言&#xff1a;Java框架&#xff1a;springbootuniappJDK版本&#xff1a;JDK1.8服務器&#xff1a;tomcat7數據庫&#xff1a;mysql 5.7&#xff08;一定要5.7版本&#xff09;數據庫工具&#xff1a;Navicat11開發軟件&#xff1a;eclipse/myeclipse/ideaMaven包&#…

藝活網DIY手工制作網站源碼 工藝制作教程平臺源碼,帶數據

帝國CMS仿《手藝活》DIY手工制作網源碼&#xff0c;仿手藝活自適應手機版模板。 帶數據庫和圖片資源&#xff0c;一共5個G大小&#xff0c;下載需耐心。 92開發 手藝活網DIY手工制作網站源碼 創意手工藝品制作教程平臺系統帝國h5自適應手機端 是一套展示各種 DIY 小物品精美又…

@react-google-maps/api實現谷歌地圖中添加多邊圍欄,并可編輯,編輯后可獲得圍欄各個點的經緯度

先上一張效果圖 看看是不是大家想要的效果&#xff5e; ?? 由于該功能微微復雜一點&#xff0c;為了讓大家精準了解 我精簡了一下地圖代碼 大家根據自己的需求將center值和paths&#xff0c;用setState做活就可以了 1.第一步要加入項目package.json中或者直接yarn install它…

[激光原理與應用-97]:激光焊接焊中檢測系統系列介紹 - 1 - 什么是焊接以及傳統的焊接方法

目錄 一、什么是焊接 1.1 概述 1.2 基本原理 二、傳統的焊接技術與方法 2.1 手工電弧焊&#xff1a; 1、定義與原理 2、特點 3、焊條類型 4、應用領域 5、安全注意事項 2.2 氣體保護焊&#xff1a; 1、原理與特點 2、應用領域 3、氣體選擇 4、注意事項 2.3 電阻…