八股

C++

简述C++语言的特点

  1. 以面向对象为主,还支持泛型,函数式编程等多种编程范式
  2. C++既可以直接操作内存,也可以利用零成本抽象的特性和元编程等技巧提高运行效率
  3. C++更安全,鼓励使用RAII机制自动管理资源,减少内存泄漏风险
  4. C++是一个不断发展的语言,每3年增加一些新特性,例如C++11引入了智能指针,右值,移动语义等特性,c++20引入了module、concept、ranges等特性

说说C语言和C++的区别

  1. C是一门面向过程的语言,C++是一门面向对象为主,支持多种编程范式的语言
  2. C++的安全性更高,C语言更倾向手动管理内存,C++更倾向通过RAII(智能指针、容器)自动管理资源生命周期。
  3. 一些关键的扩展:C++的函数支持函数重载;利用虚函数实现动态多态;对struct进行了扩展;提供了安全的enum
  4. C++提供了更为强大的STL标准库

说说 C++中 struct 和 class 的区别

  1. C++对C中的struct进行了扩展,C中的struct不能有成员函数,无法进行访问控制,不支持继承
  2. C++中的strcut默认public访问权限,class默认private访问权限,包括private、protect和public三种访问权限,在继承关系中,struct默认公有继承,class默认私有继承
  3. C++中的struct常用于定义简单的数据容器或模板,class常用于更复杂的对象封装

说说include头文件的顺序以及双引号”“和尖括号<>的区别

  1. 尖括号的头文件是系统文件,双引号的头文件是自定义文件
  2. 尖括号头文件的查找路径是编译器设置的头文件路径->系统变量;双引号头文件的查找路径是当前源文件目录->编译器设置的头文件目录->系统变量

导入C函数的关键字是什么,C++编译时和C有什么不同?

  1. C++通过extern关键字导入C函数,通过extern “C”引入的C函数会按照C语言的规定进行编译
  2. 编译的区别主要在函数上,C++由于支持函数重载,编译时不仅包括函数名,还包括参数类型,命名空间等信息,而C语言编译时通常只包括函数名

简述C++从代码到可执行二进制文件的过程

C++从源代码到二进制文件经历的过程包括:

  1. 预处理,展开#include头文件,处理#define宏和#ifdef条件宏,过滤注释
  2. 编译,进行词法/语法分析,语义分析,生成目标代码并优化
  3. 汇编,将目标代码转换为二进制代码
  4. 链接,合并多个目标文件及库,生成可执行文件,链接阶段可以分为静态链接和动态链接,静态链接在链接阶段就把要调用的函数链接到可执行文件中,动态链接是在执行过程中寻找要链接的函数

说说 static关键字的作用

  1. static关键字的作用主要是控制存储期或控制链接性
  2. 对于全局变量和函数,链接性控制为内部链接,使其只在当前文件可见,其他文件无法通过extern访问,全局变量的存储期不变,依然是静态存储期,作用域不变
  3. 对于局部变量,存储期改变为静态存储期,程序启动完成初始化,程序结束销毁,作用域不变
  4. 对于成员变量,该变量属于类本身,而不属于实例化的类对象,存储期为静态存储期,所有类对象都共享该变量,C++17后可以通过inline在类内完成初始化
  5. 对于成员函数,类似与静态成员变量,属于类本身,而不属于实例化的类对象,静态成员函数只能使用静态成员变量,调用其他静态成员函数,不可以是虚函数

说说数组和指针的区别

  1. 数组是一段连续的内存块,存储类型相同的元素,数组类型包含元素类型和长度信息;指针是一个保存地址的变量,仅包含类型信息。在c++类型系统中属于不同的复合类型
  2. 数组名是一个可隐式转换为常量指针的标识符,无法进行赋值,而指针变量可以进行赋值,对数组名的引用得到的是数组指针,对指针的引用得到的是指针变量的地址
  3. 指针的大小在32为系统下固定为4字节,64位系统下位8字节
  4. 数组在传参过程中会发生类型退化,退化为指针变量

简单说⼀下函数指针

  1. 函数指针是一个指向函数的指针变量
  2. 在编译时,每⼀个函数都有⼀个⼊⼝地址,该⼊⼝地址就是函数指针所指向的地址。函数指针最常用的地方是做回调函数

野(wild)指针与悬空(dangling)指针有什么区别?如何避免?

  1. 野指针是未初始化的指针,悬空指针是指向已被释放的内存或失效对象的指针
  2. 避免的手段通常包括,初始化或资源释放后及时置nullptr,尽量使用智能指针

说说内联函数和宏函数的区别

  1. 宏定义的函数并非真正的函数,只是预处理阶段的字符串展开,没有类型检查
  2. 内联函数是真正的函数,具有类型检查,是一种编译期的优化,编译器会在内联的位置直接插入代码,避免函数调用带来的开销,内联函数通常是函数体内容简单的函数
  3. inline关键字仅作为内联建议,一个函数是否为内联函数最终由编译器决议,C++17扩展了inline关键字的功能,inline函数允许函数定义出现在多个翻译单元

说说运算符i++和++i的区别

  1. ++i返回的是一个左值,i++返回的是一个右值,++i效率更高
  2. 以int a = i++; int b = ++i; 这个例子中,a是先赋值为i,然后i自增,b是被赋值为i自增后的结果

new / delete ,malloc / free 区别

  1. new和delete是C++运算符,支持重载,而malloc和free是C库函数,不可重载
  2. new执行了两个过程,第一步是分配未初始化的内存空间,第二步是调用构造函数进行初始化,失败会抛出异常。delete也有两个过程,首先调用析构函数完成析构,最后调用解分配函数释放内存
  3. malloc分配的内存大小需要手动计算,返回的指针需要进行强转,失败时返回null

 说说const和define的区别

  1. const用于定义常量,define可以用于定义宏,也可以用于定义常量
  2. 当用于定义常量时,const常量是具有类型的,会进行类型检查,遵循C++的作用域规则,在编译期进行处理,const常量可能会分配相应的内存,也可能会被优化为立即数
  3. define常量是无类型的,也没有类型检查,在预处理阶段进行处理,不涉及内存分配,只是简单的进行宏展开

说说const int a, int const *a, int *const a, int *const a, const int *const a分别是什么,有什么特点

  1. const int a定义了一个整型常量
  2. const int* a定义了一个指针,其指向的内容是一个整型常量
  3. int const* a同const int* a
  4. int *const a定义了一个常量指针,指向一个整型数据
  5. const int *const a定义了一个常量指针,指向一个整型常量

C++有几种传值方式,之间的区别是什么?

  1. C++有值传递,指针传递和引用传递三种方式,对于值传递,形参是实参的副本,所以修改形参不会影响实参。
  2. 对于指针传递,传递的是指针的值,形参和实参指向同一内存,在不改变指针的值的情况下,解引用修改数据会影响实参。
  3. 对于引用传递, 引用是实参的一个别名,修改引用会影响到实参

简述一下堆和栈的区别

  1. 在管理方式上,栈由编译器进行分配与释放,内存连续,分配速度快;堆由开发者手动管理分配与释放,分配可能碎片化
  2. 在生命周期方面,栈变量随作用域结束自动销毁,堆对象需要手动释放,否则会导致资源泄漏
  3. 在结构上,栈严格遵守先进后出,由于内存连续,缓存命中率会更高,堆分配通常地址不连续,易碎片化

简述C++的内存管理

  1. C++的内存可以分为栈区,堆区,静态存储区,常量存储区和自由存储区
  2. 栈区主要用于存放局部变量和函数参数,由编译器进行管理
  3. 静态存储区用于存放静态变量和全局变量,分为初始化和未初始化两个区域
  4. 常量存储区用于存放常量
  5. 堆区用于存放动态分配的对象,自由存储区是C++中的一个抽象概念,当使用默认的new和delete时,自由存储区和堆区共享同一块内存,但如果重载new运算符,自由存储区可以脱离堆

内存泄露及解决办法

  1. 内存泄漏是指没有及时回收动态分配的内存,常见的造成内存泄漏的行为有使用new和malloc,没有正确使用delete和free释放资源;存在继承关系时,父类析构函数非虚,导致资源不能及时释放;windows句柄资源使用后没有释放
  2. 可以通过使用智能指针,RAII技术来避免出现内存泄漏问题,当出现内存泄漏时,可以使用Dmalloc、Leaky等工具来进行检查

简述一个程序有哪些section

  1. 通常包括.text段,.rdata段,.data段,.bss段,堆区,共享区,栈区等组成
  2. .text段通常用于存放二进制代码
  3. .rdata段存放只读常量
  4. .data段用于存放用于存放已初始化的全局变量和静态变量
  5. .bss段用于存放未初始化的全局变量和静态变量
  6. 栈区用于存放局部变量,函数参数和返回地址,由编译期管理,栈区从高地址向低地址增长
  7. 堆区用于存放动态分配的资源,地址从低地址向高地址增长
  8. 共享区用于实现文件映射等功能

简述一下程序启动的过程

  1. 操作系统首先会创建新进程,并分配虚拟地址空间,然后加载器将代码段(.text),已初始化的数据段(.data)映射到内存,然后.bss段清零,完成内存布局初始化
  2. 对于静态链接库,直接嵌入可执行文件;对于动态链接库,加载器读取可执行文件的导入表,确定每一个依赖的动态链接库,将导入函数的实际地址写入IAT表,对于基址与预设不符的,查询重定位表进行重定位,
  3. C runtime库进行初始化,初始化堆管理器,调用全局对象的构造函数,并注册全局对象的析构函数
  4. 进入程序入口main或WinMain,开始执行

简述一下面向对象,以及面向对象的三大特征

  1. 面向对象是一种以对象为核心的编程范式,将数据和操作数据的方法封装为类,利用实例化的对象解决问题。相较于面向过程式自上而下的编码方式,面向对象可以提高代码的复用性,扩展性和维护性,
  2. 面向对象的三大特征是封装、继承和多态
  3. 封装是指将数据和操作数据的方法封装到一起,利用访问权限修饰符控制外部访问
  4. 继承是指子类继承父类的属性和方法,提高代码复用性,C++中支持private,protected和public三种继承类型,在private继承下,父类的所有成员都变为private,子类无法访问,在protected继承下,父类的public成员变为protected属性,子类可以访问,但外部对象无法访问,在public继承下,父类的成员属性不发生变化
  5. 多态是指可以通过父类指针调用子类的方法,子类可以通过重写父类的虚函数进行扩展。面向对象中的多态通常是指运行时的多态,通过虚表实现动态绑定,C++中还存在静态多态,通常是指函数重载和模板
  6. 现代oop提倡组合优于继承,更推荐使用继承+组合的形式来提高编码的灵活性,减少耦合

简述一下 C++ 的重载和重写,并说一下它们是怎么实现的

  1. 函数重载是指可以在同一作用域内定义参数列表的不同的同名函数,编译器通过“Name Mangling”技术,利用函数名和参数列表生成唯一符号,在编译期间通过实参信息进行决议,匹配最佳函数,参数的类型,顺序和个数参与决议过程,返回值不参与
  2. 重写是指派生类中重新定义了父类中除函数体外完全相同的虚函数,该函数在父类中必须是虚函数,不能是static函数,重写的函数访问修饰符可以与父类不同,可以通过final阻止进一步重写。其核心机制是通过虚函数表和虚表指针实现的,如果类存在虚函数,则编译器会为该类生成一个虚函数表,虚函数表中存放着虚函数的函数指针,在构造时完成虚表指针的初始化,指向该类的虚函数表(虚函数表类共享,而不属于某个对象)。当派生类继承了具有虚函数的基类时,首先会拷贝基类的虚函数表,如果派生类重写了虚函数,就替换为重写后的虚函数地址,如果派生类自身也有虚函数,则追加到虚函数表中

简述一下C++中所有的构造函数

  1. C++中常见的构造函数有,默认构造函数,一般构造函数,拷贝构造函数,移动构造函数,委托构造函数,继承构造函数和转换构造函数
  2. 默认构造函数也称为无参构造函数,当没有显式定义构造函数时,编译器会自动生成默认构造函数,一般构造函数是有参数的构造函数
  3. 拷贝构造函数用于实现同类型的拷贝,默认实现是浅拷贝
  4. 移动构造函数接收右值引用,转移资源的所有权
  5. 委托构造函数可以让一个构造函数调用其他同类构造函数,减少重复代码,集中初始化逻辑
  6. 继承构造函数用于继承基类构造函数,避免重复编写基类构造函数
  7. 转换构造函数是单参数构造函数(或含默认值的多参数构造),允许隐式类型转换
  8. 根据零/三/五法则,若定义了析构函数,拷贝构造,拷贝赋值,移动构造,移动赋值之一,通常需要定义全部

说说一个类,默认会生成哪些函数

  1. 默认会生成默认构造函数,析构函数,拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符
  2. 当用户定义了析构函数时,会抑制移动构造函数,移动赋值运算符的生成

说说 C++ 类对象的初始化顺序,有多重继承情况下的顺序

  1. 在创建派生类对象时,有限调用基类构造函数,如果类中有其他成员类,再调用成员类的构造函数
  2. 对于多继承的情况,按多继承的顺序调用基类构造函数,不受初始化列表顺序影响。对于成员类,按成员类的定义顺序调用构造函数,不受初始化列表顺序影响
  3. 调用派生类的构造函数

说说C++的四种强制类型转换

  1. C++的强制类型转换有static_cast,dynamic_cast,const_cast,reinterpret_cast四种
  2. static_cast是带有类型安全检查的类型转换,在多态环境下仅保证向上转换是安全的,向下转换是未定义行为,常用于基本类型之间的转换,类类型的转换需要提供有效的构造函数或转换运算符,用于转换无关联的类型(如int*转double*)时,在编译期报错,建议将大部分隐式类型转换替换为static_cast
  3. dynamic_cast专用多态环境下的类型转换,保证在down cast转换过程中是安全的,转换无虚表对象时会在编译期报错,对于转换失败的,结果将返回空指针,由于需要RTTI,因此存在一定的开销
  4. const_cast专用于const属性的转换,可以用于增加或去除const属性
  5. reinterpret_cast会从内存布局上对数据重新进行解释,无限制,但可移植性差,危险度高

说说为什么要虚析构,为什么不能虚构造

  1. 虚析构是为了保证派生类可以正确释放自身资源,如果析构函数非虚,在使用基类指针管理派生类对象进行析构时,只会调用基类的析构函数,导致派生类资源无法正确释放,导致资源泄漏
  2. 构造函数的调用先于虚函数表的建立,故虚构造是错误的

简述一下什么是常函数,有什么作用

  1. 常函数是指参数列表后带const的成员函数,其特点是常函数只能调用其他常函数,且不能修改成员变量(用mutable修饰的除外)

说说什么是虚继承,解决什么问题,如何实现?

  1. 虚继承是一种用于解决多重继承环境下菱形继承问题的一种机制,虚继承通过在继承基类时使用virtual关键字实现,确保在多重继承时,基类仅被最终派生类实例化一次,避免数据冗余和二义性
  2. 在内存布局上,虚基类的实例被放置在最终派生类的末尾,被所有虚继承的类共享,编译器为每一个虚继承的派生类插入虚基类指针(或使用虚基类表,虚基类表保存虚基类相对当前对象的偏移),指向虚基类实例的位置,访问虚基类的成员时统一通过指针间接访问,确保唯一性,但存在一定的开销
  3. 虚基类的构造函数由最终派生类直接调用

简述一下虚函数和纯虚函数,以及实现原理

  1. 虚函数的作用主要是为了实现动态多态,当使用父类指针调用子类成员函数时,通过虚函数表和晚绑定,实现了相同函数不同功能的作用
  2. 纯虚函数是成员函数参数列表后又=0的函数,包含纯虚函数的类是一个抽象类,无法被实例化,继承了纯虚函数的派生类必须定义抽象类中的每一个纯虚函数,否则也无法实例化
  3. 纯虚函数和虚函数都可以提供默认实现,但调用纯虚函数的默认实现必须显式调用(class::function)

 请问构造函数中的能不能调用虚方法

  1. 可以在构造函数中调用虚方法,但方法的行为可能与直觉不符,因为在构造函数执行期间,对象的动态类型为当前正在构造的类型
  2. 当构造函数中调用的是纯虚函数时,如果纯虚函数没有提供默认实现,将会报错

请问拷贝构造函数的参数是什么传递方式,为什么

拷贝构造函数的参数必须是引用传递,如果是值传递,那么在传参过程中需要构建临时对象,而构建临时对象又需要调用拷贝构造函数,这就导致了无限递归的现象

简述一下拷贝赋值和移动赋值

  1. 拷贝赋值的参数为左值引用,移动赋值的参数是右值引用
  2. 拷贝赋值通常是为了实现深拷贝,避免悬垂指针或引用的问题,移动赋值是资源所有权的转移

仿函数了解吗?有什么作用

  1. 仿函数也称为函数对象,是一个能像普通函数一样使用的类,通过重载()运算符实现,仿函数的优势在于可以利用成员变量记录上下文信息
  2. lambda函数本质上是一个匿名函数对象,当lambda函数的捕获列表为空时,会生成一个转换函数,使其可以转换为函数指针

C++ 中哪些函数不能被声明为虚函数

  1. c++中普通函数,构造函数,静态成员函数,友元函数和模板成员函数不能被声明为虚函数
  2. 普通函数只能重载,与重写无关,属于静态多态,故不能成为虚函数
  3. 构造函数的功能是完成初始化, 虚表指针的初始化就发生在构造函数阶段,如果构造函数是虚函数,那么就需要查虚函数表,此时就产生了逻辑矛盾,故不能为虚函数
  4. 静态成员函数为类所有,所有实例化对象共享统一静态成员函数,无this指针,无法通过虚表机制实现多态
  5. 友元函数是类外的普通函数,只是可以访问类内的private成员,不支持继承,所以不能为虚函数
  6. 模板成员函数,虚表需要在编译期确定,而模板成员在实例化时才会生成代码,故不能为虚函数

说说new和delete的实现原理,delete是如何知道释放内存的大小的

  1. new通过调用operator new,分配内存后调用构造函数,delete则是先调用析构函数,后回收内存
  2. delete是如何知道释放内存的大小的,在分配内存时通常会分配更多一点的内存用于保存元数据,元数据中会记录分配的内存大小,对于new[]和delete[],元数据中还会记录数组的长度,这样在delete[]就会按逆序依次调用析构函数

说说什么是对象复用,什么是零拷贝

  1. 对象复用是一种用于避免对象反复构造和销毁的技术,通过将对象保存到对象池中即可实现对象复用,避免资源浪费
  2. 零拷贝是指避免CPU将数据从一块内存中拷贝到另一块内存中,C++中的emplace_back就属于零拷贝技术

说说指针和引用

  1. 指针和引用分属C++类型系统中不同的复合类型,指针是一个保存地址的变量,引用是一个别名,引用可以看作是指针的语法糖
  2. 指针可以置空,引用不可以,指针可以改变指向的数据,引用不可以
  3. 从编译角度看,在编译时会将指针和引用添加到符号表,符号表中记录了变量名和变量对应的地址。对于指针来说,符号表上记录了指针变量的地址,而对于引用来说,符号表上记录引用对象的地址

说说类如何实现只静态分配和只动态分配

  1. 重载new和delete运算符为private属性即可实现只静态分配
  2. 将构造函数和析构函数设置为protected属性即可实现只动态分配

说一说函数查找和重载解析的过程

  1. 函数调用可以分为两种形式,一种是非限定调用,即只使用函数名的调用;另一种是限定调用,即作用域运算符(::)和成员访问符(.或->)
  2. 函数的查找的过程也就分为了非限定调用函数的查找和限定调用函数的查找,对于非限定调用函数,会从当前作用域逐层向外查找(局部作用域,外层函数作用域,类作用域,命名空间作用域,全局作用域),并且进行参数依赖查找,即会额外搜索参数类型所属的命名空间或类(基本类型不涉及ADL),优先级关系为当前作用域,ADL扩展的作用域,外层作用域;对于限定函数的查找,只搜索指定作用域,不会触发ADL
  3. 重载解析,重载解析的关键在于构建函数调用候选集和匹配决议,首先通过函数查找规则来构建候选集,然后通过优先级规则来匹配具体的函数。优先级顺序为参数精准匹配 > 类型提升 > 标准转换 > 类类型转换

说说volatile的作用

  1. volatile主要有三大特性,分别是易变性,不可优化性和顺序性
  2. 易变性是指该值可能被意料之外的因素修改(例如其他线程),要求编译器每次都从内存中读取
  3. 不可优化性是指不让编译器直接对变量进行优化(例如常量传播,无用代码消除等)
  4. 顺序性是指保证volatile变量之间的顺序性
  5. 值得一提的是volatile并不保证原子性

解释下 C++ 中类模板和模板类的区别

类模板是一个模板,是一个生成类的蓝图,本身并不是一个类;而模板类是一个类,是类模板实例化的一个结果

说说auto和decltype

  1. auto的作用是自动推导变量的类型,根据初始化表达式推断变量的类型,因此atuo变量必须具有初始化表达式,auto推导不会保留const属性和引用属性
  2. decltype的作用是推导表达式的类型,且保留所有修饰符,decltype的推导是不求值的。如果推导的对象是变量名,那么直接推导变量的类型。对于复杂表达式,根据值类别推导,具体来说,如果表达式的结果是左值,那么推导的结果是T&,如果表达式的结果是右值,那么推导的结果是T

为什么成员列表初始化速度比函数体内的初始化速度块

因为成员列表初始化直接匹配成员的构造函数,而函数体内的初始化本质是调用构造赋值,故速度更快

构造函数和析构函数应不应该抛出异常

  1. 构造函数可以抛出异常,但需要正确处理,因为构造函数抛出异常时,对象构造不完全,不会调用析构函数
  2. 析构函数不应该抛出异常,若析构函数在栈展开(处理异常)过程中抛出异常,C++ 会调用 std::terminate,直接终止程序

STL

请说说 STL 的基本组成部分

  1. STL由6部分组成,分别是容器,迭代器,算法,仿函数,适配器,分配器
  2. 容器是一种管理数据的模板类,包括序列容器,关联容器和无序容器
  3. 迭代器提供了访问容器的统一接口,包括输入迭代器、输出迭代器、前向迭代器、双向迭代器和随机访问迭代器,迭代器是连接容器和算法的桥梁
  4. 算法是操作数据的模板函数,通过迭代器间接操作容器
  5. 仿函数是函数对象,行为上类似于函数调用,通过重载括号实现,lambda函数一种轻量级的函数对象
  6. 适配器是一种转换组件接口的工具,包括容器适配器,迭代器适配器和函数适配器
  7. 分配器用于管理对象的创建与销毁,内存的分配与释放

说说 STL 中 map hashtable deque list 的实现原理

  1. map、hashtable、deque、list实现机理分别为红黑树、函数映射、双向队列、双向链表
  2. map内部实现了一个红黑树,由于红黑树是一个二叉搜索树,故内部数据是有序的
  3. hashtable采用了函数映射的思想,可以实现快速的查找
  4. deque内部是一个双向队列,元素在内存中连续存放,随机存取任何元素都在常数时间完成
  5. list内部实现的是一个双向链表,元素在内存不连续存放

请你来介绍一下 STL 的分配器

  1. 分配器的核心职责有两个部分,一是内存的分配和释放,二是对象的构造和析构
  2. 分配器通过allocate和deallocate管理内存,通过construct和destroy完成对象的构造与析构

STL 容器用过哪些,查找的时间复杂度是多少,为什么

  1. 常用STL容器有vector,deque,list,set,map,unordered_set,unordered_map等
  2. vector的增删时间复杂度为O(N),查找的时间复杂度为O(1)
  3. deque的增删时间复杂度为O(N),查找的时间复杂度为O(1)
  4. list的增删时间复杂度为O(1),查找的时间复杂度为O(N)
  5. map和set的增删查找时间复杂度均为O(logN)
  6. unordered_map、unordered_set的增删查找时间复杂度,最好时为O(1),最坏为O(N)

说说解决哈希冲突的方法

  1. 解决哈希冲突常用的方法有线性探测,开链,再散列,二次探测等方法
  2. 线性探测是指发生冲突时向后依次查找,找到表尾部就返回表头,直到找到空位
  3. 开链是指每个位置维护一个链表,如果计算后位置相同则放入链表中
  4. 再散列是指发生冲突后用另一种哈希算法再计算一个地址,直到没有冲突

说说迭代器失效问题

  1. 迭代器失效是因为对容器操作后容器内部的存储结构发生了变化导致的
  2. 常见的迭代器失效情况有,vector中erase一个元素后,该元素之后的所有迭代器失效,迭代器失效的原因在于,删除元素后,vector为了保证数据的连续性需要把后续的元素依次前移一个位置

操作系统

说说进程,线程和协程的联系与区别

  1. 进程是资源分配的基本单位,程序运行时可能会创建一个进程,也可能创建多个进程。进程切换的开销较大,需要由用户态切换至内核态,同时需要切换虚拟内存空间,内核栈,硬件上下文等内容,开销较大
  2. 线程是资源调度的基本单位,每一个进程都有一个唯一的主线程,主线程和进程是相互依存的关系,当主线程结束时,进程也会结束,线程的切换开销相对较小,需要由用户态切换至内核态,仅保存少量寄存器内容
  3. 协程是一个轻量级的线程,线程内部调度的基本单位,由于协程切换不需要进入内核态,故切换开销最小

说说外中断和内中断是什么

  1. 外中断是由cpu执行指令之外的事件引起的,例如IO中断,代表设备的输入输出任务已完成,需要处理器发送下一阶段的任务
  2. 内中断也称为异常,是由cpu执行指令引起的,例如除0异常,地址越界等, 可以分为 陷阱(Trap),故障(Fault)和终止(Abort)三类

说说进程调度算法

  1. 常见的进程调度算法包括,先来先服务,短作业优先,最短剩余时间优先,时间片轮转,优先级调度,多级反馈队列等算法
  2. 先来先服务时一种非抢占式的调度算法,有利于长作业,不利于短作业。而短作业优先任务则刚好相反,缺点是可能会出出现长作业饿死的情况
  3. 最短剩余时间优先是一种抢占式调度算法,当有新的作业到达时,会与当前作业的剩余时间进行对比,由此决定哪一个线程被挂起
  4. 时间片轮转是将所有进程按序存入队列中,然后将CPU时间分配给队首的进程,当时间片结束时发起时钟中断,当前进程送往队尾
  5. 优先级调度是根据进程优先级进行调度的算法,而多级反馈队列综合了前面的多种调度算法

说说Linux下的进程通信方式

  1. 可以通过消息队列,共享内存,信号,信号量,套接字和管道6种方式通信
  2. 消息队列是一个消息链表,存放在内核中,消息队列独立于收发进程
  3. 共享内存是指映射一段可供其他进程访问的内存
  4. 信号量是一个计数器,用于控制多个进程对资源的访问,常用于同步和互斥访问的情况
  5. 信号用于通知进程某个时间的发生
  6. 套接字常用于不同主机之间的进程通信
  7. 管道可以分为无名管道和有名管道,是一种半双工的通信方式,数据单向流通,其中无名管道只能用于具有亲缘关系的进程通信(父子进程或者兄弟进程),而有名管道可以用于无关进程之间的通信

说说动态分区算法

  1. 常见的动态分区算法由首次适配算法,最佳适配算法,最坏适配算法和邻近适配算法
  2. 首次适配算法是指每次都从低地址开始查找,直到找到第一个满足大小的分区,虽然可能会导致低地址部分出现较多的外部碎片,高地址部分空闲块长时间不被使用,但通常情况下表现都是最优的
  3. 最佳适配算法是指维护一个由小到大的空闲分区链表,然后每次都从最小的分区开始搜索,直到找到第一个可用的空闲分区,有点是理论上利用效率更高,但存在维护链表和搜索链表的开销,且会产生大量细小的外部碎片
  4. 最坏适配算法是指每次都使用最大的空闲分区,优点是可以减少细小外部碎片的产生,缺点是大的空闲分区被快速分割,后续如果有打的内存需求难以满足,同样需要维护链表
  5. 临近适配算法是指,每次都从上一次查找结束的位置开始搜索满足大小的分区,优点是避免出现首次适配算法中外部碎片集中在低地址部分的问题,但是会导致空闲分区更加分散

说说虚拟技术

  1. 常见的虚拟技术有时分复用和空分复用两种
  2. 时分复用的应用例如并发,空分复用的应用例如虚拟内存

说说虚拟内存的作用

  1. 扩展内存空间,通过分页和交换技术,提供远大于物理内存的连续的虚拟地址空间
  2. 实现不同进程间的内存隔离与保护,每个进程都有自己独立的虚拟内存空间,有效防止其他进程的非法访问
  3. 简化内存管理,以进程的视角,所有地址空间均是连续的,屏蔽了物理内存存在碎片, 地址不连续的问题
  4. 实现内存共享,不同的进程的虚拟页可以映射到同一物理页,例如不同进程共享同一个动态链接库。

说一说常见的几种锁

  1. 包括读写锁,互斥锁,条件变量和自旋锁
  2. 读写锁是允许同时读,不允许同时写,且写者优先于读者
  3. 互斥锁则是一次只能由一个线程持有互斥锁,其他线程只能等待,用于线程互斥
  4. 条件变量则是用于线程同步,常与互斥锁配合使用,避免出现竞态条件
  5. 自旋锁是指,当程序获取锁失败时,不会直接放弃cpu时间,而是一直循环尝试获得锁,一般用于加锁时间很短的场景

说一说内存交换

  1. 内存交换是指将内存中暂时不用的进程或数据整体移至磁盘交换区,腾出内存空间供其他进程使用,需要时再换回内存

说说地址变换中,有快表和没快表的流程

  1. 当没有快表时,首先计算页号和页内偏移,然后检查页号合法性,合法就查询页表,找到对应的块号即可访问物理地址
  2. 当存在快表时,检查完页号合法性后会查询块表,如果快表命中则可以从块表中直接获取到块号

说说malloc申请内存的流程

  1. malloc申请内存主要依赖两个系统调用,分别是brk和mmap
  2. 当申请的内存少于128KB时,如果申请的内存小于64B时从fast_bins中获取空闲块,fast_bins是一个单链表,每一个空闲块的大小都是64B;如果申请的内存大于64B小于512B时,从small_bins中挑选合适的空闲块,small_bins是多个单链表,每个单链表管理大小相同的空闲块,单链表管理的空闲块大小按一定步长递增;当申请的内存大于512B小于128KB时,从large_bins中获取空闲块;如果没有合适的空闲块,则调用brk,通过抬高.data数据段最高处的指针获取新的虚存
  3. 当申请的内存大于128kb时,调用mmap通过搜索空闲的虚拟内存获取可操作的堆内存

说说守护进程,僵尸进程和孤儿进程

  1. 孤儿进程是指父进程结束,但父进程的一个或多个子进程还在运行,这些子进程此时会成为孤儿进程,被init进程接管
  2. 僵尸进程是指一个进程使用fork函数创建子进程,如果子进程退出,而父进程并没有调用wait()或者waitpid()系统调用取得子进程的终止状态,那么子进程的进程描述符仍然保存在系统中,占用系统资源,这种进程称为僵尸进程
  3. 守护进程是一种运行在后台且生存期较长的特殊进程,它独立于控制台,常用于处理一些系统级别的任务

说说怎么处理僵尸进程

  1. 一种方式是使用signal通知内核对子进程的结束不关系,由内核回收
  2. 另一种方式是在父进程中调用wait或waitpid

说说常见的磁盘调度算法

  1. 先来先服务算法,按请求顺序调度,公平,但寻道时间可能较长
  2. 最短寻道时间优先,优先调度距离当前磁头最近的磁道,可能会导致饥饿
  3. 电梯扫描算法,按一个方向进行调度,直到该方向没有磁盘请求,不存在饥饿问题

说说抖动现象

  1. 抖动现象是指刚换出的页面立刻又要换入内存,刚换入的页面立刻又要换出,造成这一现象的原因通常是分配的物理块少于频繁使用的页面数

说说死锁产生的必要条件,以及如何解除死锁

  1. 死锁产生的必要条件包括4个,互斥条件,不剥夺条件,请求和保持条件,循环等待条件
  2. 常见的方法有死锁的检测与恢复,利用资源分配图检查是否存在死锁,恢复则可通过抢占式恢复,回滚恢复和杀死进程恢复;死锁的预防,该方法通过破坏死锁的四个必要条件完成;死锁的避免,常用的方法有银行家算法

说说什么是内部碎片,什么是外部碎片

  1. 内部碎片常见于固定分配方式,即分配的内存区域有一部分没有用上
  2. 外部碎片常见于动态分配方式,即内存的有些空闲区较小,难以利用上,外部碎片可以通过紧凑技术解决

说说冯诺依曼结构,以及分别对应现代计算机的哪几个模块

  1. 冯诺依曼结构包括输入设备,输出设备,存储器,控制器,运算器这几部分
  2. 输入设备对应键盘,输出设备对应显示器,存储器对应内存,控制器对应南桥北桥,运算器对应cpu

说说什么是大端小端,如何判断大端小端

  1. 大端存储是指数据高位存在低字节部分,符合人们的阅读习惯,小端存储则相反,数据低位存在低字节部分
  2. 可以通过int i = 1;然后用一个char指针取变量i首地址的数据,如果等于0是大端存储,如果是1就是小端存储

说说进入内核态的方式

  1. 通常有三种方式:a、系统调用。b、异常。c、设备中断。其中,系统调用是主动的,另外两种是被动的。

计算机网络

说说路由协议

  1. 路由可以分为静态路由和动态路由,静态路由由管理员手动维护,动态路由则根据路由协议维护
  2. 路由选择的必要步骤包括:1)向其他路由器传递路由信息。2)接收其他路由器的路由信息。3)根据收到的路由信息计算出到每个目的网络的最优路径,并由此生成路由选择表。4)根据网络拓扑的变化及时的做出反应,调整路由生成新的路由选择表,同时把拓扑变化以路由信息的形式告知其它路由器
  3. 常见的路由协议包括RIP,IGRP,OSPF协议等

说说DNS查询服务器的基本流程

  1. 当打开浏览器输入一个网址的时候,首先会检查本地host文件,查询是否由这个地址映射关系,当没有找到时,会向本地DNS服务器发送DNS请求,如果在本地DNS服务器的缓存中查询到了结果,则直接返回结果
  2. 当本地DNS服务器没有查询到结果时,向DNS根服务器发送请求,根服务器会返回域服务器地址,然后本机向域服务器发起请求,然后域服务器会返回域名解析服务器的地址,最后本机向域名解析服务器发送请求获得最终的IP地址,然后本地host文件缓存这个映射关系

说说TCP三次握手和四次挥手的过程

  1. 第一次握手:建立连接时,客户端向服务器发送SYN包(seq = x),请求建立连接,等待确认
  2. 第二次握手:服务器端收到客户端的SYN包,回一个ACK包(ack = x+1),确认收到,然后发送一个SYN包(seq = y)给客户端
  3. 第三次握手,客户端收到服务器端发送的SYN和ACK包,回一个ACK包(ack = y+1),告诉服务器端收到,此时连接建立成功,开始传输数据
  4. 第一次挥手,客户端发送FIN包(fin = 1)给服务器端,请求终止连接,此时客户端不再发送数据,但是可以接收数据
  5. 第二次挥手,服务器端收到FIN包,回一个ACK包给客户端,但此时还没有断开连接,等待剩余数据传送结束
  6. 第三次挥手,服务器端等待数据传送结束后,向客户端发送FIN包,表明可以断开连接
  7. 第四次挥手,客户端收到后,回一个ACK包表示收到,进入TIME_WAIT状态,等待2MSL后如果没有数据发来,正式断开连接

说说TCP和UDP的区别,怎么即利用UDP的优势,又保证可靠传输

  1. TCP是有连接的,可靠的,TCP协议保证数据按序发送,按序到达,提供超时重传保证可靠性,并且提供流量控制和拥塞控制,TCP连接是一对一的
  2. UDP是无连接的,不可靠的,只是尽力交付,UDP支持一对一,多对多和一对多的通信
  3. 如果要让UDP具备可靠性,那么可以为UDP添加超时重传的机制,具体来说,为了让UDP也能超时重传,那么UDP也需要向TCP一样利用3次握手建立连接,同时让UDP报文具备一个序号,这样当UDP超时时,既可以直到对方发送的报文超时,同时还能直到丢失了哪一个报文,这样就能做到超时重传

说说如果三次握手时候每次握手信息对方没收到会怎么样,分情况介绍

说说什么是 MSL,为什么客户端连接要等待2MSL的时间才能完全关闭

  1. MSL是指报文最长生命时间,等待2MSL的原因是,客户端发送的最后一个ACK报文可能会丢失,这就导致了服务器可能会收不到对FIN-ACK的确认报文,此时服务器会超时重传FIN-ACK,然后客户端会再发送一个ACK报文。如果客户端不等待2MSL,可能会导致服务器无法正常进入链接关闭状态

说说什么是 TCP 粘包和拆包

  1. TCP并不知道上层业务数据的具体含义,而是根据TCP缓冲区的实际情况进行包的划分,假设客户端分别发送两个数据包D1和D2给服务器端,那么就会存在3种情况:1)服务器端分两次读取到了两个独立的数据包,此时没有粘包和拆包。2)服务器端一次收到了两个数据包,此时D1和D2粘合在一起,即出现粘包。3)服务器端分两次收到两个数据包,可能是第一次收到部分D1和,第二次收到D1剩下的部分和完整的D2,也有可能是第一次收到了D1和部分D2,第二次收到了D2的剩余部分,这就是拆包

说说浏览器从输入 URL 到展现页面的全过程

  1. 首先进行域名解析,域名解析完成之后,客户端发起TCP建立请求,当TCP3次握手结束链接建立成功之后,发起HTTP请求,服务器响应请求后,浏览器得到html代码,然后完成解析,同时请求html代码中的资源(如图片等)

TCP头部中有哪些信息

  1. 32bit序号,传输方向上字节流的字节编号
  2. 32bit确认号:接收方对发送方TCP报文段的响应
  3. 4bit首部长:标识首部有多少个字节
  4. 6bit标志位:URG,ACK,PSH,RST,SYN,FIN
  5. 16bit窗口:表示接收窗口大小
  6. 16bit校验和,接收端用CRC检验整个报文段

谈谈流量控制和拥塞控制

  1. 流量控制用于控制通信双方传送数据的速率,具体而言,通信双方需要维护发送窗口和接收窗口的大小,然后在通信过程中利用TCP中的window size字段告知对方自己当前的接受能力,如果接收窗口为0,那么发送方会暂停发送,并通过计时器定期查看接收方是否恢复了接收能力
  2. 拥塞控制用于网络全局发送数据的速率,拥塞控制算法包括慢启动,拥塞避免,超时重传和快重传快恢复算法,在慢启动阶段,拥塞窗口未达到阈值,大小呈指数级增长;在拥塞避免阶段,拥塞窗口达到阈值,呈线性增长;超时重传是指发生拥塞时直接将拥塞窗口重置为1,而快恢复快重传则是将拥塞窗口大小减半
  3. 发送方的实际发送能力为发送窗口和拥塞窗口中最小的那个确定

UE5相关

GAS系统是什么

  1. Ability System component (ASC) ASC是用于管理Actor技能的组件,负责跟踪技能的状态、属性和时间

  2. Gameplay Ability (GA) GA用于实现具体技能的逻辑,例如技能的激活、效果、动画播放等

  3. Gameplay Effect (GE) GE用于管理影响Actor状态的效果,例如buff、debuff等

  4. Ability Set (AS) AS用于存储角色基础属性,如生命值、魔法值等