C++程序设计兼谈对象模型(侯捷)——课程笔记(六)

    技术2025-04-12  18

    这部分主要想谈一下new和delete相关的东西。之前的课程包含的内容有可变模板参数,auto,范围for循环,引用还有对象模型相关的知识,这些都不做总结了,因为讲得比较浅,这些概念之前都比较熟悉了。

    一、使用new和delete时发生了什么

    1. new:先分配memory,再调用ctor

    当使用new表达式时,编译器会先分配内存空间,再调用相应的构造函数,比如当我们写如下的代码时:

    string *ps = new string("Hello");

    编译器会将其转化为:

    string* ps; void* mem = operator new( sizeof(string) ); //分配内存 ps = static_cast<string*>(mem); //类型转换 ps->string::string("Hello"); //调用构造函数

    其中operator new(n)内部会调用malloc(n)

    2. delete:先调用dtor,再释放memory

    string *ps = new string("Hello"); ... delete ps;

    编译器会将其转换为:

    string::~string(ps); //调用析构函数 operator delete(ps); //释放内存

    其中operator delete(ps)内部会调用free(ps)

    3.array new一定要搭配array delete

    假如array new搭配的是普通delete,其实并不会影响new的对象本身占用内存的释放(这是因为array new的对象占用的空间是一起被编译器标记的,当delete时,不管是array delete还是普通delete,这一块内存都会被一起还给操作系统)。区别在于,普通delete只调用一次析构函数,而array delete将会对所有new的对象调用析构函数,所以如果new的对象本身会申请额外内存的话,array new不匹配array delete会造成这部分的内存泄露。

    下面是一张VC中动态分配的内存块的示意图

    其中灰色的是debug需要的内存,浅绿色为系统添加的内存,目的是使整个内存大小为16字节的倍数,从而能使上下两个标记的最后一个字节的低四位永远处于空出来的状态,这样就可以使用这四位来标记整块内存是否属于操作系统,上下两个红色就是标记大小的内存。(要注意的是,这种操作与我们常说的内存对齐无关,不要搞混了,内存对齐没有16字节对齐的)

    二、重载operator new和operator delete

    1. 重载全局operator new和operator delete

    重载的是::operator new, ::operator delete, ::operator new[], ::operator delete[]. 注意全局作用域符号::

    void* myAlloc(size_t size) { return malloc(size); } void myFree(void* ptr) { return free(ptr); } //它们不可以被声明于一个namespace内 inline void* operator new(size_t size) { cout << "my global new()\n"; return myAlloc(size); } inline void* operator new[](size_t size) { cout << "my global new[]()\n"; return myAlloc(size); } inline void operator delete(void* ptr) { cout << "my global delete()\n"; return myFree(ptr); } inline void operator delete[](void* ptr) { cout << "my global delete[]()\n"; return myFree(ptr); }

    值得注意的是,由于重载的是全局operator new和operator delete,所以这些定义不能位于任何一个namespace内。(不确定这样理解对不对)。还有一点,重载的这些函数还是供编译器调用的,我们不负责调用。

    2. 重载member operator new/delete

    class Foo{ public: void* operator new(size_t); void operator delete(void*, size_t); //size_t为可选参数 //... }; Foo* p = new Foo; ... delete p;

    则new和delete的使用会被编译器转化为:

    try{ void* mem = operator new(sizeof(Foo)); p = static_cast<Foo*>(mem); p->Foo::Foo(); } ... p->~Foo(); operator delete(p);

    其中operator new和operator delete将会调用我们定义的版本。

    值得注意的是,当这样写时

    Foo* p = new Foo; ... delete p;

    如果有member重载就会调用member的operator new和operator delete,如果没有member重载,调用的就是全局operator new和operator delete。如果要确保调用的是全局operator new和operator delete,则需要加上全局作用域符号::,像下面这样:

    Foo* p = ::new Foo; ... ::delete p;

    3. 关于operator new[]和operator delete[]中size_t的大小

    在VC中,大小为(N*对象的大小+4),其中多出的一个字用来表示new出的对象的个数

    三、重载new()和delete()

    我们可以重载class member operator new(),写出多个版本,前提是每一版本的声明都必须有独特的参数列,其中第一个参数必须是size_t,其余参数以new所指定的placement arguments为初值。出现于new(....)小括号内的便是所谓的placement arguments。就比如说这样调用:

    Foo* pf = new (300, 'c')Foo;

    那么这就说明重载的operator new有三个参数,第一个是size_t,后两个就是括号里那两个。

    我们也可以重载class member operator delete(),写出多个版本。但它们绝不会被delete调用。只有当new所调用的ctor抛出exception,才会调用相应版本的operator delete()。它只可能这样被调用,主要用来归还未能完全创建成功的object所占用的memory。但是即使operator delete(...)未能一一对应于operator new(...),也不会出现任何报错,这种情况下设计者的意思是:放弃处理ctor发出的异常。这种情况下VC6会发出warning。

    但是视频中侯捷老师经过测试后,发现这种情况下GCC4.9并没有调用相应的operator delete(...),但是GCC2.9调用了。以后可以拿别的编译器测试一下。

    下面介绍一个标准库中使用placement new的实例:

    template<...> class basic_string { private: struct Rep{ ... void release() { if(--ref == 0) delete this; } inline static void* operator new(size_t, size_t); //重载的placement new inline static void operator delete(void*); inline static Rep* create(size_t); //在这个函数里使用placement new ... }; ... }; template<class charT, class traits, class Allocator> inline void* basic_string<charT, traits, Allocator>::Rep::operator new(size_t s, size_t extra) { return Allocator::allocate(s + extra * sizeof(charT)); } template<class charT, class traits, class Allocator> inline basic_string<charT, traits, Allocator>::Rep* basic_string<charT, traits, Allocator>::Rep:: create(size_t extra) { extra = frob_size(extra + 1); Rep *p = new (extra) Rep; //调用了重载的placement new ... return p; }

    最后创建出的是像这样的东西:

    其中Rep部分表示引用计数,所以我猜测智能指针也采用了类似的设计

    Processed: 0.009, SQL: 9