C++11 新特性详解:从“能写”到“写得现代”
一、C++11 新特性整体图
二、编译 C++11 程序
使用 g++ 时,需要加上 -std=c++11:
g++ main.cpp -std=c++11 -o main
如果使用较新的编译器,也可以使用:
g++ main.cpp -std=c++14 -o main
g++ main.cpp -std=c++17 -o main
不过本文主要围绕 C++11 本身来讲。
三、auto:让编译器自动推导类型
1. 为什么需要 auto?
在 C++98 中,迭代器类型往往写起来很长:
std::vector<int> v = {1, 2, 3, 4, 5};
std::vector<int>::iterator it = v.begin();
C++11 可以写成:
auto it = v.begin();
auto 的作用是:让编译器根据右边的表达式自动推导变量类型。
2. auto 的基本用法
int a = 10;
auto b = a; // b 是 int
double d = 3.14;
auto e = d; // e 是 double
std::string s = "hello";
auto str = s; // str 是 std::string
3. auto 常用于迭代器
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it)
{
cout << *it << " ";
}
return 0;
}
比起传统写法:
vector<int>::iterator it = v.begin();
auto 明显更简洁。
4. auto 的注意事项
auto 默认会丢掉引用属性。
int x = 10;
int& r = x;
auto a = r; // a 是 int,不是 int&
a = 20;
cout << x << endl; // 10
如果希望保留引用,需要写:
auto& a = r;
a = 20;
cout << x << endl; // 20
遍历容器时也经常这样写:
for (auto& e : v)
{
e += 1;
}
如果不想修改元素,推荐写:
for (const auto& e : v)
{
cout << e << endl;
}
这样既避免拷贝,又防止误修改。
四、范围 for:更自然地遍历容器
C++11 引入了 range-based for,也就是范围 for。
1. 基本写法
vector<int> v = {1, 2, 3, 4, 5};
for (auto e : v)
{
cout << e << " ";
}
这段代码的意思是:依次取出 v 中的每个元素,赋值给 e。
2. 修改容器中的元素
如果写成这样:
for (auto e : v)
{
e += 10;
}
这里的 e 是元素的拷贝,修改 e 不会影响原容器。
正确写法应该是:
for (auto& e : v)
{
e += 10;
}
3. 只读遍历推荐写法
for (const auto& e : v)
{
cout << e << endl;
}
这种写法很常见,尤其适合遍历 string、自定义对象、容器嵌套结构等。
五、nullptr:替代 NULL 的空指针
在 C++11 之前,空指针一般写成 NULL。
int* p = NULL;
但是 NULL 本质上通常是 0,容易引起函数重载问题。
例如:
void func(int)
{
cout << "int" << endl;
}
void func(int*)
{
cout << "int*" << endl;
}
int main()
{
func(NULL);
return 0;
}
这时可能调用的是 func(int),而不是我们想象中的 func(int*)。
C++11 引入了 nullptr:
int* p = nullptr;
再看重载:
func(nullptr); // 调用 func(int*)
所以在现代 C++ 中,推荐使用:
nullptr
而不是:
NULL
六、统一初始化:用 {} 初始化对象
C++11 引入了统一初始化语法,可以用 {} 初始化变量、数组、对象、容器。
1. 基本类型初始化
int a{10};
double b{3.14};
char c{'x'};
也可以写:
int a = {10};
不过更常见的是:
int a{10};
2. 初始化容器
vector<int> v{1, 2, 3, 4, 5};
map<string, int> m{
{"apple", 1},
{"banana", 2},
{"orange", 3}
};
这种写法比 C++98 方便很多。
3. 防止窄化转换
统一初始化还有一个好处:可以防止一些危险的隐式转换。
int a = 3.14; // 可以通过,a 变成 3
但是:
int a{3.14}; // 编译报错
这能避免很多不易察觉的问题。
七、initializer_list:支持列表初始化的关键
为什么 vector<int> v{1, 2, 3}; 能这样写?
因为 C++11 引入了 std::initializer_list。
#include <iostream>
#include <initializer_list>
using namespace std;
void Print(initializer_list<int> list)
{
for (auto e : list)
{
cout << e << " ";
}
cout << endl;
}
int main()
{
Print({1, 2, 3, 4, 5});
return 0;
}
initializer_list 常用于构造函数:
class Array
{
public:
Array(initializer_list<int> list)
{
for (auto e : list)
{
cout << e << " ";
}
}
};
int main()
{
Array arr{1, 2, 3, 4};
return 0;
}
八、decltype:根据表达式推导类型
auto 是根据变量初始化表达式推导类型,而 decltype 是根据表达式本身推导类型。
int a = 10;
decltype(a) b = 20; // b 是 int
常见用法:
int x = 10;
int& r = x;
decltype(r) y = x; // y 是 int&
decltype 在模板编程中很有用,比如不知道函数返回值类型时:
template<class T, class U>
auto Add(T x, U y) -> decltype(x + y)
{
return x + y;
}
这里的返回值类型由 x + y 的结果决定。
九、lambda 表达式:就地定义匿名函数
lambda 是 C++11 非常重要的新特性。
它可以理解为:在需要函数的地方,直接写一个小函数。
1. 基本语法
[capture](parameters) -> return_type {
function_body;
};
可以简单理解为:
[捕获列表](参数列表) -> 返回值类型 {
函数体
};
2. 最简单的 lambda
auto f = []() {
cout << "hello lambda" << endl;
};
f();
3. 带参数的 lambda
auto add = [](int a, int b) {
return a + b;
};
cout << add(10, 20) << endl;
返回值类型可以省略,编译器会自动推导。
4. lambda 捕获外部变量
int x = 10;
auto f = [x]() {
cout << x << endl;
};
f();
这里 [x] 表示按值捕获变量 x。
5. 按值捕获和按引用捕获
按值捕获
int x = 10;
auto f = [x]() {
cout << x << endl;
};
x = 20;
f(); // 输出 10
按值捕获时,lambda 拿到的是当时的一份拷贝。
按引用捕获
int x = 10;
auto f = [&x]() {
cout << x << endl;
};
x = 20;
f(); // 输出 20
按引用捕获时,lambda 访问的是外部变量本身。
6. 常见捕获方式总结
| 捕获方式 | 含义 |
|---|---|
[] |
不捕获任何变量 |
[x] |
按值捕获 x |
[&x] |
按引用捕获 x |
[=] |
默认按值捕获所有用到的外部变量 |
[&] |
默认按引用捕获所有用到的外部变量 |
[=, &x] |
默认按值捕获,但 x 按引用捕获 |
[&, x] |
默认按引用捕获,但 x 按值捕获 |
7. lambda 常用于排序
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
int main()
{
vector<int> v = {5, 1, 3, 2, 4};
sort(v.begin(), v.end(), [](int a, int b) {
return a > b;
});
for (auto e : v)
{
cout << e << " ";
}
return 0;
}
输出:
5 4 3 2 1
以前写这种逻辑,经常要单独写函数或者函数对象。lambda 出现以后,小逻辑可以直接写在使用的位置,代码可读性更强。
十、右值引用与移动语义
右值引用和移动语义是 C++11 中最核心、也最容易让初学者困惑的内容。
1. 左值和右值
简单理解:
- 左值:有名字、可以取地址、表达式结束后还存在
- 右值:临时对象、表达式结束后通常就销毁
例如:
int a = 10;
a 是左值,10 是右值。
int b = a + 5;
a + 5 是一个临时结果,是右值。
2. 左值引用
C++98 中常见的是左值引用:
int a = 10;
int& r = a;
但下面这样不行:
int& r = 10; // 错误
因为普通左值引用不能直接绑定到右值。
3. 右值引用
C++11 引入了右值引用:
int&& r = 10;
右值引用使用 && 表示,可以绑定到右值。
4. 为什么需要移动语义?
先看一个场景:
vector<int> MakeVector()
{
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
return v;
}
int main()
{
vector<int> ret = MakeVector();
return 0;
}
MakeVector() 返回的是一个临时对象。
如果按照传统思路,可能需要把临时对象中的资源拷贝给 ret,然后临时对象销毁。
但是对于动态资源来说,拷贝可能很贵。
比如一个对象内部有一块很大的堆空间:
char* _data;
拷贝意味着重新开空间、复制数据。
而移动语义的思想是:既然临时对象马上就不用了,不如把它的资源直接“转移”给新对象。
5. 拷贝和移动的区别
拷贝:复制一份资源。
移动:把资源所有权转移过去。
6. std::move
std::move 的作用不是“移动”,而是把一个对象强制转换成右值。
#include <iostream>
#include <utility>
using namespace std;
int main()
{
string s1 = "hello";
string s2 = std::move(s1);
cout << s2 << endl;
cout << s1 << endl;
return 0;
}
执行完移动后,s1 仍然是一个合法对象,但它的内容处于一种“有效但未指定”的状态。
简单说:可以继续析构、重新赋值,但不要依赖它原来的内容。
7. 移动构造函数
class String
{
public:
String(const char* str = "")
{
_size = strlen(str);
_data = new char[_size + 1];
strcpy(_data, str);
}
// 拷贝构造
String(const String& s)
{
_size = s._size;
_data = new char[_size + 1];
strcpy(_data, s._data);
}
// 移动构造
String(String&& s)
{
_data = s._data;
_size = s._size;
s._data = nullptr;
s._size = 0;
}
~String()
{
delete[] _data;
}
private:
char* _data;
size_t _size;
};
移动构造的核心是:
_data = s._data;
s._data = nullptr;
也就是说,把对方的资源拿过来,然后让对方不再管理这块资源,避免析构时重复释放。
十一、智能指针:让资源自动释放
C++ 中最容易出问题的地方之一就是手动管理内存。
传统写法:
int* p = new int(10);
// 中间如果 return 或抛异常,delete 可能执行不到
delete p;
C++11 提供了智能指针,用对象的生命周期来管理资源。
需要包含头文件:
#include <memory>
1. unique_ptr
unique_ptr 表示独占所有权。
同一时间只能有一个 unique_ptr 管理某个资源。
#include <iostream>
#include <memory>
using namespace std;
int main()
{
unique_ptr<int> p(new int(10));
cout << *p << endl;
return 0;
}
离开作用域时,p 会自动释放资源。
unique_ptr 不能拷贝:
unique_ptr<int> p1(new int(10));
unique_ptr<int> p2 = p1; // 错误
但是可以移动:
unique_ptr<int> p1(new int(10));
unique_ptr<int> p2 = std::move(p1);
移动后,资源归 p2 管理,p1 不再拥有资源。
2. shared_ptr
shared_ptr 表示共享所有权。
多个 shared_ptr 可以共同管理同一块资源,内部通过引用计数判断什么时候释放资源。
shared_ptr<int> p1(new int(10));
shared_ptr<int> p2 = p1;
cout << p1.use_count() << endl; // 2
cout << p2.use_count() << endl; // 2
当最后一个 shared_ptr 被销毁时,资源才会释放。
更推荐使用:
auto p = make_shared<int>(10);
相比直接 new,make_shared 更安全,也更高效。
3. weak_ptr
weak_ptr 是为了解决 shared_ptr 循环引用问题的。
假设有两个类互相保存对方的 shared_ptr:
struct A;
struct B;
struct A
{
shared_ptr<B> _b;
};
struct B
{
shared_ptr<A> _a;
};
如果 A 和 B 互相引用,即使外部指针销毁,它们的引用计数也不会变成 0,资源就无法释放。
解决方法是把其中一方改成 weak_ptr:
struct A;
struct B;
struct A
{
shared_ptr<B> _b;
};
struct B
{
weak_ptr<A> _a;
};
weak_ptr 不增加引用计数,只是弱引用。
4. 三种智能指针对比
| 智能指针 | 所有权模型 | 是否能拷贝 | 常见用途 |
|---|---|---|---|
unique_ptr |
独占所有权 | 不可以 | 独占资源管理 |
shared_ptr |
共享所有权 | 可以 | 多对象共享资源 |
weak_ptr |
弱引用 | 可以 | 解决循环引用 |
十二、default 和 delete
C++11 可以显式要求编译器生成默认函数,也可以禁止某些函数生成。
1. default
class Person
{
public:
Person() = default;
};
= default 表示使用编译器默认生成的版本。
常见场景:
class Person
{
public:
Person() = default;
Person(const string& name)
: _name(name)
{}
private:
string _name;
};
如果自己写了带参构造,编译器就不会自动生成默认构造。
这时可以用 = default 显式生成。
2. delete
= delete 表示禁止使用某个函数。
例如禁止拷贝:
class CopyBan
{
public:
CopyBan() = default;
CopyBan(const CopyBan&) = delete;
CopyBan& operator=(const CopyBan&) = delete;
};
这样外部就不能拷贝对象:
CopyBan a;
CopyBan b(a); // 编译报错
在 C++11 之前,通常要把拷贝构造声明成 private。
C++11 的 delete 写法更直观。
十三、override 和 final
1. override
在继承中,如果子类想重写虚函数,推荐加上 override。
class Base
{
public:
virtual void Print()
{
cout << "Base" << endl;
}
};
class Derive : public Base
{
public:
void Print() override
{
cout << "Derive" << endl;
}
};
override 的好处是:让编译器帮你检查是否真的重写了父类虚函数。
例如:
class Base
{
public:
virtual void Print(int x)
{}
};
class Derive : public Base
{
public:
void Print() override
{}
};
这里参数不一致,并没有真正构成重写。
加了 override 后,编译器会直接报错。
2. final
final 可以禁止类被继承,或者禁止虚函数继续被重写。
禁止类被继承:
class A final
{
};
禁止虚函数继续重写:
class Base
{
public:
virtual void Func() final
{}
};
十四、enum class:强类型枚举
传统枚举有两个问题:
第一,枚举值会污染外层作用域。
enum Color
{
RED,
GREEN,
BLUE
};
enum Fruit
{
APPLE,
RED // 冲突
};
第二,传统枚举可以隐式转换成整数。
Color c = RED;
int x = c; // 可以转换
C++11 引入了 enum class:
enum class Color
{
RED,
GREEN,
BLUE
};
使用时必须带上作用域:
Color c = Color::RED;
不能直接隐式转成整数:
int x = c; // 错误
如果确实需要转换:
int x = static_cast<int>(c);
enum class 更安全,也更适合工程代码。
十五、constexpr:编译期常量计算
constexpr 表示一个值或函数可以在编译期求值。
constexpr int Add(int a, int b)
{
return a + b;
}
int arr[Add(2, 3)];
这里 Add(2, 3) 可以在编译期计算,结果是 5。
constexpr 和 const 的区别
const 表示只读,不一定是编译期常量。
int n;
cin >> n;
const int x = n; // x 是只读变量,但不是编译期常量
constexpr 更强调“编译期就能确定”。
constexpr int x = 10;
C++11 中的 constexpr 函数限制比较多,函数体通常只能写得比较简单。后续 C++14、C++17 又逐渐放宽了限制。
十六、static_assert:编译期断言
普通 assert 是运行时检查:
assert(p != nullptr);
C++11 引入了 static_assert,用于编译期检查:
static_assert(sizeof(int) == 4, "int must be 4 bytes");
如果条件不成立,编译阶段就会报错。
在模板中经常使用:
template<class T>
void Func(T t)
{
static_assert(sizeof(T) <= 8, "T is too large");
}
十七、using 类型别名
C++98 中常用 typedef 起别名:
typedef unsigned int uint;
C++11 可以使用 using:
using uint = unsigned int;
对于复杂类型,using 更清晰:
typedef void (*FuncPtr)(int, int);
可以写成:
using FuncPtr = void (*)(int, int);
在模板中,using 也比 typedef 更好用。
template<class T>
using Vec = std::vector<T>;
Vec<int> v;
十八、可变参数模板
C++11 支持模板参数个数可变。
template<class... Args>
void Print(Args... args)
{
}
Args... 表示模板参数包。args... 表示函数参数包。
一个简单例子:
#include <iostream>
using namespace std;
void Print()
{
cout << endl;
}
template<class T, class... Args>
void Print(T first, Args... rest)
{
cout << first << " ";
Print(rest...);
}
int main()
{
Print(1, 2.5, "hello", 'x');
return 0;
}
输出:
1 2.5 hello x
可变参数模板是很多现代库的基础,比如智能指针创建、线程库、函数包装器等。
十九、STL 容器增强
C++11 标准库也增加了不少实用容器。
1. array
std::array 是固定大小数组的封装。
#include <array>
#include <iostream>
using namespace std;
int main()
{
array<int, 5> arr = {1, 2, 3, 4, 5};
for (auto e : arr)
{
cout << e << " ";
}
return 0;
}
相比原生数组,array 更像 STL 容器,支持:
arr.size();
arr.begin();
arr.end();
2. forward_list
forward_list 是单链表。
#include <forward_list>
forward_list<int> fl = {1, 2, 3, 4};
它只支持单向遍历,占用空间比 list 更小。
3. unordered_map 和 unordered_set
C++11 引入了哈希容器:
#include <unordered_map>
#include <unordered_set>
例如:
unordered_map<string, int> count;
count["apple"]++;
count["banana"]++;
count["apple"]++;
cout << count["apple"] << endl; // 2
map 底层通常是红黑树,元素有序。unordered_map 底层是哈希表,元素无序,但平均查找效率更高。
| 容器 | 底层结构 | 是否有序 | 平均查找效率 |
|---|---|---|---|
map |
红黑树 | 有序 | O(logN) |
unordered_map |
哈希表 | 无序 | O(1) |
4. tuple
tuple 可以存放多个不同类型的数据。
#include <tuple>
#include <iostream>
using namespace std;
int main()
{
tuple<int, string, double> t(1, "hello", 3.14);
cout << get<0>(t) << endl;
cout << get<1>(t) << endl;
cout << get<2>(t) << endl;
return 0;
}
pair 只能放两个值,tuple 可以放多个值。
二十、function 和 bind
1. function
std::function 是一个通用的函数包装器。
它可以包装普通函数、函数指针、lambda、函数对象等。
#include <iostream>
#include <functional>
using namespace std;
int Add(int a, int b)
{
return a + b;
}
int main()
{
function<int(int, int)> f = Add;
cout << f(10, 20) << endl;
return 0;
}
包装 lambda:
function<int(int, int)> f = [](int a, int b) {
return a + b;
};
2. bind
std::bind 可以绑定函数参数。
#include <iostream>
#include <functional>
using namespace std;
int Add(int a, int b)
{
return a + b;
}
int main()
{
auto f = bind(Add, 10, placeholders::_1);
cout << f(20) << endl; // Add(10, 20)
return 0;
}
不过在实际开发中,很多场景下 lambda 比 bind 更直观。
auto f = [](int x) {
return Add(10, x);
};
二十一、线程库:C++ 标准层面的并发支持
C++11 之前,不同平台使用不同的线程库。
Linux 常用 pthread,Windows 有自己的线程 API。
C++11 开始,标准库正式提供线程支持。
1. thread
#include <iostream>
#include <thread>
using namespace std;
void Work()
{
cout << "子线程执行" << endl;
}
int main()
{
thread t(Work);
t.join();
return 0;
}
t.join() 表示等待子线程执行结束。
2. lambda 创建线程
thread t([]() {
cout << "hello thread" << endl;
});
t.join();
3. mutex
多线程访问共享资源时,需要加锁。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int g_count = 0;
mutex mtx;
void Add()
{
for (int i = 0; i < 100000; ++i)
{
mtx.lock();
++g_count;
mtx.unlock();
}
}
int main()
{
thread t1(Add);
thread t2(Add);
t1.join();
t2.join();
cout << g_count << endl;
return 0;
}
4. lock_guard
手动 lock 和 unlock 容易出问题。
如果中间提前 return 或抛异常,就可能忘记解锁。
C++11 提供了 lock_guard:
void Add()
{
for (int i = 0; i < 100000; ++i)
{
lock_guard<mutex> lock(mtx);
++g_count;
}
}
lock_guard 构造时加锁,析构时自动解锁。
这就是 RAII 思想:资源交给对象管理。
5. atomic
对于简单的整数原子操作,可以使用 atomic。
#include <iostream>
#include <thread>
#include <atomic>
using namespace std;
atomic<int> g_count(0);
void Add()
{
for (int i = 0; i < 100000; ++i)
{
++g_count;
}
}
int main()
{
thread t1(Add);
thread t2(Add);
t1.join();
t2.join();
cout << g_count << endl;
return 0;
}
atomic 可以保证简单操作的原子性,避免数据竞争。
二十二、chrono:时间库
C++11 提供了 <chrono> 时间库,用来表示时间点、时间间隔。
#include <iostream>
#include <chrono>
#include <thread>
using namespace std;
int main()
{
auto start = chrono::steady_clock::now();
this_thread::sleep_for(chrono::seconds(2));
auto end = chrono::steady_clock::now();
auto cost = chrono::duration_cast<chrono::milliseconds>(end - start);
cout << "耗时: " << cost.count() << " ms" << endl;
return 0;
}
常见时间单位:
| 类型 | 含义 |
|---|---|
chrono::seconds |
秒 |
chrono::milliseconds |
毫秒 |
chrono::microseconds |
微秒 |
chrono::nanoseconds |
纳秒 |
二十三、noexcept:声明函数不会抛异常
C++11 引入了 noexcept,表示函数不会抛出异常。
void Func() noexcept
{
}
它的作用不只是说明,还可能影响性能优化。
比如 STL 容器在扩容时,如果元素的移动构造是 noexcept,就更倾向于使用移动而不是拷贝。
例如:
class String
{
public:
String(String&& s) noexcept
{
// 移动资源
}
};
移动构造函数通常建议加上 noexcept。
二十四、委托构造函数
C++11 允许一个构造函数调用另一个构造函数。
class Date
{
public:
Date()
: Date(1970, 1, 1)
{}
Date(int year, int month, int day)
: _year(year), _month(month), _day(day)
{}
private:
int _year;
int _month;
int _day;
};
这样可以减少重复初始化代码。
二十五、类内成员初始化
C++11 允许在类中直接给成员变量默认值。
class Student
{
private:
string _name = "unknown";
int _age = 0;
};
以前通常要在构造函数初始化列表中写:
Student()
: _name("unknown")
, _age(0)
{}
类内成员初始化能让默认值更集中、更直观。
二十六、总结
C++11 的意义不只是新增了一批语法,而是让 C++ 的写法发生了明显变化。
以前写 C++,很多地方需要手动管理,比如手动写类型、手动释放内存、手动控制线程 API。C++11 之后,语言和标准库提供了更现代的工具:
- 用
auto、范围 for、lambda 简化代码; - 用
nullptr、enum class、override提高安全性; - 用右值引用和移动语义提升性能;
- 用智能指针减少内存泄漏;
- 用
thread、mutex、atomic支持跨平台并发; - 用
chrono、function、tuple等标准库组件提高开发效率。
如果刚开始学习 C++11,建议优先掌握下面这些内容:
其中,auto、范围 for、nullptr、lambda 比较容易上手;智能指针、移动语义、并发编程需要结合实际项目慢慢理解。
C++11 可以说是现代 C++ 的分水岭。学会这些特性之后,再去学习 C++14、C++17、C++20,就会顺很多。
更多推荐



所有评论(0)