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

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

读取未初始化的值会导致未定义行为。C++ 的初始化规则复杂且微妙,理解它们是写出正确、高效代码的关键。

开篇引言

C++ 的初始化规则可能是所有主流编程语言中最复杂的。有些变量会被自动初始化,有些不会;构造函数体内的"赋值"和初始化列表中的"初始化"有着本质区别;不同编译单元中的全局对象初始化顺序更是充满陷阱。

Scott Meyers 在《Effective C++》第四条告诫我们:确定对象被使用前已先被初始化。 这不仅是正确性的要求,更是性能优化的起点。

C++ 的初始化迷局

内置类型的初始化差异

cpp 复制代码
void initialization_demo() {
    int x;              // 未初始化!值不确定(可能是任意垃圾值)
    int y = 10;         // 显式初始化
    int z(20);          // 直接初始化
    int w{30};          // 列表初始化(C++11,最推荐)
    
    // 数组
    int arr1[5];        // 元素未初始化
    int arr2[5] = {};   // 所有元素初始化为 0
    int arr3[5] = {1};  // 第一个为 1,其余为 0
}

关键规则:

场景 是否自动初始化 说明
全局/命名空间作用域 初始化为 0
局部内置类型 值不确定
类成员(内置类型) 取决于初始化列表 否则未定义
堆分配对象 new 不初始化内置类型
new int() 值初始化为 0

类类型的初始化

cpp 复制代码
class Widget {
public:
    Widget() { std::cout << "Widget constructed\n"; }
};

void class_init_demo() {
    Widget w1;          // 调用默认构造函数
    Widget w2 = Widget(); // 值初始化
    Widget w3{};        // 列表初始化(C++11)
    
    Widget* pw1 = new Widget;    // 默认构造函数
    Widget* pw2 = new Widget();  // 值初始化
    Widget* pw3 = new Widget{};  // 列表初始化
}

初始化 vs. 赋值:效率的本质差异

这是本条款最核心的知识点。很多开发者误以为构造函数体内的赋值就是初始化,实际上两者有着天壤之别。

错误的写法:构造函数内赋值

cpp 复制代码
class PhoneNumber {
public:
    PhoneNumber(const std::string& name, const std::string& number) {
        // 这些不是初始化,而是赋值!
        theName = name;      // 先默认构造,再赋值
        theNumber = number;  // 先默认构造,再赋值
    }
    
private:
    std::string theName;
    std::string theNumber;
};

实际执行流程:

  1. 进入构造函数体之前,所有成员已经通过默认构造函数初始化
  2. 构造函数体内的 =赋值操作 ,调用 operator=

对于 std::string,这意味着:

  • 先调用默认构造函数创建空字符串
  • 再调用拷贝赋值运算符赋入新值

正确的写法:成员初始化列表

cpp 复制代码
class PhoneNumber {
public:
    PhoneNumber(const std::string& name, const std::string& number)
        : theName(name),        // 直接拷贝构造
          theNumber(number)     // 直接拷贝构造
    {
        // 构造函数体可以为空
    }
    
private:
    std::string theName;
    std::string theNumber;
};

实际执行流程:

  1. 成员通过拷贝构造函数直接初始化
  2. 没有额外的默认构造和赋值操作

效率对比

cpp 复制代码
#include <iostream>
#include <string>

class EfficiencyDemo {
public:
    // 方法 A:赋值(低效)
    EfficiencyDemo(const std::string& s) {
        data = s;  // 默认构造 + 拷贝赋值 = 2 次操作
    }
    
    // 方法 B:初始化列表(高效)
    EfficiencyDemo(const std::string& s) : data(s) {
        // 拷贝构造 = 1 次操作
    }
    
private:
    std::string data;
};
方式 std::string 操作次数 性能
构造函数内赋值 默认构造 + 拷贝赋值 = 2 次 低效
成员初始化列表 拷贝构造 = 1 次 高效

对于复杂对象,这种差异可能更加显著。

必须使用初始化列表的情况

有些成员只能用初始化列表,无法在构造函数体内赋值:

cpp 复制代码
class MustInitList {
public:
    MustInitList(int val, int& ref) 
        : constMember(val),      // const 成员必须初始化
          refMember(ref),        // 引用成员必须初始化
          baseValue(val)         // 没有默认构造的基类/成员必须初始化
    {}
    
private:
    const int constMember;
    int& refMember;
    
    class Base {
    public:
        explicit Base(int x) : value(x) {}
    private:
        int value;
    };
    
    Base baseValue;
};

成员初始化顺序

初始化顺序规则

成员初始化顺序由它们在类中的声明顺序决定,与初始化列表中的顺序无关!

cpp 复制代码
class OrderDemo {
public:
    // 警告:初始化列表顺序与声明顺序不一致!
    OrderDemo(int val)
        : y(val),    // 先写 y,但...
          x(y)       // x 实际上先初始化!此时 y 还未初始化!
    {}
    
private:
    int x;  // x 先声明,先初始化
    int y;  // y 后声明,后初始化
};

// 正确写法:保持初始化列表与声明顺序一致
class OrderDemoCorrect {
public:
    OrderDemoCorrect(int val)
        : x(val),    // 与声明顺序一致
          y(val)
    {}
    
private:
    int x;
    int y;
};

一些编译器(如 GCC、Clang)会在初始化列表顺序与声明顺序不一致时发出警告。建议始终开启这类警告并视为错误处理。

基类与成员的初始化顺序

cpp 复制代码
class Base {
public:
    Base() { std::cout << "Base\n"; }
};

class Member {
public:
    Member() { std::cout << "Member\n"; }
};

class Derived : public Base {
public:
    Derived() : member(), baseExtra(0) {
        std::cout << "Derived\n";
    }
    
private:
    Member member;
    int baseExtra;
};

// 构造顺序:
// 1. Base(基类)
// 2. Member(成员)
// 3. baseExtra(成员)
// 4. Derived 构造函数体

完整初始化顺序:

  1. 基类构造函数(从最远的祖先开始)
  2. 成员变量构造函数(按声明顺序)
  3. 自身构造函数体

跨编译单元的全局对象初始化

静态初始化顺序问题(Static Initialization Order Fiasco)

这是 C++ 中最臭名昭著的陷阱之一:

cpp 复制代码
// file1.cpp
extern int globalB;
int globalA = globalB + 1;  // 如果 globalB 还没初始化?

// file2.cpp
extern int globalA;
int globalB = globalA + 1;  // 如果 globalA 还没初始化?

不同编译单元中的非局部静态对象初始化顺序是未定义的

解决方案:Singleton 模式(局部静态对象)

cpp 复制代码
// 推荐:使用局部静态对象替代全局对象
class FileSystem {
public:
    static FileSystem& instance() {
        static FileSystem fs;  // C++11 起线程安全
        return fs;
    }
    
    std::size_t numDisks() const { return 5; }
    
private:
    FileSystem() = default;
    ~FileSystem() = default;
    FileSystem(const FileSystem&) = delete;
    FileSystem& operator=(const FileSystem&) = delete;
};

// 使用
class Directory {
public:
    Directory() {
        // 安全:FileSystem 在首次使用时初始化
        std::size_t disks = FileSystem::instance().numDisks();
    }
};

C++11 起,局部静态对象的初始化是线程安全的(Meyers' Singleton)。

现代 C++ 的初始化改进

C++11 列表初始化

cpp 复制代码
class ModernInit {
public:
    ModernInit() 
        : x{0},              // 列表初始化
          y{3.14},           // 防止窄化转换
          name{"default"}
    {}
    
    ModernInit(int val)
        : x{val},
          y{static_cast<double>(val)},
          name{"initialized"}
    {}
    
private:
    int x;
    double y;
    std::string name;
};

// 列表初始化防止窄化
// ModernInit m{3.14};  // 编译错误!double 到 int 窄化

默认成员初始化器(C++11)

cpp 复制代码
class DefaultInit {
private:
    int x = 0;                    // 默认成员初始化器
    std::string name = "default"; // 如果初始化列表未提供,使用此值
    std::vector<int> data{1, 2, 3}; // 列表初始化
    
public:
    DefaultInit() = default;  // x=0, name="default", data={1,2,3}
    
    DefaultInit(int val)
        : x(val)              // x=val, name="default", data={1,2,3}
    {}
    
    DefaultInit(int val, const std::string& s)
        : x(val), name(s)     // x=val, name=s, data={1,2,3}
    {}
};

委托构造函数(C++11)

cpp 复制代码
class DelegatingCtor {
public:
    // 主构造函数
    DelegatingCtor(int x, int y, const std::string& label)
        : x_(x), y_(y), label_(label)
    {}
    
    // 委托构造函数
    DelegatingCtor() 
        : DelegatingCtor(0, 0, "origin")  // 委托给主构造函数
    {}
    
    DelegatingCtor(int x, int y)
        : DelegatingCtor(x, y, "point")   // 委托给主构造函数
    {}
    
private:
    int x_;
    int y_;
    std::string label_;
};

实际应用场景

场景 1:资源管理类(RAII)

cpp 复制代码
#include <fstream>
#include <string>

class FileHandler {
public:
    explicit FileHandler(const std::string& path)
        : file_(path),           // 直接初始化文件流
          isOpen_(false),
          bytesRead_(0)
    {
        isOpen_ = file_.is_open();  // 构造函数体内做状态检查
    }
    
    ~FileHandler() {
        if (file_.is_open()) {
            file_.close();
        }
    }
    
    bool isOpen() const { return isOpen_; }
    
private:
    std::ifstream file_;
    bool isOpen_;
    std::size_t bytesRead_;
};

场景 2:继承体系中的初始化

cpp 复制代码
class Shape {
public:
    explicit Shape(const std::string& color) : color_(color) {}
    virtual ~Shape() = default;
    
private:
    std::string color_;
};

class Circle : public Shape {
public:
    Circle(const std::string& color, double radius)
        : Shape(color),        // 先初始化基类
          radius_(radius),     // 再初始化成员
          area_(3.14159 * radius * radius)
    {}
    
    double area() const { return area_; }
    
private:
    double radius_;
    double area_;
};

场景 3:PIMPL 惯用法中的初始化

cpp 复制代码
// widget.h
class Widget {
public:
    Widget();
    ~Widget();
    Widget(Widget&& rhs) noexcept;
    Widget& operator=(Widget&& rhs) noexcept;
    
    // ...
    
private:
    class Impl;           // 前置声明
    std::unique_ptr<Impl> pImpl;  // 智能指针管理实现
};

// widget.cpp
class Widget::Impl {
public:
    Impl() : data(0), name("default") {}
    
    int data;
    std::string name;
    std::vector<double> values;
};

Widget::Widget() 
    : pImpl(std::make_unique<Impl>())  // 在实现文件中初始化
{}

Widget::~Widget() = default;  // 必须在 .cpp 中定义,因为 Impl 在此处完整

总结与建议

核心要点

  1. 为内置型对象进行手工初始化,因为 C++ 不保证初始化它们。

  2. 构造函数最好使用成员初始化列表,而不要在构造函数本体内使用赋值操作。初始化列表列出的成员变量,其排列顺序应该和它们在 class 中的声明次序相同。

  3. 为免除"跨编译单元之初始化次序"问题,请以局部静态对象替换非局部静态对象。

初始化最佳实践 checklist

场景 建议
内置类型局部变量 始终显式初始化
类成员 优先使用初始化列表
const / 引用成员 必须使用初始化列表
无默认构造的成员 必须使用初始化列表
默认成员初始化 使用 C++11 默认成员初始化器
全局静态对象 使用局部静态对象替代
初始化列表顺序 与类内声明顺序保持一致

经典名言

确定对象被使用前已先被初始化。

这条守则看似简单,实则蕴含了 C++ 对象模型的核心知识。掌握初始化与赋值的区别,理解初始化顺序规则,是写出高效、正确 C++ 代码的必经之路。


参考阅读:

  • 《Effective C++》Scott Meyers,条款 04
  • 《C++ Primer》关于构造函数和初始化的章节
  • 《Effective Modern C++》关于列表初始化的条款

系列预告: 下一篇将深入解析条款 05------了解 C++ 默默编写并调用哪些函数,揭开编译器自动生成成员函数的神秘面纱。


如果本文对你有帮助,欢迎点赞、收藏、转发!有任何问题可以在评论区留言讨论。

相关推荐
云栖梦泽1 小时前
玩转RK3506SDK
linux·嵌入式硬件
极客先躯1 小时前
高级java每日一道面试题-2026年02月01日-实战篇[Docker]-Docker Volume 的生命周期管理是怎样的?
java·运维·docker·容器·持久化·架构图·容器卷
不想写代码的星星1 小时前
std::move 根本不移动,就像老婆饼里没有老婆
c++
NE_STOP1 小时前
Raft算法处理细节
java
redaijufeng1 小时前
C++雾中风景7:闭包
c++·算法·风景
Java面试题总结1 小时前
Linux-Ubantu-贴士-apt的地盘
linux·运维·服务器
小小龙学IT1 小时前
Go 语言后端开发:从并发模型到生产落地的工程实践
开发语言·后端·golang
努力攻坚操作系统1 小时前
编程语言编译运行机制对比:C / Java / Python
java·c语言·python
慧一居士1 小时前
对比两个文件内容是否完全一致,java实现示例
java