一、C++11 新特性整体图

C++11 新特性

语法增强

对象管理

泛型编程

标准库增强

并发支持

auto

nullptr

范围 for

lambda

统一初始化

右值引用

移动语义

智能指针

default/delete

decltype

可变参数模板

using 类型别名

array/tuple

unordered_map

function/bind

chrono

thread

mutex

atomic


二、编译 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);

相比直接 newmake_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

手动 lockunlock 容易出问题。
如果中间提前 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 简化代码;
  • nullptrenum classoverride 提高安全性;
  • 用右值引用和移动语义提升性能;
  • 用智能指针减少内存泄漏;
  • threadmutexatomic 支持跨平台并发;
  • chronofunctiontuple 等标准库组件提高开发效率。

如果刚开始学习 C++11,建议优先掌握下面这些内容:

入门优先掌握

auto

范围 for

nullptr

lambda

智能指针

右值引用和移动语义

thread/mutex

其中,auto、范围 for、nullptr、lambda 比较容易上手;智能指针、移动语义、并发编程需要结合实际项目慢慢理解。

C++11 可以说是现代 C++ 的分水岭。学会这些特性之后,再去学习 C++14、C++17、C++20,就会顺很多。

更多推荐