C++11:现代C++的里程碑

C++11:现代C++的里程碑

前言

C++11是C++语言发展史上的一个重要里程碑。从C++98到C++11,我们等了整整13年。C++11带来了约140个新特性,修正了约600个缺陷,让C++焕发出了新的生命力。本文将系统地介绍C++11中最实用、最常用的新特性,帮助大家快速掌握现代C++的精髓。


一、C++11简介

在2003年,C++标准委员会提交了一份技术勘误表(TC1),形成了C++03标准。但C++03主要是对C++98的漏洞修复,核心语言没有改动,所以人们习惯将两者合称为C++98/03标准。

C++11最初被命名为C++0x,其中"x"代表不确定的发布年份。从最初的C++07到C++08、C++09,最终在2011年才正式完成,因此定名为C++11。

相比于C++98/03,C++11更像是一门新语言:语法更加泛化和简化、更加稳定和安全、功能更强大,能显著提升程序员的开发效率。


二、统一的列表初始化

2.1 {}初始化

在C++98中,花括号{}只能用于数组或结构体的初始化。C++11大大扩展了它的使用范围:

cpp 复制代码
// C++98的用法
int array1[] = {1, 2, 3, 4, 5};
struct Point { int x; int y; };
Point p = {1, 2};

// C++11扩展的用法
int x1{2};                    // 内置类型
int array2[]{1, 2, 3, 4, 5};  // 数组
Point p2{1, 2};               // 结构体
int* pa = new int[4]{0};      // new表达式

// 自定义类型也可以使用列表初始化
class Date {
public:
    Date(int year, int month, int day) : _year(year), _month(month), _day(day) {}
private:
    int _year, _month, _day;
};

Date d1{2022, 1, 1};      // 直接列表初始化
Date d2 = {2022, 1, 2};   // 拷贝列表初始化

2.2 std::initializer_list

std::initializer_list是C++11新增的类型,它让容器能够支持列表初始化:

cpp 复制代码
// 查看initializer_list的类型
auto il = {10, 20, 30};
cout << typeid(il).name() << endl;  // class std::initializer_list<int>

// STL容器支持列表初始化
vector<int> v = {1, 2, 3, 4};
list<int> lt = {1, 2};
map<string, string> dict = {{"sort", "排序"}, {"insert", "插入"}};

// 赋值也可以使用大括号
v = {10, 20, 30};

模拟实现支持initializer_list的vector:

cpp 复制代码
namespace aramae {
    template<class T>
    class vector {
    public:
        vector(initializer_list<T> l) {
            _start = new T[l.size()];
            _finish = _start + l.size();
            _endofstorage = _start + l.size();
            T* vit = _start;
            for (auto e : l) {
                *vit++ = e;
            }
        }
        
        vector<T>& operator=(initializer_list<T> l) {
            vector<T> tmp(l);
            swap(_start, tmp._start);
            swap(_finish, tmp._finish);
            swap(_endofstorage, tmp._endofstorage);
            return *this;
        }
    private:
        T* _start;
        T* _finish;
        T* _endofstorage;
    };
}

std::initializer_list

本质上是编译器生成的"栈内存或静态内存的切片(Slice)"。它绝不动态分配堆内存,因此它的生命周期完全依赖于它所在的作用域(或静态存储期),绝不拥有数据的所有权,仅用于极轻量级的只读传递。


三、声明简化

3.1 auto

C++98中的auto是存储类型说明符,因为几乎没有实际价值而被废弃。C++11赋予了它全新的含义------自动类型推导:

cpp 复制代码
int i = 10;
auto p = &i;                // p的类型是int*
auto pf = strcpy;           // pf的类型是函数指针

map<string, string> dict = {{"sort", "排序"}, {"insert", "插入"}};
auto it = dict.begin();     // 自动推导为 map<string,string>::iterator

// auto必须显式初始化
auto x;                     // 编译错误

3.2 decltype

decltype关键字用于声明表达式的类型,在模板编程中非常有用:

cpp 复制代码
// 基本使用
const int x = 1;
double y = 2.2;
decltype(x * y) ret;        // ret的类型是double
decltype(&x) p;             // p的类型是const int*

// 在模板中的使用
template<class T1, class T2>
void F(T1 t1, T2 t2) {
    decltype(t1 * t2) ret;  // 推导出t1*t2的类型
    cout << typeid(ret).name() << endl;
}

3.3 nullptr

由于C++中NULL被定义为字面量0,可能带来指针和整数的歧义问题。C++11引入了nullptr表示空指针:

cpp 复制代码
// NULL的定义(在C++中)
#define NULL 0

void func(int) { cout << "int" << endl; }
void func(void*) { cout << "void*" << endl; }

func(NULL);    // 调用func(int),可能不是我们期望的
func(nullptr); // 调用func(void*),明确表示空指针

四、范围for循环

范围for循环是C++11中非常实用的语法糖:

cpp 复制代码
vector<int> v = {1, 2, 3, 4, 5};

// 只读访问
for (auto e : v) {
    cout << e << " ";
}

// 修改元素
for (auto& e : v) {
    e *= 2;
}

// C++17之后支持结构化绑定
map<string, int> m = {{"apple", 1}, {"banana", 2}};
for (auto& [key, value] : m) {
    cout << key << ": " << value << endl;
}

五、智能指针

C++11引入了三种智能指针来管理动态内存,从根本上解决了内存泄漏问题:

智能指针 特点 使用场景
unique_ptr 独占所有权,不能拷贝 明确独占资源所有权
shared_ptr 共享所有权,引用计数 多个对象共享同一资源
weak_ptr 弱引用,不增加引用计数 解决shared_ptr循环引用
cpp 复制代码
// 基本使用
unique_ptr<int> up1(new int(10));
unique_ptr<int> up2 = make_unique<int>(20);  // C++14

shared_ptr<int> sp1 = make_shared<int>(30);
shared_ptr<int> sp2 = sp1;  // 引用计数增加

六、STL中的新变化

6.1 新增容器

C++11新增了几个重要的容器:

  • array:固定大小的数组容器,比原生数组更安全
  • forward_list:单向链表,比list更节省内存
  • unordered_map / unordered_set:基于哈希表实现的无序关联容器,查找效率O(1)
cpp 复制代码
// array的使用
array<int, 5> arr = {1, 2, 3, 4, 5};
cout << arr.size() << endl;  // 5
cout << arr[0] << endl;      // 1

// unordered_map的使用
unordered_map<string, int> um;
um["apple"] = 1;
um["banana"] = 2;

6.2 容器的新方法

大部分容器增加了cbegin()/cend()返回const迭代器,以及emplace系列函数:

cpp 复制代码
vector<int> v = {1, 2, 3, 4};

// const迭代器
auto cit = v.cbegin();

// emplace_back:直接在容器中构造对象,避免拷贝
struct Person {
    string name;
    int age;
    Person(string n, int a) : name(n), age(a) {}
};

vector<Person> persons;
persons.emplace_back("Alice", 25);  // 直接构造
persons.push_back(Person("Bob", 30)); // 先构造再拷贝

七、右值引用和移动语义

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。

什么是右值?什么是右值引用

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。

需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。

7.1 左值与右值

左值 :可以取地址、可以出现在赋值号左边的表达式。

右值:不能取地址、不能出现在赋值号左边的表达式。

cpp 复制代码
int a = 10;        // a是左值,10是右值
int* p = &a;       // &a合法
// int* p2 = &10;  // 错误:不能对右值取地址

int b = a;         // a是左值
int c = a + b;     // a+b是右值

7.2 左值引用与右值引用

  • 左值引用总结:
  • 左值引用只能引用左值,不能引用右值。
  • 但是const左值引用既可引用左值,也可引用右值
  • 右值引用总结:
  • 右值引用只能右值,不能引用左值。
  • 但是右值引用可以move以后的左值。
cpp 复制代码
// 左值引用
int a = 10;
int& ra = a;       // ra是左值引用
const int& cra = 10; // const左值引用可以绑定右值

// 右值引用
int&& rr1 = 10;    // rr1是右值引用
int&& rr2 = a + b; // 绑定临时对象
// int&& rr3 = a;  // 错误:右值引用不能绑定左值
int&& rr4 = move(a); // 使用move将左值转为右值

左值引用的短板:

但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,

只能传值返回。例如:bit::string to_string(int value)函数中可以看到,这里只能使用传值返回,

传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)。

右值引用和移动语义解决上述问题:

在bit::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不

用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

7.3 移动语义的意义

移动语义通过"资源转移"而非"资源拷贝"来提高性能:

cpp 复制代码
class MyString {
public:
    // 移动构造:窃取资源,避免深拷贝
    MyString(MyString&& other) noexcept 
        : _str(other._str), _size(other._size) {
        other._str = nullptr;
        other._size = 0;
    }
    
    // 移动赋值
    MyString& operator=(MyString&& other) noexcept {
        if (this != &other) {
            delete[] _str;
            _str = other._str;
            _size = other._size;
            other._str = nullptr;
            other._size = 0;
        }
        return *this;
    }
private:
    char* _str;
    size_t _size;
};

按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?因为:有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于 头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。

STL容器插入接口函数也增加了右值引用版本:

http://www.cplusplus.com/reference/list/list/push_back/

http://www.cplusplus.com/reference/vector/vector/push_back/

7.4 完美转发

std::forward在模板中保持参数的左右值属性:

cpp 复制代码
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }

template<typename T>
void PerfectForward(T&& t) {
    Fun(std::forward<T>(t));  // 完美转发
}

int main() {
    int a = 10;
    PerfectForward(10);  // 右值引用
    PerfectForward(a);   // 左值引用
    PerfectForward(move(a)); // 右值引用
}

八、新的类功能

默认成员函数

原来C++类中,有6个默认成员函数:

  1. 构造函数
  2. 析构函数
  3. 拷贝构造函数
  4. 拷贝赋值重载
  5. 取地址重载
  6. const 取地址重载
    最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
    C++11 新增了两个:移动构造函数和移动赋值运算符重载。
    针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
    如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。

8.1 默认成员函数控制

C++11允许显式控制默认函数的生成和删除:

cpp 复制代码
class Person {
public:
    // 强制生成移动构造
    Person(Person&& p) = default;
    
    // 禁止拷贝构造
    Person(const Person& p) = delete;
    
    // 禁止拷贝赋值
    Person& operator=(const Person& p) = delete;
};

8.2 成员变量默认初始化

C++11允许在类定义时给成员变量设置默认值:

cpp 复制代码
class Person {
private:
    string _name = "Unknown";  // 默认值
    int _age = 0;
    vector<int> _scores = {1, 2, 3};
};

九、可变参数模板

可变参数模板允许函数或类接受任意数量的模板参数:

cpp 复制代码
// 递归展开参数包
template<class T>
void ShowList(const T& t) {
    cout << t << endl;
}

template<class T, class ...Args>
void ShowList(T value, Args... args) {
    cout << value << " ";
    ShowList(args...);  // 递归调用
}

// 使用
ShowList(1, 'A', "hello", 3.14);

STL容器中的emplace系列函数就是利用可变参数模板实现的:

cpp 复制代码
list<pair<int, string>> mylist;
mylist.emplace_back(1, "apple");  // 直接构造pair
mylist.push_back({2, "banana"});  // 先构造再移动

十、lambda表达式

10.1 基本语法

lambda表达式的格式:[capture-list] (parameters) mutable -> return-type { statement }

cpp 复制代码
// 最简单的lambda
[]{};  // 什么都不做

// 基本使用
auto add = [](int a, int b) -> int { return a + b; };
cout << add(3, 4) << endl;  // 7

// 省略返回值类型(编译器推导)
auto mul = [](int a, int b) { return a * b; };

// 捕获变量
int x = 10;
auto f1 = [x](int a) { return a + x; };      // 值捕获
auto f2 = [&x](int a) { return a + x; };     // 引用捕获
auto f3 = [=](int a) { return a + x; };      // 值捕获所有
auto f4 = [&](int a) { return a + x; };      // 引用捕获所有

10.2 实际应用

cpp 复制代码
// 排序
vector<Goods> goods = {...};
sort(goods.begin(), goods.end(),
     [](const Goods& g1, const Goods& g2) {
         return g1._price < g2._price;
     });

// 与STL算法配合
vector<int> v = {1, 2, 3, 4, 5};
int sum = 0;
for_each(v.begin(), v.end(), [&sum](int n) { sum += n; });

10.3 lambda的实现原理

lambda表达式在底层会被编译器转换为一个函数对象(仿函数)类:

cpp 复制代码
// 源码中的lambda
auto lambda = [x](int a) { return a + x; };

// 编译器生成的等价代码
class __lambda {
private:
    int x;  // 捕获的变量
public:
    __lambda(int _x) : x(_x) {}
    auto operator()(int a) const { return a + x; }
};

十一、包装器

11.1 function包装器

std::function是一个通用的可调用对象包装器:

cpp 复制代码
#include <functional>

// 普通函数
int f(int a, int b) { return a + b; }

// 函数对象
struct Functor {
    int operator()(int a, int b) { return a + b; }
};

// 类成员函数
class Plus {
public:
    static int plusi(int a, int b) { return a + b; }
    double plusd(double a, double b) { return a + b; }
};

int main() {
    // 包装普通函数
    function<int(int, int)> func1 = f;
    cout << func1(1, 2) << endl;
    
    // 包装函数对象
    function<int(int, int)> func2 = Functor();
    cout << func2(1, 2) << endl;
    
    // 包装lambda
    function<int(int, int)> func3 = [](int a, int b) { return a + b; };
    
    // 包装静态成员函数
    function<int(int, int)> func4 = &Plus::plusi;
    
    // 包装非静态成员函数(需要对象)
    function<double(Plus, double, double)> func5 = &Plus::plusd;
    cout << func5(Plus(), 1.1, 2.2) << endl;
}

function解决模板效率问题:

cpp 复制代码
// 不使用function:模板会实例化多份
template<class F, class T>
T useF(F f, T x) {
    static int count = 0;
    cout << "count:" << ++count << endl;
    return f(x);
}

// 使用function:只实例化一份
double f(double i) { return i / 2; }
struct Functor { double operator()(double d) { return d / 3; } };

int main() {
    // 每次都实例化不同的模板
    useF(f, 11.11);
    useF(Functor(), 11.11);
    useF([](double d){ return d/4; }, 11.11);
    
    // 使用function包装,只实例化一次
    function<double(double)> func1 = f;
    function<double(double)> func2 = Functor();
    function<double(double)> func3 = [](double d){ return d/4; };
    useF(func1, 11.11);
    useF(func2, 11.11);
    useF(func3, 11.11);
}

11.2 bind绑定器

std::bind用于绑定函数参数,生成新的可调用对象:

cpp 复制代码
int Plus(int a, int b) { return a + b; }

class Sub {
public:
    int sub(int a, int b) { return a - b; }
};

int main() {
    using namespace placeholders;
    
    // 绑定所有参数
    auto func1 = bind(Plus, 1, 2);
    cout << func1() << endl;  // 3
    
    // 使用占位符
    auto func2 = bind(Plus, _1, _2);
    cout << func2(1, 2) << endl;  // 3
    
    // 调整参数顺序
    auto func3 = bind(Plus, _2, _1);
    cout << func3(1, 2) << endl;  // 3 (相当于 Plus(2, 1))
    
    // 绑定成员函数
    Sub s;
    auto func4 = bind(&Sub::sub, s, _1, _2);
    cout << func4(5, 3) << endl;  // 2
    
    // 固定部分参数
    auto func5 = bind(Plus, 10, _1);
    cout << func5(5) << endl;  // 15
}

十二、线程库

C++11正式将线程支持纳入标准库,使多线程编程不再依赖平台API。

12.1 thread类

cpp 复制代码
#include <thread>

// 线程函数:函数指针
void ThreadFunc(int a) {
    cout << "Thread: " << a << endl;
}

// 线程函数:函数对象
class TF {
public:
    void operator()() {
        cout << "Thread function object" << endl;
    }
};

int main() {
    // 方式1:函数指针
    thread t1(ThreadFunc, 10);
    
    // 方式2:lambda表达式
    thread t2([](int a) { cout << "Thread: " << a << endl; }, 20);
    
    // 方式3:函数对象
    TF tf;
    thread t3(tf);
    
    // 等待线程结束
    t1.join();
    t2.join();
    t3.join();
    
    cout << "Main thread done!" << endl;
    return 0;
}

12.2 线程参数传递

线程函数参数以值拷贝方式传递,需要使用std::ref传递引用:

cpp 复制代码
void Func(int& x) {
    x += 10;
}

int main() {
    int a = 10;
    
    // 错误:无法修改外部a
    // thread t1(Func, a);
    
    // 使用ref传递引用
    thread t2(Func, std::ref(a));
    t2.join();
    cout << a << endl;  // 20
    
    // 成员函数作为线程函数,需要传递this
    class MyClass {
    public:
        void process(int x) { /* ... */ }
    };
    MyClass obj;
    thread t3(&MyClass::process, &obj, 100);
    t3.join();
}

12.3 原子操作

使用原子类型避免多线程数据竞争:

cpp 复制代码
#include <atomic>

atomic<int> counter{0};

void increment(int n) {
    for (int i = 0; i < n; ++i) {
        ++counter;  // 原子操作
    }
}

int main() {
    thread t1(increment, 1000000);
    thread t2(increment, 1000000);
    t1.join();
    t2.join();
    cout << counter << endl;  // 2000000
}

12.4 互斥锁

使用lock_guardunique_lock进行RAII式的锁管理:

cpp 复制代码
#include <mutex>

mutex mtx;
int shared_data = 0;

void increment() {
    lock_guard<mutex> lock(mtx);  // RAII自动上锁解锁
    ++shared_data;
}

// unique_lock更灵活
void increment_with_timeout() {
    unique_lock<mutex> lock(mtx, try_to_lock);
    if (lock.owns_lock()) {
        ++shared_data;
    }
}

12.5 条件变量

使用条件变量实现线程间同步:

cpp 复制代码
#include <condition_variable>

void two_thread_print() {
    mutex mtx;
    condition_variable cv;
    bool is_even = true;
    int n = 100;
    
    thread even([&]() {
        for (int i = 0; i < n; i += 2) {
            unique_lock<mutex> lock(mtx);
            cv.wait(lock, [&]() { return is_even; });
            cout << "Even: " << i << endl;
            is_even = false;
            cv.notify_one();
        }
    });
    
    thread odd([&]() {
        for (int i = 1; i < n; i += 2) {
            unique_lock<mutex> lock(mtx);
            cv.wait(lock, [&]() { return !is_even; });
            cout << "Odd: " << i << endl;
            is_even = true;
            cv.notify_one();
        }
    });
    
    even.join();
    odd.join();
}

总结

C++11带来的新特性极大地提升了C++的开发效率和代码安全性:

特性类别 主要特性 核心价值
语法简化 auto、decltype、范围for 减少冗余代码
初始化 列表初始化、initializer_list 统一初始化方式
资源管理 智能指针、移动语义 提升性能、避免内存泄漏
函数式编程 lambda、function、bind 更灵活的编程范式
并发编程 thread、atomic、mutex 标准化的多线程支持

参考资料

相关推荐
用户81818706274619 分钟前
第30章 Web 层与事务异常:参数校验、@Transactional 失效场景全集
后端
Java内核笔记22 分钟前
BeanRegistrar:Spring Boot 4 最被低估的新特性,彻底改变 Bean 注册方式
java·后端
用户81818706274628 分钟前
第28章 RPC 框架异常:Dubbo / gRPC
后端
大尚来也37 分钟前
HTML5 Web Worker:彻底解决JS阻塞页面卡顿问题
开发语言
搬砖小匠43 分钟前
SpringBoot 通过自定义注解 + AOP 实现数据字典自动翻译(通用方案)
java·后端
sycmancia1 小时前
Qt——复习篇painter
开发语言·qt
生信星球1 小时前
空间转录组常规分析(大白话教程)
后端
旖旎夜光1 小时前
LeetCode 738:单调递增的数字(贪心算法) —— 题解
数据结构·c++·算法·leetcode·贪心算法
赵钰老师1 小时前
R语言数据统计分析与ggplot2高级绘图
开发语言·数据分析·r语言