指定构造函数时列表初始化的行为差异及相关疑问
class A { public: int x; A() { std::cout << "Constructor Called !" << std::endl; } A(int y):x(y) { std::cout << "Constructor Param Called !" << std::endl; } A(const A& copy) { std::cout << "Copy Constructor Called !" << std::endl; } }; class B { public: A value; //B(const A& val) : value(val){} }; int main(){ B b { A(22)}; }
问题
- 为何注释与不注释B的构造函数时输出不同?(我了解过aggregate class和aggregate initialization,是否与此相关?)
- aggregate initialization与direct initialization有何区别?
解答
问题1:输出差异的原因
确实和**聚合类(aggregate class)以及聚合初始化(aggregate initialization)**直接相关:
- 注释掉B的构造函数时,B符合聚合类的判定条件(无用户声明的构造函数、无私有/保护非静态成员、无虚函数、无基类等)。此时
B b { A(22)};属于聚合初始化——编译器会直接在B的成员value的内存位置,用A(22)的参数构造对象,相当于直接调用A的带参构造,而且编译器会优化掉原本可能出现的拷贝/移动操作(哪怕拷贝构造有打印副作用也不会执行),所以只输出Constructor Param Called !。 - 取消注释B的构造函数后,B不再是聚合类,此时
B b { A(22)};会触发你定义的B的构造函数:先创建临时对象A(22)(输出带参构造日志),再把这个临时对象传给B的构造函数参数,随后在B的初始化列表中value(val)是对value做拷贝初始化,因此会调用A的拷贝构造函数,最终输出两行日志。
问题2:聚合初始化与直接初始化的区别
- 适用场景不同:
- 聚合初始化仅针对聚合类型生效,包括数组、满足条件的类(无用户声明构造函数、无私有/保护非静态成员、无虚函数、无基类等)。
- 直接初始化是通用的对象初始化方式,所有能通过构造函数创建的类型都能使用。
- 初始化逻辑不同:
- 聚合初始化是逐个初始化对象的成员,不需要调用类的构造函数(针对聚合类而言),直接用初始化列表中的值对应初始化每个成员。比如对聚合类B,
B b {A(22)}就是直接构造b.value,而非先构造临时对象再拷贝。 - 直接初始化是通过调用目标类型的构造函数来创建对象,常见形式有
T obj(args);、T obj{args};(当不属于聚合初始化场景时),比如A a(22)就是直接调用A的带参构造函数初始化a。
- 聚合初始化是逐个初始化对象的成员,不需要调用类的构造函数(针对聚合类而言),直接用初始化列表中的值对应初始化每个成员。比如对聚合类B,
- 优化特性不同:
- 聚合初始化中,编译器可以直接跳过临时对象的拷贝/移动步骤,直接在成员的内存位置构造目标对象(如问题1中注释构造函数的情况)。
- 直接初始化若涉及临时对象传递,是否能优化取决于具体场景和编译器版本,像问题1中取消注释构造函数的情况,因为构造函数参数传递会触发拷贝,即使C++17有保证拷贝消除,但由于你的拷贝构造有显式副作用,编译器会执行该拷贝构造逻辑。
内容的提问来源于stack exchange,提问作者MellowPotato
相关产品推荐
相关产品推荐

