C++的特殊成员函数(Special members)与关键字default和delete

特殊成员函数是在特定情况下隐式定义为类成员的成员函数。有如下六个:

一、默认构造函数:如果类申明时没有显示定义任何构造函数,则编译器假定该类具有隐式定义的默认构造函数。例如申明如下的类:
class Example {
public:
int total;
void accumulate (int x) { total += x; }
};
编译器会假定有一个默认构造函数:Example() {},因此通过如下语句直接定义一个变量是能编译过的
Example ex;
一旦类有显示申明的构造函数,编译器就不再隐式提供不带参数的默认构造函数了。例如:

class Example2 {
  public:
    int total;
    Example2 (int initial_value) : total(initial_value) { };
    void accumulate (int x) { total += x; };
};

就只能带参的对象,申明如下的不带参数的对象编译就会报错:“no matching function for call to ‘Example2::Example2()’”
Example2 ex;
那如果我们还是希望系统提供默认的不带参构造函数,default关键字就可以上场了,如下即可编译通过。

class Example2 {
  public:
int total;
Example2() = default;
    Example2 (int initial_value) : total(initial_value) { };
    void accumulate (int x) { total += x; };
};

二、析构函数
在对象生命周期结束时,负责必要地清理工作。如果没有显示申明自定义的析构函数,系统会默认提供。

三、拷贝构造函数
当一个对象被传递给和它同类型的对象,它的拷贝构造函数被调用以构造一个副本,这时执行的是浅拷贝(shallow copy)。如果类没有定义自定义拷贝构造函数或move构造函数(或赋值),则提供隐式拷贝构造函数。
以Example2为例:
Example2 ex(100);
Example2 ex1(ex); //使用的就是拷贝构造函数
此时系统隐式提供的拷贝构造函数大致如下,仅仅复制成员:
Example2:: Example2(const Example2& x):total(x.tatal){}

如果类成员中有指针成员变量,而且这些指针指向处理其存储的对象,需要注意重载拷贝构造函数,进行深拷贝(deep copy)。

class CopyConstruct{
    public:
        CopyConstruct(const string &str):ptr(new string(str)){}
        ~CopyConstruct(){
            if(ptr != NULL){
                delete ptr;
            }
            cout << "~CopyConstruct()" << endl;
        }
        //copy constructor
        CopyConstruct(const CopyConstruct& x):ptr(new string(x.content())){}
        //access content:
        const string& content() const {return *ptr;}
    private:
        string *ptr;
};
int main(){
    CopyConstruct foo("CopyConstruct");
    CopyConstruct bar = foo;
    cout << "bar's content: " << bar.content() << endl;
    return 0;
}

如果上面的没有实现对拷贝构造函数的重载CopyConstruct(const CopyConstruct& x),进行深拷贝,那么在运行时会报double free or corruption错误。通过gdb调试,可以查看到foo和bar对象中的ptr指针的值是一样的,所以会出现double free的问题。
在这里插入图片描述
四、拷贝赋值操作
拷贝赋值操作符也是一个特殊函数,如果类没有定义自定义拷贝或移动赋值(或移动构造函数),则拷贝赋值操作符也是隐式定义的。和拷贝构造函数类似,隐式版本执行的是浅拷贝,这适用于许多类,但不适用于具有指针的类,这些指针指向处理其存储的对象。这种情况下,拷贝赋值操作不但存在double free的风险,而且会导致内存泄漏,所以也需要重载拷贝赋值操作,并进行深拷贝。


```cpp
class CopyConstruct{
    public:
        CopyConstruct(const string &str):ptr(new string(str)){}
        ~CopyConstruct(){
            if(ptr != NULL){
                delete ptr;
            }
            cout << "~CopyConstruct()" << endl;
        }
        //copy constructor
        CopyConstruct(const CopyConstruct& x):ptr(new string(x.content())){
            cout << "CopyConstruct is used\n";
        }
        //access content:
        const string& content() const {return *ptr;}
        //copy assignment
        CopyConstruct& operator= (const CopyConstruct& x){
            cout << "CopyConstruct assignment\n";
            if(ptr != NULL){
                delete ptr;
                ptr = NULL;
            }
            ptr = new string(x.content());
            return *this;
        }
    private:
        string *ptr;
};

int main(){
    CopyConstruct foo("CopyConstruct");
    CopyConstruct test(foo);
    CopyConstruct bar = foo;
    bar = foo; 
    cout << "bar's content: " << bar.content() << endl;
    return 0;
}

运行结果:
在这里插入图片描述
五、移动构造函数和移动赋值函数
与copy类似,move也使用一个对象的值来设置另一个对象的值。 但是,与copy不同的是,内容实际上是从一个对象(源)传输到另一个对象(目标):源丢失了内容,由目标接管。 这种移动只发生在值的源是一个未命名的对象时。未命名对象是指本质上是临时的对象,因此甚至没有名字。未命名对象的典型例子是函数的返回值或类型转换。使用诸如此类的临时对象的值来初始化另一个对象或赋值,实际上不需要副本:对象永远不会被用于其他任何事情,因此,它的值可以被移动到目标对象中。 这些情况触发了move构造函数和move赋值,会更加有效率。move构造函数和move赋值是接受类本身的右值型参的成员。右值引用通过在类型后面跟两个与符号(&&)来指定。作为参数,右值引用匹配这种类型的临时变量的实参。
MyClass (MyClass&&); // move-constructor
MyClass& operator= (MyClass&&); // move-assignment
移动的概念对于管理其使用的存储的对象最有用,例如使用new和delete分配存储的对象。 在这些对象中,复制和移动是真正不同的操作:
—从A复制到B意味着新的内存被分配给B,然后A的全部内容被复制到分配给B的新内存中。
—从A移动到B是指已经分配给A的内存被转移到B,而不需要再分配新的存储空间。 它只涉及到复制指针。

class MoveConstruct{
    public:
        MoveConstruct(const string& str):ptr(new string(str)){
            cout << "construct with string:" << ptr <<": " << *ptr <<" : " << this << endl;
        }
        ~MoveConstruct() {
            cout << "~MoveConstruct\n";
            delete ptr;
        }
        //move constructor
        MoveConstruct(MoveConstruct&& x):ptr(x.ptr) {
            cout << "Move constructor" << ptr <<": " << *ptr << endl;
            x.ptr=nullptr;
            
        }
        //move assignment
        MoveConstruct& operator= (MoveConstruct&& x){
            delete ptr;
            ptr = x.ptr;
            x.ptr = nullptr;
            cout << "Move assignment\n";
            return *this;
        }
        //access content
        const string& content() const {return *ptr;}
        //addition
        MoveConstruct operator+(const MoveConstruct& rhs){
            return MoveConstruct(content() + rhs.content());
        }
    private:
        string *ptr;
};

int main(){
    MoveConstruct foo("Apple");
    MoveConstruct bar = MoveConstruct("Banana");  //move construction

    cout << "&foo= " << &foo <<" &bar= " << &bar << endl;

    MoveConstruct latest = foo + bar;  //move construction
    foo = latest + bar; //move assignment
    cout << "foo's content: " << latest.content() << endl;
    return 0;
}

运行结果如下:
在这里插入图片描述
预期走到移动构造函数两处实际都没有走到。原来是因为:编译器已经优化了许多在返回值优化中需要移动构造调用的情况。 最值得注意的是,当函数返回的值用于初始化对象时。 在这些情况下,实际上可能永远不会调用move构造函数。注意,尽管右值引用可以用于任何函数形参的类型,但它很少用于move构造函数之外的其他用途。 右值引用是棘手的,不必要的使用可能是错误的来源,很难跟踪。

上文所述class的六个特别成员在某些情况下被隐式地实现:
在这里插入图片描述
注意,在相同的情况下,并非所有特殊成员函数都是隐式定义的。 这主要是由于与C结构和早期c++版本的向后兼容性,事实上,有些情况还包括了已弃用的情况。 幸运的是,每个类都可以显式地选择使用默认定义存在哪些成员,或者分别使用关键字default和delete删除哪些成员。 语法是:
function_declaration = default;
function_declaration = delete;

Android的Codec2的代码中有定义如下宏:
禁止使用拷贝构造函数、拷贝赋值函数:

#define C2_DO_NOT_COPY(type) \
    type& operator=(const type &) = delete; \
    type(const type &) = delete; \

针对C++11之前有如下宏定义实现上述功能:

// A macro to disallow the copy constructor and operator= functions
// This should be used in the priavte:declarations for a class
#define    DISALLOW_COPY_AND_ASSIGN(TypeName) \
    TypeName(const TypeName&);                \
    TypeName& operator=(const TypeName&) 

使用默认的移动构造函数、移动赋值函数:

#define C2_DEFAULT_MOVE(type) \
    type& operator=(type &&) = default; \
    type(type &&) = default; \

关键字default用于定义一个如果不删除将被隐式定义的构造函数。一般来说,为了将来的兼容性,如果类显式地定义了一个复制/移动构造函数或一个复制/移动赋值,但不是同时定义了这两个函数,则鼓励对它们没有显式定义的其他特殊成员函数指定delete或default。

参考文档:
https://www.cnblogs.com/xinxue/p/5503836.html
https://www.cplusplus.com/doc/tutorial/classes2/

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值