目录
6. 对虚函数机制的理解,单继承、多继承、虚继承条件下虚函数表的结构
15. volatile 关键字的作用?什么时候需要使用volatile 关键字
17. 指针和引用有什么区别?什么情况下用指针,什么情况下用引用?
18. 一般什么情况下会出现内存泄漏?怎么用C++在编码层面尽量避免内存泄漏。
24. malloc出来20字节内存,为什么free不需要传入20呢,不会产生内存泄漏吗?
25. new[]和delete[]一定要配对使用吗?new[]和delete[]为何要配对使用?
2. 谈一谈你对左值和右值的了解,了解左值引用和右值引用吗?
3. 了解移动语义和完美转发吗?(这个大家自己百度吧,有点复杂)
5. 平时会用到function、bind、lambda 吗,都什么场景下会用到?
6. 对C++11 的mutex 和RAII lock 有过了解吗?
7. 对C++11 的智能指针了解多少,可以自己实现一个智能指针吗?
1. C++直接使用数组好还是使用std::array 好?std::array 是怎么实现的?
2. std::vector 最大的特点是什么?它的内部是怎么实现的?resize 和reserve 的区别是什么?clear 是怎么实现的?
3. deque 的底层数据结构是什么?它的内部是怎么实现的?
4. map 和unordered_map 有什么区别?分别在什么场景下使用?
5. list 的使用场景?std::find 可以传入list 对应的迭代器吗?
1.Qt基础知识
1. Qt信号槽机制的优势和不足
优点:类型安全,松散耦合。缺点:同回调函数相比,运行速度较慢。
优点:
- 类型安全:需要关联的信号槽的签名必须是等同的。即信号的参数类型和参数个数同接受该信号的槽的参数类型和参数个数相同。若信号和槽签名不一致,则编译器会报错。
- 松散耦合:信号和槽机制减弱了Qt对象的耦合度。激发信号的Qt对象无需知道是哪个对象的那个槽接收它发出的信号,它只需要在适当的时间发送适当的信号即可,而不需要关心是否被接收和哪个对象接收了。Qt保证适当的槽得到了调用,即使关联的对象在运行时删除,程序也不会崩溃。
- 灵活性:一个信号可以关联多个槽,多个信号也可以关联同一个槽。
缺点:
- 速度较慢:与回调函数相比,信号和槽机制运行速度比直接调用非虚函数慢10倍左右。
原因:
- 需要定位接收信号的对象。
- 安全地遍历所有槽。
- 编组,解组传递参数。
- 多线程的时候,信号需要排队等候。(然而,与创建对象的new操作及删除对象的delete操作相比,信号和槽的运行代价只是他们很少的一部分。信号和槽机制导致的这点性能损失,对于实时应用程序是可以忽略的。)
2. static和const的使用
static:静态变量声明,分为局部静态变量,全局静态变量,类静态成员变量。也可修饰类成员函数。有以下几类:
- 局部静态变量:存储在静态存储区,程序运行期间只被初始化一次,作用域仍然为局部作用域,在变量定义的函数或语句块中有效,程序结束时由操作系统回收资源。
- 全局静态变量:存储在静态存储区,静态存储区中的资源在程序运行期间会一直存在,直到程序结束由系统回收。未初始化的变量会默认为0,作用域在声明他的文件中有效。
- 类静态成员变量:被类的所有对象共享,包括子对象。必须在类外初始化,不可以在构造函数内进行初始化。
- 类静态成员函数:所有对象共享该函数,不含this指针,不可使用类中非静态成员。
const:常量声明,类常成员函数声明。 const和static不可同时修饰类成员函数,const修饰成员函数表示不能修改对象的状态,static修饰成员函数表示该函数属于类,不属于对象,二者相互矛盾。const修饰变量时表示变量不可修改,修饰成员函数表示不可修改任意成员变量。
3. 指针常量、常量指针,常指针常量
int main()
{
int x = 10;
int y = 20;
// 指针常量(常指针):数据类型 * const 指针 = 变量
int* const p1 = &x; // 指针常量,p1不可更改
// p1 = &y; 错误
*p1 = 30; // 正确,变量的值可以更改
cout << "x: " << x << endl; // x: 30
// 常量指针:const 数据类型 * 指针 = 变量
// 或:数据类型 cosnt * 指针 = 变量
const int* p2 = &x; // 常量指针,*p2不可变
// *p2 = y; 错误
p2 = p1; // 正确,p2可变
// 常指针常量:const 数据类型 * cosnt 指针 = 变量
const int * const p3 = &x; // p3和*p3均不可更改
// p3 = p2; 错误
// *p3 = 20; 错误
}
4. 指针和引用的异同
指针:是一个变量,但是这个变量存储的是另一个变量的地址,我们可以通过访问这个地址来修改变量。
引用:是一个别名,还是变量本身。对引用进行的任何操作就是对变量本身进行的操作。
相同点:二者都可以对变量进行修改。
不同点:指针可以不必须初始化,引用必须初始化。指针可以有多级,但是引用只有一级(int&& a不合法, int** p合法)。指针在初始化后可以改变,引用不能进行改变,即无法再对另一个同类型对象进行引用。sizeof指针可以得到指针本身大小,sizeof引用得到的是变量本身大小。指针传参还是值传递,引用传参传的是变量本身。
5. 如何理解多态
定义:同一操作作用于不同的对象,产生不同的执行结果。C++多态意味着当调用虚成员函数时,会根据调用类型对象的实际类型执行不同的操作。
实现:通过虚函数实现,用virtual声明的成员函数就是虚函数,允许子类重写。声明基类的指针或者引用指向不同的子类对象,调用相应的虚函数,可以根据指针或引用指向的子类的不同从而执行不同的操作。
Overload(重载):函数名相同,参数类型或顺序不同的函数构成重载。
Override(重写):派生类覆盖基类用virtual声明的成员函数。
Overwrite(隐藏):派生类的函数屏蔽了与其同名的基类函数。派生类的函数与基类函数同名,但是参数不同,隐藏基类函数。如果参数相同,但是基类没有virtual关键字,基类函数将被隐藏。
6. 虚函数表
多态是由虚函数实现的,而虚函数主要是通过虚函数表实现的。如果一个类中包含虚函数,那么这个类就会包含一张虚函数表,虚函数表存储的每一项是一个虚函数的地址。该类的每个对象都会包含一个虚指针(虚指针存在于对象实例地址的最前面,保证虚函数表有最高的性能),需指针指向虚函数表。注意:对象不包含虚函数表,只有需指针,类才包含虚函数表,派生类会生成一个兼容基类的虚函数表。
7. 常用数据结构
- vector:向量,连续存储,可随机访问。
- deque:双向队列,连续存储,随机访问。
- list:链表,内存不连续,不支持随机访问。
- stack:栈,不可随机访问,只允许再开头增加/删除元素。
- queue:单向队列,尾部增加,开头删除。
- set:集合,采用红黑树实现,可随机访问。查找、插入、删除时间复杂度为O(logn)。
- map:图,采用红黑树实现,可随机访问。查找、插入、删除时间复杂度为O(logn)。
- hash_set:哈希表,随机访问。查找、插入、删除时间复杂读为O(1)。
8.Tcp
1.三次握手:建立一个TCP连接时,需要客户端服务端总共发送三个包以确认连接的建立。在这一过程中由客户端执行connect来触发,流程如下:
2.四次挥手:断开一个Tcp连接时,需要客户端和服务端总共发送四个包以确认连接的端口。在socket编程中,这一过程由客户端或服务端任一方执行close来触发,流程如下:
2. C++ 基础
1. C语言和C++有什么区别?
C语言是面向过程的,抽象化的通用设计语言,主要用于底层开发,C++是C的超集,继承并扩展了C语言,C++即可以进行C语言的过程化程序设计,又可以进行以面向对象为主要特点的程序设计。
2. struct 和class 有什么区别?
- struct的成员默认是public属性的,class的成员默认是private属性的
- struct继承默认是public属性的,class继承默认是private属性的
- struct不可以使用模板,class可以
3. extern "C"的作用?
extern "C" 的主要作用就是为了能够正确实现C++代码调用其它C语言代码。加上extern "C"后,会提示编译器这部分代码按C语言(而不是C++)的方式进行编译。
由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名(而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包含函数名)。也就是说,C++和C对生成的函数名称的处理是不易的,extern "C" 的目的就是主要实现C和C++的相互调用问题。
4. 函数重载和覆盖有什么区别?
- 函数重载发生在同一个类的内部。在一个类中定义了一组函数,这组函数具有相同的函数名,但它们的参数列表却各不相同。在函数调用过程中根据传入的参数类型,匹配最佳函数调用。
- 函数覆盖发生在子类和父类之间。在父类中定义了一个虚函数,在子类中重新实现这个函数,函数在子类和父类中具有相同的函数名和参数列表,它们的函数原型是相同的。在函数调用过程中国根据对象的类型调用相应类中的函数。
- 函数重载是同一类中的不同方法,函数覆盖是不同类中的同一方法。
- 重载函数的参数列表不同,覆盖函数的参数列表相同。
- 重载函数调用时根据参数类型选择方法,覆盖函数调用时根据对象类型选择方法。
5. 谈一谈你对多态的理解,运行时多态的实现原理是什么?
多态:就是多种形态,C++的多态分为静态多态和动态多态。静态多态就是重载,因为在编译器决议确定,所以成为静态多态。在编译时就可以确定函数地址。动态多态即运行时多态是通过继承重写基类的虚函数实现的多态,因为在运行时决议确定,所以称为动态多态,也叫运行时多态。运行时在虚函数表中寻找调用函数的地址。在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是子类,就调用子类的函数。如果对象类型是父类,就调用父类的函数,(即指向父类调父类,指向子类调子类)此为多态的表现。
运行时多态实现原理:
- 用 virtual 关键字声明的函数叫虚函数,虚函数肯定是类的成员函数。
- 存在虚函数的类都有一个一维的虚函数表叫做虚表。当类中声明虚函数时,编译器会在类中生成一个虚函数表。
- 类的对象有一个指向虚表开始的虚指针。虚表是和类对应的,虚表指针是和对象对应的。
- 虚函数表是一个存储类成员函数指针的数据结构。
- 虚函数表是由编译器自动生成和维护的。
- virtual 声明的成员函数会被编译器放入虚函数表中。
- 当存在虚函数时,每个对象都有一个指向虚函数的指针(C++编译器给父类对象,子类对象提前布局vptr指针),当执行虚函数时,C++编译器不需要区分子类或父类对象,只需在基类之恨或引用中找到vptr指针即可。
- vptr一般作为类对象的第一个成员。
6. 对虚函数机制的理解,单继承、多继承、虚继承条件下虚函数表的结构
虚函数就是类中用virtual声明的成员函数,利用虚函数可以实现运行时多态。 带虚函数的对象布局如下:
单继承:将基类虚表中的内容拷贝一份到子类虚表中,如果派生类重写了基类某个虚函数,就用派生类自己的虚函数替换掉原先基类虚函数的入口地址。对象布局如下:
多继承:多重继承会有多个虚函数表,几重继承就会有几个虚函数表。这些表按照派生的顺序一次排列,如果子类改写了父类的虚函数,那么就会用子类自己的虚函数覆盖虚函数表相应的位置,如果子类有新的虚函数,那么就添加到第一个虚函数表的末尾。对象布局如下:
虚继承:对象布局和普通继承不同,普通继承下子类和基类公用一个虚表地址,而在虚继承下,子类和虚基类分别有一个虚表地址的指针。对象布局如下:
7. 如果虚函数是有效的,那为什么不把所有函数设为虚函数?
虚函数是有代价的,由于每个虚函数的对象都要维护一个虚函数表,因此在使用虚函数的时候会产生一定的系统开销,这是没有必要的。另外,虚函数的调用相对于普通函数要更慢一些,因此每次都要查找虚函数表,有一定的时间开销。
8. 为什么要虚继承?
如图:
非虚继承时,显然D会继承两次A ,内部就会存储两份A的数据,浪费空间,而且还会有二义性,D调用A的方法时,由于有两个A,究竟调用哪个A的方法呢,编译器也不知道,就会报错,所以有了虚继承,解决了空间浪费以及二义性问题。在虚继承下,只有一个共享的基类对象被继承,而无论该基类在派生层次中出现多少次。共享的基类子对象被称为虚基类,在虚继承下,基类对象的复制以及由此引起的二义性被消除了。
9. 构造函数可以是虚函数吗?析构函数可以是虚函数吗?
构造函数不能是虚函数,析构函数可以是虚函数。理由如下:构造函数就是为了在编译阶段确定对象的类型以及为对象分配空间,如果类中有虚函数,那就会在构造函数中初始化虚函数表,虚函数的执行缺需要依赖虚函数表。如果构造函数是虚函数,那么它就需要依赖虚函数表才可执行,而只有在构造函数中才会初始化虚函数表,鸡生蛋蛋生鸡的问题,很矛盾,所以构造函数不能是虚函数。而析构函数不存在这个问题,所以析构函数可以是虚函数。
10. 为什么虚函数表中有两个析构函数?
这是因为对象有两种构造方式,栈构造和堆构造,所以在对应的实现上,对象也有两种析构方式,其中堆上对象的析构和栈上对象的析构不同之处在于,栈内存的析构不需要执行delete函数,会自动被回收。
11. 为什么基类析构函数要是虚函数?
一般基类的析构函数都要设置成虚函数,因为如果不设置成虚函数,咋析构的过程中只会调用到基类的析构函数而不会调用到子类的析构函数,可能会产生内存泄漏。
12. 什么场景需要用到纯虚函数?纯虚函数的作用是什么?
在许多情况下,在基类中不能对虚函数给出有意义的的实现,而把它定义为纯虚函数,它的实现留给派生类去做。作用:1.为了方便使用多态特性。2.在很多情况下,基类本身生成对象时不合情理的。
13. 了解RAII 吗?介绍一下?
RAII: Resource Acquisition Is Initialization,资源获取即初始化,将资源的生命周期与一个对象的生命周期绑定,举例来说就是,把一些资源封装在类中,在构造函数中请求资源,在析构函数中释放资源且绝不抛出异常,而一个对象在生命周期结束时会自动调用析构函数,即资源的生命周期和一个对象的生命周期绑定。
14. 类的大小怎么计算?
空类的大小是一个特殊情况,空类的大小为1。sizeof计算。
15. volatile 关键字的作用?什么时候需要使用volatile 关键字
volatile关键字告诉编译器其修饰的变量是易变的,它会确保修饰的变量每次读操作都从内存里读取,每次写操作都将值写到内存里。volatile关键字就是给编译器做个提示,告诉编译器不要对修饰的变量做过度的优化,提示编译器该变量的值可能会以其它形式被改变。
volatile用于读写操作不可以被优化掉的内存,用于特种内存中。
16. 了解各种强制类型转换的原理及使用?
- 强制类型转换:C语言风格的类型转换,其实就是对内存的不同解析方式。高效便捷,但是没有编译时的类型检查,有可能在调用时出错。
- static_cast:带安全检查的类型转换,用于替代强制类型转换,此转换会在编译时进行类型检查,而强制转换不会。
- dynamic_cast:用于类层次间的上行转换和下行转换(基类必须有虚表)。除了在编译器进行类型安全检查,dynamic_cast还会在运行时进行类型检查。从子类转换为父类时,dynamic_cast和static_cast是一致的,而父类转换为子类的过程中,static_cast是非类型安全的,而dynamic_cast会在转换时做类型安全检查,若不能转换返回Null或抛出异常(在引用类型转换失败时抛出异常)。
- const_cast:仅是将const类型的参数转换为非const型,不做其它转换操作,其它类型转换都不能改变参数的const属性。
- reinterpret_cast:用于进行各种不同类型的指针之间,不同类型的引用之间以及指针和能容纳指针的整数类型之间的转换,转换不检查安全性。其实reinterpret_cast和普通强制类型转换没多大区别,也不是类型安全的,用reinterpret_cast的好处是:(1)将强制类型转换标准化,代码看着顺眼,而且能快速查找到强制类型转换。(2)不改变参数const属性。
17. 指针和引用有什么区别?什么情况下用指针,什么情况下用引用?
区别:
- 指针是一个变量,只不过这个变量存储的是一个地址,指向内存的一个存储单元,即指针是一个实体;而引用跟原来的变量实质上是同一个东西,只不过是原变量的一个别名而已。
- 有const指针,但是没有const引用。
- 指针可以有多级,但是引用只能是一级(int** p;合法,而int&& a;不合法)。
- 指针的值可以为空,但是引用的值不可以,并且引用在定义的时候必须初始化。
- 指针的值在初始化后可以改变,即指向其它的存储单元,而引用在初始化后就不会再改变了,从一而终。
- sizeof引用得到的是所指向的变量(对象)的大小,而sizeof指针得到的是指针本身的大小。
- 指针和引用的自增++运算意义不一样。
相同点:
- 都可以对变量就行修改。
- 都是地址的概念,指针指向一块内存,它的内容是所指内存的地址,引用是某块内存的别名。
何时使用:
- 当考虑到存在不指向任何对象的可能,这时候应该使用指针。
- 当需要在能够在不同的时刻指向不同的对象,这个时候使用指针。如果总是指向一个对象并且一旦指向一个对象后就不会改变指向,那么应该使用引用。
- 当重载某个操作符时,应该使用引用。
18. 一般什么情况下会出现内存泄漏?怎么用C++在编码层面尽量避免内存泄漏。
内存泄漏是指程序向系统申请分配内存使用(new),用完以后却没有归还(delete)。结果申请的那块内存程序不再使用,而系统也无法再讲它分配给需要的程序。
造成内存泄漏的几种情况:
- 指针重新赋值
- 错误的内存释放
- 返回值的不正确处理
- new和delete没有配对使用。
如何避免内存泄漏:
- 确保没有访问空指针。
- 尽量使用智能指针。
- new和delete配对使用。
19. unique_ptr 如何转换所有权?
利用std::move,例如:
std::unique_ptr<int> uniquePtr = std::make_unique<int>(10);
// std::shared_ptr<int> sharedPtr = std::move(unique);
std::unique_ptr<int> uniquePtr2 = std::move(unique);
20. 谈一谈你对面向对象的理解
- C++面向对象编程就是把一切事物都变成一个个对象,用属性和方法来描述对象的信息,比如定义一个猫对象,猫的眼睛、毛发、嘴巴就可以定义为猫对象的属性,猫的叫声和走路就可以定义为猫对象的方法。
- 用对象的方式编程,不仅方便了程序员,也使得代码的可复用性、可维护性变好。
- C++面向对象的三大特性是封装、继承、多态。
21. 什么场景下使用继承方式,什么场景下使用组合?
继承:通过扩展已有的类来获得新功能的代码重用方法
组合:新类由现有类的对象合并而成的类的构造方式
使用场景:
- 如果二者存在一个“是”的关系,并且一个类要对另外一个类公开所有接口,那么继承是更好的选择
- 如果二者间存在一个“有”的关系,那么首选组合
- 如果没有找到及其强烈无法辩驳的使用继承的的理由的时候,一律使用组合。组合体现为现实层面,继承主要体现在扩展方面。如果并不需要一个类的所有东西(包括接口和熟悉),那么就不需要使用继承,使用组合更好。如果使用继承,那么必须所有的都继承,如果有的东西你不需要继承但是你继承了,那么这就是滥用继承。
22. new 和malloc 有什么区别?
- new和delete是C++的关键字/运算符,malloc和free是C++/C语言的标准函数。
- malloc需要显示地指定分配的内存大小,new不需要。
- new操作符从自由存储区上为对象动态分配内存空间,而malloc函数从堆上分配内存。注:凡是通过new操作符进行内存申请,该内存即为自由存储区。
- new操作符内存分配成功时,返回对象类型,无需进行类型转换,故new是符合类型安全性的操作符,malloc返回void*,需要通过强制类型转换将void*指针转换成我们需要的类型。
- new操作符内存分配失败时,抛出bad_alloc异常,malloc内存分配失败时返回NULL。
- new操作符有构造函数和析构函数,在开辟空间的同时,会调用自定义对象的构造函数来完成初始化,malloc只会开辟空间。
- malloc分配空间后,可以通过realloc扩张内存,new操作符则不能进行再次扩张内存的操作。
- new相对malloc效率要低,因为new的底层封装了malloc。
23. malloc的内存可以用delete释放吗?
可以,但是一般不这么用。malloc和free是C语言中的函数,C++为了兼容C语言保留下来这一对函数。简单来说,new可以理解为,先执行malloc来申请内存,后调用构造函数来初始化对象,delete是先执行析构函数,后使用free来释放内存。若先new再使用free来释放空间的话,可能会出现一些错误。而先使用malloc,再使用delete的话没有问题。
24. malloc出来20字节内存,为什么free不需要传入20呢,不会产生内存泄漏吗?
因为不能保证程序员使用free时传入的参数是和malloc一致的,从而导致内存泄漏等问题。现在free的解决方式是让free函数自己确定要释放多少内存,可以使用的方式是在申请内存时多申请一些空间来存储内存大小,在free时再获取这个大小进行释放。
25. new[]和delete[]一定要配对使用吗?new[]和delete[]为何要配对使用?
1.不一定,当类型为int、float等内置类型时,可以不配对使用,但是建议还是配对使用。
2.new[]为一个数组申请内存时,编译器还会悄悄地在内存中保存一个整数,用来表示数组中元素的个数。因为在delete一块内存时,我们不仅要知道指针指向多大的内存,更重要的是要知道指针指向的数组中对象的个数。因为只有知道了对象数量才能一一调用它们的析构函数,完成对数组中所有对象的清理。如果使用的是delete,则编译器只会将指针所指的对象当作单个对象来处理。所以对于数组,需要使用delete[]来处理,符号[]会告诉编译器在delete这块内存时,先去获取保存的那个元素数量值,然后再进行一一清理。
3. C++11
1. 了解auto 和decltype 吗?
auto:可以让编译器在编译时就推导出变量的类型,代码如下:
auto a = 10; // 10是int型,自动推导出a是int
int i = 10;
auto b = i; // b是int
auto d = 2.0; // d是double
decltype:用于推导表达式类型,代码如下:
int func() { return 0; };
decltype(func()) i; // i是int
int x = 0;
decltype(x) y; // y是int
decltype(x + y) z; // z是int
2. 谈一谈你对左值和右值的了解,了解左值引用和右值引用吗?
左值:在内存中有确定存储地址、有变量名、表达式结束依然存在的值。
左值引用:绑定到左值的引用,通过&来获得左值引用。
右值:在内存中没有确定存储位置、没有变量名,表达式结束就会销毁的值。
右值引用:绑定到右值的引用,通过&&来获得右值引用。
int a1 = 10; // 非常量左值
const int a2 = 10; // 常量左值
int& b1 = a1; // 非常量左值引用
const int& b2 = a2; // 常量左值引用
int&& c1 = 10; // 非常量右值引用
const int&& c2 = 10; // 常量右值引用,10是非常量右值
3. 了解移动语义和完美转发吗?(这个大家自己百度吧,有点复杂)
移动语义:可以理解为转移所有权,拷贝是对于别人的资源,自己重新分配一块内存存储复制过来的资源,而对于移动语义,类似于转让或者资源窃取的意思,对于那块资源,转为自己所拥有,别人不再拥有也不会再使用,通过C++11新增的移动语义可以省去很多拷贝负担,如何利用移动语义,主要通过移动构造函数。
完美转发:指可以写一个接受任意实参的函数模板,并转发到其它函数,目标函数会收到与转发函数完全相同的实参。转发函数实参是左值那目标函数实参也是左值,转发函数实参是右值那目标函数也是右值。
4. 了解列表初始化吗?
列表初始化:可以直接在变量名后面加上初始化列表来进行对象的初始化。
5. 平时会用到function、bind、lambda 吗,都什么场景下会用到?
std::function:是可调用对象的封装器,可以看做是一个函数对象,用于表示函数这个抽象概念。std::function的实例可以存储、复制和调用任何可调用对象,存储的可调用对象称为std::function的目标,若std::function不含目标,则称它为空,调用空的std::function的目标会抛出std::bad_function_call异常。
#include <functional>
#include <iostream>
using namespace std;
struct Foo
{
Foo(int num) : num_(num) {}
void print_add(int i) const { cout << num_ + i << endl; }
int num_;
};
void print_num(int i) { cout << i << "\n"; }
struct PrintNum
{
void operator()(int i) const { cout << i << "\n"; }
};
int main()
{
// 存储自由函数
function<void(int)> f1 = print_num;
f1(10);
// 存储 lambda
function<void()> f2 = []() { print_num(20); };
f2();
// 存储 std::bind 的返回值
function<void()> f3 = bind(print_num, 30);
f3();
// 存储成员函数的调用
function<void(const Foo&, int)> f4 = &Foo::print_add;
const Foo foo(10);
f4(foo, 30);
f4(10, 30);
// 存储数据成员
function<int(const Foo&)> f5 = &Foo::num_;
cout << f5(Foo(50)) << "\n";
// 存储成员函数及对象的调用
function<void(int)> f6 = bind(&Foo::print_add, foo, placeholders::_1);
f6(50);
// 存储成员函数和对象指针的调用
function<void(int)> f7 = bind(&Foo::print_add, &foo, placeholders::_1);
f7(60);
// 存储函数对象的调用
function<void(int)> f8 = PrintNum();
f8(80);
return 0;
}
std::bind:使用std::bind可以将可调用对象和参数一起绑定,绑定后的结果使用std::function进行保存,并延迟调用到任何我们需要的时候。
lambda表达式:定义了一个匿名函数,可以捕获一定范围的变量在函数内部使用。
6. 对C++11 的mutex 和RAII lock 有过了解吗?
mutex:互斥量,是一种线程同步的手段,用于操作多线程同时读写的共享数据。
RAII lock:可以动态的释放锁资源,防止线程由于编码失误导致一直持有锁。
7. 对C++11 的智能指针了解多少,可以自己实现一个智能指针吗?
三种智能指针:
std::shared_ptr:使用引用计数,每一个shared_ptr的拷贝都指向相同的内存,每次拷贝都会触发引用计数+1,每次生命周期结束析构的时候引用计数-1,在最后一个shared_ptr析构的时候,内存才会释放。
std::weak_ptr:用来监视shared_ptr的生命周期,它不管理shared_ptr内部的指针,它的拷贝析构都不会影响引用计数,纯粹是作为一个旁观者监视shared_ptr中管理的资源是否存在,可以用来返回this指针和解决循环引用问题。
std::unique_ptr:独占型的智能指针,它不允许其它智能指针共享其内部指针,也不允许unique_ptr的拷贝和赋值。
8. enum 和enum class 有什么区别?
- enum class 是类型安全的
- enum class 定义被限制在枚举作用域内,不能隐式转换为整数类型,但是可以强制转换为整数类型。
- 使用enum class定义的枚举必须带作用域名。
4. STL
1. C++直接使用数组好还是使用std::array 好?std::array 是怎么实现的?
std::array好。std::array除了有传统数组支持随机访问、效率高、存储大小固定等特点外,还支持迭代器访问、获取容量、获得原始指针等高级功能。而且它还不会退化成指针T*给开发人员造成困惑。
实现原型如下:
namespace std
{
template <typename T, size_t N>
class array;
}
2. std::vector 最大的特点是什么?它的内部是怎么实现的?resize 和reserve 的区别是什么?clear 是怎么实现的?
特点:
- 指定一块如同数组一样的连续存储,但空间可以扩展。即它可以像数组一样操作,并且可以进行动态操作。通常体现在push_back()和pop_back()。
- 随机访问方便,它像数组数组一样被访问,即支持[]操作符和at()。
- 节省空间,因为它是连续存储,在存储数据的区域都没有被浪费的,但是要明确一点vector大多数情况下都是未存满的,在未存储的区域实际是浪费的。
- 在内部进行插入、删除效率非常低,这样的操作基本上是被禁止的。vector被设计成只能在尾部进行追加和删除操作,其原因是vector内部的实现使按照顺序表的原理。
- 只能在vector的尾部进行push和pop,不能在vector的头部进行push和pop。
内部实现:底层采用的数据结构非常简单,就只是一段连续的线性内存。内部使用三个迭代器来表示,分别指向vector容器对象的起始字节位置,当前最后一个元素的末尾字节,整个容器所占用内存空间的末尾字节。因此通过这三个迭代器,就可以计算出容器的size和capacity。当vector的大小和容量相等时(size==capacity)时,如果再向其添加元素,那么vector就需要扩容。扩容的过程主要是一下三步:1.完全弃用现有的内存空间,重新申请更大的内存空间(一般为原有空间大小的2倍或1.5倍,不同编译器扩容策略可能不同。)。2.将旧内存空间中的数据,按原有顺序移动到新的内存空间中。3.最后将旧的内存空间释放。
resize和reserve区别:
- resize:调整容器的元素数量大小,即改变容器的size。
- reserve:调整容器的容量大小,即改变容器的capacity。
clear实现:clear只是将vector的size置0,并不保证capacity为0,因此clear并不能释放vector已经申请的内存。
3. deque 的底层数据结构是什么?它的内部是怎么实现的?
deque是双端队列,非常适合在头部和尾部添加或删除数据。deque容器存储数据的空间是由一段一段等长的连续空间构成,各段空间之间并不一定是连续的,可以位于内存的不同区域。为了管理这些连续空间,deque容器用数组(数组名假设为map)存储着各个连续空间的首地址。也就是说,map数组中存储的都是指针,指向那些真正用来存储数据的各个连续空间。如下图所示:
通过建立map数组,deque容器中申请的这些分段的连续空间就能实现整体连续的效果。换句话说,当deque需要在头部或尾部增加存储空间时,它会申请一段新的连续空间,同时在map数组的开头或结尾添加指向该空间的指针,由此该空间就接到了deque容器的头部或尾部。deque容器的分段存储结构,提高了在序列两端添加或删除元素的效率。
4. map 和unordered_map 有什么区别?分别在什么场景下使用?
map是有序的,unorder_map是无序的,map适用于有顺序要求的问题,unorder_map适用于查找问题,unordered_map的查找效率优于map。map的内部是红黑树,unordered_map的内部是哈希表。
5. list 的使用场景?std::find 可以传入list 对应的迭代器吗?
list是双向链表容器,底层是以双向链表的的形式实现的。这意味着,list容器中的元素可以分散存储在内存空间里,而不是必须存储在一整块连续的内存空间。如下图所示:
可以看到,list容器中各个元素的前后顺序是靠指针来维系的,每个元素都配备了两个指针,分别指向它的前一个元素和后一个元素,其中第一个元素的前向指针为null,因为它前面没有元素;同样,尾部元素的后向指针也为null。因此,它可以在序列已知的任何位置快速插入或删除元素,因此在实际场景中,如果需要对序列进行大量添加或删除元素的操作,而直接访问元素的需求却很少,这种情况建议使用list容器存储序列。
可以。
6. string 的常用函数
- =,assign() // 赋值
- swap() // 交换
- +=,append(),push_back() // 尾部添加字符
- insert() // 插入字符
- erase() // 删除字符
- clear() // 清空字符
- replace() // 替换字符
- substr() // 截取子字符串
5. 设计模式
1. 分别写出饿汉和懒汉线程安全的单例模式
单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点,该实例被所有程序模块共享。根据单例对象创建时间,可分为两种模式:懒汉模式和饿汉模式。
- 懒汉模式:延迟加载,不到万不得已不会去实例化类,也就是说第一次用到类实例的时候才会实例化。
#include <iostream>
#include <mutex>
using namespace::std;
// 懒汉模式一:多线程不安全
template <typename T>
class Singleton
{
public:
static T* getInstance()
{
if (instance_ == nullptr)
{
instance_ = new T();
}
return instance_;
}
private:
Singleton() = delete;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
private:
static T* instance_;
};
template <typename T>
T* Singleton<T>::instance_ = nullptr;
// 懒汉模式二:多线程安全
template <typename T>
class Singleton2
{
public:
static T* getInstance()
{
if (instance_ == nullptr)
{
mutex_.lock();
if (instance_ == nullptr)
{
instance_ = new T();
}
mutex_.unlock();
}
return instance_;
}
private:
Singleton2() = delete;
Singleton2(const Singleton2&) = delete;
Singleton2& operator=(const Singleton2&) = delete;
private:
static T* instance_;
static mutex mutex_;
};
template <typename T>
T* Singleton2<T>::instance_ = nullptr;
template <typename T>
mutex Singleton2<T>::mutex_;
class Printer
{
friend class Singleton<Printer>;
friend class Singleton2<Printer>;
private:
Printer() = default;
Printer(const Printer&) = delete;
Printer& operator=(const Printer&) = delete;
public:
void print() { cout << "Printer" << endl; }
};
int main(int argc, char* argv[])
{
Singleton<Printer>::getInstance()->print();
Singleton2<Printer>::getInstance()->print();
}
- 饿汉模式:在单例类定义的时候(即在main函数之前)就进行实例化。因为main函数执行之前,全局作用域的类成员变量instance_已经初始化,故没有多线程的问题。
#include <iostream>
#include <mutex>
using namespace::std;
// 饿汉模式
template <typename T>
class Singleton
{
private:
Singleton() = delete;
Singleton(const Singleton&) = delete;
Singleton& operator=(const Singleton&) = delete;
public:
static T* getInstance()
{
return instance_;
}
private:
static T* instance_;
};
template <typename T>
T* Singleton<T>::instance_ = new T();
class Printer
{
friend class Singleton<Printer>;
private:
Printer() = default;
Printer(const Printer&) = delete;
Printer& operator=(const Printer&) = delete;
public:
void print() { cout << "Printer" << endl; }
};
int main(int argc, char* argv[])
{
Singleton<Printer>::getInstance()->print();
}
2. 说出观察者模式类关系和优点
定义了对象之间一对多的依赖,令多个观察者对象同时监听某一个主题对象,当主题对象发生改变时,所有的观察者都会收到通知并更新。
优点:
- 抽象耦合:在观察者和被观察者之间,建立了一个抽象的耦合,由于耦合是抽象的,可以很容易扩展观察者和被观察者。
- 广播通信:观察者模式支持广播通信,类似于消息传播,如果需要接收消息,只需要注册一下即可。
缺点:
- 依赖过多:观察者之间细节依赖过多,会增加时间消耗和程序的复杂程度。这里的细节依赖指的是触发机制,触发链条,如果观察者设置过多,每次触发都要花很长时间去处理通知。
- 循环调用:避免循环调用,观察者和被观察者之间绝对不允许循环依赖,否则会触发二者之间的循环调用,导致系统崩溃。
3. 说出代理模式类关系和优点
优点:
- 代理模式能将代理对象与真实被调用的目标对象隔离。
- 一定程度上降低了系统的耦合度,扩展性好。
- 可以起到保护目标对象的作用。
- 可以对目标对象的功能增强。
缺点:
- 代理模式会造成系统设计中类的数量增加。
- 在客户端和目标对象增加一个代理对象,会造成请求处理速度变慢。
- 增加了系统的复杂度。
4. 说出工厂模式概念和优点
定义一个创建产品对象的工厂接口,将产品对象的实际创建工作推迟到具体子工厂类当中。这满足创建型模式中所要求的“创建与使用相分离”的特点。简单工厂模式可以决定在什么时候创建哪一个产品类的实例。工厂方法模式有非常良好的扩展性。抽象工厂模式降低了模块间的耦合性,提高了团队开发效率。
5. 说出构造者模式概念
构造者模式是较为复杂的创建型模式,它将客户端与包含多个组成部分的复杂对象的创建过程分离。客户端无需知道具体的构造过程,只需要与构造器打交道即可,构建与表示分离。
6. 说出适配器模式概念
将一个类的接口转换成客户希望的另外一个接口,使得原本由于接口不兼容而不能在一起工作的那些类一起工作。
6. 操作系统
1. 进程和线程的区别?
进程的定义:一个具有一定独立功能的程序在一个数据集合上依次动态执行的过程。进程是一个正在执行程序的实例,包括程序计数器、寄存器和程序变量的当前值。简单来说,进程就是一个程序的执行流程,内部保存程序运行所需的资源。在操作系统中可以有多个进程在运行,可对于CPU来说,同一时刻,一个CPU只能运行一个进程,但在某一时间段内,CPU将这一时间段拆分成更短的时间片,CPU不停的在各个进程间游走,这就给人一种并行的错觉,像CPU可以同时运行多个进程一样,这就是伪并行。
线程的定义:线程是进程当中的一条执行流程,这几乎就是进程的定义,一个进程内可以有多个子执行流程,即线程。从资源组合的角度看,进程把一组相关的资源组合起来,构成一个资源平台环境,包括地址空间(代码段,数据段),打开的文件等各种资源。从运行的角度看:进程是代码在这个资源平台上的执行流程,然而线程貌似也是这样,但是进程比线程多了资源内容列表样式:进程 = 线程 + 共享资源。
进程是操作系统分配资源的单位,线程是调度的基本单位,线程之间共享进程资源。
2. 操作系统是怎么进行进程管理的?
这里就不得不提到一个数据结构:进程控制块(PCB),操作系统为每个进程都维护一个PCB,用来保存与该进程有关的各种状态信息。进程可以抽象理解为就是一个PCB,PCB是进程存在的唯一标志。操作系统用PCB来描述进程的基本情况及运行变化的过程,进程的任何状态变化都会通过PCB来体现。
PCB包含进程状态的重要信息,包括程序计数器、堆栈指针、内存分配状况、打开文件的状态、账号和调度信息,以及其它在进程由运行态切换到就绪态或阻塞态时必须保存的信息,从而保证该进程随后能再次启动,就像从未中断过一样。
提到进程管理,有一个概念我们必须知道,就是中断向量,中断向量是指中断服务程序的入口地址。一个进程在执行过程中可能会被中断无数次,但是每次中断后,被中断的进程都要返回到与中断发生前完全相同的状态。
3. 操作系统是如何做到进程阻塞的?
进程的每次变化都会有相应的状态,而操作系统维护了一组队列,表示系统中所有进程的当前状态。不同的状态有不同的队列,有就绪队列阻塞队列等,每个进程的PCB都根据它的状态加入到相应的队列中,当一个进程的状态变化时,它的PCB会从一个状态队列中脱离出来加入到另一个状态队列。
4. 进程之间的通信方式有哪些?
- 管道
- 消息队列
- 共享内存
- 信号量
- 套接字
- 文件
5. 什么是上下文切换,操作系统是怎么做的上下文切换?
上下文切换指的是操作系统停止当前运行进程(从运行态改变成其它状态)并且调度其它进程(就绪态转变成运行态)。操作系统必须在切换之前存储许多部分的进程上下文,必须能够在之后恢复他们,所以进程不能显示它曾经被暂停过,同时切换上下文这个过程必须快速,因为上下文切换操作是非常频繁的。那上下文指的是什么呢?指的是任务所有共享资源的工作现场,每一个共享资源都有一个工作现场,包括用于处理函数调用,局部变量分配以及工作现场保护的栈顶指针,和用于指令执行等功能的各种寄存器。
6. 线程是如何实现的?
- 用户线程:在用户空间实现的线程机制,它不依赖于操作系统的内核,由一组用户级的线程库函数来完成线程的管理,包括进程的创建终止同步和调度等。
- 内核线程:是指在操作系统的内核中实现的一种线程机制,由操作系统的内核来完成线程的创建终止和管理。
7. 线程之间私有和共享的资源有哪些?
私有:每个线程都有独立的,私有的栈区,程序计数器,栈指针以及函数运行使用的寄存器。
共有:代码区,堆区
8. 一般应用程序内存空间的堆和栈的区别是什么?
- 管理方式不同。栈由操作系统自动分配释放,无需手动控制;堆的申请和释放工作由程序员控制,容易产生内存泄漏。
- 空间大小不同。每个进程拥有的栈的大小要远远小于堆的大小。
- 生长方向不同。堆的生长方向向上,内存地址由低到高;栈的生长方向向下,内存地址由高到低。
- 效率不同。栈要比堆效率高的多。
9. 进程虚拟空间是怎么布局的?
11. 产生死锁的必要条件有哪些?如何避免死锁?
死锁:如果一组进程中的每一个进程都在等待仅有该组进程中的其它进程才能引发的事件,此时这组进程就被称为死锁。
产生死锁的必要条件:
- 互斥条件:一个资源每次只能被一个线程使用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
避免死锁:系统对进程发出每一个系统能够满足的资源申请进行动态检查,并根据检查结果决定是否分配资源,如果分配后系统可能发生死锁,则不予分配,否则予以分配。这是一种保证系统不进入死锁状态的动态策略。
- 打破互斥条件:改造独占型资源为虚拟资源,大部分资源已无法改造。
- 打破不可抢占条件:当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源。
- 采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。
- 打破循环等待条件:实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。
12. 什么是大端字节,什么是小端字节?如何转换字节序?
大端字节:将数据的低位字节存放在内存的高位地址,高位字节存放在低位地址。
小端字节:将数据的高位字节存放在内存的高位地址,低位字节存放在低位地址。
13. 信号和信号量的区别是什么?
信号:一种处理异步事件的方式。信号是比较复杂的通信方式,用于通知接收进程有某种事件发生,除了用于进程外,还可以发送信号给进程本身。
信号量:进程间通信处理同步互斥的机制。是在多线程环境下使用的一种设施,它负责协调各个线程,以保证它们能够正确,合理的使用公共资源。
7. 编译原理
1. gcc hello.c 这行命令具体的执行过程,内部究竟做了什么?
预处理-》编译-》汇编-》链接
2. 程序一定会从main 函数开始运行吗?
不一定,可以指定程序入口,main是默认的程序入口。
3. 动态链接库和静态链接库的区别是什么?
静态库在程序的链接阶段被赋值到了程序中;动态库在链接阶段没有被复制到程序中,而是程序在运行时动态加载到内存中供程序调用。使用动态库系统只需载入一次,不同的程序可以得到内存中相同的动态库的副本,因此节省了很多内存,而且使用动态库也便于模块化更新程序。