C++11新特性总结

C++11 是第二个真正意义上的 C++ 标准,也是 C++ 的一次重大升级。C++11 增加了很多现代编程语言的特性比如,自动类型推导智能指针lambda表达式等,这使得 C++ 看起来又酷又潮,一点也不输 Java 和 C#。虽然学习 C++11 需要花些时间,但这是非常值得的;C++11 非常实用,它不但提高了开发效率,还让程序更加健壮和优雅

截止到2020 年,C++ 的发展历经了以下 3 个个标准:

  • 2011 年,新的 C++ 11 标准诞生,用于取代 C++ 98 标准。此标准还有一个别名,为“C++ 0x”
  • 2014 年,C++ 14 标准发布,该标准库对 C++ 11 标准库做了更优的修改和更新;
  • 2017 年底,C++ 17 标准正式颁布。

所谓标准,即明确 C++ 代码的编写规范,所有的 C++ 程序员都应遵守此标准。

C++ auto类型推导完全攻略

C++11 赋予 auto 关键字新的含义,使用它来做自动类型推导。也就是说,使用了 auto 关键字以后,编译器会在编译期间自动推导出变量的类型,这样我们就不用手动指明变量的数据类型了。

auto 关键字基本的使用语法如下:

auto name = value;

注意auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代或者说C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。  

auto 类型推导的简单例子:

我们也可以连续定义多个变量

int n = 20;
auto *p = &n, m = 99;
  • 先看前面的第一个子表达式,&n 的类型是 int*,编译器会根据 auto *p 推导出 auto 为 int。
  • 后面的 m 变量自然也为 int 类型,所以把 99 赋值给它也是正确的。 

这里我们要注意,推导的时候不能有二义性。在本例中,编译器根据第一个子表达式已经推导出 auto 为 int 类型,那么后面的 m 也只能是 int 类型,如果写作m=12.5就是错误的,因为 12.5 是double 类型,这和 int 是冲突的。 

还有一个值得注意的地方是:使用 auto 类型推导的变量必须马上初始化,这个很容易理解,因为 auto 在 C++11 中只是“占位符”,并非如 int 一样的真正的类型声明。(个人:也就是说,在有对一个变量定义时使用auto,这个auto只是一个占位符,这个变量的类型要在编译时编译器通过初始值来推导出来,如果没有初始值,怎么编译器就无法推导,这不像,int,直接就给出了类型,C++是静态语言,定义一个变量,必须得有类型)  

auto 除了可以独立使用,还可以和某些具体类型混合使用,这样 auto 表示的就是“半个”类型,而不是完整的类型。请看下面的代码:(个人:也就是和指针标志,引用标志,常量标志结合使用)

int  x = 0;
auto *p1 = &x;   //p1 为 int *,auto 推导为 int
auto  p2 = &x;   //p2 为 int*,auto 推导为 int*
auto &r1  = x;   //r1 为 int&,auto 推导为 int
auto r2 = r1;    //r2 为  int,auto 推导为 int

接下来,我们再来看一下 auto和const的结合

  • 第 3 行代码中,n 为 const int 类型,但是 auto 却被推导为 int 类型,这说明当=右边的表达式带有 const 属性时, auto 不会使用 const 属性,而是直接推导出 non-const 类型。(个人:类比一下,int a = 12;这个12是个常量字面量,作为变量a的初始值,但是这里a不是一个常量,a的值可以改变,所以a占用的内存和12这个常量占用的内存不是一个地方,所以变量a的类型可以不是常量类型,类似的这里的auto推导const属性被抛弃)
  • 第 4行代码中,n是 const int类型,r2位const int &类型,auto 也被推导为const int 类型,这说明当左边待推导的类型为一个引用时,auto 的推导将保留右边表达式的 const 类型。(个人:很显然的,因为左边变量是一个引用,和右边是使用的同一个内存位置,既然右边是const的,也就是这个内存的值不能改变,左边作为这个内存的引用当然也不能改变这个内存的值,所以这个引用得是const类型的)  

最后我们来简单总结一下auto与const结合的用法

  • 当类型不为引用时,auto 的推导结果将不保留表达式的 const 属性
  • 当类型为引用时,auto 的推导结果将保留表达式的 const 属性

auto 的限制

  • 使用auto的时候必须对变量进行初始化
  • auto不能在函数的参数中使用。这个应该很容易理解,我们在定义函数的时候只是对参数进行了声明,指明了参数的类型,但并没有给它赋值,只有在实际调用函数的时候才会给参数赋值;而auto 要求必须对变量进行初始化,所以这是矛盾的。(个人:auto主要是用在一个变量的定义处,编译器通过定义时的初始值推断出这个变量的类型,但是在函数的参数中,主要是声明这个函数的参数原型,需要明白无误的指出这个参数的类型是什么,也就是作为函数原型的一部分,必须得说明这个函数的参数类型是什么,这里的类型只是起一个说明的作用,而不是auto那样的定义时通过初始值来推断变量的类型这样的应用场景)
  • auto不能作用于类的非静态成员变量中。(个人:因为类的对象内存空间保存的是非静态数据成员,必须知道他们的类型才能知道内存空间如何布局,这个的类型需要明确的声明出来,这个场景是明确的声明变量的类型而不是auto那样的定义变量时通过初始值推断变量类型那样的应用场景)

  • auto 关键字不能定义数组

  • auto 不能作用于模板参数,请看下面的例子:

    template <typename T>
    class A{
        //TODO:
    };
    int  main(){
        A<int> C1;
        A<auto> C2 = C1;  //错误
        return 0;
    }

auto 的两个典型应用场景

  • 使用 auto 定义迭代器

原来:

#include <vector>
using namespace std;
int main(){
    vector< vector<int> > v;
    vector< vector<int> >::iterator i = v.begin();
    return 0;
}

使用auto:

#include <vector>
using namespace std;
int main(){
    vector< vector<int> > v;
    auto i = v.begin();  //使用 auto 代替具体的类型
    return 0;
}
  • auto 用于泛型编程,auto 的另一个应用就是当我们不知道变量是什么类型,或者不希望指明具体类型的时候
#include <iostream>
using namespace std;

class A {
public:
    static int get(void) {
        return 100;
    }
};
class B {
public:
    static const char* get(void) {
        return "http://www.baidu,com";
    }
};
template <typename T>
void func(void) {
    auto val = T::get();
    cout << val << endl;
}
int main(void) {
    func<A>();
    func<B>();
    return 0;
}

本例中的模板函数 func() 会调用所有类的静态函数 get(),并对它的返回值做统一处理,但是 get() 的返回值类型并不一样,而且不能自动转换。

这种要求在以前的 C++ 版本中实现起来非常的麻烦,需要额外增加一个模板参数,并在调用时手动给该模板参数赋值,用以指明变量 val 的类型。 但是有了 auto 类型自动推导,编译器就根据 get() 的返回值自己推导出 val 变量的类型,就不用再增加一个模板参数了。 

下面的代码演示了不使用 auto 的解决办法

#include <iostream>
using namespace std;

class A {
public:
    static int get(void) {
        return 100;
    }
};
class B {
public:
    static const char* get(void) {
        return "http://www.baidu.com";
    }
};
template <typename T1, typename T2>  //额外增加一个模板参数 T2
void func(void) {
    T2 val = T1::get();
    cout << val << endl;
}
int main(void) {
    //调用时也要手动给模板参数赋值
    func<A, int>();
    func<B, const char*>();
    return 0;
}

C++ decltype类型推导完全攻略

decltype 是 C++11 新增的一个关键字,它和 auto 的功能一样,都用来在编译时期进行自动类型推导。 

decltype 是“declare type”的缩写,译为“声明类型”

auto 和 decltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的: 

auto varname = value;
decltype(exp) varname = value;
  • auto 根据=右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=右边的 value 没有关系
  • 另外,auto 要求变量必须初始化,而 decltype 不要求。这很容易理解,auto 是根据变量的初始值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。decltype 可以写成下面的形式:
decltype(exp) varname;

原则上讲,exp 就是一个普通的表达式,它可以是任意复杂的形式,但是我们必须要保证 exp 的结果是有类型的,不能是 void

C++ decltype 用法举例:(个人:也就是decltype能够安排一个变量的类型和一个表达式的类型一样)

int a = 0;
decltype(a) b = 1;  //b 被推导成了 int
decltype(10.8) x = 5.5;  //x 被推导成了 double
decltype(x + 100) y;  //y 被推导成了 double 

当程序员使用 decltype(exp) 获取类型时,编译器将根据以下三条规则得出结果:

  • 如果 exp 是一个不被括号( )包围的表达式,或者是一个类成员访问表达式,或者是一个单独的变量那么 decltype(exp) 的类型就和 exp 一致,这是最普遍最常见的情况

  • 如果 exp 是函数调用,那么 decltype(exp) 的类型就和函数返回值的类型一致
//函数声明
int& func_int_r(int, char);  //返回值为 int&
int&& func_int_rr(void);  //返回值为 int&&
int func_int(double);  //返回值为 int
const int& fun_cint_r(int, int, int);  //返回值为 const int&
const int&& func_cint_rr(void);  //返回值为 const int&&
//decltype类型推导
int n = 100;
decltype(func_int_r(100, 'A')) a = n;  //a 的类型为 int&
decltype(func_int_rr()) b = 0;  //b 的类型为 int&&
decltype(func_int(10.5)) c = 0;   //c 的类型为 int
decltype(fun_cint_r(1,2,3))  x = n;    //x 的类型为 const int &
decltype(func_cint_rr()) y = 0;  // y 的类型为 const int&&

需要注意的是,exp 中调用函数时需要带上括号和参数,但这仅仅是形式,并不会真的去执行函数代码

  • 如果表达式exp 是一个左值(个人:是一个表达式,而不是一个单独变量的情况),或者被括号( )包围,那么 decltype(exp) 的类型就是 exp 的引用;假设 exp 的类型为 T,那么 decltype(exp) 的类型就是 T&。

     

using namespace std;
class Base {
public:
    int x;
};
int main() {
    const Base obj;
    //带有括号的表达式
    decltype(obj.x) a = 0;  //obj.x 为类的成员访问表达式,符合推导规则一,a 的类型为 int
    decltype((obj.x)) b = a;  //obj.x 带有括号,符合推导规则三,b 的类型为 int&。
    //加法表达式
    int n = 0, m = 0;
    decltype(n + m) c = 0;  //n+m 得到一个右值,符合推导规则一,所以推导结果为 int
    decltype(n = n + m) d = c;  //n=n+m 得到一个左值,符号推导规则三,所以推导结果为 int&
    return 0;

这里我们需要重点说一下左值和右值:

    • 左值是指那些在表达式执行结束后依然存在的数据,也就是持久性的数据
    • 右值是指那些在表达式执行结束后不再存在的数据,也就是临时性的数据

有一种很简单的方法来区分左值和右值,对表达式取地址,如果编译器不报错就为左值,否则为右值。  

decltype 的实际应用例子:

下面是一个模板的定义:  

#include <vector>
using namespace std;
template <typename T>
class Base {
public:
    void func(T& container) {
        m_it = container.begin();
    }
private:
    T::iterator m_it;  //注意这里
};
int main()
{
    const vector<int> v;
    Base<const vector<int>> obj;
    obj.func(v);
    return 0;
}

单独看 Base 类中 m_it 成员的定义,很难看出会有什么错误,但在使用 Base 类的时候,如果传入一个 const 类型的容器,编译器马上就会弹出一大堆错误信息原因就在于,T::iterator并不能包括所有的迭代器类型,当T是一个 const 容器时,应当使用 const_iterator。 要想解决这个问题,在之前的 C++98/03 版本下只能想办法把 const 类型的容器用模板特化单独处理,增加了不少工作量,看起来也非常晦涩。但是有了 C++11 的 decltype 关键字,就可以直接这样写:(个人:验证后完全没有错误)  

template <typename T>
class Base {
public:
    void func(T& container) {
        m_it = container.begin();
    }
private:
    decltype(T().begin()) m_it;  //注意这里
};

C++ auto和decltype的区别

对 cv 限定符的处理:  

「cv 限定符」是 const 和 volatile [ˈvɒlətaɪl]关键字的统称: 

  • const 关键字用来表示数据是只读的,也就是不能被修改
  • volatile 和 const 是相反的,它用来表示数据是可变的、易变的,目的是不让 CPU 将数据缓存到寄存器,而是从原始的内存中读取

在推导变量类型时,auto 和 decltype 对 cv 限制符的处理是不一样的。

  • decltype 会保留cv限定符,(个人:这里是针对一个单独的变量而言的)
  • 而 auto有可能会去掉cv限定符。 以下是 auto 关键字对 cv 限定符的推导规则: 
    • 如果推导的最终类型不是指针或者引用,auto 会把 cv 限定符直接抛弃,推导成 non-const 或者 non-volatile 类型。(个人:定义的变量和初始值占用的不是同一个内存,彼此独立)
    • 如果推导的最终类型是指针或者引用,auto 将保留 cv 限定符。(个人:定义的变量和初始值指向的内存都是同一个,要保持状态同步)  

下面的例子演示了对 const 限定符的推导:

对引用的处理

当表达式的类型为引用时,auto 和 decltype 的推导规则也不一样;

  • decltype 会保留引用类型
  • 而 auto 会抛弃引用类型,直接推导出它的原始类型

请看下面的例子:  

 

C++返回值类型后置(跟踪返回值类型)

在泛型编程中,可能需要通过参数的运算来得到返回值的类型,那么,在 add 函数的定义上能不能直接通过 decltype 拿到返回值呢?

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

当然,直接像上面这样写是编译不过的。因为 t、u 在参数列表中,而 C++ 的返回值是前置语法,在返回值定义的时候参数变量还不存在。  

可行的写法如下:

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

考虑到 T、U 可能是没有无参构造函数的类,正确的写法应该是这样:(个人:这里(T*)0是将0这个地址值强制转化为一个T*类型的指针,然后获得这个指针所指的T类型的值))  

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

虽然成功地使用decltype完成了返回值的推导,但写法过于晦涩,会大大增加 decltype 在返回值类型推导上的使用难度并降低代码的可读性。因此,在 C++11 中增加了返回类型后置(trailing-return-type,又称跟踪返回类型)语法,将 decltype 和 auto 结合起来完成返回值类型的推导。(个人:Python中也有返回值类型后置语法)

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

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

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

int& foo(int& i);
float foo(float& f);
template <typename T>
auto func(T& val) -> decltype(foo(val))
{
    return foo(val);
}

C++11对模板实例化中连续右尖括号>>的改进

在 C++98/03 的泛型编程中,模板实例化有一个很烦琐的地方,那就是连续两个右尖括号(>>)会被编译器解释成右移操作符,而不是模板参数表的结束。  

template <typename T>
struct Foo
{
      typedef T type;
};
template <typename T>
class A
{
    // ...
};
int main(void)
{
    Foo<A<int>>::type xx;  //编译出错
    return 0;
}

这种限制无疑是很没有必要的。在 C++ 的各种成对括号中,目前只有右尖括号连续写两个会出现这种二义性。static_cast、reinterpret_cast 等 C++ 标准转换运算符,都是使用<>来获得待转换类型(type-id)的。若这个 type-id 本身是一个模板,用起来会很不方便。现在在 C++11 中,这种限制终于被取消了。在C++11 标准中,要求编译器对模板的右尖括号做单独处理,使编译器能够正确判断出>>是一个右移操作符还是模板参数表的结束标记(delimiter,界定符)。  

不过这种自动化的处理在某些时候会与老标准不兼容,比如下面这个例子:

template <int N>
struct Foo
{
    // ...
};
int main(void)
{
    Foo<100 >> 2> xx;
    return 0;
}

在 C++98/03 的编译器中编译是没问题的,但 C++11 的编译器会显示:  

解决的方法是这样写:

Foo<(100 >> 2)> xx;  // 注意括号

这种加括号的写法其实也是一个良好的编程习惯,使得在书写时倾向于写出无二义性的代码。  

C++11使用using定义别名(替代typedef)

在 C++ 中可以通过 typedef 重定义一个类型

typedef unsigned int uint_t;

被重定义的类型并不是一个新的类型,仅仅只是原有的类型取了一个新的名字。  

使用typedef重定义类型是很方便的,但它也有一些限制,比如,无法重定义一个模板。(个人:只能一个一个定义,或者在外面包一层)  

typedef std::map<std::string, int> map_int_t;
// ...
typedef std::map<std::string, std::string> map_str_t;
// ...

我们需要的其实是一个固定以 std::string 为 key 的 map,它可以映射到 int 另一个 std::string。然而这个简单的需求仅通过 typedef 却很难办到

因此,在 C++98/03 中往往不得不这样写:(个人:这里还是定义了一个模板类,在这个模板类内部使用了typedef语法,正常情况下,我们是可以在一个普通的类里面使用typedef语法的)

template <typename Val>
struct str_map
{
    typedef std::map<std::string, Val> type;
};
// ...
str_map<int>::type map1;
// ...

一个虽然简单但却略显烦琐的str_map外敷类是必要的。这明显让我们在复用某些泛型代码时非常难受。

现在,在 C++11 中终于出现了可以重定义一个模板的语法。请看下面的示例:(个人:也就是在C++11中我们可以直接使用using别名语法定义一个模板别名)

template <typename Val>
using str_map_t = std::map<std::string, Val>;
// ...
str_map_t<int> map1;

这里使用新的 using 别名语法定义了 std::map 的模板别名 str_map_t。比起前面使用外敷模板加 typedef 构建的 str_map,它完全就像是一个新的 map 类模板,因此,简洁了很多。  

实际上,using 的别名语法覆盖了 typedef 的全部功能。先来看看对普通类型的重定义示例,将这两种语法对比一下:  

// 重定义unsigned int
typedef unsigned int uint_t;
using uint_t = unsigned int;
// 重定义std::map
typedef std::map<std::string, int> map_int_t;
using map_int_t = std::map<std::string, int>;

typedef的定义方法和变量的声明类似:像声明一个变量一样,声明一个重定义类型,之后在声明之前加上 typedef 即可。这种写法凸显了 C/C++ 中的语法一致性,但有时却会增加代码的阅读难度。比如重定义一个函数指针时:  

typedef void (*func_t)(int, int);

与之相比,using 后面总是立即跟随新标识符(Identifier),之后使用类似赋值的语法,把现有的类型(type-id)赋给新类型

using func_t = void (*)(int, int);

从上面的对比中可以发现,C++11 的 using 别名语法比 typedef 更加清晰

  • 因为 typedef 的别名语法本质上类似一种解方程的思路
  • 而using语法通过赋值来定义别名,和我们平时的思考方式一致。  
/* C++98/03 */
template <typename T>
struct func_t
{
    typedef void (*type)(T, T);
};
// 使用 func_t 模板
func_t<int>::type xx_1;
/* C++11 */
template <typename T>
using func_t = void (*)(T, T);
// 使用 func_t 模板
func_t<int> xx_2;

从示例中可以看出,通过using定义模板别名的语法,只是在普通using类型别名语法的基础上增加template的参数列表。使用using可以轻松地创建一个新的模板别名,而不需要像 C++98/03 那样使用烦琐的外敷模板。  

需要注意的是,

  • using 语法和 typedef 一样,并不会创造新的类型。也就是说,上面示例中 C++11 的 using 写法只是 typedef 的等价物。
  • 虽然using重定义的func_t是一个模板,但 func_t<int> 定义的xx_2并不是一个由类模板实例化后的类,而是 void(*)(int, int) 的别名。(个人:也就是说,这里通过using重定义的func_t是一个模板,但是一个模板别名,而不是一个类模板),因此,下面这样写:
    void foo(void (*func_call)(int, int));
    void foo(func_t<int> func_call);  // error: redefinition
    

    同样是无法实现重载的,func_t<int> 只是 void(*)(int, int) 类型的等价物using重定义的func_t是一个模板,但它既不是类模板也不是函数模板(函数模板实例化后是一个函数),而是一种新的模板形式:模板别名(alias template)

其实,通过using可以轻松定义任意类型的模板表达方式。比如下面这样: 
template <typename T>
using type_t = T;
// ...
type_t<int> i;

type_t 实例化后的类型和它的模板参数类型等价。这里,type_t<int> 将等价于 int。

C++11支持函数模板的默认模板参数

 在C++98/03标准中,类模板可以有默认的模板参数,但是却不支持函数的默认模板参数
template <typename T = int>  // error in C++98/03: default template arguments
void func()
{
    // ...
}

现在这一限制在C++11 中被解除了。上面的func函数在 C++11 中可以直接使用,代码如下:(个人:实际运行后确实如此)

int main(void)
{
    func();   //T = int
    return 0;
}

此时函数模板参数 T 的类型就为默认值 int

从上面的例子中可以看出,当所有模板参数都有默认参数时,

  • 函数模板的调用如同一个普通函数。
  • 但对于类模板而言,哪怕所有参数都有默认参数,在使用时也必须在模板名后跟随<>来实例化。  

除了上面提到的部分之外,函数模板的默认模板参数在使用规则上和其他的默认参数也有一些不同

  • 它没有必须写在参数表最后的限制
  • 甚至于,根据实际场景中函数模板被调用的情形,编译器还可以自行推导出部分模板参数的类型

这意味着,当默认模板参数和编译器自行推导出模板参数类型的能力一起结合使用时,代码的书写将变得异常灵活。我们可以指定函数中的一部分模板参数采用默认参数,而另一部分使用自动推导,比如下面的例子:(个人:实际运行之,正确)  

template <typename R = int, typename U>
R func(U val)
{
    return val;
}
int main()
{
    func(97);               // R=int, U=int
    func<char>(97);         // R=char, U=int
    func<double, int>(97);  // R=double, U=int
    return 0;
}
  • C++11 标准中,我们可以像 func(97) 这样调用模板函数,因为编译器可以根据实参 97 自行推导出模板参数 U 的类型为 int,并且根据返回值 val=97 推导出 R 的类型也为 int;
  • 而 func<char>(97) 手动指定了模板参数 R 的类型为 char(默认模板参数将无效),并通过实参 97 推导出了 U = int;(个人:指定模板参数只能按照位置从左到右对应)
  • 最后func<double,int>(97) 手动指定的 R 和 U 的类型值,因此无需编译器自行推导。(个人:也就是手动指定函数模板参数后,编译器就不会进行自动推导了)  

再次强调,

  • 当默认模板参数和自行推导的模板参数同时使用时,若无法推导出函数模板参数的类型,编译器会选择使用默认模板参数;(个人:也就是默认模板参数和自行推导模板参数同时使用时,自动推导的优先级高于默认模板参数)
  • 如果模板参数即无法推导出来,又未设置其默认值,则编译器直接报错。例如:
template <typename T, typename U = double>
void func(T val1 = 0, U val2 = 0)
{
    //...
}
int main()
{
    func('c'); //T=char, U=double
    func();    //编译报错
    return 0;
}
    • 其中,func('c') 的这种调用方式,编译器通过实参 'c' 可以推导出 T=char,但由于未传递第 2 个实参,因此模板参数 U 使用的是默认参数 double;
    • 但 func() 的调用方式是不行的,虽然 val1 设置有默认值,但编译器无法通过该默认值推导出模板参数 T 的类型。由此不难看出,编译器的自动推导能力并没有想象的那么强大

总的来说,

  • C++11 支持为函数模板中的参数设置默认值,在实际使用过程中,我们可以选择使用默认值,
  • 也可以尝试由编译器自行推导得到,
  • 还可以亲自指定各个模板参数的类型。  

C++11在函数模板和类模板中使用可变参数

有些函数的参数数量是不确定的,声明函数的时候,可以使用省略号 ... 表示可变数量的参数。对于函数参数而言,C++ 一直都支持为函数设置可变参数,最典型的代表就是 printf() 函数,它的语法格式为: 
int printf ( const char * format, ... );

...就表示的是可变参数,即 printf() 函数可以接收任意个参数,且各个参数的类型可以不同,例如:  

printf("%d", 10);
printf("%d %c",10, 'A');
printf("%d %c %f",10, 'A', 1.23);

我们通常将容纳多个参数的可变参数称为参数包。借助 format 字符串,printf() 函数可以轻松判断出参数包中的参数个数和类型。 

头文件 stdarg.h 定义了一些宏,可以操作可变参数
  • va_list一个数据类型,用来定义一个可变参数对象。它必须在操作可变参数时,首先使用
  • va_start一个函数,用来初始化可变参数对象。它接受两个参数,
    • 第一个参数是可变参数对象,
    • 第二个参数是原始函数里面,可变参数之前的那个参数,用来为可变参数定位
  • va_arg一个函数,用来取出当前那个可变参数,每次调用后,内部指针就会指向下一个可变参数。它接受两个参数,
    • 第一个是可变参数对象,
    • 第二个是当前可变参数的类型
  •  va_end一个函数,用来清理可变参数对象

下面的程序中,自定义了一个简单的可变参数函数:
#include <iostream>
#include <cstdarg>
//可变参数的函数
void vair_fun(int count, ...)
{
    va_list args;
    va_start(args, count);
    for (int i = 0; i < count; ++i)
    {
        int arg = va_arg(args, int);
        std::cout << arg << " ";
    }
    va_end(args);
}

int main()
{
    //可变参数有 4 个,分别为 10、20、30、40
    vair_fun(4, 10, 20, 30, 40);
    return 0;
}

使用 ... 可变参数的过程中,需注意以下几点: 

  • ... 可变参数必须作为函数的最后一个参数,且一个函数最多只能拥有 1 个可变参数
  • 可变参数的前面至少要有 1 个有名参数(例如上面例子中的 count 参数); 

需要注意的是,...可变参数的方法仅适用于函数参数,并不适用于模板参数。C++11标准中,提供了一种实现可变模板参数的方法。(个人:也就是...这种方式使用可变参数的方式仅仅适用于函数参数,对于模板不能这么用,在C++11中提供了一种实现可变模板参数的新的语法)  

C++ 11 标准发布之前,函数模板类模板只能设定固定数量的模板参数。C++11标准对模板的功能进行了扩展,允许模板中包含任意数量的模板参数,这样的模板又称可变参数模板。  

可变参数函数模板:  

如下定义了一个可变参数的函数模板,  

template<typename... T>
void vair_fun(T... args) {
    //函数体
}
  • 模板参数中, typename(或者 class)后跟 ... 就表明 T 是一个可变模板参数,它可以接收多种数据类型,又称模板参数包
  • vair_fun() 函数中,args 参数的类型用T... 表示,表示 args 参数可以接收任意个参数,又称函数参数包

这也就意味着,此函数模板最终实例化出的 vair_fun() 函数可以指定任意类型、任意数量的参数例如,我们可以这样使用这个函数模板:  

vair_fun();
vair_fun(1, "abc");
vair_fun(1, "abc", 1.23);

使用可变参数函数模板的难点在于,如何在模板函数内部“解开”参数包(也就是如何使用包内的数据),这里给大家介绍两种简单的方法。  

递归方式解包

#include <iostream>
using namespace std;
//模板函数递归的出口
void vir_fun() {

}

template <typename T, typename... args>
void vir_fun(T argc, args... argv)
{
    cout << argc << endl;
    //开始递归,将第一个参数外的 argv 参数包重新传递给 vir_fun
    vir_fun(argv...);
}

int main()
{
    vir_fun(1, "http://www.baidu.com", 2.34);
    return 0;
}

分析这个程序的执行流程:

  • 首先,main() 函数调用 vir_fun() 模板函数时,根据所传实参的值,可以很轻易地判断出模板参数 T 的类型为 int,函数参数 argc 的值为 1,剩余的模板参数和函数参数都分别位于 args 和 argv 中
  • vir_fun() 函数中,首先输出了 argc 的值(为 1),然后重复调用自身,同时将函数参数包 argv 中的数据作为实参传递给形参 argc 和 argv;(个人:这里注意,函数参数包argv做为实参传递时,函数调用的形式传参时的方式为:argv...)
  • 再次执行 vir_fun() 函数,此时模板参数 T 的类型为 const char*,输出 argc 的值为 "http:www.baidu.com"。再次调用自身,继续将 argv 包中的数据作为实参
  • 再次执行 vir_fun() 函数,此时模板参数 T 的类型为 double,输出 argc 的值为 2.34。再次调用自身,将空的argv包作为实参
  • 由于argv包没有数据,此时会调用无任何形参、函数体为空的 vir_fun() 函数,最终执行结束。(个人:这里的无参的版本,是一个普通的函数,此时根据调用时的函数匹配,会在函数模板中调用一个普通函数)

以递归方式解包,一定要设置递归结束的出口。例如本例中,无形参、函数体为空的 vir_fun() 函数就是递归结束的出口。

非递归方法解包, 

借助逗号表达式和初始化列表,也可以解开参数包

#include <iostream>
using namespace std;

template <typename T>
void dispaly(T t) {
    cout << t << endl;
}

template <typename... args>
void vir_fun(args... argv)
{
    //逗号表达式+初始化列表
    int arr[] = { (dispaly(argv),0)... };
}

int main()
{
    vir_fun(1, "http://www.baidu.com", 2.34);
    return 0;
}

这里重点分析一下第 13 行代码,我们以{ }初始化列表的方式对数组 arr 进行了初始化, (display(argv),0)... 会依次展开为  (display(1),0)、(display("http://www.baidu.com"),0) 和 (display(2.34),0)。(个人:argv本来是一个函数可变参数,而函数display是一个单参数函数模板,这里的特殊用法argv这个可变函数参数体现在()右边的...,)

也就是说,第 13 行代码和如下代码是等价的:

int arr[] = { (dispaly(1),0), (dispaly("http://www.baidu.com"),0), (dispaly(2.34),0) };

可以看到,每个元素都是一个逗号表达式,以 (display(1), 0) 为例,它会先计算 display(1),然后将 0 作为整个表达式的值返回给数组,因此arr数组最终存储的都是0。arr 数组纯粹是为了将参数包展开,没有发挥其它作用。  

可变参数类模板 :

C++11标准中,类模板中的模板参数也可以是一个可变参数。C++ 11 标准提供的 typle 元组类就是一个典型的可变参数模板类,它的定义如下:

template <typename... Types>
class tuple;

和固定模板参数的类不同,typle 模板类实例化时,可以接收任意数量、任意类型的模板参数,例如:  

std:tuple<> tp0;
std::tuple<int> tp1 = std::make_tuple(1);
std::tuple<int, double> tp2 = std::make_tuple(1, 2.34);
std::tuple<int, double, string> tp3 = std::make_tuple(1, 2.34, "http://www.baidu.com");

如下代码展示了一个支持可变参数的类模板:

#include <iostream>
//声明模板类demo
template<typename... Values> class demo;
//继承式递归的出口,类模板特例化
template<> class demo<> {};


//以继承的方式解包
template<typename Head, typename... Tail>
class demo<Head, Tail...>//这里应该也是一种类模板的特例化吧,因为类名后出现了<>
        : private demo<Tail...>
{
public:
    demo(Head v, Tail... vtail) : m_head(v), demo<Tail...>(vtail...) {
        dis_head();
    }
    void dis_head() { std::cout << m_head << std::endl; }

protected:
    Head m_head;
};

int main() {
    demo<int, float, std::string> t(1, 2.34, "http://www.baidu.com");
    return 0;
}

 程序中,demo模板参数中的Tail就是一个参数包,解包的方式是以“递归+继承”的方式实现的。

可变参数模板类还有其它的解包方法,这里不再一一赘述,感兴趣的读者可以自行做深入的研究。  

C++11 tuple元组详解

C++11 标准新引入了一种类模板,命名为 tuple(中文可直译为元组)。tuple 最大的特点是:实例化的对象可以存储任意数量、任意类型的数据。  tuple 的应用场景很广泛,

  • 例如当需要存储多个不同类型的元素时,可以使用 tuple;
  • 当函数需要返回多个数据时,可以将这些数据存储在 tuple 中,函数只需返回一个 tuple 对象即可。  

tuple 本质是一个以可变模板参数定义的类模板,它定义在 <tuple> 头文件并位于 std 命名空间中。因此要想使用 tuple 类模板,程序中需要首先引入以下代码:  

#include <tuple>
using std::tuple;

实例化 tuple 模板类对象常用的方法有两种,

  • 一种是借助该类的构造函数,
  • 另一种是借助 make_tuple() 函数。

tuple 模板类提供有很多构造函数,包括:

1) 默认构造函数
constexpr tuple();
2) 拷贝构造函数
tuple (const tuple& tpl);
3) 移动构造函数
tuple (tuple&& tpl);
4) 隐式类型转换构造函数
template <class... UTypes>
    tuple (const tuple<UTypes...>& tpl); //左值方式
template <class... UTypes>
    tuple (tuple<UTypes...>&& tpl);      //右值方式
5) 支持初始化列表的构造函数
explicit tuple (const Types&... elems);  //左值方式
template <class... UTypes>
    explicit tuple (UTypes&&... elems);  //右值方式
6) 将pair对象转换为tuple对象
template <class U1, class U2>
    tuple (const pair<U1,U2>& pr);       //左值方式
template <class U1, class U2>
    tuple (pair<U1,U2>&& pr);            //右值方式

举个例子,

#include <iostream>     // std::cout
#include <tuple>        // std::tuple
using std::tuple;
 
int main()
{
    std::tuple<int, char> first;                             // 1)   first{}
    std::tuple<int, char> second(first);                     // 2)   second{}
    std::tuple<int, char> third(std::make_tuple(20, 'b'));   // 3)   third{20,'b'}
    std::tuple<long, char> fourth(third);                    // 4)的左值方式, fourth{20,'b'}
    std::tuple<int, char> fifth(10, 'a');                    // 5)的右值方式, fifth{10,'a'}
    std::tuple<int, char> sixth(std::make_pair(30, 'c'));    // 6)的右值方式, sixth{30,''c}
    return 0;
}

上面程序中,我们已经用到了 make_tuple() 函数,它以模板的形式定义在 <tuple> 头文件中,功能是创建一个 tuple 右值对象(或者临时对象)。  对于 make_tuple() 函数创建了 tuple 对象,我们可以上面程序中那样作为移动构造函数的参数,也可以这样用:  

auto first = std::make_tuple (10,'a');   // tuple < int, char >
const int a = 0; int b[3];
auto second = std::make_tuple (a,b);     // tuple < int, int* >

为了方便您在实际开发中使用 tuple 对象,tupe 模板类提供了一个功能实用的成员函数,<tuple> 头文件中也提供了一些和操作 tuple 对象相关的函数模板和类模板,如表 1 所示。  

表 1 tuple 对象常用函数
函数或类模板描 述
tup1.swap(tup2)
swap(tup1, tup2)
tup1 和 tup2 表示类型相同的两个 tuple 对象,tuple 模板类中定义有一个 swap() 成员函数,<tuple> 头文件还提供了一个同名的 swap() 全局函数

swap() 函数的功能是交换两个 tuple 对象存储的内容。
get<num>(tup) tup 表示某个 tuple 对象,num 是一个整数,get() 是 <tuple> 头文件提供的全局函数,功能是返回 tup 对象中第 num+1 个元素。
tuple_size<type>::value tuple_size 是定义在 <tuple> 头文件的类模板,它只有一个成员变量 value,功能是获取某个 tuple 对象中元素的个数,type 为该tuple 对象的类型。
tuple_element<I, type>::type tuple_element 是定义在 <tuple> 头文件的类模板,它只有一个成员变量 type,功能是获取某个 tuple 对象第 I+1 个元素的类型
forward_as_tuple<args...> args... 表示 tuple 对象存储的多个元素,该函数的功能是创建一个 tuple 对象,内部存储的 args... 元素都是右值引用形式的。
tie(args...) = tup tup 表示某个 tuple 对象,tie() 是 <tuple> 头文件提供的,功能是将 tup 内存储的元素逐一赋值给 args... 指定的左值变量
tuple_cat(args...) args... 表示多个 tuple 对象,该函数是 <tuple> 头文件提供的,功能是创建一个 tuple 对象,此对象包含 args... 指定的所有 tuple 对象内的元素

tuple 模板类对赋值运算符 = 进行了重载,使得同类型的 tuple 对象可以直接赋值。此外,tuple 模板类还重载了 ==、!=、<、>、>=、<= 这几个比较运算符,同类型的 tuple 对象可以相互比较(逐个比较各个元素)。  

#include <iostream>
#include <tuple>

int main()
{
    int size;
    //创建一个 tuple 对象存储 10 和 'x'
    std::tuple<int, char> mytuple(10, 'x');
    //计算 mytuple 存储元素的个数
    size = std::tuple_size<decltype(mytuple)>::value;
    //输出 mytuple 中存储的元素
    std::cout << std::get<0>(mytuple) << " " << std::get<1>(mytuple) << std::endl;
    //修改指定的元素
    std::get<0>(mytuple) = 100;
    std::cout << std::get<0>(mytuple) << std::endl;
    //使用 makde_tuple() 创建一个 tuple 对象
    auto bar = std::make_tuple("test", 3.1, 14);
    //拆解 bar 对象,分别赋值给 mystr、mydou、myint
    const char* mystr = nullptr;
    double mydou;
    int myint;
    //使用 tie() 时,如果不想接受某个元素的值,实参可以用 std::ignore 代替
    std::tie(mystr, mydou, myint) = bar;
    //std::tie(std::ignore, std::ignore, myint) = bar;  //只接收第 3 个整形值
    //将 mytuple 和 bar 中的元素整合到 1 个 tuple 对象中
    auto mycat = std::tuple_cat(mytuple, bar);
    size = std::tuple_size<decltype(mycat)>::value;
    std::cout << size << std::endl;
    return 0;
}

C++11列表初始化(统一了初始化方式)

对于普通数组POD类型(POD类型即plain old data类型,简单来说,是可以直接使用memcpy复制的对象。),C++98/03 可以使用初始化列表(initializer list)进行初始化

int i_arr[3] = { 1, 2, 3 };
long l_arr[] = { 1, 3, 2, 4 };
struct A
{
    int x;
    int y;
} a = { 1, 2 };

但是这种初始化方式的适用性非常狭窄,只有上面提到的这两种数据类型可以使用初始化列表。(个人:也就是在C++98/03中,我们主要对数组和结构体类型使用初始化列表初始化它们的元素)  

在C++11中,初始化列表的适用性被大大增加了。它现在可以用于任何类型对象的初始化,请看下面的代码。 

class Foo
{
public:
    Foo(int) {}
private:
    Foo(const Foo &);
};
 
int main(void)
{
    Foo a1(123);
    Foo a2 = 123;  //error: 'Foo::Foo(const Foo &)' is private
    Foo a3 = { 123 };
    Foo a4 { 123 };
    int a5 = { 3 };
    int a6 { 3 };
    return 0;
}
  • a3、a4使用了新的初始化方式来初始化对象,效果如同 a1 的直接初始化。这里需要注意的是,a3 虽然使用了等于号,但它仍然是列表初始化,因为,私有的拷贝构造并不会影响到它。(个人:a3,a4的这两种列表初始化方式等价,效果都等同于a1的直接初始化,都调用了以列表初始化表里的元素为参数的构造函数,列表初始化调用的构造函数需要几个参数,列表初始化时,就需要几个元素,如果不匹配,就要报错)
    #include <iostream>
    using namespace std;
    
    class A
    {
    public:
        A(int x,int y) :i(x),j(y){}
        void display() const {
            cout<<"the value of i,j is:"<<i<<","<<j<<endl;
        }
    private:
        int i;
        int j;
    };
    int main()
    {
        A a3 = { 123,456 };
        a3.display();
        return 0;
    }
    

  • a5、a6则是基本数据类型的列表初始化方式。可以看到,它们的形式都是统一的
  • a4和a6的写法,是C++98/03所不具备的。在C++11中,可以直接在变量名后面跟上初始化列表,来进行对象的初始化。这种变量名后面跟上初始化列表方法同样适用于普通数组和 POD 类型的初始化
    int i_arr[3] { 1, 2, 3 };  //普通数组
    struct A
    {
        int x;
        struct B
        {
            int i;
            int j;
        } b;
    } a { 1, { 2, 3 } };  //POD类型
    

    在初始化时,{}前面的等于号是否书写对初始化行为没有影响。(个人:也就是列表初始化主要有两种形式,一种是变量名后直接跟初始化列表,另一种是变量名后跟等号+初始化列表)

在C++98/03中,

现在在C++11中,

除了上面所述的内容之外,列表初始化还可以直接使用在函数的返回值上:  

struct Foo
{
    Foo(int, double) {}
};
Foo func(void)
{
    return { 123, 321.0 };
}

这里的return语句就如同返回了一个Foo(123, 321.0)。  

在C++11中使用初始化列表是非常便利的。

  • 它不仅统一了各种对象的初始化方式,
  • 而且还使代码的书写更加简单清晰。  

C++11 lambda匿名函数用法详解

lambda 源自希腊字母表中第11位的 λ在计算机科学领域,它则是被用来表示一种匿名函数所谓匿名函数,简单地理解就是没有名称的函数,又常被称为 lambda 函数或者 lambda 表达式。  

定义一个 lambda 匿名函数很简单,可以套用如下的语法格式:
[外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型
{
   函数体;
}
  • [外部变量访问方式说明符]: [ ] 方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略在方括号内部,可以注明当前lambda函数的函数体中可以使用哪些“外部变量”。  所谓外部变量,指的是和当前 lambda 表达式位于同一文件内的所有局部变量
表 1 [外部变量]的定义方式
外部变量格式功能
[] 空方括号表示当前 lambda 匿名函数中不导入任何外部变量。
[=] 只有一个=等号,表示以值传递的方式导入所有外部变量;
[&] 只有一个 & 符号,表示以引用传递的方式导入所有外部变量
[val1,val2,...] 表示以值传递的方式导入 val1、val2 等指定的外部变量,同时,多个变量之间没有先后次序
[&val1,&val2,...] 表示以引用传递的方式导入 val1、val2等指定的外部变量,多个变量之间没有前后次序
[val,&val2,...] 以上 2 种方式还可以混合使用,变量之间没有前后次序
[=,&val1,...] 表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。
[this] 表示以值传递的方式导入当前的 this 指针

注意,单个外部变量不允许以相同的传递方式导入多次。例如 [=,val1] 中,val1 先后被以值传递的方式导入了 2 次,这是非法的。  

  • (参数) :和普通函数的定义一样,lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略;  
  • mutable  [ˈmjuːtəbl],可变的 :此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。(个人:如果使用mutable,并且没有参数,但是省略了(),就会变成了[]mutable{},这样子可能变得不伦不类),默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字注意,对于以值传递方式引入的外部变量,lambda 表达式修改的是拷贝的那一份,并不会修改真正的外部变量
  • noexcept/throw()可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。(个人:也就是对于匿名函数,如果有位于函数体之前的修饰符,例如mutable,noexcept/throw(),则匿名函数的参数括号不能省略,可以类比位于函数体之前函数参数括号()之后的const函数修饰符),默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。  值得一提的是,如果 lambda 函数标有 noexcept 而函数体内抛出了异常,又或者使用 throw() 限定了异常类型而函数体内抛出了非指定类型的异常,这些异常无法使用 try-catch 捕获,会导致程序执行失败(本节后续会给出实例)。 
  • -> 返回值类型 :指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略-> 返回值类型
  • 函数体 :和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。需要注意的是,外部变量会受到以值传递还是以引用传递方式引入的影响,而全局变量则不会。换句话说,在lambda表达式内可以使用任意一个全局变量,必要时还可以直接修改它们的值

如下就定义了一个最简单的 lambda 匿名函数:

[]{}

显然,此 lambda 匿名函数未引入任何外部变量([] 内为空),也没有传递任何参数,没有指定 mutable、noexcept 等关键字,没有返回值和函数体。所以,这是一个没有任何功能的 lambda 匿名函数。

【例 1】lambda 匿名函数的定义和使用:  

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

//自定义的降序排序规则
//函数有两个参数,x,y,先x后y,
//函数返回值为true,说明按照默认的x,y这样的顺序排序
//函数返回值为false,说明按照y,x这样的顺序排序
//我们可以根据实际的x,y大小关系和按照这个函数
//的返回值确定的排序方式得出到底是按照升序排序
//还是按照降序排序
bool sort_down(int x,int y){
    return  x > y;
}
int main()
{
    int num[4] = {4, 2, 3, 1};
    //对 a 数组中的元素进行排序
    sort(num, num+4, [=](int x, int y) -> bool{ return x < y; } );
    cout<<"lambda 匿名函数升序排序:"<<endl;
    for(int n : num){
        cout << n << " ";
    }
    cout<<"==========================================="<<endl;

    cout<<"自定义普通函数降序排序:"<<endl;
    sort(num, num+4, sort_down);
    for(int n : num){
        cout << n << " ";
    }
    cout<<"==========================================="<<endl;

    cout<<"为匿名函数指定一个名字:"<<endl;
    //display 即为 lambda 匿名函数的函数名
    auto display = [](int a,int b) -> void {cout << a << " " << b;};
    //调用 lambda 函数
    display(10,20);
    return 0;
}

 【例 2】值传递和引用传递的区别: 

#include <iostream>
using namespace std;
//全局变量
int all_num = 0;
int main()
{
    //局部变量
    int num_1 = 1;
    int num_2 = 2;
    int num_3 = 3;
    cout << "lambda1:\n";
    auto lambda1 = [=]{
        //全局变量可以访问甚至修改
        all_num = 10;
        //函数体内只能使用外部变量,而无法对它们进行修改
        cout << num_1 << " "
             << num_2 << " "
             << num_3 << endl;
    };
    lambda1();
    cout << all_num <<endl;

    cout << "lambda2:\n";
    auto lambda2 = [&]{
        all_num = 100;
        num_1 = 10;
        num_2 = 20;
        num_3 = 30;
        cout << num_1 << " "
             << num_2 << " "
             << num_3 << endl;
    };
    lambda2();
    cout << all_num << endl;


    cout << "lambda3:\n";
    cout<<"当前局部变量的值为:\n";
    cout << "num_1: "<<num_1 << " "
         << "num_2:"<<num_2 << " "
         << "num_3:"<<num_3 << endl;
    auto lambda3 = [=]() mutable{
        num_1 = 100;
        num_2 = 200;
        num_3 = 300;
        cout<<"函数体内局部变量的值为:\n";
        cout << "num_1: "<<num_1 << " "
             << "num_2:"<<num_2 << " "
             << "num_3:"<<num_3 << endl;
        cout<<"函数调用结束:\n";
    };
    lambda3();
    cout<<"当前局部变量的值为:\n";
    cout << "num_1: "<<num_1 << " "
         << "num_2:"<<num_2 << " "
         << "num_3:"<<num_3 << endl;
    return 0;
}

#include <iostream>

using namespace std;
int num_0 = 2;

int main() {
    int num_1 = 3;

    {
        int num_2 = 6;
        auto display = [=]() mutable -> void {
            cout << "hello world!\n";
            num_2 = 66;
            num_1 = 33;
            num_0 = 22;
        };
        display();
        cout << "num_0:" << num_0 << "\n"
             << "num_1:" << num_1 << "\n"
             << "num_2:" << num_2 << endl;

    }
    return 0;
}

 【例 3】执行抛出异常类型

#include <iostream>
using namespace std;
int main()
{
    auto except = []()throw(int) {
        throw 10;
    };
    try {
        except();
    }
    catch (int) {
        cout << "捕获到了整形异常";
    }
    return 0;
}

 在此基础上,再看一下反例:

#include <iostream>
using namespace std;
int main()
{
    auto except1 = []()noexcept{
        throw 100;
    };
    try{
        except1();
    }catch(int){
        cout << "捕获到了整形异常"<< endl;
    }
    cout<<"hello world!"<<endl;
    return 0;
}

#include <iostream>
using namespace std;
int main()
{
    auto except2 = []() throw(char) {
        throw 10;
    };
    try{
        except2();
    }catch(int){
        cout << "捕获到了整形异常"<< endl;
    }
    cout<<"hello world!"<<endl;
    return 0;
}

此程序运行会直接崩溃,原因很简单,

  • except1 匿名函数指定了函数体中不发生任何异常,但函数体中却发生了整形异常;
  • except2 匿名函数指定函数体可能会发生字符异常,但函数体中却发生了整形异常。

由于指定异常类型和真正发生的异常类型不匹配,导致 try-catch 无法捕获,最终程序运行崩溃。  如果不使用 noexcept 或者 throw(),则 lambda 匿名函数的函数体中允许发生任何类型的异常。  

C++11非受限联合体(union)

在 C/C++ 中,联合体(Union)是一种构造数据类型。在一个联合体内,我们可以定义多个不同类型的成员,这些成员将会共享同一块内存空间。老版本的 C++ 为了和C语言保持兼容,对联合体的数据成员的类型进行了很大程度的限制,这些限制在今天看来并没有必要,因此 C++11 取消了这些限制。C++11 标准规定,任何非引用类型都可以成为联合体的数据成员,这种联合体也被称为非受限联合体

C++11 允许非 POD 类型

C++98 不允许联合体的成员是非 POD 类型,但是 C++1 1 取消了这种限制。

POD 是 C++ 中一个比较重要的概念,POD 是英文 Plain Old Data 的缩写,用来描述一个类型的属性。POD 类型一般具有以下几种特征(包括 class、union 和 struct等):

  • 没有用户自定义的构造函数、析构函数、拷贝构造函数和移动构造函数。
  • 不能包含虚函数和虚基类。
  • 非静态成员必须声明为 public
  • 类中的第一个非静态成员的类型与其基类不同,例如:
class B1{};
class B2 : B1 { B1 b; };

class B2 的第一个非静态成员 b 是基类类型,所以它不是 POD 类型。

在类或者结构体继承时,满足以下两种情况之一:

    • 派生类中有非静态成员,且只有一个仅包含静态成员的基类;
    • 基类有非静态成员,而派生类没有非静态成员。
class B1 { static int n; };
class B2 : B1 { int n1; };
class B3 : B2 { static int n2; };

对于 B2,派生类 B2 中有非静态成员,且只有一个仅包含静态成员的基类 B1,所以它是 POD 类型。对于 B3,基类 B2 有非静态成员,而派生类 B3 没有非静态成员,所以它也是 POD 类型。

  • 所有非静态数据成员均和其基类也符合上述规则(递归定义),也就是说 POD 类型不能包含非 POD 类型的数据。  
  • 此外,所有兼容C语言的数据类型都是 POD 类型(struct、union 等不能违背上述规则)  

C++11 允许联合体有静态成员

C++11 删除了联合体不允许拥有静态成员的限制。例如:

union U {
    static int func() {
        int n = 3;
        return n;
    }
};

需要注意的是,静态成员变量只能在联合体内定义,却不能在联合体外使用,这使得该规则很没用

C++11 规定,如果非受限联合体内有一个非 POD 的成员,而该成员拥有自定义的构造函数,那么这个非受限联合体的默认构造函数将被编译器删除;其他的特殊成员函数,例如默认拷贝构造函数、拷贝赋值操作符以及析构函数等,也将被删除。这条规则可能导致对象构造失败,请看下面的例子:

#include <string>
using namespace std;
union U {
    string s;
    int n;
};
int main() {
    U u;   // 构造失败,因为 U 的构造函数被删除
    return 0;
}

在上面的例子中,因为 string 类拥有自定义的构造函数,所以 U 的构造函数被删除;定义 U 的类型变量 u 需要调用默认构造函数,所以 u 也就无法定义成功。  

解决上面问题的一般需要用到 placement new,代码如下:  

#include <string>
using namespace std;
union U {
    string s;
    int n;
public:
    U() { new(&s) string; }
    ~U() { s.~string(); }
};
int main() {
    U u;
    return 0;
}

构造时,采用 placement new 将 s 构造在其地址 &s 上,这里 placement new 的唯一作用只是调用了一下 string 类的构造函数。注意,在析构时还需要调用 string 类的析构函数。

placement new 是 new 关键字的一种进阶用法,既可以在栈(stack)上生成对象,也可以在堆(heap)上生成对象。相对应地,我们把常见的 new 的用法称为 operator new,它只能在 heap 上生成对象。  placement new 的语法格式如下:

new(address) ClassConstruct(...)
  • address 表示已有内存的地址,该内存可以在栈上,也可以在堆上;
  • ClassConstruct(...) 表示调用类的构造函数,如果构造函数没有参数,也可以省略括号。  placement new 利用已经申请好的内存来生成对象,它不再为对象分配新的内存,而是将对象数据放在 address 指定的内存中。在本例中,placement new 使用的是 s 的内存空间。  

匿名联合体是指不具名的联合体(也即没有名字的联合体),一般定义如下:

union U{
    union { int x;
};  //此联合体为匿名联合体};

可以看到,联合体 U 内定义了一个不具名的联合体,该联合体包含一个 int 类型的成员变量,我们称这个联合体为匿名联合体。  同样的,非受限联合体也可以匿名,而当非受限的匿名联合体运用于类的声明时,这样的类被称为“枚举式类”。示例如下:

#include <cstring>
#include <iostream>
using namespace std;
class Student{
public:
    Student(bool g, int a): gender(g), age(a){}
    bool gender;
    int age;
};
class Singer {
public:
              //student,native,foreigenr
    enum Type { STUDENT, NATIVE, FOREIGENR };
    Singer(bool g, int a) : s(g, a) { t = STUDENT; }
    Singer(int i) : id(i) { t = NATIVE; }
    Singer(const char* n, int s) {
        int size = (s > 9) ? 9 : s;
        memcpy(name , n, size);
        name[s] = '\0';
        t = FOREIGENR;
    }
    ~Singer(){};

    void display(){
        cout<<t<<endl;
    }
private:
    Type t;
    union {
        Student s;
        int id;
        char name[10];
    };
};
int main() {
    auto x = Singer(true, 13);
    x.display();
    auto y = Singer(310217);
    y.display();
    auto z = Singer("J Michael", 9);
    z.display();
    return 0;
}

 

上面的代码中使用了一个匿名非受限联合体,它作为类 Singer 的“变长成员”来使用,这样的变长成员给类的编写带来了更大的灵活性,这是 C++98 标准中无法达到的(编译器会报member 'Student Singer::<anonymous union>::s' with constructor not allowed in union错误)。  

C++11 for循环

新语法格式的for循环还支持遍历用{ }大括号初始化的列表,比如:

#include <iostream>
using namespace std;
int main() {
    for (int num : {1, 2, 3, 4, 5}) {
        cout << num << " ";
    }
    return 0;
}

在使用新语法格式的 for 循环遍历某个序列时,如果需要遍历的同时修改序列中元素的值,实现方案是在 declaration 参数处定义引用形式的变量。 

#include <iostream>
#include <vector>
using namespace std;
int main() {
    char arc[] = "abcde";
    vector<char>myvector(arc, arc + 5);
    //for循环遍历并修改容器中各个字符的值
    for (auto &ch : myvector) {
        ch++;
    }
    //for循环遍历输出容器中各个字符
    for (auto ch : myvector) {
        cout << ch;
    }
    return 0;
}

 另外,基于范围的for循环完成对容器的遍历,其底层也是借助容器的迭代器实现的。举个例子:

#include <iostream>
#include <vector>
int main(void)
{
    std::vector<int>arr = { 1, 2, 3, 4, 5 };
    for (auto val : arr)
    {
        std::cout << val << std::endl;
        arr.push_back(10); //向容器中添加元素
    }
    return 0;
}

可以看到,程序的执行结果并不是我们想要的。就是因为在 for 循环遍历 arr 容器的同时向该容器尾部添加了新的元素(对 arr 容器进行了扩增),致使遍历容器所使用的迭代器失效,整个遍历过程出现错误。因此,在使用基于范围的for循环遍历容器时,应避免在循环体中修改容器存储元素的个数

C++11 constexpr和常量表达式

#include <iostream>
using namespace std;

class A{
public:
    A(int x_x,int y_y):x(x_x),y(y_y){}
private:
    int x;
    int y;
};

int main()
{
    constexpr A *x = nullptr;
    cout<<x<<endl;
}

C++11 constexpr函数

前面提到,constexpr 可用于修饰函数,而类中的成员方法完全可以看做是“位于类这个命名空间中的函数”,所以constexpr也可以修饰类中的成员函数,只不过此函数必须满足前面提到的条件。 

 注意,C++11 标准中,不支持用 constexpr 修饰带有 virtual 的成员方法

C++11 聚合类(Aggregate Classes) 

 C++11 字面值常量类(Literal Classes)

#include <iostream>
using namespace std;

class Debug {
public:
    constexpr Debug(bool b = true): hw(b), io(b), other(b) {
    }
    constexpr Debug(bool h, bool i, bool o):
            hw(h), io(i), other(o) {
    }
    constexpr bool any() { return hw || io || other; }
    void set_io(bool b) { io = b; }
    void set_hw(bool b) { hw = b; }
    void set_other(bool b) { hw = b; }
private:
    bool hw; // hardware errors other than IO errors
    bool io; // IO errors
    bool other; // other errors
};

我们再定义一个非constexpr构造函数来看一下,

#include <iostream>
using namespace std;

class Debug {
public:
    constexpr Debug(bool b = true): hw(b), io(b), other(b) {
    }
    constexpr Debug(bool h, bool i, bool o):
            hw(h), io(i), other(o) {
    }

    Debug(bool x, bool y):
            hw(x), io(y), other(y) {
    }

    constexpr bool any() { return hw || io || other; }
    void set_io(bool b) { io = b; }
    void set_hw(bool b) { hw = b; }
    void set_other(bool b) { hw = b; }
private:
    bool hw; // hardware errors other than IO errors
    bool io; // IO errors
    bool other; // other errors
};
int main()
{
    constexpr Debug a(true,false);
    return 0;
}

C++11 constexpr修饰函数模板

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

#include <iostream>
using namespace std;

class myType {
public:
    string name;
    int age;
};

template<typename T>
constexpr T dispaly(T t){
    return t;
}
int main()
{
    myType stu{"zhangsan",10};
    //普通函数
    myType ret = dispaly(stu);
    cout << ret.name << " " << ret.age << endl;
    //常量表达式函数
    constexpr int ret1 = dispaly(10);
    cout << ret1 << endl;
    return 0;
}

C++11 long long超长整型

C++ 11 标准中,基于整数大小的考虑,共提供了如表 1 所示的这些数据类型。与此同时,标准中还明确限定了各个数据类型最少占用的位数。(个人:这是C++11标准规定的最小位数,但是各个编译器实际实现时,往往会进行扩展) 

表 1 C++11标准中所有的整形数据类型
整数类型等价类型C++11标准规定占用最少位数
short short int(有符号短整型) 至少 16 位(2 个字节)
signed short
signed short int
unsigned short unsigned short int(无符号短整型)
unsigned short int
int int(有符号整形) 至少 16 位(2 个字节)
signed
signed int
unsigned unsigned int(无符号整形)
unsigned int
long long int(有符号长整形) 至少 32 位(4 个字节)
long int
signed long
signed long int
unsigned long unsigned long int(无符号长整形)
unsigned long int
long long(C++11) long long int(有符号超长整形) 至少 64 位(8 个字节)
long long int(C++11)
signed long long(C++11)
signed long long int(C++11)
unsigned long long(C++11) unsigned long long int(无符号超长整型)
unsigned long long int(C++11)

C++11 标准规定,

  • 每种整数类型必须同时具备有符号(signed)和无符号(unsigned)两种类型,且每种具体的有符号整型和无符号整型所占用的存储空间(也就是位数)必须相同
  • 注意,C++11标准中只限定了每种类型最少占用多少存储空间,不同的平台可以占用不同的存储空间

如同long类型整数需明确标注 "L" 或者 "l" 后缀一样,(个人:整数默认为int类型),要使用 long long 类型的整数,也必须标注对应的后缀:(个人:十进制,二进制,八进制,十六进制,需要的是前缀,对于类型的话,需要的是后缀) 

  • 对于有符号 long long 整形,后缀用 "LL" 或者 "ll" 标识。例如,"10LL" 就表示有符号超长整数 10;
  • 对于无符号 long long 整形,后缀用 "ULL"、"ull"、"Ull" 或者 "uLL" 标识。例如,"10ULL" 就表示无符号超长整数 10;

如果不添加任何标识,则所有的整数都会默认为 int 类型

对于任意一种数据类型,读者可能更关心的是此类型的取值范围。对于 long long 类型来说,如果想了解当前平台上 long long 整形的取值范围,可以使用<climits>头文件中与 long long整形相关的3个宏,分别为LLONG_MIN、LLONG_MAX和 ULLONG_MAX: 

  • LLONG_MIN:代表当前平台上最小的 long long 类型整数;
  • LLONG_MAX:代表当前平台上最大的 long long 类型整数;
  • ULLONG_MAX:代表当前平台上最大的 unsigned long long 类型整数(无符号超长整型的最小值为 0);
#include <iostream>
#include <iomanip>
#include <climits>
using namespace std;
int main()
{
    cout << "long long最小值:" << LLONG_MIN << " " << hex << LLONG_MIN << "\n";
    cout << dec << "long long最大值:" << LLONG_MAX << " " << hex << LLONG_MAX << "\n";
    cout << dec << "unsigned long long最大值:" << ULLONG_MAX << " " << hex << ULLONG_MAX;
    return 0;
}

C++11右值引用

很多初学者都感觉右值引用晦涩难懂,其实不然。右值引用只不过是一种新的C++语法,真正理解起来有难度的是基于右值引用引申出的 2 种 C++ 编程技巧,分别为移动语义完美转发

C++左值和右值

在 C++ 或者 C 语言中,一个表达式(可以是字面量变量对象函数的返回值等)根据其使用场景不同,分为左值表达式右值表达式。确切的说 C++ 中左值和右值的概念是从 C 语言继承过来的。值得一提的是,左值的英文简写为“lvalue”,右值的英文简写为“rvalue”。很多人认为它们分别是"left value"、"right value" 的缩写,其实不然。

  • lvalue是“locator value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,(个人:locator,定位器,定位系统)
  • 而rvalue 译为 "read value",指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)

通常情况下,判断某个表达式是左值还是右值,最常用的有以下 2 种方法。

  • 可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值
int a = 5;
5 = a; //错误,5 不能为左值

其中,变量 a 就是一个左值,而字面量5 就是一个右值。值得一提的是,C++ 中的左值也可以当做右值使用,例如:  

int b = 10; // b 是一个左值
a = b; // a、b 都是左值,只不过将 b 可以当做右值使用
  • 有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。以上面定义的变量 a、b 为例,a 和 b 是变量名,且通过 &a 和 &b 可以获得他们的存储地址,因此 a 和 b 都是左值;反之,字面量5、10,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此 5、10 都是右值

注意,以上 2 种判定方法只适用于大部分场景。  

C++右值引用

  • 其实C++98/03 标准中就有引用,使用"&"表示。但此种引用方式有一个缺陷,即正常情况下只能操作C++中的左值,无法对右值添加引用。举个例子:
int num = 10;
int &b = num; //正确
int &c = 10; //错误

如上所示,编译器允许我们为 num 左值建立一个引用,但不可以为 10 这个右值建立引用。因此,C++98/03 标准中的引用又称为左值引用。  

  • 注意,虽然C++98/03标准不支持为右值建立非常量左值引用,但允许使用常量左值引用操作右值。也就是说,常量左值引用既可以操作左值,也可以操作右值,例如:
int num = 10;
const int &b = num;
const int &c = 10;
  • 我们知道,右值往往是没有名称的,因此要使用它只能借助引用的方式。这就产生一个问题,实际开发中我们可能需要对右值进行修改(实现移动语义时就需要),显然左值引用的方式是行不通的。(个人:对于一个右值,原来C++98/03只能使用一个常量左值引用引用它,只能使用它的值,并且这个常量引用的对象只是编译器对于这个右值的一个拷贝,所以更遑论我们通过这个常量引用去改变原来右值的值),为此,C++11标准新引入了另一种引用方式,称为右值引用,用"&&"表示

需要注意的,和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:

int num = 10;
//int && a = num;  //右值引用不能初始化为左值
int && a = 10;

和常量左值引用不同的是,右值引用还可以对右值进行修改。例如:

int && a = 10;
a = 100;
cout << a << endl;//输出100

另外值得一提的是,C++语法上是支持定义常量右值引用的,例如:

const int&& a = 10;//编译器不会报错

但这种定义出来的右值引用并无实际用处

  • 一方面,右值引用主要用于移动语义和完美转发,其中前者需要有修改右值的权限;
  • 其次,常量右值引用的作用就是引用一个不可修改的右值,这项工作完全可以交给常量左值引用完成。  

其实,C++11 标准中对右值做了更细致的划分,分别称为纯右值(Pure value,简称 pvalue)将亡值(eXpiring value,简称 xvalue )。其中纯右值就是C++98/03标准中的右值,而将亡值则指的是和右值引用相关的表达式(比如某函数返回的 T && 类型的表达式)。对于纯右值和将亡值,都属于右值,读者知道即可,不必深究。  

C++11移动构造函数的功能和用法

C++11移动语义
在 C++ 11 标准之前(C++ 98/03 标准中),如果想用其它对象初始化一个同类的新对象,只能借助类中的复制(拷贝)构造函数。通过《C++拷贝构造函数》一节的学习我们知道,拷贝构造函数的实现原理很简单,就是为新对象复制一份和其它对象一模一样的数据(需要注意的是,当类中拥有指针类型的成员变量时,拷贝构造函数中需要以深拷贝(而非浅拷贝)的方式复制该指针成员.) 
#include <iostream>

using namespace std;

class demo {
public:
    //默认构造函数
    demo() : num(new int(0)) {
        cout << "construct!" << endl;
    }

    //拷贝构造函数
    demo(const demo &d) : num(new int(*d.num)) {
        cout << "copy construct!" << endl;
    }

    ~demo() {
        cout << "class destruct!" << endl;
    }

private:
    int *num;
};

demo get_demo() {
    return demo();
}

int main() {
    demo a = get_demo();
    return 0;
}

可以看到,程序中定义了一个可返回 demo 对象的 get_demo() 函数,用于在 main() 主函数中初始化 a 对象,其整个初始化的流程包含以下几个阶段: 

  • 执行 get_demo() 函数内部的 demo() 语句,即调用 demo 类的默认构造函数生成一个匿名对象
  • 执行return demo() 语句,会调用拷贝构造函数复制一份之前生成的匿名对象,并将其作为 get_demo() 函数的返回值(函数体执行完毕之前,匿名对象会被析构销毁)
  • 执行 a = get_demo() 语句再调用一次拷贝构造函数,将之前拷贝得到的临时对象复制给 a此行代码执行完毕,get_demo() 函数返回的对象会被析构
  • 程序执行结束前,会自行调用demo类的析构函数销毁 a。 

注意,目前多数编译器都会对程序中发生的拷贝操作进行优化,因此如果我们使用VS、clion+mingw64等这些编译器运行此程序时,看到的往往是优化后的输出结果:  

而同样的程序,如果使用g++ demo.cpp -fno-elide-constructors命令编译(其中 demo.cpp 是程序文件的名称),就可以看到完整的输出结果: 

construct!                <-- 执行 demo()
copy construct!       <-- 执行 return demo()
class destruct!         <-- 销毁 demo() 产生的匿名对象
copy construct!       <-- 执行 a = get_demo()
class destruct!         <-- 销毁 get_demo() 返回的临时对象
class destruct!         <-- 销毁 a

如上所示,利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次拷贝(而且是深拷贝)操作。当然,对于仅申请少量堆空间的临时对象来说,深拷贝的执行效率依旧可以接受,但如果临时对象中的指针成员申请了大量的堆空间,那么 2 次深拷贝操作势必会影响 a 对象初始化的执行效率。事实上,此问题一直存留在以 C++ 98/03 标准编写的 C++ 程序中。由于临时变量的产生、销毁以及发生的拷贝操作本身就是很隐晦的(编译器对这些过程做了专门的优化),且并不会影响程序的正确性,因此很少进入程序员的视野

那么当类中包含指针类型的成员变量,使用其它对象来初始化同类对象时,怎样才能避免深拷贝导致的效率问题C++11 标准引入了解决方案,该标准中引入了右值引用的语法,借助它可以实现移动语义。  

C++移动构造函数(移动语义的具体实现):  

所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。 
以前面程序中的demo类为例,该类的成员都包含一个整形的指针成员,其默认指向的是容纳一个整形变量的堆空间。使用 get_demo() 函数返回的临时对象初始化a时
  • 我们只需要将临时对象的 num 指针直接浅拷贝给 a.num,
  • 然后修改该临时对象中 num 指针的指向(通常令其指向 NULL)

这样就完成了 a.num 的初始化。 

事实上,对于程序执行过程中产生的临时对象,往往只用于传递数据(没有其它的用处),并且会很快会被销毁。因此,在使用临时对象初始化新对象时,我们可以将其包含的指针成员指向的内存资源直接移给新对象所有,无需再新拷贝一份,这大大提高了初始化的执行效率。(个人:可能临时对象,是将亡值,在C++11也是右值的一种)
#include <iostream>
using namespace std;
class demo{
public:
    demo():num(new int(0)){
        cout<<"construct!"<<endl;
    }

    demo(const demo &d):num(new int(*d.num)){
        cout<<"copy construct!"<<endl;
    }
    //添加移动构造函数
    demo(demo &&d):num(d.num){
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }
    ~demo(){
        cout<<"class destruct!"<<endl;
    }
private:
    int *num;
};
demo get_demo(){
    return demo();
}
int main(){
    demo a = get_demo();
    return 0;
}

可以看到,在之前demo类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“对同一块空间被释放多次”情况的发生。  

使用g++ demo.cpp -o demo.exe -std=c++11 -fno-elide-constructors命令执行此程序,输出结果为: 

通过执行结果我们不难得知,当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。  

我们知道,程序执行过程中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值

  • 当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作
  • 只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。(个人:一般的拷贝构造函数是常量左值引用,是可以引用这样的临时对象的) 

在实际开发中,通常在类中自定义移动构造函数的同时,会再为其自定义一个适当的拷贝构造函数,由此当用户利用右值初始化类对象时,会调用移动构造函数;使用左值(非右值)初始化类对象时,会调用拷贝构造函数

默认情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想调用移动构造函数,则必须使用右值进行初始化

C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数

C++11 move()函数:将左值强制转换为右值

C++11 标准中借助右值引用可以为指定类添加移动构造函数,这样当使用该类的右值对象(可以理解为临时对象)初始化同类对象时,编译器会优先选择移动构造函数

注意,移动构造函数的调用时机是:用同类的右值对象初始化新对象。那么,用当前类的左值对象(有名称,能获取其存储地址的实例对象)初始化同类对象时,是否就无法调用移动构造函数了呢?当然不是,C++11 标准中已经给出了解决方案,即调用 move() 函数。

move本意为"移动",但该函数并不能移动任何数据,它的功能很简单,就是将某个左值强制转化为右值。基于 move() 函数特殊的功能,其常用于实现移动语义。move() 函数的用法也很简单,其语法格式如下:

move( arg )

其中,arg 表示指定的左值对象。该函数会返回 arg 对象的右值形式。  

#include <iostream>
using namespace std;

class movedemo{
public:
    movedemo():num(new int(9)){
        cout<<"construct!"<<endl;
    }
    //拷贝构造函数
    movedemo(const movedemo &d):num(new int(*d.num)){
        cout<<"copy construct!"<<endl;
    }
    //移动构造函数
    movedemo(movedemo &&d):num(d.num){
        d.num = NULL;
        cout<<"move construct!"<<endl;
    }
public:
    int *num;
};

int main(){
    movedemo demo;
    cout << "demo2:\n";
    movedemo demo2 = demo;
    cout<<"now,the demo *num is:"<<*(demo.num)<<endl;

    cout << "demo3:\n";
    movedemo demo3 = std::move(demo);
    if(demo.num==NULL)
    {
        cout<<"now,the demo num equals NULL"<<endl;
        demo.num = new int(90);
    }
    else
        cout<<"now,the demo *num is:"<<*(demo.num)<<endl;
    cout << "demo4:\n";
    movedemo demo4 = demo;
    return 0;
}

#include <iostream>

using namespace std;

class first {
public:
    first() : num(new int(0)) {
        cout << "construct!" << endl;
    }

    //移动构造函数
    first(first &&d) : num(d.num) {
        d.num = NULL;
        cout << "first move construct!" << endl;
    }

public:
    int *num;
};

class second {
public:
    second() : fir() {}

    //用 first 类的移动构造函数初始化 fir
    second(second &&sec) : fir(move(sec.fir)) {
        cout << "second move construct" << endl;
    }

public:
    first fir;
};

int main() {
    second oth;
    second oth2 = move(oth);
    return 0;
}

 

C++11引用限定符的用法

左值和右值的区分也同样适用于类对象,本节中将左值的类对象称为左值对象,将右值的类对象称为右值对象

默认情况下,对于类中用 public 修饰的成员函数,既可以被左值对象调用,也可以被右值对象调用。举个例子: 

#include <iostream>
using namespace std;

class demo {
public:
    demo(int num) :num(num) {}
    int get_num() {
        return this->num;
    }
private:
    int num;
};
int main() {
    demo a(10);
    cout << a.get_num() << endl;
    cout << move(a).get_num() << endl;

    return 0;
}

某些场景中,我们可能需要限制调用成员函数的对象的类型(左值还是右值),为此 C++11 新添加了引用限定符所谓引用限定符,就是在成员函数的后面添加 "&" 或者 "&&",从而限制调用者的类型(左值还是右值)。 

#include <iostream>
using namespace std;

class demo {
public:
    demo(int num) :num(num) {}
    int get_num()& {
        return this->num;
    }
private:
    int num;
};
int main() {
    demo a(10);
    cout << a.get_num() << endl;          // 正确
    //cout << move(a).get_num() << endl;  // 错误
    return 0;
}

我们再次修改程序:

#include <iostream>
using namespace std;
 
class demo {
public:
    demo(int num):num(num){}
    int get_num()&&{
        return this->num;
    }
private:
    int num;
};
int main() {
    demo a(10);
    //cout << a.get_num() << endl;      // 错误
    cout << move(a).get_num() << endl;  // 正确
    return 0;
}

注意引用限定符不适用于静态成员函数和友元函数。  

const 和引用限定符修饰类的成员函数时,都位于函数的末尾。C++11标准规定,当引用限定符和const修饰同一个类的成员函数时,const必须位于引用限定符前面。  需要注意的一点是,

  • 当 const && 修饰类的成员函数时,调用它的对象只能是右值对象
  • 当const &修饰类的成员函数时,调用它的对象既可以是左值对象,也可以是右值对象
  • 无论是 const && 还是 const & 限定的成员函数,内部都不允许对当前对象做修改操作。  
#include <iostream>
using namespace std;
 
class demo {
public:
    demo(int num,int num2) :num(num),num2(num2) {}
    //左值和右值对象都可以调用
    int get_num() const &{
        return this->num;
    }
    //仅供右值对象调用
    int get_num2() const && {
        return this->num2;
    }
private:
    int num;
    int num2;
};
 
int main() {
    demo a(10,20);
    cout << a.get_num() << endl;        // 正确
    cout << move(a).get_num() << endl;  // 正确
   
    //cout << a.get_num2() << endl;     // 错误 
    cout << move(a).get_num2() << endl; // 正确
    return 0;
}

C++11完美转发及其实现 

完美转发: 

完美转发,它指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,

  • 即不仅能准确地转发参数的值,(个人:既然是转发,就得是引用形式)
  • 还能保证被转发参数的左、右值属性不变。(个人:不但要求转发,还要求得是完美转发)

 举个例子:

template<typename T>
void function(T t) {
    otherdef(t);
}

如上所示,function() 函数模板中调用了 otherdef() 函数。在此基础上,完美转发指的是:

  • 如果 function() 函数接收到的参数 t 为左值,那么该函数传递给 otherdef() 的参数 t 也是左值;
  • 反之如果 function() 函数接收到的参数 t 为右值,那么传递给 otherdef() 函数的参数 t 也必须为右值。

显然,function() 函数模板并没有实现完美转发

  • 一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;(个人:也就是这里是值传递,参数需要额外的一次拷贝操作,不是转发,如果是引用,则不需要)
  • 另一方面,无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值

总之,无论从那个角度看,function() 函数的定义都不“完美”。

  • 完美转发这样严苛的参数传递机制,在C++98/03 标准中几乎不会用到,
  • C++11 标准为 C++ 引入了右值引用和移动语义因此,很多场景中是否实现完美转发,直接决定了该参数的传递过程使用的是拷贝语义(调用拷贝构造函数)还是移动语义(调用移动构造函数)

事实上,C++98/03 标准下的 C++ 也可以实现完美转发,只是实现方式比较笨拙

  • 通过前面的学习我们知道C++ 98/03 标准中只有左值引用,并且可以细分为非 const 引用和 const 引用
    • 其中,使用非const 引用作为函数模板参数时,只能接收左值,无法接收右值
    • 而const左值引用既可以接收左值,也可以接收右值,但考虑到其const属性,除非被调用函数otherdef的参数也是const属性,否则将无法直接传递。 
  • 这也就意味着,单独使用任何一种引用形式,可以实现转发,但无法保证完美使用 C++ 98/03标准下的C++语言,我们可以采用函数模板重载的方式实现完美转发,例如:
#include <iostream>
using namespace std;

void otherdef(int& t) {
    cout << "lvalue\n";
}
void otherdef(const int& t) {
    cout << "rvalue\n";
}

template <typename T>
void function(const T& t) {
    otherdef(t);
}
template <typename T>
void function(T& t) {
    otherdef(t);
}

int main()
{
    function(5);//5 是右值
    int  x = 1;
    function(x);//x 是左值
    return 0;
}

从输出结果中可以看到,

  • 对于右值 5 来说,它实际调用的参数类型为 const T& 的函数模板,由于 t 为 const 类型,所以 otherdef() 函数实际调用的也是参数用 const 修饰的函数,所以输出“rvalue”;
  • 对于左值x来说,2 个重载模板函数都适用,C++编译器会选择最适合的参数类型为 T& 的函数模板,进而 therdef() 函数实际调用的是参数类型为非 const 的函数,输出“lvalue”。

显然,使用重载的模板函数实现完美转发也是有弊端的,此实现方式仅适用于模板函数仅有少量参数的情况,否则就需要编写大量的重载函数模板,造成代码的冗余。

为了方便用户更快速地实现完美转发,C++ 11 标准中允许在函数模板中使用右值引用来实现完美转发。 

C++11标准中规定,通常情况下,右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)

仍以 function() 函数为例,在 C++11 标准中实现完美转发,只需要编写如下一个模板函数即可

template <typename T>
void function(T&& t) {
    otherdef(t);
}

此模板函数的参数t既可以接收左值,也可以接收右值。但仅仅使用右值引用作为函数模板的参数是远远不够的,还有一个问题继续解决,即如果调用 function() 函数时为其传递一个左值引用或者右值引用的实参,(个人:也就是说,上面的解决方案对于传进去的是左值或者是右值,都能很好的完美转发,但是如果传进来是一个引用类型,左值引用或者右值引用,这时编译器推导T的类型时会出现问题),如下所示:  

int n = 10;
int & num = n;
function(num); 
int && num2 = 11;
function(num2);

其中,由 function(num) 实例化的函数底层就变成了 function(int &&& t),同样由 function(num2) 实例化的函数底层则变成了 function(int && && t)。要知道,C++98/03 标准是不支持这种用法的,而 C++ 11标准为了更好地实现完美转发,特意为其指定了新的类型匹配规则,又称为引用折叠规则(假设用 A 表示实际传递参数的类型):  

  • 当实参为左值或者左值引用(A&)时,函数模板中 T&& 将转变为 A&(A& && = A&);
  • 当实参为右值或者右值引用(A&&)时,函数模板中 T&& 将转变为 A&&(A&& && = A&&)。  

读者只需要知道,在实现完美转发时,只要函数模板的参数类型为 T&&,则 C++ 可以自行准确地判定出实际传入的实参是左值还是右值

通过将函数模板的形参类型设置为 T&&,我们可以很好地解决接收左、右值的问题。(个人:也就是说这一点解决了接收的问题,要不然不能函数模板不能同时调用左值或者右值),但除此之外,还需要解决一个问题,即无论传入的形参是左值还是右值,对于函数模板内部来说,形参既有名称又能寻址,因此它都是左值。(个人:这一点,不太懂,下面的代码似乎也说明了这一点,应该对于一个函数调用来说,如果一个变量能取得地址,就当它是左值,应该是一个右值引用是一个引用类型,它只能绑定到一个右值,但是这个右值引用却是一个左值,因为它有名字,可寻址,也就是右值引用和右值不是一回事)

#include <iostream>
using namespace std;

void f(int & x){
    cout<<"lvalue"<<endl;
}

void f(const int & x){
    cout<<"rvalue"<<endl;
}
int main() {
    int && num = 20;
    f(num);

    return 0;
}

  

那么如何才能将函数模板接收到的形参连同其左、右值属性,一起传递给被调用的函数?  C++11 标准的开发者已经帮我们想好了解决方案,该新标准还引入了一个模板函数 forword<T>(),我们只需要调用该函数,就可以很方便地解决此问题。

#include <iostream>
using namespace std;

//重载被调用函数,查看完美转发的效果
void otherdef(int& t) {
    cout << "lvalue\n";
}
void otherdef(const int& t) {
    cout << "rvalue\n";
}

//实现完美转发的函数模板
template <typename T>
void function(T&& t) {
    otherdef(forward<T>(t));
}

int main()
{
    function(5);
    int  x = 1;
    function(x);
    return 0;
}

总的来说,

  • 在定义模板函数时,我们采用右值引用的语法格式定义参数类型,由此,该函数既可以接收外界传入的左值,也可以接收右值;(个人:以一个统一的形式,解决接收问题)
  • 其次,还需要使用C++11标准库提供的 forword() 模板函数修饰被调用函数中需要维持左、右值属性的参数

由此即可轻松实现函数模板中参数的完美转发。 

C++11 nullptr:初始化空指针

所谓“野指针”,又称“悬挂指针”,指的是没有明确指向的指针。野指针往往指向的是那些不可用的内存区域,这就意味着像操作普通指针那样使用野指针,极可能导致程序发生异常。实际开发中,避免产生“野指针”最有效的方法,就是在定义指针的同时完成初始化操作,即便该指针的指向尚未明确,也要将其初始化为空指针。 

C++98/03 标准中,将一个指针初始化为空指针的方式有 2 种:  

int *p = 0;
int *p = NULL; //推荐使用
  • 可以看到,我们可以将指针明确指向 0(0x0000 0000)这个内存空间
    • 一方面,明确指针的指向可以避免其成为野指针;
    • 另一方面,大多数操作系统都不允许用户对地址为 0 的内存空间执行写操作,若用户在程序中尝试修改其内容,则程序运行会直接报错。(个人:对于空指针,指向的内存进行写操作,会发生错误,这一点也符合我们的直观) 
  • 相比第一种方式,我们更习惯将指针初始化为 NULL。值得一提的是,NULL 并不是 C++ 的关键字,它是 C++ 为我们事先定义好的一个宏,并且它的值往往就是字面量 0(#define NULL 0)。  

C++ 中将NULL定义为字面常量0,虽然能满足大部分场景的需要,但个别情况下,它会导致程序的运行和我们的预期不符。例如:

#include <iostream>
using namespace std;

void isnull(void* c) {
    cout << "void*c" << endl;
}
void isnull(int n) {
    cout << "int n" << endl;
}
int main() {
    isnull(0);
    isnull(NULL);
    return 0;
}

  • 对于 isnull(0) 来说,显然它真正调用的是参数为整形的 isnull() 函数;
  • 而对于 isnull(NULL),我们期望它实际调用的是参数为 void*c 的 isnull() 函数

C++ 98/03 标准中,如果我们想令 isnull(NULL) 实际调用的是 isnull(void* c),就需要对 NULL(或者 0)进行强制类型转换:  

isnull( (void*)NULL );
isnull( (void*)0 );

由于 C++ 98 标准使用期间,NULL 已经得到了广泛的应用,出于兼容性的考虑,C++11 标准并没有对 NULL 的宏定义做任何修改。为了修正 C++ 存在的这一 BUG,C++ 标准委员会最终决定另其炉灶,在 C++11 标准中引入一个新关键字,即 nullptrnullptr 是 nullptr_t 类型的右值常量,专用于初始化空类型指针nullptr_t 是 C++11 新增加的数据类型,可称为“指针空值类型”。(个人:也就是说nullptr是C++11语言中的关键字,是语言自带的,可以直接使用,不用include什么头文件)

值得一提的是,nullptr可以被隐式转换成任意的指针类型。举个例子:  

int * a1 = nullptr;
char * a2 = nullptr;
double * a3 = nullptr;

显然,不同类型的指针变量都可以使用 nullptr 来初始化,编译器分别将 nullptr 隐式转换成 int*、char* 以及 double* 指针类型。  

通过将指针初始化为 nullptr,可以很好地解决 NULL 遗留的问题,比如:

#include <iostream>
using namespace std;

void isnull(void* c) {
    cout << "void*c" << endl;
}
void isnull(int n) {
    cout << "int n" << endl;
}
int main() {
    isnull(NULL);
    isnull(nullptr);
    return 0;
}

借助执行结果不难看出,由于 nullptr 无法隐式转换为整形,而可以隐式匹配指针类型,因此执行结果和我们的预期相符

C++11 shared_ptr智能指针

内存管理机制,

垃圾自动回收”机制

  • 所谓垃圾,指的是那些不再使用或者没有任何指针指向的内存空间
  • 而“回收”则指的是将这些“垃圾”收集起来以便再次利用。  

C++98/03 标准中,支持使用 auto_ptr 智能指针来实现堆内存的自动回收C++11 新标准在废弃 auto_ptr 的同时,增添了 unique_ptr、shared_ptr 以及 weak_ptr 这 3 个智能指针来实现堆内存的自动回收

所谓智能指针,可以从字面上理解为“智能”的指针。具体来讲,

  • 智能指针和普通指针的用法是相似的,(个人:也就是智能指针作为指针的特性,用法和普通的指针类似)
  • 不同之处在于,智能指针可以在适当时机自动释放分配的内存。也就是说,使用智能指针可以很好地避免“忘记释放内存而导致内存泄漏”问题出现。(个人:智能指针智能的一面)

由此可见,C++ 也逐渐开始支持垃圾回收机制了,尽管目前支持程度还有限。C++ 智能指针底层是采用引用计数的方式实现的

  • 简单的理解,智能指针在申请堆内存空间的同时,会为其配备一个整形值(初始值为 1)
  • 每当有新对象使用此堆内存时,该整形值 +1;
  • 反之,每当使用此堆内存的对象被释放时,该整形值减 1。
  • 当堆空间对应的整形值为 0 时,即表明不再有对象使用它,该堆空间就会被释放掉。

实际上,每种智能指针都是以类模板的方式实现的,shared_ptr 也不例外。shared_ptr<T>(其中 T 表示指针指向的具体数据类型)的定义位于<memory>头文件,并位于 std 命名空间中,因此在使用该类型指针时,程序中应包含如下 2 行代码:

#include <memory>
using namespace std;

值得一提的是,和 unique_ptr、weak_ptr 不同之处在于,多个 shared_ptr 智能指针可以共同使用同一块堆内存。并且,由于该类型智能指针在实现上采用的是引用计数机制,即便有一个 shared_ptr 指针放弃了堆内存的“使用权”(引用计数减 1),也不会影响其他指向同一堆内存的 shared_ptr 指针(只有引用计数为 0 时,堆内存才会被自动释放)。  

shared_ptr智能指针的创建

  • 通过如下 2 种方式,可以构造出 shared_ptr<T> 类型的空智能指针
std::shared_ptr<int> p1;             //不传入任何实参
std::shared_ptr<int> p2(nullptr);    //传入空指针 nullptr

注意,空的shared_ptr指针,其初始引用计数为0,而不是1。  

  • 在构建shared_ptr 智能指针,也可以明确其指向。(个人:也就是在定义时,使用一个指向堆内存的指针初始化它),例如:
std::shared_ptr<int> p3(new int(10));
  • 同时,C++11 标准中还提供了 std::make_shared<T> 模板函数,其可以用于初始化 shared_ptr 智能指针,例如:
std::shared_ptr<int> p3 = std::make_shared<int>(10);

  • 除此之外,shared_ptr<T> 模板还提供有相应的拷贝构造函数和移动构造函数,例如:  
//调用拷贝构造函数
std::shared_ptr<int> p4(p3);//或者 std::shared_ptr<int> p4 = p3;
//调用移动构造函数
std::shared_ptr<int> p5(std::move(p4)); //或者 std::shared_ptr<int> p5 = std::move(p4);
    • 如上所示,p3 和 p4 都是 shared_ptr 类型的智能指针,因此可以用 p3 来初始化 p4,由于 p3 是左值,因此会调用拷贝构造函数。需要注意的是,如果 p3 为空智能指针,则 p4 也为空智能指针,其引用计数初始值为 0;反之,则表明p4和p3指向同一块堆内存,同时该堆空间的引用计数会加 1。 
    • 而对于 std::move(p4) 来说,该函数会强制将 p4 转换成对应的右值,因此初始化 p5 调用的是移动构造函数。另外和调用拷贝构造函数不同,用 std::move(p4) 初始化 p5,会使得p5拥有了 p4 的堆内存,而 p4 则变成了空智能指针。  

注意,同一普通指针不能同时为多个 shared_ptr 对象初始化,否则编译时不会报错,运行时程序会发生失败。例如:

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

int main() {
    
    int* ptr = new int;
    std::shared_ptr<int> p1(ptr);
    std::shared_ptr<int> p2(ptr);//错误
    return 0;
}

 

  • 在初始化 shared_ptr 智能指针时,还可以自定义所指堆内存的释放规则,这样当堆内存的引用计数为 0 时,会优先调用我们自定义的释放规则。  在某些场景中,自定义释放规则是很有必要的。比如,对于申请的动态数组来说,shared_ptr 指针默认的释放规则是不支持释放数组的,只能自定义对应的释放规则,才能正确地释放申请的堆内存。  对于申请的动态数组,释放规则可以使用 C++11 标准中提供的 default_delete<T> 模板类,我们也可以自定义释放规则:  
//指定 default_delete 作为释放规则
std::shared_ptr<int> p6(new int[10], std::default_delete<int[]>());
 
//自定义释放规则
void deleteInt(int*p) {
    delete []p;
}
//初始化智能指针,并自定义释放规则
std::shared_ptr<int> p7(new int[10], deleteInt);

实际上借助 lambda 表达式,我们还可以像如下这样初始化 p7,它们是完全相同的:  

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

详情见shared_ptr 的官网

shared_ptr<T>模板类提供的成员方法:  

shared_ptr<T>模板类常用成员方法
成员方法名功 能
operator=() 重载赋值号,使得同一类型的 shared_ptr 智能指针可以相互赋值。
operator*() 重载 * 号,获取当前 shared_ptr 智能指针对象指向的数据
operator->() 重载 -> 号,当智能指针指向的数据类型为自定义的结构体时,通过 -> 运算符可以获取其内部的指定成员。
swap() 交换 2 个相同类型 shared_ptr 智能指针的内容。
reset() 当函数没有实参时,该函数会使当前 shared_ptr 所指堆内存的引用计数减 1,同时将当前对象重置为一个空指针当为函数传递一个新申请的堆内存时,则调用该函数的 shared_ptr 对象会获得该存储空间的所有权,并且引用计数的初始值为 1
get() 获得 shared_ptr 对象内部包含的普通指针。
use_count() 返回同当前 shared_ptr 对象(包括它)指向相同的所有 shared_ptr 对象的数量
unique() 判断当前 shared_ptr 对象指向的堆内存,是否不再有其它 shared_ptr 对象再指向它。
operator bool() 如果是空指针,返回 false;反之,返回 true。

除此之外,C++11 标准还支持同一类型的 shared_ptr 对象,或者 shared_ptr 和 nullptr 之间,进行 ==,!=,<,<=,>,>= 运算。(个人:这里应该他们内部的指针之间的指针比较运算)

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

int main()
{
    //构建 2 个智能指针
    std::shared_ptr<int> p1(new int(10));
    std::shared_ptr<int> p2(p1);
    //输出 p2 指向的数据
    cout << *p2 << endl;
    p1.reset();//引用计数减 1,p1为空指针
    if (p1) {
        cout << "p1 不为空" << endl;
    }
    else {
        cout << "p1 为空" << endl;
    }
    //以上操作,并不会影响 p2
    cout << *p2 << endl;
    //判断当前和 p2 同指向的智能指针有多少个
    cout << p2.use_count() << endl;
    return 0;
}

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

int main()
{
    //构建 2 个智能指针
    shared_ptr<int> p1(new int(10));
    shared_ptr<int> p2(p1);
    shared_ptr<int> p3(p1);
    cout<<p2.use_count()<<endl;
    p3.reset(new int(9));
    cout<<p3.use_count()<<endl;
    cout<<p2.use_count()<<endl;
    int *x = p2.get();
    cout<<p2.use_count()<<endl;
    p2.reset();
    p1.reset();
    cout<<p2.use_count()<<endl;
    cout<<*x<<endl;

    return 0;
}

  

C++11 unique_ptr智能指针

作为智能指针的一种,unique_ptr 指针自然也具备“在适当时机自动释放堆内存空间”的能力。和shared_ptr指针最大的不同之处在于,unique_ptr指针指向的堆内存无法同其它unique_ptr共享,也就是说,每个unique_ptr 指针都独自拥有对其所指堆内存空间的所有权。这也就意味着,每个 unique_ptr 指针指向的堆内存空间的引用计数,都只能为1,一旦该unique_ptr指针放弃对所指堆内存空间的所有权,则该空间会被立即释放回收。 英 /juˈniːk/

unique_ptr智能指针是以类模板的形式提供的unique_ptr<T>(T为指针所指数据的类型)定义在<memory>头文件,并位于std命名空间中。因此,要想使用 unique_ptr 类型指针,程序中应首先包含如下 2 条语句: 

#include <memory>
using namespace std;

unique_ptr智能指针的创建:  

  • 通过以下2种方式,可以创建出空的unique_ptr指针:  
std::unique_ptr<int> p1;
std::unique_ptr<int> p2(nullptr);
  • 创建 unique_ptr 指针的同时,也可以明确其指向。例如:  
std::unique_ptr<int> p3(new int);

和可以用make_shared<T>() 模板函数初始化 shared_ptr指针不同,C++11标准中并没有为 unique_ptr类型指针添加类似的模板函数。  

  • 基于unique_ptr类型指针不共享各自拥有的堆内存,因此C++11标准中的unique_ptr模板类没有提供拷贝构造函数,只提供了移动构造函数。例如:
std::unique_ptr<int> p4(new int);
std::unique_ptr<int> p5(p4);//错误,堆内存不共享
std::unique_ptr<int> p5(std::move(p4));//正确,调用移动构造函数

值得一提的是,对于调用移动构造函数的p4和p5来说,p5 将获取p4所指堆空间的所有权,而p4将变成空指针(nullptr)

  • 默认情况下,unique_ptr指针采用 std::default_delete<T>方法释放堆内存。当然,我们也可以自定义符合实际场景的释放规则。值得一提的是,和shared_ptr指针不同,为unique_ptr自定义释放规则,只能采用函数对象的方式。例如:
//自定义的释放规则
struct myDel
{
    void operator()(int *p) {
        delete p;
    }
};
std::unique_ptr<int, myDel> p6(new int);
//std::unique_ptr<int, myDel> p6(new int, myDel());

unique_ptr<T>模板类提供的成员方法

除此之外,C++11标准还支持同类型的 unique_ptr 指针之间,以及 unique_ptr 和 nullptr 之间,做 ==,!=,<,<=,>,>= 运算。  

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

int main()
{
    std::unique_ptr<int> p5(new int);
    *p5 = 10;
    //p接收p5放弃的堆内存
    int* p = p5.release();
    cout << *p << endl;
    //判断 p5 是否为空指针
    if (p5) {
        cout << "p5 is not nullptr" << endl;
    }
    else {
        cout << "p5 is nullptr" << endl;
    }
    std::unique_ptr<int> p6;
    //p6 获取 p 的所有权
    p6.reset(p);
    cout << *p6 << endl;
    return 0;
}

C++11 weak_ptr智能指针

和 shared_ptr、unique_ptr 类型指针一样,weak_ptr 智能指针也是以类模板的方式实现的。weak_ptr<T>( T 为指针所指数据的类型)定义在<memory>头文件,并位于 std 命名空间中。因此,要想使用 weak_ptr 类型指针,程序中应首先包含如下 2 条语句: 

#include <memory>
using namespace std;

需要注意的是,C++11标准虽然将 weak_ptr 定位为智能指针的一种,但该类型智能指针通常不单独使用(没有实际用处),只能和 shared_ptr 类型指针搭配使用。甚至于,我们可以将 weak_ptr 类型指针视为 shared_ptr 指针的一种辅助工具,借助 weak_ptr 类型指针, 我们可以获取 shared_ptr 指针的一些状态信息,比如有多少指向相同堆内存的 shared_ptr 指针、shared_ptr 指针指向的堆内存是否已经被释放等等。  

需要注意的是,

  • 当 weak_ptr 类型指针的指向和某一 shared_ptr 指针相同时,weak_ptr 指针并不会使所指堆内存的引用计数加 1;
  • 同样,当 weak_ptr 指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。

也就是说,weak_ptr 类型指针并不会影响所指堆内存空间的引用计数。 

除此之外,weak_ptr<T> 类模板中没有重载 * 和 -> 运算符,这也就意味着,weak_ptr 类型指针只能访问所指的堆内存,而无法修改它。  

weak_ptr指针的创建

  • 可以创建一个空 weak_ptr 指针,例如:
std::weak_ptr<int> wp1;
  • 凭借已有的 weak_ptr 指针,可以创建一个新的 weak_ptr 指针,例如:  
std::weak_ptr<int> wp2 (wp1);

若 wp1 为空指针,则 wp2 也为空指针;反之,如果 wp1 指向某一 shared_ptr 指针拥有的堆内存,则 wp2 也指向该块存储空间(可以访问,但无所有权)。  

  • 在构建 weak_ptr 指针对象时,可以利用已有的 shared_ptr 指针为其初始化。例如:
std::shared_ptr<int> sp (new int);
std::weak_ptr<int> wp3 (sp);

由此,wp3 指针和 sp 指针有相同的指针。再次强调,weak_ptr 类型指针不会导致堆内存空间的引用计数增加或减少

表 1 weak_ptr指针可调用的成员方法
成员方法功 能
operator=() 重载 = 赋值运算符,是的, weak_ptr 指针可以直接被 weak_ptr 或者 shared_ptr 类型指针赋值
swap(x) 其中 x 表示一个同类型的 weak_ptr 类型指针该函数可以互换 2 个同类型 weak_ptr 指针的内容
reset() 将当前 weak_ptr 指针置为空指针
use_count() 查看指向和当前 weak_ptr 指针相同的 shared_ptr 指针的数量。
expired() 判断当前 weak_ptr 指针是否过期(指针为空,或者指向的堆内存已经被释放)
lock() 如果当前 weak_ptr 已经过期,则该函数会返回一个空的 shared_ptr 指针;反之,该函数返回一个和当前 weak_ptr 指向相同的 shared_ptr 指针

再次强调,weak_ptr<T> 模板类没有重载 * 和 -> 运算符,因此 weak_ptr 类型指针只能访问某一shared_ptr 指针指向的堆内存空间,无法对其进行修改

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

int main()
{
    std::shared_ptr<int> sp1(new int(10));
    std::shared_ptr<int> sp2(sp1);
    std::weak_ptr<int> wp(sp2);
    //输出和 wp 同指向的 shared_ptr 类型指针的数量
    cout << wp.use_count() << endl;
    //释放 sp2
    sp2.reset();
    cout << wp.use_count() << endl;
    //借助 lock() 函数,返回一个和 wp 同指向的 shared_ptr 类型指针,获取其存储的数据
    cout << *(wp.lock()) << endl;
    cout << wp.use_count() << endl;

    cout << "===============" << endl;
    auto ret = wp.lock();
    cout << wp.use_count() << endl;

    return 0;
}

详情见 weak_ptr 官网。 

  

posted on 2022-09-16 23:58  朴素贝叶斯  阅读(274)  评论(0编辑  收藏  举报

导航