首页 > 编程语言 >C++ 各种初始化方法总结

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

时间:2024-08-19 09:04:36浏览次数:15  
标签:总结 初始化 int 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. 移动构造函数应避免实现数据移动之外的功能

 

标签:总结,初始化,int,C++,对象,默认,拷贝,构造函数
From: https://www.cnblogs.com/lucky-bubble/p/18343274

相关文章

  • java打印流,commons-io工具包,IO总结
    一.打印流1.概述:平时我们在控制台打印输出,是调用print()方法和println()方法完成的,这两个方法都来自于java.io.PrintStream类作用:该类能够方便地打印各种数据类型的值,写入数据后可以实现自动换行。通常用于日志记录2打印流的构造方法publicPrintStream(StringfileName)......
  • 关于c++使用toml plusplus(俗称toml++)的使用(4)
    链接toml++-githubtoml++-帮助文档使用要求:c++17及以上版本toml语法-英文toml语法-中文toml读取参见官方给出的范例toml写入目标:表嵌套子表数组的写入比如:文件内容[NET_INTERFACE]bool=falsebool_arr=[false,false]complex_arr......
  • 关于c++使用toml plusplus(俗称toml++)的使用(3)
    链接toml++-githubtoml++-帮助文档使用要求:c++17及以上版本toml语法-英文toml语法-中文toml读取参见官方给出的范例toml写入目标:数组的写入文件内容[NET_INTERFACE]bool=falsebool_arr=[false,false]complex_arr=[false,'456'......
  • 动态规划 总结
    DAG上的动态规划与树形DP这两个词看上去很高大上,但实则就是记忆化搜索,而记忆化搜索其实就是DP的本质。当选择一个需要用全局变量来参与描述状态的方式时,就只能用常规搜索。但当状态可以完全被几个非全局参数确定性的描述时,就可以用记忆化搜索,记忆化搜索可以通过存储答案并直接提取......
  • C++——new对象
    new对象与之前C的"类对象"方式有所不同,"类对象"方式并不会调用构造函数和析构函数,而new对象则会调用两个函数,释放该空间时用delete。数组申请int类型的数组#define_CRT_SECURE_NO_WARNINGS#include<iostream>usingnamespacestd;intmain(){ int*a=newint[10];......
  • C++中的多线程编程和锁机制
    二、多线程、锁2.1C语言线程库pthread(POSIXthreads)2.2.1线程创建pthread_create#include<pthread.h>pthread_tthread;ThreadDataargs={1,"Hellofromparameterizedthread"};intresult=pthread_create(&thread,attr,function,args); //线程创建即......
  • 关于c++使用toml plusplus(俗称toml++)的使用(2)
    链接toml++-githubtoml++-帮助文档使用要求:c++17及以上版本toml语法-英文toml语法-中文toml读取参见官方给出的范例toml写入目标目标:数组表的写入目标文件内容如下[NET_INTERFACE]bool=falseinteger=1234567890string='thisisastring'[[f......
  • C++:新枚举与新结构
    一、枚举(一)C枚举?真整数!    考虑下面的程序#include<stdio.h>#include<stdlib.h>typedefenum{spring,summer,autumn,winter}Season;voidprintSeason(Seasonseason){ switch(season){ casespring: printf("spring"); break; case......
  • C++:从Type到Control
    一、基本数据类型     计算机的存储空间由最基本的二进制数(比特)组成,若干连续的二进制位(一般为8位)组成一个字节并被分配一个内存地址(),所以单独的比特没有地址,通常情况下CPU也不会一个比特一个比特读取数据,相反,字节被当作基本操作单位。在此前提下,一切要存储在计算机上的......
  • C++:函数
         FunctionsareC++entitiesthatassociateasequenceofstatements(afunctionbody)withanameandalistofzeroormorefunctionparameters.        函数是C++中的实体,它将一系列语句(一个函数体)与一个名称和零个或多个函数参数列表相关......