C++拷贝

拷贝控制操作即对象的拷贝,移动,赋值和销毁。一个类通过拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符和析构函数来完成这些工作。拷贝和移动构造函数定义了当用相同类型的另一个对象初始化本对象时做什么。拷贝和移动运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。
如果一个类没有定义这些拷贝控制成员,编译器会自动为它定义缺失的操作。不过,对于一些特殊的类来说,这会引起很大的麻烦、

拷贝、赋值与销毁:

拷贝构造函数:如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数

拷贝构造函数的第一个参数几乎总是一个 const 的引用。因为拷贝构造函需要数被用来初始化非引用类类型参数。如果其参数不是引用类型,则永远不会成功——为了调用拷贝构造函数,我们必须拷贝它的实参,但为了拷贝实参,我们又需要调用拷贝构造函数,如此无限循环。

拷贝构造函数在有些情况下会被隐式的使用,因此拷贝构造函数通常不应该是 explicit 的

与合成默认构造函数不同,即使我们定义了其它构造函数(没有定义拷贝构造函数),编译器也会为我们合成一个拷贝构造函数

对于某些类来说,合成拷贝构造函数用来阻止我们拷贝该类类型的对象。对于这些特殊的类(如IO类等),编译器会默认合成拷贝构造函数并将其定义为 delete 的成员

而一般情况,合成的拷贝构造函数会将其参数逐个拷贝到我们正在创建的对象中。编译器从给定的对象中将每个非 static 成员拷贝到正在创建的对象中。

每个成员的类型决定了它如何拷贝:对类类型的成员,会使用拷贝构造函数来拷贝;内置类型的成员则直接拷贝。虽然我们不能直接拷贝一个数组,但合成拷贝构造函数会逐元素地拷贝一个数组的成员。如果数组元素是类类型,则使用元素的拷贝构造函数来进行拷贝。

拷贝初始化:

#include <iostream>
using namespace std;

int main(void){
    string a(10, 'a');//直接初始化
    string b(a);//直接初始化
    string c = a;//拷贝初始化
    string d = "fk";//拷贝初始化
    string e = string("fl");//拷贝初始化
    return 0;
}

直接初始化和拷贝初始化的差异:当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。当我们使用拷贝初始化时,我们要求编译器将运算符右侧的对象拷贝到正在创建的对象中,如果需要的话还需要进行类型转换。即,通常使用了 = 运算符初始化的就是拷贝初始化,反之则是直接初始化

注意:只有用 = 运算符定义变量时才是拷贝初始化,而对于已经存在的变量使用 = 则是拷贝赋值运算。

编译器的思想是能不用临时对象就不用临时对象。在拷贝初始化过程中,编译器可以 (但不是必须) 跳过拷贝 / 移动构造函数,直接创建对象。因此对于下面这些拷贝初始化,都不会生成临时对象再进行拷贝或移动到目标对象,而是直接通过函数匹配调用相应的构造函数:

#include <iostream>
using namespace std;

class gel{
private:
    int x, y, z;

public:
    gel(int a, int b, int c) : x(a), y(b), z(c) {cout << "111" << endl;}
    gel(int a) : x(a), y(0), z(0) {cout << "222" << endl;}
    gel(const gel& it) : x(it.x), y(it.y), z(it.z){cout << "333" << endl;}
    ~gel(){cout << "444" << endl;}

};

int main(){
    //编译器的思想是能不用临时变量就不用临时变量,所以对于下面的这些拷贝初始化,编译器不会生成临时变量,而是直接通过函数匹配调用响应的构造函数
    gel a=1;//虽然是拷贝初始化,但是是直接调用gel(1)构造函数进行初始化的,
    gel b=gel(1,2,4);//相当于 gel b(1,2,3);
}

注意:即便编译器略过了拷贝 / 移动构造函数,但在这个程序点上,拷贝 / 移动构造函数必须是存在且可访问的(如,不能是 private 的)。

重载赋值运算符:

如果我们未定义自己的拷贝赋值运算符,编译器会自动生成一个合成拷贝赋值运算符。

赋值运算符重载时必须被定义为成员函数

为了与内置类型保持一致,赋值运算符通常应该返回一个指向其左侧对象的引用(不然将不能使用连等运算)。而且,标准库通常要求保持在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。

大家要注意深拷贝和浅拷贝的问题:
“深拷贝和浅拷贝最根本的区别在于是否是真正获取了一个对象的复制实体,而不是引用, 深拷贝在计算机中开辟了一块内存地址用于存放复制的对象,而浅拷贝仅仅是指向被拷贝的内存地址,如果原地址中对象被改变了,那么浅拷贝出来的对象也会相应改变。”

合成拷贝赋值运算符:

类似于拷贝构造函数,对于某些类,合成拷贝赋值运算符用来禁止该类型对象的赋值:如果类的某个成员的拷贝赋值运算符是删除的不可访问的有const成员有引用成员则类的合成拷贝赋值运算符被定义为删除的
否则:将会为每个非static成员赋值

析构函数:

析构函数执行与构造函数相反的操作:释放对象使用的资源,并销毁对象的非 static 数据成员

析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数——因此析构函数不能被重载,对于一个给定的类只会有唯一一个析构函数

析构函数:首先执行函数体,然后按照初始化的逆序进行销毁,
注意:析构函数销毁一个指针,不会销毁指针所指的对象,因此需要在析构函数中显示的delete它所指向的对象。
和普通指针不同,智能指针是类类型,会执行它自己的析构函数,因此智能指针所指的对象,会在析构阶段自动销毁。

#include <iostream>
#include <memory>
using namespace std;

class gel{
friend ostream& operator<<(ostream&, const gel&);

private:
    int x, y;

public:
    gel(int a, int b) : x(a), y(a) {}
    gel(int a) : gel(a, 0) {}
    gel() : gel(0, 0) {}
    ~gel() {
        cout << "~gel" << endl;
    }

};

ostream& operator<<(ostream &os, const gel &it){
    cout << it.x << " " << it.y;
    return os;
}

void gg(gel*it){}//这是一个指针,在离开gg作用域的时候不会调用析构函数
void yy(gel &ot){}//这是引用,在离开yy的作用遇到额时候不会调用析构函数
int main(){
    gel a(1);
    gg(&a);
    cout<<"======"<<endl;
    yy(a);
    cout<<"---------"<<endl;//很显然,只有在a离开作用域的时候会执行析构函数
}
三五法则:

需要自定义析构函数的类,也得自定义赋值和拷贝操作。
如果我们定义了析构函数,意味着我们的类中有new创建的指针成员,如果我们没有定义赋值和拷贝运算,那么赋值和拷贝是浅拷贝,会使多个类的成员指向同一块内存

原解释如下:很显然,需要自定义析构函数意味着我们的类中有用 new 创建的内置指针成员,那么如果我们使用合成拷贝构造函数以及合成拷贝赋值运算符而不自定义拷贝和赋值操作的话,进行拷贝和赋值操作都将是浅拷贝,这可能导致多个对象中的指针成员都指向相同的内存地址。

需要自定义拷贝操作的类也需要自定义赋值操作,反之亦然:

举例子:考虑每个类为每个对象分配一个独有的,唯一的序号,这个类需要一个拷贝构造函数为每个新创建的对象生成一个新的独一无二的序号。同时赋值操作也是。

使用default
我们可以使用default来显式的要求编译器生成合成版本,但是我们使用default的时候,合成的函数会隐式的声明为内联的,如果不希望如此,应该在类外使用default。

阻止拷贝:

定义删除的函数:
在函数的参数列表后面加上 = delete 来指出我们希望将它定义为 删除的:

#include<bits/stdc++.h>
using namespace std;
class Node{
    Node()=default;//使用内联的合成函数
    ~Node()=default;//使用内联的析构函数
    Node(const Node&)=delete;//阻止拷贝
    Node&operator=(const Node&)=delete;//阻止赋值
};
void f()=delete;//类外函数定义为删除的,不可用的
int main(){
    return 0;
}

和default不同,delete函数必须在第一次声明的时候就出现:默认的成员只影响这个成员生成的代码,=default只有编译器需要的时候才出现,但是编译器必须一开始就知道一个函数是删除的,从而阻止某些操作。
另外,我们可以对任何函数指定default。

删除函数的主要用途是禁止拷贝控制成员,也可以用来引导函数匹配过程

析构函数我们无法定义为删除的,这样我们无法销毁这样的内存,但是我们可以动态分配这样的对象

#include<bits/stdc++.h>
using namespace std;
class Node{
public:

    Node()=default;//使用内联的合成函数
    ~Node()=delete;//使用内联的析构函数
    Node(const Node&)=delete;//阻止拷贝
    Node&operator=(const Node&)=delete;//阻止赋值
};
void f()=delete;//类外函数定义为删除的,不可用的
int main(){
    Node *a=new Node();
    delete a;//错误,内存无法被释放
}

合成的拷贝控制成员可以是删除的:

如果类的某个成员的析构函数是删除的或不可访问的 (如,private 的),则类的合成析构函数被定义为删除的

如果类的某个成员的拷贝构造函数是删除的或不可访问的,或是某个成员的析构函数是删除的或不可访问的,则类的合成拷贝构造函数被定义为删除的

如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或是类有一个 const 的或引用的成员,则类的合成拷贝赋值运算符被定义为删除的

如果类的某个成员的析构函数是删除的或不可访问的,或是类有一个引用的成员,它没有类内初始化器,或是类有一个 const 成员,它没有类内初始化器且未显示定义默认构造函数,则该类的默认构造函数被定义为删除的。

本质上:如果一个类不能默认构造,拷贝,赋值,或者销毁,所以对应的函数是删除的

在C++11标准之前,这种delete是通过private来进行的,但是这样的话代码会非常难读。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值