本文将记录我阅读别人面经/自己面经记录自己不懂的问题,方便以后巩固。
WebServer 项目
介绍下项目
- 服务器开始运行,创建(初始化)线程池(I/O密集型,线程数 n + 1)
- 创建 epoll 对连接进行监听
- 监听到连接事件,调用线程池处理 http 请求
- 读取 http 请求并对其进行解析(空格,\r\n 字段提取)
- 返回解析结果
为什么选择 epoll
select :有数量限制(1024)、每次调用需要将 fd 从用户态拷贝到内核态、需要遍历所有的 fd 才知道哪个有事件发生
poll :链表没有数量限制,每次调用需要将 fd 从用户态拷贝到内核态、需要遍历所有的 fd 才知道哪个有事件发生
epoll:红黑树增删改综合效率高,有就绪的描述符的链表。当有的连接就绪的时候,不用去遍历整棵树
C++
struct 和 class 区别
- 默认访问权限不同:struct 默认为 public,class 默认为 private
- 继承方式不同:struct 默认为 public,class 默认为 private
- 成员变量和成员函数的默认访问权限不同,同上
- 使用方式不同:struct 一般用于表示数据的集合,class 表示对象的行为
重载
- 函数名称相同:函数重载必须具有相同的函数名称。
- 参数数量不同:函数重载的参数数量必须不同。
- 参数类型不同:函数重载的参数类型必须不同,或者在参数类型相同的情况下,参数顺序不同也可以。
- 参数的类型顺序不同:函数重载的参数类型顺序不同也可以。
- 返回值类型不同:函数的返回值类型不同不能作为函数重载的条件,因为C++不允许函数重载仅仅因为返回类型不同。
NULL 和 nullptr 区别
NULL 是一个宏定义,通常被定义为 0 或者 void* 0,表示空指针或无效指针
nullptr 是 C++11 新增关键字,表示空指针或无效指针,是一个常量表达式,不需要进行类型转换
类型检查:使用 NULL 指针时,编译器无法检查它的类型是否匹配,因为它只是一个宏定义,没有类型信息。使用 nullptr 指针时,编译器可以进行类型检查,因为它是一个具有类型信息的关键字
如果子类中的函数是虚函数,父类中的同名函数不是,调用该函数的情况
- 父类指针或引用调用:当使用父类指针或引用调用该函数时,将会调用父类中的非虚函数,而不会调用子类中的虚函数。这是因为在编译时,
编译器会根据父类的类型来确定调用哪个函数,而不会考虑子类的实际类型
。 - 子类对象调用:当使用子类对象调用该函数时,将会调用子类中的虚函数,而不会调用父类中的非虚函数。这是
因为在运行时,编译器会根据对象的实际类型来确定调用哪个函数,而不是根据指针或引用的类型
。
map 和unordered_map 区别,常用哪个?
map和unordered_map都是C++ STL中的关联容器,用于存储键值对(key-value pair)。
区别:
- 实现方式不同:map 是基于红黑树(Red-Black Tree)实现的,而 unordered_map 是基于哈希表(Hash Table)实现的。
- 操作复杂度不同:由于实现方式的不同,map 的操作复杂度较为稳定,各项操作的复杂度都是 O(log n),而unordered_map 的各项操作的复杂度取决于哈希函数的质量和哈希表的装载因子,最好情况下为 O(1),最坏情况下为 O(n)。
- 元素顺序不同:map 中的元素是按照键值进行排序的,而 unordered_map 中的元素是无序的。
一般来说,如果需要对元素进行频繁的查找操作,建议使用 unordered_map,因为其具有更好的查找效率;如果需要对元素进行频繁的插入和删除操作,建议使用 map,因为其具有更好的插入和删除效率。如果需要按照键值进行排序或需要保证元素的顺序,也应该使用 map。
C++ 空类的大小?一个只包含int 变量的空class和只包含int变量的空struct的内存各占多大?
空类和空结构体大小都为 1 字节,这样可以确保两个不同的对象,拥有不同的地址
含有虚函数的类的大小 4 字节,因为虚函数类对象中都有一个虚函数指针 __vptr,其大小是 4 字节
1 2 3 4 5 6 7
class A { virtual Fun(){} }; int main(){ cout<<sizeof(A)<<endl;// 输出 4(32位机器)/8(64位机器); A a; cout<<sizeof(a)<<endl;// 输出 4(32位机器)/8(64位机器); return 0; }
只含有一个 int 成员变量的类的大小
1 2 3 4 5 6 7
class A { int a; }; int main(){ cout<<sizeof(A)<<endl;// 输出 4; A a; cout<<sizeof(a)<<endl;// 输出 4; return 0; }
只含有一个静态成员变量的类大小为 1 字节,因为静态成员变量存放在静态存储去,不占用类的大小,普通函数也不占用类大小
1 2 3 4 5 6 7
class A { static int a; }; int main(){ cout<<sizeof(A)<<endl;// 输出 1; A a; cout<<sizeof(a)<<endl;// 输出 1; return 0; }
为什么一般构造函数定义为虚函数?
如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄露。
所以在实现多态时,当用基类操作派生类,在析构时防止只析构基类而不析构派生类的状况发生,要将基类的析构函数声明为虚函数。
为什么构造函数不写为虚构函数?
从存储空间角度:虚函数对应一个vtable,可是这个vtable其实是存储在对象的内存空间的。问题出来了,如果构造函数是虚的,就需要通过 vtable来调用,可是对象还没有实例化,也就是内存空间还没有,无法找到vtable,所以构造函数不能是虚函数。
从使用角度:虚函数的作用在于通过父类的指针或者引用来调用它的时候能够变成调用子类的那个成员函数。而构造函数是在创建对象时自动调用的,不可能通过父类的指针或者引用去调用,因此也就规定构造函数不能是虚函数。
static
不考虑类的情况:
- 有时候希望某些全局变量或者函数只在本文件中被使用,而不能被其他外部文件引用,这个时候可以在全局变量前加一个 static
- 默认初始化为 0,
未初始化的全局静态变量与局部静态变量
,都存在全局未初始化区 - 静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用域与局部变量相同,函数退出后仍然存在,但是不能使用
考虑类的情况:
- static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。
- static成员函数:不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile;可以被非static成员函数任意访问
静态局部变量
静态局部变量属于静态存储类别,在静态存储区内分配存储单元,在整个程序运行期间始终存在。
静态局部变量只初始化一次,并且之后再次调用函数时不再重新分配空间和赋初值,而保留上次函数调用结束时的值(而普通局部变量每调用一次就会重新分配空间并赋一次初值)
静态局部变量默认初始化为0
函数调用结束之后静态局部变量依然存在,但是只能在该函数内进行使用该静态局部变量
extern 的作用
将全局变量的作用域扩展到其定义之前:如果全局变量不在文件的开头定义,其作用范围只限定于从定义处到文件结尾,如果在定义点之前的函数想引用该变量,就应该在引用之前使用extern关键字对该变量进行声明,之后该全局变量的作用域就从声明处一直到文件结尾了
将某一个源文件中全局变量的作用域扩展到其他源文件中:一个C++项目很多情况是由多个源文件构成,如果在一个文件中想引用另一个文件中已定义的全局变量,比如现在两个文件都要使用到同一个全局变量int a,正确的做法应该是:在一个文件中定义变量a,而在另一个文件中使用extern int a;对该变量进行声明,这样就可以两个文件同时使用同一个变量了
const 作用
不考虑类的情况
- const常量在定义时必须初始化,之后无法更改
- const形参可以接收const和非const类型的实参,例如// i 可以是 int 型或者 const int 型void fun(const int& i){ //…}
考虑类的情况
- const成员变量:不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化。
- const成员函数:const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值。
C++ sort() 函数
- 判断元素个数是否小于 stl_threshold (16),小于则使用插入排序
- 大于判断能不能使用快速排序(判断递归深度有没有达到递归深度的限制 2*lg(n)),没有则使用快速排序
- 超过则使用堆排序,稳定 O(nlogn) 的时间复杂度
操作系统
线程和进程的区别
- 进程是资源的分配单位,线程是 CPU 的调度单位
- 进程拥有一个独立完整的资源平台,不与其他进程共享;线程只独享必不可少的资源,如寄存器和栈,而一个进程可以有多个线程,彼此共享同一个地址空间
- 线程同样具有就绪、阻塞、执行三种基本状态,同样具有状态之间的转换关系
- 线程能减少并发执行的时间和空间开销
对于,线程相比进程能减少开销,体现在:
- (1. 创建时间少)线程的创建时间比进程快,因为进程在创建的过程中,还需要资源管理信息,比如内存、文件管理信息切换虚拟地址空间,切换内核栈和硬件上下文,页表切换开销很大,而线程在创建的过程中,不会涉及这些信息,而是共享它们,只需保存和设置少量寄存器内容,因此开销很小;
- (2. 终止时间少)线程的终止时间比进程快,因为线程释放的资源相比进程少很多;
- (3. 不需要切换页表,切换时间块)同一个进程内的线程切换比进程切换快,因为线程具有相同的地址空间(虚拟内存共享),这意味着同一个进程的线程都具有同一个页表,那么在切换的时候不需要切换页表。而对于进程之间的切换,切换的时候要把页表给切换掉,而页表的切换过程开销是比较大的;
- (4. 共享、线程之间数据传递效率高)由于同一进程的各线程间共享内存和文件资源,那么在线程之间数据传递的时候,就不需要经过内核了,这就使得线程之间的数据交互效率更高了;
所以,不管是时间效率,还是空间效率线程比进程都要高
心得:线程使用有一定难度,需要处理数据一致性问题,比如要使用互斥锁和条件变量等同步机制保证线程安全(原子性操作)
衡量内存占用,一般会有哪部分
内存:
- total :系统总的可用物理内存大小
- used :已被使用的物理内存大小
- free :还有多少物理内存可用
- share :被共享使用的物理内存大小
- buff/cache :被 buffer 和 cache 使用的物理内存大小
- available :还可以被应用程序使用的物理内存大小
交换空间内存。
操作系统会占用一部分内存空间,包括内核空间、内核栈、内核堆等等。
用户程序则会占用另一部分内存空间,包括用户栈、用户堆、代码段等等。
清理内存,涉及哪些内存区域进行操作
文件页:内核缓存的硬盘数据 buffer 和内核缓存的文件数据 cache。大部分文件页可以直接释放内存,以后有需要再读取就行;被应用程序修改过的,且展示还没有写入硬盘的数据,就得先写入磁盘,再进行内存释放
匿名页:没有实际载体(如堆、栈数据)、通过 lInux 的 Swap 机制,Swap 会把最不常访问的内存写到磁盘中,然后释放这些内存。
并发编程,不加锁会有什么问题
两个线程使用同一个全局变量会有不一致的问题,比如a线程把全局变量加1,b线程读的时候,如果还是从缓存中读的,那么会没有发现这个更新,就会产生不一致的问题。
如何避免死锁
两个进程诶了保护两个不同的共享资源,而使用了两个互斥锁,使用不当可能会造成两个线程都在等待对方释放锁。
使用前考虑死锁条件:互斥条件、占有并等待、不可剥夺、循环等待
解决方法:资源一次性分配、占有时可被打断、考虑资源分配顺序
计算机网络
状态码
- 1xx 信息响应
- 2xx 成功(204 响应头无 body 数据)
- 3xx 重定向:301 永久重定向,302 临时重定向
- 4xx 客户端错误:403 服务器禁止访问资源,404 请求资源没找到
- 5xx 服务端错误:502 网关问题,503 服务器当前忙
参考
- https://mp.weixin.qq.com/s/9XmrE51zsQhRw19WqlmgbA
写在最后
感谢你在茫茫人海中找到我🕵🏼
🎉你是第 个读者
㊗️ 你平安喜乐,顺遂无忧!
希望你读完有所收获~
🥂🥂🥂