
文章目录引言一、初始化 ≠ 赋值1.1 构造函数体内的 是赋值不是初始化1.2 初始化顺序是声明顺序不是初始化列表顺序1.3 必须用初始化列表的三种情况二、非局部静态对象的初始化顺序问题2.1 问题——跨编译单元的初始化顺序未定义2.2 解决方案——函数内 staticMeyers Singleton三、用 const 成员函数让编译器帮你查错3.1 const 成员函数 这个操作不修改对象3.2 const 成员函数可以重载非 const 版本3.3 mutable——const 方法里可以修改的例外四、成员初始化列表的完整示例总结本系列为《C深度修炼基础、STL源码与多线程实战》第34篇对应 Effective C 条款3-4前置条件理解 const第8篇、引用第9篇、构造与析构第3篇引言C 语言的变量初始化是手动的——你写int x;然后 x 里面是垃圾值你必须记得初始化它。C 的哲学是自动化的——让编译器帮你确保所有对象在被使用前已经初始化。// C 语言——易出错intmain(){intx;// x 是垃圾值intarr[10];// arr 是垃圾值// ... 你必须记得在使用前初始化x42;}// C——可以用构造函数、成员初始化列表保证初始化classWidget{intx_;// 声明public:Widget():x_(0){}// 在进入构造函数体之前x_ 已经被初始化为 0};本文讲两件事对象在使用前一定被初始化了条款 4以及const 成员函数如何让编译器帮你捕获意外修改条款 3。一、初始化 ≠ 赋值1.1 构造函数体内的是赋值不是初始化#includestring#includeiostreamclassPerson{std::string name_;intage_;public:// ❌ 这是赋值——不是初始化Person(conststd::stringname,intage){name_name;// name_ 在这里被赋值之前已经被默认构造为空字符串了age_age;// age_ 在这里被赋值之前是垃圾值——int 不会自动初始化}// ✅ 这是初始化——使用成员初始化列表Person(conststd::stringname,intage):name_(name),age_(age){}// name_ 直接拷贝构造age_ 直接初始化};区别赋值版本先调用name_的默认构造函数创建空字符串然后在函数体内调用赋值运算符。两步操作。初始化列表版本直接调用name_的拷贝构造函数。一步操作。对std::string这种复杂类型成员初始化列表可以减少一次默认构造 一次赋值。对int这种内置类型代价差不多——但一致性要求你一律用初始化列表。1.2 初始化顺序是声明顺序不是初始化列表顺序classWidget{inta_;intb_;public:// ⚠️ 陷阱a_ 在声明中排在 b_ 之前所以 a_ 先初始化——无论初始化列表怎么写Widget(intval):b_(val),a_(b_){// a_ 先于 b_ 初始化a_(b_) 中的 b_ 还没有初始化——是垃圾值}};规则成员初始化的顺序是它们在类定义中声明的顺序。初始化列表中的书写顺序不影响初始化顺序。为避免困惑——初始化列表的顺序应该和声明顺序一致。1.3 必须用初始化列表的三种情况classMustInit{constintCI_;// ① const 成员——必须在初始化列表中初始化intRI_;// ② 引用成员——必须在初始化列表中初始化// ③ 基类没有默认构造函数时——派生类必须在初始化列表中调用基类构造函数public:MustInit(intri,intci):CI_(ci),RI_(ri){}// 初始化列表是唯一选择};二、非局部静态对象的初始化顺序问题2.1 问题——跨编译单元的初始化顺序未定义// file1.cpp classLogger{public:Logger(){std::coutLogger init\n;}voidlog(conststd::stringmsg){std::coutmsg\n;}};externLogger globalLogger;// 声明——定义在别处Logger globalLogger;// 定义——在 file1.cpp 中初始化// file2.cpp externLogger globalLogger;// 来自 file1.cppclassApp{public:App(){globalLogger.log(App started);// ⚠️ globalLogger 初始化了吗不确定}};App globalApp;// file2.cpp 中的全局对象——可能在 globalLogger 之前初始化如果globalApp在globalLogger之前构造调用globalLogger.log()就是未定义行为——globalLogger还没构造。这就是臭名昭著的**“静态初始化顺序灾难”Static Initialization Order Fiasco**。2.2 解决方案——函数内 staticMeyers Singleton// logger.h classLogger{public:voidlog(conststd::stringmsg){std::coutmsg\n;}};// 用函数包装——确保在第一次调用时才初始化LoggergetLogger(){staticLogger instance;// C11 保证多线程安全只初始化一次returninstance;}// app.cpp classApp{public:App(){getLogger().log(App started);// ✅ 第一次调用 getLogger() 时才初始化 Logger}};AppgetApp(){staticApp instance;returninstance;}intmain(){getApp();// getLogger() 在 getApp() 的构造函数中被首次调用——Logger 在此时初始化}核心技巧将非局部静态对象替换为函数内的局部静态对象。局部静态对象在函数第一次被调用时才初始化——初始化顺序由调用顺序决定不再是随机的。三、用const成员函数让编译器帮你查错3.1 const 成员函数 “这个操作不修改对象”classTextBlock{std::string text_;public:TextBlock(conststd::strings):text_(s){}// const 成员函数——承诺不修改对象constcharoperator[](size_t pos)const{returntext_[pos];}// 非 const 版本——允许修改charoperator[](size_t pos){returntext_[pos];}};intmain(){TextBlocktb(hello);charctb[0];// 调用非 const 版本——返回 char可以修改cH;// ✅ tb 被修改constTextBlockctb(world);// ctb[0] W; // ❌ 编译错误——const 版本返回 const char不能赋值charchctb[0];// ✅ 只读——调用 const 版本}3.2 const 成员函数可以重载非 const 版本classTextBlock{std::string text_;public:constcharoperator[](size_t pos)const{std::coutconst [] called\n;returntext_[pos];}charoperator[](size_t pos){std::coutnon-const [] called\n;// 复用 const 版本避免代码重复returnconst_castchar(static_castconstTextBlock(*this)[pos]);}};消除 const/non-const 代码重复的技巧让非 const 版本调用 const 版本——先把*this转为const TextBlock来强制调用 const 版本然后把返回的const char转为char。这样逻辑只写在 const 版本中——单点维护。千万不要反过来——让 const 版本调用非 const 版本——那是在承诺不修改的方法里调用可能修改的方法逻辑上和语法上都危险。3.3mutable——const 方法里可以修改的例外classCache{std::string data_;mutablesize_t accessCount_0;// mutable——即使在 const 方法中也能修改mutableboolvalid_false;public:size_tgetAccessCount()const{accessCount_;// ✅ mutable 允许在 const 方法中修改returnaccessCount_;}conststd::stringdata()const{if(!valid_){// 在 const 方法中计算缓存——因为标记了 mutablevalid_true;}returndata_;}};mutable适用于从逻辑上看不改变对象但从实现上需要改变某些内部状态的场景——缓存、统计计数器、互斥锁。四、成员初始化列表的完整示例#includestring#includeiostream#includevectorclassStudent{constintid_;// ① const 成员std::stringnameRef_;// ② 引用成员std::vectorintscores_;// ③ 复杂类型成员public:// 全部用初始化列表——一步构造到位Student(intid,std::stringname,std::initializer_listintscores):id_(id),nameRef_(name),scores_(scores){}// const 成员函数——不修改对象状态doubleaverage()const{if(scores_.empty())return0.0;intsum0;for(autos:scores_)sums;returnstatic_castdouble(sum)/scores_.size();}intid()const{returnid_;}voidprint()const{std::coutStudent #id_ (nameRef_) avgaverage()\n;}};intmain(){std::string nameAlice;Students(1001,name,{85,92,78,95});s.print();// Student #1001 (Alice) avg87.5}总结条目 3-4 教你养成两个从根本上减少 bug 的习惯初始化列表比函数体内赋值更高效——成员变量在进入函数体之前已经构造好了函数体内的是赋值而非初始化——对复杂类型多了一次默认构造初始化顺序是声明顺序不是初始化列表顺序——保持两者一致以避免用一个未初始化的成员去初始化另一个成员的隐蔽 bug用函数内 static解决跨翻译单元的初始化顺序问题——让对象的初始化时机由第一次调用自然决定而不是被链接器随机决定const 成员函数让编译器帮你检查不会修改对象这一承诺——对 const 对象只能调用 const 成员函数const 和 non-const 可以构成重载消除 const/non-const 重复的技巧——让非 const 版本调用 const 版本反之不行用static_cast和const_cast完成类型转换mutable是 const 的合法例外——用于缓存、计数器和锁等逻辑上 const 但实现上需要修改的场景动手练习写一个类在构造函数体内用赋值初始化成员——然后用初始化列表重写——对于std::string类型的成员思考每一步发生了什么故意把初始化列表的顺序写成和声明顺序不同——然后让一个成员的初始化依赖另一个成员——观察垃圾值的出现实现跨文件的全局对象初始化问题——写两个 .cpp 文件各自定义一个全局对象一个依赖另一个——观察程序崩溃为你的类写 const 和 non-const 两个版本的operator[]——让 non-const 版本调用 const 版本——消除代码重复给一个类添加mutable缓存字段——在 const 方法中更新缓存——验证调用方的 const 对象确实可以通过 const 方法修改 mutable 字段