C++ 各种初始化方法总结

在各种编程语言中,初始化都是非常重要的步骤,用于确保对象在使用前具有确定的初始状态。C++ 提供了多种初始化方法,每种方法都有其特定的使用场景和注意事项。

以下是一些主要的初始化方法及其注意事项:

  1. 默认初始化(Default-initialization)
    • 形如T objnew T等方式的初始化,其中T为类型名称、obj为对象名称,T也可以是数组类型。
    • 对于具有自动存储期和动态存储期的对象,如局部变量、用new分配的结构体等:
      • 对于基本类型的对象,默认初始化仅分配对象空间,对象的值是不确定的。
      • 对于类对象,默认初始化会调用其默认构造函数,如果默认构造函数没有显式定义或由=default定义,成员的值也是不确定的。
    • 对于具有静态或线程存储期的对象,如全局对象、用staticthread_local关键字限定的对象:
      • 对于基本类型的对象,会将对象的值初始化为 0。
      • 对于类对象,会调用其默认构造函数,如果默认构造函数没有显式定义或由=default定义,成员的值也被初始化为 0。
      • 没有调用显式默认构造函数的初始化属于“零初始化”,后文介绍。
    • 如果对象的初始值是不确定的,而且程序用到了这种不确定的值,不算正确初始化,会导致程序不可预测的行为,造成严重错误。
      复制代码
      int i;  // 全局对象,初始值为 0
      
      struct A {
          int x;
      };
      int fun1() {
          A a;         // 默认初始化,成员 x 的值是不确定的
          return a.x;  // 未定义的行为
      }
      A* fun2() {
          return new A[5];  // 默认初始化数组,成员 x 的值都是不确定的
      }
      struct B {
          int x;
          B(): x(1) {}  // 默认构造函数
      };
      int fun3() {
          B b;         // 默认初始化,调用默认构造函数
          return b.x;  // OK
      }
      B* fun4() {
          return new B;  // 默认初始化,调用默认构造函数,成员 x 的值为 1
      }
      复制代码
    • 关于对象的存储周期(即生命周期),请参见“storage durations”。
  2. 直接初始化(Direct-initialization)
    • 对于内置基本类型的对象,直接初始化相当于直接赋值。
    • 对于类对象,直接初始化会调用相应的构造函数进行初始化。
      复制代码
      int i = 1;  // 直接初始化
      int j(0);   // 直接初始化
      
      struct A {
          int x, y;
          A(int x, int y): x(x), y(y) {}
      };
      A a(1, 2);           // 直接初始化
      A* p = new A(1, 2);  // 直接初始化
      复制代码
  3. 拷贝初始化(Copy-initialization)
    • 对于内置基本类型的对象,拷贝初始化和直接初始化几乎没有区别。
    • 对于类对象,拷贝初始化调用拷贝构造函数将已存在的对象复制成新对象。
      复制代码
      int i = 1;  // 直接初始化
      int j = i;  // 拷贝初始化
      
      struct T {
          int x;
          T(int i = 0): x(i) {}     // 默认构造函数
          T(const T& a): x(a.x) {}  // 拷贝构造函数
      };
      T a;             // 默认初始化
      T b(a);          // 拷贝初始化
      T c = a;         // 拷贝初始化
      T* p = new T(a); // 拷贝初始化
      复制代码
    • 应注意浅拷贝问题,即仅复制对象的成员变量值(如指针),而不复制其指向的数据,导致多个对象共享同一份数据,一个对象修改了数据会影响其他对象,也可能造成资源被重复释放。为避免这种问题,应显式定义拷贝构造函数和拷贝赋值操作符,实现深拷贝。
    • 按值传递的参数对象、按值返回的对象、按值抛出的异常、按值捕获的异常均为拷贝初始化。
      复制代码
      int fun1(int x) {
          return x + 1;  // 拷贝初始化,返回值是 x + 1 的副本
      }
      
      void fun2() {
          fun1(0);  // 用 0 拷贝初始化参数 x
      }
      
      void fun3() {
          std::exception e;
          throw e;  // 拷贝初始化,抛出的对象是 e 的副本
      }
      
      void fun4() {
          try { fun3(); }
          catch (std::exception e)  // 拷贝初始化,但使用引用捕获异常更合理
          {}
      }
      复制代码
    • 从 C++11 开始,通过移动构造函数初始化对象也被视为一种特殊的拷贝初始化,尽管实际上并不涉及拷贝,而是资源的转移。
      std::string s1("abc");
      std::string s2(std::move(s1));  // 将 s1 的数据移动到 s2 中

      例中 s1 的数据被转移到 s2 中,s2 与原来的 s1 相同,而 s1 不再持有有效数据。

    • 在某些情况下,C++ 标准还允许省略拷贝或移动操作( copy/move elision),以减少不必要的对象拷贝或移动,进一步提高性能。
  4. 聚合初始化(Aggregate-initialization)
    • ={}初始化数组、结构体、联合体等聚合类型的对象。
    • 可被聚合初始化的对象要求:所有非静态数据成员都是公有的,没有定义用户提供的构造函数,没有定义私有或受保护的非静态数据成员,没有基类,也没有虚函数。
    • 聚合初始化是为了与 C 语言兼容而提出的,C++11 后应使用更完善的列表初始化。
      int a[] = {1, 2, 3}; // 聚合初始化数组
      
      struct Point {
          int x, y;
      };
      Point p = {0, 1};   // 聚合初始化结构体
      Point q[3] = {{1, 2}, {3, 4}, {5, 6}}; // 聚合初始化结构体数组
  5. 列表初始化(List-initialization)
    • 使用花括号{}进行初始化,包含聚合初始化。
    • 由 C++11 引入,又称为万能初始化(uniform initialization),建议使用列表初始化代替其他初始化方法。
    • 列表初始化会进行更严格的类型检查,如果类型转换会造成数据丢失等错误,则不会通过编译。
      void fun(double x) {
          float a = x;   // 可能丢失数据
          float b(x);    // 可能丢失数据
          float c{x};    // 可能丢失数据,但不会通过编译
      
          float d{static_cast<float>(x)};  // OK,有意转换
          // ...
      }

      例中 double 类型的参数转为 float 变量可能会丢失数据,用列表初始化可有效避免意料之外的错误。

    • 列表出初始化也可以用于直接、拷贝等初始化,如对于类类型,列表初始化也会调用相应的构造函数,如果列表为空,则调用默认构造函数。
      struct A {
          int x, y;
          A(int x, int y): x{x}, y{y} {}
      };
      A a{1, 2};  // 直接初始化,调用构造函数
      A b{a};     // 拷贝初始化
    • 通过={}初始化在理论上是拷贝初始化,不带等号的{}才是直接初始化,虽然复制成本可被优化,但仍应避免使用多余的等号。
      struct T {
          int x;
          explicit T(int i): x(i) {}
      };
      T a{1};     // OK,直接初始化
      T b = {1};  // 无法通过编译

      例中 ={1} 实际上先由 {1} 初始化一个临时对象,再由 = 完成拷贝初始化,但由于构造函数由 explicit 关键字限定,临时对象无法隐式转为 T 类型的对象,所以无法通过编译。

    • 初始化列表的类型为std::initializer_list<T>T为元素类型,如果相关构造函数对其有重载,则调用相关重载了的构造函数。
      std::vector<int> v(5, 0);  // 五个值为 0 的元素
      std::vector<int> w{5, 0};  // 两个元素,第一个是 5,第二个是 0

      std::vector 对 initializer_list 进行了重载,可以像初始化数组一样初始化 vector,v 有 5 个元素,每个元素都是 0,与 v 不同,w 有两个元素,第一个是 5,第二个是 0,这一点列表初始化无法代替直接初始化。

    • C++20 引入通过指派符初始化的方法,与 C 语言的指派初始化相似,以 “.成员名称 = ... ” 的形式对结构体对象进行初始化。
    • 可通过指派符初始化的对象要求:只包含有 public 的直接非静态数据成员,没有用户声明的构造函数或者继承的构造函数,没有虚基类、private 基类或 protected 基类,也没有虚成员函数。
      struct A {
          int x, y, z;
      };
      A a{.y = 2, .x = 1};  // 语法错误, 指派符 .x 应排在 .y 之前
      A b{.x = 1, .z = 2};  // 正确,b.x 为 1,b.z 为 2,而 b.y 会被初始化为 0
  6. 零初始化(Zero-initialization)
    • 用空括号()、空花括号{},以及用花括号对部分数组元素初始化。
    • 零初始化是以上几种初始化方法的特殊形式,可以将变量、数组、类对象成员初始化为零。
      复制代码
      static int n;  // 零初始化,n 的值为 0
      static int* p;  //  零初始化,ptr 的值为 nullptr
      
      int i{};  // 零初始化,i 的值为 0
      int f();  // 非初始化,f 是一个函数
      
      int* pi = new int{};     // 零初始化, *pi 的值为 0
      int* qi = new int[5]();  // 零初始化堆数组
      
      int a[8]{};   // 零初始化数组,所有元素均为 0
      int b[8]{0};  // 零初始化数组
      int c[8]{1};  // c[0] 为 1,从第二个元素开始零初始化,c[1] 到 c[7] 均为 0
      复制代码

      注意,例中int f();不是零初始化,而是声明了一个函数,这是一种常见笔误,改用{}可以避免这种问题。

    • 对于类对象,如果默认构造函数没有显式定义,或用=default定义,可以进行零初始化。
      复制代码
      struct A {
          int x, y;
      };
      
      A a{};      // 零初始化,成员均为 0
      A b = A();    // 零初始化
      auto c = A();   // 零初始化
      
      A* p = new A();     // 零初始化
      A* q = new A[4]();  // 零初始化数组
      复制代码
    • 对于类对象,如果显式定义了默认构造函数,则调用默认构造函数,不属于零初始化,属于“值初始化”。
      struct A {
          int x, y;
          A(): x(1), y(2) {}  // 默认构造函数
      };
      A a{};          // 非零初始化,成员 x 的值为 1,y 的值为 2
      A* p = new A();   // 同上
      A* q = new A[4]();  // 同上
    • 应尽量完善类的构造函数,对于无法显式定义构造函数的类型,则应及时使用零始初化。
  7. 值初始化(Value-initialization):
    • 也是用空括号()、空花括号{}初始化。
    • 值初始化包含零初始化。
    • 如果类对象的默认构造函数没有显式定义,或由=default定义,则优先进行零初始化,否则进行值初始化。
      struct A {
          int x, y;
          A():
              x(),  // 值初始化,也是零初始化
              y(1)  // 直接初始化,值为 1
          {}
      };
      A a{};  // 值初始化,a.x 为 0,a.y 为 1
  8. 常量初始化(Constant initialization)

    • 用于常量的编译期初始化。
      复制代码
      const int i = 5;  // 常量初始化
      int j = 3;
      const int k = j;  // 直接初始化,但不是常量初始化
      
      void foo() {
          std::array<int, i> ai;  // OK
          std::array<int, k> ak;  // 编译错误,k 不是编译期常量
          // ...
      }
      复制代码

      例中,i 是常量初始化,在编译期完成,j 是变量,不能用于常量初始化。

    • 常量初始化和零初始化统称为静态初始化,其他初始化均为动态初始化。
    • 静态初始化应在动态初始化之前完成。.
  9. 引用初始化(Reference initialization)
    • 用于将对象绑定到引用。
      int i = 0;
      int& r = i;  // 引用初始化
      
      const double& crd = i;  // 引用初始化,引用的是由 i 转成的临时 double 对象
      double&& rrd = i;      // 引用初始化,引用的是由 i 转成的临时 double 对象
      
      double& rd = i;  // 编译错误

      例中 crd 和 rrd 引用的是临时对象,临时对象的生命周期也被延长。

 

综上所述,C++ 的初始化方法各有特点和使用场景,开发者在选择初始化方法时需要根据具体情况谨慎考虑,并注意避免常见的错误和陷阱。

更进一步地,可参见如下详细介绍:

  1.  不可访问未初始化或已释放的资源
  2. 全局对象的初始化不可依赖未初始化的对象
  3. 合理初始化各枚举项
  4. 用 {} 代替 = 或 () 进行初始化
  5. 在初始化列表中对聚合体也应使用初始化列表
  6. 初始化列表中不应存在重复的指派符
  7. 对象初始化不可依赖自身的值
  8. 全局对象的初始化过程不可抛出异常
  9. 局部对象在使用前应被初始化
  10. 成员须在声明处或构造时初始化
  11. 成员初始化应遵循声明的顺序
  12. 不可解引用未初始化的指针
  13. 拷贝构造函数应避免实现复制之外的功能
  14. 移动构造函数应避免实现数据移动之外的功能

 

posted @   幸运泡泡  阅读(262)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示