说说C++的默认构造函数

编译器经常会偷偷地干一些事情,所以有时候会让人困惑,有时候你以为它没做,其实它做了,有时候你以为它做了,其实它没做!今天我们讨论一下默认构造函数那些事儿。

当一个类没有添加构造函数时,编译器会在需要的时候,自动添加默认构造函数、拷贝构造函数、赋值运算符。

class A
{
};
A a;
A a2(a);
a2 = a;

经过编译器处理后,它变成下面这样

//编译器处理后的class A
class A
{
public:
A(){}
A(const A&) {}
A& operator = (const A&){return *this;}
};
A a;
A a2(a);
a2 = a;

这种行为是理论上的行为,但实际上编译器并不一定会这样做。默认构造函数也分为两种,一种叫做trivial default constructor,一种叫做nontrivail default constructor,前者合成的构造函数完全没有做任何事情,后者才会做一些有用的事情。

对于trivial default constructor来说,编译器在优化的时候,甚至压根就不会合成,因为编译器认为它完全没有用,为它生成额外的函数代码是浪费的。

一旦我们手动写了构造函数,编译器将不会生成的默认构造函数,如

class A
{
public:
A(int){}
};
A a;
A a2(a);
a2 = a;

经过编译器处理后,它变成下面这样

//编译器处理后的class A
class A
{
public:
A(int){}
A(const A&) {}
A& operator = (const A&){return *this;}
};
A a;    //error C2512: “A”: 没有合适的默认构造函数可用
A a2(a);
a2 = a;

可以看出,这里没有了默认的A()构造函数了!此时A a这种调用将被提示编译错误!

于是这里引出第一个问题:为什么手动添加构造函数后,编译器不再为我们合成默认构造函数了?

我的理解是:当你已经手动控制类的构造以后,说明你已经有着明确的设计思想了,这个时候再偷偷地合成默认构造函数,使得A a这种合法的话,很可能是出乎设计者初衷的。

编译器的这种处理思想是:当你需要一些东西又没写的时候,我会为你做这些事。但你不需要一些东西的时候,我绝不做这些事!

我们知道,当我们写下一个类的时候,很多时候,我们都需要默认构造、拷贝构造、赋值操作,那么一个必须的步骤就是我们要写出以下代码

class A
{
public:
A(int){}
A(const A&) {}
A& operator = (const A&){return *this;}
};

每次都这样做很麻烦不是?编译器说:不用每次都这样做,如果你需要,我为你搞定!于是我们可以轻松地写出如下代码

class A
{};

Good!它已经能够支持默认构造、拷贝构造、赋值操作了!编译器做了我们需要却没写下代码的事情。

而当我们为A添加一个A(int)这种构造函数时,事情发生了变化。写下以int为参数的构造函数后,我们应当认为:A将接受一个int为参数,生成一个A对象。但我们可能忘记了一点:A是否还需要默认构造?如果没写,C++认为:嗯,你写了A(int),我如果为你生成A(),那不是画蛇添足吗?所以,放弃合成A()。

这样有一个明显的好处,这可以杜绝掉A a带来的负面作用。如果A的设计者依然需要A(),因为C++没有合成,所以设计者发现A a编译不过,他理所当然地意识到错误:啊,我竟然忘了默认构造!OK,这个时候只需要写下A()=default,就可以继续使用默认构造函数了。

如果编译器这个时候擅自添加了A()会怎么样?A的设计者认为A既然接受了int为参数的构造函数,就不应该再凭空生成,即A a这种不应该是对的!但编译器如果合成的话,会造成A a这种形式是合法的,并且A的设计者无法意识到错误!造成A a这种错误的形式泛滥存在而无法及时纠正。

所以编译器不会为手动添加构造函数的类合成默认构造函数!Never!如果程序员认为需要,自己添加一个=default即可,非常显式地处理,成本也很低廉。

合成的拷贝构造函数

拷贝构造函数只有一种形式A(const A&),不像构造函数那样可以有不同的参数。当我们没有写拷贝构造函数却又需要拷贝构造函数时,它将被合成,要么是这样

//编译器处理后的A
class A
{
public:
A(const A&) { //...}
...//一些成员
};

这种情况,A的所有成员变量,都能够处理好拷贝构造操作。

要么是这样

//编译器处理后的A
class A
{
public:
A(const A&)=delete
...//一些成员
};

这种情况,A的成员变量里面有些成员无法处理好拷贝构造操作,所以A自身的拷贝构造将被合成为delete的。因为编译器认为,A的一些变量自己都不能拷贝,A拷贝的时候该如何处理这些成员?这是搞不定的事情,既然搞不定,但程序员自己又写下了A a2(a)这种代码,好嘛!我就把函数标记为delete,程序员你看见这个错误,就自己操作吧!

所以,当编译器认为程序员需要拷贝构造函数的时候,会合成默认构造函数,但如果它也不知道如何处理类的成员默认拷贝构造,它就把合成的默认构造函数标记为delete!撂摊子了,程序员你自己接手处理!

合成的移动构造函数

在没有为类提供拷贝构造函数、赋值操作符的前提下,如果发生了移动操作,编译器会合成默认的移动构造函数。对于以下代码

class A
{};
A a1;
A a2(std::move(a1));

经过编译器处理后,它变成下面这样

class A
{
public:
    A(){}
    A(A&&) {}
};
A a1;
A a2(std::move(a1));

核心思想还是:编译器认为你需要,如果它确实能搞定,那它就为你搞定!

但似乎有例外,一个程序员写出了如下代码

class A
{
public:
A()=default;
A(const& A) {}
};
A a1;
A a2(std::move(a1)); //ok

问题来了,A a2(std::move(a1))似乎使用了移动构造,编译器还会合成A(A&&)吗?

答案是:NO!编译器不会为A合成移动构造函数!

为什么会这样呢?也许我们认为,我既然调用了A a2(std::move(a1));说明我需要移动构造,编译器为什么这个时候不帮我合成了?

关键点在于类的设计代码,这里设计者手动添加了A(const A&){} 说明什么?说明A的内存构造很可能是特别的,所以设计者才会手动接管拷贝构造来处理内存问题!此时对于A的构造需要极为谨慎!这个时候编译器认为,你手动添加了拷贝构造,我就不再擅自为你合成移动构造函数。

这时候又有个问题:既然编译器不再合成移动构造,为什么不干脆将移动构造合成为delete?显式地警告程序员使用移动操作难道不是一种更好的办法吗?

我的理解是:移动是可选的,在不提供它的情况下,可以通过拷贝替代。所以,A a2(std::move(a1))实际上调用的是A的拷贝构造函数。所以,在能正常工作的前提下,编译器并没有强迫程序员必须提供移动构造!移动构造永远是一个可选项,不是一个必选项!

有拷贝构造而没移动,可以!那么有移动构造而没拷贝,可以吗?不行!!!请看如下代码

class A
{
public:
A()=default;
A(A&&){}
};
A a1;
A a2(a1); //error!!  尝试引用已删除的函数!
A a3(std::move(a1)); //ok

这个情况,A手动添加了移动构造,却没手动添加拷贝构造,编译器认为是不合理的,于是将合成的拷贝构造标记为delete,实际代码使用拷贝构造的时候,及时地收到了错误!

这个时候我们就必须发出疑问了:为啥?有拷贝构造没有移动就行,怎么有移动构造没拷贝就不行了?

原因其实也比较简单, 因为移动构造能完成的事情,其实拷贝构造都能完成(效率可能稍微差一些),而拷贝能完成的事情,移动却不能胜任!

所以,我需要拷贝,但你只有移动,移动不能干拷贝的事,所以报错是正常的,是吧?很合理!

这个时候又有个问题:移动确实干不了拷贝的事,那编译器合成一个拷贝构造不行了吗?干嘛把拷贝构造标记为delete?

答案也很简单,作为一个程序员,你为什么添加移动构造?肯定是类的内存有特殊处理,所以才需要移动构造!这个时候你这个类的设计是特别的,内存需要特殊处理,而合成的拷贝构造只能进行一些小打小闹(比如为成员变量执行拷贝构造),但它不知道该怎样处理类的定制内存策略,所以它躺平了,放弃了!直接标记为delete,程序员您自己玩吧,我搞不定!

编译器这样坦白地交待,总比硬着头皮合成一个隐患极大的拷贝构造函数要好得多!

编译器合成构造函数的时候干的这些偷偷摸摸的事情,潜规则比较多(啥时候干啥时候不干?满足某些条件就干,不满足就不干!哪些条件?OK,巴拉巴拉一大堆,背去吧:D)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值