学习笔记:C++ 11 新特性

C++ 11 新特性之 auto 和 decltype 知识点

C++11 引入了 auto 和 decltype 关键字,使用它们可以在编译期就推导出变量或者表达式的类型,方便开发者编码的同时也简化了代码。

auto

auto 可以让编译器在编译器就推导出变量的类型,看代码:

auto a = 10; // 10是int型,可以自动推导出a是int
int i = 10;auto b = i; // b是int型
auto d = 2.0; // d是double型

这就是 auto 的基本用法,可以通过=右边的类型推导出变量的类型。

auto 推导规则

直接看代码

代码 1:

int i = 10;
auto a = i, &b = i, *c = &i; // a是int,b是i的引用,c是i的指针,auto就相当于int
auto d = 0, f = 1.0; // error,0和1.0类型不同,对于编译器有二义性,没法推导
auto e; // error,使用auto必须马上初始化,否则无法推导类型

代码 2:

void func(auto value) {} // error,auto不能用作函数参数

class A {
    auto a = 1; // error,在类中auto不能用作非静态成员变量
    static auto b = 1; // error,这里与auto无关,正常static int b = 1也不可以
    static const auto int c = 1; // ok
};

void func2() {
    int a[10] = {0};
    auto b = a; // ok
    auto c[10] = a; // error,auto不能定义数组,可以定义指针
    vector<int> d;
    vector<auto> f = d; // error,auto无法推导出模板参数
}

auto 的限制:

  • auto 的使用必须马上初始化,否则无法推导出类型

  • auto 在一行定义多个变量时,各个变量的推导不能产生二义性,否则编译失败

  • auto 不能用作函数参数

  • 在类中 auto 不能用作非静态成员变量

  • auto 不能定义数组,可以定义指针

  • auto 无法推导出模板参数

再看这段代码:

int i = 0;
auto *a = &i; // a是int*
auto &b = i; // b是int&
auto c = b; // c是int,忽略了引用

const auto d = i; // d是const int
auto e = d; // e是int

const auto& f = e; // f是const int&
auto &g = f; // g是const int&

首先,介绍下,这里的 cv 是指 const 和 volatile

推导规则

  • 在不声明为引用或指针时,auto 会忽略等号右边的引用类型和 cv 限定
  • 在声明为引用或者指针时,auto 会保留等号右边的引用和 cv 属性

什么时候使用 auto?

这里没有绝对答案,在不影响代码代码可读性的前提下尽可能使用 auto 是蛮好的,复杂类型就使用 auto,int、double 这种就没有必要使用 auto 了,看下面这段代码:

auto func = [&] {
    cout << "xxx";
}; // 对于func难道不使用auto吗,反正是不关心lambda表达式究竟是什么类型。

auto asyncfunc = std::async(std::launch::async, func);
// 对于asyncfunc难道不使用auto吗,懒得写std::futurexxx等代码,而且也记不住它返回的究竟是什么...

decltype

上面介绍 auto 用于推导变量类型,而 decltype 则用于推导表达式类型,这里只用于编译器分析表达式的类型,表达式实际不会进行运算,上代码:

int func() { return 0; }
decltype(func()) i; // i为int类型

int x = 0;
decltype(x) y; // y是int类型
decltype(x + y) z; // z是int类型

注意:decltype 不会像 auto 一样忽略引用和 cv 属性,decltype 会保留表达式的引用和 cv 属性

cont int &i = 1;
int a = 2;
decltype(i) b = 2; // b是const int&

decltype 推导规则

对于 decltype(exp)有

  • exp 是表达式,decltype(exp)和 exp 类型相同

  • exp 是函数调用,decltype(exp)和函数返回值类型相同

  • 其它情况,若 exp 是左值,decltype(exp)是 exp 类型的左值引用

int a = 0, b = 0;
decltype(a + b) c = 0; // c是int,因为(a+b)返回一个右值
decltype(a += b) d = c;// d是int&,因为(a+=b)返回一个左值

d = 20;
cout << "c " << c << endl; // 输出c 20

auto 和 decltype 的配合使用

auto 和 decltype 一般配合使用在推导函数返回值的类型问题上。

下面这段代码

template<typename T, typename U>
return_value add(T t, U u) { // t和v类型不确定,无法推导出return_value类型
    return t + u;
}

上面代码由于 t 和 u 类型不确定,那如何推导出返回值类型呢,可能会想到这种

template<typename T, typename U>
decltype(t + u) add(T t, U u) { // t和u尚未定义
    return t + u;
}

这段代码在 C++11 上是编译不过的,因为在 decltype(t +u)推导时,t 和 u 尚未定义,就会编译出错,所以有了下面的叫做返回类型后置的配合使用方法:

template<typename T, typename U>
auto add(T t, U u) -> decltype(t + u) {
    return t + u;
}

返回值后置类型语法就是为了解决函数返回值类型依赖于参数但却难以确定返回值类型的问题。

C++11 新特性之左值引用、右值引用、移动语义、完美转发

C++11 新增了右值引用,谈右值引用也可以扩展一些相关概念:

  • 左值

  • 右值

  • 纯右值

  • 将亡值

  • 左值引用

  • 右值引用

  • 移动语义

  • 完美转发

  • 返回值优化

左值、右值

概念 1:

左值:可以放到等号左边的东西叫左值。

右值:不可以放到等号左边的东西就叫右值。

概念 2:

左值:可以取地址并且有名字的东西就是左值。

右值:不能取地址的没有名字的东西就是右值。

举例:

int a = b + c;

a 是左值,有变量名,可以取地址,也可以放到等号左边, 表达式 b+c 的返回值是右值,没有名字且不能取地址,&(b+c)不能通过编译,而且也不能放到等号左边。

int a = 4; // a是左值,4作为普通字面量是右值

左值一般有:

  • 函数名和变量名

  • 返回左值引用的函数调用

  • 前置自增自减表达式++i、–i

  • 由赋值表达式或赋值运算符连接的表达式(a=b, a += b 等)

  • 解引用表达式*p

  • 字符串字面值"abcd"

纯右值、将亡值

纯右值和将亡值都属于右值。

纯右值

运算表达式产生的临时变量、不和对象关联的原始字面量、非引用返回的临时变量、lambda 表达式等都是纯右值。

举例:

  • 除字符串字面值外的字面值

  • 返回非引用类型的函数调用

  • 后置自增自减表达式 i++、i–

  • 算术表达式(a+b, a*b, a&&b, a==b 等)

  • 取地址表达式等(&a)

将亡值

将亡值是指 C++11 新增的和右值引用相关的表达式,通常指将要被移动的对象、T&&函数的返回值、std::move 函数的返回值、转换为 T&&类型转换函数的返回值,将亡值可以理解为即将要销毁的值,通过“盗取”其它变量内存空间方式获取的值,在确保其它变量不再被使用或者即将被销毁时,可以避免内存空间的释放和分配,延长变量值的生命周期,常用来完成移动构造或者移动赋值的特殊任务。

举例:

class A {
    xxx;
};
A a;
auto c = std::move(a); // c是将亡值
auto d = static_cast<A&&>(a); // d是将亡值

左值引用、右值引用

根据名字大概就可以猜到意思,左值引用就是对左值进行引用的类型,右值引用就是对右值进行引用的类型,他们都是引用,都是对象的一个别名,并不拥有所绑定对象的堆存,所以都必须立即初始化。

type &name = exp; // 左值引用
type &&name = exp; // 右值引用

左值引用

看代码:

int a = 5;
int &b = a; // b是左值引用
b = 4;
int &c = 10; // error,10无法取地址,无法进行引用
const int &d = 10; // ok,因为是常引用,引用常量数字,这个常量数字会存储在内存中,可以取地址

可以得出结论:对于左值引用,等号右边的值必须可以取地址,如果不能取地址,则会编译失败,或者可以使用 const 引用形式,但这样就只能通过引用来读取输出,不能修改数组,因为是常量引用。

右值引用

如果使用右值引用,那表达式等号右边的值需要时右值,可以使用 std::move 函数强制把左值转换为右值。

int a = 4;
int &&b = a; // error, a是左值
int &&c = std::move(a); // ok

移动语义

谈移动语义前,首先需要了解深拷贝与浅拷贝的概念

深拷贝、浅拷贝

直接拿代码举例:

class A {
public:
    A(int size) : size_(size) {
        data_ = new int[size];
    }
    A(){}
    A(const A& a) {
        size_ = a.size_;
        data_ = a.data_;
        cout << "copy " << endl;
    }
    ~A() {
        delete[] data_;
    }
    int *data_;
    int size_;
};
int main() {
    A a(10);
    A b = a;
    cout << "b " << b.data_ << endl;
    cout << "a " << a.data_ << endl;
    return 0;
}

上面代码中,两个输出的是相同的地址,a 和 b 的 data指针指向了同一块内存,这就是浅拷贝,只是数据的简单赋值,那再析构时 data内存会被释放两次,导致程序出问题,这里正常会出现 double free 导致程序崩溃的,这样的程序肯定是有隐患的,如何消除这种隐患呢,可以使用如下深拷贝:

class A {
public:
    A(int size) : size_(size) {
        data_ = new int[size];
    }
    A(){}
    A(const A& a) {
        size_ = a.size_;
        data_ = new int[size_];
        cout << "copy " << endl;
    }
    ~A() {
        delete[] data_;
    }
    int *data_;
    int size_;
};
int main() {
    A a(10);
    A b = a;
    cout << "b " << b.data_ << endl;
    cout << "a " << a.data_ << endl;
    return 0;
}

深拷贝就是再拷贝对象时,如果被拷贝对象内部还有指针引用指向其它资源,自己需要重新开辟一块新内存存储资源,而不是简单的赋值。

移动语义可以理解为转移所有权,之前的拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用,通过 C++11 新增的移动语义可以省去很多拷贝负担,怎么利用移动语义呢,是通过移动构造函数。

class A {
public:
    A(int size) : size_(size) {
        data_ = new int[size];
    }
    A(){}
    A(const A& a) {
        size_ = a.size_;
        data_ = new int[size_];
        cout << "copy " << endl;
    }
    A(A&& a) {
        this->data_ = a.data_;
        a.data_ = nullptr;
        cout << "move " << endl;
    }
    ~A() {
        if (data_ != nullptr) {
         delete[] data_;
        }
    }
    int *data_;
    int size_;
};
int main() {
    A a(10);
    A b = a;
    A c = std::move(a); // 调用移动构造函数
    return 0;
}

如果不使用 std::move(),会有很大的拷贝代价,使用移动语义可以避免很多无用的拷贝,提供程序性能,C++所有的 STL 都实现了移动语义,方便使用。例如:

std::vector<string> vecs;
...
std::vector<string> vecm = std::move(vecs); // 免去很多拷贝

注意:移动语义仅针对于那些实现了移动构造函数的类的对象,对于那种基本类型 int、float 等没有任何优化作用,还是会拷贝,因为它们实现没有对应的移动构造函数。

完美转发

完美转发指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参,转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数实参也是右值。那如何实现完美转发呢,答案是使用 std::forward()。

void PrintV(int &t) {
    cout << "lvalue" << endl;
}

void PrintV(int &&t) {
    cout << "rvalue" << endl;
}

template<typename T>
void Test(T &&t) {
    PrintV(t);
    PrintV(std::forward<T>(t));

    PrintV(std::move(t));
}

int main() {
    Test(1); // lvalue rvalue rvalue
    int a = 1;
    Test(a); // lvalue lvalue rvalue
    Test(std::forward<int>(a)); // lvalue rvalue rvalue
    Test(std::forward<int&>(a)); // lvalue lvalue rvalue
    Test(std::forward<int&&>(a)); // lvalue rvalue rvalue
    return 0;
}

分析

  • Test(1):1 是右值,模板中 T &&t 这种为万能引用,右值 1 传到 Test 函数中变成了右值引用,但是调用 PrintV()时候,t 变成了左值,因为它变成了一个拥有名字的变量,所以打印 lvalue,而 PrintV(std::forward(t))时候,会进行完美转发,按照原来的类型转发,所以打印 rvalue,PrintV(std::move(t))毫无疑问会打印 rvalue。

  • Test(a):a 是左值,模板中 T &&这种为万能引用,左值 a 传到 Test 函数中变成了左值引用,所以有代码中打印。

  • Test(std::forward(a)):转发为左值还是右值,依赖于 T,T 是左值那就转发为左值,T 是右值那就转发为右值。

返回值优化

返回值优化(RVO)是一种 C++编译优化技术,当函数需要返回一个对象实例时候,就会创建一个临时对象并通过复制构造函数将目标对象复制到临时对象,这里有复制构造函数和析构函数会被多余的调用到,有代价,而通过返回值优化,C++标准允许省略调用这些复制构造函数。

那什么时候编译器会进行返回值优化呢?

  • return 的值类型与函数的返回值类型相同
  • return 的是一个局部对象

看几个例子:

示例 1:

std::vector<int> return_vector(void) {
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}
std::vector<int> &&rval_ref = return_vector();

不会触发 RVO,拷贝构造了一个临时的对象,临时对象的生命周期和 rval_ref 绑定,等价于下面这段代码:

const std::vector<int>& rval_ref = return_vector();

示例 2:

std::vector<int>&& return_vector(void) {
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

这段代码会造成运行时错误,因为 rval_ref 引用了被析构的 tmp。讲道理来说这段代码是错的,自己运行过程中却成功了,继续向下看什么时候会触发 RVO。

示例 3:

std::vector<int> return_vector(void) {
    std::vector<int> tmp {1,2,3,4,5};
    return std::move(tmp);
}

std::vector<int> &&rval_ref = return_vector();

和示例 1 类似,std::move 一个临时对象是没有必要的,也会忽略掉返回值优化。

最好的代码:

std::vector<int> return_vector(void) {
    std::vector<int> tmp {1,2,3,4,5};
    return tmp;
}

std::vector<int> rval_ref = return_vector();

这段代码会触发 RVO,不拷贝也不移动,不生成临时对象。

C++11 新特性之列表初始化

C++11 新增了列表初始化的概念。

在 C++11 中可以直接在变量名后面加上初始化列表来进行对象的初始化。

struct A {
    public:
    A(int) {}
    private:
    A(const A&) {}
};
int main() {
    A a(123);
    A b = 123; // error
    A c = { 123 };
    A d{123}; // c++11

    int e = {123};
    int f{123}; // c++11

    return 0;
}

列表初始化也可以用在函数的返回值上

std::vector<int> func() {
    return {};
}

列表初始化的一些规则

首先说下聚合类型可以进行直接列表初始化,这里需要了解什么是聚合类型:

  1. 类型是一个普通数组,如 int[5],char[],double[]等
  2. 类型是一个类,且满足以下条件:
    • 没有用户声明的构造函数
    • 没有用户提供的构造函数(允许显示预置或弃置的构造函数)
    • 没有私有或保护的非静态数据成员
    • 没有基类
    • 没有虚函数
    • 没有{}和=直接初始化的非静态数据成员
    • 没有默认成员初始化器
struct A {
    int a;
    int b;
    int c;
    A(int, int){}
};
int main() {
    A a{1, 2, 3};// error,A有自定义的构造函数,不能列表初始化
}

上述代码类 A 不是聚合类型,无法进行列表初始化,必须以自定义的构造函数来构造对象。

struct A {
    int a;
    int b;
    virtual void func() {} // 含有虚函数,不是聚合类
};

struct Base {};
struct B : public Base { // 有基类,不是聚合类
    int a;
    int b;
};

struct C {
    int a;
    int b = 10; // 有等号初始化,不是聚合类
};

struct D {
    int a;
    int b;
    private:
    int c; // 含有私有的非静态数据成员,不是聚合类
};

struct E {
    int a;
    int b;
    E() : a(0), b(0) {} // 含有默认成员初始化器,不是聚合类
};

上面列举了一些不是聚合类的例子,对于一个聚合类型,使用列表初始化相当于对其中的每个元素分别赋值;对于非聚合类型,需要先自定义一个对应的构造函数,此时列表初始化将调用相应的构造函数。

std::initializer_list

平时开发使用 STL 过程中可能发现它的初始化列表可以是任意长度,大家有没有想过它是怎么实现的呢,答案是 std::initializer_list,看下面这段示例代码:

struct CustomVec {
    std::vector<int> data;
    CustomVec(std::initializer_list<int> list) {
        for (auto iter = list.begin(); iter != list.end(); ++iter) {
            data.push_back(*iter);
        }
    }
};

这个 std::initializer_list 其实也可以作为函数参数。

注意:std::initializer_list,它可以接收任意长度的初始化列表,但是里面必须是相同类型 T,或者都可以转换为 T。

列表初始化的好处

列表初始化的好处如下:

  1. 方便,且基本上可以替代括号初始化

  2. 可以使用初始化列表接受任意长度

  3. 可以防止类型窄化,避免精度丢失的隐式类型转换

什么是类型窄化,列表初始化通过禁止下列转换,对隐式转化加以限制:

  • 从浮点类型到整数类型的转换

  • 从 long double 到 double 或 float 的转换,以及从 double 到 float 的转换,除非源是常量表达式且不发生溢出

  • 从整数类型到浮点类型的转换,除非源是其值能完全存储于目标类型的常量表达式

  • 从整数或无作用域枚举类型到不能表示原类型所有值的整数类型的转换,除非源是其值能完全存储于目标类型的常量表达式

示例:

int main() {
    int a = 1.2; // ok
    int b = {1.2}; // error

    float c = 1e70; // ok
    float d = {1e70}; // error

    float e = (unsigned long long)-1; // ok
    float f = {(unsigned long long)-1}; // error
    float g = (unsigned long long)1; // ok
    float h = {(unsigned long long)1}; // ok

    const int i = 1000;
    const int j = 2;
    char k = i; // ok
    char l = {i}; // error

    char m = j; // ok
    char m = {j}; // ok,因为是const类型,这里如果去掉const属性,也会报错
}

打印如下:

test.cc:24:17: error: narrowing conversion of 1.2e+0 from double to int inside { } [-Wnarrowing]
    int b = {1.2};
                ^
test.cc:27:20: error: narrowing conversion of 1.0000000000000001e+70 from double to float inside { } [-Wnarrowing]
     float d = {1e70};

test.cc:30:38: error: narrowing conversion of 18446744073709551615 from long long unsigned int to float inside { } [-Wnarrowing]
    float f = {(unsigned long long)-1};
                                     ^
test.cc:36:14: warning: overflow in implicit constant conversion [-Woverflow]
    char k = i;
             ^
test.cc:37:16: error: narrowing conversion of 1000 from int to char inside { } [-Wnarrowing]
    char l = {i};

C++11 新特性 std::function 和 lambda 表达式

c++11 新增了std::functionstd::bindlambda表达式等封装使函数调用更加方便。

std::function

std::function前首先需要了解下什么是可调用对象

满足以下条件之一就可称为可调用对象:

  • 是一个函数指针

  • 是一个具有operator()成员函数的类对象(传说中的仿函数),lambda 表达式

  • 是一个可被转换为函数指针的类对象

  • 是一个类成员(函数)指针

  • bind 表达式或其它函数对象

std::function就是上面这种可调用对象的封装器,可以把std::function看做一个函数对象,用于表示函数这个抽象概念。std::function的实例可以存储、复制和调用任何可调用对象,存储的可调用对象称为std::function的目标,若std::function不含目标,则称它为空,调用空的std::function的目标会抛出std::bad_function_call异常。

使用参考如下实例代码:

std::function<void(int)> f; // 这里表示function的对象f的参数是int,返回值是void
#include <functional>
#include <iostream>

struct Foo {
    Foo(int num) : num_(num) {}
    void print_add(int i) const { std::cout << num_ + i << '\n'; }
    int num_;
};

void print_num(int i) { std::cout << i << '\n'; }

struct PrintNum {
    void operator()(int i) const { std::cout << i << '\n'; }
};

int main() {
    // 存储自由函数
    std::function<void(int)> f_display = print_num;
    f_display(-9);

    // 存储 lambda
    std::function<void()> f_display_42 = []() { print_num(42); };
    f_display_42();

    // 存储到 std::bind 调用的结果
    std::function<void()> f_display_31337 = std::bind(print_num, 31337);
    f_display_31337();

    // 存储到成员函数的调用
    std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
    const Foo foo(314159);
    f_add_display(foo, 1);
    f_add_display(314159, 1);

    // 存储到数据成员访问器的调用
    std::function<int(Foo const&)> f_num = &Foo::num_;
    std::cout << "num_: " << f_num(foo) << '\n';

    // 存储到成员函数及对象的调用
    using std::placeholders::_1;
    std::function<void(int)> f_add_display2 = std::bind(&Foo::print_add, foo, _1);
    f_add_display2(2);

    // 存储到成员函数和对象指针的调用
    std::function<void(int)> f_add_display3 = std::bind(&Foo::print_add, &foo, _1);
    f_add_display3(3);

    // 存储到函数对象的调用
    std::function<void(int)> f_display_obj = PrintNum();
    f_display_obj(18);
}

从上面可以看到std::function的使用方法,当给std::function填入合适的参数表和返回值后,它就变成了可以容纳所有这一类调用方式的函数封装器。std::function还可以用作回调函数,或者在 C++里如果需要使用回调那就一定要使用std::function,特别方便。

std::bind

使用std::bind可以将可调用对象和参数一起绑定,绑定后的结果使用std::function进行保存,并延迟调用到任何需要的时候。

std::bind通常有两大作用:

  • 将可调用对象与参数一起绑定为另一个std::function供调用
  • 将 n 元可调用对象转成 m(m < n)元可调用对象,绑定一部分参数,这里需要使用std::placeholders

具体示例:

#include <functional>
#include <iostream>
#include <memory>

void f(int n1, int n2, int n3, const int& n4, int n5) {
    std::cout << n1 << ' ' << n2 << ' ' << n3 << ' ' << n4 << ' ' << n5 << std::endl;
}

int g(int n1) { return n1; }

struct Foo {
    void print_sum(int n1, int n2) { std::cout << n1 + n2 << std::endl; }
    int data = 10;
};

int main() {
    using namespace std::placeholders;  // 针对 _1, _2, _3...

    // 演示参数重排序和按引用传递
    int n = 7;
    // ( _1 与 _2 来自 std::placeholders ,并表示将来会传递给 f1 的参数)
    auto f1 = std::bind(f, _2, 42, _1, std::cref(n), n);
    n = 10;
    f1(1, 2, 1001);  // 1 为 _1 所绑定, 2 为 _2 所绑定,不使用 1001
    // 进行到 f(2, 42, 1, n, 7) 的调用

    // 嵌套 bind 子表达式共享占位符
    auto f2 = std::bind(f, _3, std::bind(g, _3), _3, 4, 5);
    f2(10, 11, 12);  // 进行到 f(12, g(12), 12, 4, 5); 的调用

    // 绑定指向成员函数指针
    Foo foo;
    auto f3 = std::bind(&Foo::print_sum, &foo, 95, _1);
    f3(5);

    // 绑定指向数据成员指针
    auto f4 = std::bind(&Foo::data, _1);
    std::cout << f4(foo) << std::endl;

    // 智能指针亦能用于调用被引用对象的成员
    std::cout << f4(std::make_shared<Foo>(foo)) << std::endl;
}

lambda表达式

lambda 表达式可以说是 c++11 引用的最重要的特性之一,它定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用,一般有如下语法形式:

auto func = [capture] (params) opt -> ret { func_body; };

其中func是可以当作lambda表达式的名字,作为一个函数使用,capture是捕获列表,params是参数表,opt是函数选项(mutable 之类), ret 是返回值类型,func_body 是函数体。

一个完整的 lambda 表达式:

auto func1 = [](int a) -> int { return a + 1; };
auto func2 = [](int a) { return a + 2; };
cout << func1(1) << " " << func2(2) << endl;

如上代码,很多时候 lambda 表达式返回值是很明显的,c++11 允许省略表达式的返回值定义。

lambda表达式允许捕获一定范围内的变量:

  • []不捕获任何变量

  • [&]引用捕获,捕获外部作用域所有变量,在函数体内当作引用使用

  • [=]值捕获,捕获外部作用域所有变量,在函数内内有个副本使用

  • [=, &a]值捕获外部作用域所有变量,按引用捕获 a 变量

  • [a]只值捕获 a 变量,不捕获其它变量

  • [this]捕获当前类中的 this 指针

lambda 表达式示例代码:

int a = 0;
auto f1 = [=](){ return a; }; // 值捕获a
cout << f1() << endl;

auto f2 = [=]() { return a++; }; // 修改按值捕获的外部变量,error
auto f3 = [=]() mutable { return a++; };

代码中的 f2 是编译不过的,因为修改了按值捕获的外部变量,其实 lambda 表达式就相当于是一个仿函数,仿函数是一个有operator()成员函数的类对象,这个operator()默认是const的,所以不能修改成员变量,而加了mutable,就是去掉const属性。

还可以使用 lambda 表达式自定义 stl 的规则,例如自定义 sort 排序规则:

struct A {
    int a;
    int b;
};

int main() {
    vector<A> vec;
    std::sort(vec.begin(), vec.end(), [](const A &left, const A &right) { return left.a < right.a; });
}

总结

std::functionstd::bind在平时编程过程中封装函数更加的方便,而 lambda 表达式将这种方便发挥到了极致,可以在需要的时间就地定义匿名函数,不再需要定义类或者函数等,在自定义 STL 规则时候也非常方便,让代码更简洁,更灵活,提高开发效率。

C++11 新特性之模板改进

C++11 关于模板有一些细节的改进:

  • 模板的右尖括号

  • 模板的别名

  • 函数模板的默认模板参数

模板的右尖括号

C++11 之前是不允许两个右尖括号出现的,会被认为是右移操作符,所以需要中间加个空格进行分割,避免发生编译错误。

模板的别名

C++11 引入了 using,可以轻松的定义别名,而不是使用繁琐的 typedef。

int main() {
    std::vector<std::vector<int>> a; // error
    std::vector<std::vector<int> > b; // ok
}

使用 using 明显简洁并且易读,大家可能之前也见过使用 typedef 定义函数指针之类的操作。

typedef void (*func)(int, int);
using func = void (*)(int, int); // 起码比typedef容易看的懂

上面的代码使用 using 起码比 typedef 容易看的懂一些,但是我还是看不懂,因为我从来不用这种来表示函数指针,用 std::function()、std::bind()、std::placeholder()、lambda 表达式它不香吗。

函数模板的默认模板参数

C++11 之前只有类模板支持默认模板参数,函数模板是不支持默认模板参数的,C++11 后都支持。

template <typename T, typename U=int>
class A {
    T value;
};

template <typename T=int, typename U> // error
class A {
    T value;
};

类模板的默认模板参数必须从右往左定义,而函数模板则没有这个限制。

template <typename R, typename U=int>
R func1(U val) {
   return val;
}

template <typename R=int, typename U>
R func2(U val) {
    return val;
}

int main() {
    cout << func1<int, double>(99.9) << endl; // 99
    cout << func1<double, double>(99.9) << endl; // 99.9
    cout << func1<double>(99.9) << endl; // 99.9
    cout << func1<int>(99.9) << endl; // 99
    cout << func2<int, double>(99.9) << endl; // 99
    cout << func1<double, double>(99.9) << endl; // 99.9
    cout << func2<double>(99.9) << endl; // 99.9
    cout << func2<int>(99.9) << endl; // 99
    return 0;
}

C++11 新特性之线程相关知识点

c++11 关于并发引入了好多新东西,这里按照如下顺序介绍:

  • std::thread 相关

  • std::mutex 相关

  • std::lock 相关

  • std::atomic 相关

  • std::call_once 相关

  • volatile 相关

  • std::condition_variable 相关

  • std::future 相关

  • async 相关

std::thread 相关

c++11 之前可能使用 pthread_xxx 来创建线程,繁琐且不易读,c++11 引入了 std::thread 来创建线程,支持对线程 join 或者 detach。直接看代码:

#include <iostream>
#include <thread>

using namespace std;

int main() {
    auto func = []() {
        for (int i = 0; i < 10; ++i) {
            cout << i << " ";
        }
        cout << endl;
    };
    std::thread t(func);
    if (t.joinable()) {
        t.detach();
    }
    auto func1 = [](int k) {
        for (int i = 0; i < k; ++i) {
            cout << i << " ";
        }
        cout << endl;
    };
    std::thread tt(func1, 20);
    if (tt.joinable()) { // 检查线程可否被join
        tt.join();
    }
    return 0;
}

上述代码中,函数 func 和 func1 运行在线程对象 t 和 tt 中,从刚创建对象开始就会新建一个线程用于执行函数,调用 join 函数将会阻塞主线程,直到线程函数执行结束,线程函数的返回值将会被忽略。如果不希望线程被阻塞执行,可以调用线程对象的 detach 函数,表示将线程和线程对象分离。

如果没有调用 join 或者 detach 函数,假如线程函数执行时间较长,此时线程对象的生命周期结束调用析构函数清理资源,这时可能会发生错误,这里有两种解决办法,一个是调用 join(),保证线程函数的生命周期和线程对象的生命周期相同,另一个是调用 detach(),将线程和线程对象分离,这里需要注意,如果线程已经和对象分离,那就再也无法控制线程什么时候结束了,不能再通过 join 来等待线程执行完。

这里可以对 thread 进行封装,避免没有调用 join 或者 detach 可导致程序出错的情况出现:

class ThreadGuard {
    public:
    enum class DesAction { join, detach };

    ThreadGuard(std::thread&& t, DesAction a) : t_(std::move(t)), action_(a){};

    ~ThreadGuard() {
        if (t_.joinable()) {
            if (action_ == DesAction::join) {
                t_.join();
            } else {
                t_.detach();
            }
        }
    }

    ThreadGuard(ThreadGuard&&) = default;
    ThreadGuard& operator=(ThreadGuard&&) = default;

    std::thread& get() { return t_; }

    private:
    std::thread t_;
    DesAction action_;
};

int main() {
    ThreadGuard t(std::thread([]() {
        for (int i = 0; i < 10; ++i) {
            std::cout << "thread guard " << i << " ";
        }
        std::cout << std::endl;}), ThreadGuard::DesAction::join);
    return 0;
}

c++11 还提供了获取线程 id,或者系统 cpu 个数,获取 thread native_handle,使得线程休眠等功能

std::thread t(func);
cout << "当前线程ID " << t.get_id() << endl;
cout << "当前cpu个数 " << std::thread::hardware_concurrency() << endl;
auto handle = t.native_handle();// handle可用于pthread相关操作
std::this_thread::sleep_for(std::chrono::seconds(1));

std::mutex 相关

std::mutex 是一种线程同步的手段,用于保存多线程同时操作的共享数据。

mutex 分为四种:

  • std::mutex:独占的互斥量,不能递归使用,不带超时功能

  • std::recursive_mutex:递归互斥量,可重入,不带超时功能

  • std::timed_mutex:带超时的互斥量,不能递归

  • std::recursive_timed_mutex:带超时的互斥量,可以递归使用

拿一个 std::mutex 和 std::timed_mutex 举例,别的都是类似的使用方式:

std::mutex:

#include <iostream>
#include <mutex>
#include <thread>

using namespace std;
std::mutex mutex_;

int main() {
    auto func1 = [](int k) {
        mutex_.lock();
        for (int i = 0; i < k; ++i) {
            cout << i << " ";
        }
        cout << endl;
        mutex_.unlock();
    };
    std::thread threads[5];
    for (int i = 0; i < 5; ++i) {
        threads[i] = std::thread(func1, 200);
    }
    for (auto& th : threads) {
        th.join();
    }
    return 0;
}

std::timed_mutex:

#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>

using namespace std;
std::timed_mutex timed_mutex_;

int main() {
    auto func1 = [](int k) {
        timed_mutex_.try_lock_for(std::chrono::milliseconds(200));
        for (int i = 0; i < k; ++i) {
            cout << i << " ";
        }
        cout << endl;
        timed_mutex_.unlock();
    };
    std::thread threads[5];
    for (int i = 0; i < 5; ++i) {
        threads[i] = std::thread(func1, 200);
    }
    for (auto& th : threads) {
        th.join();
    }
    return 0;
}

std::lock 相关

这里主要介绍两种 RAII 方式的锁封装,可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁。

c++11 主要有 std::lock_guard 和 std::unique_lock 两种方式,使用方式都类似,如下:

#include <iostream>
#include <mutex>
#include <thread>
#include <chrono>

using namespace std;
std::mutex mutex_;

int main() {
    auto func1 = [](int k) {
        // std::lock_guard<std::mutex> lock(mutex_);
        std::unique_lock<std::mutex> lock(mutex_);
        for (int i = 0; i < k; ++i) {
            cout << i << " ";
        }
        cout << endl;
    };
    std::thread threads[5];
    for (int i = 0; i < 5; ++i) {
        threads[i] = std::thread(func1, 200);
    }
    for (auto& th : threads) {
        th.join();
    }
    return 0;
}

std::lock_gurad 相比于 std::unique_lock 更加轻量级,少了一些成员函数,std::unique_lock 类有 unlock 函数,可以手动释放锁,所以条件变量都配合 std::unique_lock 使用,而不是 std::lock_guard,因为条件变量在 wait 时需要有手动释放锁的能力,具体关于条件变量后面会讲到。

std::atomic 相关

c++11 提供了原子类型 std::atomic,理论上这个 T 可以是任意类型,但是平时只存放整形,别的还真的没用过,整形有这种原子变量已经足够方便,就不需要使用 std::mutex 来保护该变量啦。看一个计数器的代码:

struct OriginCounter { // 普通的计数器
    int count;
    std::mutex mutex_;
    void add() {
        std::lock_guard<std::mutex> lock(mutex_);
        ++count;
    }

    void sub() {
        std::lock_guard<std::mutex> lock(mutex_);
        --count;
    }

    int get() {
        std::lock_guard<std::mutex> lock(mutex_);
        return count;
    }
};

struct NewCounter { // 使用原子变量的计数器
    std::atomic<int> count;
    void add() {
        ++count;
        // count.store(++count);这种方式也可以
    }

    void sub() {
        --count;
        // count.store(--count);
    }

    int get() {
        return count.load();
    }
};

是不是使用原子变量更加方便了呢?

std::call_once 相关

c++11 提供了 std::call_once 来保证某一函数在多线程环境中只调用一次,它需要配合 std::once_flag 使用,直接看使用代码:

std::once_flag onceflag;

void CallOnce() {
    std::call_once(onceflag, []() {
        cout << "call once" << endl;
    });
}

int main() {
    std::thread threads[5];
    for (int i = 0; i < 5; ++i) {
        threads[i] = std::thread(CallOnce);
    }
    for (auto& th : threads) {
        th.join();
    }
    return 0;
}

volatile 相关

貌似把 volatile 放在并发里介绍不太合适,但是貌似很多人都会把 volatile 和多线程联系在一起,一起介绍下。

volatile 通常用来建立内存屏障,volatile 修饰的变量,编译器对访问该变量的代码通常不再进行优化,看下面代码:

int *p = xxx;
int a = *p;
int b = *p;

a 和 b 都等于 p 指向的值,一般编译器会对此做优化,把*p 的值放入寄存器,就是传说中的工作内存(不是主内存),之后 a 和 b 都等于寄存器的值,但是如果中间 p 地址的值改变,内存上的值改变啦,但 a,b 还是从寄存器中取的值(不一定,看编译器优化结果),这就不符合需求,所以在此对 p 加 volatile 修饰可以避免进行此类优化。

注意:volatile 不能解决多线程安全问题,针对特种内存才需要使用 volatile,它和 atomic 的特点如下:• std::atomic 用于多线程访问的数据,且不用互斥量,用于并发编程中• volatile 用于读写操作不可以被优化掉的内存,用于特种内存中

std::condition_variable 相关

条件变量是 c++11 引入的一种同步机制,它可以阻塞一个线程或者个线程,直到有线程通知或者超时才会唤醒正在阻塞的线程,条件变量需要和锁配合使用,这里的锁就是上面介绍的 std::unique_lock。

这里使用条件变量实现一个 CountDownLatch:

class CountDownLatch {
    public:
    explicit CountDownLatch(uint32_t count) : count_(count);

    void CountDown() {
        std::unique_lock<std::mutex> lock(mutex_);
        --count_;
        if (count_ == 0) {
            cv_.notify_all();
        }
    }

    void Await(uint32_t time_ms = 0) {
        std::unique_lock<std::mutex> lock(mutex_);
        while (count_ > 0) {
            if (time_ms > 0) {
                cv_.wait_for(lock, std::chrono::milliseconds(time_ms));
            } else {
                cv_.wait(lock);
            }
        }
    }

    uint32_t GetCount() const {
        std::unique_lock<std::mutex> lock(mutex_);
        return count_;
    }

    private:
    std::condition_variable cv_;
    mutable std::mutex mutex_;
    uint32_t count_ = 0;
};

关于条件变量其实还涉及到通知丢失和虚假唤醒问题,因为不是本文的主题,这里暂不介绍,大家有需要可以留言。

std::future 相关

c++11 关于异步操作提供了 future 相关的类,主要有 std::future、std::promise 和 std::packaged_task,std::future 比 std::thread 高级些,std::future 作为异步结果的传输通道,通过 get()可以很方便的获取线程函数的返回值,std::promise 用来包装一个值,将数据和 future 绑定起来,而 std::packaged_task 则用来包装一个调用对象,将函数和 future 绑定起来,方便异步调用。而 std::future 是不可以复制的,如果需要复制放到容器中可以使用 std::shared_future。

std::promise 与 std::future 配合使用

#include <functional>
#include <future>
#include <iostream>
#include <thread>

using namespace std;

void func(std::future<int>& fut) {
    int x = fut.get();
    cout << "value: " << x << endl;
}

int main() {
    std::promise<int> prom;
    std::future<int> fut = prom.get_future();
    std::thread t(func, std::ref(fut));
    prom.set_value(144);
    t.join();
    return 0;
}

std::packaged_task 与 std::future 配合使用

#include <functional>
#include <future>
#include <iostream>
#include <thread>

using namespace std;

int func(int in) {
    return in + 1;
}

int main() {
    std::packaged_task<int(int)> task(func);
    std::future<int> fut = task.get_future();
    std::thread(std::move(task), 5).detach();
    cout << "result " << fut.get() << endl;
    return 0;
}

三者之间的关系

std::future 用于访问异步操作的结果,而 std::promise 和 std::packaged_task 在 future 高一层,它们内部都有一个 future,promise 包装的是一个值,packaged_task 包装的是一个函数,当需要获取线程中的某个值,可以使用 std::promise,当需要获取线程函数返回值,可以使用 std::packaged_task。

async 相关

async 是比 future,packaged_task,promise 更高级的东西,它是基于任务的异步操作,通过 async 可以直接创建异步的任务,返回的结果会保存在 future 中,不需要像 packaged_task 和 promise 那么麻烦,关于线程操作应该优先使用 async,看一段使用代码:

#include <functional>
#include <future>
#include <iostream>
#include <thread>

using namespace std;

int func(int in) { return in + 1; }

int main() {
    auto res = std::async(func, 5);
    // res.wait();
    cout << res.get() << endl; // 阻塞直到函数返回
    return 0;
}

使用 async 异步执行函数是不是方便多啦。

async 具体语法如下:

async(std::launch::async | std::launch::deferred, func, args...);

第一个参数是创建策略:

  • std::launch::async 表示任务执行在另一线程
  • std::launch::deferred 表示延迟执行任务,调用 get 或者 wait 时才会执行,不会创建线程,惰性执行在当前线程。

如果不明确指定创建策略,以上两个都不是 async 的默认策略,而是未定义,它是一个基于任务的程序设计,内部有一个调度器(线程池),会根据实际情况决定采用哪种策略。

若从 std::async 获得的 std::future 未被移动或绑定到引用,则在完整表达式结尾, std::future 的析构函数将阻塞直至异步计算完成,实际上相当于同步操作:

std::async(std::launch::async, []{ f(); }); // 临时量的析构函数等待 f()
std::async(std::launch::async, []{ g(); }); // f() 完成前不开始

注意:关于 async 启动策略这里以 cppreference 为主。

有时候如果想真正执行异步操作可以对 async 进行封装,强制使用 std::launch::async 策略来调用 async。

template <typename F, typename... Args>
inline auto ReallyAsync(F&& f, Args&&... params) {
    return std::async(std::launch::async, std::forward<F>(f), std::forward<Args>(params)...);
}

总结

• std::thread 使线程的创建变得非常简单,还可以获取线程 id 等信息。 • std::mutex 通过多种方式保证了线程安全,互斥量可以独占,也可以重入,还可以设置互斥量的超时时间,避免一直阻塞等锁。 • std::lock 通过 RAII 技术方便了加锁和解锁调用,有 std::lock_guard 和 std::unique_lock。 • std::atomic 提供了原子变量,更方便实现实现保护,不需要使用互斥量 • std::call_once 保证函数在多线程环境下只调用一次,可用于实现单例。 • volatile 常用于读写操作不可以被优化掉的内存中。 • std::condition_variable 提供等待的同步机制,可阻塞一个或多个线程,等待其它线程通知后唤醒。 • std::future 用于异步调用的包装和返回值。 • async 更方便的实现了异步调用,异步调用优先使用 async 取代创建线程。

C++11 的异步操作-async

C++11 中增加了 async,如它的名字一样,这个关键字就是用来创建异步操作的,c++11 中有个更常用的异步操作,叫做线程 thread,那么 thread 和 async 有什么区别呢?以及 async 的优势是什么?应该怎么使用?

C++11 使用 std::async 创建异步程序

C++11 中增加了线程,可以非常方便的创建线程,它的基本用法是这样的:

void f(int n);
std::thread t(f, n + 1);
t.join();

但是线程毕竟是属于比较低层次的东西,有时候使用有些不便,比如希望获取线程函数的返回结果的时候,就不能直接通过 thread.join()得到结果,这时就必须定义一个变量,在线程函数中去给这个变量赋值,然后 join,最后得到结果,这个过程是比较繁琐的。

c++11 还提供了异步接口std::async,通过这个异步接口可以很方便的获取线程函数的执行结果。std::async会自动创建一个线程去调用线程函数,它返回一个std::future,这个 future 中存储了线程函数返回的结果,当需要线程函数的结果时,直接从 future 中获取,非常方便。

其实 std::async 提供的便利可不仅仅是这一点,它首先解耦了线程的创建和执行,可以在需要的时候获取异步操作的结果;其次它还提供了线程的创建策略(比如可以通过延迟加载的方式去创建线程),可以以多种方式去创建线程。在介绍async具体用法以及为什么要用std::async代替线程的创建之前,先看看std::futurestd::promisestd::packaged_task

std::future

std::future 是一个非常有用也很有意思的东西,简单说 std::future 提供了一种访问异步操作结果的机制。从字面意思来理解, 它表示未来,因为一个异步操作是不可能马上就获取操作结果的,只能在未来某个时候获取,但是可以以同步等待的方式来获取结果,可以通过查询 future 的状态(future_status)来获取异步操作的结果。future_status 有三种状态:

  • deferred:异步操作还没开始

  • ready:异步操作已经完成

  • timeout:异步操作超时

//查询future的状态
std::future_status status;
do {
    status = future.wait_for(std::chrono::seconds(1));
    if (status == std::future_status::deferred) {
        std::cout << "deferred\n";
    } else if (status == std::future_status::timeout) {
        std::cout << "timeout\n";
    } else if (status == std::future_status::ready) {
        std::cout << "ready!\n";
} while (status != std::future_status::ready);

获取 future 结果有三种方式:get、wait、wait_for,其中 get 等待异步操作结束并返回结果,wait只是等待异步操作完成,没有返回值,wait_for是超时等待返回结果。

std::promise

std::promise 为获取线程函数中的某个值提供便利,在线程函数中给外面传进来的 promise 赋值,当线程函数执行完成之后就可以通过 promis 获取该值了,值得注意的是取值是间接的通过 promise 内部提供的 future 来获取的。它的基本用法:

std::promise<int> pr;
std::thread t([](std::promise<int>& p){
    p.set_value_at_thread_exit(9);
},std::ref(pr));
std::future<int> f = pr.get_future();
auto r = f.get();

std::packaged_task

std::packaged_task 它包装了一个可调用的目标(如 function, lambda expression, bind expression, or another function object),以便异步调用,它和 promise 在某种程度上有点像,promise 保存了一个共享状态的值,而packaged_task保存的是一 个函数。它的基本用法:

std::packaged_task<int()> task([](){ return 7; });
std::thread t1(std::ref(task));
std::future<int> f1 = task.get_future();
auto r1 = f1.get();

std::promise、std::packaged_task 和 std::future 的关系

看了std::async相关的几个对象std::futurestd::promisestd::packaged_task,其中 std::promisestd::packaged_task的结果最终都是通过其内部的 future 返回出来的,看看他们之间的关系到底是怎样的,std::future提供了一个访问异步操作结果的机制,它和线程是一个级别的属于低层次的对象,在它之上高一层的是std::packaged_taskstd::promise,他们内部都有 future 以便访问异步操作结果,std::packaged_task包装的是一个异步操作,而std::promise包装的是一个值,都是为了方便异步操作的,因为有时需要获取线程中的某个值,这时就用std::promise,而有时需要获一个异步操作的返回值,这时就用std::packaged_task

std::promisestd::packaged_task之间又是什么关系呢?说他们没关系也没关系,说他们有关系也有关系,都取决于如何使用他们了,可以将一个异步操作的结果保存到std::promise中。

为什么要用std::async代替线程的创建

std::async是为了让开发者的少费点脑子的,它让这三个对象默契的工作。大概的工作过程是这样的:std::async先将异步操作用std::packaged_task包 装起来,然后将异步操作的结果放到std::promise中,这个过程就是创造未来的过程。外面再通过future.get/wait来获取这个未来的结果!

现在来看看std::async的原型

async(std::launch::async | std::launch::deferred, f, args...) 第一个参数是线程的创建策略,有两种策略,默认的策略是立即创建线程:

std::launch::async:在调用 async 就开始创建线程。

std::launch::deferred:延迟加载方式创建线程。调用 async 时不创建线程,直到调用了futureget或者wait时才创建线程。

第二个参数是线程函数,第三个参数是线程函数的参数。

std::async基本用法

std::future<int> f1 = std::async(std::launch::async, []() {
    return 8;
    });
cout << f1.get() << endl; //output: 8
std::future<void> f2 = std::async(std::launch::async, []() {
    cout << 8 << endl;
    //return 8;
    });
f2.wait(); //output: 8
std::future<int> future = std::async(std::launch::async, []() {
    std::this_thread::sleep_for(std::chrono::seconds(3));
    return 8;
    });
std::cout << "waiting...\n";
//Test12();
std::future_status status;
Sleep(3000);
do {
    status = future.wait_for(std::chrono::seconds(1));
    if (status == std::future_status::deferred) {
        std::cout << "deferred\n";
    }
    else if (status == std::future_status::timeout) {
        std::cout << "timeout\n";
    }
    else if (status == std::future_status::ready) {
        std::cout << "ready!\n";
    }
} while (status != std::future_status::ready);
std::cout << "result is " << future.get() << '\n';

可能的结果:waiting… timeout timeout ready! result is 8

总结

std::async是更高层次上的异步操作,它的存在可以使开发者不用关注线程创建内部细节,就能方便的获取异步执行状态和结果,还可以指定线程创建策略,应该用std::async替代线程的创建,让它成为做异步操作的首选。

C++11 新特性之智能指针

c++11 引入了三种智能指针:

  • std::shared_ptr

  • std::weak_ptr

  • std::unique_ptr

shared_ptr

shared_ptr 使用了引用计数,每一个 shared_ptr 的拷贝都指向相同的内存,每次拷贝都会触发引用计数+1,每次生命周期结束析构的时候引用计数-1,在最后一个 shared_ptr 析构的时候,内存才会释放。

使用方法如下:

struct ClassWrapper {
    ClassWrapper() {
        cout << "construct" << endl;
        data = new int[10];
    }
    ~ClassWrapper() {
        cout << "deconstruct" << endl;
        if (data != nullptr) {
            delete[] data;
        }
    }
    void Print() {
        cout << "print" << endl;
    }
    int* data;
};

void Func(std::shared_ptr<ClassWrapper> ptr) {
    ptr->Print();
}

int main() {
    auto smart_ptr = std::make_shared<ClassWrapper>();
    auto ptr2 = smart_ptr; // 引用计数+1
    ptr2->Print();
    Func(smart_ptr); // 引用计数+1
    smart_ptr->Print();
    ClassWrapper *p = smart_ptr.get(); // 可以通过get获取裸指针
    p->Print();
    return 0;
}

智能指针还可以自定义删除器,在引用计数为 0 的时候自动调用删除器来释放对象的内存,代码如下:

std::shared_ptr<int> ptr(new int, [](int *p){ delete p; });

关于 shared_ptr 有几点需要注意:

• 不要用一个裸指针初始化多个 shared_ptr,会出现 double_free 导致程序崩溃

• 通过 shared_from_this()返回 this 指针,不要把 this 指针作为 shared_ptr 返回出来,因为 this 指针本质就是裸指针,通过 this 返回可能 会导致重复析构,不能把 this 指针交给智能指针管理。

class A {
    shared_ptr<A> GetSelf() {
        return shared_from_this();
        // return shared_ptr<A>(this); 错误,会导致double free
    }
};
  • 尽量使用 make_shared,少用 new。

  • 不要 delete get()返回来的裸指针。

  • 不是 new 出来的空间要自定义删除器。

  • 要避免循环引用,循环引用导致内存永远不会被释放,造成内存泄漏。

using namespace std;
struct A;
struct B;

struct A {
    std::shared_ptr<B> bptr;
    ~A() {
        cout << "A delete" << endl;
    }
};

struct B {
    std::shared_ptr<A> aptr;
    ~B() {
        cout << "B delete" << endl;
    }
};

int main() {
    auto aaptr = std::make_shared<A>();
    auto bbptr = std::make_shared<B>();
    aaptr->bptr = bbptr;
    bbptr->aptr = aaptr;
    return 0;
}

上面代码,产生了循环引用,导致 aptr 和 bptr 的引用计数为 2,离开作用域后 aptr 和 bptr 的引用计数-1,但是永远不会为 0,导致指针永远不会析构,产生了内存泄漏,如何解决这种问题呢,答案是使用 weak_ptr。

weak_ptr

weak_ptr 是用来监视 shared_ptr 的生命周期,它不管理 shared_ptr 内部的指针,它的拷贝的析构都不会影响引用计数,纯粹是作为一个旁观者监视 shared_ptr 中管理的资源是否存在,可以用来返回 this 指针和解决循环引用问题。

  • 作用 1:返回 this 指针,上面介绍的 shared_from_this()其实就是通过 weak_ptr 返回的 this 指针。
  • 作用 2:解决循环引用问题。
struct A;
struct B;

struct A {
    std::shared_ptr<B> bptr;
    ~A() {
        cout << "A delete" << endl;
    }
    void Print() {
        cout << "A" << endl;
    }
};

struct B {
    std::weak_ptr<A> aptr; // 这里改成weak_ptr
    ~B() {
        cout << "B delete" << endl;
    }
    void PrintA() {
        if (!aptr.expired()) { // 监视shared_ptr的生命周期
            auto ptr = aptr.lock();
            ptr->Print();
        }
    }
};

int main() {
    auto aaptr = std::make_shared<A>();
    auto bbptr = std::make_shared<B>();
    aaptr->bptr = bbptr;
    bbptr->aptr = aaptr;
    bbptr->PrintA();
    return 0;
}

输出:

A
A delete
B delete

unique_ptr

std::unique_ptr 是一个独占型的智能指针,它不允许其它智能指针共享其内部指针,也不允许 unique_ptr 的拷贝和赋值。使用方法和 shared_ptr 类似,区别是不可以拷贝:

using namespace std;

struct A {
    ~A() {
        cout << "A delete" << endl;
    }
    void Print() {
        cout << "A" << endl;
    }
};


int main() {
    auto ptr = std::unique_ptr<A>(new A);
    auto tptr = std::make_unique<A>(); // error, c++11还不行,需要c++14
    std::unique_ptr<A> tem = ptr; // error, unique_ptr不允许移动
    ptr->Print();
    return 0;
}
上一页