Featured image of post C++面向对象高级开发-兼谈对象模型-关于const、new、delete、basic_string使用new(extra)扩充申请量

C++面向对象高级开发-兼谈对象模型-关于const、new、delete、basic_string使用new(extra)扩充申请量

关于const, 关于new, delete, basic_string使用new(extra)扩充申请量

C++面向对象高级开发-兼谈对象模型-关于const、new、delete、basic_string使用new(extra)扩充申请量

Notes

  • 关于const
  • 关于new, delete
  • basic_string使用new(extra)扩充申请量

关于const

在设计规范中,第4条规定了若成员函数内对数据无任何改动,则在函数参数列后本体前增加const关键字。 关于const

当对象调用函数时,对象可为const对象或非const对象,函数可为const成员函数或非const成员函数。当const对象调用const函数时,const函数不会更改数据,故const对象可以调用const函数。非const对象调用const函数时,const函数不改变数据,所以此时仍成立。当非const对象调用非const函数时,数据可能改变也可能不改变,条件均满足。若const对象调用非const函数时,数据不可改变,函数要改变数据,故此路不通。

const String str("Hello World!");
str.print();

代码中定义了const字符串str,调用来自String类的print函数。若类设计者在print函数定义中没有const关键字,此时为const对象调用非const函数,故编译器报错。所以,在类设计中成员函数是否增加const关键字至关重要。 上图中右侧的部分取自标准库std::basic_string。其对操作符[]重载函数有带const的版本和不带const的版本,返回类型不同,实现函数重载。故在函数定义中是否包含const是函数不同的signature。设计带与不带const函数版本是因为标准库的字符串使用了引用计数(Reference Counting)。若多个字符串使用相同的内容时,通过引用计数,使得多个字符串共享相同的数据,可以减少空间占用。若其中某个字符串要更改内容,可单独拷贝一份给要更改内容的字符串,拷贝并更改内容可称之为CopyOnWrite。若调用操作符[]重载的对象是常量字符串,内容不可能改变时,不需要CopyOnWrite。区分操作符是常量字符串调用或非常量字符串调用,根据上面讲述的原则,const对象可以调用const函数,const对象不可以调用非const函数,故常量字符串调用带const关键字的操作符重载函数,不必考虑CopyOnWrite,非常量字符串调用非const函数。此时对于const函数而言,可能存在非const对象调用的情况,C++编译器对于此种情况规定:成员函数的const和非const版本同时存在时,onst对象只能调用带const的成员函数,非const对象只能调用不带const的成员函数。

关于new, delete

在先前的讲述中,已经包含了new:先分配内存,再调用构造函数delete:先调用构造函数,再释放内存。这里的new与delete均为表达式。使用new关键字初始化对象时,编译器转换为3个步骤,使用operator new调用malloc函数申请内存空间,并使用static_cast进行空间类型转换为目标类型并赋值,最后调用构造函数进行对象初始化。使用delete关键字销毁对象时,编译器转换为2个步骤,首先调用当前类的析构函数,而后使用operator delete调用free函数释放空间。new关键字转换为3步与delete关键字转换为2步执行的过程为编译器自动转换,其过程不可改变,如使用operator newoperator delete调用的函数mallocfree等可重载。之前的课程也谈到包括内存块动态分配以及arraynew与arraydelete的问题。类可以通过用来重载相关的函数做内存池设计。

重载::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) {
    std::cout << "Fa1c0n Global New()" << std::endl;
    return myAlloc(size);
}

inline void* operator new[](size_t size) {
    std::cout << "Fa1c0n Global New[]()" << std::endl;
    return myAlloc(size);
}

inline void operator delete(void* ptr) {
    std::cout << "Fa1c0n Global Delete()" << std::endl;
    myFree(ptr);
}

inline void operator delete[](void* ptr) {
    std::cout << "Fa1c0n Global Delete[]()" << std::endl;
    myFree(ptr);
}

operator new函数中必有参数size_t size,new函数由编译器转换调用。operator delete函数接收指针void* ptr,delete函数也由编译器转换调用。由于对new与delete操作符重载为全局重载时,应慎重考虑是否使用全局重载。函数的接口如上面代码所示。对于函数operator new与delete的重载:代码如下所示:

class Foo {
public:
    void* operator new(size_t); //per-class allocator.
    void  operator delete(void*, size_t); //size_t is optional.
};

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

编译器对Foo* p = new Foo;语句中的new关键字展开的代码为:

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

编译器对delete p;语句中的delete关键字展开的代码为:

p->~Foo();
operator delete(p);

当在Foo类中进行全局new与delete关键字的重载时,类使用者通过语句Foo* p = new Foo;执行类对象初始化操作,通过语句delete p;执行类对象销毁操作,编译器分别自动转换为语句void* mem = operator new(sizeof(Foo));与语句operator delete(p);。其调用的操作符重载函数恰好是在类Foo中定义的operator newoperator delete函数。接管后可实现内存池设计。对于函数operator new[]operator delete[]的重载:代码如下所示:

class Foo {
public:
    void* operator new[] (size_t);  //per-class allocator.
    void  operator delete[] (void*, size_t);    //size_t is optional.
    //...
};

Foo* p = new Foo[N];
...
delete[] p;

编译器对Foo* p = new Foo[N];语句中的new关键字展开的代码为:

try {
    void* mem = operator new(sizeof(Foo));
    p = static_cast<Foo*>(mem);
    p->Foo::Foo();  //N次
}

编译器对delete[] p;语句中的delete关键字展开的代码为:

p->~Foo();  //N次
operator delete(p);

同上,类使用者通过语句Foo* p = new Foo[N];执行类对象初始化操作,通过语句delete[] p;执行类对象销毁操作,编译器分别自动转换为语句void* mem = operator new(sizeof(Foo));与语句operator delete(p);。其调用的操作符重载函数恰好是在类Foo中定义的operator new[]operator delete[]函数。

示例程序

//
//  main.cpp
//  C++NewDeleteOverload
//
//  Created by Fa1c0n on 2020/5/5.
//  Copyright © 2020 Fa1c0n. All rights reserved.
//

#include <iostream>
#include <cstdlib>

class Foo {
public:
    int _id;
    long _data;
    std::string _str;
    Foo(): _id(0) {
        std::cout << "default ctor.this = " << this << " id = " << _id << std::endl;
    }
    Foo(int i) : _id(i) {
        std::cout << "ctor.this = " << this << " id = " << _id << std::endl;
    }


    //virtual
    ~Foo() {
        std::cout << "dtor.this = " << this << " id = " << _id << std::endl;
    }

    static void* operator new(size_t size);
    static void  operator delete(void* pdead, size_t size);
    static void* operator new[](size_t size);
    static void  operator delete[](void* pdead, size_t size);
};


void* Foo::operator new(size_t size) {
    Foo* p = (Foo*)malloc(size);
    std::cout << "Foo::operator new(), size = " << size << "\t return: " << p << std::endl;
    return p;
}

void Foo::operator delete(void* pdead, size_t size) {
    std::cout << "Foo::operator delete(), pdead = " << pdead << " size = " << size << std::endl;
    free(pdead);
}

void* Foo::operator new[](size_t size) {
    Foo* p = (Foo*)malloc(size);
    std::cout << "Foo::operator new[](), size = " << size << "\t return: " << p << std::endl;
    return p;
}

void Foo::operator delete[](void* pdead, size_t size) {
    std::cout << "Foo::operator delete[](), pdead = " << pdead << " size = " << size << std::endl;
    free(pdead);
}

int main(int argc, const char * argv[]) {
    std::cout << sizeof(int) << " " << sizeof(long) << " " <<  sizeof(std::string) << " "  << sizeof(int*) << std::endl;
    Foo* pf = new Foo;      //若无成员new函数则调用全局new函数
    delete pf;              //若无成员delete函数则调用全局delete函数
    Foo* pfa = ::new Foo;   //强制采用全局new函数
    ::delete pfa;           //强制采用全局delete函数
    std::cout << "sizeof(Foo) = " << sizeof(Foo) << std::endl;
    Foo* p = new Foo(7);
    delete p;
    Foo* pArray = new Foo[5];
    delete[] pArray;

    Foo* pglobal = ::new Foo(7);
    ::delete p;
    Foo* pArrayglobal = ::new Foo[5];
    ::delete[] pArray;
    return EXIT_SUCCESS;
}

本机执行结果:

4 8 24 8
Foo::operator new(), size = 40	 return: 0x1006688a0
default ctor.this = 0x1006688a0 id = 0
dtor.this = 0x1006688a0 id = 0
Foo::operator delete(), pdead = 0x1006688a0 size = 40
default ctor.this = 0x1006688a0 id = 0
dtor.this = 0x1006688a0 id = 0
sizeof(Foo) = 40
Foo::operator new(), size = 40	 return: 0x1006688a0
ctor.this = 0x1006688a0 id = 7
dtor.this = 0x1006688a0 id = 7
Foo::operator delete(), pdead = 0x1006688a0 size = 40
Foo::operator new[](), size = 208	 return: 0x1006688d0
default ctor.this = 0x1006688d8 id = 0
default ctor.this = 0x100668900 id = 0
default ctor.this = 0x100668928 id = 0
default ctor.this = 0x100668950 id = 0
default ctor.this = 0x100668978 id = 0
dtor.this = 0x100668978 id = 0
dtor.this = 0x100668950 id = 0
dtor.this = 0x100668928 id = 0
dtor.this = 0x100668900 id = 0
dtor.this = 0x1006688d8 id = 0
Foo::operator delete[](), pdead = 0x1006688d0 size = 208
ctor.this = 0x1006688a0 id = 7
dtor.this = 0x1006688a0 id = 7
default ctor.this = 0x1006688d8 id = 0
default ctor.this = 0x100668900 id = 0
default ctor.this = 0x100668928 id = 0
default ctor.this = 0x100668950 id = 0
default ctor.this = 0x100668978 id = 0
dtor.this = 0x100668978 id = 0
dtor.this = 0x100668950 id = 0
dtor.this = 0x100668928 id = 0
dtor.this = 0x100668900 id = 0
dtor.this = 0x1006688d8 id = 0
Program ended with exit code: 0

参考执行结果: 参考执行结果

参考执行结果与本机执行结果不同的原因在于,教程示例中使用32位的编译器编译并执行,本机测试使用64位编译器编译并执行。经测试,在多台64位机器中执行结果相同。其根本原因在于int,long,string,int*(指针)在64位编译器中所占内存大小与32位编译器不同。32位中,int类型占用4字节,long类型占用4字节,string类型为指针,占用4字节,故在参考结果中,sizeof(Foo)的大小为12。而64位系统中,int类型占用4字节,long类型占用8字节,string类型占用24字节,int*(指针)类型占用8字节。故其sizeof(Foo)的大小为40。参考示例中Foo的大小为12,在无虚函数的情况下,初始化5个Foo对象的数组占用空间12x5=60个字节,还有4个字节的counter(本例中共5个数据,故counter的值为5,大小为4),共64个字节。而带有虚函数析构函数的Foo对象因含有vptr与vtbl,虚指针占用4个字节,故带有虚函数析构函数的Foo对象占用16个字节共80个字节,加上4个字节的counter共84个字节,如参考执行结果所示。调用构造函数的方向为从上至下,析构函数的方向为从下至上(图例)。故运行结果中,共调用5次构造函数,调用5次析构函数。编译器从counter获取调用构造函数与析构函数的次数。在示例代码中后半部分,使用::new::delete即为绕开所有成员函数内部的重载函数,强制使用全局的new与delete函数。至此,我们可以重载局部成员函数或全局的new与delete函数,可针对单一对象重载,也可针对数组对象重载。我们还可以重载类成员函数的operator new()operator delete()。 关于operator new(),可存在多个版本,但必须保证每个版本的声明必须为独有的参数列。第一参数必须是size_t size,其余参数以new所指定的替代参数为初值。替代参数即为出现在new()括号内部的参数。如Foo* pf = new(300, 'c') Foo;,语句中new(300,'c')含有两个参数,加上第一参数size_t size,共有三个参数。 关于operator delete(),同样可存在多个版本,但他们绝不会被delete关键字调用。只有对应版本的operator new调用的构造函数抛出异常时,才会调用重载的operator delete()。主要目的为实现析构未能完全创建成功的对象所占用的内存空间。示例代码如下所示:

//
//  main.cpp
//  C++NewDeleteOverloadMultiImplementation
//
//  Created by Fa1c0n on 2020/5/10.
//  Copyright © 2020 Fa1c0n. All rights reserved.
//

#include <iostream>
#include <cstdlib>

class Bad {
public:
    Bad() {
        std::cout << "Bad::Bad()" << std::endl;
    }
};

class Foo {
private:
    int m_i;
public:
    Foo() {
        std::cout << "Foo::Foo()" << std::endl;
    }
    Foo(int num) {
        std::cout << "Foo::Foo(" << num << ")" << std::endl;
        throw Bad();    // 故意抛出异常,测试placement operator delete.
    }

    // (1) 一般的operator new()重载
    void* operator new(size_t size) {
        std::cout << "operator new(size_t size), size = " << size << std::endl;
        return malloc(size);
    }

    // (2) 标准库提供的placement new()的重载,模拟标准placement new,只传回pointer
    void* operator new(size_t size, void* start) {
        std::cout << "operator new(size_t size, void* start), size = " << size << " start = " << start << std::endl;
        return start;
    }
    // (3) 崭新的placement new
    void* operator new(size_t size, long extra) {
        std::cout << "operator new(size_t size, long extra), size = " << size << " extra = " << extra << std::endl;
        return malloc(size+extra);
    }

    // (4) 又一个placement new
    void* operator new(size_t size, long extra, char init) {
        std::cout << "operator new(size_t size, long extra, char init), size = " << size << " extra = " << extra << " init = " << init << std::endl;
        return malloc(size+extra);
    }

    // (5) placement new, 第一参数type错误示例
    // 'operator new' takes type 'size_t'('unsigned int') as first parameter
    /*void* operator new(long extra, char init) {
        return malloc(extra);
    }*/

    // 构造函数发生异常,对应的operator delete会被调用,以释放对应的placement new分配所得内存
    // (1) 一般的operator delete()
    void operator delete(void*, size_t) {
        std::cout << "operator delete(void*, size_t)" << std::endl;
    }

    // (2) 标准库提供的placement delete()的重载
    void operator delete(void*, void*) {
        std::cout << "operator delete(void*, void*)" << std::endl;
    }

    // (3) 崭新的placement delete
    void operator delete(void*, long) {
        std::cout << "operator delete(void*, long)" << std::endl;
    }

    // (4) 又一个placement delete
    void operator delete(void*, long, char) {
        std::cout << "operator delete(void*, long, char)" << std::endl;
    }
};

int main(int argc, const char * argv[]) {
    Foo start;
    Foo* p1 = new Foo;
    Foo* p2 = new(&start) Foo;
    Foo* p3 = new(100) Foo;
    Foo* p4 = new(100, 'a') Foo;
    Foo* p5 = new(100) Foo(1);
    Foo* p6 = new(100, 'a') Foo(1);
    Foo* p7 = new(&start) Foo(1);
    Foo* p8 = new Foo(1);
    return EXIT_SUCCESS;
}

不带抛出异常的运行结果:

Foo::Foo()
operator new(size_t size), size = 4
Foo::Foo()
operator new(size_t size, void* start), size = 4 start = 0x7ffeefbff3c8
Foo::Foo()
operator new(size_t size, long extra), size = 4 extra = 100
Foo::Foo()
operator new(size_t size, long extra, char init), size = 4 extra = 100 init = a
Foo::Foo()
operator new(size_t size, long extra), size = 4 extra = 100
Foo::Foo(1)
operator new(size_t size, long extra, char init), size = 4 extra = 100 init = a
Foo::Foo(1)
operator new(size_t size, void* start), size = 4 start = 0x7ffeefbff3c8
Foo::Foo(1)
operator new(size_t size), size = 4
Foo::Foo(1)
Program ended with exit code: 0

带抛出异常的运行结果:

Foo::Foo()
operator new(size_t size), size = 4
Foo::Foo()
operator new(size_t size, void* start), size = 4 start = 0x7ffeefbff3c8
Foo::Foo()
operator new(size_t size, long extra), size = 4 extra = 100
Foo::Foo()
operator new(size_t size, long extra, char init), size = 4 extra = 100 init = a
Foo::Foo()
operator new(size_t size, long extra), size = 4 extra = 100
Foo::Foo(1)
Bad::Bad()
libc++abi.dylib: terminating with uncaught exception of type Bad
Program ended with exit code: 9

带参构造函数抛出异常,并没有调用operator delete(void*, long)。经测试GNU_C_VER_2.9存在调用析构函数。即使operator delete(...)未能与operator new(...)一一对应,编译器不强制存在也不会报错,即选择放弃构造函数发生的异常。

basic_string使用new(extra)扩充申请量

basic_string

目前已知的new,包含new表达式、operator newarray newplacement new等。标准库中使用placement new的实例之一,即为basic_string使用new(extra)扩充申请量。

template <...>
class basic_string {
private:
    struct Rep {
    ...
    void release() { if (--ref == 0) delete this; }
    inline static void* operator new(size_t, size_t);
    inline static void operator delete(void*);
    inline static Rep* create(size_t);
    ...
    };
    ...
};

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;
    ...
    return p;
}

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 void basic_string<charT, traits, Allocator>::Rep::operator delete(void* ptr) {
    Allocator::deallocate(ptr, sizeof(Rep) + reinterpret_cast<Rep *>(ptr)->res* sizeof(charT));
}

basic_string类的定义中,结构体Rep实现功能为字符串引用计数。结构体Rep中重载了operator newoperator delete。在函数basic_string::Rep::create(size_t extra)中,语句Rep* p = new(extra) Rep;会调用函数basic_string::Rep::operator new(size_t s, size_t extra);,即分配的内存空间为引用计数Rep所占空间与字符串内容所占空间extra,即当我们需要在底层分配较多的空间时,可使用placement new实现。