拷贝控制之拷贝、赋值、销毁

拷贝控制操作:拷贝构造函数 拷贝赋值运算符 移动构造函数 移动赋值运算符 析构函数

如果一个类没有定义所有这些拷贝控制成员,编译器会自动为它定义缺失的操作。

一、拷贝构造函数

定义:一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值(拷贝构造函数第一个参数必须是引用类型,而且一般是const引用)

原因:拷贝构造函数被用来初始化非引用类型参数,如果其参数不是引用类型,为了调用拷贝构造函数,我们必须拷贝它的实参,无限循环。

使用拷贝初始化的场合

1.将一个对象作为实参传递给一个非引用类型的形参(对象以值传递的方式传入函数参数)

2.从一个返回类型为非引用类型的函数返回一个对象(对象以指传递的方式从函数返回)

3.对象需要另外一个对象初始化

如果两个对象已经存在,就会使用拷贝赋值运算符,如果是使用一个对象去创建(注意是创建)另一个不存在的对象,则会使用拷贝构造函数。

拷贝运算(对象在声明的同时马上进行初始化操作):class1 A("af"); class1 B=A;        赋值运算:class1 A("af"); class1 B;B=A;

合成的拷贝构造函数会将其参数成员逐个拷贝到正在创建的对象中,编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中(浅拷贝)。(因为static成员属于类,不属于对象)

class A{
public:
    A(){ sn++; }
    ~A(){ sn--; }
    static int GetValue(){
        return sn;
    }
private:
    static int sn;
};
int A::sn = 0;
int main(void)
{
    A a1;//调用构造函数,sn++
    cout << "1:" << A::GetValue() << endl;
    A a2(a1);//调用合成拷贝构造函数,对sn无操作,因为sn不属于对象
    cout << "2:" << A::GetValue() << endl;
    system("pause");
    return 0;
}

输出均为1,注意析构的时候sn会减两次,变为-1

二、拷贝赋值运算符

如果赋值操作符可以作为全局函数重载的话,可能会出现表达错误的语句

int operator=(int a, integer b);
这样重载之后,语句
2 = a; 表述也是正确的,但是却是明显的语法错误
为了避免此类错误,需要将赋值操作符重载为成员函数
--------------------------------------------------
首先要知道,如果类中没有重载赋值操作符时,类会自动生成一个默认的赋值操作符。例如,有两个同类对象A和B,当你没有将赋值操作符重载,而进行 A=B 的操作时,编译器会自动调用赋值操作将B的数据成员拷贝到A中。
而如果你重载了一个全局的赋值操作符,那么编译器不知道是否还需要再自己合成一个赋值操作符,从而引发歧义。

重载运算符的参数表示运算符的运算对象,某些运算符,包括赋值运算符,必须定义为成员函数,如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数

拷贝赋值运算符接受一个与其所在类相同类型的参数(引用):

class foo
{
public:
    foo& operator=(const foo&);
    //...
};

为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用(return *this),目的是进行链式赋值(a=b=c).

class A
{
public:

    A(){}
    A(int id, char *t_name)
    {
        _id = id;
        name = new char[strlen(t_name) + 1];
        strcpy(name, t_name);
    }
    //重载copy构造函数,深拷贝
    A::A(A &a)
    {
        _id = a._id;
        name = new char[strlen(a.name) + 1];
        if (name != NULL)
            strcpy(name, a.name);
    }
    //如果直接返回对象,而不是引用,则会调用拷贝构造函数
    A& operator =(A& a)
    {
        if (this == &a)//  问:什么需要判断这个条件?(不是必须,只是优化而已)。答案:提示:考虑a=a这样的操作。
            return *this;
        //首先释放自己的内存,避免内存泄漏
        if (name != NULL)
            delete name;
        this->_id = a._id;
        int len = strlen(a.name);
        name = new char[len + 1];
        strcpy(name, a.name);
        return *this;
    }
    ~A()
    {
        cout << "~destructor" << endl;
        delete name;
    }

    int _id;
    char *name;
};

三、析构函数

由于析构函数不接受参数,因此它不能被重载,对一个给定类,只会有唯一一个析构函数。

析构函数完成的工作:在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化,在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁

四、三五法则

1.需要析构函数的类也需要拷贝和赋值操作

合成析构函数不会delete一个指针数据成员,故在构造函数中分配动态内存后需要定义一个析构函数来释放内存。合成的拷贝和赋值操作只是简单的拷贝指针成员,这样可能会出现多个对象拥有同一个指针,当它们销毁时同一个指针会被delete多次,出现未定义错误。

2.需要拷贝操作的类也需要赋值操作,反之亦然。

五、阻止拷贝

1.定义删除的函数

在新标准下,我们可以通过将拷贝构造函数和拷贝赋值运算符定义为delete function来阻止拷贝delete function是指我们虽然声明了它们,但不能以任何方式使用它们。

=delete必须出现在函数第一次声明的时候,因为编译器需要知道一个函数是删除的,以便禁止试图使用它的操作。

注意:我们不能删除析构函数,对于一个删除了析构函数的类型,编译器将不允许定义该类型的变量或创建该类型的临时对象,但可以动态分配这种类型的对象,不能释放这些对象

struct NoDtor
{
    NoDtor() = default;//使用合成默认构造函数
    ~NoDtor() = delete;
};

NoDtor nd;//错误
NoDtor *p = new NoDtor();//动态分配,准确
delete p;//错误,编译阶段就出错

2.private拷贝控制

在新标准发布之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private的来阻止拷贝,为了阻止友元和成员函数进行拷贝,我们将这些拷贝控制成员声明为private的,但是不定义它们。(正常情况下,友元可以访问private成员)

声明但不定义一个成员函数是合法的,试图访问一个未定义的成员将导致一个链接错误。试图拷贝对象的用户代码将在编译阶段被标记为错误(因为不能访问private成员),成员函数或友元函数中的拷贝操作将会导致链接时错误。

原文地址:https://www.cnblogs.com/ljygoodgoodstudydaydayup/p/3819769.html