c++拷贝控制

拷贝控制

当我们定义一个类的时候,一定要注意类的拷贝控制,这就直接关系到类的内存管理。对于一个类的基本操作有:拷贝构造,拷贝赋值,移动构造,移动赋值,析构销毁。拷贝的两种方式:类值和类指针。其实就是深度拷贝和浅拷贝。

编译器做了什么

即使我们不定义任务以上的基本操作,编译器同样会生成他们,包括:拷贝构造函数,析构函数,拷贝赋值,默认构造。如果我们定义了任意构造函数都不会生成默认构造。

先理解拷贝初始化与直接初始化

直接初始化(directed initialization):他的过程其实是调用最优匹配重载构造函数的过程。

官方定义
Initializes an object from explicit set of constructor arguments.

直接初始化发生的时间
直接初始化发生的时间

拷贝初始化(copy
initialization)
:他的过程是类型转化的过程,从一个类型转为另一个类型,并且完成初始化。

官方定义
Initializes an object from another object.

拷贝初始化发生时间
拷贝初始化发生时间

拷贝初始化得以执行的根本条件是构造函数(转化函数)是隐式的,否则将会有语法错误。并且发生在拷贝初始化的隐式转换只能有一次。

class A
{
    A(string str){}
    string str;
}
void main()
{
    A a("abc");//ok
    A a = "abc"//error 这里"abc"是char*类型,需要发生char*->string->A
}

另外拷贝构造函数被调用不一定是拷贝初始化,应该从根本定义来区分。拷贝初始化可能调用移动构造也可能调用拷贝构造。

拷贝构造函数

构造函数的第一个参数是自身的引用(必须是自身引用,否则会发生构造函数递归调用),也可以有其他参数,但是其他参数都需要有默认值。一般情况下,引用加上const修饰。
拷贝构造函数一般发生在拷贝初始化的时候。不同的编译器,可能会在执行拷贝初始化的时候不执行拷贝构造函数。

拷贝赋值运算符

class A
{
    public:
        string str;
        double a;
        float *ptr;
        A& operator=(const A& rhs)
        {
            str = rhs.str;//调用string的=
            a = rhs.a;
            ptr = rhs.ptr;//指向右操作数的内存,两个对象共享这一字段的内存
            return *this;
        }
};

编译器默认生成的赋值运算符有时候会和自己想要的结果有差别。

析构函数

一个类只有一个,不接受参数,所以不能重载。

作用

析构函数的作用就是释放资源,如果类内没有动态的资源则可以不显式的定义析构函数。析构函数真正销毁成员的操作,并不在函数体之中,而是在函数体执行完成之后,隐式执行的。成员销毁顺序和构造顺序相反(整个构造和析构是两个相反的过程)。

析构函数一般不能显式的调用,即使调用也很有可能不会使所期望的结果,析构函数隐式调用除了执行函数内的语句还进行了栈上内存的释放,而显式调用不会释放。

发生时间

  • 变量离开作用域。
  • delete
  • 容器销毁时。(容器里如果是指针,也不会去释放指针指向的对象)
  • 表达式的临时变量,在表达式结束的时候

构造函数的构造顺序是成员声明的顺序而析构函数的顺序与之相反


移动构造函数

这是新标准中加入的。他的目的是更加快速的构造一个对象。“移动”,就是把一个对象的所有资源转移到另一个对象中,这个过程结束后目的对象完全获取到源对象的资源,源对象即将要销毁,如果不销毁,我们也不能对源对象的值做任何的假设,当然它可以被赋予新的值。
要理解移动操作,还必须要理解右值引用。右值引用是相对于常规引用(左值引用)。c++premier中的区别左值右值的说法是左值持久,右值短暂,所有右值都是即将要销毁的,且没有其他对象使用。

int a =5;
int &b = a;//正确 左值引用
int &&c=a;//错误 右值引用只能绑定在即将销毁的变量 字面值 临时变量
int &&d = 5;//正确
int &&e=d;//错误 右值引用不能绑定右值引用
int &&f = std::move(a);//正确 move操作可以将一个左值移动到右值引用上用cout输出a和b的地址会发现地址是一样的
//拷贝构造函数的一般写法(c++primer)
StrVec::StrVec(StrVec&& s) noexcept
//初始化参数列表,如果以下成员可以移动也会调用相应的移动构造函数
:elements(s.elements),first_free(s.first_free),cap(s.cap)
{
    //为确保移后源对象是可以析构
    s.elements = s.first_free = s.cap = nullptr;
}

为什么要使用noexcept关键字呢?因为,在标准库中的很多操作要求构造函数是异常安全的,即使发生了异常,原来的对象都不会改变。如果移动操作发生了异常,那么可能会出现旧的对象部分内存被析构,而新的对象却没有被创建好,所以可能会发生异常的移动构造函数标准库是不会调用的,一般会使用拷贝构造函数来完成。但是,在实际使用中,移动构造函数不分配新的内存,所以一般是不会发生异常的。那么,要想让标准库调用自己写的移动构造函数,就必须将其声明为noexcept。同样地,在移动构赋值运算符中,同样是需要将其声明为noexcept的。

移动赋值运算符

StrVec & StrVec::operator=(StrVec &&rhs) noexcept
{
    //检测自赋值------------------------------------1
    if(this != &rhs)
    {
        //释放已有元素------------------------------2
        free();
        elements = rhs.elements;
        first_free = rhs.first_free;
        cap = rhs.cap;
        //将rhs置于可析构的状态---------------------3
        rhs.elements = rhs.first_free = rhs.cap = nullptr;
    }
    return *this;
}

关于以上的移动操作,只有当用户没有自定义任何拷贝函数(拷贝构造、拷贝赋值、析构),并且所有他的成员都是可以移动的时候,才会生成移动构造活着移动赋值。

三五法则

c++perimer中说的三五法则的意思是,各种拷贝控制函数之间的依赖关系。
控制拷贝操作的三个函数:拷贝构造函数 赋值操作符 析构函数。再加上新标准引入的:移动构造和移动赋值

  • 需要析构函数的类一般也需要显式的声明拷贝构造函数和赋值运算符

析构函数之所以显式存在是因为需要释放动态开辟的内存空间,这也说明了类成员包括指针,如果不显示指定拷贝和复制操作,就会出现浅拷贝,发生内存多次释放的情况

  • 需要拷贝构造函数则一定也会需要显式声明赋值运算符

可以看出拷贝构造和赋值操作都对一个对象的拷贝操作,如果只声明其中之一很可能会造成操作不能按预期实现,但析构函数不一定需要

  • 上一条反之亦然

一般来说,定义了任意一个拷贝控制成员,最好将五个都显式的定义出来


使用=default delete

使用default关键字可以显式的要求编译器生成合成版本。
使用delete关键字禁用函数,例如拷贝构造和复制等

class A
{
public:
    A() = default;//内联
    A(const A&) = delete;//禁用拷贝构造
    A& operator = (const A&);
    A(int v):val(v){}
    ~A()= delete;//析构函数被delete后就不能定义对象了
    int val;
};
A& A::operator = (const A&) = default;//非内联

default只可以使用在编译器会合成的函数上而delete则对任意函数都可以。

在新标准之前,一般都是用private来禁用拷贝操作,如果使类内成员也不能访问则可以只声明而不定义,这样就会发生链接错误。

析构函数最好不要显示调用 显示调用的析构函数不会达到期望的要求

另外,在一些情况下编译器是不会产生一些拷贝操作函数,比如类内的成员本身是不可拷贝,赋值,析构。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容