#《C++11新特兴》

概述

  • 提高运行效率的语言特性:右值引用、泛化常量表达式
  • 原有语法的使用性增强:初始化列表、统一的初始化语法、类型推导、范围for循环、Lambda表达式、final和override、构造函数委托
  • 语言能力的提升:空指针nullptr、default和delete、长整数、静态assert
  • C++标准库的更新:智能指针、正则表达式、哈希表等

PART1:提高运行效率的语言特性

特性1:右值引用

首先先介绍左值lvalue是和右值rvalue

基本定义

  • 左值lvalue全称是location value表示存储在内存中==有明确存储地址(可取地址)==的数据。例如一个变量。

  • 右值rvalue全称是read value表示==提供数据值(不可取地址)==的数据。例如,一个整数或字符串。c++11中的右值分为两种:

    • 纯右值:例如字符串、数字之类的。
    • 将亡值:表示一个即将消亡的对象。例如与右值引用相关的表达式,比如:T&&类型函数的返回值,std::move的返回值等。

    ==简单来说,左值可以被取地址,右值不可以被取地址。==

书写方式

  • 左值引用,采用一个&

    1
    2
    int a=4;
    int& b=a; //左值引用

    常量左值引用是一个万能引用类型,它可以接受一个左值、右值、常量左值和常量右值。

  • 右值引用,采用两个&&

    1
    int&& b=6; // 右值引用只能赋值右值和右值引用

作用

  • 左值引用:==提高程序效率==,不进行拷贝对象仅仅是起别名。例如再传递形参时,不需要拷贝对象。

  • 右值引用:==延长临时对象的生命周期==。主要是对一些临时对象的寿命延长,提高程序的效率。

    • 考虑情景:在c++中进行对象赋值操作的时候,很多情况下会发生对象之间的拷贝构造,如果堆内存较大,则深拷贝的代价较高,因此在有些情况下需要避免对象的深拷贝,就可以使用右值引用进行性能的优化。案例如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      #include <iostream>
      using namespace std;
      class Test {
      public:
      int* m_p;
      public:
      Test():m_p(new int(10))
      {// 构造函数
      cout << " construct function" << endl;
      cout << "address m_p=" << m_p<<endl;
      }

      // 析构函数
      ~Test() {
      cout << " delete construct" << endl;
      delete m_p;
      m_p = nullptr;
      }
      // 复制构造函数
      Test(const Test & obj):m_p(new int(*obj.m_p)) { // 本类类型对象的引用
      // 有指针需要进行深拷贝
      cout << "copy constructor" << endl;
      }
      };
      Test getobject() {
      Test obj;
      return obj;
      }

      int main() {
      Test t = getobject();
      return 0;
      }

      上述代码运行结果如下:

      首先调用构造函数生成一个临时对象obj,然后进行拷贝构造函数赋值main中的对象t,然后临时对象obj和对象t调用析构函数,释放内存。但该过程存在的问题是==临时对象并没有使用就进行释放==,如果可以使用创建出的临时对象obj则可以节省资源,因此考虑右值引用。

      image-20220307221259133

      为了使用右值引用,我们对类中==添加移动构造函数(将堆中的资源进行移动,并将原来的指针置为nullptr,否则会析构两次相同地址)==,代码如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      #include <iostream>

      using namespace std;

      class Test {
      public:
      int* m_p;
      public:
      Test():m_p(new int(10))
      {// 构造函数
      cout << " construct function" << endl;
      cout << "address m_p=" << m_p<<endl;
      }
      // 析构函数
      ~Test() {
      cout << " delete construct" << endl;
      delete m_p;
      m_p = nullptr;
      }
      // 复制构造函数
      Test(const Test & obj):m_p(new int(*obj.m_p)) { // 本类类型对象的引用
      // 有指针需要进行深拷贝
      cout << "copy constructor" << endl;
      }
      // 移动构造函数
      Test( Test&& obj) :m_p(obj.m_p) {
      obj.m_p = nullptr;
      cout << "move constructor" << endl;
      }

      };
      Test getobject() {
      Test obj;
      return obj;
      }
      int main() {
      /*
      这里没有使用拷贝构造函数的原因是因为右边的对象是一个临时对象,编译器会调用移动构造函数,否则会使用拷贝构造函数
      */
      Test&& t2 = getobject();
      cout << "address m_p=" << t2.m_p << endl; // 使用堆中的空间
      return 0;
      }

      此时的数据结果如下所示,可以看到此时调用的是移动构造函数(==此时没有调用拷贝构造的原因是因为=右边是一个临时对象,编译器会调用移动构造函数,否则会使用拷贝构造函数。==):

      image-20220307224454482

      另外一种不使用移动构造函数的方法是直接返回一个右值引用,具体如下:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      #include <iostream>

      using namespace std;

      class Test {
      public:
      int* m_p;
      public:
      Test():m_p(new int(10))
      {// 构造函数
      cout << " construct function" << endl;
      cout << "address m_p=" << m_p<<endl;
      }


      // 析构函数
      ~Test() {
      cout << " delete construct" << endl;
      delete m_p;
      m_p = nullptr;
      }
      // 复制构造函数
      Test(const Test & obj):m_p(new int(*obj.m_p)) { // 本类类型对象的引用
      // 有指针需要进行深拷贝
      cout << "copy constructor" << endl;
      }

      };
      Test getobject2() { // 返回一个将忘值

      return Test(); // 不能取地址
      }
      Test&& getobject2() { // 返回一个将忘值

      return Test(); // 不能取地址
      }
      int main() {
      // 如果没有移动构造函数,使用右值引用初始化要求更高,要求右侧是一个临时的不能取地址的对象。
      Test&& t2 = getobject2();
      cout << "address m_p=" << t2.m_p << endl; //使用所有资源
      Test&& t3 = getobject3();
      cout << "address m_p=" << t3.m_p << endl; //使用所有资源
      return 0;
      }

      image-20220307225547642

    注意:

    • 如果==不在类中定义移动构造函数==,则对右值引用初始化==要求更高==,要求右侧是一个临时的不能取地址的对象,如代码中的return Test();
    • 定义移动构造函数函数是使用==部分==临时对象的值,而使用右值引用(不定义移动构造函数)则是使用==所有==临时对象的资源。

注:constconstexpr都可以用于定义常量,但是constexpr在==编译阶段==会将对应的常量替换为数据提高运行效率。

特性2:泛化的常量表达式

解决问题&作用

c++11之前存在常量关键字const,存在两种含义:变量只读修饰常量。如下案例所示:

1
2
3
4
5
6
void f(const int num){  // 此时是变量只读的含义并非常量
int b=23;
int arr[b]; // error 由于arr为静态变量因此需要一个常量
const int c=90; // c为一个常量
int arr1[c]; // right
}

常量表达式发生在编译阶段,而非常量表达式在运行阶段计算。常量表达式可以提高运行效率。

c++11中引入了constexpr用于修饰常量表达式,常量表达式是指由多个常量组成并且在编译阶段就能得到计算结果的表达式。

而问题在于如何让编译器知道一个表达式是常量表达式,因此在c++11中引入了constexpr关键字用于指示常量表达式,提高程序运行效率。

使用方法

  • 修饰常量:可以在任何自定义类型前加上constexpr关键字,而在定义时对于自定义类型的structclass之前加constexpr无效的,而可以定义一个该类型的常量对象

  • 常量表达式函数

    • 修饰函数:需要符合以下几个条件

      • 函数必须要有返回值,并且return的表达式必须是常量表达式
      • 使用之前必须要对应的定义语句
      • 函数体中不能出现非常量表达式以外的语句(using指令、typedef语句以及static_assert断言、return语句除外)
      • 以上规则对于成员函数也是适用的。
    • 修饰模版函数

      • 模版类型中存在不确定性,因此函数模版实例化后的模版函数是否符合常量表达式函数的要求也不确定。==如果constexpr修饰的模版函数实例化结果不满足常量表达式函数的要求,则constexpr会被自动忽略,即该函数等同于一个普通函数==。

        1
        2
        3
        4
        template <typename 	T>
        constexpr T funct(T t){ //由于不能确定T是否传入一个常量类型因此不能返回值可能不符合常量表达式要求,会被自动忽略。
        return t;
        }
    • 修饰构造函数

      • 可以使用constexpr修饰一个构造函数,这样可以得到一个常量构造函数,但是要求是==构造函数的函数体必须为空,并且采用初始化列表的方式为各个成员赋值==。

        1
        2
        3
        4
        5
        6
        7
        8
        class Test{
        public:
        int a;
        public:
        constexpr Test():a(100){

        }
        };

Tips:

  • 采用using 定义新类型和typedef定义新类型效果一样,只是using更加直观。

    1
    2
    using mytype=int;
    mytype num1=10;
  • 函数模版与模版函数的区别:函数模版是指创建函数的一个模版,模版函数是使用模版实例化的一个函数