C++11 核心特性全解析:,一篇吃透现代 C++ 基石
前言
C++11 是 C++ 语言发展史上里程碑式的版本,作为继 C++98 之后第二个主要版本,它终结了长达 8 年的版本空窗期,彻底重构了 C++ 的编程范式,也为后续 C++14/17/20/23 的迭代奠定了核心基础。
很多开发者对 C++11 的认知停留在auto、lambda这些表层语法上,但实际上,C++11 的核心价值在于解决了 C++98 长期存在的性能痛点、语法不统一、泛型能力不足等问题,真正让 C++ 迈入了 “现代 C++” 的时代。
本文将从原理出发,结合实战代码,系统拆解 C++11 的核心特性,帮你彻底吃透这些改变 C++ 编程方式的新能力。
一、统一初始化:告别混乱的初始化方式
1.1 C++98 的初始化困境
在 C++98 中,初始化规则极其混乱,不同类型的初始化方式完全不统一:
- 普通数组、POD 结构体可以使用花括号
{}进行聚合初始化 - 内置类型、自定义类类型只能用
()或=初始化 - STL 容器想要批量初始化,只能先创建空对象,再循环插入数据
#include <iostream>
#include <vector>
using namespace std;
// POD结构体
struct Point {
int x;
int 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() {
// C++98 支持的初始化方式
int arr1[] = {1, 2, 3, 4, 5}; // 数组聚合初始化
int arr2[5] = {0}; // 数组零初始化
Point p = {1, 2}; // 结构体聚合初始化
// C++98 不支持的方式(编译报错)
// int a {10}; // 内置类型不能用{}
// Date d {2025, 5, 1}; // 自定义类不能用{}
// vector<int> v {1,2,3,4,5}; // 容器不能直接用{}批量初始化
// C++98 容器只能这样初始化
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
return 0;
}
1.2 C++11 统一列表初始化
C++11 引入了列表初始化(Uniform Initialization),核心目标是用一套花括号{}规则,统一所有类型的初始化方式,无论是内置类型、自定义类、数组还是 STL 容器,都可以使用{}完成初始化。
核心规则:
- 所有类型都可以使用
{}进行初始化,可省略赋值符= - 列表初始化会进行严格的类型检查,禁止隐式窄化转换
- 对于自定义类型,底层会先通过
{}内的参数构造临时对象,编译器会通过 RVO 优化为直接构造,无额外拷贝开销
#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;
}
Date(const Date& d)
: _year(d._year), _month(d._month), _day(d._day) {
cout << "Date拷贝构造函数调用" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main() {
// 1. 内置类型初始化
int a1 = {10}; // 带=
int a2 {20}; // 省略=,C++11核心特性
double d {3.14};
// 禁止窄化转换(编译报错)
// int num {3.14}; // double -> int 窄化转换,编译不通过
// 2. 结构体/数组初始化
Point p1 = {1, 2};
Point p2 {3, 4}; // 省略=
int arr[] {1,2,3,4,5};
// 3. 自定义类类型初始化
Date d1 = {2025, 5, 1}; // 构造+拷贝构造,编译器优化为直接构造
Date d2 {2025, 5, 2}; // 直接构造,无拷贝
const Date& d3 = {2025, 5, 3}; // 引用绑定临时对象,延长生命周期
// 4. STL容器初始化(核心便利场景)
vector<int> v1 {1,2,3,4,5}; // 直接批量初始化
map<string, int> dict {
{"苹果", 5},
{"香蕉", 3},
{"橙子", 4}
}; // 键值对容器直接初始化
// 5. 容器插入时的便捷用法
vector<Date> vd;
vd.push_back({2025, 5, 1}); // 无需显式构造匿名对象,{}直接传参
return 0;
}
运行代码可以发现,d1和d2都只调用了构造函数,没有调用拷贝构造,这是编译器的返回值优化(RVO),把临时对象的构造和目标对象的构造合二为一了。
1.3 底层支撑:std::initializer_list
STL 容器能直接用{1,2,3,4,5}初始化,背后的核心就是 C++11 新增的std::initializer_list。
核心原理:
- 当编译器遇到
{x1,x2,x3...}这样的花括号列表时,会自动构造一个std::initializer_list<T>类型的临时对象,其中 T 是列表中元素的类型。 std::initializer_list本质是一个轻量级的代理对象,内部只包含两个指针,分别指向常量数组的首元素和尾后元素,数组本身存储在常量区 / 栈上,拷贝开销极低。- STL 容器都新增了接收
std::initializer_list的构造函数和赋值运算符重载,因此支持花括号列表初始化。
下面我们模拟实现 vector 的initializer_list构造函数,彻底理解其底层逻辑:
#include <iostream>
#include <vector>
using namespace std;
// 模拟实现vector的initializer_list构造函数
template<class T>
class MyVector {
public:
typedef T* iterator;
MyVector() : _start(nullptr), _finish(nullptr), _endofstorage(nullptr) {}
// 接收initializer_list的构造函数
MyVector(initializer_list<T> il)
: _start(nullptr), _finish(nullptr), _endofstorage(nullptr) {
// 遍历initializer_list,逐个插入元素
for (const auto& e : il) {
push_back(e);
}
cout << "MyVector initializer_list构造函数调用" << endl;
}
// 接收initializer_list的赋值运算符重载
MyVector& operator=(initializer_list<T> il) {
clear();
for (const auto& e : il) {
push_back(e);
}
return *this;
}
~MyVector() {
delete[] _start;
_start = _finish = _endofstorage = nullptr;
}
void push_back(const T& val) {
if (_finish == _endofstorage) {
size_t newcapacity = capacity() == 0 ? 4 : capacity() * 2;
reserve(newcapacity);
}
*_finish = val;
_finish++;
}
void reserve(size_t n) {
if (n > capacity()) {
T* tmp = new T[n];
size_t oldsize = size();
for (size_t i = 0; i < oldsize; i++) {
tmp[i] = _start[i];
}
delete[] _start;
_start = tmp;
_finish = _start + oldsize;
_endofstorage = _start + n;
}
}
void clear() {
_finish = _start;
}
size_t size() const { return _finish - _start; }
size_t capacity() const { return _endofstorage - _start; }
iterator begin() { return _start; }
iterator end() { return _finish; }
private:
T* _start;
T* _finish;
T* _endofstorage;
};
int main() {
// 1. initializer_list基本用法
initializer_list<int> il = {1,2,3,4,5};
cout << "il的元素个数:" << il.size() << endl;
// 迭代器遍历
for (auto it = il.begin(); it != il.end(); it++) {
cout << *it << " ";
}
cout << endl;
// 2. 自定义vector使用initializer_list初始化
MyVector<int> mv1 = {1,2,3,4,5};
cout << "mv1的元素:";
for (auto e : mv1) {
cout << e << " ";
}
cout << endl;
// 3. initializer_list赋值
mv1 = {10,20,30,40,50};
cout << "赋值后mv1的元素:";
for (auto e : mv1) {
cout << e << " ";
}
cout << endl;
return 0;
}
二、C++11 的灵魂:右值引用与移动语义
这是 C++11 最核心的特性,没有之一,它从根本上解决了 C++ 长期以来的深拷贝性能损耗问题。
2.1 C++98 的性能痛点:无谓的深拷贝
在 C++98 中,对于带有堆内存资源的类(比如 string、vector),传值返回、值传递参数时,会触发拷贝构造函数,执行深拷贝。而很多时候,拷贝的源对象是临时对象,拷贝完成后就会被销毁,这就导致了 “申请堆空间 -> 拷贝数据 -> 释放源对象堆空间” 的无谓开销,尤其是当对象存储大量数据时,性能损耗极其严重。
#include <iostream>
#include <cstring>
using namespace std;
// C++98风格的string类,只有深拷贝构造
class MyString {
public:
MyString(const char* str = "") {
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
cout << "MyString:构造函数" << endl;
}
// 深拷贝构造函数
MyString(const MyString& s) {
cout << "MyString:深拷贝构造函数" << endl;
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
// 深拷贝赋值运算符
MyString& operator=(const MyString& s) {
cout << "MyString:深拷贝赋值运算符" << endl;
if (this != &s) {
delete[] _str;
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
return *this;
}
~MyString() {
cout << "MyString:析构函数" << endl;
delete[] _str;
_str = nullptr;
}
// 字符串拼接
MyString operator+(const MyString& s) const {
MyString tmp;
tmp._size = _size + s._size;
tmp._capacity = tmp._size;
tmp._str = new char[tmp._capacity + 1];
strcpy(tmp._str, _str);
strcat(tmp._str, s._str);
return tmp;
}
const char* c_str() const { return _str; }
private:
char* _str;
size_t _size;
size_t _capacity;
};
// 返回局部MyString对象
MyString GetString() {
MyString s("hello ");
return s; // 返回局部对象,会生成临时对象
}
int main() {
MyString s1 = GetString(); // 用返回的临时对象拷贝构造s1
MyString s2 = s1 + "world"; // 拼接返回临时对象,拷贝构造s2
cout << "s1: " << s1.c_str() << endl;
cout << "s2: " << s2.c_str() << endl;
return 0;
}
在关闭编译器优化的情况下(g++ 编译时加-fno-elide-constructors),这段代码会发生多次深拷贝,每一次都要申请堆内存、拷贝数据。而这些被拷贝的源对象,都是马上要被销毁的临时对象,我们完全可以直接 “偷” 走它的资源,而不是重新拷贝 —— 这就是移动语义的核心思想。
2.2 左值与右值:核心区别是能否取地址
要理解右值引用,首先要搞清楚左值和右值的本质定义。
左值(lvalue):可以取地址、有持久生命周期的表达式。简单来说,能通过&运算符拿到地址的,就是左值。左值可以出现在赋值号的左边,也可以出现在右边。
- 典型例子:变量名、解引用的指针、const 修饰的常量、数组元素、函数返回的左值引用
右值(rvalue):无法取地址、生命周期短暂的临时表达式。不能通过&运算符拿到地址,只能出现在赋值号的右边,不能出现在左边。
- 典型例子:字面量常量、表达式计算结果、函数返回的传值对象、lambda 表达式
C++11 把右值进一步分为两类:
- 纯右值(prvalue):字面量、表达式求值产生的临时对象,对应 C++98 中的右值概念
- 将亡值(xvalue):即将被销毁、资源可以被转移的对象,比如
std::move(左值)的返回值、函数返回的右值引用
#include <iostream>
#include <string>
using namespace std;
int main() {
// 左值:可以取地址
int a = 10;
int* p = &a;
const int b = 20;
const int* pb = &b; // const左值也能取地址,也是左值
string s("hello");
char* pc = &s[0];
// 以下都是左值,可以取地址
cout << &a << endl;
cout << &b << endl;
cout << &s << endl;
cout << p << endl;
// 右值:无法取地址,编译报错
// cout << &10 << endl; // 字面量10是右值
// cout << &(a + 1) << endl; // 表达式结果是右值
// cout << &(s + " world") << endl; // 临时string对象是右值
return 0;
}
2.3 左值引用与右值引用
C++98 中的引用,在 C++11 中被称为左值引用,用Type&表示,只能给左值取别名;C++11 新增的右值引用,用Type&&表示,只能给右值取别名。
核心规则:
- 左值引用不能直接引用右值,但const 左值引用可以引用右值(C++98 就支持)
- 右值引用不能直接引用左值,但可以通过
std::move(左值)将左值转为右值,从而绑定 - 右值引用绑定到右值后,会延长该右值的生命周期,和右值引用变量的生命周期一致
- 右值引用变量本身是左值(因为可以取地址),这是完美转发的核心前提
#include <iostream>
#include <string>
using namespace std;
int main() {
int a = 10;
double x = 1.1, y = 2.2;
// 1. 左值引用:给左值取别名
int& r1 = a;
// int& r2 = 10; // 错误:左值引用不能直接引用右值
const int& r3 = 10; // 正确:const左值引用可以引用右值
// 2. 右值引用:给右值取别名
int&& rr1 = 10; // 字面量右值
double&& rr2 = x + y; // 表达式结果右值
string&& rr3 = string("hello world"); // 临时对象右值
// int&& rr4 = a; // 错误:右值引用不能直接引用左值
// 3. std::move:将左值转为右值,让右值引用可以绑定
int&& rr5 = move(a);
cout << "move前a = " << a << ", rr5 = " << rr5 << endl;
rr5 = 100;
cout << "move后a = " << a << ", rr5 = " << rr5 << endl;
// 注意:move只是类型转换,本身不移动资源,真正的资源移动是移动构造/赋值做的
// 4. 生命周期延长
// 临时对象string("test")的生命周期被延长,和rr4一致
string&& rr4 = string("test") + " demo";
rr4 += "!"; // 右值引用是非const的,可以修改引用的对象
cout << rr4 << endl;
const string& r4 = string("test") + " demo"; // const左值引用也能延长生命周期,但不能修改
// r4 += "!"; // 错误:const引用不能修改
// 5. 关键:右值引用变量本身是左值
cout << &rr1 << endl; // 可以取地址,说明rr1是左值
int& r6 = rr1; // 正确:左值引用可以绑定rr1(左值)
// int&& rr6 = rr1; // 错误:右值引用不能绑定左值rr1
int&& rr6 = move(rr1); // 正确:move转为右值
return 0;
}
2.4 移动构造与移动赋值:资源窃取,告别深拷贝
有了右值引用,我们就可以实现移动构造函数和移动赋值运算符重载,它们的核心是:接收右值引用参数,直接 “窃取” 源对象的资源,而不是重新申请内存拷贝数据,源对象最后被置为空,不会释放被窃取的资源。
移动构造的核心规则:
- 第一个参数必须是当前类类型的右值引用(
Type&&),其他参数必须有默认值 - 不申请新的堆内存,只转移源对象的资源指针
- 源对象的指针要置空,避免析构时释放被转移的资源
- 对于内置类型成员,直接按字节拷贝即可
现在给之前的 MyString 类加上移动构造和移动赋值,对比性能差异:
#include <iostream>
#include <cstring>
using namespace std;
class MyString {
public:
MyString(const char* str = "") {
_size = strlen(str);
_capacity = _size;
_str = new char[_capacity + 1];
strcpy(_str, str);
cout << "MyString:构造函数" << endl;
}
// 深拷贝构造函数
MyString(const MyString& s) {
cout << "MyString:深拷贝构造函数" << endl;
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
// 移动构造函数
MyString(MyString&& s) noexcept {
cout << "MyString:移动构造函数" << endl;
// 直接窃取源对象的资源,无需深拷贝
_str = s._str;
_size = s._size;
_capacity = s._capacity;
// 源对象置空,避免析构时释放资源
s._str = nullptr;
s._size = 0;
s._capacity = 0;
}
// 深拷贝赋值运算符
MyString& operator=(const MyString& s) {
cout << "MyString:深拷贝赋值运算符" << endl;
if (this != &s) {
delete[] _str;
_size = s._size;
_capacity = s._capacity;
_str = new char[_capacity + 1];
strcpy(_str, s._str);
}
return *this;
}
// 移动赋值运算符
MyString& operator=(MyString&& s) noexcept {
cout << "MyString:移动赋值运算符" << endl;
if (this != &s) {
// 先释放当前对象的资源
delete[] _str;
// 窃取源对象资源
_str = s._str;
_size = s._size;
_capacity = s._capacity;
// 源对象置空
s._str = nullptr;
s._size = 0;
s._capacity = 0;
}
return *this;
}
~MyString() {
cout << "MyString:析构函数" << endl;
delete[] _str; // 源对象_str为nullptr,delete空指针无风险
_str = nullptr;
}
MyString operator+(const MyString& s) const {
MyString tmp;
tmp._size = _size + s._size;
tmp._capacity = tmp._size;
tmp._str = new char[tmp._capacity + 1];
strcpy(tmp._str, _str);
strcat(tmp._str, s._str);
return tmp;
}
const char* c_str() const { return _str; }
private:
char* _str;
size_t _size;
size_t _capacity;
};
MyString GetString() {
MyString s("hello ");
return s;
}
int main() {
cout << "===== 场景1:用临时对象构造新对象 =====" << endl;
MyString s1 = GetString(); // 临时对象是右值,调用移动构造
cout << "s1: " << s1.c_str() << endl;
cout << "\n===== 场景2:用临时对象赋值 =====" << endl;
MyString s2;
s2 = GetString(); // 临时对象是右值,调用移动赋值
cout << "s2: " << s2.c_str() << endl;
cout << "\n===== 场景3:move左值,触发移动 =====" << endl;
MyString s3("world");
MyString s4 = move(s3); // s3被转为右值,调用移动构造
cout << "s4: " << s4.c_str() << endl;
// 注意:s3的资源已经被转移,此时s3是无效状态,不能再使用
cout << "\n===== 场景4:字符串拼接 =====" << endl;
MyString s5 = s1 + s4; // 拼接返回临时对象,调用移动构造
cout << "s5: " << s5.c_str() << endl;
cout << "\n===== 主函数结束,对象析构 =====" << endl;
return 0;
}
在关闭编译器优化的情况下,这段代码中原本的深拷贝都被替换成了移动构造 / 赋值。移动操作没有申请堆内存,只是转移了指针,开销和拷贝内置类型差不多,性能提升极其显著。
2.5 引用折叠与万能引用
在模板编程中,我们会遇到这样的情况:模板参数是T&&,但它既能接收左值,也能接收右值,这就是万能引用(Universal Reference),而支撑它的底层规则就是引用折叠。
引用折叠规则:C++ 不允许直接定义 “引用的引用”,但在模板推导、typedef/using 中会间接产生,此时编译器会按照以下规则折叠:
- 右值引用的右值引用(
T&& &&),折叠为右值引用T&& - 其他所有组合(
T& &、T& &&、T&& &),全部折叠为左值引用T&
万能引用:当模板参数 T 处于推导阶段时,T&&就是万能引用,它会根据传入的实参类型自动推导:
- 如果传入的是左值,T 会被推导为
Type&,结合引用折叠,最终参数类型是Type& &&->Type&(左值引用) - 如果传入的是右值,T 会被推导为
Type,最终参数类型是Type&&(右值引用)
#include <iostream>
using namespace std;
// 万能引用模板
template <typename T>
void Func(T&& t) {
cout << "万能引用调用" << endl;
}
// 普通右值引用,只能接收右值
void Test(int&& t) {
cout << "普通右值引用调用" << endl;
}
int main() {
int a = 10;
const int b = 20;
// 万能引用:既能接收左值,也能接收右值
Func(a); // 传入左值,T推导为int&,参数类型int&
Func(b); // 传入const左值,T推导为const int&,参数类型const int&
Func(10); // 传入右值,T推导为int,参数类型int&&
Func(move(a)); // 传入右值,T推导为int,参数类型int&&
// 普通右值引用:只能接收右值
Test(10); // 正确
// Test(a); // 错误:不能接收左值
return 0;
}
2.6 完美转发:保持值属性的传递
万能引用解决了参数类型的通用接收问题,但还有一个坑:右值引用变量本身是左值。这就导致,当我们把万能引用的参数传递给下一层函数时,它的右值属性会丢失,永远只会匹配左值版本的重载函数。
C++11 提供了std::forward<T>()函数来实现完美转发,它会根据模板参数 T 的类型,保持参数的原始值属性:
- 如果 T 是左值引用类型,参数会被转为左值
- 如果 T 是值类型,参数会被转为右值
#include <iostream>
#include <utility> // forward头文件
using namespace std;
void Fun(int& x) { cout << "左值引用重载" << endl; }
void Fun(const int& x) { cout << "const左值引用重载" << endl; }
void Fun(int&& x) { cout << "右值引用重载" << endl; }
void Fun(const int&& x) { cout << "const右值引用重载" << endl; }
template <typename T>
void Transmit(T&& t) {
Fun(forward<T>(t)); // 完美转发,保持t的原始值属性
}
int main() {
int a = 10;
const int b = 20;
Transmit(a); // 左值,调用左值版本
Transmit(b); // const左值,调用const左值版本
Transmit(10); // 右值,调用右值版本
Transmit(move(a)); // 右值,调用右值版本
return 0;
}
完美转发在 STL 的 emplace 系列接口、线程库、绑定器中都有大量应用,是泛型编程中不可或缺的能力。
三、可变参数模板:让模板支持任意个数参数
C++98 的模板只能固定参数个数和类型,C++11 引入的可变参数模板(Variadic Templates),让模板可以接收任意个数、任意类型的参数,是 C++11 泛型能力的重大升级,也是 STL 中 tuple、emplace 系列接口的底层支撑。
3.1 基本语法
可变参数模板的核心是参数包,分为两种:
- 模板参数包:
typename... Args/class... Args,表示零个或多个模板类型参数 - 函数参数包:
Args... args,表示零个或多个函数参数
sizeof...(args)运算符可以计算参数包中参数的个数。
#include <iostream>
using namespace std;
// 空参数的终止函数
void Print() {
cout << endl;
}
// 可变参数模板:递归展开参数包
template <typename T, typename... Args>
void Print(T first, Args... args) {
cout << first << " "; // 处理第一个参数
Print(args...); // 递归处理剩下的参数包
}
// 计算参数包个数
template <typename... Args>
void CountArgs(Args... args) {
cout << "参数包中参数个数:" << sizeof...(args) << endl;
}
int main() {
Print(1);
Print(1, 2.5, "hello", string("world"));
Print(10, 20, 30, 40, 50);
Print(); // 空参数包
CountArgs(1, 2.5, "hello");
CountArgs();
CountArgs(1,2,3,4,5,6,7,8,9,10);
return 0;
}
3.2 参数包的展开方式
可变参数模板的核心是参数包的展开,主要有两种常用方式:
- 递归展开:每次提取参数包的第一个参数处理,然后递归处理剩下的参数,直到参数包为空,匹配终止函数。
- 逗号表达式展开:利用逗号表达式的执行顺序,在初始化列表中展开参数包,无需递归。
#include <iostream>
using namespace std;
// 辅助函数,处理单个参数
template <typename T>
void HandleArg(const T& arg) {
cout << arg << " ";
}
// 逗号表达式展开参数包
template <typename... Args>
void Print(Args... args) {
// 初始化列表+逗号表达式展开
initializer_list<int> { (HandleArg(args), 0)... };
cout << endl;
}
int main() {
Print(1, 2.5, "hello", string("world"));
Print(10, 20, 30, 40, 50);
return 0;
}
3.3 实战应用:emplace 系列接口
C++11 给 STL 容器新增了 emplace 系列接口,比如emplace_back、emplace,它们的底层就是可变参数模板 + 完美转发,相比 push_back/insert,性能更优。
emplace_back 和 push_back 的核心区别:
- push_back 接收容器元素类型的对象,只能传入已经构造好的对象,会触发拷贝构造或移动构造
- emplace_back 接收构造元素对象的参数包,直接在容器的内存空间上构造对象,无需拷贝 / 移动,零开销
#include <iostream>
#include <string>
#include <utility>
using namespace std;
// 模拟list的节点
template <typename T>
struct ListNode {
T _data;
ListNode* _next;
ListNode* _prev;
// 普通构造
ListNode(const T& data) : _data(data), _next(nullptr), _prev(nullptr) {
cout << "ListNode:拷贝构造" << endl;
}
ListNode(T&& data) : _data(move(data)), _next(nullptr), _prev(nullptr) {
cout << "ListNode:移动构造" << endl;
}
// 可变参数模板构造:接收构造T的参数包,直接构造_data
template <typename... Args>
ListNode(Args&&... args) : _data(forward<Args>(args)...), _next(nullptr), _prev(nullptr) {
cout << "ListNode:直接构造" << endl;
}
};
// 模拟list类
template <typename T>
class MyList {
typedef ListNode<T> Node;
public:
MyList() {
_head = new Node(T());
_head->_next = _head;
_head->_prev = _head;
}
// push_back:接收已构造的对象
void push_back(const T& val) {
Node* newnode = new Node(val);
Node* tail = _head->_prev;
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;
}
void push_back(T&& val) {
Node* newnode = new Node(move(val));
Node* tail = _head->_prev;
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;
}
// emplace_back:可变参数模板+完美转发,接收构造T的参数
template <typename... Args>
void emplace_back(Args&&... args) {
// 直接把参数包传给Node的构造函数,在节点内存上直接构造T对象
Node* newnode = new Node(forward<Args>(args)...);
Node* tail = _head->_prev;
tail->_next = newnode;
newnode->_prev = tail;
newnode->_next = _head;
_head->_prev = newnode;
}
private:
Node* _head;
};
// 测试用的自定义类
class Person {
public:
Person(string name, int age) : _name(name), _age(age) {
cout << "Person:构造函数" << endl;
}
Person(const Person& p) : _name(p._name), _age(p._age) {
cout << "Person:拷贝构造函数" << endl;
}
Person(Person&& p) : _name(move(p._name)), _age(p._age) {
cout << "Person:移动构造函数" << endl;
}
private:
string _name;
int _age;
};
int main() {
MyList<Person> lt;
cout << "===== push_back 左值 =====" << endl;
Person p1("张三", 20);
lt.push_back(p1); // 拷贝构造
cout << "\n===== push_back 右值 =====" << endl;
lt.push_back(Person("李四", 21)); // 构造+移动构造
cout << "\n===== emplace_back =====" << endl;
lt.emplace_back("王五", 22); // 直接构造,无拷贝无移动
return 0;
}
运行结果可以清晰看到,emplace_back 只触发一次 Person 构造,直接在节点内存上构造对象,零额外开销,这也是 C++11 推荐优先使用 emplace_back 代替 push_back 的原因。
四、类的新功能
C++11 对类的默认成员函数、控制能力做了很多增强,让类的设计更灵活、更安全。
4.1 默认移动构造与移动赋值
C++98 中,类有 6 个默认成员函数,C++11 新增了两个默认成员函数:移动构造函数和移动赋值运算符重载。
编译器生成默认移动构造 / 赋值的规则:
- 如果你没有自己实现移动构造 / 赋值,并且没有实现析构函数、拷贝构造、拷贝赋值中的任意一个,编译器会自动生成默认的移动构造 / 赋值。
- 默认生成的移动构造 / 赋值,对内置类型成员按字节拷贝,对自定义类型成员,会调用它的移动构造 / 赋值,如果没有则调用拷贝构造 / 赋值。
- 如果你自己实现了移动构造 / 赋值,编译器不会再自动生成拷贝构造和拷贝赋值。
4.2 类内成员变量默认初始化
C++11 允许在类内声明成员变量时,直接给缺省值,这个缺省值会在初始化列表中使用,如果我们没有在初始化列表中显式初始化该成员,就会用这个缺省值初始化。
#include <iostream>
#include <string>
using namespace std;
class Person {
public:
Person() {} // 无参构造,没有在初始化列表初始化成员,会用缺省值
Person(string name, int age) : _name(name), _age(age) {} // 显式初始化,不用缺省值
void Print() {
cout << "姓名:" << _name << ",年龄:" << _age << ",性别:" << _gender << endl;
}
private:
// 类内成员声明时给缺省值
string _name = "未知";
int _age = 0;
string _gender = "男";
};
int main() {
Person p1;
p1.Print(); // 用缺省值
Person p2("李四", 21);
p2.Print(); // 显式初始化的值
return 0;
}
4.3 default 与 delete:控制默认函数的生成
C++11 提供了default和delete关键字,让我们可以精准控制编译器是否生成默认成员函数。
- default 关键字:显式要求编译器生成该函数的默认版本,即使我们自己实现了其他函数,导致编译器不会自动生成,也可以用 default 强制生成。
- delete 关键字:显式禁止编译器生成该函数,比 C++98 的 “声明为 private 不实现” 更简洁、更安全。
#include <iostream>
using namespace std;
// 禁止拷贝的类
class NoCopy {
public:
NoCopy() = default; // 强制生成默认构造
~NoCopy() = default; // 强制生成默认析构
// 禁止拷贝构造和拷贝赋值
NoCopy(const NoCopy&) = delete;
NoCopy& operator=(const NoCopy&) = delete;
// 强制生成默认移动构造和移动赋值
NoCopy(NoCopy&&) = default;
NoCopy& operator=(NoCopy&&) = default;
};
int main() {
NoCopy nc1;
// NoCopy nc2 = nc1; // 错误:拷贝构造被delete
NoCopy nc3 = move(nc1); // 正确:移动构造被default生成
return 0;
}
4.4 final 与 override:继承与多态的安全控制
- final 关键字:修饰类时,该类不能被继承;修饰虚函数时,该虚函数不能在子类中被重写。
- override 关键字:修饰子类的虚函数,显式告诉编译器这个函数是重写父类的虚函数,编译器会检查函数签名是否和父类一致,不一致会编译报错,避免手滑写错函数名 / 参数导致的隐藏 bug。
五、lambda 表达式:让 C++ 支持匿名函数
C++98 中,可调用对象只有函数指针和仿函数,函数指针语法繁琐,仿函数需要定义类,使用起来很麻烦。C++11 引入的lambda 表达式,让我们可以在代码中定义匿名函数,极大简化了代码,尤其是在算法、回调函数等场景。
5.1 lambda 的语法格式
lambda 表达式的完整格式:
[capture-list] (parameters) mutable -> return-type { function-body }
各部分说明:
- [capture-list] 捕捉列表:必须存在,不能省略,用于捕捉外层作用域的变量,供 lambda 函数体使用。
- (parameters) 参数列表:和普通函数的参数列表一致,无参数时可以省略
()。 - mutable:默认情况下,值捕捉的变量在 lambda 中是 const 的,加 mutable 可以取消 const 属性。
- -> return-type 返回值类型:无返回值或返回值类型明确时可以省略,编译器会自动推导。
- {function-body} 函数体:必须存在,不能为空,实现函数逻辑。
5.2 捕捉列表的用法
捕捉列表是 lambda 的核心,它决定了 lambda 可以访问哪些外层变量,主要分为:
- 值捕捉:
[var],拷贝外层变量 var 的值到 lambda 中。 - 引用捕捉:
[&var],给外层变量 var 取别名,修改会影响外部。 - 隐式值捕捉:
[=],编译器自动捕捉 lambda 中用到的所有外层变量,全部值捕捉。 - 隐式引用捕捉:
[&],编译器自动捕捉 lambda 中用到的所有外层变量,全部引用捕捉。 - 混合捕捉:
[=, &a, &b],默认值捕捉,a 和 b 引用捕捉;[&, a, b],默认引用捕捉,a 和 b 值捕捉。
5.3 lambda 的底层原理
lambda 的本质就是仿函数(函数对象)。编译器在编译时,会把 lambda 表达式转换成一个唯一命名的仿函数类,这个类重载了operator(),捕捉列表的变量会变成仿函数类的成员变量,lambda 的参数、返回值、函数体就是operator()的参数、返回值和实现。
5.4 实战场景:算法中的排序
lambda 最常用的场景之一,就是 STL 算法的自定义规则,相比仿函数,lambda 代码更简洁,可读性更高。
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
using namespace std;
// 商品结构体
struct Goods {
string _name;
double _price;
int _evaluate;
Goods(string name, double price, int evaluate)
: _name(name), _price(price), _evaluate(evaluate) {}
};
int main() {
vector<Goods> v = {
{"苹果", 2.1, 5},
{"香蕉", 3.0, 4},
{"橙子", 2.2, 3},
{"菠萝", 1.5, 4}
};
// 按价格降序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._price > g2._price;
});
cout << "按价格降序:" << endl;
for (auto& g : v) {
cout << g._name << " " << g._price << endl;
}
// 按评价降序
sort(v.begin(), v.end(), [](const Goods& g1, const Goods& g2) {
return g1._evaluate > g2._evaluate;
});
cout << "\n按评价降序:" << endl;
for (auto& g : v) {
cout << g._name << " " << g._evaluate << endl;
}
return 0;
}
六、函数包装器与绑定器
C++11 提供了std::function和std::bind两个工具,分别用于包装可调用对象、调整可调用对象的参数,解决了可调用对象类型不统一、参数不匹配的问题。
6.1 std::function:统一可调用对象类型
std::function是一个可调用对象的包装器,它可以把所有类型兼容的可调用对象(普通函数、函数指针、仿函数、lambda、类成员函数),包装成统一的std::function类型。
std::function是一个类模板,定义在<functional>头文件中,原型如下:
template <class Ret, class... Args>
class function<Ret(Args...)>;
其中Ret是返回值类型,Args...是函数的参数类型列表。
#include <iostream>
#include <functional>
#include <map>
#include <string>
using namespace std;
// 普通函数
int Add(int a, int b) {
return a + b;
}
// 仿函数
struct Sub {
int operator()(int a, int b) {
return a - b;
}
};
int main() {
// 包装不同类型的可调用对象,统一为function<int(int, int)>类型
map<string, function<int(int, int)>> calcMap = {
{"+", Add},
{"-", Sub()},
{"*", [](int a, int b) { return a * b; }},
{"/", [](int a, int b) { return a / b; }}
};
cout << "简易计算器:" << endl;
cout << "10 + 5 = " << calcMap["+"](10, 5) << endl;
cout << "10 - 5 = " << calcMap["-"](10, 5) << endl;
cout << "10 * 5 = " << calcMap["*"](10, 5) << endl;
cout << "10 / 5 = " << calcMap["/"](10, 5) << endl;
return 0;
}
6.2 std::bind:调整可调用对象的参数
std::bind是一个函数适配器,可以把一个可调用对象包装成新的可调用对象,实现调整参数顺序、绑死固定参数的功能。
#include <iostream>
#include <functional>
using namespace std;
using namespace placeholders;
// 原函数
int Sub(int a, int b) {
return a - b;
}
int main() {
// 1. 调整参数顺序
auto sub1 = bind(Sub, _1, _2); // 原顺序:a=_1, b=_2
cout << "10-5=" << sub1(10, 5) << endl;
auto sub2 = bind(Sub, _2, _1); // 调换顺序:a=_2, b=_1
cout << "10-5=" << sub2(10, 5) << endl; // 实际是5-10=-5
// 2. 绑死固定参数,调整参数个数
auto sub3 = bind(Sub, 100, _1); // 绑死a=100,只需要传b
cout << "100-20=" << sub3(20) << endl;
return 0;
}
七、STL 的其他变化
-
新增容器:
unordered_map/unordered_set:基于哈希表的无序关联容器,增删查改平均时间复杂度 O (1)。array:固定大小的数组,比原生数组更安全,支持 STL 接口。forward_list:单向链表,比 list 更节省内存。tuple:元组,支持任意个数、任意类型的元素组合。
-
容器新增接口:
- 所有容器都新增了
initializer_list版本的构造和赋值,支持花括号初始化。 - 所有容器都新增了移动构造和移动赋值,避免整个容器的深拷贝。
- 序列式容器新增了 emplace 系列接口,性能更优。
- 所有容器都新增了
-
范围 for 循环:C++11 新增了范围 for 循环,可以更简洁地遍历容器、数组等可迭代对象,底层就是迭代器实现的。
总结
C++11 不是对 C++98 的小修小补,而是一次彻底的革新,它从语法、性能、泛型能力、编程范式多个维度,重塑了 C++ 语言。
本文讲解的这些核心特性,各有其核心价值:
- 列表初始化 + initializer_list:统一了初始化规则,让代码更简洁、更易读。
- 右值引用 + 移动语义:从根本上解决了深拷贝的性能痛点,让 C++ 程序的性能有了质的提升。
- 可变参数模板 + 完美转发:极大增强了 C++ 的泛型能力,是现代 STL 的底层基石。
- lambda 表达式:让 C++ 支持匿名函数,简化了代码,让函数式编程成为可能。
- function+bind:统一了可调用对象的类型,让回调函数、函数映射的实现更灵活。
- 类的新功能:让类的设计更安全、更灵活,对继承和多态的控制更精准。
掌握这些 C++11 的核心特性,是从 “传统 C++” 迈向 “现代 C++” 的关键一步,也是后续学习 C++14/17/20 新特性的基础。在实际开发中,合理使用这些特性,不仅能大幅提升程序的运行效率,还能让代码更简洁、更易维护、更具现代 C++ 的风格。
更多推荐

所有评论(0)