跳转到内容
新建笔记

C++ 构造与成员初始化:顺序、引用和 C2533 排错

进入构造函数体之前,基类和非静态数据成员已经按规则完成初始化。函数体中的赋值是在已有对象上操作,不能补做一个没有成功发生的构造,也不能重新绑定引用。

本篇中的“初始化列表”指构造函数冒号后的 成员初始化列表,不是 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 官方说明

检查时先处理编译器报告的最早语法错误,再看构造函数定义及其前一段类声明。不同编译器的诊断文本不同,不应仅依赖某个错误编号判断根因。