从今天起,豆芽有空也尽己所能,帮助一下大家。

面经来源:https://www.nowcoder.com/discuss/694328?channel=1009&source_id=discuss_center_discuss_history_nctrack&ncTraceId=828c6977acfc42d680c9803e2a376f4f.286.16276989747911046


1. C++14有啥新特性?

  1. 泛型Lambda

  2. constexpr:C++11的constexpr函数只能包含一个表达式,C++14放松了这些限制,支持诸如if 和switch等条件语句,支持循环,其中包括基于区间(range)的for 循环。

  3. 类型推导

    • 支持所有函数的返回类型推导

    • 改进decltype(auto)语法,它支持使用与auto同样的机制计算给定表达式的类型。


2. C++17有啥新特性?

  1. 构造函数模板推导

  2. 新增Attribute

  3. namespace嵌套

  4. std::apply函数

等等


3. C++20有啥新特性?

  1. 概念(concept)

  2. 协程(Coroutines)

  3. Modules


4. lambda如何实现的?

原理:编译器会把一个lambda表达式生成一个匿名类的匿名对象,并在类中重载函数调用运算符()

我们举个简单的例子:

   auto print = []{cout << "Douya" << endl; };

那么编译器生成一个匿名类的匿名对象,形式可能如下:

   //用给定的lambda表达式生成相应的类
   class print_class{
   public:
       void operator()(void) const{
           cout << "Douya" << endl;
       }
   };
   //用构造的类创建对象,print此时就是一个函数对象
   auto print = print_class();

可以看到匿名类里重载了函数调用运算符()。还生成了一个函数对象,那么我们就直接可以使用这个函数对象了。


5. shared_ptr介绍一下,来手撕一下

shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。

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

    #include <iostream>  
    #include <stdlib.h>  
    using namespace std;  

    template <typename T>  
    class mysharedPtr {  
    public:  
        mysharedPtr(T* p = NULL);  
        ~mysharedPtr();  
        mysharedPtr(const mysharedPtr<T>& other);  
        mysharedPtr<T>& operator=(const mysharedPtr<T>& other);  
    private:  
        T* m_ptr;  
        unsigned int* m_count;  
    };  

    template <typename T>  
    mysharedPtr<T>::mysharedPtr(T* p) {  
        m_ptr = p;  
        m_count = new unsigned int(0);  
        ++(*m_count);  
        cout << "Constructor is succeed!" << endl;  
    }  

    template <typename T>  
    mysharedPtr<T>::~mysharedPtr() {  
        --(*m_count);  
        if ((*m_count) == 0) {  
            delete[] m_ptr;  
            m_ptr = NULL;  
            delete[] m_count;  
            m_count = NULL;  
            cout << "Destructor is succeed!" << endl;  
        }  
    }  

    template <typename T>  
    mysharedPtr<T>::mysharedPtr(const mysharedPtr<T>& other) {  
        m_ptr = other.m_ptr;  
        m_count = other.m_count;  
        ++(*m_count);  
        cout << "Copy constructor is succeed!" << endl;  
    }  

    template <typename T>  
    mysharedPtr<T>& mysharedPtr<T>::operator=(const mysharedPtr<T>& other) {  
        // 《C++ primer》:“这个赋值操作符在减少左操作数的使用计数之前使other的使用计数加1,  
        // 从而防止自身赋值”而导致的提早释放内存  
        ++(*other.m_count);  
        --(*m_count);  
        // 将左操作数对象的使用计数减1,若该对象的使用计数减至0,则删除该对象  
        if ((*m_count) == 0) {  
            delete[] m_ptr;  
            m_ptr = NULL;  
            delete[] m_count;  
            m_count = NULL;  
            cout << "Left side object is deleted!" << endl;  
        }  
        m_ptr = other.m_ptr;  
        m_count = other.m_count;  
        cout << "Assignment operator overloaded is succeed!" << endl;  
        return *this;  
    }  

6. 引用计数器的值是堆上的还是栈上的?

堆上。涉及到new申请的都是堆上。


7. 拷贝构造函数参数必须要const?

必须要有。

我们给出例子:不加const

#include <iostream>
using namespace std;

class Test{
public:
    Test(int x):m_val(x){};
    Test(Test&){};
    ~Test(){};
    void GetVal() const {cout << m_val << endl;};
    //Test GetObj(void){return Test(20);}
private:
    int m_val;
};

int main()
{
    Test obj1(20);
    obj1.GetVal();

    //Test obj2 = obj1.GetObj();

    cout << "Hello World";
    return 0;
}

20
Hello World

不加const对于普通构造没有问题。我们接下来考虑一种场景,加入:Test GetObj(void){return Test(20);}

#include <iostream>
using namespace std;

class Test{
public:
    Test(int x):m_val(x){};
    Test(Test&){};
    ~Test(){};
    void GetVal() const {cout << m_val << endl;};
    Test GetObj(void){return Test(20);}
private:
    int m_val;
};

int main()
{
    Test obj1(20);
    obj1.GetVal();

    Test obj2 = obj1.GetObj();

    cout << "Hello World";
    return 0;
}

报错:
error: cannot bind non-const lvalue reference of type ‘Test&’ to an rvalue of type ‘Test’

原因是:返回一个局部变量是通过一个临时的变量返回,对象也不例外,这里也会产生一个临时的对象,而这个临时对象,具有常性,也就是const,不可被修改。

就是告诉我们非const,所以我们为拷贝构造函数加入const后编译器通过。

返回临时对象是C++中非常常见的一种操作,由此看来,为拷贝构造函数加入const是很有必要的


8. 为什么不能返回局部的引用?

返回值是栈上的值,函数结束,栈被系统回收,内存的值就不存在了。


9. 为什么要operator=重载等于号,C++为啥要这么设计?

对于C++提供的所有操作符,通常只支持对于基本数据类型(如int、float)和标准库中提供的类(如string)的操作,而对于用户自己定义的类,如果想要通过该操作符实现一些基本操作(比如比较大小,判断是否相等),就需要用户自己来定义关于这个操作符的具体实现了。

比如:

class myselfclass{  
public:  
    myselfclass();  
    ~myselfclass();  
private:  
    int m_val;  
    bool m_flag;  
};  

myselfclass a,b;  
if (a == b){  
    ......  
}  

这里我们定义了一个类,然后定义了两个对象a、b,然后判断a == b,那问题来了,这个语句是表达什么意思呢?是a整个对象空间都等于b的对象空间呢,还是仅仅a的某个成员变量等于b的某个成员变量呢?

关键看程序员在这里想实现什么样的逻辑,就重定义怎样的行为。所以需要对 == 操作符进行个人定义。


10. push_back和emplace_back的区别

  1. 原版的push_back需要调用拷贝构造函数,2倍扩容机制。而C++11后, push_back直接调用emplace_back,利用了右值引用的移动语义特性,避免了拷贝构造函数的调用,提升了效率

  2. push_back没有利用可变参数模板,emplace_back可以传入多个参数,到函数内调用placement new构造。


11. weak_ptr了解吗?

shared_ptr存在共享指针的循环引用计数问题。weak_ptr是弱引用,weak_ptr的构造和析构不会引起引用计数的增加或减少。我们可以将其中一个改为weak_ptr指针就可以了。比如我们将class B里shared_ptr换成weak_ptr


12. push_back插入均摊复杂度

采用均摊分析的方法,公式如下:

img

公式里面,假定有 n 个元素,倍增因子为 m(我们设定为2),那么完成这 n 个元素往一个 vector 中的push_back操作,需要重新分配内存的次数大约为 log2(n),第 i 次重新分配将会导致复制 2^i (也就是当前的vector.size() 大小)个旧空间中元素,因此 npush_back 操作所花费的总时间:

img

然后 n 次push_back操作,每次分摊O(1),即为常数时间复杂度了。


13. epoll的原理?在对比一下select的效率

epoll提供了三个函数,epoll_create、epoll_ctl和epoll_wait。

首先创建一个epoll对象,然后使用epoll_ctl对这个对象进行操作(添加、删除、修改),把需要监控的描述符加进去,这些描述符将会以epoll_event结构体的形式组成一颗红黑树,接着阻塞在epoll_wait,进入大循环,当某个fd上有事件发生时,内核将会把其对应的结构体放入一个链表中,返回有事件发生的链表。

(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。

(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把当前进程往设备等待队列中挂一次,而epoll只要一次拷贝,而且把当前进程往等待队列上挂也只挂一次,这也能节省不少的开销。


14. epoll为什么是红黑树?

查询更稳定(对比哈希表,哈希表还存在扩容问题),内存占用更小(对比哈希表,哈希表一般占得空间更大)



以上所有题的答案其实都来源于我的博客面经,欢迎大家围观:https://blog.nowcoder.net/jiangwenbo