標籤:

這段c++代碼存在內存泄露的可能嗎?

int main()
{
int* p = (int*)(new double);
delete p;
return 0;
}

這裡假設int佔4位元組,double佔8位元組.

-----------------------------------------------------------

十分感謝各位大牛的耐心解答.


語言律師模式:On

可能。

C++規定,delete表達式的參數,可以是nullptr,或者先前的new表達式的結果,或者是一個子類的指針代表一個基類或這樣的一個對象。除此之外都是undefined behaviour。

你這裡delete表達式的參數不是先前new表達式的結果,而是轉換表達式的結果。所以是UB。

而undefined behavior,意思就是發生什麼都是有可能的,正常執行也有可能,觸發了美國的核彈發射也有可能。所以,內存泄漏也是有可能的。

參考n4659

語言律師模式:Off

我用clang,gcc和MSVC試過了,執行沒問題。


我來提供點別的思路吧,各位不要只揪著delete不放啊。

你問我滋補資詞UB?我當然是資詞的啦。但是,「UB」首先發生在第一行的類型轉換上!

題里的代碼是一個C-style cast(cast notation),這種cast會被按順序嘗試如下轉換方法,選擇第一個合適的轉換:

- const_cast (5.2.11),
- static_cast (5.2.9),
- static_cast followed by a const_cast,
- reinterpret_cast (5.2.10), or
- reinterpret_cast followed by a const_cast,

double* 無法合法 static_cast 到int*,所以只好是reinterpret_cast了。

然後,5.2.10:

3. The mapping performed by reinterpret_cast is implementation-defined. [ Note: it might, or might not, produce a
representation different from the original value. — end note ]
7. A pointer to an object can be explicitly converted to a pointer to an object of different type.70) Except that converting
an rvalue of type 「pointer to T1」 to the type 「pointer to T2」 (where T1 and T2 are object types and where the alignment
requirements of T2 are no stricter than those of T1) and back to its original type yields the original pointer value, the
result of such a pointer conversion is unspecified.

Boom!

用人話說就是,reinterpret_cast 轉換到 int*之後,返回值並不確定。


至少在我的思考以及Clang / LLVM 吐出的信息,我覺得不會泄漏。

在你的例子中,LLVM IR是這樣的:

define i32 @main() #0 {
entry:
%retval = alloca i32, align 4
%p = alloca i32*, align 8
store i32 0, i32* %retval, align 4
%call = call i8* @_Znwm(i64 8) #3
%0 = bitcast i8* %call to double*
%1 = bitcast double* %0 to i32*
store i32* %1, i32** %p, align 8
%2 = load i32*, i32** %p, align 8
%isnull = icmp eq i32* %2, null
br i1 %isnull, label %delete.end, label %delete.notnull

delete.notnull: ; preds = %entry
%3 = bitcast i32* %2 to i8*
call void @_ZdlPv(i8* %3) #4
br label %delete.end

delete.end: ; preds = %delete.notnull, %entry
ret i32 0
}

new調用了_Znwm,delete是_ZdlPv,你可以通過c++filt來還原,也可以通過ast-dump來看

|-FunctionDecl 0x7f938900b4a0 & line:1:5 main "int (void)"
| `-CompoundStmt 0x7f938900be00 & | |-DeclStmt 0x7f938900bd48 & | | `-VarDecl 0x7f938900b600 & col:10 used p "int *" cinit
| | `-CStyleCastExpr 0x7f938900bd20 & "int *" &
| | `-CXXNewExpr 0x7f938900bcb8 & "double *" Function 0x7f938900b6d8 "operator new" "void *(unsigned long)"
| |-CXXDeleteExpr 0x7f938900bda0 & "void" Function 0x7f938900ba08 "operator delete" "void (void *) noexcept"
| | `-ImplicitCastExpr 0x7f938900bd88 & "int *" &
| | `-DeclRefExpr 0x7f938900bd60 & "int *" lvalue Var 0x7f938900b600 "p" "int *"
| `-ReturnStmt 0x7f938900bde8 & | `-IntegerLiteral 0x7f938900bdc8 & "int" 0
|-FunctionDecl 0x7f938900b6d8 &<&&> & implicit used operator new "void *(unsigned long)"
| |-ParmVarDecl 0x7f938900b7c8 &<&&> & implicit "unsigned long"
| `-VisibilityAttr 0x7f938900b780 &<&&> Implicit Default
|-FunctionDecl 0x7f938900b830 &<&&> & implicit operator new[] "void *(unsigned long)"
| |-ParmVarDecl 0x7f938900b920 &<&&> & implicit "unsigned long"
| `-VisibilityAttr 0x7f938900b8d8 &<&&> Implicit Default
|-FunctionDecl 0x7f938900ba08 &<&&> & implicit used operator delete "void (void *) noexcept"
| |-ParmVarDecl 0x7f938900baf8 &<&&> & implicit "void *"
| `-VisibilityAttr 0x7f938900bab0 &<&&> Implicit Default
`-FunctionDecl 0x7f938900bb60 &<&&> & implicit operator delete[] "void (void *) noexcept"
|-ParmVarDecl 0x7f938900bc50 &<&&> & implicit "void *"
`-VisibilityAttr 0x7f938900bc08 &<&&> Implicit Default

總而言之,new調用的是"operator new" "void *(unsigned long)", delete是"operator delete" "void (void *) noexcept"

然後,我們看LLVM IR

%call = call i8* @_Znwm(i64 8) #3
%0 = bitcast i8* %call to double*
%1 = bitcast double* %0 to i32*
store i32* %1, i32** %p, align 8
%2 = load i32*, i32** %p, align 8
%isnull = icmp eq i32* %2, null
br i1 %isnull, label %delete.end, label %delete.notnull

delete.notnull: ; preds = %entry
%3 = bitcast i32* %2 to i8*
call void @_ZdlPv(i8* %3) #4

在這裡,new記錄的是分配大小為double的8,然後返回一個i8*出去,即分配的內存的地址。然後這個指針進行bitcast,無論說轉換double*,然後再轉換為i32*,其實都記錄下來了這個地址。然後這個i32*指針,在傳遞給delete時,又轉換為了i8*。所以經歷了i8* -&> double* -&>i32*-&>i8*。所以,你完全可以想像的到,delete的大小,跟類型指針完全沒有任何關係。

然後我們打開operator new / delete的實現:http://llvm.org/svn/llvm-project/libcxx/trunk/src/new.cpp

_LIBCPP_WEAK
void *
operator new(std::size_t size) _THROW_BAD_ALLOC
{
if (size == 0)
size = 1;
void* p;
while ((p = ::malloc(size)) == 0)
{
// If malloc fails and there is a new_handler,
// call it to try free up memory.
std::new_handler nh = std::get_new_handler();
if (nh)
nh();
else
#ifndef _LIBCPP_NO_EXCEPTIONS
throw std::bad_alloc();
#else
break;
#endif
}
return p;
}

_LIBCPP_WEAK
void
operator delete(void* ptr) _NOEXCEPT
{
if (ptr)
::free(ptr);
}

也就是說其實依賴的是malloc / free 。這時候你可以想像一下C,無論是何種類型指針,如void *p = malloc(sizeof(int)); free(p);或者void *p = malloc(sizeof(double)); free(p);我們都是可以正常析構的,所以malloc / free 也肯定不是跟這個指針類型有關了。那麼,若你對為什麼free可以正常析構感興趣,對我曾在這裡簡單提到過malloc之後再進行free,free的內存空間一定被OS回收了嗎? - 知乎。你也可以參考這裡:A quick tutorial on implementing and debugging malloc, free, calloc, and realloc,這裡簡單的探尋了malloc / free。


VC++的話不會,其他不知道。VC++下面delete和delete[]混用、以及new Derived[?]後強轉成Base*然後delete[],都不會出問題,但是題主千萬不要這麼做。


這取決於 new/delete 是如何實現的。通常 new/delete 會採用類似 malloc/free 的方式,這時候關於 memory chunk 的 meta-data 是存在 chunk 之前的內存里。此時源代碼中的類型信息並不參與內存的回收。但是 new/delete 可以被 overloaded。而且 C++ 標準也沒規定 memory chunk meta-data 一定是完全存在內存中的,compiler 完全可以根據類型進行優化。


先用 Valgrind 和 AddressSanitizer 跑一遍。


簡單類型一般不會,但是對於複雜類型,你delete[]的時候還會根據類型信息調用析構函數,如果調用了new A,卻析構了~B(),就會爆炸。


你要是用VS的話可以對new轉到定義,看看裡面的實現——一般來說是用malloc/free實現的new/delete,而前者不需要類型信息,所以對於簡單的類型來說很可能不會內存泄漏。

實際上不要這麼做。


你還不如放棄治療,直接用malloc和free。人家只管地址,不管類型,保證在此不會有未定義行為。


把 double* 轉換成 int*

本身就是未定義行為了

不用考慮會不會內存泄漏

沒意義


不會出現內存泄漏,

大部分編譯器new和delete都不依賴於類型的,就比如說delete是不依賴於類型來獲取刪除內存大小的,如果這樣實現new和delete的話,程序員稍有不慎做了類型轉換就容易出現內存泄漏了,new底層是malloc,定義為extern void *malloc(unsigned int num_bytes);delete底層是free,定義為, void free(void *ptr);看到到沒,底層實際上是void*的指針,malloc底層分配的內存,不是實際的內存大小,比如 void * p =malloc(7),malloc實際分配的內存一定大於7位元組,malloc會在7位元組前面預留4個位元組來記錄後面的內存大小,我沒查資料看是不是預留的4個位元組,不用糾結這些細節,但是實現原理是這樣的,我們就假設是4個位元組,delete p的時候,會從p指向的地址往前4個地址來獲取p所指的內存大小,刪除指定大小的內存,所以內存分配器底層是記錄了每一塊分配出去的內存的大小的,不依賴於高級的數據類型。舉個列子,char* p=new char(),char* p1=new char[100],delete p,delete [] p1,或者是delete p1,都不會存在內存泄漏的,按裝你的想法,delete [] p1,或者是delete p1都要造成內存泄漏的,可是你實際想想,你平常的工作中,肯定沒少這麼寫


沒有,main 結束,操作系統會負責回收所有內存。


怎麼感覺這是一道C++作業題。。 正常的話沒人會這樣寫的。。


你這段代碼都沒有觸發缺頁中斷分配物理內存,哪來的內存泄漏呢?虛內存分配只會在調用庫層和操作系統記個帳,對系統沒有實際壓力。正確的內存泄漏的姿勢應該是:

int* p = (int*)(new double);

p* = 1;

......

好吧,就算虛內存泄漏也是泄漏。那回到問題,

我先改述一下:free函數需不需要知道指針類型,來逆推需要釋放內存的大小。

答案是不需要:

先看人家free的申明:

void free(void *ptr)

C library function - free()

所以free傳進去的只是一個地址(虛地址),至於已經需要釋放的內存大小,這個記賬系統(調用庫層,即用戶態層)是知道的,所以應用層不用再傳了。這個記賬系統的另一個名字叫內存管理(mm(memory management))。

參考和延伸問題:

http://www.cplusplus.com/reference/cstdlib/free/

https://github.com/lattera/glibc/blob/master/malloc/malloc.c

mmap(2) - Linux manual page

Does free() unmap the memory of a process?


linux下沒有,動動手。

$ valgrind --tool=memcheck ./a.out

==71655== LEAK SUMMARY:

==71655== definitely lost: 0 bytes in 0 blocks

==71655== indirectly lost: 0 bytes in 0 blocks

==71655== possibly lost: 72 bytes in 3 blocks

==71655== still reachable: 200 bytes in 6 blocks

==71655== suppressed: 17,956 bytes in 153 blocks

==71655== Rerun with --leak-check=full to see details of leaked memory


這個算reinpreter_cast轉換,看編譯器怎麼做了,風險太大,建議別這麼干


內置類型,new/delete和malloc/free等同。不需要知道具體大小的。


int main()
{
void* p = (void*)(new double);
delete p;
return 0;
}

大家猜一下會發生什麼情況。


說白了,delete是具有一定智能的,你在new的時候,就在內部記錄了這個地址(地址是沒有類型的哦)當時申請時是多長。

那麼delete的時候,只要還是這個地址,應該長度就不會錯。delete並不會傻到你說是int就是int,你說是double就是double。

(但是刪除數組時要注意,delete [] array,這時又不是那麼智能了 XD)

這個是個語言未定義行為,但是按照正常人實現delete的思路,那個長度已經早就記錄了,這個程序不會錯誤。當然如果某個編譯器是崩的,那也在情理之中。


推薦閱讀:

如何格式化代碼能夠將類成員/函數的名字對齊?
請問這個程序為什麼會死在18,19行?
應該如何熟悉GNU工具鏈?例如GCC/Makefile/GDB
C++ 編程軟體有哪些推薦?有沒有比 vc 6 更好的?
怎樣用C++實現生產者消費者的模擬?

TAG:C |