當(dāng)前位置:首頁(yè) > 公眾號(hào)精選 > CPP開(kāi)發(fā)者
[導(dǎo)讀]在ModernC之前,C無(wú)疑是個(gè)更容易寫(xiě)出坑的語(yǔ)言,無(wú)論從開(kāi)發(fā)效率,和易坑性,讓很多新手望而卻步。比如內(nèi)存泄露問(wèn)題,就是經(jīng)常會(huì)被寫(xiě)出來(lái)的坑,本文就讓我們一起來(lái)看看,這些讓現(xiàn)在或者曾經(jīng)的C程序員淚流滿(mǎn)面的內(nèi)存泄露場(chǎng)景吧。你是否有踩過(guò)?1.函數(shù)內(nèi)或者類(lèi)成員內(nèi)存未釋放這類(lèi)問(wèn)題可以稱(chēng)之為...

Modern C 之前,C 無(wú)疑是個(gè)更容易寫(xiě)出坑的語(yǔ)言,無(wú)論從開(kāi)發(fā)效率,和易坑性,讓很多新手望而卻步。比如內(nèi)存泄露問(wèn)題,就是經(jīng)常會(huì)被寫(xiě)出來(lái)的坑,本文就讓我們一起來(lái)看看,這些讓現(xiàn)在或者曾經(jīng)的C 程序員淚流滿(mǎn)面的內(nèi)存泄露場(chǎng)景吧。你是否有踩過(guò)?

1. 函數(shù)內(nèi)或者類(lèi)成員內(nèi)存未釋放

這類(lèi)問(wèn)題可以稱(chēng)之為out of scope的時(shí)候,并沒(méi)有釋放相應(yīng)對(duì)象的堆上內(nèi)存。有時(shí)候最簡(jiǎn)單的場(chǎng)景,反而是最容易犯錯(cuò)的。這個(gè)我想主要是因?yàn)榻?jīng)常寫(xiě),哪有不出錯(cuò)。下面場(chǎng)景一看就知道了,當(dāng)你在寫(xiě)XXX_Class * pObj = new XXX_Class();這一行的時(shí)候,腦子里面還在默念記得要釋放pObj ,記得要釋放pObj, 可能因?yàn)橹匾氖虑橐f(shuō)三遍,而你只喊了兩遍,最終還是忘記了寫(xiě)delete pObj;?這樣去釋放對(duì)象。

void?MemoryLeakFunction()
{
??XXX_Class?*?pObj?=?new?XXX_Class();
??pObj->DoSomething();
??return;?
}
下面這個(gè)場(chǎng)景,就是析構(gòu)函數(shù)中并沒(méi)有釋放成員所指向的內(nèi)存。這個(gè)我們就要注意了,一般當(dāng)你構(gòu)建一個(gè)類(lèi)的時(shí)候,寫(xiě)析構(gòu)函數(shù)一定要切記釋放類(lèi)成員關(guān)聯(lián)的資源。

class?MemoryLeakClass
{
public:
??MemoryLeakClass()?
??{?
????m_pObj?=?new?XXX_ResourceClass;
??}
??void?DoSomething()
??{
????m_pObj->DoSomething();
??}
??~MemoryLeakClass()
??{
????;
??}
private:
??XXX_ResourceClass*?m_pObj;
};
上述這兩種代碼例子,是不是讓一個(gè)C 工程師如履薄冰,完全看自己的大腦在不在狀態(tài)。在boost或者C 11后,通過(guò)智能指針去進(jìn)行包裹這個(gè)原始指針,這是一種RAII的思想(可以參閱本文末尾的關(guān)聯(lián)閱讀), 在out of scope的時(shí)候,釋放自己所包裹的原始指針指向的資源。將上述例子用unique_ptr改寫(xiě)一下。

void?MemoryLeakFunction()
{
??std::unique_ptr?pObj?=?make_unique();
??pObj->DoSomething();
??return;?
}

2. delete []

大家知道C 中這樣一個(gè)語(yǔ)句XXX_Class * pObj = new XXX_Class();?中的new我們一般稱(chēng)其為C 關(guān)鍵字?(keyword), 就以這個(gè)語(yǔ)句為例做了兩個(gè)操作:

  1. 調(diào)用了operator new從堆上申請(qǐng)所需的空間

  2. 調(diào)用XXX_Class的構(gòu)造函數(shù)

那么當(dāng)你調(diào)用delete pObj;的時(shí)候,道理同new,剛好相反:

  1. 調(diào)用了XXX_Class的析構(gòu)函數(shù)

  2. 通過(guò)operator delete?釋放了內(nèi)存

一切似乎都沒(méi)有什么問(wèn)題,然后又一個(gè)坑來(lái)了。但如果申請(qǐng)的是一個(gè)數(shù)組呢,入下述例子:

class?MemoryLeakClass
{

public:
??MemoryLeakClass()?
??{?
????m_pStr?=?new?char[100];
??}
??void?DoSomething()
{
????strcpy_s(m_pStr,?100,?"Hello?Memory?Leak!");
????std::cout?<std::endl;
??}
??~MemoryLeakClass()
??{
????delete?m_pStr;
??}
private:
??char?*m_pStr;
};

void?MemoryLeakFunction()
{
??const?int?iSize?=?5;
??MemoryLeakClass*?pArrayObjs?=?new?MemoryLeakClass?[iSize];
??for?(int?i?=?0;?i???{
????(pArrayObjs i)->DoSomething();
??}
??delete?pArrayObjs;
}
上述例子通過(guò)MemoryLeakClass* pArrayObjs = new MemoryLeakClass [iSize];申請(qǐng)了一個(gè)MemoryLeakClass數(shù)組,那么調(diào)用不匹配的delete pArrayObjs;, 會(huì)產(chǎn)生內(nèi)存泄露。先看看下圖, 然后結(jié)合剛講的delete的行為:
那么其實(shí)調(diào)用delete pArrayObjs;的時(shí)候,釋放了整個(gè)pArrayObjs的內(nèi)存,但是只調(diào)用了pArrayObjs[0]析構(gòu)函數(shù)并釋放中的m_pStr指向的內(nèi)存。pArrayObjs 1~4并沒(méi)有調(diào)用析構(gòu)函數(shù),從而導(dǎo)致其中的m_pStr指向的內(nèi)存沒(méi)有釋放。所以我們要注意newdelete要匹配使用,當(dāng)使用的new []申請(qǐng)的內(nèi)存最好要用delete[]。那么留一個(gè)問(wèn)題給讀者, 上面代碼delete m_pStr;會(huì)導(dǎo)致同樣的問(wèn)題嗎?如果總是要讓我們自己去保證,newdelete的配對(duì),顯然還是難以避免錯(cuò)誤的發(fā)生的。這個(gè)時(shí)候也可以使用unique_ptr, 修改如下:

void?MemoryLeakFunction()
{
??const?int?iSize?=?5;
??std::unique_ptr?pArrayObjs?=?std::make_unique(iSize);
??for?(int?i?=?0;?i???{
????(pArrayObjs.get() i)->DoSomething();
??}
}

3. delete (void*)

如果上一個(gè)章節(jié)已經(jīng)有理解,那么對(duì)于這個(gè)例子,就很容易明白了。正因?yàn)?code style="font-size: 14px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">C 的靈活性,有時(shí)候會(huì)將一個(gè)對(duì)象指針轉(zhuǎn)換為void *,隱藏其類(lèi)型。這種情況SDK比較常用,實(shí)際上返回的并不是SDK用的實(shí)際類(lèi)型,而是一個(gè)沒(méi)有類(lèi)型的地址,當(dāng)然有時(shí)候我們會(huì)為其親切的取一個(gè)名字,比如叫做XXX_HANDLE。那么繼續(xù)用上述為例MemoryLeakClass, SDK假設(shè)提供了下面三個(gè)接口:

  1. InitObj創(chuàng)建一個(gè)對(duì)象,并且返回一個(gè)PROGRAMER_HANDLE(即void *),對(duì)應(yīng)用程序屏蔽其實(shí)際類(lèi)型

  2. DoSomething?提供了一個(gè)功能去做一些事情,輸入的參數(shù),即為通過(guò)InitObj申請(qǐng)的對(duì)象

  3. 應(yīng)用程序使用完畢后,一般需要釋放SDK申請(qǐng)的對(duì)象,提供了FreeObj

typedef?void?*?PROGRAMER_HANDLE;

PROGRAMER_HANDLE?InitObj()
{
??MemoryLeakClass*?pObj?=?new?MemoryLeakClass();
??return?(PROGRAMER_HANDLE)pObj;
}

void?DoSomething(PROGRAMER_HANDLE?pHandle)
{
??((MemoryLeakClass*)pHandle)->DoSomething();
}

void?FreeObj(void?*pObj)
{
??delete?pObj;
}
看到這里,也許有讀者已經(jīng)發(fā)現(xiàn)問(wèn)題所在了。上述代碼在調(diào)用FreeObj的時(shí)候,delete看到的是一個(gè)void *, 只會(huì)釋放對(duì)象所占用的內(nèi)存,但是并不會(huì)調(diào)用對(duì)象的析構(gòu)函數(shù),那么對(duì)象內(nèi)部的m_pStr所指向的內(nèi)存并沒(méi)有被釋放,從而會(huì)導(dǎo)致內(nèi)存泄露。修改也是自然比較簡(jiǎn)單的:

void?FreeObj(void?*pObj)
{
??delete?((MemoryLeakClass*)pObj);
}
那么一般來(lái)說(shuō),最好由相對(duì)資深的程序員去進(jìn)行SDK的開(kāi)發(fā),無(wú)論從設(shè)計(jì)和實(shí)現(xiàn)上面,都盡量避免了各種讓人淚流滿(mǎn)滿(mǎn)的坑。

4. Virtual destructor

現(xiàn)在大家來(lái)看看這個(gè)很容易犯錯(cuò)的場(chǎng)景, 一個(gè)很常用的多態(tài)場(chǎng)景。那么在調(diào)用delete pObj;會(huì)出現(xiàn)內(nèi)存泄露嗎?

class?Father
{

public:
??virtual?void?DoSomething()
{
????std::cout?<"Father?DoSomething()"?<std::endl;
??}
};

class?Child?:?public?Father
{
public:
??Child()
??{
????std::cout?<"Child()"?<std::endl;
????m_pStr?=?new?char[100];
??}

??~Child()
??{
????std::cout?<"~Child()"?<std::endl;
????delete[]?m_pStr;
??}

??void?DoSomething()
{
????std::cout?<"Child?DoSomething()"?<std::endl;
??}
protected:
??char*?m_pStr;
};

void?MemoryLeakVirualDestructor()
{
??Father?*?pObj?=?new?Child;
??pObj->DoSomething();
??delete?pObj;
}
會(huì)的,因?yàn)?code style="font-size: 14px;font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;color: rgb(233, 105, 0);background: rgb(248, 248, 248);">Father沒(méi)有設(shè)置Virtual 析構(gòu)函數(shù),那么在調(diào)用delete pObj;的時(shí)候會(huì)直接調(diào)用Father的析構(gòu)函數(shù),而不會(huì)調(diào)用Child的析構(gòu)函數(shù),這就導(dǎo)致了Child中的m_pStr所指向的內(nèi)存,并沒(méi)有被釋放,從而導(dǎo)致了內(nèi)存泄露。并不是絕對(duì),當(dāng)有這種使用場(chǎng)景的時(shí)候,最好是設(shè)置基類(lèi)的析構(gòu)函數(shù)為虛析構(gòu)函數(shù)。修改如下:

class?Father
{

public:
??virtual?void?DoSomething()
{
????std::cout?<"Father?DoSomething()"?<std::endl;
??}
??virtual?~Father()?{?;?}
};

class?Child?:?public?Father
{
public:
??Child()
??{
????std::cout?<"Child()"?<std::endl;
????m_pStr?=?new?char[100];
??}

??virtual?~Child()
??{
????std::cout?<"~Child()"?<std::endl;
????delete[]?m_pStr;
??}

??void?DoSomething()
{
????std::cout?<"Child?DoSomething()"?<std::endl;
??}
protected:
??char*?m_pStr;
};

5. 對(duì)象循環(huán)引用

看下面例子,既然為了防止內(nèi)存泄露,于是使用了智能指針shared_ptr;并且這個(gè)例子就是創(chuàng)建了一個(gè)雙向鏈表,為了簡(jiǎn)單演示,只有兩個(gè)節(jié)點(diǎn)作為演示,創(chuàng)建了鏈表后,對(duì)鏈表進(jìn)行遍歷。
那么這個(gè)例子會(huì)導(dǎo)致內(nèi)存泄露嗎?

struct?Node
{

??Node(int?iVal)
??{
????m_iVal?=?iVal;
??}
??~Node()
??{
????std::cout?<"~Node():?"?<"Node?Value:?"?<std::endl;
??}
??void?PrintNode()
{
????std::cout?<"Node?Value:?"?<std::endl;
??}

??std::shared_ptr?m_pPreNode;
??std::shared_ptr?m_pNextNode;
??int?m_iVal;
};

void?MemoryLeakLoopReference()
{
??std::shared_ptr?pFirstNode?=?std::make_shared(100);
??std::shared_ptr?pSecondNode?=?std::make_shared(200);
??pFirstNode->m_pNextNode?=?pSecondNode;
??pSecondNode->m_pPreNode?=?pFirstNode;

??//Iterate?nodes
??auto?pNode?=?pFirstNode;
??while?(pNode)
??{
????pNode->PrintNode();
????pNode?=?pNode->m_pNextNode;
??}
}
先來(lái)看看下圖,是鏈表創(chuàng)建完成后的示意圖。有點(diǎn)暈乎了,怎么一個(gè)雙向鏈表畫(huà)的這么復(fù)雜,黃色背景的均為智能指針或者智能指針的組成部分。其實(shí)根據(jù)雙向鏈表的簡(jiǎn)單性和下圖的復(fù)雜性,可以想到,智能指針的引入雖然提高了安全性,但是損失的是性能。所以往往安全性和性能是需要互相權(quán)衡的。?我們繼續(xù)往下看,哪里內(nèi)存泄露了呢?

如果函數(shù)退出,那么m_pFirstNodem_pNextNode作為棧上局部變量,智能指針本身調(diào)用自己的析構(gòu)函數(shù),給引用的對(duì)象引用計(jì)數(shù)減去1(shared_ptr本質(zhì)采用引用計(jì)數(shù),當(dāng)引用計(jì)數(shù)為0的時(shí)候,才會(huì)刪除對(duì)象)。此時(shí)如下圖所示,可以看到智能指針的引用計(jì)數(shù)仍然為1, 這也就導(dǎo)致了這兩個(gè)節(jié)點(diǎn)的實(shí)際內(nèi)存,并沒(méi)有被釋放掉, 從而導(dǎo)致內(nèi)存泄露。

你可以在函數(shù)返回前手動(dòng)調(diào)用pFirstNode->m_pNextNode.reset();強(qiáng)制讓引用計(jì)數(shù)減去1, 打破這個(gè)循環(huán)引用。
還是之前那句話(huà),如果通過(guò)手動(dòng)去控制難免會(huì)出現(xiàn)遺漏的情況, C 提供了weak_ptr。

struct?Node
{

??Node(int?iVal)
??{
????m_iVal?=?iVal;
??}
??~Node()
??{
????std::cout?<"~Node():?"?<"Node?Value:?"?<std::endl;
??}
??void?PrintNode()
{
????std::cout?<"Node?Value:?"?<std::endl;
??}

??std::shared_ptr?m_pPreNode;
??std::weak_ptr????m_pNextNode;
??int?m_iVal;
};

void?MemoryLeakLoopRefference()
{
??std::shared_ptr?pFirstNode?=?std::make_shared(100);
??std::shared_ptr?pSecondNode?=?std::make_shared(200);
??pFirstNode->m_pNextNode?=?pSecondNode;
??pSecondNode->m_pPreNode?=?pFirstNode;

??//Iterate?nodes
??auto?pNode?=?pFirstNode;
??while?(pNode)
??{
????pNode->PrintNode();????
????pNode?=?pNode->m_pNextNode.lock();
??}
}
看看使用了weak_ptr之后的鏈表結(jié)構(gòu)如下圖所示,weak_ptr只是對(duì)管理的對(duì)象做了一個(gè)弱引用,其并不會(huì)實(shí)際支配對(duì)象的釋放與否,對(duì)象在引用計(jì)數(shù)為0的時(shí)候就進(jìn)行了釋放,而無(wú)需關(guān)心weak_ptrweak計(jì)數(shù)。注意shared_ptr本身也會(huì)對(duì)weak計(jì)數(shù)加1.
那么在函數(shù)退出后,當(dāng)pSecondNode調(diào)用析構(gòu)函數(shù)的時(shí)候,對(duì)象的引用計(jì)數(shù)減一,引用計(jì)數(shù)為0,釋放第二個(gè)Node,在釋放第二個(gè)Node的過(guò)程中又調(diào)用了m_pPreNode的析構(gòu)函數(shù),第一個(gè)Node對(duì)象的引用計(jì)數(shù)減1,再加上pFirstNode析構(gòu)函數(shù)對(duì)第一個(gè)Node對(duì)象的引用計(jì)數(shù)也減去1,那么第一個(gè)Node對(duì)象的引用計(jì)數(shù)也為0,第一個(gè)Node對(duì)象也進(jìn)行了釋放。

如果將上述代碼改為雙向循環(huán)鏈表,去除那個(gè)循環(huán)遍歷Node的代碼,那么最后Node的內(nèi)存會(huì)被釋放嗎?這個(gè)問(wèn)題留給讀者。

6. 資源泄露

如果說(shuō)些作文的話(huà),這一章節(jié),可能有點(diǎn)偏題了。本章要講的是廣義上的資源泄露,比如句柄或者fd泄露。這些也算是內(nèi)存泄露的一點(diǎn)點(diǎn)擴(kuò)展,寫(xiě)作文的一點(diǎn)點(diǎn)延伸吧。
看看下述例子, 其在操作完文件后,忘記調(diào)用CloseHandle(hFile);了,從而導(dǎo)致內(nèi)存泄露。

void?MemroyLeakFileHandle()
{
??HANDLE?hFile?=?CreateFile(LR"(C:\test\doc.txt)",?
????GENERIC_READ,
????FILE_SHARE_READ,
????NULL,?
????OPEN_EXISTING,?
????FILE_ATTRIBUTE_NORMAL,
????NULL);

??if?(INVALID_HANDLE_VALUE?==?hFile)
??{
????std::cerr?<"Open?File?error!"?<std::endl;
????return;
??}

??const?int?BUFFER_SIZE?=?100;
??char?pDataBuffer[BUFFER_SIZE];
??DWORD?dwBufferSize;
??if?(ReadFile(hFile,
??????pDataBuffer,
??????BUFFER_SIZE,
??????
本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請(qǐng)聯(lián)系該專(zhuān)欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請(qǐng)及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

9月2日消息,不造車(chē)的華為或?qū)⒋呱龈蟮莫?dú)角獸公司,隨著阿維塔和賽力斯的入局,華為引望愈發(fā)顯得引人矚目。

關(guān)鍵字: 阿維塔 塞力斯 華為

倫敦2024年8月29日 /美通社/ -- 英國(guó)汽車(chē)技術(shù)公司SODA.Auto推出其旗艦產(chǎn)品SODA V,這是全球首款涵蓋汽車(chē)工程師從創(chuàng)意到認(rèn)證的所有需求的工具,可用于創(chuàng)建軟件定義汽車(chē)。 SODA V工具的開(kāi)發(fā)耗時(shí)1.5...

關(guān)鍵字: 汽車(chē) 人工智能 智能驅(qū)動(dòng) BSP

北京2024年8月28日 /美通社/ -- 越來(lái)越多用戶(hù)希望企業(yè)業(yè)務(wù)能7×24不間斷運(yùn)行,同時(shí)企業(yè)卻面臨越來(lái)越多業(yè)務(wù)中斷的風(fēng)險(xiǎn),如企業(yè)系統(tǒng)復(fù)雜性的增加,頻繁的功能更新和發(fā)布等。如何確保業(yè)務(wù)連續(xù)性,提升韌性,成...

關(guān)鍵字: 亞馬遜 解密 控制平面 BSP

8月30日消息,據(jù)媒體報(bào)道,騰訊和網(wǎng)易近期正在縮減他們對(duì)日本游戲市場(chǎng)的投資。

關(guān)鍵字: 騰訊 編碼器 CPU

8月28日消息,今天上午,2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)開(kāi)幕式在貴陽(yáng)舉行,華為董事、質(zhì)量流程IT總裁陶景文發(fā)表了演講。

關(guān)鍵字: 華為 12nm EDA 半導(dǎo)體

8月28日消息,在2024中國(guó)國(guó)際大數(shù)據(jù)產(chǎn)業(yè)博覽會(huì)上,華為常務(wù)董事、華為云CEO張平安發(fā)表演講稱(chēng),數(shù)字世界的話(huà)語(yǔ)權(quán)最終是由生態(tài)的繁榮決定的。

關(guān)鍵字: 華為 12nm 手機(jī) 衛(wèi)星通信

要點(diǎn): 有效應(yīng)對(duì)環(huán)境變化,經(jīng)營(yíng)業(yè)績(jī)穩(wěn)中有升 落實(shí)提質(zhì)增效舉措,毛利潤(rùn)率延續(xù)升勢(shì) 戰(zhàn)略布局成效顯著,戰(zhàn)新業(yè)務(wù)引領(lǐng)增長(zhǎng) 以科技創(chuàng)新為引領(lǐng),提升企業(yè)核心競(jìng)爭(zhēng)力 堅(jiān)持高質(zhì)量發(fā)展策略,塑強(qiáng)核心競(jìng)爭(zhēng)優(yōu)勢(shì)...

關(guān)鍵字: 通信 BSP 電信運(yùn)營(yíng)商 數(shù)字經(jīng)濟(jì)

北京2024年8月27日 /美通社/ -- 8月21日,由中央廣播電視總臺(tái)與中國(guó)電影電視技術(shù)學(xué)會(huì)聯(lián)合牽頭組建的NVI技術(shù)創(chuàng)新聯(lián)盟在BIRTV2024超高清全產(chǎn)業(yè)鏈發(fā)展研討會(huì)上宣布正式成立。 活動(dòng)現(xiàn)場(chǎng) NVI技術(shù)創(chuàng)新聯(lián)...

關(guān)鍵字: VI 傳輸協(xié)議 音頻 BSP

北京2024年8月27日 /美通社/ -- 在8月23日舉辦的2024年長(zhǎng)三角生態(tài)綠色一體化發(fā)展示范區(qū)聯(lián)合招商會(huì)上,軟通動(dòng)力信息技術(shù)(集團(tuán))股份有限公司(以下簡(jiǎn)稱(chēng)"軟通動(dòng)力")與長(zhǎng)三角投資(上海)有限...

關(guān)鍵字: BSP 信息技術(shù)
關(guān)閉
關(guān)閉