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 标准化的多线程支持

参考资料

相关推荐
执行x1 小时前
wsl2安装+桥接模式+固定IP
linux·windows·ubuntu
weixin_446729161 小时前
java实现发送邮件
java·开发语言
无相求码2 小时前
为什么你的第二个 scanf 还没输入就直接跳过了?C语言输入缓冲区陷阱
c语言·后端
叼馒女友郭芙蓉2 小时前
Function Calling理解
后端
玉宇夕落2 小时前
LangChain RAG Retriever 的学习
后端
用户298698530142 小时前
Python 一键导出 Excel 中的图表和形状为图片
后端·python·excel
用户8130077816372 小时前
PageHelper分页查询步骤
后端
是小李呀2 小时前
MyBatis、MyBatis-Plus 与全自动 ORM 的本质区别
后端
米尔的可达鸭2 小时前
深入操作系统 Socket 底层:EPOLLOUT 可写事件管理 + 非阻塞异步
开发语言·网络·数据结构·经验分享·websocket·网络协议