🥭01-C++面试之C++11新特性总结

0 前述

针对于自己在秋招的面试中,对于Cpp部分遇到的问题,其中大部分是以此为起点,尼克以基于这些点,将自己对于Cpp的学习,串联起来,无论面试官,问这一类问题中的那个点,你都应该可以将这一个珠子串联到自己一大串知识点上来讲。这是一种拓展知识的能力。

在此专栏下面个人校招记录:回馈牛客,对CPP做一个小小的总结。

本部分关于C++11新特性总结,挑选几个比较重要的点来展开即可,如**lambda表达式、右值引用和三种智能**指针来展开。

  • 下面对应的是之前发布的个人校招其他公司面试总结,希望可以更好的帮到你

这里是Cpp一些面试问题整理

1 关键字/语法层面

  • nullptr替代 NULL

  • 引入了 autodecltype 这两个关键字实现了类型推导:使得编译器在编译阶段就推导出变量的类型,可以通过=右边的类型推导出变量的类型。(比较方便,自己经常使用到的)

  • 继承关键字:final 和override,默认构造函数**=default**、=delete,内存对齐alignof和alignasdeleteusing的使用。(这些完全都可以是个引子,展开再说)

    • final一个规范化的关键字:

    如果后面不允许继承该类的话,后面应使用 final关键字来结束:

    // final 用法
    class A final {
        //  
    };
    //声明为final,B再继承的话,编译报错
    class B : A {
        // 
    };
    
    • override(重写/覆盖)关键字:要求基类函数是寻函数,其派生类去override这个虚函数,需要保证与基类的虚函数名字、类型、参数等完全一致。好的写法应该是:在重写函数的后面加上关键字override。1)避免写错函数名,若写错的话,编译器会认为这是一个独立的函数,编译器不会检查这个错误。

1 usingtypedef的区别使用:

using可以用于模版别名,typedef不可以用于模版别名。

(自己认为,C++里面还是using用着比较方便)

template<typename T>
class A {
   public:
   A() {
       // ...        
   }
};
template <typename T>
using B =A<T>;
template <typename T>
typedef A<T> C;

int main() {
    A<int> a;
    B<int> b; // Ok, B is an alias of class A
    C<int> c; // Syntax Error, C cannot be recognized as a type
    return 0;
}
  • 基于范围的for循环,结合auto关键字,也使用的比较多

    for (auto& val : array) {
       // ... 
    }
    

2 标准库层面

  • 增加无序容器:哈希表hashtable
  • 正则表达式

1 Lambda表达式(展开讲讲)

  • 基本形式:

    [捕获列表](参数列表){函数体};其中捕获列表和函数体不能省略但是捕获列表可以为空,也就是最简单的lambda表达式:[]{};

  • lambda 表达式又叫匿名函数,C++中,一个lambda表达式表示一个可调用的代码单元。我们可以将其理解为一个未命名的内联函数,特别注意,他们的返回值类型由函数体的return语句决定,因此一般使用auto关键字来自动推导。

  • 使用实例:

    int i =[] {return 1;}
    
  • 捕获列表[]使用:

    [=],表示所有捕获的参数都是以值传入的,不可修改

    [&],表示所有捕获的参数都是以应用传入的,可以修改,修改之后,对外部的变量产生影响。

    int p =100, q =200;
    auto func =[&] {q++, p+=2; return p +q;} // 
    
  • 带调用参数的lambda表达式

    // 返回和 int a, int b是调用时传递的参数
    auto myADD =[](int v1, int v2) {
        return a +b;
    }
    // 用法
    auto ans =myADD(2, 3);
    
  • 带明确的返回值的lambda表达式,使用->

    // 返回最大值
    auto myMAX =[](int a, int b) ->int {
        if (a >b) return a;
        else return b;
    }
    // 用法
    auto ans =myMAX(2, 3);
    
  • 利用lambda表达式可以编写内嵌的匿名函数,用以独立函数或者函数对象,在向算法传递函数的时候,比如sort排序的时候,我们按照什么顺序你,可以河面穿进去一元谓词(接受一个参数)或者二元谓词(接受两个参数)

  • Lambda 的类型被定义为“闭包”的类,其通常用于STL 库中,在某些场景下可用于简化仿函数的使用,同时Lambda 作为局部函数,也会提高复杂代码的开发加速,轻松在函数内重用代码,无须费心设计接口。

    // 语法形式: 
    // [capture list](parameter list) -> return type {function body}
    // [capture list]: 声明使用那些局部变量,但只能使用那些明确指明的变量,只有捕获了才能在{function body}中使用。
    
  • 参考网址

    C++中的lambda表达式

3 右值引用

1 右值引用概念

右值引用也是别名,但是只能对右值进行引用。引用的形式是int&& ra =10,只是为了区分C++98里面的引用,C++11将该种方式称之为右值引用。

2 左值与右值使用形式

  • 普通类型的变量,int a,因为有名字,可以取地址,都认为是左值。
  • const修饰的常量,代表是不可修改,只读类型,但是我们可以取地址,C++11认为其是左值。也可以指向右值,比如vectorpush_back (const int & val)函数的设计,直接插入push_back (5)就是这个原理。
  • 表达式的运行结果是一个临时变量或者对象,如(a+b),认为是右值;
  • 表达式的运行结果是单个变量或是一个引用,如(c=a+b)认为是左值;
  • 右值区分:
    • C语言中的纯右值,比如说:a+b, 100
    • 将亡值,也就是生命周期即将结束的变量,比如临时变量:表达式的中间结果,函数按照值的方式进行返回,匿名变量。(后面展开说)

3 实际用法介绍

int main()
{
    // 1.普通类型引用只能引用左值,不能引用右值
    int a = 10;
    int& ra1 = a; // ra为a的别名
    
    //int& ra2 = 10; // 编译失败,因为10是右值
    
    //const引用既可以引用左值也可以引用右值
    const int& ra3 = 10;
    const int& ra4 = a;
    return 0;
}
  • 普通引用只能引用左值,不能引用右值;const引用既可以引用左值,也可以引用右值;
  • C++11中右值引用;只能引用右值,一般情况不能直接引用左值(需要使用move处理一下);
int main()
{
    // 10纯右值,本来只是一个符号,没有具体的内存地址,
    // 右值引用变量r1在定义过程中,编译器产生了一个临时变量,r1实际引用的是临时变量
    int&& r1 = 10; // 可以是常数,出来之后就可以表示一个左值来用
    r1 = 100;
    
    int a = 10;
    // int&& r2 = a; // 编译失败:右值引用不能引用左值
    int&& r3=std::move(a);// 作为函数&& 返回值是右值 ,直接声明出来的 && 是左值
    return 0;
}
  1. 右值引用只能引用右值,不能引用左值;

  2. 右值引用可以进行引用std::move()处理以后的左值,move表示将该变量(左值)识别为右值,并没有移动什么,只是将左值强制转换为右值

  3. 右值引用本质是将引用的右值内容存储到空间中,使得该右值引用变量具有名称和地址,所以右值引用变量一般是一个左值。作为函数&& 返回值是右值 ,直接声明出来的 && 是左值

4 右值引用和std::move的使用场景

1)一点结论

  • 从性能上讲,左右值引用并没有区别,传参使用左右值引用都可以避免一次参数拷贝。
  • 右值引用可以直接指向右值,还可以通过std::move()指向左值;而左值引用只能指向左值(除去const左值引用,也能指向右值)
  • 作为函数的形参时,右值引用更加灵活,虽然const左值引用也可以做到左右值都接受,但它无法修改传进来的参数,有一定的局限性。

2)右值实现移动语义

在实际的场景中,右值引用和std::move别广泛用于在STL和自定义的类中实现移动语义,避免拷贝,从而提升程序性能

在没有右值引用之前,一个数组类需要有构造函数、拷贝构造、赋值运算符重载、析构函数等。

class Array {
public:
    Array(int size) : size_(size) {
        data_ = new int[size_];
    }
     
    // 深拷贝构造
    Array(const Array& temp_array) {
        size_ = temp_array.size_;
        data_ = new int[size_];
        for (int i = 0; i < size_; i ++) {
            data_[i] = temp_array.data_[i];
        }
    }
     
    // 深拷贝赋值
    Array& operator=(const Array& temp_array) {
        delete[] data_;
 
        size_ = temp_array.size_;
        data_ = new int[size_];
        for (int i = 0; i < size_; i ++) {
            data_[i] = temp_array.data_[i];
        }
    }
    
 	// 析构函数
    ~Array() {
        delete[] data_;
    }
 
public:
    int *data_;
    int size_;
};

该类的拷贝构造、赋值运算符重载函数已经通过使用左值引用避免一次多余的拷贝了,但是内部实现还是要深拷贝,无法避免。

这时候,就感觉可不可以提供一个移动构造函数,把被拷贝者的数据移动过来,被拷贝者后边就不要了,这样就可以避免深拷贝。

创造类似下面这样的情况:

	// 移动构造函数,可以浅拷贝,不优雅的实现方式
    Array(const Array& temp_array, bool move) {
        data_ = temp_array.data_;
        size_ = temp_array.size_;
        // 为防止temp_array析构时delete data,提前置空其data_      
        temp_array.data_ = nullptr;
    }

有2个问题

  1. 不优雅,要与构造函数区分,需要引入额外的参数
  2. 无法实现,const类型的左值引用,你不能修改参数类型。若改成非const类型的,直接插入的形式(直接常量,直接一个数组)就不能使用了。

这样,左值引用就用着很不爽了,右值引用的出现解决了这个问题,STL的容器中,都实现了以右值引用为参数的移动构造函数移动赋值重载函数;常见的比如vectorpush_back()emplace_back(),如下P5进行展示。参数为左值引用意味着拷贝,为右值引用意味着移动

	// 优雅
    Array(Array&& temp_array) {
        data_ = temp_array.data_;
        size_ = temp_array.size_;
        // 为防止temp_array析构时delete data,提前置空其data_      
        temp_array.data_ = nullptr;
    }

5 右值引用中std::move语义在STL容器中的使用

// 例2:std::vector和std::string的实际例子
int main() {
    std::string str1 = "aacasxs";
    std::vector<std::string> vec;
     
    vec.push_back(str1); // 传统方法,copy
    vec.push_back(std::move(str1)); // 调用移动语义的push_back方法,避免拷贝,str1会失去原有值,变成空字符串
    vec.emplace_back(std::move(str1)); // emplace_back效果相同,str1会失去原有值
    vec.emplace_back("axcsddcas"); // 当然可以直接接右值
}
 
// std::vector方法定义
void push_back (const value_type& val);
void push_back (value_type&& val);
 
void emplace_back (Args&&... args);

在vector和string这个场景,加个std::move会调用到移动语义函数,避免深拷贝。可用在对象在需要拷贝并且拷贝之后不再被需要的场合,建议使用std::move触发移动 语义,提升性能。

另外,在STL中有些类是move-only的,比如unique_ptr只有移动构造函数。

6 完美转发std::forward

std::move一样,它的兄弟std::forward也充满了迷惑性,forward意思是转发,但是它并不会做转发,同样也是类型转换,

不过他更高级比兄弟move,``move只能转出右值,forward是左右值都可以。

std::forward(u)有两个参数:T与 u。

a. 当T为左值引用类型时,u将被转换为T类型的左值;

b. 否则u将被转换为T类型右值

看下面两个例子,进一步加深理解:

例1:

void B(int&& ref_r) {
    ref_r = 1;
}
 
// A、B的入参是右值引用
// 有名字的右值引用是左值,因此ref_r是左值
void A(int&& ref_r) {
    B(ref_r);  // 错误,B的入参是右值引用,需要接右值,ref_r是左值,编译失败
     
    B(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
    B(std::forward<int>(ref_r));  // ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值
}
 
int main() {
    int a = 5;
    A(std::move(a));
}

例2:

void change2(int&& ref_r) {
    ref_r = 1;
}
 
void change3(int& ref_l) {
    ref_l = 1;
}
 
// change的入参是右值引用
// 有名字的右值引用是 左值,因此ref_r是左值
void change(int&& ref_r) {
    change2(ref_r);  // 错误,change2的入参是右值引用,需要接右值,ref_r是左值,编译失败
    change1(ref_r); //  ok 表示的左值就没问题
     
    change2(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
    change2(std::forward<int &&>(ref_r));  // ok,std::forward的T是右值引用类型(int &&),符合条件b,因此u(ref_r)会被转换为右值,编译通过
     
    change3(ref_r); // ok,change3的入参是左值引用,需要接左值,ref_r是左值,编译通过
    change3(std::forward<int &>(ref_r)); // ok,std::forward的T是左值引用类型(int &),符合条件a,因此u(ref_r)会被转换为左值,编译通过
    // 可见,forward可以把值转换为左值或者右值
}
 
int main() {
    int a = 5;
    change(std::move(a));
}

7 参考链接

4 三个智能指针

C++11引入智能指针的概念,方便管理堆内存。使用普通指针容易造成内存泄漏,忘记释放、或者是二次释放、程序发生异常是内存泄漏问题。使用智能指针能够更好的管理堆内存;包含在头文件#include <memory>中。智能指针是RAII(resource acquire instance initalize🙆‍♂️)最具代表性的实现。三种指针的大小测试说明:

1 shared_ptr(正常指针大小的二倍):

使用引用计数,每一个shared_ptr的拷贝都指向相同的内存地方。每次使用拷贝,内部的应用计数加1,每析构一次,内部的引用计数减1,减为0时,自动删除所指向的堆内存。另外,shared_ptr的内部引用计数是线程安全的,但是对象的读取是需要加锁的。

初始化的时候。智能指针是个模板类,1)可以指定类型,传入指针通过构造函数初始化。2)也可以使用make_shared函数初始化。不能将指针直接赋值给一个智能指针,因为一个是类,一个是指针。例如std::shared_ptr p4 =new int(1),需要使用构造函数的形式。

2 unique_ptr(正常指针大小):

表示唯一拥有其所指对象,同一时刻只能有一个unique_ptr指向给定对象(通过禁止拷贝语义,只用移动语义来实现)。

相比于原始指针,unique_ptr指针用于其RAIIResource Acquisition is Initialization)直译过来就是,资源获取即初始化,也就是说在构造函数中申请分配资源,在析构函数中释放资源。由于C++的语言机制保证了,当一个对象创建的时候,自动调用构造函数,当对象超出作用域会自动调用析构函数。所以,在RAII的指导下,我们应该使用类来管理资源,将资源和对象的生命周期绑定。

unique_ptr的声明周期内,可以改变其所指对象,如创建智能指针是通过构造函数指定,中间通过reset方法重新指定,通过release方法释放所有权,通过移动语义转移所有权。

3 weak_ptr(正常指针大小的二倍):

是一种不控制对象生命周期的智能指针,它指向shared_ptr管理的对象,weak_ptr只是提供了对管理对象的一个访问手段。引入的目的是为了配合shared_ptr而引入的一种智能指针来协助shared_ptr工作,它只能从一个shared_ptr或者另一个weak_ptr进行对象构造,它的构造和析构不会引起引用计数的增加或者减少。

解决shared_ptr循环引用形成的锁,问题如下

template <typename T>
class Node {
public:
    Node(const T& value) :_pre(NULL), _next(NULL), _value(value) {
        cout << "Node()" << endl;
    }
    ~Node() {
        cout << "~Node()" << endl;
        cout << "this:" << this << endl;
    }

    shared_ptr<Node<T>> _pre;
    shared_ptr<Node<T>> _next;
    T _value;
};

void fun_test() {
    shared_ptr<Node<int>> sp1(new Node<int>(1));
    shared_ptr<Node<int>> sp2(new Node<int>(2));

    cout << "sp1.use_count: " << sp1.use_count() << endl;
    cout << "sp2.use_count: " << sp2.use_count() << endl;

    sp1->_next =sp2; // sp2 引用+1
    cout << "sp1.use_count: " << sp1.use_count() << endl;
    cout << "sp2.use_count: " << sp2.use_count() << endl;
    sp2->_pre =sp1; // sp1 引用+1
    cout << "sp1.use_count: " << sp1.use_count() << endl;
    cout << "sp2.use_count: " << sp2.use_count() << endl;

}

int main() {
    fun_test();
    system("pause"); // 导致离开作用域的时候,两个都不会释放,造成内存泄漏
    return 0;
}

// 运行结果
Node()
Node()
sp1.use_count: 1
sp2.use_count: 1
sp1.use_count: 1
sp2.use_count: 2
sp1.use_count: 2
sp2.use_count: 2
请按任意键继续. . .

在实际的编程过程,应该尽量避免出现智能指针之间相互指向的情况,如果不可避免,可以使用weak_ptr它不增加引用计数,只要出了作用域就会自动析构。

在使用的过程中,需要得知该std::weak_ptr指向的资源是否有效呢?STL提供一个expired的方法来检测,返回true的话,表示该资源有效,这时,可以使用std::weak_ptrlock函数得到一个std::shared_ptr对象后继续操作资源。

// weak_sp是一个weaked_ptr
if (weak_sp.expired()) {
    return;
}
std::shared_ptr<int> shared_sp =weak.sp.lock();
if (shared_sp) {
    //
}

// 这是不允许进行判断的
if (weak_sp) {
    
}

注意这里不能像一般指针那样去判断weak_ptr是否有效,不能直接判断资源是否有效的方式进行解决。因为,std::weak_ptr没有重写operator->operator*,因此,该指针不能直接操作对象,也没有重写bool()操作,不能通过判断本身是否为nullptr来确定该资源是否有效。

4 shared_ptr的底层实现一个简单版本

template <typename T>
class shared_ptr {
  public:
    // constructor
    shared_ptr(T* ptr =NULL) : _ptr(ptr), _pcount(new int(1)) {}
    
    // copy constructor
    shared_ptr(const shared_ptr& s) : _ptr(s._ptr), _pcount(s._pcount) {
        *(_pcount)++;
    }
    
    // copy assignment
    shared_ptr<T>& operator=(const shared_ptr& s) {
        if (this !=&s) {
            if (--(*(this->_pcount)) ==0) {
                delete this->_ptr;
                delete this->_pcount;
            }
            _ptr =s._ptr;
            _pcount =s._pcount;
            *(_pcount)++;
        }
        return *this;
    }
    
    // overloading operator
    T& operator*() {
        return *(this->_ptr);
    }
    T* operator->() {
        return this->ptr;
    }
    
    // destruct constructor
    ~shared_ptr() {
        --(*(this->_pcount));
        if (this->_pcount ==0) {
            delete _ptr;
            _ptr =NULL;
            delete _pcount;
            _pcount =NULL;
        }
    }
  private:
    T* _ptr; // 指向内存处的地址
    int* _pcount; // 指向引用计数的指针
};

5 延伸一个问题:智能指针的应用计数在内存部分:堆上:hamster:

shared_ptr中会有引用计数的出现,unique_ptrweak_ptr是没有使用引用计数的

一句话:为了共享这个reference count值。

首先一点需要明确:如果有多个智能指针指向同一对象(内存地址),他们的reference count必须相同。rf值的更改需要“广播”给所有对应的智能指针。

考虑如下代码:

shared_ptr<int> p1 =make_shared(3); // rf(p1) =1;
shared_ptr<int> p2(p1); // copy constructor rf(p1) =rf(p2) =2
p1 =nullptr;
p2 =nullptr;
  • 如果引用计数在栈区,那么当p1重新改指向nullptr时,如何通知p2的rf该减1,如果各自有各自的rf,那么就不会统一指向同一资源的rf值了。就会造成一定的内存泄漏,就乱套了

  • 如果rf在堆区,每个智能指针保存的是它在堆上的地址。那么当p1更改指向时,就调用(*rf)--,其作用就直接扩散到p2 的rf上来了,其堆内存才能正确释放。

所以引用计数分配在堆上,就是使得一个智能指针更新rf时,其它指向这块内存的智能指针的rf值都能一起更新。

另一种说法:

因为STL的shared_ptr想设计成“可以指向任何东西”,这就意味着你必须额外为引用计数来分配空间,不然引用计数放哪?放栈上的话函数return后引用计数就废了!

换个思路,我看到的一些自研游戏引擎是这样做的:做一个基类ISharedObject,这里面放引用计数。所有能被智能指针(引擎自己实现的智能指针)指向的东西都继承自这个类(也就意味着不继承自这个类的对象都不能被智能指针指向)。这样这个引用计数就被放到了object上。就不用额外new一个引用计数了。其实这是牺牲了一点通用性但是换来了性能提升(不用额外new引用计数,cache也是连续的)

作者:知乎用户7Az15m 链接:https://www.zhihu.com/question/290143401/answer/471490511 来源:知乎 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。s

6 网址参考

#C++##晒一晒我的offer##我发现了面试通关密码##秋招开了,你想投哪些公司呢#
个人校招记录【回馈牛客】 文章被收录于专栏

这是一个求职总结专栏,求职过程中,牛客里面各位同志,提供了很多面试的信息,对我个人有很大的帮助。这里简单将自己面试记录总结于此。 本人23届校招生,双非硕士,投递岗位嵌入式,控制算法,后台开发均有涉猎,优先级递减。简历累计投递数量:提前批(34)+正式批(128),累计Offer(5+)。

全部评论
好东西 码了
点赞 回复 分享
发布于 2023-09-09 22:58 广东
shared_ptr实现里面*(_pcount)++;应该是(*_pcount)++;吧
点赞 回复 分享
发布于 2023-09-10 13:12 浙江
m
点赞 回复 分享
发布于 2023-10-20 21:48 北京

相关推荐

02-08 20:56
已编辑
南京工业大学 Java
在等offer的比尔很洒脱:我也是在实习,项目先不说,感觉有点点小熟悉,但是我有点疑问,这第一个实习,公司真的让实习生去部署搭建和引入mq之类的吗,是不是有点过于信任了,我实习过的两个公司都是人家正式早搭好了,根本摸不到部署搭建的
点赞 评论 收藏
分享
评论
16
72
分享

创作者周榜

更多
牛客网
牛客企业服务