C++内存高级话题


一、进一步认识new和delete

new类对象时,加与不加括号的差别:

class A {
   public:
};

int main() {
    A *pa = new A();
    A *pa2 = new A;
    return 0;
}

如果类A是一个空类,那么main()函数中的两行代码没有区别。如果类A有成员变量:

class A {
   public:
    int m_i;
};

int main() {
    A *pa = new A();  // m_i被初始化为0
    A *pa2 = new A;   // m_i是随机值
    return 0;
}

说明带括号这种初始化对象的方式会把一些和成员变量有关的内存设置为0(内存中显示的内容是0)。如果类A有构造函数:

class A {
   public:
    A() {}
   public:
    int m_i;
};

发现main()中的两行代码执行的结果又变得相同了,A *pa = new A();这种写法,成员变量m_i内存也不设置为0了,可能是成员变量的初始化工作要转交给构造函数做(而不再是系统内部做),而构造函数的函数体为空,所以m_i没有被初始化。再看下面三行代码:

int *p1 = new int;
int *p2 = new int();
int *p3 = new int(100);

第一行代码执行后,p1的初值为随机值;第二行代码执行后,p2的初值为0;第三行代码执行后,p3的初值为100。new可以称为关键字,也可以称为操作符,查看到new的定义:

_Ret_notnull_ _Post_writable_byte_size_(_Size)
_VCRT_ALLOCATOR void* __CRTDECL operator new(
    size_t _Size
    );

在代码A *pa = new A()的地方设置一个断点,按F5进行调试,当断点停留在该行代码时,通过选择“调试”→“窗口”→“反汇编”命令打开反汇编窗口,看到这行代码对应的汇编语言代码:

示例代码

new关键字主要做了两件事,一是调用operator new,二是调用类A的构造函数。调试中使用F11跳转进operator new,发现调用了malloc

示例图片

operator new是一个函数,可以调用它:

operator new(12);

new关键字分配内存时的大概调用关系是(用缩进四格来表现调用关系):

// new调用了operator new()和A::A(),operator new()调用了malloc()
A *pa = new A();     // 操作符
    operator new();  // 函数
        malloc();    // C风格函数分配内存
    A::A();          // 有构造函数就调用构造函数

delete关键字释放内存时的大概调用关系是(用缩进四格来表现调用关系):

// delete调用了A::~A()和operator delete(),operator delete()调用了free()
delete pa;
    A::~A();            // 如果有析构函数,则先调用析构函数
    operator delete();  // 函数
        free();         // C风格函数释放内存

new 和 malloc 的区别:
① new 是关键字/操作符,而 malloc 是函数;
② new 一个对象的时候,不但分配内存,而且还会调用类的构造函数(如果没有构造函数而且系统也没生成构造函数,那就没法调用构造函数了);
③ 在某些情况下,A *pa = new A();可以把对象的某些成员变量(如m_i)设置为0,malloc没这个能力。

delete 和 free 的区别:
① delete 不但释放内存,而且在释放内存之前会调用类的析构函数(当然必须要类的析构函数存在)。

new 是通过调用 malloc 来分配内存的,那么 malloc 是怎样分配内存的呢?malloc 内部有各种链表,实现比较复杂,可能不同的操作系统有不同的做法。malloc 可能还需要调用与操作系统有关的更底层的函数来实现内存的分配和管理。malloc 是跨平台的、通用的函数。

二、new 内存分配细节

看下面的代码:

int main() {
    char* ppoint = new char[10];
    memset(ppoint, 0, 10);  // 观察从哪里初始化
    delete[] ppoint;        // 观察释放影响的内存位置
}

调试代码可以看到ppoint所指向的内存起始地址是0x0120F140,观察调试窗口:

示例代码

目前分配的是10字节内存,每字节内存中的内容都是00。为了进一步观察内存,把内存地址提前40字节(能查看到更前面的内存中的内容),现在查看的内存地址是0x0120F140,用该数字减40(十进制数字),得到的内存地址是0x0120F118(十六进制),在内存1窗口中直接输人地址0x0120F118来查看该地址内容如图:

示例图片

图中红框标示出了分配给ppoint的10字节,其中的内容全是0。执行代码行delete[] ppoint;,内存的变化如图:

示例图片

可以注意到,释放一块内存影响的范围很广,虽然分配内存的时候分配出去的是10字节,但释放内存的时候影响的远远不止是10字节的内存单元,而是一大片(图中变红色的40多字节的内存单元内容都受到影响)。
 
观察下图,看一看内存的分配与释放时临近内存的合并:

示例

①图(a)所示这5块表示分配出去了5块内存(一共new了5次);
②图(b)表示率先释放了第3块内存;
③图©表示再过一会儿,第2块内存也释放了。这时free函数还要负责把临近的空闲块也合并到一起(把原来的第2和第3块内存合并成一大块),这种与临近空闲内存块的合并是free的责任。

分配内存的时候指明了分配10字节,但释放内存的时候并没有告诉编译器要释放多少字节,显然编译器肯定在哪里记录了这块内存分配出去的是10字节,在释放内存的时候编译器才能正好把这10字节的内存释放掉。调试以下代码:

int main() {
    char* ppoint = new char[55];  // 十进制的55对应十六进制的37
    memset(ppoint, 0, 10);  // 观察从哪里初始化
    delete[] ppoint;        // 观察释放影响的内存位置
}

观察下图,在ppoint所指向的首地址之前的12字节的位置有一个37,这是十六进制数字,转换成十进制数字就是55,这里就是记录着所分配的内存大小。

示例图片

假设分配出去的内存是10字节,但绝不意味着只是简单分配出去10字节(而是比10字节多很多),在这10字节周围的内存中记录了很多其他内容。

一般来说,分配10字节内存,编译器或者说真正负责分配内存的malloc函数可能会分配出如图所示的内存(不同的编译器可能这里的其他信息项不太一样):

示例图片

编译器最终是把它分出去的这一大块内存中间某个位置的指针返回给ppoint,作为程序员能够使用的内存的起始地址。不难想象,若是程序员一次只申请1字节(如char *ppoint = new char;),结果系统一下多分配出40多字节,浪费得实在太多。为了记录和管理分配出去的内存,额外多分配了不少内存,造成了浪费,尤其是对于频繁分配小块的内存,浪费就显得更加严重。

三、重载类内 operator new、operator delete 操作符

前面讲到newdelete关键字的调用关系,现在写一个类A的operator newoperator delete成员函数来取代系统的operator newoperator delete函数。这种写法比较固定:

class A {
   public:
    static void *operator new(size_t size);  // 应该为静态函数,但不写static似乎也行,估计是
                                             // 编译器内部有处理,因为new一个对象时还没对象呢,
                                             // 静态成员函数跟着类走,和对象无关
    static void operator delete(void* phead);
};

void* A::operator new(size_t size) {
    cout << "A::operator new 被调用了" << endl;
    A *ppoint = (A*)malloc(size);
    return ppoint;
}

void A::operator delete(void* phead) {
    cout << "A::operator delete 被调用了" << endl;
    free(phead);
}

int main() {
    A *pa = new A();
    delete pa;
    return 0;
}

调试发现可以调用类A的operator newoperator delete函数,调用operator new传进去的参数size是1,因为类A的大小就是1字节。

类A增加public修饰的构造函数和析构函数:

class A {
   public:
    static void *operator new(size_t size);
    static void operator delete(void* phead);
    A() {
        cout << "类A的构造函数执行了" << endl;
    }
    ~A() {
        cout << "类A的析构函数执行了" << endl;
    }
};

执行结果:

A::operator new 被调用了
类A的构造函数执行了
类A的析构函数执行了
A::operator delete 被调用了

如果不想用自己写的operator newoperator delete函数了,只需要在使用newdelete关键字时在其之前增加"::"即可,表示调用全局的newdelete,两个冒号叫“作用域运算符”。

A* pa2 = ::new A();
::delete pa2;

四、重载类中的 operator new[] 和 operator delete[] 操作符

在主函数中增加如下代码:

A *pa = new A[3]();
delete[] pa;

这种写法并不调用上面类A中的operator newoperator delete。因为这两行代码是为数组分配内存,需要重载operator new[]operator delete[]。在类A中增加两个public修饰的成员函数声明:

   public:
    static void* operator new[](size_t size);
    static void operator delete[](void* phead);

这两个函数的实现:

void *A::operator new[](size_t size) {
    cout << "A::operator new[]被调用了" << endl;
    A *ppoint = (A*)malloc(size);
    return ppoint;
}

void A::operator delete[](void *phead) {
    cout << "A::operator delete[]被调用了" << endl;
    free(phead);
}

这里写的这两个成员函数和不带"[]"的两个成员函数代码是完全相同的。要特别注意这种数组操作符的调用流程,执行起来,结果如下:

A::operator new[]被调用了
类A的构造函数执行了
类A的构造函数执行了
类A的构造函数执行了
类A的析构函数执行了
类A的析构函数执行了
类A的析构函数执行了
A::operator delete[]被调用了

operator new[]operator delete[]只会被调用1次,类A的构造函数和析构函数会被分别调用3次。调试发现调用operator new[]时,形参size是7,因为创建3个对象的数组,每个对象是1字节,这就占用了3字节,那另外的4字节是做什么用呢?调试operator new[]里面的代码行A *ppoint = (A*)malloc(size);,观察到ppoint返回的内存地址是0x0103e398,当执行完主函数的A *pa = new A[3]();,观察到pa的内存地址是0x0103e39c。

也就是说真正拿到手的指针是0x0103e39c,而0x0103e398实际上是编译器malloc分配内存时得到的首地址,这里9c比98多了4字节,4加3正好是7,等于operator new[]函数中形参size的值。多出的4字节其实是用来记录数组大小的,释放数组内存的时候必然会用到这个大小(这里数组的大小是3),从而知道调用多少次类A的构造函数和析构函数。

对象数组内存分配概貌如下图:

示例图片

五、内存池

1、内存池简介

使用malloc这种分配方式来分配内存会产生比较大的内存浪费,尤其是频繁分配小块内存时,浪费更加明显,所以一个叫作“内存池”的词汇就应运而生。内存池要解决的主要问题是:减少malloc调用次数,这意味着减少对内存的浪费,也能提高程序的一些运行效率或者说是运行速度(效率提升并不太多,因为malloc的执行速度其实是极快的)。

内存池的实现原理是用malloc申请一大块内存,分配内存的时候就以这一大块内存中一点点分配给程序员,当一大块内存差不多用完的时候,再申请一大块内存,然后再一点一点地分配给程序员使用,这涉及怎样分成一小块一小块以及怎样回收的问题。内在池的代码实现千差万别,但不管怎么说,减少内存浪费是根本,提高程序运行效率是顺带(不是最主要的)的。

2、针对一个类的内存池实现演示代码

提供一段相对比较简单,又比较有代表性的,能够体现内存池用途的代码:

class A {
   public:
    static void *operator new(size t size);
    static void operator delete(void *phead);
    static int m_iCount;  // 用于分配计数统计,每new一次+1
    static int m_iMallocCount;  // 用于统计malloc次数,每malloc一次+1
   private:
    A *next;
    static A *m_FreePosi;  // 总是指向一块可以分配出去的内存的首地址
    static int m_sTrunkCount;  // 一次分配多少倍该类的内存
};

void* A::operator new(size_t size) {
    // A *ppoint = (A*)malloc(size);
    // return ppoint;
    A *tmplink;
    if (m_FreePosi == nullptr) {
        // 为空,我们要申请内存,申请的是很大一块内存
        size_t realsize = m_sTrunkCount * size;  // 申请m_sTrunkCount这么多倍的内存
        m_FreePosi = reinterpret_cast<A*>(new char[realsize]);  // 这是传统new,调用底层传统malloc
        tmplink = m_FreePosi;

        // 把分配出来的这一大块内存链接起来,供后续使用
        for (; tmplink != &m_FreePosi[m_sTrunkCount - 1]; ++tmplink) {
            tmplink->next = tmplink + 1;
        }
        tmplink->next = nullptr;
        ++m_iMallocCount;
    }
    tmplink = m_FreePosi;
    m_FreePosi = m_FreePosi->next;
    ++m_iCount;
    return tmplink;
}

void A::operator delete(void *phead) {
    // free(phead);  // 不再用传统方式实现,针对内存池有特别的实现
    (static_cast<A*>(phead))->next = m_FreePosi;
    m_FreePosi = static_cast<A*>(phead); 
}

int A::m_iCount = 0;
int A::m_iMallocCount = 0;
A *A::m_FreePosi = nullptr;
int A::m_sTrunkCount = 5;  // 一次分配5倍的该类内存作为内存池的大小

int main() {
    A *pa = new A();
    delete pa;
    return 0;
}

先分析一下申请内存这段代码:

  • 第一次调用operator new成员函数时,if (m_FreePosi == nullptr)是成立的,执行完整个if条件中的代码后,情形如下图

示例图片

看起来像一个链表,提前分配了5块内存(每块正好是一个类A对象的大小),每一块的next指向下一块的首地址。跳出if语句并执行接下来几行代码,这几行代码的含义是:m_FreePosi总是指向下一个能分配的空闲块的开始地址,而后把tmplink返回去,如下图

示例图片

  • 每次new一个该类(类A)对象,m_FreePosi都会往下走指向下一块空闲内存块的首地址。假设程序员new了5次对象,把内存中事先准备好的5块内存都消耗光了,m_FreePosi就会指向nullptr了。此时第6次new,程序中if (m_FreePosi == nullptr)就又成立了,又会分配出5块内存。

示例图片

 
再来看一看内存的回收。下图描述了new了9次A类对象的情形:

示例图片

operator delete函数并不是把内存真正归还给系统,因为把内存真正归还给系统是需要调用free函数的,operator delete做的事情是把要释放的内存块链回到空闲的内存块链表中来。

  • m_FreePosi串起来的这个链代表的是空闲内存块的链,m_FreePosi指向的是整个链的第一个空闲块的位置,当需要分配内存时,就把这第一个空闲块分配出去,m_FreePosi就指向第二个空闲块。
  • 当回收内存块的时候,m_FreePosi就会立即指向这块回收回来的内存块的首地址,然后让回收回来的这块内存的next指针指向原来m_FreePosi所指向的那个空闲块。所以m_FreePosi始终是空闲块这个链的第一个空闲块(链表头)。
  • 对于已经分配出去的内存块的next指针指向什么已经没有实际意义了,可以不用理会。已经分配出去的内存块,程序要对它们负责,程序要保证及时地delete它们促使类A的operator delete成员函数被及时执行,从而把不用的内存块归还到内存池中。

将上图中左边已经分配出去的第3块内存回收回来后的情形如下图:

示例图片

当然,这个内存池代码不完善,例如分配内存的时候是用new分配的,释放内存的时候并没有真正地用delete来释放,而是把这块要释放的内存通过一个空闲链连起来而已。但是只要分配内存时不是一直用new分配下去,那这个内存池即便后续变得很大,只要有分配,有回收,这个内存池耗费的内存空间总归还是有限的。

3、嵌入式指针概念及范例

嵌入式指针,英文名字叫作embedded pointer,常用于内存池的代码实现中。在上面内存池代码中,类A中引入了一个成员变量A *next;,在当前 Visual Studio 2019 的 x86 平台下,这个指针是占4字节的,每new一个类A对象,都会有这么一个4字节的next指针出现,是属于内存空间的浪费。这就需要用到嵌入式指针技术,把这4字节省下来。

嵌人式指针的工作原理就是:借用类A对象所占的内存空间的前4字节(代替next指针),这4字节专门用来链住这些空闲的内存块。当一个空闲的内存块分配出去之后,这前4字节的内容就不需要了(因为上面说过,对于已经分配出去的内存块的next指针指向什么已经没有实际意义),即便这4字节的内容被具体的对象内的数据所覆盖,也无所谓。

嵌入式指针技术要成功地使用需要一个前提条件:那就是这个类A的sizeof(new 一个该类对象时所占用的内存字节数)必须要不少于4字节。当然,在实际的项目中,一般来讲,一个类对象(或者说一个类)的sizeof肯定会超过4字节,不巧的是,在上面的范例中类A的sizeof值正好是4字节,而这4字节恰好是next成员变量所占的4字节。现在的主要目的是要把next取消掉,那本类这个sizeof(A)的4字节,这next一取消掉,变成0了,其实不是0,而是1字节(任何一个类或者类对象的sizeof最少是1字节)。

为了演示嵌入式指针技术,向类A中随便增加两个public修饰的int成员变量,则sizeof(A)立即变成8。

   public:
    int m_i;
    int m_j;

引入嵌入式指针技术后,初始创建的内存池情形如下图,每个对象块中都借用该块前面的4字节来保存这个嵌入式指针值(这个值用来指向下一个空闲块)。

示例图片

下面看一看嵌入式指针的实现代码,写一个sizeof值不小于4的类:

class TestEP {
   public:
    int m_i;
    int m_j;
   public:
    struct obj {
        struct obj *next;  // next就是一个嵌入式指针
    };
};
  • 类里多了一个结构的定义,这跟在类外定义这个结构没什么区别,只不过如果把这个结构定义在类外面的话,外界要用这个obj结构名时直接写成obj,如果定义在类里面,外界要用obj类名时需要写成TestEP::obj,所以这个嵌入式指针只不过是嵌到类里面的一个类(结构),这就是“嵌入式”这三个字的由来。
  • struct obj *next;是一个指针变量,名字叫next,指向一个obj结构,这就是一个链表。

示例图片

看看嵌入式指针的使用,在main函数中写入如下代码:

TestEP mytest;
cout << sizeof(mytest) << endl;  // 8
TestEP::obj *ptmp;  // 定义一个指针
ptmp = (TestEP::obj*)&mytest;  // 把对象mytest的首地址给了这个指针
ptmp->next = nullptr;  // 前4字节给成了00 00 00 00

根据上面几行代码,绘制一下对象的结构数据图(ptmp指向了mytest的首地址,而ptmp->next代表的是mytest首地址开始的4字节的内容):

示例图片

ptmp->next = nullptr;执行完后,mytest对象内存地址的前4字节清0,这就是借用对象的前4字节保存嵌入式指针指向的内容。如果将上述代码中的嵌入式指针指向下一个内存池中内存块的地址,那最终就是把前面内存池代码中的next指针省下来了。这就是嵌入式指针的工作原理。

4、内存池代码的改进

有了嵌入式指针概念后,可以对内存池进行改进,应用嵌入式指针这个技术来作为块与块之间的链。这里单独为内存池技术的使用写一个类:

// 专门的内存池类
class myallocator {  // 必须保证使用本类的类sizeof()不少于4字节,否则崩溃报错
   public:
    // 分配内存接口
    void *allocate(size_t size) {
        obj *tmplink;
        if (m_FreePosi == nullptr) {
            // 为空,我要申请内存,要申请一大块内存
            size_t realsize = m_sTrunkCout * size;  // 申请m_sTrunkCout这么多倍的内存
            m_FreePosi = (obj*)malloc(realsize);
            tmplink = m_FreePosi;

            // 把分配出来的这一大块内存(5小块)彼此链接起来,供后续使用
            for (int i = 0; i < m_sTrunkCout - 1; ++i) {
                tmplink->next = (obj*)((char*)tmplink + size);
                tmplink = tmplink->next;
            }
            tmplink->next = nullptr;
        }
        tmplink = m_FreePosi;
        m_FreePosi = m_FreePosi->next;
        return tmplink;
    }

    // 释放内存接口
    void deallocate(void *phead) {
        ((obj*)phead)->next = m_FreePosi;
        m_FreePosi = (obj*)phead;
    }

   private:
    // 写在类内的结构,这样只让其在类内使用
    struct obj {
        struct obj *next;  // 这个next就是一个嵌入式指针
    };
    int m_sTrunkCout = 5;  // 一次分配5倍的该类内存作为内存池的大小
    obj *m_FreePosi = nullptr;
};

这个就是专用的内存池类或者说是内存分配类,接下来看一下如何使用,改造上面类A的代码如下:

class A {
   public:
    // 必须保证sizeof(A)凑够4字节,这里两个int成员8字节,所以使用类myallocator毫无问题
    int m_i;
    int m_j;

   public:
    static myallocator myalloc;  // 静态成员变量,跟着类A走
    static void *operator new(size_t size) {
        return myalloc.allocate(size);
    }
    static void operator delete(void *phead) {
        return myalloc.deallocate(phead);
    }
};
myallocator A::myalloc;  // 在类A之外定义一下这个静态成员变量

代码中定义了一个静态成员变量myalloc,然后直接改造了一下类A中的operator newoperator delete成员函数。现在在main函数中写入如下的测试代码:

A *mypa[100];
for (int i = 0; i < 15; ++i)  {
    mypa[i] = new A();
    printf("%p\n", mypa[i]);
}
for (int i = 0; i < 15; ++i) {
    delete mypa[i];
}

执行结果为:

013AAF98
013AAFA0
013AAFA8
013AAFB0
013AAFB8
013A5840
013A5848
013A5850
013A5858
013A5860
013A4E58
013A4E60
013A4E68
013A4E70
013A4E78

通过上面的结果看到,每5个分配的内存地址都是挨着的(间隔8字节),这说明内存池机制在发挥作用。如果觉得在类A中加入的代码还是有点多,可以用宏来简化,分别定义两个宏如下:

#define DECLARE_POOL_ALLOC()\
public:\
    static void *operator new(size_t size)\
    {\
        return myalloc.allocate(size);\
    }\
    static void operator delete(void *phead)\
    {\
        return myalloc.deallocate(phead);\
    }\
    static myallocator myalloc;

#define IMPLEMENT_POOL_ALLOC(classname)\
myallocator classname::myalloc;

这样,整个类A的定义写成下面的样子即可:

class A {
    DECLARE_POOL_ALLOC();
   public:
    int m_i;
    int m_j;
};
IMPLEMENT_POOL_ALLOC(A)

六、重载全局 new/delete、定位 new 及重载

1、重载全局 new/delete

除了可以重载类中的operator newoperator delete以及operator new[]operator delete[],还可以重载全局的operator newoperator delete以及operator new[]operator delete[]。在重载这些全局函数的时候,一定要放在全局空间里,否则编译器会报语法错误。

void *operator new(size_t size) {  // 重载全局operator new
    return malloc(size);
}
void *operator new[](size_t size) {  // 重载全局operator new[]
    return malloc(size);
}
void operator delete(void *phead) {  // 重载全局operator delete
    free(phead);
}
void operator delete[](void *phead) {  // 重载全局operator delete[]
    free(phead);
}

class A {
   public:
    A() {
        cout << "A::A()" << endl;
    }
    ~A() {
        cout << "A::~A()" << endl;
    }
};

main主函数中写一些测试代码:

int *pint = new int(12);    // 调用重载的operator new
delete pint;                // 调用重载的operator delete
char *parr = new char[10];  // 调用重载的operator new[]
delete[] parr;              // 调用重载的operator delete[]

A *p= newA();  // 调用重载的operator new,之后也执行了类A的构造函数
delete p;  // 执行了类A的析构函数,之后也调用了重载的operator delete
A *pa = new A[3]();  // 调用一次重载的operator new[],之后执行了三次类A的构造函数
delete[] pa;  // 执行了三次类A的析构函数,之后也调用了重载的operator delete[]

这种重载影响面太广,一般都是重载某个类中的,这样影响面比较小(只限制在某个类内),也更实用。如果类A中又重载了operator newoperator deleteoperator new[]operator delete[],那么类中的重载会覆盖掉全局的重载。

2、定位 new(placement new)

除了传统new之外,还有一种new叫作“定位new”,翻译成英文就是placement new。因为它的用法比较独特,所以并没有对应的placement delete的说法。

定位new的功能是:在已经分配的原始内存中初始化一个对象。这句话有两个重要描述点:

  • 已经分配,意味着定位new并不分配内存,也就是使用定位new之前内存必须先分配好。
  • 初始化一个对象,也就是初始化这个对象的内存,可以理解成其实就是调用对象的构造函数。

总而言之,定位new就是能够在一个预先分配好的内存地址中构造一个对象。定位new的格式如下:

new(地址)类类型(参数)

通过一个范例来演示定位new,创建一个叫作PLA的类:

class PLA {
   public:
    int m_a;
    PLA() : m_a(0) {
        cout << "PLA::PLA()构造函数执行” << endl;
    }
    PLA(int tempvalue) : m_a(tempvalue) {
        cout << "PLA::PLA(int tempvalue)构造函数执行” << endl;
    }
    ~PLA() {
        cout << "PLA::~PLA()析构函数执行” << endl;
    }
};

main主函数中增加如下代码:

void *mymemPoint = (void*)new char[sizeof(PLA)];  // 内存必须事先分配出来,为了内存分配通用性,这里返回void*类型
// 开始用这个返回的void*指针
PLA *pmyAobj1 = new(mymemPoint) PLA();  // 定位new:调用无参构造函数,这里并不额外分配内存

void *mymemPoint2 = (void*)new char[sizeof(PLA)];
PLA *pmyAobj2 = new(mymemPoint2) PLA(12);  // 定位new:调用带一个参数的构造函数,这里并不额外分配内存

// 释放
pmyAobj1->~PLA();  // 根据需要,有析构函数就可以调用析构函数
pmyAobj2->~PLA();
delete[](void*)pmyAobj1;  // 分配时用new char[],释放时用delete[],本行等价于 delete[](void*)mymemPoint
delete[](void*)pmyAobj2;  // 本行等价于 delete[](void*)mymemPoint2;

执行起来,结果如下:

PLA::PLA()构造函数执行
PLA::PLA(int tempvalue)构造函数执行
PLA::~PLA()析构函数执行
PLA::~PLA()析构函数执行

可以看到,一般来说,写程序的时候,构造函数都是不会被直接调用的(直接调用编译器会报错),而上面这种定位new的写法就等同于可以直接调用构造函数。而析构函数是能够直接调用的(上面的代码就直接调用了析构函数)。

跟踪调试可以看到定位new的调用关系如下:

// 内存必须已经分配好
PLA *pa = new(分配好的内存的首地址) PLA();  // 定位new操作符
    operator new ();                      // 函数,这里并没有调用malloc
    PLA::PLA();                           // 调用构造函数

定位new所调用的operator new操作符也能重载,在类PLA中增加用public修饰的operator new成员函数,注意其形参:

   public:
    // 定位new操作符的重载,注意参数是比传统new多一个参数的
    void* operator new(size_t size, void *phead) {
        // 这里增加一些自己的额外代码,用于统计之类的,但不要分配内存
        return phead;  // 收到内存开始地址也只返回内存开始地址即可
    }

3、多种版本的 operator new 重载

其实可以重载很多版本的operator new,只要每个版本参数不同就可以。第一个参数固定,类型都是size_t(类似于无符号型),表示这个对象的sizeof值,其他参数通过调用new时指定进去即可。在main主函数中,代码如下:

PLA *pla = new(1234, 56) PLA();  // 这其实并没有实际分配内存,也没有调用类的构造函数

在类PLA 中,增加一个public修饰的operator new重载如下,注意其形参数量为3个:

   public:
    void *operator new(size_t size, int tvp1, int tvp2) {
        return NULL;
    }

编译会出现警告:void *PLA::operator new(size_t, int, int)表示未找到匹配的删除运算符。这个警告可以不理会,也可以在PLA类中增加对应的operator delete重载以避免这个警告:

   public:
    void operator delete(void *phead, int tvp1, int tvp2) {
        return;
    }

跟踪调试发现上面重载的operator new的第二个参数和第三个参数分别传递进去了1234和56,而第一个参数,系统默认传递进去的是sizeof(PLA)的值。这种new的用法并不会去调用类PLA的构造函数,所以这个重载的operator new里面要做什么事,完全由程序员来控制。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值