Linux | c&cpp | Email | github | QQ群:425043908 关注本站

itarticle.cc

您现在的位置是:网站首页 -> 代码相关 文章内容

深入探究C++的new/delete操作符-itarticl.cc-IT技术类文章记录&分享

发布时间: 8年前代码相关 145人已围观返回

今天在重温《More Effective C++》的时候,又看到讲 operator new 和 operator delete 的那条规则,虽然大概明白其原理,但是实际中却从来没用过,所以,就想写个小程序来试一试。如果你还不知道它们,或者听说过但不知道具体意义,那我就正好吓吓你:

new/delete/new[]/delete[] operator

operator new/delete/new[]/delete[]

placement new/new[] // 注意,没有 placement delete/delete[]

这些都是些什么呢?吓傻了?其实,这些都是那些学院派死扣字眼吓唬人的,真实理解起来很容易。就像博士写论文《关于一对自然数1的代数和与自然数2在绝对数学意义上相等的可能性》,其实,就是《论1 + 1 = 2》。。

嗯,我总喜欢扯蛋,对,是扯蛋,不是扯淡。好了,看下面的代码:

Class *pc = new Class;

// ...

delete pc;

上面代码的第一行即为 new operator ,而第三行即为 delete operator ,代码很简单,但对编译器来说,它需要做额外的工作,将上述代码翻译为近似于下面的代码:

void *p = operator new(sizeof(Class));

// 对p指向的内存调用Class的构造函数,此处无法用直观的代码展现

Class *pc = static_cast<Class*>(p);

// ...

pc->~Class();

operator delete(pc);

上面代码中,第一行即为 operator new ,而最后一行即为 operator delete ,很简单明了吧。所以, new operator 实际上做了两件事情:

调用 operator new 分配内存

在分配好的内存上初始化对象,并返回指向该对象的指针

而 delete operator 类似,调用析构函数,再调用 operator delete 释放内存。

让我们来看看C++标准库的实现之一——Clang的libcxx是如何实现全局的 operator new/delete 的(头文件声明在这里,实现在这里,我去掉了一些控制编译选项的看着很乱的宏定义,只留下了核心代码):

void * operator new(std::size_t size) throw(std::bad_alloc) {

if (size == 0)

size = 1;

void* p;

while ((p = ::malloc(size)) == 0) {

std::new_handler nh = std::get_new_handler();

if (nh)

nh();

else

throw std::bad_alloc();

}

return p;

}

void operator delete(void* ptr) {

if (ptr)

::free(ptr);

}

这段代码再简单不过,原来,神秘的 operator new/delete 在背后也不过是在偷偷地调用C函数库的 malloc/free 嘛!当然,这跟实现有关,libcxx这样实现,不代表其它实现也是如此。

需要意识到的是, operator new 和 operator+() 一样,只不过是普通的函数,是可以重载的,所谓的 placement new ,即是一个全局 operator new 的重载版本,在libcxx中定义如下:

inline _LIBCPP_INLINE_VISIBILITY void* operator new (std::size_t, void* __p) _NOEXCEPT {return __p;}

可以看到, placement new 除了正常的size参数外,还多了一个空指针参数,而且,它也没干什么内存分配的活,而是直接返回了这个指针。那么,该如何使用它呢?

void *buf = // 在这里为buf分配内存

Class *pc = new (buf) Class();

没错,就是这么简单,我们自己构建出一个缓冲区buf,再调用 placement new 在这个缓冲区上初始化Class的实例。甚至,我们可以传空指针给它:

Class *pc = new (nullptr) Class(); // nullptr是C++11中的空指针定义

当然,这样的代码毫无意义,虽然编译可以通过,但在运行时必然会crash。

关于这几个operator的区别基本已经讲清楚了,它们的兄弟——带[]的版本原理基本是一样的,这里就不细说了。需要注意的是,C++并没有 placement delete/delete[] 一说,因为它们没有存在的意义。

实例

码农?找不到对象?没事儿,不拼爹妈,更不靠干爹干妈,我们自己new一个出来。不过,别人都从堆上new,太没挑战了,我们从栈上new!

#include <iostream>

using namespace std;

class C {

public:

C(int i) : i(i) {

cout << "C constructor." << endl;

}

~C() {

cout << "C destructor." << endl;

}

// 此处声明为static或non-static均可,下同

/* static */ void *operator new(size_t size, void *p, const string& str) {

cout << "In our own operator new." << endl;

cout << str << endl;

if (!p) {

cout << "Hey man, are you aware what you are doing?" << endl;

return ::operator new(size);

}

return p;

}

/* static */ void operator delete(void *p) {

cout << "We should do nothing in operator delete." << endl;

// 如果取消下一行的注释,程序会在执行时crash

// ::operator delete(p);

}

void f() {

cout << "hello object, i: " << i << endl;

}

private:

int i;

};

int main() {

char buf[sizeof(C)];

C *pc = new (buf, "Yeah, I'm crazy!") C(1024);

pc->f();

// 此处原本不应该调用delete,而应该只显式调用析构函数,但因为我们重载的operator delete并不做什么操作,所以是安全的

delete pc;

return 0;

}

这个代码还是挺简单的,我们在类 C 中重载了 operator new 和 operator delete ,前者接受三个参数,第一个是必须要带的size,第二个是指针,第三个是用于测试的字符串,如果指针不为空,我们直接返回,如果为空,我们就分配一片内存出来并返回。当然,这段代码是有问题的,恶作剧地传递null指针给 operator new 会导致memory leak,不过,这不是我们要关注的。我们要关注的是,将栈上的buf指针传给 operator new 后,我们就真的在栈上new了一个对象出来了有没有!!上述代码的输出如下:

In our own operator new.

Yeah, I'm crazy!

C constructor.

hello object, i: 1024

C destructor.

We should do nothing in operator delete.

演变

我突然想到了一个坏主意:我们对上面的例子做一个小小的改动,将main函数中buf的长度变短,其它不变:

int main() {

char buf[sizeof(C) - 3]; // 注意此处

C *pc = new (buf, "Yeah, I'm crazy!") C(1024);

pc->f();

delete pc;

return 0;

}

在我的机器上, int 类型的大小是4,所以 sizeof(C) 大小也是4,因此 buf 的大小就是1。

等等:在只有一个字节的内存中分配一个占4字节的对象?看来是真的Crazy了,坐等程序crash吧!

事实上,我也是这么想的。只是,程序 不但没有crash,而且一切输出正常!

更进一步

怎么回事?难不成编译器智能地探测到buf的内存不足以装下C的实例,所以自动扩充了3个字节?于是,我们再稍作修改,在buf的两边都加上指示性的变量,以方便探测其边界:

int main() {

int a = 0x01020304; // 定义成这样是为了在GDB中调试时方便查看内存,下同

char buf[sizeof(C) - 3];

int b = 0x04030201;

C *pc = new(buf, "Yeah, I'm crazy!") C(0xFEDCBA98);

pc->f();

delete pc;

}

使用 g++ -g -O0 new.cpp -o new 来编译以输出symbol方便调试,同时防止编译器优化掉我们的边界变量。然后,在GDB中开始调试,在main函数处打一个断点,开始运行:

(gdb) b main

Breakpoint 1 at 0x100001082: file new.cpp, line 41.

(gdb) r

Starting program: /Users/kelvin/new

Breakpoint 1, main () at new.cpp:41

41 int a = 0x01020304;

先来看看几个变量的地址,以及内存:

(gdb) p &a

$1 = (int *) 0x7fff5fbffa50

(gdb) p &buf

$2 = (char (*)[1]) 0x7fff5fbffa4f

(gdb) p &b

$3 = (int *) 0x7fff5fbffa48

(gdb) x/24b &b

0x7fff5fbffa48: -56 -6 -65 95 -1 127 0 0

0x7fff5fbffa50: 0 0 0 0 0 0 0 0

0x7fff5fbffa58: 0 0 0 0 0 0 0 0

a在栈的最下面,所以a的地址最高。从打印出的内存来看,此时内存还是随机的。执行一步对a的赋值看看:

(gdb) n

43 int b = 0x04030201;

(gdb) x/24b &b

0x7fff5fbffa48: -56 -6 -65 95 -1 127 0 0

0x7fff5fbffa50: 4 3 2 1 0 0 0 0

0x7fff5fbffa58: 0 0 0 0 0 0 0 0

很明显,a的地址0x7fff5fbffa50处的内存被赋值为0x01020304,其它没变。再执行一步看看:

(gdb) n

44 C *pc = new(buf, "Yeah, I'm crazy!") C(0xFEDCBA98);

(gdb) x/24b &b

0x7fff5fbffa48: 1 2 3 4 -1 127 0 0

0x7fff5fbffa50: 4 3 2 1 0 0 0 0

0x7fff5fbffa58: -128 46 0 0 1 0 0 0

GDB机智地跳过了声明buf的语句,直接执行了对b的赋值语句,于是b的地址0x7fff5fbffa48所指向的内存被赋值为0x04030201,但是,位于0x7fff5fbffa58处的两字节内存也发生了变化,我们尚不明确此处内存所代表的意义,不管它,继续单步执行:

(gdb) n

In our own operator new.

Yeah, I'm crazy!

C constructor.

45 pc->f();

(gdb) x/24b &b

0x7fff5fbffa48: 1 2 3 4 -1 127 0 -104

0x7fff5fbffa50: -70 -36 -2 1 0 0 0 0

0x7fff5fbffa58: -128 46 0 0 1 0 0 0

pc被正常构造,但需要注意的是,从地址0x7fff5fbffa4f到0x7fff5fbffa52都发生了变化!0x7fff5fbffa4f是buf的地址,但是,0x7fff5fbffa50是a的地址!上面的内存不太直观,我们用十六进制再看看:

(gdb) x/24x &b

0x7fff5fbffa48: 0x01 0x02 0x03 0x04 0xff 0x7f 0x00 0x98

0x7fff5fbffa50: 0xba 0xdc 0xfe 0x01 0x00 0x00 0x00 0x00

0x7fff5fbffa58: 0x80 0x2e 0x00 0x00 0x01 0x00 0x00 0x00

这下就很清楚了,buf的一个字节被写为0x98,而因为buf的长度不够装下C的实例,于是位于buf后面的a变量就倒了霉,被覆盖了三个字节!于是,我们可以得出结论,编译器还没有这么智能。长度不够,该覆盖的还是会覆盖,之前的代码是因为幸运,位于buf后面的3个字节的内存刚好是可读写的,所以没有crash。

现在,再打印一下变量a:

(gdb) p a

$4 = 33479866

(gdb) p/x a

$5 = 0x1fedcba

果然,a已经被覆盖了。

需要说明的是,上面的输出中,在变量b和buf之间还有三个字节,地址是0x7fff5fbffa4c到0x7fff5fbffa4e。我最初真的以为这是编译器智能预留的三个字节!后面发现它们的值始终没有变化,才意识到,这三个字节应该是为了内存对齐而产生的无效字节。照此说来,C的实例内存没有对齐,所以,在访问其成员变量i的时候,需要访问两次内存。

总结

废话了这么多,那 operator new/delete, placement new/delete 到底有什么用呢?

实现自己的内存管理:有些程序需要高效的内存管理,比方说使用内存池,就可以用这个来实现,在new的时候直接从内存池取,delete的时候放回内存池

用来判断对象是否在堆上分配:这个是在《More Effective C++》中介绍的一个用法,在执行new操作时,将在堆上分配的地址保存起来,后面在判断一个对象是否在堆上分配时,就可以到这些保存的地址中查找这个对象的地址,如果找到,就说明是在堆上分配的

像我这样装逼地实现在栈上new对象

其它尚待挖掘的用法

实际上到目前为止,我还没看到有项目使用此类技术,一是很生僻,二是很容易出错。所以,在确实有这样的需求的情况下,再使用这类技术吧。

发布时间: 8年前代码相关145人已围观返回回到顶端

很赞哦! (1)

文章评论

  • 请先说点什么
    热门评论
    144人参与,0条评论

站点信息

  • 建站时间:2016-04-01
  • 文章统计:728条
  • 文章评论:82条
  • QQ群二维码:扫描二维码,互相交流