八股文
1.哪些情况会引发段错误?如何调试段错误的?
引发段错误:
- 1.对空指针解引用
- 2.下标越界
- 3.访问只读内存
- 4.栈溢出,开辟的局部数组较大/递归调用的层次过多
2.哪些多态技术?
- 1.编译期多态:函数模板、函数/运算符重载
- 1.1 函数重载: 函数重载底层是通过name mangling实现,通过符号表可以看到,_Z3addii中Z为规定,3为函数名字符个数,随后是函数名和参数名,所以只有返回类型会报重定义错误。
- 1.2 函数模板:其实不是函数,模板的实例化是在编译期,根据传入的实参类型,推导生成对应类型的函数。作用域仅限当前文件。通过<>进行函数模板的特例化。
- 2.运行时多态:虚函数
3.printf如何实现变参的?
4.ping 127.0.0.1 、0.0.0.0和 localhost有什么区别
流程:首先会根据ip查询路由表,如果是外网ip走网卡发送,本机地址发送到接收缓存。
- 1.linux下:三者毫无区别
- 2.windows下:ping 127.0.0.1可以通, ping 0.0.0.0 常见故障, ping localhost,能通,但是为来自::1的回复
5.智能指针三个作用 :
- 1.避免内存泄漏
- 2.避免野指针
- 3.避免多次释放同一片内存
6.如何改造UDP为可靠传输?
- 1.SEQ机制,方便数据重排为正确的顺序
- 2.ACK机制,可以获取tcp段的到达情况,决定是否重传和重传哪些tcp段
- 3.重传机制,对缺失的数据进行重传
- 4.重排机制,按序号重排
- 5.滑动窗口,进行流量控制
- 6.定时器机制
- 7.拥塞避免机制
7.vector的扩容原理?
在vector初始化时,分配对应的固定大小的capacity,不管奇数偶数,两倍扩容(gcc)
*resize和size与capacity的关系: 一定改变size,但是未抹除数据,不一定改变capacity。
扩容因子为什么是1.5或者2? 1.5倍空间回收问题,可以回收前面的不连续空间来为第k次分配
msvc两倍扩容,gcc一点五倍扩容,不再使用2以上是避免空间浪费
8. map和unordered_map的区别?
unordered_map底层使用hashtable,hashtable中每一个键会被hasher哈希到一个bucket上,一个bucket就是一个链表(开链法);
map底层使用红黑树,如果emplace已有的键,不会改变原来的键值
9. 哈希法解决碰撞冲突的方式?
开放定址法、链地址法、再哈希、公共缓存区
10. 拉链法可能会被退化成O(n)的查找效率,如何优化?
在链表长度大于一定值的时候,比如64时,将其转化为红黑树,这也是stl底层的实现
11.自定义的数据结构如何在map中使用?除了重载,还有其他方法实现吗?
- 1.重载 < 运算符
- 2.类外写比较器或者写仿函数,类内labmda函数
12. new和malloc的区别?如果混用会如何?malloc(0)返回什么?
区别:new是运算符,malloc是库函数;返回值不一样,malloc返回的是void类型指针
可以混用: new与malloc混用,malloc不会调用构造函数,也不会进行类中成员初始化,只是开辟了一片指定大小的空间;
但是可以手动调用构造函数和析构函数来达成混用,流程如下:
int main()
{
A *pa = (A*)malloc(sizeof(A)); //malloc
pa = new(pa)A(); //placement new
cout << pa->m_a; //成员已初始化
pa->~A(); //析构
free(pa); //释放
}
malloc(0)返回一个非空的地址。
14.如何只在堆上创建对象?
将析构函数设置为私有函数,因为栈为编译器自动管理,需要在释放时调用析构函数。而析构函数被设置为私有函数后,就无法在栈上创建对象了。
15.在使用new运算符的情况下,也只在栈上创建对象?
使用alloca分配栈内存,再使用placement new在栈上分配内存。
16.介绍一下同步和互斥的概念?
同步:合作进程/线程之间互相通信,互相等待的过程
互斥:同一时刻只允许一个进程访问临界区的临界资源
17.只使用互斥锁不可以实现同步吗?
可以
18.写一段线程死锁的代码
void printLock(){
m.lock();
cout <<"子线程拿到锁!"<<endl;
}
int main()
{
m.lock();
thread t1(printLock);
t1.join();
m.unlock();
cout <<"父线程结束!"<<endl;
}
19.多线程和多进程开发的区别?
- 1.资源共享和同步,线程在共享方面有优势,进程在同步方面 ?为什么有优势?
- 2.资源开销:包括 创建/调度/结束
- 3.可靠性:进程之间互不影响;线程挂掉会导致同组线程一起挂掉
20.进程调度算法?
绝对公平算法CFS(completely fair scheduler):
- 1.引入nice值对应的权重,[-20,19]权重数组
- 2.虚拟运行时间 = 实际运行时间 * nice0权重/进程权重
- 3.使用红黑树将虚拟运行时间排序,每次取最小
- 4.最小粒度的说明:如果进程剩余时间片比最小粒度大,保证至少运行最小粒度后才会被抢占;如果进程剩余时间片比最小粒度小,那么不保证能运行最小粒度的时间,而是在时间片完后切换。
- 5.何为公平? 在一个调度周期内,所有进程的虚拟运行时间都相同。
调度周期:将所有进程都运行一遍所需要的最小时间
调度周期 = 进程数 ∗ 最小粒度 调度周期 = 进程数 * 最小粒度 调度周期=进程数∗最小粒度
进程的虚拟执行时间 = 进程实际执行时间 ∗ n i c e 0 的权重 进程的权重 进程的虚拟执行时间 = 进程实际执行时间 * \frac{nice0的权重}{进程的权重} 进程的虚拟执行时间=进程实际执行时间∗进程的权重nice0的权重
一个调度周期内: 每个进程的实际执行时间 = 调度周期 ∗ 进程的权重 所有进程的总权重 每个进程的实际执行时间 = 调度周期 * \frac{进程的权重}{所有进程的总权重} 每个进程的实际执行时间=调度周期∗所有进程的总权重进程的权重
一个调度周期内: 进程的虚拟执行时间 = 调度周期 ∗ 进程的权重 所有进程的总权重 n i c e 0 的权重 进程的权重 = 调度周期 ∗ n i c e 0 的权重 所有进程的总权重 进程的虚拟执行时间 = 调度周期* \frac{进程的权重}{所有进程的总权重} \frac{nice0的权重}{进程的权重} = 调度周期 *\frac{nice0的权重}{所有进程的总权重} 进程的虚拟执行时间=调度周期∗所有进程的总权重进程的权重进程的权重nice0的权重=调度周期∗所有进程的总权重nice0的权重
可以看到一个周期内的虚拟运行时间与进程无关,但是进程实际运行时间依据进程的权重分配。
21.在leetcode上遇到的坑
- 1.auto类型无法推导引用,即使返回值是引用
- 2.使用emplace_back原地构造,结果在最后插入了N个数 emplace_back(cur[left],cur[right]);
- 3.自定义排序不是严格意义上的弱排序,导致空指针错误。
22.红黑树的特点
红黑树可以看做是AVL平衡二叉树的改进版,但是它不是严格的二叉平衡树,只是大体上的一个平衡。(为什么?)它有5个特点:
-
- 一个节点要么是红色,要么是黑色
-
- 根节点为黑色
-
- 叶子节点为黑色
-
- 如果一个节点是红色,那么它的子节点是黑色
-
- 一个节点的所有子孙节点到这个节点的所有路径上黑色节点的数量一样,这里路径是到Nil节点
最重要的三个操作:变色,左旋,右旋
变色:如果当前节点的父节点和叔节点为红色,那么其爷节点肯定为黑色,父节点和叔节点黑化,爷节点变为红色,检查爷节点是否满足
左旋:父红叔黑,当前为右子树,仅旋转
右旋:父红叔黑,当前为左子树,父变爷变
23.关键字总结
1.decltype
decltype仅仅是分析表达式,不会实际计算,不用担心会调用函数;decltype自定义比较函数中的作用,自定义比较函数的第三个参数是类名或者说类型,而labmda函数实际是一个function类的实例,所以需要获取cmp的类型,最后用cmp作为优先级队列的模板参数。
另问:为什么优先级队列中比较不要括号而sort中就需要括号?
因为在优先级队列中需要的是模板参数,在sort函数中需要的是类实例。
priority_queue<myPair,vector<myPair>,decltype(cmp2)> q(cmp2);
2.final
final关键字用于类名后或者函数名后,表示不能被继承或重写
3.extern
extern 存储类型说明符,extern C 用C风格编译,函数无法重载,因为同名函数在符号表中命名相同
4.volatile
volatile修饰变量和成员函数。
修饰可能被意外改变的变量,要求从内存中重新取出,而不要使用寄存器的备份值进行优化。因为有可能外部硬件在读取变量的值,如根据变量停留的时间做出相应的反应。
volatile修饰成员函数,非volatile对象仍可访问该成员函数,但是volatile对象只能访问volatile成员函数。
5.const
const修饰变量和成员函数。
const同理,非const对象可以访问const函数,但是const对象只能访问const成员函数。
一个变量可以既是const又是volatile吗?
可以,只读状态存储器,const是希望不要去修改,volatile是有可能被意外改变。
指针可以是volatile吗?
可以,中断服务子程序可能更改指向buffer的地址。
6.mutable
mutable修饰成员变量的话,即使是在const函数中,也能修改此变量。
7.explicit
修饰的构造函数无法通过隐式调用。
8.virtual:
1.构造函数可以为虚函数吗?
构造函数只能使用两个关键字修饰,一个是inline(不建议),一个是explicit。
2.析构函数可以为虚函数吗?
如果会被继承,设置为虚函数;如果不会被继承,不需要设为虚函数,而这种情况又可以加上final关键字。因为如果在栈中声明派生类对象,会有父子子父的构造析构顺序,但是如果是通过基类指针声明(往往是这种情况),申请堆内存,如果基类析构函数不是虚函数,那么派生类的析构函数不会自动被调用,有可能造成内存泄露。C++指导原则之一是不为不使用的特性付出代价。所以确定不会被继承,则无需虚析构函数。因为声明为虚函数会做许多额外的工作,比如生成虚表指针,指向虚函数,不止是内存占用,在运行时针对虚函数也会有额外的动作。
3.析构函数和构造函数可以调用虚函数吗?
可以,但是不建议,因为这样会失去虚函数的多态性。因为基类和派生类的构造析构是具有顺序的。
4.静态成员函数可以为虚函数吗?
虚函数实际上通过vptr指针来访问,在每一个对象中都有vptr,即vptr与this指针是关联的,通过this指针才能访问到vptr,而静态成员函数没有this指针。
5.派生类中的同名函数是虚函数吗?
是的,就算没有声明virtual,只要和基类同名同参,会自动被声明为虚函数。
9.inline:
虚函数可以内联吗?
可以,但为通过对象被调用时才会生效,因为内联优化是在编译期,使用指针调用时编译器无法在编译期确定具体是哪个函数。
构造函数可以内联吗?
可以,但是不建议,因为代码量不止看起来这么少。
析构函数可以内联吗?
可以,但是不建议,还会调用基类的析构函数,释放内存,代码量不止看起来这么少。
24.类型转换符
-
- const_cast<> 将const指针转换为非const指针
-
- dynamic_cast<> 进行运行时检查,需要被转换的指针具有虚函数表,会检查指针的实际指向的对象,如果是下行转换会返回nullptr(将指向基类的指针转换为指向派生类的指针时)
class B{
public:
virtual void myPrint(){
cout << "hello"<<endl;
}
};
class D:public B{
public:
virtual void myPrint2(){
cout << "world"<<endl;
}
};
int main()
{
B* pb = new D();
//B* pb = new B(); 如果使用该语句,pd1经过dynamic_cast转换后为nullptr
D* pd1 = dynamic_cast<D*> (pb);
if(pd1 == nullptr)
cout << "nullptr" <<endl;
pd1->myPrint2();
}
- reinterpret_cast<> 强制转换
- static_cast<> 所有可以用隐式转换的地方,仅在编译期检查