问题描述:
有一次review代码,看到一个类似这样的代码(代码已简化),是否有问题?
struct Box {
int a;
int b;
};
// allocate a char array
char* buffer = new char[sizeof(Box) * 5];
// cast a Box array pointer
Box *p = reinterpret_cast<Box *>(buffer);
//free the pointer? is it bad?
delete p;
上述代码通过new char[xx] 一个字符数组,然后强制转为一个结构体指针,然后直接进行释放?在学C++语言的时候,我们被告诫new/delete,new xx[]/delete []p要配对,否则会出问题。但是运行上面代码,并不会发生什么异常或者内存泄露。
为了搞清楚这个问题,需要考虑两方面内容:编译器做了什么和标准库做了什么?
c++标准库做了什么:
为了简化问题,这个地方new/delete, new xx[]/delete []都是C++ Standard Library缺省版本,不考虑自定义的对操作符重载情况;我们以Android系统使用的libcxx库来看看new/delete
libcxx(https://github.com/llvm/llvm-project/tree/master/libcxx/)
libcxxabi:https://github.com/llvm/llvm-project/tree/master/libcxxabi/
libcxx中定义的new/delete等函数week属性,在android系统上会被libcxxabi里面的实现进行覆盖,不过两者差异不大。
_LIBCXXABI_WEAK
void
operator delete(void* ptr) _NOEXCEPT
{
if (ptr)
::free(ptr);
}
_LIBCXXABI_WEAK
void
operator delete[] (void* ptr) _NOEXCEPT
{
::operator delete(ptr);
}
标准库里面实现delete等操作是比较直观的,delete[]调用就是delete,然后调用就是c库的free函数,从基础库实现看,上面调用并没有什么问题。
编译器做了什么?
学过C++都知道,针对C++里面的new A() 不仅仅是分配对象,还会调用对应构造函数,析构函数,因此我们还需要去了解一下编译器做了什么事情?下面我们以clang/llvm编译器针对对象的分配都做来那些事情?看一下不同类型产生的llvm ir都是什么?
clang -S -emit-llvm main.cc
struct Struct {
int a;
int b;
};
class Object {
public:
Object() {
a = 1;
}
~Object() {
}
private:
int a;
};
int main() {
// Primte type
int *a = new int[5];
// Struct type
Struct *s = new Struct[5];
// Object Type
Object *o = new Object[5];
delete []a;
delete []s;
delete []o;
return 0;
}
~
//llvm ir
%1 = alloca i32, align 4
%2 = alloca i32*, align 8 //局部变量a
%3 = alloca %struct.Struct*, align 8 //局部变量s
%4 = alloca %class.Object*, align 8 //局部变量o
store i32 0, i32* %1, align 4
%5 = call i8* @_Znam(i64 20) #4 //申请5个int类型20字节内存
%6 = bitcast i8* %5 to i32*
store i32* %6, i32** %2, align 8 //存储在a
%7 = call i8* @_Znam(i64 40) #4 //申请40个字节的内存
%8 = bitcast i8* %7 to %struct.Struct*
store %struct.Struct* %8, %struct.Struct** %3, align 8 //存储在s中
%9 = call i8* @_Znam(i64 28) #4 //每个类5*4个字节+extra8个字节
%10 = bitcast i8* %9 to i64*
store i64 5, i64* %10, align 8 //5存储第一个i64类型,表示长度
%11 = getelementptr inbounds i8, i8* %9, i64 8
%12 = bitcast i8* %11 to %class.Object*
%13 = getelementptr inbounds %class.Object, %class.Object* %12, i64 5
br label %14
; <label>:14: ; preds = %14, %0
%15 = phi %class.Object* [ %12, %0 ], [ %16, %14 ]
call void @_ZN6ObjectC1Ev(%class.Object* %15)
%16 = getelementptr inbounds %class.Object, %class.Object* %15, i64 1
%17 = icmp eq %class.Object* %16, %13
br i1 %17, label %18, label %14
从llvm ir可以看出创建prime,struct,object在new的时候做的不一样,尤其object类型,会多申请一个额外的内存存储元素的个数。
; <label>:26: ; preds = %23
%27 = bitcast %struct.Struct* %24 to i8*
call void @_ZdaPv(i8* %27) #5
br label %28
//针对object释放,会先读取Object长度;
; <label>:31: ; preds = %28
%32 = bitcast %class.Object* %29 to i8*
%33 = getelementptr inbounds i8, i8* %32, i64 -8
%34 = bitcast i8* %33 to i64*
%35 = load i64, i64* %34, align 4
%36 = getelementptr inbounds %class.Object, %class.Object* %29, i64 %35
%37 = icmp eq %class.Object* %29, %36
br i1 %37, label %42, label %38
; <label>:38: ; preds = %38, %31
%39 = phi %class.Object* [ %36, %31 ], [ %40, %38 ]
%40 = getelementptr inbounds %class.Object, %class.Object* %39, i64 -1
call void @_ZN6ObjectD1Ev(%class.Object* %40)
%41 = icmp eq %class.Object* %40, %29
br i1 %41, label %42, label %38
clang中如果不使用delete [],使用delete产生ir会发现Struct基本没有变化,但是Object很大差别,无法识别是数组类型产生错误:
; <label>:26: ; preds = %23
%27 = bitcast %struct.Struct* %24 to i8*
call void @_ZdlPv(i8* %27) #5
br label %28
; <label>:28: ; preds = %26, %23
%29 = load %class.Object*, %class.Object** %4, align 8
%30 = icmp eq %class.Object* %29, null
br i1 %30, label %33, label %31
; <label>:31: ; preds = %28
call void @_ZN6ObjectD1Ev(%class.Object* %29)
%32 = bitcast %class.Object* %29 to i8*
call void @_ZdlPv(i8* %32) #5
br label %33
对于Prime和Struct Plain类型,基本没有差异,但是对于Object类型是错误的。
问题结论:
对于起初给的例子来看,因为都是简单的Struct/Prime类型,并不会出错,如果是Object具备构造函数的类型,就会出现内存泄露等各种潜在等错误,建议大家还是按照标准等写法去写。
new and delete Operatorsdocs.microsoft.com