新闻详情

新闻详情

首页 / 资讯中心 / 详情

Effective C++ 条款04:确定对象被使用前已先被初始化

发布时间:2026/6/9 1:31:12
Effective C++ 条款04:确定对象被使用前已先被初始化
Effective C 条款04确定对象被使用前已先被初始化读取未初始化的值会导致未定义行为。C 的初始化规则复杂且微妙理解它们是写出正确、高效代码的关键。开篇引言C 的初始化规则可能是所有主流编程语言中最复杂的。有些变量会被自动初始化有些不会构造函数体内的赋值和初始化列表中的初始化有着本质区别不同编译单元中的全局对象初始化顺序更是充满陷阱。Scott Meyers 在《Effective C》第四条告诫我们确定对象被使用前已先被初始化。这不仅是正确性的要求更是性能优化的起点。C 的初始化迷局内置类型的初始化差异voidinitialization_demo(){intx;// 未初始化值不确定可能是任意垃圾值inty10;// 显式初始化intz(20);// 直接初始化intw{30};// 列表初始化C11最推荐// 数组intarr1[5];// 元素未初始化intarr2[5]{};// 所有元素初始化为 0intarr3[5]{1};// 第一个为 1其余为 0}关键规则场景是否自动初始化说明全局/命名空间作用域是初始化为 0局部内置类型否值不确定类成员内置类型取决于初始化列表否则未定义堆分配对象否new不初始化内置类型new int()是值初始化为 0类类型的初始化classWidget{public:Widget(){std::coutWidget constructed\n;}};voidclass_init_demo(){Widget w1;// 调用默认构造函数Widget w2Widget();// 值初始化Widget w3{};// 列表初始化C11Widget*pw1newWidget;// 默认构造函数Widget*pw2newWidget();// 值初始化Widget*pw3newWidget{};// 列表初始化}初始化 vs. 赋值效率的本质差异这是本条款最核心的知识点。很多开发者误以为构造函数体内的赋值就是初始化实际上两者有着天壤之别。错误的写法构造函数内赋值classPhoneNumber{public:PhoneNumber(conststd::stringname,conststd::stringnumber){// 这些不是初始化而是赋值theNamename;// 先默认构造再赋值theNumbernumber;// 先默认构造再赋值}private:std::string theName;std::string theNumber;};实际执行流程进入构造函数体之前所有成员已经通过默认构造函数初始化构造函数体内的是赋值操作调用operator对于std::string这意味着先调用默认构造函数创建空字符串再调用拷贝赋值运算符赋入新值正确的写法成员初始化列表classPhoneNumber{public:PhoneNumber(conststd::stringname,conststd::stringnumber):theName(name),// 直接拷贝构造theNumber(number)// 直接拷贝构造{// 构造函数体可以为空}private:std::string theName;std::string theNumber;};实际执行流程成员通过拷贝构造函数直接初始化没有额外的默认构造和赋值操作效率对比#includeiostream#includestringclassEfficiencyDemo{public:// 方法 A赋值低效EfficiencyDemo(conststd::strings){datas;// 默认构造 拷贝赋值 2 次操作}// 方法 B初始化列表高效EfficiencyDemo(conststd::strings):data(s){// 拷贝构造 1 次操作}private:std::string data;};方式std::string操作次数性能构造函数内赋值默认构造 拷贝赋值 2 次低效成员初始化列表拷贝构造 1 次高效对于复杂对象这种差异可能更加显著。必须使用初始化列表的情况有些成员只能用初始化列表无法在构造函数体内赋值classMustInitList{public:MustInitList(intval,intref):constMember(val),// const 成员必须初始化refMember(ref),// 引用成员必须初始化baseValue(val)// 没有默认构造的基类/成员必须初始化{}private:constintconstMember;intrefMember;classBase{public:explicitBase(intx):value(x){}private:intvalue;};Base baseValue;};成员初始化顺序初始化顺序规则成员初始化顺序由它们在类中的声明顺序决定与初始化列表中的顺序无关classOrderDemo{public:// 警告初始化列表顺序与声明顺序不一致OrderDemo(intval):y(val),// 先写 y但...x(y)// x 实际上先初始化此时 y 还未初始化{}private:intx;// x 先声明先初始化inty;// y 后声明后初始化};// 正确写法保持初始化列表与声明顺序一致classOrderDemoCorrect{public:OrderDemoCorrect(intval):x(val),// 与声明顺序一致y(val){}private:intx;inty;};一些编译器如 GCC、Clang会在初始化列表顺序与声明顺序不一致时发出警告。建议始终开启这类警告并视为错误处理。基类与成员的初始化顺序classBase{public:Base(){std::coutBase\n;}};classMember{public:Member(){std::coutMember\n;}};classDerived:publicBase{public:Derived():member(),baseExtra(0){std::coutDerived\n;}private:Member member;intbaseExtra;};// 构造顺序// 1. Base基类// 2. Member成员// 3. baseExtra成员// 4. Derived 构造函数体完整初始化顺序基类构造函数从最远的祖先开始成员变量构造函数按声明顺序自身构造函数体跨编译单元的全局对象初始化静态初始化顺序问题Static Initialization Order Fiasco这是 C 中最臭名昭著的陷阱之一// file1.cppexternintglobalB;intglobalAglobalB1;// 如果 globalB 还没初始化// file2.cppexternintglobalA;intglobalBglobalA1;// 如果 globalA 还没初始化不同编译单元中的非局部静态对象初始化顺序是未定义的解决方案Singleton 模式局部静态对象// 推荐使用局部静态对象替代全局对象classFileSystem{public:staticFileSysteminstance(){staticFileSystem fs;// C11 起线程安全returnfs;}std::size_tnumDisks()const{return5;}private:FileSystem()default;~FileSystem()default;FileSystem(constFileSystem)delete;FileSystemoperator(constFileSystem)delete;};// 使用classDirectory{public:Directory(){// 安全FileSystem 在首次使用时初始化std::size_t disksFileSystem::instance().numDisks();}};C11 起局部静态对象的初始化是线程安全的Meyers’ Singleton。现代 C 的初始化改进C11 列表初始化classModernInit{public:ModernInit():x{0},// 列表初始化y{3.14},// 防止窄化转换name{default}{}ModernInit(intval):x{val},y{static_castdouble(val)},name{initialized}{}private:intx;doubley;std::string name;};// 列表初始化防止窄化// ModernInit m{3.14}; // 编译错误double 到 int 窄化默认成员初始化器C11classDefaultInit{private:intx0;// 默认成员初始化器std::string namedefault;// 如果初始化列表未提供使用此值std::vectorintdata{1,2,3};// 列表初始化public:DefaultInit()default;// x0, namedefault, data{1,2,3}DefaultInit(intval):x(val)// xval, namedefault, data{1,2,3}{}DefaultInit(intval,conststd::strings):x(val),name(s)// xval, names, data{1,2,3}{}};委托构造函数C11classDelegatingCtor{public:// 主构造函数DelegatingCtor(intx,inty,conststd::stringlabel):x_(x),y_(y),label_(label){}// 委托构造函数DelegatingCtor():DelegatingCtor(0,0,origin)// 委托给主构造函数{}DelegatingCtor(intx,inty):DelegatingCtor(x,y,point)// 委托给主构造函数{}private:intx_;inty_;std::string label_;};实际应用场景场景 1资源管理类RAII#includefstream#includestringclassFileHandler{public:explicitFileHandler(conststd::stringpath):file_(path),// 直接初始化文件流isOpen_(false),bytesRead_(0){isOpen_file_.is_open();// 构造函数体内做状态检查}~FileHandler(){if(file_.is_open()){file_.close();}}boolisOpen()const{returnisOpen_;}private:std::ifstream file_;boolisOpen_;std::size_t bytesRead_;};场景 2继承体系中的初始化classShape{public:explicitShape(conststd::stringcolor):color_(color){}virtual~Shape()default;private:std::string color_;};classCircle:publicShape{public:Circle(conststd::stringcolor,doubleradius):Shape(color),// 先初始化基类radius_(radius),// 再初始化成员area_(3.14159*radius*radius){}doublearea()const{returnarea_;}private:doubleradius_;doublearea_;};场景 3PIMPL 惯用法中的初始化// widget.hclassWidget{public:Widget();~Widget();Widget(Widgetrhs)noexcept;Widgetoperator(Widgetrhs)noexcept;// ...private:classImpl;// 前置声明std::unique_ptrImplpImpl;// 智能指针管理实现};// widget.cppclassWidget::Impl{public:Impl():data(0),name(default){}intdata;std::string name;std::vectordoublevalues;};Widget::Widget():pImpl(std::make_uniqueImpl())// 在实现文件中初始化{}Widget::~Widget()default;// 必须在 .cpp 中定义因为 Impl 在此处完整总结与建议核心要点为内置型对象进行手工初始化因为 C 不保证初始化它们。构造函数最好使用成员初始化列表而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量其排列顺序应该和它们在 class 中的声明次序相同。为免除跨编译单元之初始化次序问题请以局部静态对象替换非局部静态对象。初始化最佳实践 checklist场景建议内置类型局部变量始终显式初始化类成员优先使用初始化列表const / 引用成员必须使用初始化列表无默认构造的成员必须使用初始化列表默认成员初始化使用 C11 默认成员初始化器全局静态对象使用局部静态对象替代初始化列表顺序与类内声明顺序保持一致经典名言确定对象被使用前已先被初始化。这条守则看似简单实则蕴含了 C 对象模型的核心知识。掌握初始化与赋值的区别理解初始化顺序规则是写出高效、正确 C 代码的必经之路。参考阅读《Effective C》Scott Meyers条款 04《C Primer》关于构造函数和初始化的章节《Effective Modern C》关于列表初始化的条款系列预告下一篇将深入解析条款 05——了解 C 默默编写并调用哪些函数揭开编译器自动生成成员函数的神秘面纱。如果本文对你有帮助欢迎点赞、收藏、转发有任何问题可以在评论区留言讨论。
网站建设 高端定制 企业官网