C++八股文(四)

C++八股文(四)

1. C++11 的新特性有哪些

C++新特性主要包括包含语法改进和标准库扩充两个方面,主要包括以下11点:

  1. 语法的改进

    (1)统一的初始化方法

    (2)成员变量默认初始化

    (3)auto关键字 用于定义变量,编译器可以自动判断的类型(前提:定义一个变量时对其进行初始化)

    (4)decltype 求表达式的类型

    (5)智能指针 shared_ptr

    (6)空指针 nullptr(原来NULL)

    (7)基于范围的for循环

    (8)右值引用和move语义 让程序员有意识减少进行深拷贝操作

  2. 标准库扩充(往STL里新加进一些模板类,比较好用)

    (9)无序容器(哈希表) 用法和功能同map一模一样,区别在于哈希表的效率更高

    (10)正则表达式 可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字符串

    (11)Lambda表达式

具体展开:

  1. 统一的初始化方法

    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++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 的直接初始化。a5、a6 则是基本数据类型的列表初始化方式。可以看到,它们的形式都是统一的。这里需要注意的是,a3 虽然使用了等于号,但它仍然是列表初始化,因此,私有的拷贝构造并不会影响到它。a4 和 a6 的写法,是 C++98/03 所不具备的。在 C++11 中,可以直接在变量名后面跟上初始化列表,来进行对象的初始化。

  2. 成员变量默认初始化

    好处:构建一个类的对象不需要用构造函数初始化成员变量。

    //程序实例
    #include
    using namespace std;
    class B
    {
    public:
        int m = 1234; //成员变量有一个初始值
        int n;
    };
    int main()
    {
        B b;
        cout << b.m << endl;
        return 0;
    }
    
  3. auto关键字

    用于定义变量,编译器可以自动判断的类型(前提:定义一个变量时对其进行初始化)。

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

    可以看出来,定义迭代器 i 的时候,类型书写比较冗长,容易出错。然而有了 auto 类型推导,我们大可不必这样,只写一个 auto 即可。

  4. decltype 求表达式的类型

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

    (1)为什么要有decltype

    ​ 因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来非常不方便,甚至压根无法使用,所以 decltype 关键字也被引入到 C++11 中。

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

    auto varname = value;
    decltype(exp) varname = value;
    

    其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。

    ​ auto 根据"=“右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟”="右边的 value 没有关系。

    ​ 另外,auto 要求变量必须初始化,而 decltype 不要求。这很容易理解,auto 是根据变量的初始值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。decltype 可以写成下面的形式:

    decltype(exp) varname;
    

    (2)代码示例

    // decltype 用法举例
    int a = 0;
    decltype(a) b = 1;  //b 被推导成了 int
    decltype(10.8) x = 5.5;  //x 被推导成了 double
    decltype(x + 100) y;  //y 被推导成了 double
    
  5. 智能指针 shared_ptr

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

    #include 
    #include 
    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;
    }
    
    /*    程序运行结果:        
                10
                p1 为空
                10
                1    
    */          
    
  6. 空指针 nullptr(原来NULL)

    nullptr 是 nullptr_t 类型的右值常量,专用于初始化空类型指针。nullptr_t 是 C++11 新增加的数据类型,可称为“指针空值类型”。也就是说,nullptr 仅是该类型的一个实例对象(已经定义好,可以直接使用),如果需要我们完全定义出多个同 nullptr 完全一样的实例对象。值得一提的是,nullptr 可以被隐式转换成任意的指针类型。例如:

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

    显然,不同类型的指针变量都可以使用 nullptr 来初始化,编译器分别将 nullptr 隐式转换成 int*、char* 以及 double* 指针类型。另外,通过将指针初始化为 nullptr,可以很好地解决 NULL 遗留的问题,比如:

    #include 
    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;
    }
    
    /*    程序运行结果:        
            int n
            void*c
    */          
    
  7. 基于范围的for循环

    如果要用 for 循环语句遍历一个数组或者容器,只能套用如下结构:

    for(表达式 1; 表达式 2; 表达式 3){
        //循环体
    }
    
    //程序实例
    #include 
    #include 
    #include 
    using namespace std;
    int main() {
        char arc[] = "www.123.com";
        int i;
        //for循环遍历普通数组
        for (i = 0; i < strlen(arc); i++) {
            cout << arc[i];
        }
        cout << endl;
        vector<char>myvector(arc,arc+3);
        vector<char>::iterator iter;
        //for循环遍历 vector 容器
        for (iter = myvector.begin(); iter != myvector.end(); ++iter) {
            cout << *iter;
        }
        return 0;
    }
    /*    程序运行结果:        
            www.123.com
            www
    */          
    
  8. 右值引用和move语义

    • 右值引用

      ​ 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++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示。

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

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

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

      int && a = 10;
      a = 100;
      cout << a << endl;
      /*    程序运行结果:        
              100    
      */          
      

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

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

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

    • move语义

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

      move( arg ) //其中,arg 表示指定的左值对象。该函数会返回 arg 对象的右值形式。
      
      //程序实例
      #include 
      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:    //这里应该是 private,使用 public 是为了更方便说明问题
          int *num;
      };
      class second {
      public:
          second() :fir() {}
          //用 first 类的移动构造函数初始化 fir
          second(second && sec) :fir(move(sec.fir)) {
              cout << "second move construct" << endl;
          }
      public:    //这里也应该是 private,使用 public 是为了更方便说明问题
          first fir;
      };
      int main() {
          second oth;
          second oth2 = move(oth);
          //cout << *oth.fir.num << endl;   //程序报运行时错误
          return 0;
      }
      
      /*    程序运行结果:
                construct!
              first move construct!
              second move construct
      */            
      
  9. 无序容器(哈希表)

    用法和功能同map一模一样,区别在于哈希表的效率更高。

    (1) 无序容器具有以下 2 个特点:

    ​ a. 无序容器内部存储的键值对是无序的,各键值对的存储位置取决于该键值对中的键,

    ​ b. 和关联式容器相比,无序容器擅长通过指定键查找对应的值(平均时间复杂度为 O(1));但对于使用迭代器遍历容器中存储的元素,无序容器的执行效率则不如关联式容器。

    (2) 和关联式容器一样,无序容器只是一类容器的统称,其包含有 4 个具体容器,分别为 unordered_map、unordered_multimap、unordered_set 以及 unordered_multiset。功能如下表:

    无序容器 功能
    unordered_map 存储键值对 类型的元素,其中各个键值对键的值不允许重复,且该容器中存储的键值对是无序的。
    unordered_multimap 和 unordered_map 唯一的区别在于,该容器允许存储多个键相同的键值对
    unordered_set 不再以键值对的形式存储数据,而是直接存储数据元素本身(当然也可以理解为,该容器存储的全部都是键 key 和值 value 相等的键值对,正因为它们相等,因此只存储 value 即可)。另外,该容器存储的元素不能重复,且容器内部存储的元素也是无序的。
    unordered_multiset 和 unordered_set 唯一的区别在于,该容器允许存储值相同的元素

    (3) 程序实例(以 unordered_map 容器为例)

    #include 
    #include 
    #include 
    using namespace std;
    int main()
    {
        //创建并初始化一个 unordered_map 容器,其存储的  类型的键值对
        std::unordered_map<std::string, std::string> my_uMap{
            {"教程1","www.123.com"},
            {"教程2","www.234.com"},
            {"教程3","www.345.com"} };
        //查找指定键对应的值,效率比关联式容器高
        string str = my_uMap.at("C语言教程");
        cout << "str = " << str << endl;
        //使用迭代器遍历哈希容器,效率不如关联式容器
        for (auto iter = my_uMap.begin(); iter != my_uMap.end(); ++iter)
        {
            //pair 类型键值对分为 2 部分
            cout << iter->first << " " << iter->second << endl;
        }
        return 0;
    }
    
    /*    程序运行结果:
              教程1 www.123.com
              教程2 www.234.com
              教程3 www.345.com
    */            
    
  10. 正则表达式

    1. 可以认为正则表达式实质上是一个字符串,该字符串描述了一种特定模式的字符串。常用符号的意义如下:

      符号 意义
      ^ 匹配行的开头
      $ 匹配行的结尾
      . 匹配任意单个字符
      […] 匹配[]中的任意一个字符
      (…) 设定分组
      \ 转义字符
      \d 匹配数字[0-9]
      \D \d 取反
      \w 匹配字母[a-z],数字,下划线
      \W \w 取反
      \s 匹配空格
      \S \s 取反
      + 前面的元素重复1次或多次
      * 前面的元素重复任意次
      ? 前面的元素重复0次或1次
      {n} 前面的元素重复n次
      {n,} 前面的元素重复至少n次
      {n,m} 前面的元素重复至少n次,至多m次
      | 逻辑或
  11. Lambda匿名函数

    所谓匿名函数,简单地理解就是没有名称的函数,又常被称为 lambda 函数或者 lambda 表达式。

    (1)定义

    ​ lambda 匿名函数很简单,可以套用如下的语法格式:

    ​ [外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型
    ​ {
    ​ 函数体;
    ​ };

    其中各部分的含义分别为:

    a. [外部变量方位方式说明符]

    [ ] 方括号用于向编译器表明当前是一个 lambda 表达式,其不能被省略。在方括号内部,可以注明当前 lambda 函数的函数体中可以使用哪些“外部变量”。

    所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量

    b. (参数)

    和普通函数的定义一样,lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略;

    c. mutable

    此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字。

    ​ **注意:**对于以值传递方式引入的外部变量,lambda 表达式修改的是拷贝的那一份,并不会修改真正的外部变量;

    d. noexcept/throw()

    可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。

    e. -> 返回值类型

    指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略"-> 返回值类型"。

    f. 函数体

    和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。

    (2)程序实例

    #include 
    #include 
    using namespace std;
    int main()
    {
        int num[4] = {4, 2, 3, 1};
        //对 a 数组中的元素进行排序
        sort(num, num+4, [=](int x, int y) -> bool{ return x < y; } );
        for(int n : num){
            cout << n << " ";
        }
        return 0;
    }
    
    /*    程序运行结果:
              1 2 3 4
    */            
    

2. C++ 中智能指针和指针的区别是什么?

  1. 智能指针

    ​ 如果在程序中使用new从堆(自由存储区)分配内存,等到不需要时,应使用delete将其释放。C++引用了智能指针auto_ptr,以帮助自动完成这个过程。随后的编程体验(尤其是使用STL)表明,需要有更精致的机制。基于程序员的编程体验和BOOST库提供的解决方案,C++11摒弃了auto_ptr,并新增了三种智能指针:unique_ptr、shared_ptr和weak_ptr。所有新增的智能指针都能与STL容器和移动语义协同工作。

  2. 指针

    ​ C 语言规定所有变量在使用前必须先定义,指定其类型,并按此分配内存单元。指针变量不同于整型变量和其他类型的变量,它是专门用来存放地址的,所以必须将它定义为“指针类型”。

  3. 智能指针和普通指针的区别

智能指针和普通指针的区别在于智能指针实际上是对普通指针加了一层封装机制,区别是它负责自动释放所指的对象,这样的一层封装机制的目的是为了使得智能指针可以方便的管理一个对象的生命期。

3. C++中的智能指针有哪些?分别解决的问题以及区别?

  1. C++中的智能指针有4种,分别为:shared_ptr、unique_ptr、weak_ptr、auto_ptr,其中auto_ptr被C++11弃用。

  2. 使用智能指针的原因

    ​ 申请的空间(即new出来的空间),在使用结束时,需要delete掉,否则会形成内存碎片。在程序运行期间,new出来的对象,在析构函数中delete掉,但是这种方法不能解决所有问题,因为有时候new发生在某个全局函数里面,该方法会给程序员造成精神负担。此时,智能指针就派上了用场。使用智能指针可以很大程度上避免这个问题,因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,析构函数会自动释放资源。所以,智能指针的作用原理就是在函数结束时自动释放内存空间,避免了手动释放内存空间。

  3. 四种指针分别解决的问题以及各自特性如下:

    (1)auto_ptr(C++98的方案,C++11已经弃用)

    ​ 采用所有权模式。

    auto_ptr<string> p1(new string("I reigned loney as a cloud."));
    auto_ptr<string> p2;
    p2=p1; //auto_ptr不会报错
    

    此时不会报错,p2剥夺了p1的所有权,但是当程序运行时访问p1将会报错。所以auto_ptr的缺点是:存在潜在的内存崩溃问题。

    2)unique_ptr(替换auto_ptr)

    ​ unique_ptr实现独占式拥有或严格拥有概念,保证同一时间内只有一个智能指针可以指向该对象。它对于避免资源泄露,例如,以new创建对象后因为发生异常而忘记调用delete时的情形特别有用。

    ​ 采用所有权模式,和上面例子一样。

    auto_ptr<string> p3(new string("I reigned loney as a cloud."));
    auto_ptr<string> p4;
    p4=p3; //此时不会报错
    

    编译器认为P4=P3非法,避免了p3不再指向有效数据的问题。因此,unique_ptr比auto_ptr更安全。 另外unique_ptr还有更聪明的地方:当程序试图将一个 unique_ptr 赋值给另一个时,如果源 unique_ptr 是个临时右值,编译器允许这么做;如果源 unique_ptr 将存在一段时间,编译器将禁止这么做,比如:

    unique_ptr<string> pu1(new string ("hello world"));
    unique_ptr<string> pu2;
    pu2 = pu1;                                      // #1 not allowed
    unique_ptr<string> pu3;
    pu3 = unique_ptr<string>(new string ("You"));   // #2 allowed
    

    其中#1留下悬挂的unique_ptr(pu1),这可能导致危害。而#2不会留下悬挂的unique_ptr,因为它调用 unique_ptr 的构造函数,该构造函数创建的临时对象在其所有权让给 pu3 后就会被销毁。这种随情况而已的行为表明,unique_ptr 优于允许两种赋值的auto_ptr 。

    ​ **注意:**如果确实想执行类似与#1的操作,要安全的重用这种指针,可给它赋新值。C++有一个标准库函数std::move(),让你能够将一个unique_ptr赋给另一个。例如:

    unique_ptr<string> ps1, ps2;
    ps1 = demo("hello");
    ps2 = move(ps1);
    ps1 = demo("alexia");
    cout << *ps2 << *ps1 << endl;
    

    (3)shared_ptr(非常好使)

    ​ shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。从名字share就可以看出了资源可以被多个指针共享,它使用计数机制来表明资源被几个指针共享。可以通过成员函数use_count()来查看资源的所有者个数。除了可以通过new来构造,还可以通过传入auto_ptr, unique_ptr,weak_ptr来构造。当我们调用release()时,当前指针会释放资源所有权,计数减一。当计数等于0时,资源会被释放。

    ​ shared_ptr 是为了解决 auto_ptr 在对象所有权上的局限性(auto_ptr 是独占的), 在使用引用计数的机制上提供了可以共享所有权的智能指针。

    成员函数:

    use_count 返回引用计数的个数

    unique 返回是否是独占所有权( use_count 为 1)

    swap 交换两个 shared_ptr 对象(即交换所拥有的对象)

    reset 放弃内部对象的所有权或拥有对象的变更, 会引起原有对象的引用计数的减少

    get 返回内部对象(指针), 由于已经重载了()方法, 因此和直接使用对象是一样的.如 shared_ptr sp(new int(1)); sp 与 sp.get()是等价的

    (4)weak_ptr

    ​ weak_ptr 是一种不控制对象生命周期的智能指针, 它指向一个 shared_ptr 管理的对象。进行该对象的内存管理的是那个强引用的 shared_ptr。weak_ptr只是提供了对管理对象的一个访问手段。weak_ptr 设计的目的是为配合 shared_ptr 而引入的一种智能指针来协助 shared_ptr 工作,它只可以从一个 shared_ptr 或另一个 weak_ptr 对象构造, 它的构造和析构不会引起引用记数的增加或减少。weak_ptr是用来解决shared_ptr相互引用时的死锁问题,如果说两个shared_ptr相互引用,那么这两个指针的引用计数永远不可能下降为0,资源永远不会释放。它是对对象的一种弱引用,不会增加对象的引用计数,和shared_ptr之间可以相互转化,shared_ptr可以直接赋值给它,它可以通过调用lock函数来获得shared_ptr。

    class B;
    class A
    {
    public:
        shared_ptr<B> pb_;
        ~A()
    {
        cout<<"A delete\n";
    }
    };
    class B
    {
    public:
        shared_ptr<A> pa_;
        ~B()
    {
        cout<<"B delete\n";
    }
    };
    void fun()
    {
        shared_ptr<B> pb(new B());
        shared_ptr<A> pa(new A());
        pb->pa_ = pa;
        pa->pb_ = pb;
        cout<<pb.use_count()<<endl;
        cout<<pa.use_count()<<endl;
    }
    int main()
    {
        fun();
        return 0;
    }
    

    可以看到fun函数中pa ,pb之间互相引用,两个资源的引用计数为2,当要跳出函数时,智能指针pa,pb析构时两个资源引用计数会减一,但是两者引用计数还是为1,导致跳出函数时资源没有被释放(A B的析构函数没有被调用),如果把其中一个改为weak_ptr就可以了,我们把类A里面的shared_ptr pb_; 改为weak_ptr pb; 这样的话,资源B的引用开始就只有1,当pb析构时,B的计数变为0,B得到释放,B释放的同时也会使A的计数减一,同时pa析构时使A的计数减一,那么A的计数为0,A得到释放。

4.简述 C++ 右值引用与转移语义

  1. 右值引用

​ 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++11 标准新引入了另一种引用方式,称为右值引用,用 “&&” 表示。

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

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

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

int && a = 10;
a = 100;
cout << a << endl;
/*    程序运行结果:        
        100    
*/          

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

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

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

  1. move语义

见问题1中的8小点

5. 简述 C++ 中智能指针的特点

(1)auto_ptr

​ auto指针存在的问题是,两个智能指针同时指向一块内存,就会两次释放同一块资源,自然报错。

(2)unique_ptr

​ unique指针规定一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存,编译报错。

​ **实现原理:**将拷贝构造函数和赋值拷贝构造函数申明为private或delete。不允许拷贝构造函数和赋值操作符,但是支持移动构造函数,通过std:move把一个对象指针变成右值之后可以移动给另一个unique_ptr

(3)shared_ptr

​ 共享指针可以实现多个智能指针指向相同对象,该对象和其相关资源会在引用为0时被销毁释放。

​ **实现原理:**有一个引用计数的指针类型变量,专门用于引用计数,使用拷贝构造函数和赋值拷贝构造函数时,引用计数加1,当引用计数为0时,释放资源。

**注意:**weak_ptr、shared_ptr存在一个问题,当两个shared_ptr指针相互引用时,那么这两个指针的引用计数不会下降为0,资源得不到释放。因此引入weak_ptr,weak_ptr是弱引用,weak_ptr的构造和析构不会引起引用计数的增加或减少。

6. weak_ptr 能不能知道对象计数为 0,为什么?

不能。

​ weak_ptr是一种不控制对象生命周期的智能指针,它指向一个shared_ptr管理的对象。进行该对象管理的是那个引用的shared_ptr。weak_ptr只是提供了对管理 对象的一个访问手段。weak_ptr设计的目的只是为了配合shared_ptr而引入的一种智能指针,配合shared_ptr工作,它只可以从一个shared_ptr或者另一个weak_ptr对象构造,它的构造和析构不会引起计数的增加或减少。从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存,但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。

7. share_ptr 怎么知道跟它共享对象的指针释放了

多个shared_ptr对象可以同时托管一个指针,系统会维护一个托管计数。当无shared_ptr托管该指针时,delete该指针。

8. shared_ptr 线程安全性,原理

多线程环境下,调用不同shared_ptr实例的成员函数是不需要额外的同步手段的,即使这些shared_ptr拥有的是同样的对象。但是如果多线程访问(有写操作)同一个shared_ptr,则需要同步,否则就会有race condition 发生。也可以使用 shared_ptr overloads of atomic functions来防止race condition的发生。

​ 多个线程同时读同一个shared_ptr对象是线程安全的,但是如果是多个线程对同一个shared_ptr对象进行读和写,则需要加锁。

​ 多线程读写shared_ptr所指向的同一个对象,不管是相同的shared_ptr对象,还是不同的shared_ptr对象,也需要加锁保护。例子如下:

//程序实例
shared_ptr<long> global_instance = make_shared<long>(0);
std::mutex g_i_mutex;

void thread_fcn()
{
    //std::lock_guard lock(g_i_mutex);	// 实现加锁保护

    //shared_ptr local = global_instance;

    for(int i = 0; i < 100000000; i++)
    {
        *global_instance = *global_instance + 1;
        //*local = *local + 1;
    }
}

int main(int argc, char** argv)
{
    thread thread1(thread_fcn);
    thread thread2(thread_fcn);

    thread1.join();
    thread2.join();

    cout << "*global_instance is " << *global_instance << endl;

    return 0;
}

​ 在线程函数thread_fcn的for循环中,2个线程同时对global_instance进行加1的操作。这就是典型的非线程安全的场景,最后的结果是未定的,运行结果如下:

​ *global_instance is 197240539

​ 如果使用的是每个线程的局部shared_ptr对象local,因为这些local指向相同的对象,因此结果也是未定的,运行结果如下:

​ *global_instance is 160285803

​ 因此,这种情况下必须加锁,将thread_fcn中的第一行代码的注释去掉之后,不管是使用global_instance,还是使用local,得到的结果都是:

​ *global_instance is 200000000

9. 简述一下 C++11 中四种类型转换

C++中四种类型转换分别为const_cast、static_cast、dynamic_cast、reinterpret_cast,四种转换功能分别如下:

  1. const_cast

    ​ 将const变量转为非const

  2. static_cast

    ​ 最常用,可以用于各种隐式转换,比如非const转const,static_cast可以用于类向上转换,但向下转换能成功但是不安全。

  3. dynamic_cast

​ 只能用于含有虚函数的类转换,用于类向上和向下转换

​ **向上转换:**指子类向基类转换。

​ **向下转换:**指基类向子类转换。

​ 这两种转换,子类包含父类,当父类转换成子类时可能出现非法内存访问的问题。

​ dynamic_cast通过判断变量运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。dynamic_cast可以做类之间上下转换,转换的时候会进行类型检查,类型相等成功转换,类型不等转换失败。运用RTTI技术,RTTI是”Runtime Type Information”的缩写,意思是运行时类型信息,它提供了运行时确定对象类型的方法。在c++层面主要体现在dynamic_cast和typeid,vs中虚函数表的-1位置存放了指向type_info的指针,对于存在虚函数的类型,dynamic_cast和typeid都会去查询type_info。

​ 4. reinterpret_cast

​ reinterpret_cast可以做任何类型的转换,不过不对转换结果保证,容易出问题。

**注意:**为什么不用C的强制转换:C的强制转换表面上看起来功能强大什么都能转,但是转换不够明确,不能进行错误检查,容易出错。

10.简述一下 C++ 11 中 auto 的具体用法

​ auto用于定义变量,编译器可以自动判断变量的类型。auto主要有以下几种用法:

  1. auto的基本使用方法

    (1)基本使用语法如下

    auto name = value; //name 是变量的名字,value 是变量的初始值
    

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

    (2)程序实例如下

    auto n = 10; 
    auto f = 12.8; 
    auto p = &n; 
    auto url = "www.123.com";
    

​ a. 第 1 行中,10 是一个整数,默认是 int 类型,所以推导出变量 n 的类型是 int。

​ b. 第 2 行中,12.8 是一个小数,默认是 double 类型,所以推导出变量 f 的类型是 double。

​ c. 第 3 行中,&n 的结果是一个 int* 类型的指针,所以推导出变量 f 的类型是 int*。

​ d. 第 4 行中,由双引号""包围起来的字符串是 const char* 类型,所以推导出变量 url 的类型是 const char*,也即一个常量指针。

  1. auto和 const 的结合使用

    (1) auto 与 const 结合的用法

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

    ​ b. 当类型为引用时,auto 的推导结果将保留表达式的 const 属性。

    (2)程序实例如下

    int  x = 0;
    const  auto n = x;  //n 为 const int ,auto 被推导为 int
    auto f = n;      //f 为 const int,auto 被推导为 int(const 属性被抛弃)
    const auto &r1 = x;  //r1 为 const int& 类型,auto 被推导为 int
    auto &r2 = r1;  //r1 为 const int& 类型,auto 被推导为 const int 类型
    
  2. 使用auto定义迭代器

    在使用 stl 容器的时候,需要使用迭代器来遍历容器里面的元素;不同容器的迭代器有不同的类型,在定义迭代器时必须指明。而迭代器的类型有时候比较复杂,请看下面的例子:

    #include 
    using namespace std;
    int main(){
        vector< vector<int> > v;
        //vector< vector >::iterator i = v.begin();
        auto i = v.begin();  //使用 auto 代替具体的类型,该句比上一句简洁,根据表达式 v.begin() 的类型(begin() 函数的返回值类型)来推导出变量i的类型
        return 0;
    }
    
  3. 用于泛型编程

    auto 的另一个应用就是当我们不知道变量是什么类型,或者不希望指明具体类型的时候,比如泛型编程中。请看下面例子:

    #include 
    using namespace std;
    class A{
    public:
        static int get(void){
            return 100;
        }
    };
    class B{
    public:
        static const char* get(void){
            return "www.123.com";
        }
    };
    template <typename T>
    void func(void){
        auto val = T::get();
        cout << val << endl;
    }
    int main(void){
        func<A>();
        func<B>();
        return 0;
    }
    
    /*        运行结果:
                100
                www.123.com
    */
    

    ​ 本例中的模板函数 func() 会调用所有类的静态函数 get(),并对它的返回值做统一处理,但是 get() 的返回值类型并不一样,而且不能自动转换。这种要求在以前的 C++ 版本中实现起来非常的麻烦,需要额外增加一个模板参数,并在调用时手动给该模板参数赋值,用以指明变量 val 的类型。但是有了 auto 类型自动推导,编译器就根据 get() 的返回值自己推导出 val 变量的类型,就不用再增加一个模板参数了。

你可能感兴趣的:(C++基础,c++,开发语言,八股文)