面试必备-C++11新特性

本文的笔记来自于b站视频的爱编程的大丙,博客链接:https://subingwen.cn/,有做了相应的补充!

一、字符串原始字面量

在 C++11 中添加了定义原始字符串的字面量,定义方式为:R “xxx(原始字符串)xxx” 其中()两边的字符串可以省略。原始字面量 R 可以直接表示字符串的实际含义,而不需要额外对字符串做转译或连接等操作。

比如:编程过程中,使用的字符串中常带有一些特殊字符,对于这些字符往往要做专门的处理,使用了原始字面量就可以轻松的解决这个问题了,比如打印路径:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string str = "D:\hello\world\test.text";
    cout << str << endl;
    string str1 = "D:\\hello\\world\\test.text";
    cout << str1 << endl;
    string str2 = R"(D:\hello\world\test.text)";
    cout << str2 << endl;

    return 0;
}

 结果: 

D:helloworld    est.text
D:\hello\world\test.text
D:\hello\world\test.text
  • 在 D:\hello\world\test.text 中 \h 和 \w 转译失败,对应的字符会原样输出;

  • 在 D:\\hello\\world\\test.text 中路径的间隔符为 \ 但是这个字符又是转译字符,因此需要使用转译字符将其转译,最终才能得到一个没有特殊含义的普通字符 \;

  • 在 R"(D:\hello\world\test.text)" 使用了原始字面量 R()中的内容就是描述路径的原始字符串,无需做任何处理;

通过测试可以看到,使用原始字面量 R 可以直接得到其原始意义的字符串,再看一个输出 HTML 标签的例子:

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string str = "<html>\
        <head>\
        <title>\
        海贼王\
        </title>\
        </head>\
        <body>\
        <p>\
        我是要成为海贼王的男人!!!\
        </p>\
        </body>\
        </html>";
    cout << str << endl;
    return 0;
}

在 C++11 之前如果一个字符串分别写到了不同的行里边,需要加连接符,这种方式不仅繁琐,还破坏了表达式的原始含义,如果使用原始字面量就变得简单很多,很强直观,可读性强。

#include<iostream>
#include<string>
using namespace std;
int main()
{
    string str = R"(<html>
        <head>
        <title>
        海贼王
        </title>
        </head>
        <body>
        <p>
        我是要成为海贼王的男人!!!
        </p>
        </body>
        </html>)";
    cout << str << endl;

    return 0;
}

 最后强调一个细节:在R “xxx(raw string)xxx” 中,原始字符串必须用括号()括起来,括号的前后可以加其他字符串,所加的字符串会被忽略,并且加的字符串必须在括号两边同时出现

二、指针空值类型-nullptr

在C++程序开发中,为了提高程序的健壮性,一般在定义指针的同时完成初始化操作,或者在指针的指向尚未明确的情况下,都会给指针初始化为NULL,避免产生野指针(没有明确指向的指针,操作也这种指针极可能导致程序异常)。

C++11之前,将一个指针初始化为空指针的方式有2种:

char* ptr=0;
char* ptr=NULL;

 在底层源码中NULL这个宏是这样定义的:

#ifndef NULL
   #ifdef __cplusplus
      #define NULL 0
   #else
      #define NULL ((void*)0)
   #endif
#endif

 也就是说如果源码是C++程序NULL就是0,如果是C程序NULL表示为(void*)0。那么为什么要这样做呢?是由于C++中,void*类型无法隐式转换为其它类型的指针,此时使用0代替(void*)0,用于解决空指针的问题。这个0(0x00000000)表示的就是虚拟地址空间中的0的地址,这块地址是只读的。

C++中将NULL定义为字面常量0,并不能保证在所有场景下能很好的工作,比如,函数重载时,NULL和0无法区分,

#include <iostream>
using namespace std;

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

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

int main(int argc,char* argv[]){
    func(NULL);   // 想要调用重载函数 void func(char *p)
    func(250);    // 想要调用重载函数 void func(int p)

    return 0;
}

结果: 

void func(int p)
void func(int p) 

通过打印结果可以看成,虽然调用func(NULL);最终链接到的还是void func(int p)和预期是不一样的,其实这个原因是在C++中NULL和0是等价的。

处于兼容性考虑,C++11标准并没有对NULL的宏定义做任何修改,而另起炉灶,引入了一个新的关键字nullptr。nullptr专用于初始化空类型指针,不同类型的指针变量都可以使用nullptr来初始化:

int*    ptr1 = nullptr;
char*   ptr2 = nullptr;
double* ptr3 = nullptr;

 对应上面的代码编译器会分别将 nullptr 隐式转换成 int*、char* 以及 double* 指针类型。

使用 nullptr 可以很完美的解决上边提到的函数重载问题:

#include <iostream>
using namespace std;

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

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

int main(int argc,char* argv[]){
    func(nullptr);
    func(250);
    return 0;
}

结果:

void func(char *p)
void func(int p)

通过输出的结果可以看出,nullptr无法隐式转换为整形,但是可以隐式匹配指针类型。在C++11标准下,相比NULL和0,使用nullptr初始化空指针可以令我们编写的代码更加稳定! 

三、constexptr修饰常量表达式

const与constexptr的主要区别:

  • const变量的初始化在编译、运行初始化(函数参数),可以利用变量初始化,对等号右边的变量没有约束的;
  • constexptr必须在编译时进行初始化,其值不会改变而且在编译过程中能够得到计算结果的表达式,因此声明constexptr的变量一定是常量,对等号右边的变量有约束,必须用常量表达式来初始化;

1、const

在C++11之前只有const关键字,从功能上来说这个关键字有双重语义:变量只读(比如:修饰函数参数等),修饰常量,

void func(const int num){
    const int count = 24;
    int array[num];            // error,num是一个只读变量,不是常量
    int array1[count];         // ok,count是一个常量

    int a1 = 520;
    int a2 = 250;
    const int& b = a1;
    b = a2;                         // error
    a1 = 1314;
    cout << "b: " << b << endl;     // 输出结果为1314
}

  • 函数voidfunc(const int num)的参数num表示这个变量是只读的,但不是常量,因此使用int array[num]这种方式定义一个数组,编译器是会报错的,提示num不可作为常量来使用;
  • const int count=24中的count却是一个常量,因此可以使用这个常量来定义一个静态数组;

此外,变量只读并不等价于常量,二者是两个不同的概念,不能混为一谈,

int a1=10,a2=20;
const int&b=a1;
b=a2;
  • b是一个常量引用,因此b引用的变量是不能被修改的,也就是b=a2这句话存在语法错误!
  • 在const对于变量a1是没有任何约束的,a1的值变了b的值也就变了!
  • 引用b是只读的,但是并不能保证它的值是可以改变的名,也就是说它不是常量!

2 、constexpr

 在C++11中添加了新的关键字constexpr,这个关键字是用来修饰常量表达式的。常量表达式指的是由多个(>=1)常量(值是不会改变)组成并且在编译过程中就能得到计算结果的表达式

C++程序从编写完毕到执行分为四个阶段:预处理、编译、汇编、链接这4个阶段,得到可执行程序之后就能运行了!需要额外强调的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果,但是常量表达式往往发生在程序的编译的阶段,这可以极大提高程序的执行效率!因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。

那么问题来了,编译器如何识别表达式是不是常量表达式呢?在 C++11 中添加了 constexpr 关键字之后就可以在程序中使用它来修改常量表达式,用来提高程序的执行效率。在使用中建议将 const 和 constexpr 的功能区分开,即凡是表达“只读”语义的场景都使用 const,表达“常量”语义的场景都使用 constexpr。

在定义常量时,const 和 constexpr 是等价的,都可以在程序的编译阶段计算出结果,例如:

const int m = f();  // 不是常量表达式,m的值只有在运行时才会获取。
const int i=520;    // 是一个常量表达式
const int j=i+1;    // 是一个常量表达式

constexpr int i=520;    // 是一个常量表达式
constexpr int j=i+1;    // 是一个常量表达式

对于 C++ 内置类型的数据,可以直接用 constexpr 修饰,但如果是自定义的数据类型(用 struct 或者 class 实现),直接用 constexpr 修饰是不行的。

// 此处的constexpr修饰是无效的
constexpr struct Test{
    int id;
    int num;
};

 如果要定义一个结构体 / 类常量对象,可以这样写:

struct Test{
    int id;
    int num;
};

int main(int argc,char* argv[]){
    constexpr Test t{ 1, 2 };
    constexpr int id = t.id;
    constexpr int num = t.num;
    // error,不能修改常量
    t.num += 100;
    cout << "id: " << id << ", num: " << num << endl;

    return 0;
}

在第 13 行的代码中 t.num += 100; 的操作是错误的,对象 t 是一个常量,因此它的成员也是常量,常量是不能被修改的。

3、 修饰函数

constexpr 并不能修改任意函数的返回值,时这些函数成为常量表达式函数,必须要满足以下几个条件,

(1)函数必须要有返回值,并且 return 返回的表达式必须是常量表达式。

// error,不是常量表达式函数
constexpr void func1(){
    int a = 100;
    cout << "a: " << a << endl;
}

// error,不是常量表达式函数
constexpr int func1(){
    int a = 100;
    return a;
}
  • 函数 func1() 没有返回值,不满足常量表达式函数要求;

  • 函数 func2() 返回值不是常量表达式,不满足常量表达式函数要求;

(2)函数在使用之前,必须有对应的定义语句。

#include <iostream>
using namespace std;

constexpr int func1();
int main(int argc,char* argv[]){
    constexpr int num = func1();	// error
    return 0;
}

constexpr int func1(){
    constexpr int a = 100;
    return a;
}

在测试程序 constexpr int num = func1(); 中,还没有定义 func1() 就直接调用了,应该将 func1() 函数的定义放到 main() 函数的上边。

(3)整个函数的函数体中,不能出现非常量表达式之外的语句(using 指令、typedef 语句以及 static_assert 断言、return 语句除外)

// error
constexpr int func1(){
    constexpr int a = 100;
    constexpr int b = 10;
    for (int i = 0; i < b; ++i)
    {
        cout << "i: " << i << endl;
    }
    return a + b;
}

// ok
constexpr int func2(){
    using mytype = int;
    constexpr mytype a = 100;
    constexpr mytype b = 10;
    constexpr mytype c = a * b;
    return c - (a + b);
}

因为 func1() 是一个常量表达式函数,在函数体内部是不允许出现非常量表达式以外的操作,因此函数体内部的 for 循环是一个非法操作。

以上三条规则不仅对应普通函数适用,对应类的成员函数也是适用的:

class Test{
public:
    constexpr int func(){
        constexpr int var = 100;
        return 5 * var;
    }
};

int main(int argc,char* argv[]){
    Test t;
    constexpr int num = t.func();
    cout << "num: " << num << endl;

    return 0;
}

4、 修饰模板函数

C++11 语法中,constexpr 可以修饰函数模板,但由于模板中类型的不确定性,因此函数模板实例化后的模板函数是否符合常量表达式函数的要求也是不确定的。如果 constexpr 修饰的模板函数实例化结果不满足常量表达式函数的要求,则 constexpr 会被自动忽略,即该函数就等同于一个普通函数。

#include <iostream>
using namespace std;

struct Person {
    const char* name;
    int age;
};

// 定义函数模板
template<typename T>
constexpr T dispaly(T t) {
    return t;
}

int main(int argc,char* argv[]){
    struct Person p { "luffy", 19 };
    //普通函数
    struct Person ret = dispaly(p);
    cout << "luffy's name: " << ret.name << ", age: " << ret.age << endl;

    //常量表达式函数
    constexpr int ret1 = dispaly(250);
    cout << ret1 << endl;

    constexpr struct Person p1 { "luffy", 19 };
    constexpr struct Person p2 = dispaly(p1);
    cout << "luffy's name: " << p2.name << ", age: " << p2.age << endl;
    return 0;
}

在上面示例程序中定义了一个函数模板 display(),但由于其返回值类型未定,因此在实例化之前无法判断其是否符合常量表达式函数的要求:

  • struct Person ret = dispaly(p); 由于参数 p 是变量,所以实例化后的函数不是常量表达式函数,此时 constexpr 是无效的;

  • constexpr int ret1 = dispaly(250); 参数是常量,符合常量表达式函数的要求,此时 constexpr 是有效的;

  • constexpr struct Person p2 = dispaly(p1); 参数是常量,符合常量表达式函数的要求,此时 constexpr 是有效的。

5、 修饰构造函数

如果想用直接得到一个常量对象,也可以使用 constexpr 修饰一个构造函数,这样就可以得到一个常量构造函数了。常量构造函数有一个要求:构造函数的函数体必须为空,并且必须采用初始化列表的方式为各个成员赋值。

#include <iostream>
using namespace std;

struct Person {
    constexpr Person(const char* p, int age) :name(p), age(age)
    {
    }
    const char* name;
    int age;
};

int main(int argc,char* argv[]) {
    constexpr struct Person p1("luffy", 19);
    cout << "luffy's name: " << p1.name << ", age: " << p1.age << endl;
    return 0;
}

四、使用auto进行类型自动推导

一般情况下,auto在自动推导类型在编译时期确定的,但当模板函数的返回值依赖于模板的参数时,我们依旧无法在编译代码前确定模板参数的类型,故也无从知道返回值的类型,这时我们可以使用auto。

template <typename _Tx, typename _Ty>
auto multiply(_Tx x, _Ty y)->decltype(_Tx*_Ty)
{
    return x*y;
}

C++11中auto并不代表一种实际的数据类型,只是一个类型声明的“占位符”,auto并不是万能的在任意场景下都能够推导出变量的实际类型,使用auto声明的变量必须要 进行初始化,以让编译器推导出它的实际类型,在编译时将auto占位符替换成真正的类型。

auto 变量名=变量值;

 基本类型的推导,

int temp = 110;
auto *a = &temp;	
auto b = &temp;		
auto &c = temp;		
auto d = temp;		
  • 变量 a 的数据类型为 int*,因此 auto 关键字被推导为 int类型;

  • 变量 b 的数据类型为 int*,因此 auto 关键字被推导为 int* 类型;

  • 变量 c 的数据类型为 int&,因此 auto 关键字被推导为 int类;

  • 变量 d 的数据类型为 int,因此 auto 关键字被推导为 int 类型;

在来看一组带 const 限定的变量,使用 auto 进行类型推导的例子:

int tmp = 250;
const auto a1 = tmp;
auto a2 = a1;
const auto &a3 = tmp;
auto &a4 = a3;
  •  变量 a1 的数据类型为 const int,因此 auto 关键字被推导为 int 类型;
  • 变量 a2 的数据类型为 int,但是 a2 没有声明为指针或引用因此 const 属性被去掉,auto 被推导为 int;
  • 变量 a3 的数据类型为 const int&,a3 被声明为引用因此 const 属性被保留,auto 关键字被推导为 int 类型;
  • 变量 a4 的数据类型为 const int&,a4 被声明为引用因此 const 属性被保留,auto 关键字被推导为 const int 类型;

auto 关键字并不是万能的,在以下这些场景中是不能完成类型推导的, 

1、不允许使用auto的四个场景

(1)不能作为函数参数使用。因为只有在函数调用的时候才会给函数参数传递实参,auto 要求必须要给修饰的变量赋值,因此二者矛盾

int func(auto a, auto b)	// error
{	
    cout << "a: " << a <<", b: " << b << endl;
}

(2)不能用于类的非静态成员变量的初始化

class Test{
    auto v1 = 0;                    // error
    static auto v2 = 0;             // error,类的静态非常量成员不允许在类内部直接初始化
    static const auto v3 = 10;      // ok
}

(3)不能使用 auto 关键字定义数组

int func(){
    int array[] = {1,2,3,4,5};  // 定义数组
    auto t1 = array;            // ok, t1被推导为 int* 类型
    auto t2[] = array;          // error, auto无法定义数组
    auto t3[] = {1,2,3,4,5};;   // error, auto无法定义数组
}

(4)无法使用 auto 推导出模板参数

template <typename T>
struct Test{}

int func(){
    Test<double> t;
    Test<auto> t1 = t;           // error, 无法推导出模板类型
    return 0;
}

2、推荐使用auto的场景

(1)用于STL的容器遍历

#include <map>
int main(int argc,char* argv[]){
    map<int, string> person;
    map<int, string>::iterator it = person.begin();
    for (; it != person.end(); ++it)
    {
        // do something
    }
    return 0;
}

(2)用于泛型编程,在使用模板的时候,很多情况下我们不知道变量应该定义为什么类型,比如下面的代码:

#include <iostream>
#include <string>
using namespace std;

class T1{
public:
    static int get(){
        return 10;
    }
};

class T2{
public:
    static string get(){
        return "hello, world";
    }
};

template <class A>
void func(void){
    auto val = A::get();
    cout << "val: " << val << endl;
}

int main(int argc,char* argv[]){
    func<T1>();
    func<T2>();
    return 0;
}

在这个例子中定义了泛型函数 func,在函数中调用了类 A 的静态方法 get () ,这个函数的返回值是不能确定的,如果不使用 auto,就需要再定义一个模板参数,并且在外部调用时手动指定 get 的返回值类型,具体代码如下:

#include <iostream>
#include <string>
using namespace std;

class T1{
public:
    static int get()
    {
        return 0;
    }
};

class T2{
public:
    static string get()
    {
        return "hello, world";
    }
};

template <class A, typename B>        // 添加了模板参数 B
void func(void){
    B val = A::get();
    cout << "val: " << val << endl;
}

int main(int argc,char* argv[])
{
    func<T1, int>();                  // 手动指定返回值类型 -> int
    func<T2, string>();               // 手动指定返回值类型 -> string
    return 0;
}

五、decltype类型推导规则

在某些情况下,不需要或者不能定义变量,但是希望得到某种类型,这时候就可以使用 C++11 提供的 decltype 关键字了,它的作用是在编译器编译的时候推导出一个表达式的类型,语法格式如下,

decltype (表达式)

decltype 是 “declare type” 的缩写,意思是 “声明类型”。decltype 的推导是在编译期完成的,它只是用于表达式类型的推导,并不会计算表达式的值。来看一组简单的例子,

int a = 10;
decltype(a) b = 99;                 // b -> int
decltype(a+3.14) c = 52.13;         // c -> double
decltype(a+b*c) d = 520.1314;       // d -> double

 可以看到 decltype 推导的表达式可简单可复杂,在这一点上 auto 是做不到的,auto 只能推导已初始化的变量类型

1、decltype类型推导规则

通过上面的例子我们初步感受了一下 decltype 的用法,但不要认为 decltype 就这么简单,在它简单的背后隐藏着很多的细节,下面分三个场景依次讨论一下:

(1)表达式为普通变量或者普通表达式或者类表达式,在这种情况下,使用 decltype 推导出的类型和表达式的类型是一致的

#include <iostream>
#include <string>
using namespace std;

class Test{
public:
    string text;
    static const int value = 110;
};

int main(int argc,char* argv[]){
    int x = 99;
    const int &y = x;
    decltype(x) a = x;
    decltype(y) b = x;
    decltype(Test::value) c = 0;

    Test t;
    decltype(t.text) d = "hello, world";

    return 0;
}
  •  变量 a 被推导为 int 类型
  • 变量 b 被推导为 const int & 类型
  • 变量 c 被推导为 const int 类型
  • 变量 d 被推导为 string 类型

(2)表达式是函数调用,使用 decltype 推导出的类型和函数返回值一致

class Test{...};
//函数声明
int func_int();                 // 返回值为 int
int& func_int_r();              // 返回值为 int&
int&& func_int_rr();            // 返回值为 int&&

const int func_cint();          // 返回值为 const int
const int& func_cint_r();       // 返回值为 const int&
const int&& func_cint_rr();     // 返回值为 const int&&

const Test func_ctest();        // 返回值为 const Test

//decltype类型推导
int n = 100;
decltype(func_int()) a = 0;		
decltype(func_int_r()) b = n;	
decltype(func_int_rr()) c = 0;	
decltype(func_cint())  d = 0;	
decltype(func_cint_r())  e = n;	
decltype(func_cint_rr()) f = 0;	
decltype(func_ctest()) g = Test();	
  • 变量 a 被推导为 int 类型

  • 变量 b 被推导为 int& 类型

  • 变量 c 被推导为 int&& 类型

  • 变量 d 被推导为 int 类型

  • 变量 e 被推导为 const int & 类型

  • 变量 f 被推导为 const int && 类型

  • 变量 g 被推导为 const Test 类型

函数 func_cint () 返回的是一个纯右值(在表达式执行结束后不再存在的数据,也就是临时性的数据),对于纯右值而言,只有类类型可以携带const、volatile限定符,除此之外需要忽略掉这两个限定符,因此推导出的变量 d 的类型为 int 而不是 const int。

(3)表达式是一个左值,或者被括号 ( ) 包围,使用 decltype 推导出的是表达式类型的引用(如果有 const、volatile 限定符不能忽略)

#include <iostream>
#include <vector>
using namespace std;

class Test{
public:
    int num;
};

int main(int argc,char* argv[]) {
    const Test obj;
    //带有括号的表达式
    decltype(obj.num) a = 0;
    decltype((obj.num)) b = a;
    //加法表达式
    int n = 0, m = 0;
    decltype(n + m) c = 0;
    decltype(n = n + m) d = n;
    return 0;
}
  • obj.num 为类的成员访问表达式,符合场景 1,因此 a 的类型为 int

  • obj.num 带有括号,符合场景 3,因此 b 的类型为 const int&

  • n+m 得到一个右值,符合场景 1,因此 c 的类型为 int

  • n=n+m 得到一个左值 n,符合场景 3,因此 d 的类型为 int&

2、decltype在泛型编程中的使用规则

关于 decltype 的应用多出现在泛型编程中。比如我们编写一个类模板,在里边添加遍历容器的函数,操作如下,

#include <iostream>
#include <list>
using namespace std;

template <class T>
class Container{
public:
    void func(T& c){
        for (m_it = c.begin(); m_it != c.end(); ++m_it){
            cout << *m_it << " ";
        }
        cout << endl;
    }
private:
    ??? m_it;  // 这里不能确定迭代器类型
};

int main(int argc,char* argv[]){
    const list<int> lst;
    Container<const list<int>> obj;
    obj.func(lst);
    return 0;
}

在程序的第 17 行出了问题,关于迭代器变量一共有两种类型:只读(T::const_iterator)和读写(T::iterator),有了 decltype 就可以完美的解决这个问题了,当 T 是一个 非 const 容器得到一个 T::iterator,当 T 是一个 const 容器时就会得到一个 T::const_iterator。

#include <list>
#include <iostream>
using namespace std;

template <class T>
class Container{
public:
    void func(T& c){
        for (m_it = c.begin(); m_it != c.end(); ++m_it){
            cout << *m_it << " ";
        }
        cout << endl;
    }
private:
    decltype(T().begin()) m_it;  // 这里不能确定迭代器类型
};

int main(int argc,char* argv[]){
    const list<int> lst{ 1,2,3,4,5,6,7,8,9 };
    Container<const list<int>> obj;
    obj.func(lst);
    return 0;
}

3、返回值类型后置

在泛型编程中,可能需要通过参数的运算来得到返回值的类型,比如下面这个场景,

#include <iostream>
using namespace std;
// R->返回值类型, T->参数1类型, U->参数2类型
template <typename R, typename T, typename U>
R add(T t, U u){
    return t + u;
}

int main(int argcc,char* argv[]){
    int x = 520;
    double y = 13.14;
    // auto z = add<decltype(x + y), int, double>(x, y);
    auto z = add<decltype(x + y)>(x, y);	// 简化之后的写法
    cout << "z: " << z << endl;
    return 0;
}

关于返回值,从上面的代码可以推断出和表达式 t+u 的结果类型是一样的,因此可以通过通过 decltype 进行推导,关于模板函数的参数 t 和 u 可以通过实参自动推导出来,因此在程序中就也可以不写。虽然通过上述方式问题被解决了,但是解决方案有点过于理想化,因为对于调用者来说,是不知道函数内部执行了什么样的处理动作的。

因此如果要想解决这个问题就得直接在 add 函数身上做文章,先来看第一种写法:

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

当我们在编译器中将这几行代码改出来后就直接报错了,因此 decltype 中的 t 和 u 都是函数参数,直接这样写相当于变量还没有定义就直接用上了,这时候变量还不存在,有点心急了。

在C++11中增加了返回类型后置语法,说明白一点就是将decltype和auto结合起来完成返回类型的推导。其语法格式如下:

// 符号 -> 后边跟随的是函数返回值的类型
auto func(参数1, 参数2, ...) -> decltype(参数表达式)

通过对上述返回类型后置语法代码的分析,得到结论:auto 会追踪 decltype() 推导出的类型,因此上边的 add() 函数可以做如下的修改:

#include <iostream>
using namespace std;

template <typename T, typename U>
// 返回类型后置语法
auto add(T t, U u) -> decltype(t+u) 
{
    return t + u;
}

int main(int argc,char* argv[]){
    int x = 520;
    double y = 13.14;
    // auto z = add<int, double>(x, y);
    auto z = add(x, y);		// 简化之后的写法
    cout << "z: " << z << endl;
    return 0;
}

 为了进一步说明这个语法,我们再看一个例子:

#include <iostream>
using namespace std;

int& test(int &i){
    return i;
}

double test(double &d){
    d = d + 100;
    return d;
}

template <typename T>
// 返回类型后置语法
auto myFunc(T& t) -> decltype(test(t))
{
    return test(t);
}

int main(int argc,char* argv[]){    
    int x = 520;
    double y = 13.14;
    // auto z = myFunc<int>(x);
    auto z = myFunc(x);             // 简化之后的写法
    cout << "z: " << z << endl;

    // auto z = myFunc<double>(y);
    auto z1 = myFunc(y);            // 简化之后的写法
    cout << "z1: " << z1 << endl;
    return 0;
}

在这个例子中,通过 decltype 结合返回值后置语法很容易推导出来 test(t) 函数可能出现的返回值类型,并将其作用到了函数 myFunc() 上。

// 输出结果
z: 520
z1: 113.14

六、final关键字的使用

C++中增加了final关键字来限制某个类不能被继承,或者某个虚函数不能被重写,和Java的final关键字的功能是类型的。如果使用final修饰函数,只能修饰虚函数,并且要把final关键字放到类或者函数后面

1、修饰函数

如果使用final修饰函数,只能修饰虚函数,这样就能阻止子类重写父类的这个函数了。

class Base{
public:
    virtual void test(){
        cout << "Base class...";
    }
};

class Child : public Base{
public:
    void test() final{//修饰虚函数
        cout << "Child class...";
    }
};

class GrandChild : public Child{
public:
    // 语法错误, 不允许重写
    void test(){
        cout << "GrandChild class...";
    }
};

一共三个类:

  • 基类:Base
  • 子类:Child
  • 孙子类:GrandChild 

test() 是基类中的一个虚函数,在子类中重写了这个方法,但是不希望孙子类中继续重写这个方法了,因此在子类中将 test() 方法标记为 final,孙子类中对这个方法就只有使用的份了。 

2、修饰类

使用final关键字修饰过的类是不允许被继承的,也就是说这个类不能有派生类!

class Base{
public:
    virtual void test(){
        cout << "Base class...";
    }
};

class Child final: public Base{
public:
    void test(){
        cout << "Child class...";
    }
};

// error, 语法错误
class GrandChild : public Child{
public:
};

 Child 类是被 final 修饰过的,因此 Child 类不允许有派生类 GrandChild 类的继承是非法的,Child 是个断子绝孙的类。

七、override关键字的使用

重写父类的方法必须有关键字virtual,即为虚函数,为了实现多态性。什么是多态?

  1. 有继承关系;
  2. 子类重写父类的虚函数;
  3. 父类指针或引用指向子类对象;
  • 重载是在同一个作用域或同一类内,函数名字相同,函数参数类型、个数、顺序不同;是在编译过程中实现!
  • 重写是子类关系,子类重新定义父类的虚函数,要求函数名,函数参数、类型、个数 、顺序以及返回值都一致!多态是在代码运行过程中实现!
  • 隐藏是作用在父子类中,子类重新定义父类的公有非虚函数,在定义子类对象时,只会调用子类的方法,自动屏蔽父类的方法,以致子类调用父类的方法失败,若需要调用,需要加上父类的作用域!

override关键字确保在派生类中声明的重写函数与基类的虚函数有相同的签名,同时也明确将会重写基类的虚函数,这样就可以保证重写的虚函数的正确性,也提高代码的可读性,和final一样这个关键字要写道方法的后面。

class Base{
public:
    virtual void test(){
        cout << "Base class...";
    }
};

class Child : public Base{
public:
    void test() override{
        cout << "Child class...";
    }
};

class GrandChild : public Child{
public:
    void test() override{
        cout << "Child class...";
    }
};

上述代码中第 13 行和第 22 行就是显示指定了要重写父类的 test() 方法,使用了 override 关键字之后,假设在重写过程中因为误操作,写错了函数名或者函数参数或者返回值编译器都会提示语法错误,提高了程序的正确性,降低了出错的概率。

八、对模板右尖括号的优化

在泛型编程中,模板实例化有一个非常繁琐的地方,那就是连续的两个右尖括号(>>)会被编译器解析成右移操作符,而不是模板参数表的结束。我们先来看一段关于容器遍历的代码,在创建的类模板 Base 中提供了遍历容器的操作函数 traversal():

// test.cpp
#include <iostream>
#include <vector>
using namespace std;

template <typename T>
class Base{
public:
    void traversal(T& t){
        auto it = t.begin();
        for (; it != t.end(); ++it){
            cout << *it << " ";
        }
        cout << endl;
    }
};


int main(int argc,char* argv[]){
    vector<int> v{ 1,2,3,4,5,6,7,8,9 };
    Base<vector<int>> b;
    b.traversal(v);

    return 0;
}

如果使用 C++98/03 标准来编译上边的这段代码,就会得到如下的错误提示:

test.cpp:25:20: error: '>>' should be '> >' within a nested template argument list
     Base<vector<int>> b;

根据错误提示中描述模板的两个右尖括之间需要添加空格,这样写起来就非常的麻烦,C++11改进了编译器的解析规则,尽可能地将多个右尖括号(>)解析成模板参数结束符,方便我们编写模板相关的代码。

上面的这段代码,在支持 C++11 的编译器中编译是没有任何问题的,如果使用 g++ 直接编译需要加参数 -std=c++11:

$ g++ test.cpp -std=c++11 -o app

九、函数模板的默认模板参数

当默认模板参数和模板参数自动推导同时使用时(优先级从高到低):

template<typename T=long,typename U=int>
void test(T t='A',U u='b'){
   cout<<"t: "<<t<<" "<<"u: "<<u<<endl;
}
  • 如果可以使用推导出参数类型则使用推导出的类型;
  • 如果函数模板无法推导出参数类型,那么编译器会使用默认模板参数;
  • 如果无法推导出模板参数类型并且没有设置默认模板参数,编译器就会报错; 

在 C++98/03 标准中,类模板可以有默认的模板参数:

#include <iostream>
using namespace std;

template <typename T=int, T t=520>
class Test{
public:
    void print(){
        cout << "current value: " << t << endl;
    }
};

int main(int argc,char* argv[]){
    Test<> t;
    t.print();

    Test<int, 1024> t1;
    t1.print();

    return 0;
}

但是不支持函数的默认模板参数,在C++11中添加了对函数模板默认参数的支持:

#include <iostream>
using namespace std;

template <typename T=int>	// C++98/03不支持这种写法, C++11中支持这种写法
void func(T t){
    cout << "current value: " << t << endl;
}

int main(int argc,char* argv[])
{
    func(100);
    return 0;
}

通过上面的例子可以得到如下结论:当所有模板参数都有默认参数时,函数模板的调用如同一个普通函数。但对于类模板而言,哪怕所有参数都有默认参数,在使用时也必须在模板名后跟随 <> 来实例化。

另外:函数模板的默认模板参数在使用规则上和其他的默认参数也有一些不同,它没有必须写在参数表最后的限制。这样当默认模板参数和模板参数自动推导结合起来时,书写就显得非常灵活了。我们可以指定函数模板中的一部分模板参数使用默认参数,另一部分使用自动推导,比如下面的例子:

#include <iostream>
#include <string>
using namespace std;

template <typename R = int, typename N>
R func(N arg){
    return arg;
}

int main(int argc,char* argv[]){
    auto ret1 = func(520);
    cout << "return value-1: " << ret1 << endl;

    auto ret2 = func<double>(52.134);
    cout << "return value-2: " << ret2 << endl;

    auto ret3 = func<int>(52.134);
    cout << "return value-3: " << ret3 << endl;

    auto ret4 = func<char, int>(100);
    cout << "return value-4: " << ret4 << endl;

    return 0;
}

测试代码输出的结果为:

return value-1: 520
return value-2: 52.134
return value-3: 52
return value-4: d

 根据得到的日志输出,分析一下示例代码中调用的模板函数:

  • auto ret = func(520);函数返回值类型使用了默认的模板参数,函数的参数类型是自动推导出来的为 int 类型。
  • auto ret1 = func<double>(52.134);函数的返回值指定为 double 类型,函数参数是通过实参推导出来的,为 double 类型
  • auto ret3 = func<int>(52.134);函数的返回值指定为 int 类型,函数参数是通过实参推导出来的,为 double 类型
  • auto ret4 = func<char, int>(100);函数的参数为指定为 int 类型,函数返回值指定为 char 类型,不需要推导

当默认模板参数和模板参数自动推导同时使用时(优先级从高到低):

  • 如果可以推导出参数类型则使用推导出的类型;

  • 如果函数模板无法推导出参数类型,那么编译器会使用默认模板参数;

  • 如果无法推导出模板参数类型并且没有设置默认模板参数,编译器就会报错。

#include <iostream>
#include <string>
using namespace std;

// 函数模板定义
template <typename T, typename U = char>
void func(T arg1 = 100, U arg2 = 100)
{
    cout << "arg1: " << arg1 << ", arg2: " << arg2 << endl;
}

int main(int argc,char* argv[]){
    // 模板函数调用
    func('a');
    func(97, 'a');
    // func();    //编译报错
    return 0;
}

 arg1: a, arg2: d
arg1: 97, arg2: a

分析一下调用的模板函数 func():

  • func('a'):参数 T 被自动推导为 char 类型,U 使用的默认模板参数为 char 类型
  • func(97, 'a');:参数 T 被自动推导为 int 类型,U 使用推导出的类型为 char
  • func():参数 T 没有指定默认模板类型,并且无法自动推导,编译器会直接报错;(1)模板参数类型的自动推导是根据模板函数调用时指定的实参进行推断的,没有实参则无法推导;(2)模板参数类型的自动推导不会参考函数模板中指定的默认参数

 十、通过using定义基础类型和函数指针别名

在C++用于声明命名空间,使用命名空间也可以防止命名冲突,在程序中声明了命名空间之后,就可以直接使用命名空间中的定义的类了。C++using更加轻便,更加高效、直观!

1、定义别名

定义函数指针,两种方法等效的,而且using更加容易理解!

typedef int(*func)(int,string);
using func=int(*)(int,string);

在 C++ 中可以通过 typedef 重定义一个类型,语法格式如下:

typedef 旧的类型名 新的类型名;
// 使用举例
typedef unsigned int uint_t;

被重定义的类型并不是一个新的类型,仅仅只是原有的类型取了一个新的名字。和以前的声明语句一样,这里的声明符也可以包含类型修饰,从而也能由基本数据类型构造出复合类型来。C++11 中规定了一种新的方法,使用别名声明 (alias declaration) 来定义类型的别名,即使用 using。

在使用的时候,关键字 using 作为别名声明的开始,其后紧跟别名和等号,其作用是把等号左侧的名字规定成等号右侧类型的别名。类型别名和类型的名字等价,只要是类型的名字能出现的地方,就能使用类型别名。使用typedef定义的别名和使用using定义的别名在语义上是等效的。

使用 using 定义别名的语法格式是这样的:

using 新的类型 = 旧的类型;
// 使用举例
using uint_t = int;

通过 using 和 typedef 的语法格式可以看到二者的使用没有太大的区别,假设我们定义一个函数指针,using 的优势就能凸显出来了,看一下下面的例子:

// 使用typedef定义函数指针
typedef int(*func_ptr)(int, double);

// 使用using定义函数指针
using func_ptr1 = int(*)(int, double);

如果不是特别熟悉函数指针与 typedef,第一眼很难看出 func_ptr 其实是一个别名,其本质是一个函数指针,指向的函数返回类型是 int,函数参数有两个分别是 int,double 类型。

使用 using 定义函数指针别名的写法看起来就非常直观了,把别名的名字强制分离到了左边,而把别名对应的实际类型放在了右边,比较清晰,可读性比较好。

2、使用using和typedef给模板定义别名

使用 typedef 重定义类似很方便,但是它有一点限制,比如无法重定义一个模板,比如我们需要一个固定以 int 类型为 key 的 map,它可以和很多类型的 value 值进行映射,如果使用 typedef 这样直接定义就非常麻烦:

template<typename T>
typedef map<int,T> mapType;
//上面代码在外部无法直接编译,需要在类的内部
template<typename T>
struct MyMap{
   typedef map<int,T> mapType;
}

//使用,编译通过
MyMap<double>::mapType mt;
typedef map<int, string> m1;
typedef map<int, int> m2;
typedef map<int, double> m3;

 在这种情况下我们就不自觉的想到了模板:

template <typename T>
typedef map<int, T> type;	// error, 语法错误

使用 typename 不支持给模板定义别名,这个简单的需求仅通过 typedef 很难办到,需要添加一个外敷类:

#include <iostream>
#include <functional>
#include <map>
using namespace std;

template <typename T>
// 定义外敷类
struct MyMap{
    typedef map<int, T> type;
};

int main(int argc,char* argv[]){
    MyMap<string>::type m;
    m.insert(make_pair(1, "luffy"));
    m.insert(make_pair(2, "ace"));

    MyMap<int>::type m1;
    m1.insert(1, 100);
    m1.insert(2, 200);

    return 0;
}

通过上边的例子可以直观的感觉到,需求简单但是实现起来并不容易。在 C++11 中,新增了一个特性就是可以通过使用 using 来为一个模板定义别名,使用using定义,可以在类外部,

template<typename T>
using MMap=map<int,T>;

//编译通过
MMap<string> mp;

完整示例:

#include <iostream>
#include <functional>
#include <map>
using namespace std;

template <typename T>
using mymap = map<int, T>;

int main(int argc,char* argv[]){
    // map的value指定为string类型
    mymap<string> m;
    m.insert(make_pair(1, "luffy"));
    m.insert(make_pair(2, "ace"));

    // map的value指定为int类型
    mymap<int> m1;
    m1.insert(1, 100);
    m1.insert(2, 200);

    return 0;
}

上面的例子中通过使用 using 给模板指定别名,就可以基于别名非常方便的给 value 指定相应的类型,这样使编写的程序变得更加灵活,看起来也更加简洁一些。

最后在强调一点:using 语法和 typedef 一样,并不会创建出新的类型,它们只是给某些类型定义了新的别名。using 相较于 typedef 的优势在于定义函数指针别名时看起来更加直观,并且可以给模板定义别名

 十一、委托构造函数

委托构造函数允许使用一个类中的一个构造函数调用其它的构造函数,从而简化相关变量的初始化。

委托构造函数允许使用同一个类中的一个构造函数调用其它的构造函数,从而简化相关变量的初始化。下面举例说明:

#include <iostream>
using namespace std;

class Test{
public:
    Test() {};
    Test(int max){
        this->m_max = max > 0 ? max : 100;
    }

    Test(int max, int min){
        this->m_max = max > 0 ? max : 100;              // 冗余代码
        this->m_min = min > 0 && min < max ? min : 1;   
    }

    Test(int max, int min, int mid){
        this->m_max = max > 0 ? max : 100;             // 冗余代码
        this->m_min = min > 0 && min < max ? min : 1;  // 冗余代码
        this->m_middle = mid < max && mid > min ? mid : 50;
    }

    int m_min;
    int m_max;
    int m_middle;
};

int main(int argc,char* argv[]){
    Test t(90, 30, 60);
    cout << "min: " << t.m_min << ", middle: " 
         << t.m_middle << ", max: " << t.m_max << endl;
    return 0;
}

在上面的程序中有三个构造函数,但是这三个函数中都有重复的代码,在 C++11 之前构造函数是不能调用构造函数的,加入了委托构造之后,我们就可以轻松地完成代码的优化了:

#include <iostream>
using namespace std;

class Test{
public:
    Test() {};
    Test(int max){
        this->m_max = max > 0 ? max : 100;
    }

    Test(int max, int min):Test(max){
        this->m_min = min > 0 && min < max ? min : 1;
    }

    Test(int max, int min, int mid):Test(max, min){
        this->m_middle = mid < max && mid > min ? mid : 50;
    }

    int m_min;
    int m_max;
    int m_middle;
};

int main(int argc,char* argv[]){
    Test t(90, 30, 60);
    cout << "min: " << t.m_min << ", middle: " 
         << t.m_middle << ", max: " << t.m_max << endl;
    return 0;
}

在修改之后的代码中可以看到,重复的代码全部没有了,并且在一个构造函数中调用了其他的构造函数用于相关数据的初始化,相当于是一个链式调用。在使用委托构造函数的时候还需要注意一些几个问题:

  • 这种链式的构造函数调用不能形成一个闭环(死循环),否则会在运行期抛异常。
  • 如果要进行多层构造函数的链式调用,建议将构造函数的调用的写在初始列表中而不是函数体内部,否则编译器会提示形参的重复定义。
Test(int max){
    this->m_max = max > 0 ? max : 100;
}

Test(int max, int min){
    Test(max);	// error, 此处编译器会报错, 提示形参max被重复定义
    this->m_min = min > 0 && min < max ? min : 1;
}

在初始化列表中调用了代理构造函数初始化某个类成员变量之后,就不能在初始化列表中再次初始化这个变量了。

// 错误, 使用了委托构造函数就不能再次m_max初始化了
Test(int max, int min) : Test(max), m_max(max){
    this->m_min = min > 0 && min < max ? min : 1;
}

十二、继承构造函数

C++11 中提供的继承构造函数可以让派生类直接使用基类的构造函数,而无需自己再写构造函数,尤其是在基类有很多构造函数的情况下,可以极大地简化派生类构造函数的编写。先来看没有继承构造函数之前的处理方式。

#include <iostream>
#include <string>
using namespace std;

class Base{
public:
    Base(int i) :m_i(i) {}
    Base(int i, double j) :m_i(i), m_j(j) {}
    Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}

    int m_i;
    double m_j;
    string m_k;
};

class Child : public Base{
public:
    Child(int i) :Base(i) {}
    Child(int i, double j) :Base(i, j) {}
    Child(int i, double j, string k) :Base(i, j, k) {}
};

int main(int argc,char* argv[]){
    Child c(520, 13.14, "i love you");
    cout << "int: " << c.m_i << ", double: " 
         << c.m_j << ", string: " << c.m_k << endl;
    return 0;
}

通过测试代码可以看出,在子类中初始化从基类继承的类成员,需要在子类中重新定义和基类一致的构造函数,这是非常繁琐的,C++11 中通过添加继承构造函数这个新特性完美的解决了这个问题,使得代码更加精简。

继承构造函数的使用方法是这样的:通过使用 using 类名::构造函数名(其实类名和构造函数名是一样的)来声明使用基类的构造函数,这样子类中就可以不定义相同的构造函数了,直接使用基类的构造函数来构造派生类对象。

#include <iostream>
#include <string>
using namespace std;

class Base{
public:
    Base(int i) :m_i(i) {}
    Base(int i, double j) :m_i(i), m_j(j) {}
    Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}

    int m_i;
    double m_j;
    string m_k;
};

class Child : public Base{
public:
    using Base::Base;
};

int main(int argc,char* argv[]){
    Child c1(520, 13.14);
    cout << "int: " << c1.m_i << ", double: " << c1.m_j << endl;
    Child c2(520, 13.14, "i love you");
    cout << "int: " << c2.m_i << ", double: " 
         << c2.m_j << ", string: " << c2.m_k << endl;
    return 0;
}

子类中使用using,继承父类不需要在初始化列表初始化,

using  Base::Base;

 在修改之后的子类中,没有添加任何构造函数,而是添加了 using Base::Base; 这样就可以在子类中直接继承父类的所有的构造函数,通过他们去构造子类对象了。

另外如果在子类中隐藏了父类中的同名函数,也可以通过 using 的方式在子类中使用基类中的这些父类函数:

#include <iostream>
#include <string>
using namespace std;

class Base{
public:
    Base(int i) :m_i(i) {}
    Base(int i, double j) :m_i(i), m_j(j) {}
    Base(int i, double j, string k) :m_i(i), m_j(j), m_k(k) {}

    void func(int i){
        cout << "base class: i = " << i << endl;
    }
    
    void func(int i, string str){
        cout << "base class: i = " << i << ", str = " << str << endl;
    }

    int m_i;
    double m_j;
    string m_k;
};

class Child : public Base{
public:
    using Base::Base;
    using Base::func;
    void func(){
        cout << "child class: i'am luffy!!!" << endl;
    }
};

int main(int argc,char* argv[]){
    Child c(250);
    c.func();
    c.func(19);
    c.func(19, "luffy");
    c.Base::fun(19,"luffy);//这样也可以
    return 0;
}

代码显示结果:

child class: i'am luffy!!!
base class: i = 19
base class: i = 19, str = luffy

子类中的 func() 函数隐藏了基类中的两个 func() 因此默认情况下通过子类对象只能调用无参的 func()。子类继续父类的成员方法,然后重新定义了,如果想调用父类的,不使用using或定义父类作用域,会报错 。在上面的子类代码中添加了 using Base::func; 之后,就可以通过子类对象直接调用父类中被隐藏的带参 func() 函数了。

 十三、统一的数据初始化方式

在 C++98/03 中,对应普通数组和可以直接进行内存拷贝(memcpy ())的对象是可以使用列表初始化来初始化数据的。

// 数组的初始化
int array[] = { 1,3,5,7,9 };
double array1[3] = { 1.2, 1.3, 1.4 };

// 对象的初始化
struct Person{
    int id;
    double salary;
}zhang3{ 1, 3000 };

在 C++11 中,列表初始化变得更加灵活了,来看一下下面这段初始化类对象的代码:

#include <iostream>
using namespace std;

class Test{
public:
    Test(int) {}
private:
    Test(const Test &);
};

int main(int argc,char* argv[]){
    Test t1(520);
    Test t2 = 520; 
    Test t3 = { 520 };
    Test t4{ 520 };
    int a1 = { 1314 };
    int a2{ 1314 };
    int arr1[] = { 1, 2, 3 };
    int arr2[]{ 1, 2, 3 };
    return 0;
}

 具体地来解读一下上面代码中使用的各种初始化方式:

  • t1:最中规中矩的初始化方式,通过提供的带参构造进行对象的初始化;

  • t2:语法错误,因为提供的拷贝构造函数是私有的。如果拷贝构造函数是公共的;

  • t3 和 t4:使用了 C++11 的初始化方式来初始化对象,效果和 t1 的方式是相同的。在初始时,{} 前面的等号是否书写对初始化行为没有任何影响。t3 虽然使用了等号,但是它仍然是列表初始化,因此私有的拷贝构造对它没有任何影响;

  • t1、arr1 和 t2、arr2:这两个是基础数据类型的列表初始化方式,可以看到,和对象的初始化方式是统一的;

  • t4、a2、arr2 的写法,是 C++11 中新添加的语法格式,使用这种方式可以直接在变量名后边跟上初始化列表,来进行变量或者对象的初始化。

既然使用列表初始化可以对普通类型以及对象进行直接初始化,那么在使用 new 操作符创建新对象的时候可以使用列表初始化进行对象的初始化吗?答案是肯定的,来看下面的例子:

int * p = new int{520};
double b = double{52.134};
int * array = new int[3]{1,2,3};
  •  指针p 指向了一个 new 操作符返回的内存,通过列表初始化将内存数据初始化为了 520
  • 变量b 是对匿名对象使用列表初始之后,再进行拷贝初始化。
  • 数组array 在堆上动态分配了一块内存,通过列表初始化的方式直接完成了多个元素的初始化。

除此之外,列表初始化还可以直接用在函数返回值上:

#include <iostream>
#include <string>
using namespace std;

class Person{
public:
    Person(int id, string name){
        cout << "id: " << id << ", name: " << name << endl;
    }
};

Person func(){
    return { 9527, "华安" };
}

int main(int argc,char* argv[]){
    Person p = func();
    return 0;
}

 代码中的 return { 9527, "华安" }; 就相当于 return (9527, "华安" );,直接返回了一个匿名对象。通过上面的几个例子可以看出在 C++11 使用列表初始化是非常便利的,它统一了各种对象的初始化方式,而且还让代码的书写更加简单清晰

1、使用初始化列表初始化聚合类型的变量

在C++11,初始化列表的使用范围被大大增强了,但是一些模糊的概念也随之而来,初始化列表可以用于自定义类型的初始化,但是对于一个自定义类型,初始化列表可能有两种执行结果:

#include <iostream>
#include <string>
using namespace std;

struct T1{
    int x;
    int y;
}a = {123, 321 };

struct T2{
    int x;
    int y;
    T2(int, int) : x(10), y(20) {}
}b = {123, 321 };

int main(int argc,char* argv[]){
    cout << "a.x: " << a.x << ", a.y: " << a.y << endl;
    cout << "b.x: " << b.x << ", b.y: " << b.y << endl;
    return 0;
}

程序执行的结果是这样的:

a.x: 123, a.y: 321
b.x: 10, b.y: 20

 在上边的程序中都是用列表初始化的方式对对象进行了初始化,但是得到结果却不同,对象 b 并没有被初始化列表中的数据初始化,这是为什么呢?

  • 对象 a 是对一个自定义的聚合类型进行初始化,它将以拷贝的形式使用初始化列表中的数据来初始化 T1 结构体中的成员。
  • 在结构体 T2 中自定义了一个构造函数,因此实际的初始化是通过这个构造函数完成的。

现在很多小伙伴可能就一头雾水了,同样是自定义结构体并且在创建对象的时候都使用了列表初始化来初始化对象,为什么在类内部对对象的初始化方式却不一样呢?因为如果使用列表初始化对对象初始化时,还需要判断这个对象对应的类型是不是一个聚合体,如果是初始化列表中的数据就会拷贝到对象中。

那么,使用列表初始化时,对于什么样的类型 C++ 会认为它是一个聚合体呢?

  • 普通数组可以看做是一个聚合类型
int x[]={1,2,3,4};
double y[2][2]={{1.0,2.0},{3,0,5.0}};
char arr[]={'y','r','x'};
std::string ss[]={"hell","world"};
  • 满足以下条件的类(struct、class、union)可以被看成一个聚合类:
  • 无用户自定义的构造函数;
  • 无私有或保护的非静态成员;
  • 无基类;

  • 无虚函数;

  • 类中不能有使用 {} 和 = 直接初始化的非静态数据成员;

(1)场景 1: 类中有私有成员,无法使用列表初始化进行初始化

struct T1{
    int x;
    long y;
protected:
    int z;
}t{ 1, 100, 2};		// error, 类中有私有成员, 无法使用初始化列表初始化

(2)场景 2:类中有非静态成员可以通过列表初始化进行初始化,但它不能初始化静态成员变量

struct T2{
    int x;
    long y;
protected:
    static int z;
}t{ 1, 100, 2};		// error

结构体中的静态变量 z 不能使用列表初始化进行初始化,它的初始化遵循静态成员的初始化方式。

struct T2{
    int x;
    long y;
protected:
    static int z;
}t{ 1, 100};		// ok
// 静态成员的初始化
int T2::z = 2;

2、使用初始化列表初始化非聚合类型的变量

对于聚合类型的类可以直接使用列表初始化进行对象的初始化,如果不满足聚合条件还想使用列表初始化其实也是可以的,需要在类的内部自定义一个构造函数, 在构造函数中使用初始化列表对类成员变量进行初始化:

#include <iostream>
#include <string>
using namespace std;

struct T1{
    int x;
    double y;
    // 在构造函数中使用初始化列表初始化类成员
    T1(int a, double b, int c) : x(a), y(b), z(c){}
    virtual void print(){
        cout << "x: " << x << ", y: " << y << ", z: " << z << endl;
    }
private:
    int z;
};

int main(int argc,char* argv[]){
    T1 t{ 520, 13.14, 1314 };	// ok, 基于构造函数使用初始化列表初始化类成员
    t.print();
    return 0;
}

另外,需要额外注意的是聚合类型的定义并非递归的,也就是说当一个类的非静态成员是非聚合类型时,这个类也可能是聚合类型,比如下面的这个例子:

#include <iostream>
#include <string>
using namespace std;

struct T1{
    int x;
    double y;
private:
    int z;
};

struct T2{
    T1 t1;
    long x1;
    double y1;
};

int main(int argc,char* argv[]){
    T2 t2{ {}, 520, 13.14 };
    return 0;
}

可以看到,T1 并非一个聚合类型,因为它有一个 Private 的非静态成员。但是尽管 T2 有一个非聚合类型的非静态成员 t1,T2 依然是一个聚合类型,可以直接使用列表初始化的方式进行初始化。

最后强调一下 t2 对象的初始化过程,对于非聚合类型的成员 t1 做初始化的时候,可以直接写一对空的大括号 {},这相当于调用是 T1 的无参构造函数。

对于一个聚合类型,使用列表初始化相当于对其中的每个元素分别赋值,而对于非聚合类型,则需要先自定义一个合适的构造函数,此时使用列表初始化将会调用它对应的构造函数。

十四、initializer_list模板类的使用

在C++的STL容器中,可以进行任意长度的数据初始化,使用初始化列表也只能进行固定参数的初始化,如果想要做到和STL一样任意长度初始化的能力,可以使用std::initalizer_list这个轻量级的类模板来实现。

先来介绍这个类模板的特点:

  • 它是一个轻量级的容器类型,内部定义了迭代器iterator等容器必须的概念,遍历可得到的迭代器是只读的;
  • 对于std::initalizer_list<T>而言,它可以接受任意长度的初始化列表,但是要求元素必须是同种类型;
  • 在std::initalizer_list内部有三个成员接口:size( )、begin( )、end( );
  • std::initalizer_list对象只能被整体初始化或者赋值;

1、作为普通函数参数

如果想要自定义一个函数并且接收任意个数的参数(变参函数),只需要将函数参数指定为 std::initializer_list,使用初始化列表 { } 作为实参进行数据传递即可。

#include <iostream>
#include <string>
using namespace std;

void traversal(std::initializer_list<int> a){
    for (auto it = a.begin(); it != a.end(); ++it){
        cout << *it << " ";
    }
    cout << endl;
}

int main(int argc,char* argv[]){
    initializer_list<int> list;
    cout << "current list size: " << list.size() << endl;
    traversal(list);

    list = { 1,2,3,4,5,6,7,8,9,0 };
    cout << "current list size: " << list.size() << endl;
    traversal(list);
    cout << endl;
    
    list = { 1,3,5,7,9 };
    cout << "current list size: " << list.size() << endl;
    traversal(list);
    cout << endl;

    
    // 直接通过初始化列表传递数据 //
    
    traversal({ 2, 4, 6, 8, 0 });
    cout << endl;

    traversal({ 11,12,13,14,15,16 });
    cout << endl;


    return 0;
}

示例代码输出的结果:

current list size: 0

current list size: 10
1 2 3 4 5 6 7 8 9 0

current list size: 5
1 3 5 7 9

2 4 6 8 0

11 12 13 14 15 16

std::initializer_list拥有一个无参构造函数,因此,它可以直接定义实例,此时将得到一个空的std::initializer_list,因为在遍历这种类型的容器的时候得到的是一个只读的迭代器,因此我们不能修改里边的数据,只能通过值覆盖的方式进行容器内部数据的修改。虽然如此,在效率方面也无需担心,std::initializer_list的效率是非常高的,它的内部并不负责保存初始化列表中元素的拷贝,仅仅存储了初始化列表中元素的引用。

2、作为构造函数参数

自定义的类如果在构造对象的时候想要接收任意个数的实参,可以给构造函数指定为 std::initializer_list 类型,在自定义类的内部还是使用容器来存储接收的多个实参。

#include <iostream>
#include <string>
#include <vector>
using namespace std;

class Test{
public:
    Test(std::initializer_list<string> list){
        for (auto it = list.begin(); it != list.end(); ++it)
        {
            cout << *it << " ";
            m_names.push_back(*it);
        }
        cout << endl;
    }
private:
    vector<string> m_names;
};

int main(int argc,char* argv[]){
    Test t({ "jack", "lucy", "tom" });
    Test t1({ "hello", "world", "nihao", "shijie" });
    return 0;
}

输出的结果:

jack lucy tom
hello world nihao shijie

 十五、for循环新特点

在介绍新语法之前,先来看一个使用迭代器遍历容器的例子:

#include <iostream>
#include <vector>
using namespace std;

int main(int argc,char* argv[]){
    vector<int> t{ 1,2,3,4,5,6 };
    for (auto it = t.begin(); it != t.end(); ++it)
    {
        cout << *it << " ";
    }
    cout << endl;
    
    return 0;
}

我们在遍历的过程中需要给出容器的两端:开头(begin)和结尾(end),因为这种遍历方式不是基于范围来设计的。在基于范围的for循环中,不需要再传递容器的两端,循环会自动以容器为范围展开,并且循环中也屏蔽掉了迭代器的遍历细节,直接抽取容器中的元素进行运算,使用这种方式进行循环遍历会让编码和维护变得更加简便。

C++98/03 中普通的 for 循环,语法格式:

for(表达式 1; 表达式 2; 表达式 3){
    // 循环体
}

C++11 基于范围的 for 循环,语法格式:

for (declaration : expression){
    // 循环体
}

在上面的语法格式中 declaration 表示遍历声明,在遍历过程中,当前被遍历到的元素会被存储到声明的变量中。expression 是要遍历的对象,它可以是表达式、容器、数组、初始化列表等

1、基于非范围的for循环

(1)关系型容器

使用基于范围的 for 循环有一些需要注意的细节,先来看一下对关系型容器 map 的遍历。

#include <iostream>
#include <string>
#include <map>
using namespace std;

int main(int argc,char* argv[]){
    map<int, string> m{
        {1, "lucy"},{2, "lily"},{3, "tom"}
    };

    // 基于范围的for循环方式
    for (auto& it : m){
        cout << "id: " << it.first << ", name: " << it.second << endl;
    }

    // 普通的for循环方式
    for (auto it = m.begin(); it != m.end(); ++it){
        cout << "id: " << it->first << ", name: " << it->second << endl;
    }

    return 0;
}

在上面的例子中使用两种方式对 map 进行了遍历,通过对比有两点需要注意的事项:

  • 使用普通的 for 循环方式(基于迭代器)遍历关联性容器, auto 自动推导出的是一个迭代器类型,需要使用迭代器的方式取出元素中的键值对(和指针的操作方法相同):it->first,it->second
  • 使用基于访问的 for 循环遍历关联性容器,auto 自动推导出的类型是容器中的 value_type,相当于一个对组(std::pair)对象,提取键值对的方式如下:it.first,it.second

(2)元素只读

通过对基于范围的 for 循环语法的介绍可以得知,在 for 循环内部声明一个变量的引用就可以修改遍历的表达式中的元素的值,但是这并不适用于所有的情况,对应 set 容器来说,内部元素都是只读的,这是由容器的特性决定的,因此在 for 循环中 auto & 会被视为 const auto & 。

#include <iostream>
#include <set>
using namespace std;

int main(int argc,char* argv[]){
    set<int> st{ 1,2,3,4,5,6 };
    for (auto &item : st){
        cout << item++ << endl;		// error, 不能给常量赋值
    }
    return 0;
}

除此之外,在遍历关联型容器时也会出现同样的问题,基于范围的for循环中,虽然可以得到一个std::pair引用,但是我们是不能修改里边的first值的,也就是key值。

#include <iostream>
#include <string>
#include <map>
using namespace std;

int main(int argc,char* argv[]){
    map<int, string> m{
        {1, "lucy"},{2, "lily"},{3, "tom"}
    };

    for (auto& item : m){
        // item.first 是一个常量
        cout << "id: " << item.first++ << ", name: " << item.second << endl;// error
    }

    return 0;
}

(3) 访问次数

基于范围的 for 循环遍历的对象可以是一个表达式或者容器 / 数组等。假设我们对一个容器进行遍历,在遍历过程中 for 循环对这个容器的访问频率是一次还是多次呢?我们通过下面的例子验证一下:

#include <iostream>
#include <vector>
using namespace std;

vector<int> v{ 1,2,3,4,5,6 };
vector<int>& getRange(){
    cout << "get vector range..." << endl;
    return v;
}

int main(int argc,char* argv[]){
    for (auto val : getRange()){
        cout << val << " ";
    }
    cout << endl;

    return 0;
}

输出结果:

get vector range...
1 2 3 4 5 6

从上面的结果中可以看到,不论基于范围的 for 循环迭代了多少次,函数 getRange () 只在第一次迭代之前被调用,得到这个容器对象之后就不会再去重新获取这个对象了。

2、基于范围的for循环

基于范围的for循环遍历的对象可以是一个表达式或容器、数组等,假设对一个容器进行遍历,在遍历过程中for循环对这个容器的访问频率是一次的!

#include <iostream>
#include <vector>
using namespace std;

int main(int argc,char* argv[]){
    vector<int> t{ 1,2,3,4,5,6 };
    for (auto value : t){
        cout << value << " ";
    }
    cout << endl;

    return 0;
}

在上面的例子中,是将容器中遍历的当前元素拷贝到了声明的变量 value 中,因此无法对容器中的元素进行写操作,如果需要在遍历过程中修改元素的值,需要使用引用。

#include <iostream>
#include <vector>
using namespace std;

int main(int argc,char* argv[]){
    vector<int> t{ 1,2,3,4,5,6 };
    cout << "遍历修改之前的容器: ";
    for (auto &value : t){
        cout << value++ << " ";
    }
    cout << endl << "遍历修改之后的容器: ";

    for (auto &value : t){
        cout << value << " ";
    }
    cout << endl;

    return 0;
}

输出结果:

遍历修改之前的容器: 1 2 3 4 5 6
遍历修改之后的容器: 2 3 4 5 6 7

对容器的遍历过程中,如果只是读数据,不允许修改元素的值,可以使用 const 定义保存元素数据的变量,在定义的时候建议使用 const auto &,这样相对于 const auto 效率要更高一些。

#include <iostream>
#include <vector>
using namespace std;

int main(int argc,char* argv[]){
    vector<int> t{ 1,2,3,4,5,6 };
    for (const auto& value : t){
        cout << value << " ";
    }

    return 0;
}

十六、可调用对象包装器、绑定器

1、可调用对象

注意:类内部定义operator 类型名( ){ return 对象;} 表示为类类型转换内置类型或其他类型,返回值类型为与类型名相同;

在 C++ 中存在 “可调用对象” 这么一个概念。准确来说,可调用对象有如下几种定义:

(1)是一个函数指针

int print(int a, double b){
    cout << a << b << endl;
    return 0;
}
// 定义函数指针
int (*func)(int, double) = &print;

 (2)是一个具有operator()成员函数的类对象(仿函数)

#include <iostream>
#include <string>
#include <vector>
using namespace std;

struct Test{
    // ()操作符重载
    void operator()(string msg){
        cout << "msg: " << msg << endl;
    }
};

int main(int argc,char* argv[]){
    Test t;
    t("我是要成为海贼王的男人!!!");	// 仿函数
    return 0;
}

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

#include <iostream>
#include <string>
#include <vector>
using namespace std;

using func_ptr = void(*)(int, string);
struct Test{
    static void print(int a, string b){
        cout << "name: " << b << ", age: " << a << endl;
    }

    // 将类对象转换为函数指针
    operator func_ptr(){
        return print;
    }
};

int main(int argc,char* argv[]){
    Test t;
    // 对象转换为函数指针, 并调用
    t(19, "Monkey D. Luffy");

    return 0;
}

(4)是一个类成员函数指针或者类成员指针

#include <iostream>
#include <string>
#include <vector>
using namespace std;

struct Test{
    void print(int a, string b){
        cout << "name: " << b << ", age: " << a << endl;
    }
    int m_num;
};

int main(int argc,char* argv[]){
    // 定义类成员函数指针指向类成员函数
    void (Test::*func_ptr)(int, string) = &Test::print;
    // 类成员指针指向类成员变量
    int Test::*obj_ptr = &Test::m_num;

    Test t;
    // 通过类成员函数指针调用类成员函数
    (t.*func_ptr)(19, "Monkey D. Luffy");
    // 通过类成员指针初始化类成员变量
    t.*obj_ptr = 1;
    cout << "number is: " << t.m_num << endl;

    return 0;
}

在上面的例子中满足条件的这些可调用对象对应的类型被统称为可调用类型。C++ 中的可调用类型虽然具有比较统一的操作形式,但定义方式五花八门,这样在我们试图使用统一的方式保存,或者传递一个可调用对象时会十分繁琐。现在,C++11通过提供std::function 和 std::bind统一了可调用对象的各种操作。

2、可调用对象包装器

std::function是可调用对象的包装器,它是一个类模板,可以容纳除了类成员(函数)之外的所有可能可调用的对象。通过指向它的模板参数,它可以用统一的方式处理函数,函数对象,并允许保存和延迟执行它们。

(1)基本用法

std::function 必须要包含一个叫做 functional 的头文件,可调用对象包装器使用语法如下:

#include <functional>
std::function<返回值类型(参数类型列表)> funcname=可调用对象;

下面的实例代码中演示了可调用对象包装器的基本使用方法:

#include <iostream>
#include <functional>
using namespace std;

int add(int a, int b){
    cout << a << " + " << b << " = " << a + b << endl;
    return a + b;
}

class T1{
public:
    static int sub(int a, int b){
        cout << a << " - " << b << " = " << a - b << endl;
        return a - b;
    }
};

class T2{
public:
    int operator()(int a, int b){
        cout << a << " * " << b << " = " << a * b << endl;
        return a * b;
    }
};

int main(int argc,char* argv[]){
    // 绑定一个普通函数
    function<int(int, int)> f1 = add;
    // 绑定以静态类成员函数
    function<int(int, int)> f2 = T1::sub;
    // 绑定一个仿函数
    T2 t;
    function<int(int, int)> f3 = t;

    // 函数调用
    f1(9, 3);
    f2(9, 3);
    f3(9, 3);

    return 0;
}

输入结果如下:

9 + 3 = 12
9 - 3 = 6
9 * 3 = 27

通过测试代码可以得到结论:std::function 可以将可调用对象进行包装,得到一个统一的格式,包装完成得到的对象相当于一个函数指针,和函数指针的使用方式相同,通过包装器对象就可以完成对包装的函数的调用了。

(2)作为回调函数使用
因为回调函数本身就是通过函数指针实现的,使用对象包装器可以取代函数指针的作用,来看一下下面的例子:

#include <iostream>
#include <functional>
using namespace std;

class A{
public:
    // 构造函数参数是一个包装器对象
    A(const function<void()>& f) : callback(f){ }
    void notify(){
        callback(); // 调用通过构造函数得到的函数指针
    }
private:
    function<void()> callback;
};

class B{
public:
    void operator()(){
        cout << "我是要成为海贼王的男人!!!" << endl;
    }
};
int main(int argc,char* argv[]){
    B b;
    A a(b); // 仿函数通过包装器对象进行包装
    a.notify();

    return 0;
}

通过上面的例子可以看出,使用对象包装器 std::function 可以非常方便的将仿函数转换为一个函数指针,通过进行函数指针的传递,在其他函数的合适的位置就可以调用这个包装好的仿函数了。

另外,使用 std::function 作为函数的传入参数,可以将定义方式不相同的可调用对象进行统一的传递,这样大大增加了程序的灵活性。

3、绑定器

std::bind用来将可调用对象与其参数一起进行绑定,绑定后的结果可以使用std::function进行保存,并延迟调用到任何需要的地方,通俗来讲,有两大作用:

  • 将可调用对象与其参数绑定成一个仿函数;
  • 将多元(参数个数为n,n>1)可调用对象转换为一元或者(n-1)元可调用对象即只绑定部分参数;

绑定器函数使用语法格式如下:

//绑定非类成员函数或变量
auto f=std::bind(可调用对象,绑定的参数/占位符);
//绑定类成员函数或变量
auto f=std::bind(类函数/成员地址,类实例对象地址,绑定的参数/占位符);

//占位符,占用位置,初始化入参,placeholders::_1
//void test_add(int,int)
//auto f=std::bind(test_add,placeholders::_1,4)(10);

下面来看一个关于绑定器的实际使用的例子:

#include <iostream>
#include <functional>
using namespace std;

void callFunc(int x, const function<void(int)>& f){
    if (x % 2 == 0){
        f(x);
    }
}

void output(int x){
    cout << x << " ";
}

void output_add(int x){
    cout << x + 10 << " ";
}

int main(int argc,char* argv[]){
    // 使用绑定器绑定可调用对象和参数
    auto f1 = bind(output, placeholders::_1);
    for (int i = 0; i < 10; ++i){
        callFunc(i, f1);
    }
    cout << endl;

    auto f2 = bind(output_add, placeholders::_1);
    for (int i = 0; i < 10; ++i){
        callFunc(i, f2);
    }
    cout << endl;

    return 0;
}

 测试代码输出的结果:

0 2 4 6 8
10 12 14 16 18

在上面的程序中,使用了 std::bind 绑定器,在函数外部通过绑定不同的函数,控制了最后执行的结果。std::bind绑定器返回的是一个仿函数类型,得到的返回值可以直接赋值给一个std::function,在使用的时候我们并不需要关心绑定器的返回值类型,使用auto进行自动类型推导就可以了。

placeholders::_1 是一个占位符,代表这个位置将在函数调用时被传入的第一个参数所替代。同样还有其他的占位符 placeholders::_2、placeholders::_3、placeholders::_4、placeholders::_5 等……

有了占位符的概念之后,使得 std::bind 的使用变得非常灵活:

#include <iostream>
#include <functional>
using namespace std;

void output(int x, int y){
    cout << x << " " << y << endl;
}

int main(int argc,char* argv[]){
    // 使用绑定器绑定可调用对象和参数, 并调用得到的仿函数
    bind(output, 1, 2)();
    bind(output, placeholders::_1, 2)(10);
    bind(output, 2, placeholders::_1)(10);

    // error, 调用时没有第二个参数
    // bind(output, 2, placeholders::_2)(10);
    // 调用时第一个参数10被吞掉了,没有被使用
    bind(output, 2, placeholders::_2)(10, 20);

    bind(output, placeholders::_1, placeholders::_2)(10, 20);
    bind(output, placeholders::_2, placeholders::_1)(10, 20);


    return 0;
}

结果:

1  2        // bind(output, 1, 2)();
10 2        // bind(output, placeholders::_1, 2)(10);
2 10        // bind(output, 2, placeholders::_1)(10);
2 20        // bind(output, 2, placeholders::_2)(10, 20);
10 20        // bind(output, placeholders::_1, placeholders::_2)(10, 20);
20 10        // bind(output, placeholders::_2, placeholders::_1)(10, 20);

通过测试可以看到,std::bind 可以直接绑定函数的所有参数,也可以仅绑定部分参数。在绑定部分参数的时候,通过使用 std::placeholders 来决定空位参数将会属于调用发生时的第几个参数。

可调用对象包装器 std::function 是不能实现对类成员函数指针或者类成员指针的包装的,但是通过绑定器 std::bind 的配合之后,就可以完美的解决这个问题了,再来看一个例子,然后再解释里边的细节:

#include <iostream>
#include <functional>
using namespace std;

class Test{
public:
    void output(int x, int y){
        cout << "x: " << x << ", y: " << y << endl;
    }
    int m_number = 100;
};

int main(int argc,char* argv[]){
    Test t;
    // 绑定类成员函数
    function<void(int, int)> f1 = 
        bind(&Test::output, &t, placeholders::_1, placeholders::_2);
    // 绑定类成员变量(公共)
    function<int&(void)> f2 = bind(&Test::m_number, &t);

    // 调用
    f1(520, 1314);
    f2() = 2333;
    cout << "t.m_number: " << t.m_number << endl;

    return 0;
}

示例代码输出的结果:

x: 520, y: 1314
t.m_number: 2333

在用绑定器绑定类成员函数或者成员变量的时候需要将它们所属的实例对象一并传递到绑定器函数内部。f1的类型是function<void(int, int)>,通过使用std::bind将Test的成员函数output的地址和对象t绑定,并转化为一个仿函数并存储到对象f1中。

使用绑定器绑定的类成员变量m_number得到的仿函数被存储到了类型为function<int&(void)>的包装器对象f2中,并且可以在需要的时候修改这个成员。其中int是绑定的类成员的类型,并且允许修改绑定的变量,因此需要指定为变量的引用,由于没有参数因此参数列表指定为void

示例程序中是使用 function 包装器保存了 bind 返回的仿函数,如果不知道包装器的模板类型如何指定,可以直接使用 auto 进行类型的自动推导,这样使用起来会更容易一些。

十七、lambda表达式

1、表达式原型

lambda表达式式C++11最重要也是最常用的特性之一,本质上是匿名函数,有以下的一些优点:

  • 声明式的编程风格,就地匿名定义目标函数或函数对象,不需要额外写一个命名函数对象;
  • 简洁,避免了代码膨胀和功能的分散,让开发更加高效;
  • 在需要的时间和地点实现功能闭包,使程序更加灵活;

语法如下:

[capture](params) opt->ret{body;}
  • capture:捕获对象,不能省略
  • params:参数列表,可以省略
  • opt:函数选项,mutable等,可以省略
  • ret:返回类型
  • body:函数体

(1)捕获列表 []: 捕获一定范围内的变量

(2)参数列表 (): 和普通函数的参数列表一样,如果没有参数参数列表可以省略不写。

auto f = [](){return 1;}	// 没有参数, 参数列表为空
auto f = []{return 1;}		// 没有参数, 参数列表省略不写

 (3)opt 选项, 不需要可以省略

  • mutable: 可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)

  • exception: 指定函数抛出的异常,如抛出整数类型的异常,可以使用 throw ()

(4)返回值类型:在 C++11 中,lambda 表达式的返回值是通过返回值后置语法来定义的。

(5)函数体:函数的实现,这部分不能省略,但函数体可以为空。

2、捕获列表

  • [ ]-不捕获任何变量
  • [=]-捕获外部作用域中所有变量,按赋值方式在函数体内使用
  • [&]-捕获外部作用域中所有变量,按引用方式在函数体内使用
  • [bar]-按赋值捕获bar变量,同时不捕获其他变量
  • [&bar]-按引用捕获bar变量,同时不捕获其他变量
  • [=, &foo] - 按值捕获外部作用域中所有变量,并按照引用捕获外部变量foo
  • [this] - 捕获当前类中的 this 指针,让 lambda 表达式拥有和当前类成员函数同样的访问权限;如果已经使用了 & 或者 =, 默认添加此选项

下面通过一个例子,看一下初始化列表的具体用法:

#include <iostream>
#include <functional>
using namespace std;

class Test{
public:
    void output(int x, int y){
        auto x1 = [] {return m_number; };                      // error
        auto x2 = [=] {return m_number + x + y; };             // ok
        auto x3 = [&] {return m_number + x + y; };             // ok
        auto x4 = [this] {return m_number; };                  // ok
        auto x5 = [this] {return m_number + x + y; };          // error
        auto x6 = [this, x, y] {return m_number + x + y; };    // ok
        auto x7 = [this] {return m_number++; };                // ok
    }
    int m_number = 100;
};
  • x1:错误,没有捕获外部变量,不能使用类成员 m_number

  • x2:正确,以值拷贝的方式捕获所有外部变量

  • x3:正确,以引用的方式捕获所有外部变量

  • x4:正确,捕获 this 指针,可访问对象内部成员

  • x5:错误,捕获 this 指针,可访问类内部成员,没有捕获到变量 x,y,因此不能访问

  • x6:正确,捕获 this 指针,x,y

  • x7:正确,捕获 this 指针,并且可以修改对象内部变量的值

int main(int argc,char* argv[]){
    int a = 10, b = 20;
    auto f1 = [] {return a; };                        // error
    auto f2 = [&] {return a++; };                     // ok
    auto f3 = [=] {return a; };                       // ok
    auto f4 = [=] {return a++; };                     // error
    auto f5 = [a] {return a + b; };                   // error
    auto f6 = [a, &b] {return a + (b++); };           // ok
    auto f7 = [=, &b] {return a + (b++); };           // ok

    return 0;
}
  • f1:错误,没有捕获外部变量,因此无法访问变量 a

  • f2:正确,使用引用的方式捕获外部变量,可读写

  • f3:正确,使用值拷贝的方式捕获外部变量,可读

  • f4:错误,使用值拷贝的方式捕获外部变量,可读不能写

  • f5:错误,使用拷贝的方式捕获外部变量 a,没有捕获外部变量 b,因此无法访问变量 b

  • f6:正确,使用拷贝的方式捕获了外部变量 a,只读,使用引用的方式捕获外部变量 b,可读写

  • f7:正确,使用值拷贝的方式捕获所有外部变量以及 b 的引用,b 可读写,其他只读

在匿名函数内部,需要通过 lambda 表达式的捕获列表控制如何捕获外部变量,以及访问哪些变量。默认状态下 lambda 表达式无法修改通过复制方式捕获外部变量,如果希望修改这些外部变量,需要通过引用的方式进行捕获。 

3、返回值

很多时候,lambda 表达式的返回值是非常明显的,因此在 C++11 中允许省略 lambda 表达式的返回值。

// 完整的lambda表达式定义
auto f = [](int a) -> int{
    return a+10;  
};

// 忽略返回值的lambda表达式定义
auto f = [](int a){
    return a+10;  
};

一般情况下,不指定 lambda 表达式的返回值,编译器会根据 return 语句自动推导返回值的类型,但需要注意的是 labmda表达式不能通过列表初始化自动推导出返回值类型。

// ok,可以自动推导出返回值类型
auto f = [](int i){
    return i;
}

// error,不能推导出返回值类型
auto f1 = [](){
    return {1, 2};	// 基于列表初始化推导返回值,错误
}

4、函数的本质

使用lambda表达式捕获列表捕获外部变量,如果希望去修改按值捕获的外部变量,那么应该如何处理呢?默认情况是右值的,不能修改,这就需要使用mutable选项,被mutable修改是lambda表达式就算没有参数也要写明参数列表,并且可以去掉按值捕获的不歪变量的只读const属性。

int a=2;
auto f1=[=]{return a++;}  //error,按值捕获外部变量,a只能读
auto f2-[=]()mutable{return a++;} //可以通过

 剖析一下为什么值拷贝的方式捕获外部变量是只读的?

  • lambda表达式的类型在C++11会被看成一个带operator( )的类,即仿函数;
  • 按照C++标准,lambda表达式的operator( )默认是const类型的,一个const成员函数是无法修改成员变量值的;

mutable选项的作用是取消operator( )的const属性!

因为lambda表达式在C++11中会被看成一个仿函数,因此可以使用std::function和std::bind来存储和操作lambda表达式。

因为 lambda 表达式在 C++ 中会被看做是一个仿函数,因此可以使用std::function和std::bind来存储和操作lambda表达式:

#include <iostream>
#include <functional>
using namespace std;

int main(int argc,char* argv[]){
    // 包装可调用函数
    std::function<int(int)> f1 = [](int a) {return a; };
    // 绑定可调用函数
    std::function<int(int)> f2 = bind([](int a) {return a; }, placeholders::_1);

    // 函数调用
    cout << f1(100) << endl;
    cout << f2(200) << endl;
    return 0;
}

 对于没有捕获任何变量的 lambda 表达式,还可以转换成一个普通的函数指针:

using func_ptr = int(*)(int);
// 没有捕获任何外部变量的匿名函数
func_ptr f = [](int a){
    return a;  
};
// 函数调用
f(1314);

十八、右值引用

1、右值

C++11增加了一个新的类型,称为右值引用(R-value reference),标记为&&,

  • lvalue是locator value的缩写,rvalue是read value的缩写,只读类型;
  • 左值是指存储在内存中,有明确存储地址(可取地址)的数据;
  • 右值是指可以提供数据值的数据(不可取地址);

通过描述可以确定,区分出左值和右值的便捷方法:可以对表达式取地址(&)的就是左值,否则为右值。所有名字的变量或对象都是左值,而右值是匿名的。

int a=520;
int b=1314;
a=b;

一般情况下,位于“=”前的表达式为左值,位于“=”后面的表达式为右值,也就是例子说的a,b为左值,520、1314为右值,a=b是一种特殊情况,在这个表达式中,a、b都是左值,因为变量b是可以取地址的,不能视为右值。

C++11 中右值可以分为两种:一个是将亡值( xvalue, expiring value),另一个则是纯右值( prvalue, PureRvalue):

  • 纯右值:非引用返回的临时变量、运算表达式产生的临时变量、原始字面量和 lambda 表达式等
  • 将亡值:与右值引用相关的表达式,比如,T&& 类型函数的返回值、 std::move 的返回值等。
int value = 520;

在上面的语句中,value 是左值,520 是字面量也就是右值。其中 value 可以被引用,但是 520 就不行了,因为字面量都是右值。

右值可以初始化右值引用,但不能初始化左值引用!左值可以初始化左值引用,但不能初始化右值;左值引用是定义变量的别名,不占用内存的!但可以通过右值引用类型变量初值化左值引用!

int a=10;
int &b=a;
const int&h=a;//常量左值引用
int &b=10;//error,右值不能初始化左值引用

int &&c=a;//error,左值不能初始化右值引用
int &&c=10;
const int&&e=11;//常量右值引用

int &d=c;//可以通过右值引用初始化左值引用
int &d=e;

//常量左值引用
const int& k=b;
const int& g=h;
const int& u=e;
const int& y=c;

2、右值引用

右值引用就是对一个右值进行引用的类型。因为右值是匿名的,所以只能通过引用的方式找到它,无论声明左值引用还是右值引用都必须立即进行初始化,因为引用类型本身并不拥有所绑定对象的内存,只是该对象的一个别名,通过右值引用的声明,该右值又获得“新生”,其生命周期与右值引用类型变量的声明周期一样,只要该变量还活着,该右值临时量将会一直存活下去。

#include <iostream>
using namespace std;

int&& value = 520;
class Test{
public:
    Test(){
        cout << "construct: my name is jerry" << endl;
    }
    Test(const Test& a){
        cout << "copy construct: my name is tom" << endl;
    }
};

Test getObj(){
    return Test();
}

int main(int argc,char* argv[]){
    int a1;
    int &&a2 = a1;        // error
    Test& t = getObj();   // error
    Test && t = getObj();
    const Test& t = getObj();
    return 0;
}
  • 在上面的例子中 int&& value = 520; 里面 520 是纯右值,value 是对字面量 520 这个右值的引用。

  • 在 int &&a2 = a1; 中 a1 虽然写在了 = 右边,但是它仍然是一个左值,使用左值初始化一个右值引用类型是不合法的。

  • 在 Test& t = getObj() 这句代码中语法是错误的,右值不能给普通的左值引用赋值。

  • 在 Test && t = getObj(); 中 getObj() 返回的临时对象被称之为将亡值,t 是这个将亡值的右值引用。

  • const Test& t = getObj() 这句代码的语法是正确的,常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值。

自定义拷贝构造可以防止浅拷贝,在对象拷贝过程中,如果没有自定义拷贝构造函数,系统会提供一个默认的构造函数,默认的构造函数对于基本类型的成员变量,是按照字节进行复制的,比如有一个对象A赋值给对象B,都属于同一类的,如果这个类有一个指针,而且A、B对象都指向同一个内存,B对象在构造构成中并没有new新的内存,其指针指向了A构造的内存,当对象析构的时候,若A对象先析构,B对象就无法访问这块内存!

移动构造函数目的使用浅拷贝,不让深拷贝!转移之后原来的对象就不再拥有这个资源!新对象继续使用以前的资源。

对于需要动态申请资源的类,应该设计移动构造函数,以提高程序效率,需要注意的是,一般在提供移动构造函数的同时,也会提供常量左值引用(const int&)的拷贝构造函数,以保证移动不成还可以使用拷贝构造函数!

在 C++ 中在进行对象赋值操作的时候,很多情况下会发生对象之间的深拷贝,如果堆内存很大,这个拷贝的代价也就非常大,在某些情况下,如果想要避免对象的深拷贝,就可以使用右值引用进行性能的优化。

再来修改一下上面的实例代码:

#include <iostream>
using namespace std;

class Test{
public:
    Test() : m_num(new int(100)){
        cout << "construct: my name is jerry" << endl;
    }

    Test(const Test& a) : m_num(new int(*a.m_num)){
        cout << "copy construct: my name is tom" << endl;
    }

    ~Test(){
        delete m_num;
    }

    int* m_num;
};

Test getObj(){
    Test t;
    return t;
}

int main(int argc,char* argv[]){
    Test t = getObj();
    cout << "t.m_num: " << *t.m_num << endl;
    return 0;
};

测试代码执行的结果为:

construct: my name is jerry
copy construct: my name is tom
t.m_num: 100

通过输出的结果可以看到调用 Test t = getObj(); 的时候调用拷贝构造函数对返回的临时对象进行了深拷贝得到了对象 t,在 getObj() 函数中创建的对象虽然进行了内存的申请操作,但是没有使用就释放掉了。如果能够使用临时对象已经申请的资源,既能节省资源,还能节省资源申请和释放的时间,如果要执行这样的操作就需要使用右值引用了,右值引用具有移动语义,移动语义可以将资源(堆、系统对象等)通过浅拷贝从一个对象转移到另一个对象这样就能减少不必要的临时对象的创建、拷贝以及销毁,可以大幅提高 C++ 应用程序的性能。

#include <iostream>
using namespace std;

class Test{
public:
    Test() : m_num(new int(100)){
        cout << "construct: my name is jerry" << endl;
    }

    Test(const Test& a) : m_num(new int(*a.m_num)){
        cout << "copy construct: my name is tom" << endl;
    }

    // 添加移动构造函数
    Test(Test&& a) : m_num(a.m_num){
        a.m_num = nullptr;
        cout << "move construct: my name is sunny" << endl;
    }

    ~Test(){
        delete m_num;
        cout << "destruct Test class ..." << endl;
    }

    int* m_num;
};

Test getObj(){
    Test t;
    return t;
}

int main(int argc,char* argv[]){
    Test t = getObj();
    cout << "t.m_num: " << *t.m_num << endl;
    return 0;
};

测试代码执行的结果如下:

construct: my name is jerry
move construct: my name is sunny
destruct Test class ...
t.m_num: 100
destruct Test class ...

通过修改,在上面的代码给 Test 类添加了移动构造函数(参数为右值引用类型),这样在进行 Test t = getObj(); 操作的时候并没有调用拷贝构造函数进行深拷贝,而是调用了移动构造函数,在这个函数中只是进行了浅拷贝,没有对临时对象进行深拷贝,提高了性能。

如果不使用移动构造,在执行 Test t = getObj() 的时候也是进行了浅拷贝,但是当临时对象被析构的时候,类成员指针 int* m_num; 指向的内存也就被析构了,对象 t 也就无法访问这块内存地址了。

在测试程序中 getObj() 的返回值就是一个将亡值,也就是说是一个右值,在进行赋值操作的时候如果 = 右边是一个右值,那么移动构造函数就会被调用。移动构造中使用了右值引用,会将临时对象中的堆内存地址的所有权转移给对象t,这块内存被成功续命,因此在t对象中还可以继续使用这块内存。

对于需要动态申请大量资源的类,应该设计移动构造函数,以提高程序效率。需要注意的是,我们一般在提供移动构造函数的同时,也会提供常量左值引用的拷贝构造函数,以保证移动不成还可以使用拷贝构造函数。

3、&&的特性

在C++中,并不是所有情况下&&都代表的是一个右值引用,具体的场景出现在模板和类型推导中,如果是模板参数需要指定为T&&,如果是自动类型推导需要指定为auto&&在这种情况下被称为未定义的引用类型,额外注意的是const T&&表示一个右值引用,不是未定引用类型。

在函数模板中使用&&,

template<typename T>
void f(T&& param);
void d(const T&& param);
f(10);
int x=10;
f(x);
d(x);

在上面的例子中函数模板进行了自动推导类型,需要通过传入实参来确定参数param的实际参数 param的实际类型。

  • 第4行,对于f(10)来说传入的实参10是左右值,因此T&&表示右值引用;
  • 第6行,对于f(x)来说传入的实参x是左值,因此T&&表示左值引用;
  • 第7行,d(x)的参数是const T&&不是未定义引用类型,不需要推导,本身就表示一个右值引用 
int main(int argc,char* argv[]){
    int x = 520, y = 1314;
    auto&& v1 = x;
    auto&& v2 = 250;
    decltype(x)&& v3 = y;   // error
    cout << "v1: " << v1 << ", v2: " << v2 << endl;
    return 0;
};
  • 第 4 行中 auto&& 表示一个整形的左值引用

  • 第 5 行中 auto&& 表示一个整形的右值引用

  • 第 6 行中 decltype(x)&& 等价于 int&& 是一个右值引用不是未定引用类型,y 是一个左值,不能使用左值初始化一个右值引用类型。

由于上述代在T&&或者auto&&这种未定义引用类型,当它作为参数时,有可能被一个右值引用初始化,也有可能被一个左值引用初始化,在进行类型推导时右值引用类型(&&)会发生变化,这种变化被称为引用折叠!

在C++的引用折叠如下:

  • 通过右值推导T&&或者auto&&得到的是一个右值引用类型;
  • 通过非右值(右值引用、左值、左值引用、常量右值引用、常量左值引用)推导T&&或者auto&&得到的是一个左值引用类型; 
int&& a1 = 5;
auto&& bb = a1;
auto&& bb1 = 5;

int a2 = 5;
int &a3 = a2;
auto&& cc = a3;
auto&& cc1 = a2;

const int& s1 = 100;
const int&& s2 = 100;
auto&& dd = s1;
auto&& ee = s2;

const auto&& x = 5;

案例分析:

  • 第 2 行:a1 为右值引用,推导出的 bb 为左值引用类型
  • 第 3 行:5 为右值,推导出的 bb1 为右值引用类型
  • 第 7 行:a3 为左值引用,推导出的 cc 为左值引用类型
  • 第 8 行:a2 为左值,推导出的 cc1 为左值引用类型
  • 第 12 行:s1 为常量左值引用,推导出的 dd 为常量左值引用类型
  • 第 13 行:s2 为常量右值引用,推导出的 ee 为常量左值引用类型
  • 第 15 行:x 为右值引用,不需要推导,只能通过右值初始化

再看最后一个例子,代码如下:

#include <iostream>
using namespace std;

void printValue(int &i){
    cout << "l-value: " << i << endl;
}

void printValue(int &&i){
    cout << "r-value: " << i << endl;
}

void forward(int &&k){
    printValue(k);
}

int main(int argc,char* argv[]){
    int i = 520;
    printValue(i);
    printValue(1314);
    forward(250);

    return 0;
};

测试代码输出的结果如下:

l-value: 520
r-value: 1314
l-value: 250

根据测试代码可以得出,编译器会根据传入的参数类型(左值或右值)调用相应的重置函数(printValue),函数forward( )接受的是一个右值,但是在这个函数中调用函数printValue( )时,参数k变成一个命名对象,编译器将其当做左值来处理!

总结一下:

  • 左值和右值是独立于它们类型的,右值引用类型可能是左值,也可能是右值;
  • 编译器会将已经命名的右值引用变量视为左值,将未命名的右值引用视为右值;
  • auto&&或者函数参数类型自动推导的T&&是一个未定义的引用类型,它可能是左值引用也可能是右值引用类型,这就取决于初始化值的类型;
  • 通过右值推导T&&或者auto&&得到的是一个右值引用类型,其余都是左值引用类型!

十九、move资源的转移

 在C++11添加了右值引用,并且不能使用左值初始化右值引用,如果想要使用左值初始化 一个右值引用需要借助std::move( )函数,在使用std::move( )可以将左值转换为右值,使用这个函数并不能移动任何东西。而是和移动构造函数一样都具有移动语义,将对象的状态或者所有权从一个对象转移到另一个对象,只是转移,没有内存拷贝!

从实现上将,std::move基本等同于一个类型转换:static_cast<T&&>(lvalue),就是将左值转换为右值,变成将亡值,这样就实现了资源的转移;

template<class _Ty>
_NODISCARD constexpr remove_reference_t<_Ty>&& move(_Ty&& _Arg) _NOEXCEPT
{	// forward _Arg as movable
    return (static_cast<remove_reference_t<_Ty>&&>(_Arg));
}

使用方法如下:

class Test{
public:
    Test(){}
    ......
}
int main(int argc,char* argv[]){
    Test t;
    decltype(x) && v1 = t;          // error
    decltype(x) && v2 = move(t);    // ok
    return 0;
}
  • 在第 4 行中,使用左值初始化右值引用,因此语法是错误的
  • 在第 5 行中,使用 move() 函数将左值转换为了右值,这样就可以初始化右值引用了。

假设一个临时容器很大,并且需要将这个容器赋值给另一个容器,就可以执行如下操作:

list<string> ls;
ls.push_back("hello");
ls.push_back("world");
......
list<string> ls1 = ls;        // 需要拷贝, 效率低
list<string> ls2 = move(ls);

如果不使用 std::move,拷贝的代价很大,性能较低。使用 move 几乎没有任何代价,只是转换了资源的所有权。如果一个对象内部有较大的堆内存或者动态数组时,使用 move () 就可以非常方便的进行数据所有权的转移。另外,我们也可以给类编写相应的移动构造函数(T::T(T&& another))和和具有移动语义的赋值函数(T&& T::operator=(T&& rhs)),在构造对象和赋值的时候尽可能的进行资源的重复利用,因为它们都是接收一个右值引用参数。

二十、forward完美转发

右值引用独立于值的,一个右值引用作为函数参数的形参时,在函数内部转发该参数给内部其他函数时,它就变成一个左值,并不是原来的类型了,如果按照参数原来的类型转发到另外一个函数,可以使用C++11提供的std::forward( )函数,该函数实现的功能称为完美转发!这个可以保证原类型在传递过程中类型不发生变化

//函数原型
template<class T>T&& forward(typename remove_reference<T>::type& t) noexcept;
template<class T>T&& forward(typename remove_reference<T>::type&& t) noexcept;

//精简之后的样子
std::forward<T>(t);
  • 当T为左值引用类型(比如:int&)时,t将被转换为T类型的左值;
  • 当T不是左值引用类型(比如:int,int&&)时,t将被转换为T类型的右值;

下面通过一个例子演示一下关于 forward 的使用:

#include <iostream>
using namespace std;

template<typename T>
void printValue(T& t){
    cout << "l-value: " << t << endl;
}

template<typename T>
void printValue(T&& t){
    cout << "r-value: " << t << endl;
}

template<typename T>
void testForward(T && v){
    printValue(v);
    printValue(move(v));
    printValue(forward<T>(v));
    cout << endl;
}

int main(int argc,char* argv[]){
    testForward(520);
    int num = 1314;
    testForward(num);
    testForward(forward<int>(num));
    testForward(forward<int&>(num));
    testForward(forward<int&&>(num));

    return 0;
}

测试代码打印的结果如下:

l-value: 520
r-value: 520
r-value: 520

l-value: 1314
r-value: 1314
l-value: 1314

l-value: 1314
r-value: 1314
r-value: 1314

l-value: 1314
r-value: 1314
l-value: 1314

l-value: 1314
r-value: 1314
r-value: 1314

(1)testForward(520); 函数的形参为未定引用类型 T&&,实参为右值,初始化后被推导为一个右值引用

  • printValue(v); 已命名的右值 v,编译器会视为左值处理,实参为左值;
  • printValue(move(v)); 已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值;
  • printValue(forward<T>(v));forward 的模板参数为右值引用,最终得到一个右值,实参为 ``右值`;

(2)testForward(num); 函数的形参为未定引用类型 T&&,实参为左值,初始化后被推导为一个左值引用

  • printValue(v); 实参为左值;
  • printValue(move(v)); 通过 move 将左值转换为右值,实参为右值;
  • printValue(forward<T>(v));forward 的模板参数为左值引用,最终得到一个左值引用,实参为左值;

(3)testForward(forward<int>(num));forward 的模板类型为 int,最终会得到一个右值,函数的形参为未定引用类型 T&& 被右值初始化后得到一个右值引用类型

  • printValue(v); 已命名的右值 v,编译器会视为左值处理,实参为左值;
  • printValue(move(v)); 已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值;
  • printValue(forward<T>(v));forward 的模板参数为右值引用,最终得到一个右值,实参为右值;

(4)testForward(forward<int&>(num));forward 的模板类型为 int&,最终会得到一个左值,函数的形参为未定引用类型 T&& 被左值初始化后得到一个左值引用类型

  • printValue(v); 实参为左值;
  • printValue(move(v)); 通过 move 将左值转换为右值,实参为右值;
  • printValue(forward<T>(v));forward 的模板参数为左值引用,最终得到一个左值,实参为左值;

(5)testForward(forward<int&&>(num));forward 的模板类型为 int&&,最终会得到一个右值,函数的形参为未定引用类型 T&& 被右值初始化后得到一个右值引用类型

  • printValue(v); 已命名的右值 v,编译器会视为左值处理,实参为左值;
  • printValue(move(v)); 已命名的右值编译器会视为左值处理,通过 move 又将其转换为右值,实参为右值;
  • printValue(forward<T>(v));forward 的模板参数为右值引用,最终得到一个右值,实参为右值;

二十一、共享的智能指针

在C++中没有垃圾回收机制,必须自己释放分配的内存,否则就会造成内存的泄露,解决这个问题最有效的方法是使用智能指针(smart pointer)。

智能指针是存储指向动态分配(堆)对象指针的类,用于生存期的控制,能够确保在离开指针所在作用域时,自动地销毁动态分配的对象,防止内存泄露。智能指针的核心实现技术是在引用计数,每使用它一次,内部引用计数加1,没析构一次内部的引用计数减1,减为0时,删除所指向的堆内存!

C++11提供了3种智能指针,使用这些智能指针时需要引用头文件<memory>:

  • std::share_ptr:共享的智能指针
  • std::unique_ptr:独占的智能指针
  • std::weak_ptr:弱引用的智能指针,它不共享指针,不能操作资源,是用来监视shared_ptr的

1、shared_ptr的初始化

共享智能指针是指多个智能指针可以同时管理同一块内存,共享智能指针是一个模板类,如果要进行初始化有三种方式:通过构造函数,std::make_shared辅助函数以及reset方法。共享智能指针对象初始完毕之后就指向了要管理的那块内存,如果想要查看当前有多少个智能指针同时管理这块内存可以使用共享智能指针提供的一个成员函数use_count,

long use_count( )const noexcept;

(1)通过构造函数初始化

// shared_ptr<T> 类模板中,提供了多种实用的构造函数, 语法格式如下:
std::shared_ptr<T> 智能指针名字(创建堆内存);

测试代码如下:

#include <iostream>
#include <memory>
using namespace std;

int main(int argc,char* argv[]){
    // 使用智能指针管理一块 int 型的堆内存
    shared_ptr<int> ptr1(new int(520));
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    // 使用智能指针管理一块字符数组对应的堆内存
    shared_ptr<char> ptr2(new char[12]);
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    // 创建智能指针对象, 不管理任何内存
    shared_ptr<int> ptr3;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    // 创建智能指针对象, 初始化为空
    shared_ptr<int> ptr4(nullptr);
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;

    return 0;
}

 测试代码输出的结果如下:

ptr1管理的内存引用计数: 1
ptr2管理的内存引用计数: 1
ptr3管理的内存引用计数: 0
ptr4管理的内存引用计数: 0

如果智能指针被初始化了一块有效内存,那么这块内存的引用计数 + 1,如果智能指针没有被初始化或者被初始化为 nullptr 空指针,引用计数不会 + 1。另外,不要使用一个原始指针初始化多个 shared_ptr。

int *p = new int;
shared_ptr<int> p1(p);
shared_ptr<int> p2(p);		// error, 编译不会报错, 运行会出错

(2)通过拷贝和移动构造函数初始化

  • 如果使用拷贝的方式初始化共享智能指针对象,这两个对象会共同管理同一块内存,堆内存对应的引用计数会增加;如果使用移动的方式(move函数)初始智能指针,只是转让了内存的所有权,管理内存的对象并不会增加,因此内存的引用计数不会发生改变!
#include <iostream>
#include <memory>
using namespace std;

int main(int argc,char* argv[]){
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr<int> ptr1(new int(520));
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    //调用拷贝构造函数
    shared_ptr<int> ptr2(ptr1);
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    shared_ptr<int> ptr3 = ptr1;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    //调用移动构造函数
    shared_ptr<int> ptr4(std::move(ptr1));
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
    std::shared_ptr<int> ptr5 = std::move(ptr2);
    cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;

    return 0;
}

 测试程序输入的结果:

ptr1管理的内存引用计数: 1
ptr2管理的内存引用计数: 2
ptr3管理的内存引用计数: 3
ptr4管理的内存引用计数: 3
ptr5管理的内存引用计数: 3

(3)通过std::make_shared的方式初始化

通过 C++ 提供的 std::make_shared() 就可以完成内存对象的创建并将其初始化给智能指针,函数原型如下:

template< class T, class... Args >
shared_ptr<T> make_shared( Args&&... args );
  • T:模板参数的数据类型
  • Args&&... args :要初始化的数据,如果是通过 make_shared 创建对象,需按照构造函数的参数列表指定

测试代码如下;

#include <iostream>
#include <string>
#include <memory>
using namespace std;

class Test{
public:
    Test() {
        cout << "construct Test..." << endl;
    }
    Test(int x) {
        cout << "construct Test, x = " << x << endl;
    }
    Test(string str) {
        cout << "construct Test, str = " << str << endl;
    }
    ~Test(){
        cout << "destruct Test ..." << endl;
    }
};

int main(int argc,char* argv[]){
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr<int> ptr1 = make_shared<int>(520);
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;

    shared_ptr<Test> ptr2 = make_shared<Test>();
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;

    shared_ptr<Test> ptr3 = make_shared<Test>(520);
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;

    shared_ptr<Test> ptr4 = make_shared<Test>("我是要成为海贼王的男人!!!");
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;
    return 0;
}

使用 std::make_shared() 模板函数可以完成内存地址的创建,并将最终得到的内存地址传递给共享智能指针对象管理。如果申请的内存是普通类型,通过函数的()可完成地址的初始化,如果要创建一个类对象,函数的()内部需要指定构造对象需要的参数,也就是类构造函数的参数。

(4)通过reset的方式初始化

共享智能指针类提供的 std::shared_ptr::reset 方法函数原型如下:

void reset() noexcept;

template< class Y >
void reset( Y* ptr );

template< class Y, class Deleter >
void reset( Y* ptr, Deleter d );

template< class Y, class Deleter, class Alloc >
void reset( Y* ptr, Deleter d, Alloc alloc );
  • ptr:指向要取得所有权的对象的指针

  • d:指向要取得所有权的对象的指针

  • aloc:内部存储所用的分配器

测试代码如下:

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main(int argc,char* argv[]){
    // 使用智能指针管理一块 int 型的堆内存, 内部引用计数为 1
    shared_ptr<int> ptr1 = make_shared<int>(520);
    shared_ptr<int> ptr2 = ptr1;
    shared_ptr<int> ptr3 = ptr1;
    shared_ptr<int> ptr4 = ptr1;
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;

    ptr4.reset();
    cout << "ptr1管理的内存引用计数: " << ptr1.use_count() << endl;
    cout << "ptr2管理的内存引用计数: " << ptr2.use_count() << endl;
    cout << "ptr3管理的内存引用计数: " << ptr3.use_count() << endl;
    cout << "ptr4管理的内存引用计数: " << ptr4.use_count() << endl;

    shared_ptr<int> ptr5;
    ptr5.reset(new int(250));
    cout << "ptr5管理的内存引用计数: " << ptr5.use_count() << endl;

    return 0;
}

测试代码输入的结果: 

ptr1管理的内存引用计数: 4
ptr2管理的内存引用计数: 4
ptr3管理的内存引用计数: 4
ptr4管理的内存引用计数: 4
    
ptr1管理的内存引用计数: 3
ptr2管理的内存引用计数: 3
ptr3管理的内存引用计数: 3
ptr4管理的内存引用计数: 0
    
ptr5管理的内存引用计数: 1

对于一个未初始化的共享智能指针,可以通过 reset 方法来初始化,当智能指针中有值的时候,调用 reset 会使引用计数减 1。

(5)获取原始指针

对应基础数据类型来说,通过操作智能指针和操作智能指针管理的内存效果是一样的,可以直接完成数据的读写。但是如果共享智能指针管理的是一个对象,那么就需要取出原始内存的地址再操作,可以调用共享智能指针类提供的 get () 方法得到原始地址,其函数原型如下:

T* get( )const noexcept;

测试代码如下:

#include <iostream>
#include <string>
#include <memory>
using namespace std;

int main(int argc,char* argv[]){
    int len = 128;
    shared_ptr<char> ptr(new char[len]);
    // 得到指针的原始地址
    char* add = ptr.get();
    memset(add, 0, len);
    strcpy(add, "我是要成为海贼王的男人!!!");
    cout << "string: " << add << endl;
    
    shared_ptr<int> p(new int);
    *p = 100;
    cout << *p.get() << "  " << *p << endl;
    
    return 0;
}

2、指定删除器

当智能指针的内存对应的引用计数为0的时候,这块内存就会被智能指针析构了,另外在初始化智能指针的时候也可以自己指定删除动作,这个删除操作对应的函数被称之为删除器,这个删除器函数本质是一个回调函数,只需要进行研究实现,

比如数组类型指针,默认只能析构一个,

shared_ptr<int> t(new int[5]);//析构不完全
shared_ptr<int> t(new int[5],[](int* x){
    delete []x;//数组
});
//或者自带default_delete
shared_ptr<int> t(new int[5],default_delte<int[]>());

例子如下:

#include <iostream>
#include <memory>
using namespace std;

// 自定义删除器函数,释放int型内存
void deleteIntPtr(int* p){
    delete p;
    cout << "int 型内存被释放了...";
}

int main(int argc,char* argv[]){
    shared_ptr<int> ptr(new int(250), deleteIntPtr);
    return 0;
}

 删除器函数也可以是 lambda 表达式,因此代码也可以写成下面这样:

int main(int argc,char* argv[]){
    shared_ptr<int> ptr(new int(250), [](int* p) {delete p; });
    return 0;
}

在上面的代码中,lambda表达式的参数就是智能指针管理的内存的地址,有了这个地址之后函数体内部就可以完成删除操作了。

在 C++11 中使用 shared_ptr 管理动态数组时,需要指定删除器,因为 std::shared_ptr的默认删除器不支持数组对象,具体的处理代码如下:

int main(int argc,char* argv[]){
    shared_ptr<int> ptr(new int[10], [](int* p) {delete[]p; });
    return 0;
}

在删除数组内存时,除了自己编写删除器,也可以使用 C++ 提供的 std::default_delete<T>() 函数作为删除器,这个函数内部的删除功能也是通过调用 delete 来实现的,要释放什么类型的内存就将模板类型 T 指定为什么类型即可。具体处理代码如下:

int main(int argc,char* argv[]){
    shared_ptr<int> ptr(new int[10], default_delete<int[]>());
    return 0;
}

另外,我们还可以自己封装一个 make_shared_array 方法来让 shared_ptr 支持数组,代码如下:

#include <iostream>
#include <memory>
using namespace std;

template <typename T>
shared_ptr<T> make_share_array(size_t size){
    // 返回匿名对象
    return shared_ptr<T>(new T[size], default_delete<T[]>());
}

int main(int argc,char* argv[]){
    shared_ptr<int> ptr1 = make_share_array<int>(10);
    cout << ptr1.use_count() << endl;
    shared_ptr<char> ptr2 = make_share_array<char>(128);
    cout << ptr2.use_count() << endl;
    return 0;
}

 二十二、独占的智能指针

std::unique_ptr 是一个独占型的智能指针,它不允许其他的智能指针共享其内部的指针,可以通过它的构造函数初始化一个独占智能指针对象,但是不允许通过赋值将一个 unique_ptr 赋值给另一个 unique_ptr。

1、初始化

(1)通过构造函数初始化

// 通过构造函数初始化对象
unique_ptr<int> ptr1(new int(10));
// error, 不允许将一个unique_ptr赋值给另一个unique_ptr
unique_ptr<int> ptr2 = ptr1;

(2)通过移动构造函数初始化

std::unique_ptr 不允许复制,但是可以通过函数返回给其他的 std::unique_ptr,还可以通过 std::move 来转译给其他的 std::unique_ptr,这样原始指针的所有权就被转移了,这个原始指针还是被独占的。

#include <iostream>
#include <memory>
using namespace std;

unique_ptr<int> func(){
    return unique_ptr<int>(new int(520));
}

int main(int argc,char* argv[]){
    // 通过构造函数初始化
    unique_ptr<int> ptr1(new int(10));
    // 通过转移所有权的方式初始化
    unique_ptr<int> ptr2 = move(ptr1);
    unique_ptr<int> ptr3 = func();

    return 0;
}

(3)通过reset初始化

函数原型:

void reset( pointer ptr = pointer() ) noexcept;

使用 reset 方法可以让 unique_ptr 解除对原始内存的管理,也可以用来初始化一个独占的智能指针。重置之后,原有的内存就会直接释放了!

int main(int argc,char* argv[]){
    unique_ptr<int> ptr1(new int(10));
    unique_ptr<int> ptr2 = move(ptr1);

    ptr1.reset();
    ptr2.reset(new int(250));

    return 0;
}
  • ptr1.reset(); 解除对原始内存的管理

  • ptr2.reset(new int(250)); 重新指定智能指针管理的原始内存

(4) 获取原始指针

如果想要获取独占智能指针管理的原始地址,可以调用 get () 方法,函数原型如下:

pointer get() const noexcept;
int main(int argc,char* argv[]){
    unique_ptr<int> ptr1(new int(10));
    unique_ptr<int> ptr2 = move(ptr1);

    ptr2.reset(new int(250));
    cout << *ptr2.get() << endl;	// 得到内存地址中存储的实际数值 250

    return 0;
}

 2、删除器

unique_ptr指定删除器和shared_ptr指定删除器是有区别的,unique_ptr指定删除器的时候需要确定删除器的类型,所以不能像shared_ptr那样直接指定删除器,

shared_ptr<int> ptr1(new int(10), [](int*p) {delete p; });	// ok
unique_ptr<int> ptr1(new int(10), [](int*p) {delete p; });	// error

int main(int argc,char* argv[]){
    using func_ptr = void(*)(int*);
    unique_ptr<int, func_ptr> ptr1(new int(10), [](int*p) {delete p; });

    return 0;
}

在上面的代码中第 7 行,func_ptr 的类型和 lambda表达式的类型是一致的。在 lambda 表达式没有捕获任何变量的情况下是正确的,如果捕获了变量,编译时则会报错:

int main(int argc,char* argv[]){
    using func_ptr = void(*)(int*);
    unique_ptr<int, func_ptr> ptr1(new int(10), [&](int*p) {delete p; });	// error
    return 0;
}

上面的代码中错误原因是这样的,在 lambda 表达式没有捕获任何外部变量时,可以直接转换为函数指针,一旦捕获了就无法转换了,如果想要让编译器成功通过编译,那么需要使用可调用对象包装器来处理声明的函数指针:

int main(int argc,char* argv[]){
    using func_ptr = void(*)(int*);
    unique_ptr<int, function<void(int*)>> ptr1(new int(10), [&](int*p) {delete p; });
    return 0;
}

二十三、弱引用的智能指针

1、基本使用方法

弱引用智能指针std::weak_ptr可以看做是shared_ptr的助手,它不管理shared_ptr内部的指针,std::weak_ptr没有重载操作符*和->,因为它不共享指针,不能操作资源,所以它的构造不会增加引用计数,析构也不会减少引用计数,它的主要作用就是作为旁观者监视shared_ptr中管理的资源是否存在!

shared_ptr使用的注意事项:

  • 不能使用一个原始地址初始化多个共享智能指针;
  • 函数不能返回管理了this的共享智能指针对象;
  • 共享智能指针不能循环引用;
int* p=new int(5);
shared_ptr<int> ptr1(p);
shared_ptr<int> ptr2(p);//error
shared_ptr<int> ptr3=ptr1;//ok

以上案例析构了两次,因此不能使用一个原始地址初始化多个共享智能指针!

// 默认构造函数
constexpr weak_ptr() noexcept;
// 拷贝构造
weak_ptr (const weak_ptr& x) noexcept;
template <class U> weak_ptr (const weak_ptr<U>& x) noexcept;
// 通过shared_ptr对象构造
template <class U> weak_ptr (const shared_ptr<U>& x) noexcept;

在 C++11 中,weak_ptr 的初始化可以通过以上提供的构造函数来完成初始化,具体使用方法如下:

#include <iostream>
#include <memory>
using namespace std;

int main(int argc,char* argv[]){
    shared_ptr<int> sp(new int);

    weak_ptr<int> wp1;
    weak_ptr<int> wp2(wp1);
    weak_ptr<int> wp3(sp);
    weak_ptr<int> wp4;
    wp4 = sp;
    weak_ptr<int> wp5;
    wp5 = wp3;
    
    return 0;
}
  • weak_ptr<int> wp1; 构造了一个空 weak_ptr 对象

  • weak_ptr<int> wp2(wp1); 通过一个空 weak_ptr 对象构造了另一个空 weak_ptr 对象

  • weak_ptr<int> wp3(sp); 通过一个 shared_ptr 对象构造了一个可用的 weak_ptr 实例对象

  • wp4 = sp; 通过一个 shared_ptr 对象构造了一个可用的 weak_ptr 实例对象(这是一个隐式类型转换)

  • wp5 = wp3; 通过一个 weak_ptr 对象构造了一个可用的 weak_ptr 实例对象

2、其他常用方法

(1)use_count()

通过调用 std::weak_ptr 类提供的 use_count() 方法可以获得当前所观测资源的引用计数,函数原型如下,

// 函数返回所监测的资源的引用计数
long int use_count() const noexcept;

修改一下上面的测试程序,添加打印资源引用计数的代码:

#include <iostream>
#include <memory>
using namespace std;

int main(int argc,char* argv[]){
    shared_ptr<int> sp(new int);

    weak_ptr<int> wp1;
    weak_ptr<int> wp2(wp1);
    weak_ptr<int> wp3(sp);
    weak_ptr<int> wp4;
    wp4 = sp;
    weak_ptr<int> wp5;
    wp5 = wp3;

    cout << "use_count: " << endl;
    cout << "wp1: " << wp1.use_count() << endl;
    cout << "wp2: " << wp2.use_count() << endl;
    cout << "wp3: " << wp3.use_count() << endl;
    cout << "wp4: " << wp4.use_count() << endl;
    cout << "wp5: " << wp5.use_count() << endl;
    return 0;
}

测试程序输出的结果为:

use_count:
wp1: 0
wp2: 0
wp3: 1
wp4: 1
wp5: 1

通过打印的结果可以知道,虽然弱引用智能指针 wp3、wp4、wp5 监测的资源是同一个,但是它的引用计数并没有发生任何的变化,也进一步证明了 weak_ptr只是监测资源,并不管理资源。

(2) expired()

通过调用 std::weak_ptr 类提供的 expired() 方法来判断观测的资源是否已经被释放,函数原型如下:

// 返回true表示资源已经被释放, 返回false表示资源没有被释放
bool expired() const noexcept;

函数的使用方法如下:

#include <iostream>
#include <memory>
using namespace std;

int main(int argc,char* argv[]){
    shared_ptr<int> shared(new int(10));
    weak_ptr<int> weak(shared);
    cout << "1. weak " << (weak.expired() ? "is" : "is not") << " expired" << endl;

    shared.reset();
    cout << "2. weak " << (weak.expired() ? "is" : "is not") << " expired" << endl;

    return 0;
}

 测试结果:

 weak is not expired
 weak is expired

weak_ptr 监测的就是 shared_ptr 管理的资源,当共享智能指针调用 shared.reset(); 之后管理的资源被释放,因此 weak.expired() 函数的结果返回 true,表示监测的资源已经不存在了。

(3)lock()

通过调用 std::weak_ptr 类提供的 lock() 方法来获取管理所监测资源的 shared_ptr 对象,函数原型如下:

shared_ptr<element_type> lock() const noexcept;

函数的使用方法如下:

#include <iostream>
#include <memory>
using namespace std;

int main(int argc,char* argv[]){
    shared_ptr<int> sp1, sp2;
    weak_ptr<int> wp;

    sp1 = std::make_shared<int>(520);
    wp = sp1;
    sp2 = wp.lock();
    cout << "use_count: " << wp.use_count() << endl;

    sp1.reset();
    cout << "use_count: " << wp.use_count() << endl;

    sp1 = wp.lock();
    cout << "use_count: " << wp.use_count() << endl;

    cout << "*sp1: " << *sp1 << endl;
    cout << "*sp2: " << *sp2 << endl;

    return 0;
}

测试代码输出的结果为:

use_count: 2
use_count: 1
use_count: 2
*sp1: 520
*sp2: 520

  • sp2 = wp.lock(); 通过调用 lock() 方法得到一个用于管理 weak_ptr 对象所监测的资源的共享智能指针对象,使用这个对象初始化 sp2,此时所监测资源的引用计数为 2
  • sp1.reset(); 共享智能指针 sp1 被重置,weak_ptr 对象所监测的资源的引用计数减 1
  • sp1 = wp.lock();sp1 重新被初始化,并且管理的还是 weak_ptr 对象所监测的资源,因此引用计数加 1
  • 共享智能指针对象 sp1 和 sp2 管理的是同一块内存,因此最终打印的内存中的结果是相同的,都是 520

(4)reset()
通过调用 std::weak_ptr 类提供的 reset() 方法来清空对象,使其不监测任何资源,函数原型如下:

void reset() noexcept;

函数的使用是非常简单的,示例代码如下:

#include <iostream>
#include <memory>
using namespace std;

int main(int argc,char* argv[]){
    shared_ptr<int> sp(new int(10));
    weak_ptr<int> wp(sp);
    cout << "1. wp " << (wp.expired() ? "is" : "is not") << " expired" << endl;

    wp.reset();
    cout << "2. wp " << (wp.expired() ? "is" : "is not") << " expired" << endl;

    return 0;
}

测试代码输出的结果为:

wp is not expired
wp is expired

weak_ptr 对象 sp 被重置之后 wp.reset(); 变成了空对象,不再监测任何资源,因此 wp.expired() 返回 true。

3、返回管理 this 的 shared_ptr

如果在一个类中编写了一个函数,通过这个得到管理当前对象的共享智能指针,我们可能会写出如下代码:

#include <iostream>
#include <memory>
using namespace std;

struct Test{
    shared_ptr<Test> getSharedPtr(){
        return shared_ptr<Test>(this);
    }
    
    ~Test(){
        cout << "class Test is disstruct ..." << endl;
    }

};

int main(int argc,char* argv[]){
    shared_ptr<Test> sp1(new Test);
    cout << "use_count: " << sp1.use_count() << endl;
    shared_ptr<Test> sp2 = sp1->getSharedPtr();
    cout << "use_count: " << sp1.use_count() << endl;
    return 0;
}

执行上面的测试代码,运行中会出现异常,在终端还是能看到对应的日志输出:

use_count: 1
use_count: 1
class Test is disstruct ...
class Test is disstruct ...

通过输出的结果可以看到一个对象被析构了两次,其原因是这样的:在这个例子中使用同一个指针 this 构造了两个智能指针对象 sp1 和 sp2,这二者之间是没有任何关系的,因为 sp2 并不是通过 sp1 初始化得到的实例对象。在离开作用域之后 this 将被构造的两个智能指针各自析构,导致重复析构的错误。

这个问题可以通过 weak_ptr 来解决,通过 wek_ptr 返回管理 this 资源的共享智能指针对象 shared_ptr。C++11 中为我们提供了一个模板类叫做 std::enable_shared_from_this<T>,这个类中有一个方法叫做 shared_from_this(),通过这个方法可以返回一个共享智能指针,在函数的内部就是使用 weak_ptr 来监测 this 对象,并通过调用 weak_ptr 的 lock() 方法返回一个 shared_ptr 对象。

修改之后的代码为:

#include <iostream>
#include <memory>
using namespace std;

struct Test : public enable_shared_from_this<Test>{
    shared_ptr<Test> getSharedPtr(){
        return shared_from_this();
    }
    ~Test(){
        cout << "class Test is disstruct ..." << endl;
    }
};

int main(int argc,char* argv[]){
    shared_ptr<Test> sp1(new Test);
    cout << "use_count: " << sp1.use_count() << endl;
    shared_ptr<Test> sp2 = sp1->getSharedPtr();
    cout << "use_count: " << sp1.use_count() << endl;
    return 0;
}

测试代码输出的结果为:

use_count: 1
use_count: 2
class Test is disstruct ...

最后需要强调一个细节:在调用 enable_shared_from_this 类的 hared_from_this () 方法之前,必须要先初始化函数内部 weak_ptr 对象,否则该函数无法返回一个有效的 shared_ptr 对象(具体处理方法可以参考上面的示例代码)。

4、解决循环引用问题

智能指针如果循环引用会导致内存泄露,比如下面的例子:

#include <iostream>
#include <memory>
using namespace std;

struct TA;
struct TB;

struct TA{
    shared_ptr<TB> bptr;
    ~TA(){
        cout << "class TA is disstruct ..." << endl;
    }
};

struct TB{
    shared_ptr<TA> aptr;
    ~TB(){
        cout << "class TB is disstruct ..." << endl;
    }
};

void testPtr(){
    shared_ptr<TA> ap(new TA);
    shared_ptr<TB> bp(new TB);
    cout << "TA object use_count: " << ap.use_count() << endl;
    cout << "TB object use_count: " << bp.use_count() << endl;

    ap->bptr = bp;
    bp->aptr = ap;
    cout << "TA object use_count: " << ap.use_count() << endl;
    cout << "TB object use_count: " << bp.use_count() << endl;
}

int main(int argc,char* argv[]){
    testPtr();
    return 0;
}

测试程序输出的结果如下:

TA object use_count: 1
TB object use_count: 1
TA object use_count: 2
TB object use_count: 2

在测试程序中,共享智能指针 ap、bp 对 TA、TB 实例对象的引用计数变为 2,在共享智能指针离开作用域之后引用计数只能减为1,这种情况下不会去删除智能指针管理的内存,导致类 TA、TB 的实例对象不能被析构,最终造成内存泄露。通过使用 weak_ptr 可以解决这个问题,只要将类 TA 或者 TB 的任意一个成员改为 weak_ptr,修改之后的代码如下:

#include <iostream>
#include <memory>
using namespace std;

struct TA;
struct TB;

struct TA{
    weak_ptr<TB> bptr;
    ~TA(){
        cout << "class TA is disstruct ..." << endl;
    }
};

struct TB{
    shared_ptr<TA> aptr;
    ~TB(){
        cout << "class TB is disstruct ..." << endl;
    }
};

void testPtr(){
    shared_ptr<TA> ap(new TA);
    shared_ptr<TB> bp(new TB);
    cout << "TA object use_count: " << ap.use_count() << endl;
    cout << "TB object use_count: " << bp.use_count() << endl;

    ap->bptr = bp;
    bp->aptr = ap;
    cout << "TA object use_count: " << ap.use_count() << endl;
    cout << "TB object use_count: " << bp.use_count() << endl;
}

int main(int argc,char* argv[]){
    testPtr();
    return 0;
}

程序输出的结果:

TA object use_count: 1
TB object use_count: 1
TA object use_count: 2
TB object use_count: 1
class TB is disstruct ...
class TA is disstruct ...

通过输出的结果可以看到类 TA 或者 TB 的对象被成功析构了。

上面程序中,在对类 TA 成员赋值时 ap->bptr = bp; 由于 bptr 是 weak_ptr 类型,这个赋值操作并不会增加引用计数,所以 bp 的引用计数仍然为 1,在离开作用域之后 bp 的引用计数减为 0,类 TB 的实例对象被析构。

在类 TB 的实例对象被析构的时候,内部的 aptr 也被析构,其对 TA 对象的管理解除,内存的引用计数减为 1,当共享智能指针 ap 离开作用域之后,对 TA 对象的管理也解除了,内存的引用计数减为 0,类 TA 的实例对象被析构!

  • 1
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值