在各种编程语言中,初始化都是非常重要的步骤,用于确保对象在使用前具有确定的初始状态。C++ 提供了多种初始化方法,每种方法都有其特定的使用场景和注意事项。
以下是一些主要的初始化方法及其注意事项:
- 默认初始化(Default-initialization):
- 形如
T obj
、new T
等方式的初始化,其中T
为类型名称、obj
为对象名称,T
也可以是数组类型。 - 对于具有自动存储期和动态存储期的对象,如局部变量、用
new
分配的结构体等:- 对于基本类型的对象,默认初始化仅分配对象空间,对象的值是不确定的。
- 对于类对象,默认初始化会调用其默认构造函数,如果默认构造函数没有显式定义或由
=default
定义,成员的值也是不确定的。
- 对于具有静态或线程存储期的对象,如全局对象、用
static
或thread_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”。
- 形如
- 直接初始化(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); // 直接初始化
- 拷贝初始化(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),以减少不必要的对象拷贝或移动,进一步提高性能。
- 聚合初始化(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}}; // 聚合初始化结构体数组
- 用
- 列表初始化(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
- 使用花括号
- 零初始化(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](); // 同上
- 应尽量完善类的构造函数,对于无法显式定义构造函数的类型,则应及时使用零始初化。
- 用空括号
- 值初始化(Value-initialization):
- 也是用空括号
()
、空花括号{}
初始化。 - 值初始化包含零初始化。
- 如果类对象的默认构造函数没有显式定义,或由
=default
定义,则优先进行零初始化,否则进行值初始化。struct A { int x, y; A(): x(), // 值初始化,也是零初始化 y(1) // 直接初始化,值为 1 {} }; A a{}; // 值初始化,a.x 为 0,a.y 为 1
- 也是用空括号
-
常量初始化(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 是变量,不能用于常量初始化。
- 常量初始化和零初始化统称为静态初始化,其他初始化均为动态初始化。
- 静态初始化应在动态初始化之前完成。.
- 用于常量的编译期初始化。
- 引用初始化(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++ 的初始化方法各有特点和使用场景,开发者在选择初始化方法时需要根据具体情况谨慎考虑,并注意避免常见的错误和陷阱。
更进一步地,可参见如下详细介绍:
- 不可访问未初始化或已释放的资源
- 全局对象的初始化不可依赖未初始化的对象
- 合理初始化各枚举项
- 用 {} 代替 = 或 () 进行初始化
- 在初始化列表中对聚合体也应使用初始化列表
- 初始化列表中不应存在重复的指派符
- 对象初始化不可依赖自身的值
- 全局对象的初始化过程不可抛出异常
- 局部对象在使用前应被初始化
- 成员须在声明处或构造时初始化
- 成员初始化应遵循声明的顺序
- 不可解引用未初始化的指针
- 拷贝构造函数应避免实现复制之外的功能
- 移动构造函数应避免实现数据移动之外的功能
标签:总结,初始化,int,C++,对象,默认,拷贝,构造函数 From: https://www.cnblogs.com/lucky-bubble/p/18343274