进入构造函数体之前,基类和非静态数据成员已经按规则完成初始化。函数体中的赋值是在已有对象上操作,不能补做一个没有成功发生的构造,也不能重新绑定引用。
本篇中的“初始化列表”指构造函数冒号后的 成员初始化列表,不是 std::initializer_list。示例使用 C++17。
哪些情况必须先提供初始化方式
跳转到“哪些情况必须先提供初始化方式”| 场景 | 为什么函数体赋值不够 | 可用的初始化路径 |
|---|---|---|
const int value | 构造完成后不能再赋值给它 | 成员初始化列表,或类内默认成员初值 |
int& ref | 引用创建时必须绑定,之后赋值会修改被引用对象 | 成员初始化列表,或有效的默认成员初值 |
| 没有可用默认构造的基类 | 进入派生构造函数体前必须先构造基类 | 初始化列表中给基类传参,或通过委托构造复用相应构造过程 |
| 没有可用默认构造的类类型成员 | 执行函数体前不能留下一个尚未构造的成员 | 初始化列表,或调用正确构造方式的默认成员初值 |
原笔记把这些情况概括成“一律必须写在初始化列表中”,范围过宽。C++11 起的默认成员初值可以覆盖不少成员场景;如果类类型本身有可用默认构造,也不必因为它另有一个带参数构造函数,就认定省略初始化一定报错。基类与成员的初始化
例如,member = MemberClass(x); 只可能在 member 已经成功构造之后执行。如果 MemberClass 根本没有可用的无参构造路径,这条赋值就没有机会“补救”。
完整程序:四种成员需求与默认初值
跳转到“完整程序:四种成员需求与默认初值”#include <cassert>#include <string>#include <type_traits>#include <vector>
std::vector<std::string> construction;
class Base {public: explicit Base(int value) : data_(value) { construction.push_back("base"); } int baseData() const { return data_; }private: int data_;};
class MemberClass {public: explicit MemberClass(int value) : data_(value) { construction.push_back("member"); } int data() const { return data_; }private: int data_;};
class MyClass : public Base {public: const int value; int& ref; MemberClass member;
MyClass(int initial, int& external) : Base(initial), value(initial), ref(external), member(initial) { construction.push_back("body"); }};
struct Defaults { int anchor = 7; int& ref = anchor; const int limit = 10; MemberClass member{11};};
struct OverrideDefault { const int value = 10; explicit OverrideDefault(int input) : value(input) {}};
class Ordered {public: explicit Ordered(int input) : first_(input), doubled_(first_ * 2) {} int doubled() const { return doubled_; }private: int first_; int doubled_;};
int main() { static_assert(!std::is_default_constructible_v<Base>); static_assert(!std::is_default_constructible_v<MemberClass>); static_assert(std::is_default_constructible_v<Defaults>);
int original = 5; int other = 9; construction.clear(); MyClass object{42, original}; assert((construction == std::vector<std::string>{"base", "member", "body"})); assert(object.baseData() == 42 && object.value == 42 && object.member.data() == 42); object.ref = other; assert(original == 9 && &object.ref == &original); other = 12; assert(object.ref == 9); // 之前是赋值,没有改绑到 other
Defaults defaults; defaults.ref = 8; assert(defaults.anchor == 8 && defaults.limit == 10 && defaults.member.data() == 11); assert(OverrideDefault{30}.value == 30); assert(Ordered{6}.doubled() == 12);}同一构造函数如果显式初始化了某个成员,就不再使用该成员的类内默认初值,所以 OverrideDefault{30}.value 是 30。
Defaults::ref 绑定到本对象的 anchor,并不意味着自动复制时会重新绑定到新对象的 anchor。引用成员会使复制、赋值语义变得特殊;拥有内部引用的类需要另行设计复制策略。本例没有复制 Defaults。相关条件见 特殊成员函数。
初始化顺序由声明决定
跳转到“初始化顺序由声明决定”非委托构造中,先由最派生类初始化虚基类,再按基类声明顺序初始化直接基类,然后按成员声明顺序初始化成员,最后执行函数体。调换冒号列表中的书写顺序不会改变这些规则。
因此应按声明顺序书写初始化项;若第二个成员依赖第一个成员,就像 Ordered 那样把被依赖者先声明。不要读取尚未初始化的成员,也不要通过调整列表外观掩盖实际依赖。
引用初值还必须指向寿命足够长的对象。构造成功不保证外部引用以后始终有效;把短命局部对象的引用保存在长期对象里,仍会悬垂。
构造函数不能写返回类型:C2533 排错
跳转到“构造函数不能写返回类型:C2533 排错”构造函数声明写作 MyClass(...);,类外定义写作 MyClass::MyClass(...) { ... },前面不能加 int 或 void。类定义自身则必须以 }; 结束。
原短笔记记录的是:前一个类定义右花括号后漏了分号,随后构造函数定义被错误解析,MSVC 报 C2533。这个排查方向有实际价值,但不是唯一原因;也要检查构造函数是否真的被写上了返回类型。MSVC C2533 官方说明
检查时先处理编译器报告的最早语法错误,再看构造函数定义及其前一段类声明。不同编译器的诊断文本不同,不应仅依赖某个错误编号判断根因。