作为 C++ 开发的里程碑版本,C++11 带来了近 20 年来最重大的语言革新。它不仅解决了 C++98/03 长期存在的语法痛点,更引入了现代编程语言的核心特性,让 C++ 在保持高性能的同时,大幅提升了开发效率和代码可读性。本文将从发展历史、列表初始化、值类别体系、引用机制到移动语义的实战应用,全方位拆解 C++11 的部分核心特性,带大家从理论到实践,真正掌握这门经典语言的现代化升级。下面就让我们正式开始吧!
C++ 的发展并非一帆风顺,从 1998 年首个标准 C++98 发布到 2011 年 C++11 正式落地,中间经历了长达 8 年的漫长等待。在这期间,C++03 仅做了小幅修正,而编程语言领域却发生了巨大变革 ——Java、C# 等语言凭借更简洁的语法和更完善的标准库迅速崛起,C++ 因繁琐的语法和落后的特性支持逐渐显得力不从心。
C++11 最初被命名为 "C++0x",这个名字蕴含着大家对它的期待 —— 原本计划在 2010 年(21 世纪第一个十年,即 "0x")前发布。但由于特性设计的复杂性和标准委员会的反复讨论,发布时间一再推迟,最终在 2011 年 8 月 12 日由 ISO 正式采纳,正式命名为 C++11。
这八年的等待并非徒劳,C++11 整合了业界多年的实践经验,标准化了大量民间流行的编程技巧,同时引入了全新的语言特性和标准库组件。它的发布不仅让 C++ 重焕生机,更确立了后续每 3 年一个版本的迭代节奏,形成了 C++14、C++17、C++20、C++23 的良性发展脉络。
C++11 的核心目标是 "让 C++ 成为更安全、更高效、更易用的编程语言"。在保持与 C 语言兼容和零成本抽象的核心优势基础上,它主要解决了三大痛点:
从实际开发角度看,C++11 的特性已经成为当前 C++ 开发的基础标配。无论是大型开源项目(如 Boost、LLVM),还是企业级应用开发,C++11 特性的使用率都超过 90%。掌握 C++11,已经成为 C++ 开发者的必备技能。
自 C++11 起,C++ 标准进入了规律迭代的快车道,每 3 年一个主要版本,逐步完善语言特性和标准库:
版本 | 发布年份 | 核心特性 |
|---|---|---|
C++98 | 1998 年 | 首个正式标准,确立类、模板、STL 等核心机制 |
C++03 | 2003 年 | 小幅修正,主要修复 C++98 的技术缺陷 |
C++11 | 2011 年 | 重大革新,引入移动语义、lambda、智能指针等核心特性 |
C++14 | 2014 年 | 完善 C++11 特性,优化泛型编程支持 |
C++17 | 2017 年 | 新增文件系统库、并行算法、结构化绑定等特性 |
C++20 | 2020 年 | 引入概念(Concepts)、协程(Coroutines)、模块(Modules) |
C++23 | 2023 年 | 增强范围库、新增 print 函数、优化移动语义 |

可以看到,C++11 是整个迭代路线的基石,后续版本都是在其基础上的完善和扩展。因此,深入理解 C++11 的核心特性,是掌握现代 C++ 的关键。
在 C++98 中,初始化语法混乱是开发者公认的痛点之一。不同类型的对象有着不同的初始化方式,不仅增加了记忆负担,还容易导致代码出错。C++11 引入的列表初始化(List Initialization),用统一的{}语法解决了这一问题,实现了 "一切对象皆可列表初始化" 的目标。
C++98 中,不同类型的初始化方式各不相同,容易让人混淆:
{}初始化,如int arr[] = {1,2,3};{}初始化,如Point p = {1,2};int a = 5或int a(5); 这种分散的初始化方式,不仅让代码风格不统一,还存在功能限制。例如,STL 容器无法直接用初始化列表赋值,必须通过循环或insert函数逐个添加元素,代码繁琐且低效。
// C++98中的初始化方式
#include <iostream>
#include <vector>
using namespace std;
struct Point {
int _x;
int _y;
};
int main() {
// 数组初始化
int array1[] = {1, 2, 3, 4, 5};
int array2[5] = {0}; // 部分初始化,剩余元素默认为0
// 结构体初始化
Point p = {1, 2};
// 容器初始化:繁琐的方式
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
// 无法直接写成 vector<int> v = {1,2,3};
return 0;
} C++11 扩展了{ }的使用范围,让几乎所有对象都能通过列表初始化,并且支持省略 = 号,语法更加简洁。其核心特性包括:
无论是内置类型、自定义类型,还是 STL 容器,都可以使用{}进行初始化,语法统一且直观:
// C++11列表初始化示例
#include <iostream>
#include <vector>
#include <map>
using namespace std;
struct Point {
int _x;
int _y;
};
class Date {
public:
Date(int year = 1, int month = 1, int day = 1)
: _year(year), _month(month), _day(day) {
cout << "Date构造函数调用" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
// 内置类型初始化:支持省略=
int a1 = {10};
int a2 {20}; // 省略=,更简洁
double d {3.14};
// 数组初始化:兼容旧语法,支持省略=
int arr1[] = {1,2,3,4,5};
int arr2[] {6,7,8,9,10};
// 结构体初始化
Point p1 = {1, 2};
Point p2 {3, 4}; // 省略=
// 自定义类型初始化
Date d1 = {2025, 10, 1}; // 等价于Date d1(2025,10,1)
Date d2 {2025, 10, 2}; // 省略=,直接构造
const Date& d3 {2025, 10, 3}; // 绑定临时对象
// STL容器初始化:C++11新增支持
vector<int> v {1,2,3,4,5}; // 直接用列表初始化容器
map<string, string> dict {{"sort", "排序"}, {"vector", "向量"}};
return 0;
} 对于自定义类型,列表初始化本质上是通过{}构造一个临时对象,再通过拷贝构造初始化目标对象。但编译器会进行优化,将 "构造临时对象 + 拷贝构造" 合并为直接构造,避免了额外的性能开销。
// 列表初始化的优化验证
#include <iostream>
using namespace std;
class Date {
public:
Date(int year, int month, int day)
: _year(year), _month(month), _day(day) {
cout << "Date(int, int, int):直接构造" << endl;
}
Date(const Date& d)
: _year(d._year), _month(d._month), _day(d._day) {
cout << "Date(const Date&):拷贝构造" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
// 列表初始化:编译器优化后仅调用直接构造
Date d {2025, 10, 1};
// 输出:Date(int, int, int):直接构造
// 没有调用拷贝构造,说明优化生效
return 0;
}列表初始化的一个重要安全特性是禁止窄化转换(Narrowing Conversion),即不允许将范围大的类型隐式转换为范围小的类型,避免数据丢失。这是传统初始化方式不具备的安全保障。
// 列表初始化禁止窄化转换
int main() {
int a = 3.14; // 允许:double隐式转换为int,数据丢失(3.14→3)
// int b {3.14}; // 编译错误:列表初始化禁止窄化转换
char c = 1000; // 允许:int隐式转换为char,数据溢出
// char d {1000}; // 编译错误:列表初始化禁止窄化转换
return 0;
} 虽然列表初始化语法简洁,但 STL 容器要支持任意长度的列表初始化,还需要一个底层机制来传递初始化数据。C++11 为此引入了std::initializer_list模板类,它本质上是一个轻量级的容器,存储了一组同类型的值,提供了迭代器接口供遍历。
std::initializer_list内部维护了两个指针,分别指向数据的起始位置和结束位置。当我们使用{ }初始化容器时,编译器会自动将{ }中的元素转换为std::initializer_list对象,容器再通过这个对象完成初始化。
// std::initializer_list的使用示例
#include <iostream>
#include <initializer_list>
using namespace std;
int main() {
// 定义initializer_list对象
initializer_list<int> il = {10, 20, 30, 40};
// 遍历initializer_list
for (auto it = il.begin(); it != il.end(); ++it) {
cout << *it << " ";
}
cout << endl; // 输出:10 20 30 40
// initializer_list的大小
cout << "size: " << il.size() << endl; // 输出:4
// 重新赋值
il = {50, 60, 70};
for (auto val : il) {
cout << val << " ";
}
cout << endl; // 输出:50 60 70
return 0;
} STL 容器(如vector、list、map等)都在 C++11 中新增了接受std::initializer_list参数的构造函数和赋值运算符重载,从而支持列表初始化和列表赋值。
// 容器的initializer_list构造函数模拟实现
template <class T>
class vector {
public:
// 接受initializer_list的构造函数
vector(initializer_list<T> il) {
// 预留空间
reserve(il.size());
// 遍历initializer_list,插入元素
for (auto& e : il) {
push_back(e);
}
}
// 接受initializer_list的赋值运算符
vector& operator=(initializer_list<T> il) {
vector<T> temp(il); // 用il构造临时对象
swap(temp); // 交换当前对象和临时对象的资源
return *this;
}
// 其他成员函数...
private:
T* _start;
T* _finish;
T* _end_of_storage;
};通过这种方式,STL 容器就能无缝支持列表初始化,让代码更加简洁高效。例如:
// 容器列表初始化与赋值
#include <vector>
#include <map>
using namespace std;
int main() {
// 列表初始化
vector<int> v1 {1,2,3,4,5};
map<string, int> m1 {{"a", 1}, {"b", 2}, {"c", 3}};
// 列表赋值
v1 = {6,7,8,9,10};
m1 = {{"x", 10}, {"y", 20}, {"z", 30}};
return 0;
}列表初始化在实际开发中有着广泛的应用,以下是几个典型场景:
这是最常见的场景,直接用{}初始化容器,避免了繁琐的push_back调用,代码更简洁。
// 容器初始化实战
#include <vector>
#include <string>
#include <set>
using namespace std;
int main() {
// 初始化字符串向量
vector<string> fruits {"apple", "banana", "orange", "grape"};
// 初始化整数集合
set<int> nums {10, 20, 30, 40, 50};
// 初始化二维向量
vector<vector<int>> matrix {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};
return 0;
}当函数参数为容器类型时,使用列表初始化可以直接传递一组值,无需先构造容器对象。
// 列表初始化作为函数参数
#include <iostream>
#include <vector>
using namespace std;
void printVector(const vector<int>& v) {
for (auto val : v) {
cout << val << " ";
}
cout << endl;
}
int main() {
// 直接用列表初始化作为函数参数
printVector({1,2,3,4,5}); // 输出:1 2 3 4 5
printVector({10,20,30}); // 输出:10 20 30
return 0;
}函数返回容器类型时,可以直接用列表初始化作为返回值,语法简洁且高效。
// 列表初始化作为函数返回值
#include <vector>
using namespace std;
vector<int> getEvenNumbers(int n) {
vector<int> res;
for (int i = 2; i <= n; i += 2) {
res.push_back(i);
}
return res;
// 或者直接返回列表初始化
// return {2,4,6,8,10};
}
int main() {
auto evenNums = getEvenNumbers(10);
// evenNums = {2,4,6,8,10}
return 0;
}要掌握 C++11 的移动语义,首先需要理解 C++ 中的值类别(Value Categories)。C++11 重新定义了值的分类,将所有表达式分为左值(Lvalue)和右值(Rvalue),其中右值又细分为纯右值(Prvalue)和将亡值(Xvalue)。正确区分左值和右值,是理解后续引用机制和移动语义的基础。
在 C++ 中,左值和右值的核心区别在于是否可以取地址和是否具有持久状态:
左值是指能够标识一个存储位置的表达式,具有以下特征:
常见的左值包括:
// 左值示例
#include <iostream>
#include <string>
using namespace std;
int main() {
// 变量名:左值
int a = 10;
const int b = 20;
cout << "&a: " << &a << endl; // 合法:可以取地址
cout << "&b: " << &b << endl; // 合法:const左值也可以取地址
// 数组元素:左值
int arr[] = {1,2,3};
cout << "&arr[0]: " << &arr[0] << endl; // 合法
// 解引用的指针:左值
int* p = &a;
*p = 30; // 合法:可以赋值
cout << "&*p: " << &*p << endl; // 合法
// 字符串字面量:左值(C风格字符串)
const char* str = "hello world";
cout << "&\"hello world\": " << &"hello world" << endl; // 合法
// 函数返回左值引用
int& getLeftValue(int& x) {
return x;
}
getLeftValue(a) = 40; // 合法:左值可以赋值
cout << "a: " << a << endl; // 输出:40
return 0;
}右值是指不标识存储位置的表达式,具有以下特征:
常见的右值包括:
// 右值示例
#include <iostream>
#include <string>
using namespace std;
int add(int x, int y) {
return x + y; // 返回非引用类型,是右值
}
class Date {
public:
Date(int year, int month, int day)
: _year(year), _month(month), _day(day) {}
private:
int _year;
int _month;
int _day;
};
int main() {
int a = 10, b = 20;
// 字面量常量:右值
30; // 右值
// cout << &30 << endl; // 编译错误:不能取地址
// 表达式求值结果:右值
a + b; // 右值
// cout << &(a + b) << endl; // 编译错误:不能取地址
// 函数返回非引用类型:右值
add(a, b); // 右值
// cout << &add(a, b) << endl; // 编译错误:不能取地址
// 匿名对象:右值
Date(2025, 10, 1); // 右值
// cout << &Date(2025, 10, 1) << endl; // 编译错误:不能取地址
// std::move转换后的对象:右值
string s = "hello";
move(s); // 右值
// cout << &move(s) << endl; // 编译错误:不能取地址
return 0;
}C++11 将右值进一步细分为纯右值(Prvalue)和将亡值(Xvalue):
// 纯右值与将亡值示例
#include <iostream>
#include <string>
using namespace std;
// 返回右值引用的函数:返回将亡值
string&& getXvalue() {
string s = "hello";
return move(s); // s是局部变量,返回后即将销毁,是将亡值
}
int main() {
// 纯右值
int x = 10 + 20; // 10+20是纯右值
string s1 = string("world"); // string("world")是纯右值
// 将亡值
string&& s2 = getXvalue(); // getXvalue()返回将亡值
string&& s3 = move(s1); // move(s1)将s1转换为将亡值
return 0;
}引用是 C++ 的核心特性之一,它为对象提供了一个别名,避免了拷贝开销。C++98 中只有左值引用(Lvalue Reference),而 C++11 新增了右值引用(Rvalue Reference),从而形成了完整的引用体系。
左值引用是对左值的引用,语法为Type& 引用名 = 左值对象。其核心特征:
// 左值引用示例
#include <iostream>
using namespace std;
int main() {
int a = 10;
int& ra = a; // 合法:左值引用绑定左值
ra = 20; // 等价于a = 20
cout << "a: " << a << endl; // 输出:20
// int& rb = 30; // 编译错误:左值引用不能直接绑定右值
// const左值引用可以绑定右值
const int& rc = 30; // 合法:const左值引用延长临时对象生命周期
const int& rd = a + 20; // 合法:绑定表达式求值结果(右值)
return 0;
} 右值引用是对右值的引用,语法为Type&& 引用名 = 右值对象。其核心特征:
// 右值引用示例
#include <iostream>
using namespace std;
int main() {
// 右值引用绑定纯右值
int&& rr1 = 30; // 合法:30是纯右值
rr1 = 40; // 合法:非const右值引用可以修改绑定的右值
cout << "rr1: " << rr1 << endl; // 输出:40
// 右值引用绑定将亡值
int a = 10;
int&& rr2 = move(a); // 合法:move(a)将a转换为将亡值
rr2 = 20;
cout << "a: " << a << endl; // 输出:20(a的值被修改)
// int&& rr3 = a; // 编译错误:右值引用不能直接绑定左值
return 0;
}C++11 中引用的绑定规则可以总结为以下几点:
// 引用绑定规则验证
#include <iostream>
#include <string>
using namespace std;
int main() {
int a = 10; // 左值
const int b = 20; // const左值
int&& c = 30; // 右值引用(绑定右值)
// 左值引用绑定
int& r1 = a; // 合法:绑定左值
// int& r2 = b; // 编译错误:左值引用不能绑定const左值
// int& r3 = 30; // 编译错误:左值引用不能绑定右值
// int& r4 = move(a); // 编译错误:左值引用不能绑定将亡值
// const左值引用绑定
const int& cr1 = a; // 合法:绑定左值
const int& cr2 = b; // 合法:绑定const左值
const int& cr3 = 30; // 合法:绑定右值
const int& cr4 = move(a); // 合法:绑定将亡值
// 右值引用绑定
// int&& r5 = a; // 编译错误:不能绑定左值
// int&& r6 = b; // 编译错误:不能绑定const左值
int&& r7 = 30; // 合法:绑定右值
int&& r8 = move(a); // 合法:绑定将亡值
return 0;
}在 C++ 中,临时对象的生命周期通常很短暂,表达式求值结束后就会被销毁。但通过引用绑定,可以延长临时对象的生命周期,这在实际开发中非常有用。
C++98 就支持 const 左值引用绑定临时对象,并延长其生命周期,直到引用本身被销毁。这一特性在函数参数传递和返回值接收中广泛应用。
// const左值引用延长临时对象生命周期
#include <iostream>
#include <string>
using namespace std;
string getName() {
return "Zhang San"; // 返回临时对象(右值)
}
int main() {
// const左值引用绑定临时对象,延长其生命周期
const string& name = getName();
cout << "name: " << name << endl; // 合法:临时对象未被销毁
// 如果不使用引用,临时对象在赋值后销毁
string name2 = getName(); // 临时对象赋值给name2后销毁
return 0;
}C++11 中,右值引用同样可以延长临时对象的生命周期,并且相比 const 左值引用,右值引用可以修改临时对象(非 const 情况下)。
// 右值引用延长临时对象生命周期
#include <iostream>
#include <string>
using namespace std;
string getName() {
return "Li Si"; // 临时对象(右值)
}
int main() {
// 右值引用绑定临时对象,延长其生命周期
string&& name = getName();
name += " (modified)"; // 合法:非const右值引用可以修改临时对象
cout << "name: " << name << endl; // 输出:Li Si (modified)
return 0;
}需要注意的是,引用延长生命周期的特性有一定限制:
// 生命周期延长的限制
#include <iostream>
#include <string>
using namespace std;
string&& getTempString() {
string s = "Hello";
return move(s); // s是局部变量,返回后销毁
}
int main() {
// 错误:右值引用绑定的是已销毁的局部变量,产生悬垂引用
string&& s = getTempString();
// cout << s << endl; // 未定义行为:s引用的对象已销毁
return 0;
}C++11 中,函数重载可以基于参数的左值 / 右值属性进行区分,编译器会根据实参的类型(左值 / 右值)选择最匹配的重载版本。这一特性是移动语义的基础,让函数能够根据参数类型选择最优的实现方式。
当存在多个重载函数,参数分别为左值引用、const 左值引用、右值引用时,编译器的匹配规则如下:
// 左值和右值的参数匹配示例
#include <iostream>
using namespace std;
// 左值引用参数重载
void printValue(int& x) {
cout << "左值引用版本:x = " << x << endl;
}
// const左值引用参数重载
void printValue(const int& x) {
cout << "const左值引用版本:x = " << x << endl;
}
// 右值引用参数重载
void printValue(int&& x) {
cout << "右值引用版本:x = " << x << endl;
}
int main() {
int a = 10; // 左值
const int b = 20; // const左值
printValue(a); // 输出:左值引用版本:x = 10(实参为左值)
printValue(b); // 输出:const左值引用版本:x = 20(实参为const左值)
printValue(30); // 输出:右值引用版本:x = 30(实参为右值)
printValue(a + b); // 输出:右值引用版本:x = 30(实参为右值)
printValue(move(a)); // 输出:右值引用版本:x = 10(实参为将亡值)
return 0;
}一个重要的细节是:右值引用变量本身是左值。也就是说,当你将一个右值引用变量作为实参传递时,它会被当作左值处理,匹配左值引用的重载版本。
// 右值引用变量的属性
#include <iostream>
using namespace std;
void printValue(int& x) {
cout << "左值引用版本:x = " << x << endl;
}
void printValue(int&& x) {
cout << "右值引用版本:x = " << x << endl;
}
int main() {
int&& rr = 30; // rr是右值引用变量,但本身是左值
printValue(rr); // 输出:左值引用版本:x = 30(rr是左值)
printValue(move(rr)); // 输出:右值引用版本:x = 30(move(rr)是右值)
return 0;
} 这一设计看似矛盾,实则非常合理。因为右值引用变量是一个具名变量,具有持久的生命周期,可以取地址,符合左值的特征。如果想要将其作为右值传递,需要通过std::move进行转换。
移动语义是 C++11 最核心的特性之一,它的核心目标是避免不必要的拷贝,提升程序性能。在 C++98 中,当我们进行对象拷贝(如函数返回对象、容器插入对象)时,会执行深拷贝,拷贝大量数据,导致性能开销。而移动语义通过 "窃取" 右值对象的资源,避免了拷贝操作,大幅提升了程序效率。
移动语义的核心思想是:对于那些即将被销毁的对象(右值对象),我们不需要进行拷贝,而是直接 "窃取" 其内部资源(如内存、文件句柄等),将其转移到新对象中。这样一来,新对象获得了资源,而原对象则变为空状态(不再拥有资源),避免了拷贝的性能开销。
举个形象的例子:你有一个装满书籍的箱子(对象 A),现在需要给小明一个一模一样的箱子(对象 B)。拷贝语义是重新买一批同样的书,放进新箱子里;而移动语义是直接将箱子里的书转移到小明的空箱子里,你的箱子变成空箱子。显然,移动语义的效率要高得多。
要实现移动语义,需要在类中定义移动构造函数(Move Constructor)和移动赋值运算符重载(Move Assignment Operator)。这两个函数的参数都是右值引用,用于接收右值对象。
移动构造函数的语法为:类名(类名&& 源对象) [noexcept]。其核心功能是:
noexcept关键字声明(也可以不加),表明不会抛出异常(便于容器优化)。// 移动构造函数示例
#include <iostream>
#include <cstring>
#include <utility>
using namespace std;
class String {
public:
// 构造函数
String(const char* str = "") {
cout << "String(const char*):构造函数" << endl;
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝构造函数(深拷贝)
String(const String& other) {
cout << "String(const String&):拷贝构造函数(深拷贝)" << endl;
_size = other._size;
_capacity = other._capacity;
_str = new char[_capacity + 1];
strcpy(_str, other._str);
}
// 移动构造函数(窃取资源)
String(String&& other) noexcept {
cout << "String(String&&):移动构造函数(窃取资源)" << endl;
// 窃取other的资源
_str = other._str;
_size = other._size;
_capacity = other._capacity;
// 将other置为空状态
other._str = nullptr;
other._size = 0;
other._capacity = 0;
}
// 析构函数
~String() {
cout << "~String():析构函数" << endl;
if (_str) {
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
int main() {
// 用右值对象初始化,调用移动构造函数
String s1 = String("hello");
// 输出:
// String(const char*):构造函数(创建临时对象)
// String(String&&):移动构造函数(窃取临时对象的资源)
// ~String():析构函数(临时对象被析构,但其_str为nullptr,无资源释放)
// 用move转换左值为右值,调用移动构造函数
String s2("world");
String s3 = move(s2);
// 输出:
// String(const char*):构造函数(创建s2)
// String(String&&):移动构造函数(窃取s2的资源)
return 0;
} 移动赋值运算符的语法为:类名& operator=(类名&& 源对象) [noexcept]。其核心功能与移动构造函数类似,但用于对象赋值场景:
// 移动赋值运算符示例
#include <iostream>
#include <cstring>
#include <utility>
using namespace std;
class String {
public:
// 构造函数
String(const char* str = "") {
cout << "String(const char*):构造函数" << endl;
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// 拷贝赋值运算符(深拷贝)
String& operator=(const String& other) {
cout << "operator=(const String&):拷贝赋值运算符(深拷贝)" << endl;
if (this != &other) {
// 释放当前对象的资源
delete[] _str;
// 深拷贝other的资源
_size = other._size;
_capacity = other._capacity;
_str = new char[_capacity + 1];
strcpy(_str, other._str);
}
return *this;
}
// 移动赋值运算符(窃取资源)
String& operator=(String&& other) noexcept {
cout << "operator=(String&&):移动赋值运算符(窃取资源)" << endl;
if (this != &other) {
// 释放当前对象的资源
delete[] _str;
// 窃取other的资源
_str = other._str;
_size = other._size;
_capacity = other._capacity;
// 将other置为空状态
other._str = nullptr;
other._size = 0;
other._capacity = 0;
}
return *this;
}
// 析构函数
~String() {
cout << "~String():析构函数" << endl;
if (_str) {
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
int main() {
String s1("hello");
String s2("world");
// 移动赋值:将s2的资源移动到s1
s1 = move(s2);
// 输出:
// String(const char*):构造函数(s1)
// String(const char*):构造函数(s2)
// operator=(String&&):移动赋值运算符(s1窃取s2的资源)
return 0;
}C++11 中,编译器会在满足一定条件时,自动生成默认的移动构造函数和移动赋值运算符。生成条件为:
默认生成的移动函数会对内置类型成员执行逐成员拷贝(浅拷贝),对自定义类型成员调用其移动函数(如果存在)。
// 默认移动函数示例
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
Person(const char* name = "", int age = 0)
: _name(name), _age(age) {
cout << "Person:构造函数" << endl;
}
// 没有显式定义移动函数、拷贝函数和析构函数
// 编译器会自动生成默认移动构造和移动赋值
private:
string _name; // 自定义类型,有移动函数
int _age; // 内置类型
};
int main() {
Person p1("Zhang San", 20);
Person p2 = move(p1); // 调用默认移动构造函数
Person p3("Li Si", 30);
p3 = move(p2); // 调用默认移动赋值运算符
return 0;
}移动语义在实际开发中有广泛的应用,主要集中在以下几个场景:
在 C++98 中,函数返回对象时会执行两次拷贝构造(创建局部对象→拷贝到临时对象→拷贝到目标对象),而 C++11 中通过移动语义和编译器优化,可以避免这些拷贝。
// 移动语义在函数返回对象中的应用
#include <iostream>
#include <cstring>
#include <utility>
using namespace std;
class String {
public:
String(const char* str = "") {
cout << "String(const char*):构造函数" << endl;
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
String(const String& other) {
cout << "String(const String&):拷贝构造函数" << endl;
_size = other._size;
_capacity = other._capacity;
_str = new char[_capacity + 1];
strcpy(_str, other._str);
}
String(String&& other) noexcept {
cout << "String(String&&):移动构造函数" << endl;
_str = other._str;
_size = other._size;
_capacity = other._capacity;
other._str = nullptr;
other._size = 0;
other._capacity = 0;
}
~String() {
cout << "~String():析构函数" << endl;
if (_str) {
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
String AddString(const char* str) {
String s(str);
return s; // 返回局部对象,是右值
}
int main() {
// 场景1:直接初始化
String s1 = AddString("hello");
// 输出(关闭优化时):
// String(const char*):构造函数(s)
// String(String&&):移动构造函数(临时对象窃取s的资源)
// String(String&&):移动构造函数(s1窃取临时对象的资源)
// ~String():析构函数(临时对象)
// ~String():析构函数(s)
// 场景2:赋值
String s2;
s2 = AddString("world");
// 输出(关闭优化时):
// String(const char*):构造函数(s2)
// String(const char*):构造函数(s)
// String(String&&):移动构造函数(临时对象窃取s的资源)
// operator=(String&&):移动赋值运算符(s2窃取临时对象的资源)
// ~String():析构函数(临时对象)
// ~String():析构函数(s)
return 0;
}在开启编译器优化(如 VS 的 Release 模式、GCC 的 - O2 优化)后,编译器会进一步优化,将多次移动合并为一次直接构造,性能更佳。
STL 容器(如 vector、list、map 等)在 C++11 中都新增了支持右值引用的接口(如 push_back、insert),当插入右值对象时,会调用移动构造函数,避免拷贝。
// 容器插入对象的移动语义应用
#include <iostream>
#include <vector>
#include <utility>
using namespace std;
class String {
public:
String(const char* str = "") {
cout << "String(const char*):构造函数" << endl;
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
String(const String& other) {
cout << "String(const String&):拷贝构造函数" << endl;
_size = other._size;
_capacity = other._capacity;
_str = new char[_capacity + 1];
strcpy(_str, other._str);
}
String(String&& other) noexcept {
cout << "String(String&&):移动构造函数" << endl;
_str = other._str;
_size = other._size;
_capacity = other._capacity;
other._str = nullptr;
other._size = 0;
other._capacity = 0;
}
~String() {
cout << "~String():析构函数" << endl;
if (_str) {
delete[] _str;
_str = nullptr;
}
}
private:
char* _str;
size_t _size;
size_t _capacity;
};
int main() {
vector<String> vec;
// 插入左值:调用拷贝构造
String s1("hello");
vec.push_back(s1);
// 输出:
// String(const char*):构造函数(s1)
// String(const String&):拷贝构造函数(容器内对象拷贝s1)
cout << "------------------------" << endl;
// 插入右值:调用移动构造
vec.push_back(String("world"));
// 输出:
// String(const char*):构造函数(临时对象)
// String(String&&):移动构造函数(容器内对象窃取临时对象资源)
// ~String():析构函数(临时对象)
cout << "------------------------" << endl;
// 插入move转换的左值:调用移动构造
vec.push_back(move(s1));
// 输出:
// String(String&&):移动构造函数(容器内对象窃取s1的资源)
return 0;
}C++11 的标准库已经全面支持移动语义,所有容器和算法都进行了优化。例如:
std::vector的push_back、emplace_back支持右值引用;std::string支持移动构造和移动赋值;std::algorithm中的算法会优先使用移动语义(如std::sort)。// 标准库移动语义支持示例
#include <iostream>
#include <vector>
#include <string>
#include <utility>
using namespace std;
int main() {
vector<string> vec;
// 插入字符串右值,调用移动构造
vec.push_back("hello");
vec.push_back(string("world"));
// 移动vector的资源
vector<string> vec2 = move(vec);
cout << "vec.size(): " << vec.size() << endl; // 输出:0(vec的资源已被窃取)
cout << "vec2.size(): " << vec2.size() << endl; // 输出:2(vec2获得资源)
return 0;
}使用移动语义时,需要注意以下几点,避免出现错误:
移动操作后,原对象(右值)会变为空状态(不再拥有资源),此时不能再对其进行操作(除非重新赋值)。如果尝试使用移动后的原对象,会导致未定义行为。
// 移动后的原对象状态
#include <iostream>
#include <string>
#include <utility>
using namespace std;
int main() {
string s1 = "hello";
string s2 = move(s1);
// s1已被移动,变为空字符串
cout << "s1: " << s1 << endl; // 输出:空字符串
cout << "s2: " << s2 << endl; // 输出:hello
// 可以重新赋值给s1
s1 = "world";
cout << "s1: " << s1 << endl; // 输出:world
return 0;
} 移动构造函数和移动赋值运算符应该使用noexcept关键字声明,表明不会抛出异常。这是因为容器(如vector)在扩容时,如果元素的移动构造函数不抛出异常,会使用移动语义;否则会使用拷贝语义,以保证异常安全。
// noexcept关键字的重要性
#include <iostream>
#include <vector>
using namespace std;
class NoExceptMove {
public:
NoExceptMove() {}
NoExceptMove(NoExceptMove&&) noexcept {
cout << "NoExceptMove:移动构造函数(noexcept)" << endl;
}
};
class ThrowMove {
public:
ThrowMove() {}
ThrowMove(ThrowMove&&) {
cout << "ThrowMove:移动构造函数(可能抛出异常)" << endl;
}
};
int main() {
vector<NoExceptMove> vec1;
vec1.reserve(1);
vec1.emplace_back();
vec1.emplace_back(); // 扩容时使用移动构造
vector<ThrowMove> vec2;
vec2.reserve(1);
vec2.emplace_back();
vec2.emplace_back(); // 扩容时使用拷贝构造(因为移动构造可能抛出异常)
return 0;
} std::move本身不会移动任何东西,它只是将左值转换为右值引用,提示编译器可以进行移动操作。滥用std::move会导致对象的资源被意外窃取,引发错误。
// 避免滥用std::move
#include <iostream>
#include <string>
#include <utility>
using namespace std;
int main() {
string s = "hello";
// 错误:move(s)后,s的资源被窃取
string s1 = move(s);
cout << s << endl; // 输出:空字符串(s的资源已被s1窃取)
// 正确:只对即将销毁的对象使用move
string s2 = "world";
string s3 = move(s2); // s2不再使用,移动是安全的
return 0;
}C++11 作为现代 C++ 的基石,其核心价值在于:在保持 C++ 零成本抽象和高性能的传统优势基础上,引入了现代编程语言的核心特性,让 C++ 变得更加安全、高效、易用。本文介绍的列表初始化、值类别体系、引用机制和移动语义,是 C++11 最核心的特性,它们相互关联,共同构成了现代 C++ 编程的基础。 掌握这些特性,不仅能让你写出更高效、更简洁的 C++ 代码,还能帮助你理解后续 C++14、C++17、C++20 等版本的新特性。在实际开发中,建议尽量使用 C++11 及以上版本的特性,充分发挥现代 C++ 的优势。 C++ 的发展从未停止,未来的版本会继续完善语言特性和标准库,让 C++ 在高性能计算、系统开发、游戏开发等领域保持领先地位。作为 C++ 开发者,持续学习现代 C++ 特性,是提升自身竞争力的关键。