C++98 传统的 {} 初始化
基本概念
在 C++98 标准中,{} 主要用于对聚合类型(数组、结构体、联合体)进行初始化。
使用条件
- 简单的结构体 (POD):即没有构造函数、私有成员等的纯数据结构。
- 不能有用户自定义的构造函数
- 不能有私有/保护成员
- 不能有虚函数
代码示例
cpp
struct Point
{
int _x;
int _y;
};
struct Student
{
char name[20];
int age;
};
int main()
{
// 1. 数组初始化
int array1[] = { 1, 2, 3, 4, 5 }; // 自动推断大小
int array2[5] = { 0 }; // 全部初始化为0
int array3[3] = { 1, 2 }; // {1, 2, 0} 未指定补0
// 2. 结构体初始化
Point p = { 1, 2 }; // p._x = 1, p._y = 2
Point p2 = { 1 }; // p2._x = 1, p2._y = 0
// 3. 结构体数组
Student students[2] = {
{ "Tom", 18 },
{ "Jerry", 20 }
};
// 4. 二维数组
int matrix[2][3] = {
{1, 2, 3},
{4, 5, 6}
};
return 0;
}
C++11 列表初始化({})
一、核心目标
C++11 试图实现统一初始化 ,让一切对象都可以用 {} 初始化,也称为列表初始化(List Initialization)。
二、基本语法
cpp
// 1. 可以省略 = 号,并且可以使用{}对内置类型初始化
int a{10}; // 等价于 int a = {10};
int b = {20}; // 传统写法也保留
// 2. 数组
int arr[]{1, 2, 3, 4};
// 3. 结构体
struct Point { int x; int y; };
Point p{1, 2};
// 4. 类对象(需要匹配的构造函数)
std::vector<int> v{1, 2, 3, 4};
std::pair<int, std::string> pr{1, "hello"};
//5.自定义类型,本质是类型转换,中间会产生临时对象,编译器优化后变成直接构造。
class MyClass {
public:
MyClass(int a, int b) {}
};
MyClass obj{1, 2}; // 调用构造函数
// 等价于 MyClass obj = {1, 2}; 编译器优化后直接构造
什么时候会产生临时对象?
为什么在自定义类型中使用{}初始化会产生临时对象,但是创建容器时不会?
cpp
// 情况1:使用 = 且类型不匹配时(C++98/03风格)
std::string s = "hello"; // 先构造临时 string("hello"),再拷贝构造 s
// 但现代编译器会优化为直接构造(拷贝省略,Copy Elision)
// 情况2:显式创建临时对象
std::vector<int> v = std::vector<int>{1, 2, 3}; // 有临时对象,但会被优化
// 情况3:函数参数传递
void func(std::vector<int> v) {}
func({1, 2, 3}); // 从 {} 构造临时 vector,再拷贝给参数(C++11后可能移动)
之所以{}初始化容器时不会产生临时变量是因为std::initializer_list(一种伪容器/视图)的存在。
C++11 std::initializer_list
C++11 之前的容器初始化非常不便:
cpp
// 想要用多个值初始化 vector,需要多次 push_back 或实现多个构造函数
std::vector<int> v1;
v1.push_back(1);
v1.push_back(2);
v1.push_back(3);
// 或者用数组赋值
int arr[] = {1, 2, 3};
std::vector<int> v2(arr, arr + 3);
不同的元素个数需要不同的构造函数,无法统一处理。
std::initializer_list 底层会开辟一个数组,将数据拷贝过来,内部有两个指针(也可能是头指针加长度):
它指向的数据通常是由编译器在幕后生成的临时数组(可能在栈上,也可能在静态存储区)。
一旦这个临时的初始化表达式结束,底层的数组可能就会失效,而 initializer_list 里的指针就会变成悬空指针。
真正的容器 :你可以随意修改里面的元素(v[0] = 10),可以增删元素(push_back)。
std::initializer_list :它是完全只读的。
它的迭代器类型是 const T*。
你不能通过它修改元素的值,也不能往里面添加或删除元素。
cpp
// 概念实现(简化版)
template<typename T>
class initializer_list {
const T* _begin; // 指向数组起始
const T* _end; // 指向数组末尾
// ...
};
核心工作流程 :先由编译器生成一个临时的"只读数组",然后 vector(或其他容器)遍历这个临时数组,把元素一个个拷贝(或移动)到自己的内存里。
cpp
std::vector<std::string> v = {"hello", "world"};
第一次拷贝:字符串常量 → initializer_list 的底层数组
第二次拷贝:initializer_list 底层数组 → vector 内部存储
initializer_list 返回值是常量左值引用,因为要求不能修改initializer_list 的内容,所以const对象的指针是无法绑定到非const对象上的,而容器实际上要求支持修改,所以拷贝无法避免,理论上initializer_list 返回右值引用可以实现移动,避免拷贝,但是由于历史问题和其他安全性原因,c++委员会并未修改这一标准。
当然实际上编译器可以直接优化掉地一次拷贝,使用临时变量直接构造initializer_list对象。称之为复制消除
容器⽀持⼀个std::initializer_list的构造函数,也就⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化。STL中的容器⽀持任意多个值构成的 {x1,x2,x3...} 进⾏初始化,就是通过
std::initializer_list的构造函数⽀持的。
右值引用和移动语义
一、背景概述
C++98 中的引用现在称为左值引用 。C++11 新增了右值引用语法特性。无论是左值引用还是右值引用,本质都是给对象取别名。
二、左值和右值
1. 左值(lvalue)
定义:表示数据的表达式(如变量名或解引用的指针),一般有持久状态,存储在内存中。
特点:
- 可以获取地址(
&) - 可以出现在赋值符号左边
- 也可以出现在赋值符号右边
const修饰的左值不能赋值,但可以取地址
示例:
cpp
int x = 10; // x 是左值
int* p = &x; // 可以取地址
int y = x; // x 出现在右边
x = 20; // x 出现在左边
const int z = 30; // z 是 const 左值
// z = 40; // 错误:不能赋值
int* pz = &z; // 可以取地址(需要 const_cast 转换)
2. 右值(rvalue)
定义:表示数据的表达式,通常是字面值常量或表达式求值过程中创建的临时对象。
特点:
-
不能取地址
-
只能出现在赋值符号右边
-
不能出现在赋值符号左边
cpp
int x = 10; // 10 是右值
int y = x + 20; // x + 20 是右值(临时结果)
// 10 = x; // 错误:右值不能在左边
// int* p = &10; // 错误:不能取地址
cpp
#include<iostream>
using namespace std;
int main()
{
// 左值:可以取地址
// 以下的p、b、c、*p、s、s[0]就是常⻅的左值
int* p = new int(0);
int b = 1;
const int c = b;
*p = 10;
string s("111111");
s[0] = 'x';
cout << &c << endl;
cout << (void*)&s[0] << endl;
// 右值:不能取地址
double x = 1.1, y = 2.2;
// 以下⼏个10、x + y、fmin(x, y)、string("11111")都是常⻅的右值
10;
x + y;
fmin(x, y);
string("11111");
//cout << &10 << endl;
//cout << &(x+y) << endl;
//cout << &(fmin(x, y)) << endl;
//cout << &string("11111") << endl;
return 0;
}
左值引用与右值引用
一、基本语法
cpp
Type& r1 = x; // 左值引用:给左值取别名
Type&& rr1 = y; // 右值引用:给右值取别名
二、绑定规则
- 左值引用的绑定
cpp
int a = 10;
int& r1 = a; // 左值引用绑定左值
// int& r2 = 10; // 错误:左值引用不能直接绑定右值
const int& r3 = 10; // const 左值引用可以绑定右值(延长生命周期)
- 右值引用的绑定
cpp
int&& rr1 = 10; // 右值引用绑定右值
int a = 10;
// int&& rr2 = a; // 错误:右值引用不能直接绑定左值
int&& rr3 = std::move(a); // 使用 move 将左值转为右值引用
std::move
std::move 是一个函数模板,内部进行强制类型转换(涉及引用折叠),将左值转换为右值引用。它的作用是告诉编译器:"这个变量虽然是个有名字的左值,但我保证之后不会再使用它了,你可以把它当作一个临时的右值来处理,放心地'偷走'它的资源。
cpp
int a = 10;
int&& rr = std::move(a); // 将左值 a 转为右值引用
// 注意:move 只是转换类型,并不移动任何东西
// 真正的移动发生在移动构造函数或移动赋值运算符中
template <class T>
typename remove_reference<T>::type&& move(T&& arg);
右值引用变量本身是左值
关键规则:变量表达式都是左值属性。
cpp
int&& rr = 10; // rr 是右值引用,绑定到右值 10
// 但 rr 本身是一个变量,有名字,可取地址
// 所以 rr 作为表达式时,是左值!
int* p = &rr; // 可以取地址,证明 rr 是左值
// int&& rr2 = rr; // 错误:不能将右值引用绑定到左值
int&& rr2 = std::move(rr); // 需要再次 move
C++ 移动构造与移动赋值详解
在 C++11 及更高版本中,移动语义(Move Semantics)是提升性能的关键特性,特别是对于涉及深拷贝(Deep Copy)的类。
核心概念
-
移动构造函数
- 类似于拷贝构造函数,但其参数必须是右值引用 (
T&&)。 - 如果存在其他参数,这些参数必须有默认值。
- 作用:当通过临时对象(右值)初始化新对象时调用,直接"窃取"源对象的资源,避免深拷贝。
- 类似于拷贝构造函数,但其参数必须是右值引用 (
-
移动赋值运算符
- 是赋值运算符的重载,与拷贝赋值运算符构成重载关系。
- 参数同样必须是右值引用 (
T&&)。 - 作用:当将一个临时对象(右值)赋值给一个已存在的对象时调用,接管资源。
为什么需要移动语义?
对于像 std::string、std::vector 这样管理堆内存的类,或者包含深拷贝成员变量的类,传统的拷贝操作开销巨大(需要重新分配内存并复制数据)。
本质区别:
- 拷贝(Copy): 复制资源(深拷贝),开销大。
- 移动(Move): "窃取"资源(浅拷贝指针),源对象被置为有效但未指定的状态(通常指针置空),开销极小,效率极高。
简化string类实现:
cpp
#include <iostream>
#include <cstring> // 用于 strcpy, strlen
#include <utility> // 用于 std::move
class MyString {
private:
char* _data; // 管理堆内存的指针
int _size;
public:
// 1. 普通构造函数
MyString(const char* str = "") {
std::cout << "[普通构造] 分配内存: " << str << std::endl;
_size = strlen(str);
_data = new char[_size + 1];
strcpy(_data, str);
}
// 2. 拷贝构造函数 (深拷贝)
// 场景:MyString s2 = s1;
MyString(const MyString& other) {
std::cout << "[拷贝构造] 深拷贝数据..." << std::endl;
_size = other._size;
_data = new char[_size + 1]; // 重新分配内存
strcpy(_data, other._data); // 复制内容
}
// 3. 移动构造函数 (核心重点)
// 参数必须是右值引用 (MyString&&)
// 场景:MyString s3 = std::move(s1); 或者 MyString s3 = MyString("临时对象");
MyString(MyString&& other) noexcept {
std::cout << "[移动构造] 窃取资源..." << std::endl;
// --- 核心步骤开始 ---
// 第一步:直接接管对方的资源(指针)
_data = other._data;
_size = other._size;
// 第二步:把对方置空(防止析构时重复释放内存)
other._data = nullptr;
other._size = 0;
// --- 核心步骤结束 ---
}
4. 移动赋值运算符
// ==========================================
MyString& operator=(MyString&& other) noexcept {
std::cout << "[移动赋值] 开始..." << std::endl;
// --- 步骤一:自赋值检查 ---
// 如果写 s1 = std::move(s1),必须防止自己释放自己的资源
if (this != &other) {
// --- 步骤二:释放旧资源 ---
// 这一点与移动构造不同!
// 移动构造时对象是新的,没有旧资源。
// 但赋值时,this 对象可能已经持有一块内存(比如 "Hello"),必须先释放,否则内存泄漏。
delete[] _data;
// --- 步骤三:窃取新资源 ---
_data = other._data;
_size = other._size;
// --- 步骤四:掏空源对象 ---
other._data = nullptr;
other._size = 0;
}
return *this;
}
// 析构函数
~MyString() {
if (_data) {
std::cout << "[析构] 释放内存: " << _data << std::endl;
delete[] _data;
} else {
std::cout << "[析构] 释放空指针 (无需操作)" << std::endl;
}
}
// 打印内容的辅助函数
void print() const {
if (_data) std::cout << "内容: " << _data << std::endl;
else std::cout << "内容: (空)" << std::endl;
}
};
int main() {
std::cout << "=== 1. 创建源对象 s1 ===" << std::endl;
MyString s1("Hello World");
std::cout << "\n=== 2. 演示拷贝构造 (深拷贝) ===" << std::endl;
MyString s2 = s1; // 调用拷贝构造
s2.print(); // s2 有自己的内存
std::cout << "\n=== 3. 演示移动构造 (窃取资源) ===" << std::endl;
// 使用 std::move 将 s1 转为右值,触发移动构造
MyString s3 = std::move(s1);
std::cout << "s3 (新对象): ";
s3.print(); // s3 接管了 s1 的内存
std::cout << "s1 (原对象): ";
s1.print(); // s1 的内存被偷走了,变为空
std::cout << "\n=== 4. 程序结束,调用析构 ===" << std::endl;
return 0;
}
之所以参数必须是右值引用,是因为右值马上就要销毁了,所以我们可以放心大胆地把它的资源拿走,而不需要担心它后续会被使用。如果是左值引用,就可能会造成被引用资源移动,但后续该内存依旧在某些部分被访问,会导致非法内存访问"