从今天起,豆芽有空也尽己所能,帮助一下大家。
1. C++14有啥新特性?
泛型Lambda
constexpr:C++11的constexpr函数只能包含一个表达式,C++14放松了这些限制,支持诸如if 和switch等条件语句,支持循环,其中包括基于区间(range)的for 循环。
类型推导
支持所有函数的返回类型推导
改进decltype(auto)语法,它支持使用与auto同样的机制计算给定表达式的类型。
2. C++17有啥新特性?
构造函数模板推导
新增Attribute
namespace嵌套
std::apply函数
等等
3. C++20有啥新特性?
概念(concept)
协程(Coroutines)
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的区别
原版的push_back需要调用拷贝构造函数,2倍扩容机制。而C++11后, push_back直接调用emplace_back,利用了右值引用的移动语义特性,避免了拷贝构造函数的调用,提升了效率
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插入均摊复杂度
采用均摊分析的方法,公式如下:
公式里面,假定有 n 个元素,倍增因子为 m(我们设定为2),那么完成这 n 个元素往一个 vector 中的push_back操作,需要重新分配内存的次数大约为 log2(n),第 i 次重新分配将会导致复制 2^i (也就是当前的vector.size() 大小)个旧空间中元素,因此 n 次 push_back 操作所花费的总时间:
然后 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