Modern C++(五)初始化
5、初始化
5.1、初始化器
5.1.1、复制初始化
复制初始化指的是利用赋值符号(=)对变量进行初始化:
class MyClass {
public:MyClass(int val) : value(val) {std::cout << "Conversion constructor called." << std::endl;}MyClass(const MyClass& other) : value(other.value) {std::cout << "Copy constructor called." << std::endl;}
private:int value;
};int main() {// 1 复制初始化,调用转换构造函数将42隐式转换为MyClass对象MyClass obj = 42; // 2 复制初始化,调用拷贝构造函数MyClass obj2 = obj; return 0;
}
5.1.2、直接初始化()
T obj(value),直接调用匹配的构造函数而不需要额外的转换步骤
5.1.3、列表初始化{}
禁止缩窄转换(即不允许将一个可能导致数据丢失的值赋给对象);可用于聚合初始化(如数组、结构体等);如果类提供了初始化列表构造函数(std::initializer_list),列表初始化会优先调用该构造函数。
#include <string>std::string s1; // 默认初始化
std::string s2(); // 不是初始化!// 实际上声明了没有形参并且返回 std::string 的函数 “s2”
std::string s3 = "hello"; // 复制初始化
std::string s4("hello"); // 直接初始化
std::string s5{'a'}; // 列表初始化(C++11 起)char a[3] = {'a', 'b'}; // 聚合初始化(C++11 起是列表初始化的一部分)
char& c = a[0]; // 引用初始化
要注意的如果没有参数,又想用直接初始化,不需要加括号,直接string s就可以了(默认初始化)。
- 非局部变量的初始化:具有静态存储期的非局部变量的初始化,会作为程序启动的一部分在 main 函数的执行之前进行。所有具有线程局部存储期的非局部变量的初始化,会作为线程启动的一部分进行,并按顺序早于线程函数的执行开始。
- 静态初始化:有两种形式,常量初始化,或者零初始化
- 动态初始化:在所有静态初始化完成后,进行无序的动态初始化、部分有序的动态初始化、有序的动态初始化
5.2、默认初始化
不使用初始化器构造变量时执行的初始化
- T 对象;
- new T;
如果 T 是数组类型,那么该数组的每个元素都被默认初始化。
对于const修饰的对象,如果想用默认初始化,需要注意以下几点:
- 对于非类类型:不能对 const 限定的对象进行默认初始化
const int x; // 错误,不能对 const int 进行默认初始化
- 类类型或者类类型数组:要对 const 限定的对象进行默认初始化,该类必须有一个可访问的默认构造函数
#include <iostream>class MyClass {
public:MyClass() { // 默认构造函数std::cout << "Default constructor called" << std::endl;}
};class AnotherClass {
public:AnotherClass(int value) : data(value) {} // 没有默认构造函数
private:int data;
};int main() {const MyClass obj1; // 正确,MyClass 有默认构造函数// const AnotherClass obj2; // 错误,AnotherClass 没有默认构造函数return 0;
}
默认初始化对于自动存储期的内置类型不会赋予特定的值,为了避免使用到不确定值而引发的潜在问题,在使用具有自动或动态存储期的对象之前,应该对其进行显式初始化
5.3、值初始化
以空初始化器构造对象时进行的初始化,值初始化会将内置类型初始化为0或者nullptr
- T () // 创建无名临时对象
- new T ()
- 类::类(…) : 成员() { … }
- T 对象 {};
- T {} // 创建无名临时对象
- new T {}
- 类::类(…) : 成员{} { … }
上面创建无名临时变量有什么用呢?其实用的非常多:
// 如果 T 是基本类型(如 int, double),则初始化为 0 或 0.0。
int x = int(); // x = 0
double y = double(); // y = 0.0
int x = int{}; // x = 0// 如果 T 是类类型,则调用默认构造函数:
std::string s = std::string(); // 调用默认构造函数,s 是空字符串
std::string s = std::string{}; // s 是空字符串// 如果 T 是数组,则每个元素被值初始化:
int arr[5] = int(); // arr = {0, 0, 0, 0, 0}
// 如果 T 是聚合类型(如数组、结构体),可以初始化成员:
struct Point { int x, y; };
Point p = Point{}; // p.x = 0, p.y = 0
我们要养成使用值初始化的习惯,避免未定义的行为:
int x; // 未初始化,可能是垃圾值
int y = int(); // y = 0
int z{}; // z = 0
可以将他们作为默认参数或者返回值:
std::string getDefaultString() {return std::string{}; // 返回空字符串
}
构造临时对象用于函数传参
void process(const std::string& s);
process(std::string{}); // 传入一个空字符串
在容器中填充默认值
std::vector<int> vec(10, int{}); // vec = {0, 0, 0, ..., 0}
对于某些类型(如 std::unique_ptr),T() 和 T{} 都返回 nullptr,可能不如直接 nullptr 清晰:
std::unique_ptr<int> p1 = std::unique_ptr<int>(); // p1 = nullptr
std::unique_ptr<int> p2{}; // p2 = nullptr
std::unique_ptr<int> p3 = nullptr; // 更清晰
优先使用T{},它是现代C++ 推荐的初始化方式,支持初始化列表,支持聚合类型,支持成员初始化。
5.4、零初始化
将一个对象的初始值设为零。零初始化没有专用语法,以下用法会将对象初始化为0
- static T 对象 ;
- T () ;
- T t = {} ;
- T {} ; (C++11 起)
- CharT 数组 [ n ] = " 短序列 "; // 若初始化列表中的元素少于数组长度,剩余元素会被默认初始化为 0
- int arr[3] = {0}; // 第一个元素设为 0,其余自动初始化为 0
5.5、复制初始化
从另一个对象初始化对象。
- T 对象 = 其他对象; //
- T 对象 = {其他对象}; // C++11起归类为列表初始化
- f(其他对象) // 当按值传递实参到函数时。
- return 其他对象; // 从按值返回的函数中返回时
- throw 对象; catch (T 对象) // 按值抛出或捕获异常时
- T 数组[N] = {其他序列}; // 作为聚合初始化的一部分,用以初始化每个提供了初始化器的元素
class A {
public:A(int) {} // 非 explicit 构造函数
};A a = 1;
语法上属于复制初始化,这里会发生隐式转换:将 1 转换为 A 类型的临时对象(通过 A(int) 构造函数),再调用复制构造函数 A(const A&),用临时对象初始化 a。但实际编译时,编译器会优化掉临时对象和复制构造函数(称为 返回值优化,RVO),直接调用 A(int) 构造函数。如果复制构造函数是私有的或被删除,这段代码会编译失败(即使有优化)。
5.6、直接初始化
以一组明确的构造函数实参对对象进行初始化。
- T 对象 ( 实参 );
- T 对象 { 实参 };
- T ( 其他对象 )
- static_cast< T >( 其他对象 )
- new T(实参列表, …)
- 类::类() : 成员(实参列表, …) { … }
- 实参{ … } // 在 lambda 表达式中从按复制捕获的变量初始化闭包对象的成员
5.7、聚合初始化
从初始化器列表初始化聚合体
- T 对象 = { 实参1, 实参2, … };
- T 对象 { 实参1, 实参2, … };
- T 对象 = { .指派符1 = 实参1 , .指派符2 { 实参2 } … };
- T 对象 { .指派符1 = 实参1 , .指派符2 { 实参2 } … };
5.8、列表初始化 (C++11 起)
从花括号包围的初始化器列表列表初始化对象。
直接列表初始化:
- T 对象 { 实参1, 实参2, … };
- T { 实参1, 实参2, … }
- new T { 实参1, 实参2, … }
- 类 { T 成员 { 实参1, 实参2, … }; };
- 类::类() : 成员 { 实参1, 实参2, … } {…
复制列表初始化:
- T 对象 = { 实参1, 实参2, … };
- 函数 ({ 实参1, 实参2, … })
- return { 实参1, 实参2, … };
- 对象 [{ 实参1, 实参2, … }]
- 对象 = { 实参1, 实参2, … } // 在赋值表达式中,以列表初始化对重载的运算符的形参进行初始化
5.9、引用初始化
将一个引用绑定到一个对象。
非列表初始化:
- T & 引用 = 目标 ;
- T & 引用 ( 目标 );
- T && 引用 = 目标 ;
- T && 引用 ( 目标 );
- return 目标 ; // 在返回引用的函数的定义中
一般列表初始化 (C++11 起)
- T & 引用 = { 实参1, 实参2, … };
- T & 引用 { 实参1, 实参2, … };
- T && 引用 = { 实参1, 实参2, … };
- T && 引用 { 实参1, 实参2, … };
5.10、常量初始化
设置静态变量的初值为编译时常量。常量初始化在实践中通常在程序被加载到内存时进行,作为程序运行环境的初始化的一部分。在定义常量时,如果依赖于未初始化的变量,会导致该常量不是常量表达式。
5.11、复制消除
在 C++ 里,当创建一个对象时,可能会涉及到复制构造函数或移动构造函数的调用,这些操作会带来一定的性能开销。复制消除技术允许编译器在某些情况下直接创建对象,而无需进行额外的复制或移动操作,从而提高程序的性能。
比较常见的有:
- 函数返回值优化(RVO,Return Value Optimization)
class MyClass {
public:MyClass() {std::cout << "Default constructor" << std::endl;}MyClass(const MyClass& other) {std::cout << "Copy constructor" << std::endl;}~MyClass() {std::cout << "Destructor" << std::endl;}
};MyClass createObject() {return MyClass();
}int main() {MyClass obj = createObject();return 0;
}
按照常规情况,返回的临时对象会调用复制构造函数将其内容复制到 main 函数中的 obj 对象中。但由于复制消除技术,编译器会直接在 obj 的位置构造临时对象,从而省略了复制构造函数的调用。
- 直接初始化临时对象
class MyClass {
public:MyClass() {std::cout << "Default constructor" << std::endl;}MyClass(const MyClass& other) {std::cout << "Copy constructor" << std::endl;}~MyClass() {std::cout << "Destructor" << std::endl;}
};int main() {MyClass obj = MyClass();return 0;
}
语句原本需要先创建一个临时的 MyClass 对象,然后使用复制构造函数将其内容复制到 obj 中。但编译器会进行复制消除,直接在 obj 的位置构造对象,避免了复制构造函数的调用。