【嵌入式面试】

一、数据结构和算法

1.数组、链表、二叉排序增删改查的时间复杂度

数据结构插入优点缺点
数组O(1)O(n)O(n)O(n)插入效率高,查找速度快空间利用率不高、数组空间大小固定、内存空间要求高
有序数组O(n)O(n)O(logn)O(logn)
无序链表O(1)O(n)O(n)O(n)插入元素速度快、内存利用率高、可以动态拓展随机访问效率低
有序序链表O(n)O(n)O(n)O(n)
二叉树O(logn)-O(n)O(logn)-O(n)O(logn)-O(n) O(logn)-O(n)查找、插入、删除都快、树保持平衡算法复杂

2.哈希表及其原理

Hash 表即散列表,是通过关键字(key)根据哈希算法计算出应该存储地址的位置。其最突出的优点是查找和插入删除是O(1),最坏的就是hash值全都映射在同一个地址上,这样哈希表就会退化成链表。
实现原理

  1. 把 Key 通过哈希函数转换成一个整型数字,然后将这份数字对数组长度进行取余,取余结果就当作数组的下标,将value 存储在以该数字为下标的数组空间里。
  2. 当使用哈希表进行查询的时候,就是再次使用哈希函数将 key 转换为对应的数组下标,并定位到该空间获取 value

常见的哈希算法
哈希表的组成取决于哈希算法,也就是哈希函数的构成,下面列举几种常见的哈希算法。

  1. 直接定址法
    取关键字或关键字的某个线性函数值为散列地址。
    即 f(key) = key 或 f(key) = a*key + b,其中a和b为常数。
  2. 除留余数法
    取关键字被某个不大于散列表长度 m 的数 p 求余,得到的作为散列地址。
    即 f(key) = key % p, p < m。这是最为常见的一种哈希算法。
  3. 数字分析法
    当关键字的位数大于地址的位数,对关键字的各位分布进行分析,选出分布均匀的任意几位作为散列地址。
    仅适用于所有关键字都已知的情况下,根据实际应用确定要选取的部分,尽量避免发生冲突。
  4. 平方取中法
    先计算出关键字值的平方,然后取平方值中间几位作为散列地址。
    随机分布的关键字,得到的散列地址也是随机分布的。
  5. 随机数法
    选择一个随机函数,把关键字的随机函数值作为它的哈希值。
    通常当关键字的长度不等时用这种方法。

哈希hash冲突

哈希冲突是指哈希函数算出来的地址被别的元素占用了
key1 ≠ key2 , f(key1) = f(key2)
一般来说,哈希冲突是无法避免的,如果要完全避免的话,也就是一个值就有一个索引,这样一来,空间就会增大,甚至内存溢出。
解决办法:

  1. 线性探测
    使用hash函数计算出的位置如果已经有元素占用了,则向后依次寻找,找到表尾则回到表头,直到找到一个空位
  2. 开链
    每个表格维护一个链表list,如果hash函数计算出的格子相同,则按顺序存在这个list中
  3. 再散列
    发生冲突时使用另一种hash函数再计算一个地址,直到不冲突
  4. 二次探测
    使用hash函数计算出的位置如果已经有元素占用了,按照 1 2 1^2 12 2 2 2^2 22 3 2 3^2 32…的步长依次寻找,如果步长是随机数序列,则称之为伪随机探测
  5. 公共溢出区
    一旦hash函数计算的结果相同,就放入公共溢出区

3.常用数据结构

链表、栈、队列、树

  1. 链表
    是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列节点组成,这些节点不必在内存中相连。每个节点由 数据部分 Data 和链部分 Next,Next 指向下一个节点,这样当添加或者删除时,只需要改变相关 节点的 Next 的指向,效率很高

4.二叉查找树、红黑树

二叉树是每个结点最多有两个子树的树结构。通常子树被称作“左子树”(left subtree)和“右子树”;

平衡二叉树(AVL树)在符合二叉查找树左子树的键值小于根的键值,右子树的键值大于根的键值)的条件下,还满足任何节点的两个子树的高度最大差为1;

二叉查找树(中序遍历,时间O(n))
在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都要大于这个节点的值。

  1. 查找
    首先取根节点,如果它等于要查找的数据,则直接返回,如果小于要查找的数据,则在右子树中继续查找,如果大于要查找的数据,则在左子树中继续查找,也就是二分查找的思想,这样一直递归。
  2. 插入
    首先还是从根节点开始,然后依次它与节点的关系。如果要插入的数据比节点的数据大,并且节点的右子树为空,就将新数据直接插到右子节点的位置;如果不为空,就再递归遍历右子树,查找插入位置。同理,如果要插入的数据比节点的数据小,也是类似的操作。
  3. 删除
    如果要删除的节点没有子节点,只需要将父节点中,指向要删除节点的指针置为NULL,
    如果要删除的节点只有一个子节点(只有左子节点或者右子节点),只需要删除父节点中,指向要删除的指针,让它指向要删除的节点的子节点就可以了。
    如果要删除的节点上有两个子节点,要稍微复杂一点。首先找到这个节点的右子树中最小的节点,把它替换到要删除的节点,然后再删除这个最小节点。因为最小节点肯定没有左子节点。

红黑树
红黑树是一个近似平衡的二叉树,
7. 定义:
具有二叉查找树的特点;节点是黑色
每个叶子节点都是黑色的空节点(NIL),也就是说,叶子节点不存数据
任何相邻的节点都不能同时为红色,也就是说,红色节点是被黑色节点隔开的
每个节点,从该节点到达其可达的叶子节点是所有路径,都包含相同数目的黑色节点

5.STL常用容器

C++ STL从广义来讲包括了三类:算法,容器和迭代器。
算法包括排序,复制等常用算法,以及不同容器特定的算法。
容器就是数据的存放形式,包括序列式容器和关联式容器,序列式容器就是list,vector等,关联式容器就是set,map等。
迭代器就是在不暴露容器内部结构的情况下对容器的遍历。
顺序容器:
3. vector
是一种动态数组,具有连续的存储空间,支持快速随机访问。但在插入和删除操作方面,效率比较
底层
底层结构为数组,由于数组的特点,vector也具有以下特性:
1)、O(1)时间的快速访问;
2)、顺序存储,所以插入到非尾结点位置所需时间复杂度为O(n),删除也一样;
3)、扩容规则
当我们新建一个vector的时候,会首先分配给他一片连续的内存空间,如std::vector vec,当通过push_back向其中增加元素时,如果初始分配空间已满,就会引起vector扩容,其扩容规则在gcc下以2倍方式完成:
首先重新申请一个2倍大的内存空间
然后将原空间的内容拷贝过来;
最后将原空间内容进行释放,将内存交还给操作系统;
4. deque
和 vector 类似,支持快速随机访问。二者最大的区别在于,vector 只能在末端插入 数据,而 deque 支持双端插入数据。deque 空间的重新分配要比 vector 快,重新分配空间后,原有的元素是不需要拷贝的。
底层:
底层数据结构为一个中央控制器(map)和多个缓冲区,支持首位(中间不能)快速增删,也支持也随访问,deque 的内存空间分布是小片的连续小片间用链表相连中控器(map保存着一组指针,每个指针指向一段数据空间的起始位置,通过中控器可以找到所有的数据空间。如果中控器的数据空间满了,会重新申请一块更大的空间,并将中控器的所有指针拷贝到新空间中。
1.start迭代器:绑定到第一个有效的map结点和该结点对应的缓冲区。
2.finish迭代器:绑定到最后一个有效的map结点和该结点对应的缓冲区。
5. list
是一个
双向链表
,它的内存空间可以不连续,通过指针来进行数据的访问,导致其随机存储非常低效;但 list 可以很地支持任意地方的插入和删除,只需移动相应的指针即可
底层:双向链表

关联容器:

  1. map && multimap
    是一种关联性容器,该容器用唯一的关键字来映射相应的值,即具有 key-value 功能。map 内部自建一棵红黑树(一种自平衡二叉树),这棵树具有数据自动排序的功能,内部数据都是有序的。
    map与multimap的区别在于,multimap允许关键字重复,而map不允许重复。
    底层:
    根据红黑树的原理,map与multimap可以实现O(lgn)的查找,插入和删除
  2. unordered_map 与unordered_multimap
    无序排序,低层是哈希表,因此其查找时间复杂度理论上达到了O(n)
  3. set & multiset
    是一种关联性容器,set系与map系的区别在于map中存储的是,而set可以理解为关键字即值,即只保存关键字的容器。
    低层
    底层使用红黑树实现,插入删除操作时仅仅移动指针即可,涉及内存的移动和拷贝,所以效率比较高。set 中的元素都是唯一的,而且默认情 况下会对元素进行升序排列。所以在 set 中,要改变元素值必须先删除旧元素再插入新元素。不提供直接存取元素的任何操作函数, 只能通过迭代器进行间接存取
  4. queue
    是一个队列,实现先进先出功能,queue 不是标准的 STL 容器,却以标准的 STL 容器为基础。(stack和queue其实是适配器,而不叫容器,因为是对容器的再封装)
    底层:
    queue 是在 deque 的基础上封装的。
  5. stack
    实现先进后出的功能,和 queue 一样,也是内部封装了 deque
  6. priority_queue:
    底层数据结构一般为vector为底层容器堆heap为处理规则来管理底层容器实现。

5. 迭代器失效

  1. vector迭代器失效
    (1)当执行erase方法时,指向删除节点的迭代器全部失效,指向删除节点之后的全部迭代器也失效
    (2)当进行push_back()方法时,end操作返回的迭代器肯定失效
    (3)当插入(push_back)一个元素后,capacity返回值与没有插入元素之前相比有改变,则需要重新加载整个容器,此时first和end操作返回的迭代器都会失效。
    (4)当插入(push_back)一个元素后,如果空间未重新分配,指向插入位置之前的元素的迭代器仍然有效,但指向插入位置之后元素的迭代器全部失效。
  2. deque迭代器
    (1)对于deque,插入到除首尾位置之外的任何位置都会导致迭代器、指针和引用都会失效,但是如果在首尾位置添加元素,迭代器会失效,但是指针和引用不会失效
    (2)如果在首尾之外的任何位置删除元素,那么指向被删除元素外其他元素的迭代器全部失效
    (3)在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效。
  3. map
    对于map,当进行erase操作后,只会使当前迭代器失效,不会造成其他迭代器失效,这是因为map底层实现是由红黑树实现的,所以当删除一个元素时,会进行二叉树的调整,但每个节点在内存中的地址是没有改变的,改变的只是他们之间的指针指向。

6.为什么要有迭代器,不是有指针吗?

Iterator(迭代器)模式是运用于聚合对象的一种模式,通过运用该模式,使得我们可以在不知道对象内部表示的情况下,按照一定顺序(由iterator提供的 方法)访问聚合对象中的各个元素。迭代器不是指针,是类模板,表现的像指针,提供了比指针更高级的行为,相当于一种智能指针,他可以根据不同类型的数据结构来实现不同的++,–等操作。

7.bfs(广度优先搜索)和dfs(深度优先搜索)

广度优先遍历:指的是从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再依次遍历每个相邻节点的相邻节点。常用于搜索路径的最优解;
深度优先遍历:主要思路是从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底…,不断递归重复此过程,直到所有的顶点都遍历完成,它的特点是不撞南墙不回头,先走完一条路,再换一条路继续走。dfs就是搜索全部的解。
在DFS中关键点是递归以及回溯,在BFS中,关键点则是状态的选取和标记。bfs先进先出(队列),dfs先进后出(栈)
bfs 适用于求源点与目标节点距离近的情况,例如:求最短路径。dfs 更适合于求解一个任意符合方案中的一个或者遍历所有情况,例如:全排列、拓扑排序、求到达某一点的任意一条路径。

8.找到比K大的数,使用快排实现和堆数据结构实现

  1. 最小堆思路:
    维护一个K大小的最小堆,堆中元素个数小于K时,新元素直接进入堆;否则
    当堆顶小于新元素时,弹出堆顶,将新元素加入堆。
    解释:
    由于堆是最小堆,堆顶是堆中最小元素,新元素都会保证比堆顶小,否则新元素替换堆顶,故堆中K个元素是已扫描的元素里最大的K个;堆顶即为第K大的数。

  2. 快排思路:快速排序一趟之后,主元pivot到了他最终应该在的位置,如果是从大到小排序,那么一趟排序之后,他左边的数都是比它大的,他右边的数都是比它小的数,所以,如果我们要找出第K大的数,我们可以在一趟排序后,检查主元的位置和K是否对应
    if(pos+1 == k),因为下标是从0开始的,所以要加1。如果相等的话,就正好是要找的第K大的数,否则,if(pos+1>k),说明要找的数在主元的左边,我们就只需要对左边继续进行快排,这样可以节约时间。if(pos+1<k),就说明要找的数是在主元的右半部分。

二、C++基础

1.C++ 程序编译过程

编译过程分为四个过程:编译(编译预处理、编译、优化),汇编,链接。

  1. 预处理:将所有的#include头文件以及宏定义替换成其真正的内容,gcc的预处理是预处理器cpp来完成的,得到的是文本文件;
  2. 编译\优化:将源码 .cpp 文件翻译成 .s 汇编代码;
  3. 汇编:将汇编代码 .s 翻译成机器指令 .o 目标文件;
  4. 链接:汇编程序生成的目标文件,并不会立即执行,因为可能会出现:.cpp 文件中的函数引用了另一个 .cpp 文件中定义的符号或者调用了某个库文件中的函数。将多个目标文件以及所需的库文件(.so等)链接成最终的可执行文件exe
    在这里插入图片描述

静态连接和动态连接

  1. 静态链接:将程序调用的库一起打包到可执行文件中,这样执行时就不需要调用别的库了(浪费空间、更新困难,但是执行的时候运行速度快)
  2. 动态链接:代码被放到动态链接库或共享对象的某个目标文件中,是在程序执行时才载入引用的库,因此方便更新(节省内存、更新方便,但是每次执行都需要连接,存在一定的性能损失)

2.面向对象三大特性

面向对象:对象是指具体的某一个事物,这些事物的抽象就是类,类中包含数据(成员变量)和动作(成员方法)。

  1. 封装
    将具体的实现过程数据封装成一个,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏,例如将公共的数据或方法使用public修饰,而不希望被访问的数据或
    方法采用private修饰。
  2. 继承
    指可以让某个类型的对象获得另一个类型的对象的属性的方法子类继承父类的特征和行为,子类有父类的非 private 方法或成员变量,子类可以对父类的方法进行重写增强了类之间的耦合性但是当父类中的成员变量、成员函数或者类本身被 final关键字修饰时,修饰的类不能继承,修饰的成员不能重写或修改。
  3. 多态
    多态就是不同继承类对象,对同一消息做出不同的响应,同一个函数,在调用父类对象和子类对象的时候会产生不同的行为。(重载实现编译时多态,虚函数实现运行时多态)。

3.多态如何实现,为什么用多态

  1. 实现方法:
    基类的函数前加上 virtual 关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
  2. 实现过程
    (1)编译器在发现基类中有虚函数时,会自动为每个含有虚函数的类生成一份虚表,该表是一个一维数组,虚表里保存了虚函数的入口地址
    (2)编译器会在每个对象的前四个字节中保存一个虚表指针,指向对象所属类的虚表。在构造时,根据对象的类型去初始化虚指针vptr,从而让vptr指向正确的虚表,从而在调用虚函数时,能找到正确的函数;
    (3)在派生类定义对象时,程序运行会自动调用构造函数,在构造函数中创建虚表并对虚表初始化。在构造子类对象时,会先调用父类的构造函数,此时,编译器只“看到了”父类,并为父类对象初始化虚表指针,令它指向父类的虚表;当调用子类的构造函数时,为子类对象初始化虚表指针,令它指向子类的虚表
    (4)当派生类对基类的虚函数没有重写时,派生类的虚表指针指向的是基类的虚表;当派生类对基类的虚函数重写时,派生类的虚表指针指向的是自身的虚表;当派生类中有自己的虚函数时,在自己的虚表中将此虚函数地址添加在后面
    (5)这样指向派生类的基类指针在运行时,就可以根据派生类对虚函数重写情况动态的进行调用,从而实现多态性。
  3. 虚表:虚函数表的缩写,类中含有virtual关键字修饰的方法时,编译器会自动生成虚表
    虚表指针:在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚表的指针
  4. 多态的种类和表现形式?
    静态多态(编译器多态):用模板和函数重载实现

动态多态(运行时多态):用虚函数,派生类实现

为什么用多态:

1.代码重用封装可以隐藏实现细节,使得代码模块化;继承可以扩展已存在的代码模块(类);
2. 提高程序的可复用性:应用程序不必为每一个派生类编写功能调用,只需要对抽象基类进行处理即可。
3. 提高可扩充性和可维护性派生类的功能可以被基类的方法或引用变量所调用,可以解决项目中紧偶合的问题。
4. 【 耦合度讲的是模块与模块之间,代码与代码之间的关联度,通过对系统的分析把他分解成一个一个子模块,子模块提供稳定的接口,达到降低系统耦合度的目的,模块与模块之间尽量使用模块接口访问,而不是随意引用其他模块的成员变量。】

4.重载、重写、隐藏的区别

  1. 重载:是指同一可访问区内被声明几个具有不同参数列(参数的类型、个数、顺序)的同名函数,根据参数列表确定调用哪个函数,重载不关心函数返回类型。
  2. 隐藏:是指派生类的函数屏蔽了与其同名的基类函数,主要只要同名函数,不管参数列表是否相同,基类函数都会被隐藏。
  3. 重写(覆盖):是指派生类中存在重新定义的函数。函数名、参数列表、返回值类型都必须同基类中被重写的函数一致,只有函数体不同。
  4. 重写和重载的区别:
    范围区别:对于类中函数的重载或者重写而言,重载发生在同一个类的内部,重写发生在不同的类之间(子类和父类之间)。
    参数区别重载的函数需要与函数有相同函数名不同的参数列表,不关注函数的返回值类型;重写的函数的函数名、参数列表和返回值类型需要和原函数相同,父类中被重写的函数需要有 virtual 修饰。
    virtual 关键字重载的函数可以有也可没有,重写的函数基类中必须有 virtual关键字的修饰,
  5. 隐藏和重写,重载的区别:
    范围区别:隐藏与重载范围不同,隐藏发生在不同类中。
    参数区别:隐藏函数和被隐藏函数参数列表可以相同,也可以不同,但函数名一定相同;当参数不同时,无论基类中的函数是否被 virtual 修饰,基类函数都是被隐藏,而不是重写。

5.让类对象只在栈(堆)上分配空间

  1. operator new()设为私有即可禁止对象被new在堆上
    将operator new()函数和operator delete()函数进行重载,这两个函数不做任何事情不分配内存,并将他们设置为private
  2. 让编译器不能自动调用构造函数和析构函数(将构造函数和析构函数都设置为protected),但是创建堆对象也要由编译器调用构造函数和析构函数怎么办呢,定义两个函数create和destroy函数分别在内部调用构造函数和析构函数,这样就可以由编译器在堆上创建和释放对象。

6.虚函数和纯虚函数

  1. 虚函数:被 virtual 关键字修饰的成员函数,就是虚函数,可以直接使用必须实现,否则编译器报错;
  2. 纯虚函数:在类中声明时,加上 =0,必须在派生类中实现
    含有纯虚函数的类称为抽象类(只要含有纯虚函数这个类就是抽象类),类中只有接口,没有具体的实现方法;
    继承纯虚函数的派生类,如果没有完全实现基类纯虚函数,依然是抽象类,不能实例化对象。
  3. 虚函数和纯虚函数可以出现在同一个类中,该类称为抽象基类。
  4. 虚函数作用
    实现动态绑定,可以让成员函数操作一般化,用基类的指针指向不同的派生类的对象时, 基类指针调用其虚成员函数,则会调用其真正指向对象的成员函数, 而不是基类中定义的成员函数。 若不是虚函数,则不管基类指针指向的哪个派生类对象,调用时都 会调用基类中定义的那个函数。

7.虚函数的实现机制

  1. 实现机制:虚函数通过虚函数表来实现。虚函数的地址保存在虚函数表中,在类的对象所在的内存空间中,保存了指向虚函数表的指针(称为“虚表指针”),通过虚表指针可以找到类对应的虚函数表。
  2. 虚函数表解决了基类和派生类的继承问题和类中成员函数的覆盖问题,当用基类的指针来操作一个派生类的时候,这张虚函数表就指明了实际应该调用的函数。
  3. 虚函数表相关知识点:
    虚函数表存放的内容:类的虚函数的地址。
    虚函数表建立的时间编译阶段
    虚表指针保存的位置:虚表指针存放在对象的内存空间中最前面的位置,这是为了保证正确取到虚函数的偏移量。

8.析构函数和构造函数

  1. 析构函数为什么是虚函数
    因为基类的指针可以指向派生类对象,如果不是虚函数的话,删除基类的指针, 只会调用基类的析构函数而不会调用派生类的,这样派生类就不能完全被析构, 会造成内存泄漏。

  2. 构造函数不能是虚函数
    (1)没法创建对象:创建对象的时候要知道对象的类型,而虚函数是在运行的时候动态确定类型的,而构造函数调用的时候,对象还没有被创造,所以不知道类型,也就没办法创建对象。
    (2) 没有虚函数表:虚函数调用的时候需要虚函数表,虚函数表放在这个对象的内存空间中。而对象还没有创建的时候,也就没有虚函数表,也就无法调用虚函数。
    (3)没有意义虚函数主要用于在信息不全的情况下,能使重载的函数得到相应的调用。构造函数本身就是要初始化实例,那使用虚函数没有实际意义。

构造函数析构函数调用的顺序

(1) 类对象的初始化顺序:基类构造函数–>派生类成员变量的构造函数–>自身构造函数
(2) 析构函数 先调用派生类的,再调用成员类对象的,再调用基类的。

9.怎么初始化一个类的成员和顺序

成员变量在使用初始化列表初始化时,与构造函数中初始化成员列表的顺序无关,只与定义成员变量的顺序有关。

  1. 一般变量可以在初始化列表里或者构造函数里初始化,不能直接初始化或者类外初始化
  2. 静态成员变量static必须在类外初始化
  3. 常量const必须在初始化列表里初始化
  4. 静态常量必须只能在定义的时候初始化(定义时直接初始化)

一个派生类构造函数的执行顺序如下:
虚拟基类的构造函数(多个虚拟基类则按照继承的顺序执行构造函数)。
基类的构造函数(多个普通基类也按照继承的顺序执行构造函数)。
类类型的成员对象的构造函数(按照初始化顺序)
派生类自己的构造函数。

10.什么情况下必须用初始化列表

  1. 成员类型是没有默认构造函数的类
    若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
  2. const 成员或引用类型的成员
    因为 const 对象或引用类型只能初始化,不能对他们赋值。
    对于普通数据成员而言,其值的设定可以放在初始化阶段或者普通计算阶段完成
    对于 const类型和&引用类型数据成员,其初始化必须在初始化阶段完成。若通过普通计算阶段来初始化该值,编译器会报错:该变量未初始化。
  3. 作用
    ① 编译器会一一操作初始化列表,以适当的顺序在构造函数之内安插初始化操作,并且在任何显示用户代码之前;
    ② list中的项目顺序是由类中的成员声明顺序决定的,不是由初始化列表的顺序决定的;

11.深copy浅copy

  1. 浅拷贝是增加了一个指针,指向原来已经存在的内存。
  2. 深拷贝是增加了一个指针,并新开辟了一块空间让指针指向这块新开辟的空间。浅拷贝在多个对象指向一块空间的时候,释放一个空间会导致其他对象所使用的空间也被释放了,再次释放便会出现错误。

12.内存分配

在C++中内存分为5个区,分别是堆、栈、自由存储区、全局/静态存储区和常量存储区。

  1. :用于程序的内存动态分配
  2. :在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。
  3. 自由存储区:自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内存申请,该内存即为自由存储区。
  4. 全局/静态存储区:这块内存是在程序编译的时候就已经分配好的,在程序整个运行期间都存在。例如全局变量,静态变量。
  5. 常量存储区:这是一块比较特殊的存储区,他们里面存放的是常量(const),不允许修改。
    动态内存分配
    在程序执行的过程中动态地分配或者回收存储空间的分配内存的方法。
    动态内存分配不像数组等静态内存分配方法那样需要预先分配存储空间,而是由系统根据程序的需要即时分配,且分 配的大小就是程序要求的大小。

13.堆和栈的区别

  1. 申请方式:栈是系统自动分配,堆是程序员主动申请
  2. 是内存中连续的一块空间,向低地址扩展,最大容量是系统预定好的,在内存中的空间,向高地址扩展,是不连续的。
  3. 申请效率栈申请效率高,但是程序员无法控制,堆效率低
  4. 存放内容中存放局部变量,函数的参数;中存放的内容由程序员控制。
  5. 是指程序运行是申请的动态内存,而只是指一种使用堆的方法(即先进后出。
  6. 栈是先进后出的,但是于堆而言却没有这个特性,两者都是存放临时数据的地方。

14.数组和指针的区别

数组:数组是用于储存多个相同类型数据的集合。
指针:指针相当于一个变量,它存放的是其它变量在内存中的地址
区别:

  1. 赋值:同类型指针变量可以相互赋值数组不行
  2. 存储方式数组在内存中是连续存放的;指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。
  3. 求 sizeof数组所占存储空间的内存;在 32 位平台下,无论指针的类型是什么,sizeof(指针名)都是 4,在 64下都是 8
  4. 初始化方式不同。
  5. 传参方式数组传参时,会退化为指针;将整个数组拷贝一份传入函数时,将数组名看做常量指针,传数组首元素的地址
    一级指针传参可以接受的参数类型:(1)整形指针 (2)整型变量地址 (3)一维整型数组数组名;
    函数参数部分是二级指针时(1)二级指针变量(2)一级指针变量地
    3)一维指针数组的数组名

15.引用和指针的区别

  1. 指针是一个变量,存储的是一个地址,引用跟原来的变量实质上是同一个东西,是原变量的别名
  2. 指针所指向的内存空间在程序运行过程中可以改变,而引用所绑定的对象一旦绑定就不能改变。(是否可变)
  3. 指针在内存中占有存储内存空间,引用相当于变量的别名,在内存中会占4字节内存
  4. 指针可以为,但是引用必须绑定对象。(是否可为空)
  5. 指针可以有多级,但是引用只能一级。(是否能为多级)

引用在创建的时候必须初始化,只有在调用虚函数时,才能实现动态绑定

常量指针和指针常量的区别

  1. 常量指针
    常量指针本质上是个指针,只不过这个指针指向的对象是常量
    特点:const 的位置在指针声明运算符的侧。只要 const 位于的左侧,无论它在类型名的左边或右边,都表示指向常量的指针。
  2. 指针常量:
    指针常量的本质上是个常量,只不过这个常量的值是一个指针
    特点:const 位于指针声明操作符侧,表明该对象本身是一个常量,* 左侧表示该指针指向的类型。

16.类创建自动创建的函数

  1. **默认构造函数,析构函数,拷贝构造函数以及默认赋值函数。

17.malloc/free和new/delete的区别

  1. malloc、free函数,而new、delete关键字
  2. new 申请空间时,编译器会根据类型自行计算空间;malloc 在申请空间时,需要确定所申请空间的大小
  3. new 申请空间时,返回的类型是对象的指针类型,无需强制类型转换,是类型安全的操作符;malloc 申请空间时,返回的是 void* 类型,需要进行强制类型的转换,转换为对象类型的指针。
  4. new 分配失败时,会抛出 bad_alloc 异常,malloc 分配失败时返回空指针
  5. 对于自定义的类型,new 首先调用 operator new() 函数申请空间,然后调用构造函数进行初始化,最后返回自定义类型的指针;delete 首先调用析构函数,然后调用 operator delete() 释放空间malloc、free 无法进行自定义类型的对象的构造和析构
  6. new 操作符从自由存储区上为对象动态分配内存,而 malloc 函数从堆上动态分配内存。(自由存储区不等于堆)

18.struct和class的区别

  1. 默认访问控制不同:structpublic的,而classprivate
  2. 在继承关系,struct默认是public的,而classprivate,默认的防控属性取决于子类而不是基类
  3. class可用于定义模板参数,就像typename。但是strcut不用
  4. 比起C语言来,struct可以拥有静态成员、成员数据可进行初始化、拥有函数、也可以继承、甚至多态也支持

19.define宏定义和const的区别

  1. 编译阶段
    define是在编译的预处理阶段起作用,
    const是在编译、运行的时候起作用
  2. 安全性
    define只做替换,不做类型检查和计算,也不求解,容易产生错误,一般可以用大括号包含住全部的内容,要不然很容易出错
    const常量有数据类型,编译器可以对其进行类型安全检查
  3. 内存占用
    (1)define在内存中会产生多份相同的备份const在程序运行中只有一份备份,且可以执行常量折叠,能将复杂的的表达式计算出结果放入常量表
    (2)宏替换发生在编译阶段之前,属于文本插入替换;const作用发生于编译过程中。
    (3)define不检查类型const会检查数据类型。
    (4)define定义的数据没有分配内存空间,只是插入替换掉;const定义的变量只是值不能改变,但要分配内存空间

20.const的作用

  1. 不考虑类的情况
    (1)const常量在定义时必须初始化,之后无法更改
    (2)const形参可以接收const和非const类型的实参
  2. 考虑类的情况
    (1)const成员变量
    不能在类定义外部初始化,只能通过构造函数初始化列表进行初始化,并且必须有构造函数;不同类对其const数据成员的值可以不同,所以不能在类中声明时初始化
    (2)const成员函数
    const对象不可以调用非const成员函数;非const对象都可以调用;不可以改变非mutable(用该关键字声明的变量可以在const成员函数中被修改)数据的值

21.static的作用

  1. static 定义静态变量,静态函数
  2. 不考虑类的情况
    (1)隐藏。所有不加static的全局变量和函数具有全局可见性,可以在其他文件中使用,加了之后只能在该文件所在的编译模块中使用
    (2)默认初始化为0,包括未初始化的全局静态变量与局部静态变量,都存在全局未初始化区
    (3)静态变量在函数内定义,始终存在,且只进行一次初始化,具有记忆性,其作用范围与局部变量相同,函数退出后仍然存在,但不能使用
  3. 考虑类的情况
    (1)static成员变量:只与类关联,不与类的对象关联。定义时要分配空间,不能在类声明中初始化,必须在类定义体外部初始化,初始化时不需要标示为static;可以被非static成员函数任意访问。
    (2)static成员函数不具有this指针,无法访问类对象的非static成员变量和非static成员函数;不能被声明为const、虚函数和volatile可以被非static成员函数任意访问

22:volatile关键字的作用

①与const关键词对立,表示“可变的”,修饰变量时表明该变量需要从内存单元中读取,不能直接从寄存器缓存中拷贝。

②常用于并行设备的硬件寄存器,中断服务子程序访问到的变量,多线程之间被几个任务共享变量。

22.全局变量、局部变量、静态全局变量、静态局部变量的区别

C++ 变量根据定义的位置的不同的生命周期,具有不同的作用域,作用域可分为 6 种全局作用域,局部作用域,语句作用域,类作用域,命名空间作用域和文件作用域

  1. 从作用域看
    全局变量:具有全局作用域。全局变量只需在一个源文件中定义,就可以作用于所有的源文件。当然,其他不包含全局变量定义的源文件需要用 extern 关键字再次声明这个全局变量。
    静态全局变量:具有文件作用域。它与全局变量的区别在于如果程序包含多个文件的话,它作用于定义它的文件里,不能作用到其它文件里,即被 static 关键字修饰过的变量具有文件作用域 。这样即使两个不同的源文件都定义了相同名字的静态全局变量,它们也是不同的变量。
    局部变量:具有局部作用域。它是自动对象(auto),在程序运行期间不是一直存在,而是只在函数执行期间存在,函数的一次调用执行结束后,变量被撤销,其所占用的内存也被收回。
    静态局部变量:具有局部作用域它只被初始化一次,自从第一次被初始化直到程序运行结束都一直存在,它和全局变量的区别在于全局变量对所有的函数都是可见的,而静态局部变量只对定义自己的函数体始终可见。
  2. 从分配内存空间看
    静态存储区:全局变量,静态局部变量,静态全局变量。
    :局部变量。
    说明:
    静态变量和栈变量(存储在栈中的变量)、堆变量(存储在堆中的变量)的区别:静态变量会被放在程序的静态数据存储区(.data 段,中(静态变量会自动初始化),这样可以在下一次调用的时候还可以保持原来的赋值。而栈变量或堆变量不能保证在下一次调用的时候依然保持原来的值。
    静态变量和全局变量的区别:静态变量用 static 告知编译器,自己仅仅在变量的作用范围内可见。

23.初始化的和未初始化的全局变量分别放在什么地方?

未初始化和初值为0的全局变量存放在bss段,有初值的全局变量存放data段
**注意:BSS(Block Started by Symbol)通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。特点是:可读写的,在程序执行之前BSS段会自动清0。所以,未初始的全局变量在程序执行之前已经成0了。

24.inline 作用与内联函数

  1. inline 是一个关键字,可以用于定义内联函数
  2. 内联函数,像普通函数一样被调用,但是在调用时并不通过函数调用的机制而是直接在调用点处展开,这样可以大大减少由函数调用带来的开销,从而提高程序的运行效率。
  3. 使用方法
    类内定义成员函数默认是内联函数
    类外定义成员函数,若想定义为内联函数,需用inline关键字声明
    4.宏定义(define)和内联函数(inline)的区别
    4.1 内联函数是在编译时展开,而宏在编译预处理时展开;在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。
    4.2 内联函数是真正的函数,和普通函数调用的方法一样,在调用点处直接展开,避免了函数的参数压栈操作,减少了调用的开销。而宏定义编写较为复杂,常需要增加一些括号来避免歧义。
    4.3 宏定义只进行文本替换,不会对参数的类型、语句能否正常编译等进行检查。而内联函数是真正的函数,会对参数的类型、函数体内的语句编写是否正确等进行检查。

25.main函数之前执行什么

  1. 在调用main函数之前,会先进行初始化栈,打开标准输入输出错误流,把参数压栈。还有一些全局变量、对象和静态变量、对象的空间分配和赋初值。
  2. 在调用main函数之后,要销毁堆内存关闭标准输入,输出,错误流。

25.对main函数的了解,在main函数之前运行

解释一下这三个参数:
第一个参数是一个整型,其中包括的是命令行参数的个数,第二参数是一个二维的char型指针,存放命令行参数字符串,第三参数是二维的char型指针,存放环境变量。
attribute ((constructor))会使函数在main()函数之前被执行
attribute ((destructor))会使函数在main()退出后执行;

26. 内存溢出

内存溢出(OutOfMemory)是指程序申请内存时,没有足够的内存供申请者使用。

  1. 内存溢出的原因:
    1.内存中加载的数据量过于庞大,如一次从数据库取出过多数据。
    2.集合类中有对象的引用,使用完后为清空,使得不能回收
    3.代码中存在死循环或循环产生过多重复的对象实体。

27.什么是内存泄漏,怎么防止和检测

  1. 内存泄漏:由于疏忽或错误导致的程序未能释放已经不再使用的内存,造成了内存的浪费。一般是:调用了malloc/new等内存申请的操作,但缺少了对应的free/delete,
  2. 防止:
    (1)计数法:使用new或者malloc时,让该数+1,delete或free时,该数-1,程序执行完打印这个计数,如果不为0则表示存在内存泄露
    (2)一定要将基类析构函数声明为虚函数
    (3)对象数组的释放一定要用delete []
    (4)有new就有delete,有malloc就有free,保证它们一定成对出现
    (5)智能指针:智能指针是 C++ 中已经对内存泄漏封装好了一个工具,可以直接拿来使用。
  3. 后果
    只发生一次小的内存泄漏可能不被注意,但泄漏大量内存的程序将会出现各种证照:性能下降到内存逐渐用完,导致另一个程序失败;
  4. 检测:
    (1) mtrace :检测一些内存分配和泄漏的失败等.(linux下)
    使用方法:程序开始时调用mtace()函数
    mtrace 会将内存情况记录下来存在.log 文件中,存放结果可由环境变量malloc_trace 设定。
    #gcc -o test test.c -g ;
    #./test ;
    #mtrace ./test malloc.log 会显示多少行出现问题,内存没释放。
#include <stdlib.h>
#include <mcheck.h> 
int main(void) {  
    mtrace(); /* 开始记录内存分配和释放 */
    int* a = NULL; 
    a = malloc(sizeof(int)); /* 分配内存并将其分配给指针 */
    if (a == NULL) {
        return 1; /* error */
    } 
    free(a); /*我们释放分配的内存,这样就不会有泄漏*/
    muntrace(); 
    return 0;  
}

(2) valgrind :(linux)
Valgrind包括如下一些工具:
Memcheck。这是valgrind应用最广泛的工具,一个重量级的内存检查器,能够发现开发中绝大多数内存错误使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。这也是本文将重点介绍的部分。
Callgrind。它主要用来检查程序中函数
调用过程
中出现的问题。
Cachegrind。它主要用来检查程序中缓存使用出现的问题。
Helgrind。它主要用来检查多线程程序中出现的竞争问题。
Massif。它主要用来检查程序中堆栈使用中出现的问题。
Extension。可以利用core提供的功能,自己编写特定的内存调试工具
6. Memcheck
$ valgrind --tool=memcheck ./val 命令,而我们想使用的工具是通过’-tool’选项来指定的. 上面的‘a.out’指的是我们想使用memcheck运行的可执行文件.

28.段错误产生的原因

  1. 原因主要有
    解引用空指针
    访问不可访问的内存空间(如内核空间)
    访问不存在的内存地址
    试图写一个只读内存空间(如代码段)
    栈溢出(函数递归调用)
    使用未初始化的指针(定义时没有初始化或者已经回收)
  2. 避免段错误
    定义指针后初始化
    数组下标是否越界
    在堆上分配空间是否足够(内存限制)
    变量处理时格式控制是否合理

29.智能指针有几种?实现原理,可能出现的问题

智能指针是存储指向动态分配(堆)对象指针的类,用于生存期控制,能够确保自动正确的销毁动态分配的对象,防止内存泄露(利用自动调用类的析构函数来释放内存)。

  1. 共享指针(shared_ptr):资源可以被多个指针共享,使用计数机制表明资源被几个指针共享。通过 use_count() 查看资源的所有者的个数,可以通过 unique_ptr、weak_ptr 来构造,调用 release() 释放资源的所有权,计数减一,当计数减为 0 时,会自动释放内存空间,从而避免了内存泄漏。
  2. 独占指针(unique_ptr):独享所有权的智能指针,资源只能被一个指针占有,该指针不能拷贝构造和赋值。但可以进行移动构造和移动赋值构造(调用 move() 函数),即一个 unique_ptr 对象赋值给另一个 unique_ptr 对象,可以通过该方法进行赋值。
  3. 如果希望只有一个智能指针管理资源或管理数组就用unique_ptr,如果希望多个智能指针管理同一个资源就用shared_ptr
  4. 弱指针(weak_ptr):指向 share_ptr 指向的对象,能够解决由shared_ptr带来的循环引用问题。weak_ptr不会使shared_ptr引用计数加一,它不管理shared_ptr内部的指针,主要是为了监视shared_ptr的生命周期,更像是shared_ptr的一个助手。

30.内存对齐/字节对齐

  1. 内存对齐:
    编译器将程序中的每个“数据单元”安排在字的整数倍的地址指向的内存之中
  2. 内存对齐的原则
    1.1 结构体变量的首地址能够被其最宽基本类型成员大小与对齐基数中的较小者所整除;
    2.2 结构体每个成员相对于结构体首地址的偏移量 (offset) 都是该成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在成员之间加上填充字节 (internal padding);
    3.3 结构体的总大小为结构体最宽基本类型成员大小与对齐基数中的较小者的整数倍,如有需要编译器会在最末一个成员之后加上填充字节 (trailing padding)。
  3. 进行内存对齐的原因:(主要是硬件设备方面的问题)
    3.1 某些硬件设备只能存取对齐数据,存取非对齐的数据可能会引发异常;
    3.2 某些硬件设备不能保证在存取非对齐数据的时候的操作是原子操作;
    3.3 相比于存取对齐的数据,存取非对齐的数据需要花费更多的时间;
    3.4 某些处理器虽然支持非对齐数据的访问,但会引发对齐陷阱(alignment trap);
    3.5 某些硬件设备只支持简单数据指令非对齐存取,不支持复杂数据指令的非对齐存取。
  4. 内存对齐的优点:
    4.1 便于在不同的平台之间进行移植,因为有些硬件平台不能够支持任意地址的数据访问,只能在某些地址处取某些特定的数据,否则会抛出异常;
    4.2 提高内存的访问效率,因为 CPU 在读取内存时,是一块一块的读取。
  5. 如何不进行内存对齐
    ①添加预处理指令 #pragma pack(1) 该文件下结构体在分配内存时按一个字节对齐;
    attribute ((packed)),定义结构体时使用关键字。

31.memcpy 函数(用于内存复制)

void *memcpy(void *destin, void *source, unsigned n);

作用是:以source指向的地址为起点,将连续的n个字节数据,复制到以destin指向的地址为起点的内存中。
函数有三个参数,第一个是目标地址,第二个是源地址,第三个是数据长度。

    char a[10] = "abcdefgh";
    unsigned n = 2;
    void * p = memcpy(a+3, a, n);

32.strcpy()(src 所指向的字符串复制到 dest)

char *strcpy(char *dest, const char *src) 

把 src 所指向的字符串复制到 dest。
缺陷:strcpy 函数不检查目的缓冲区的大小边界,而是将源字符串逐一的全部赋值给目的字符串地址起始的一块连续的内存空间,同时加上字符串终止符,会导致其他变量被覆盖。

33.菱形继承

B和C从A中继承,而D多重继承于B,C。那就意味着D中会有A中的两个拷贝。因为成员函数不体现在类的内存大小上,所以实际上可以看到的情况是D的内存分布中含有2组A的成员变量。
菱形继承带来了二义性,还会有有数据冗余浪费内存空间。

  1. 解决方法:
    虚拟继承
  2. 虚拟继承和普通继承的区别:
    对象中多了4个字节
    为子类中的构造函数中填充一个指针
    对象模型和普通的对象模型不一样,是颠倒过来的。
  3. 如何解决:
    在对象的前面添加4字节,用来存放虚基表指针,这个指针指向虚基表,表中存放的是偏移量,分别是子类对像对于自己的偏移量,和派生类对于基类的偏移量。
    通过偏移量即可访问到数据,所以只用保存一份数据即可,解决了二义性的问题。

35.回调函数

如果把函数的指针作为参数传递给一个函数,当这个指针被用来调用其所指向的函数时,就是回调函数。简单的说:把一段可执行的代码像参传递那样传给其他代码,而这段代码会在某个时刻被调用执行,就叫做回调。如果代码立即被执行称为同步回调,如果在之后晚点的某个时间再执行,称为异步回调。
回调函数的本质是对函数指针的应用
使用回调函数,和普通函数调用区别:
1)在主入口程序中,把回调函数像参数一样传入库函数。这样一来,只要我们改变传进库函数的参数,就可以实现不同的功能,且不需要修改库函数的实现,变的很灵活,这就是解耦。

2)主函数和回调函数是在同一层的,而库函数在另外一层。如果库函数对我们不可见,我们修改不了库函数的实现,也就是说不能通过修改库函数让库函数调用普通函数那样实现,那我们就只能通过传入不同的回调函数了,这也就是在日常工作中常见的情况。

36.C和C++的区别

①c面向过程,完成一件事情,按照步骤一步步完成;c++面对对象,将不同的食物看成一个对象,依靠多个对象之间的相互联系、相互操作,完成一定的功能。

②c++输入输出流不同

③c++有命名空间namespace

④c++有缺省函数,对每个函数的参数有一个初值

⑤c++函数重载,相同的函数,不同的函数列表

⑥c++有引用,c是指针,详情见标题6(引用和指针的区别)

⑦动态内存分配不同

⑧c++智能指针

⑨c++有stl,vector list等

⑩struct在c++可相当于class使用,唯一的区别是struct默认修饰词是public,calss默认修饰词是private

⑪const修饰的变量,在c中不能用于数组的下标,c++可以

四、操作系统

1.讲一下进程和线程的区别

  1. 进程是系统资源分配的最小单位,线程是程序执行的最小单位,一个进程可以有多个线程,一个线程只属于一个进程。

  2. 系统开销和资源共享:进程拥有自己独立的地址空间,每次启动一个进程系统会为它分配数据段、代码段、堆栈段,而线程与进程共享相同资源,仅仅拥有自己的局部变量(栈)和寄存器,共享全局变量、堆和文件等公共资源,因此cpu开销线程明显小于进程

  3. 消息通信:线程之间的通信很方便,但要注意互斥和同步,进程间通信用到IPC,主要有共享内存,消息队列,套接字,管道,信号量

  4. 健壮性:多进程程序不容易崩溃,多线程程序会因为某一线程掉线而崩溃

2.线程的哪些资源共享,那些资源不共享

  1. 由于堆是在进程空间中开辟出来的,所以它是理所当然地被共享的;因此new出来的都是共享的
  2. 全局变量 它是与具体某一函数无关的,所以也与特定线程无关;因此也是共享的
  3. 静态变量 虽然对于局部变量来说,它在代码中是“放”在某一函数中的,但是其存放位置和全局变量一样,存于堆中开辟的.bss和.data段,是共享的
  4. 文件等公用资源 这个是共享的,使用这些公共资源的线程必须同步。Win32 提供了几种同步资源的方式,包括信号、临界区、事件和互斥体。
    独享的资源有
  5. 栈是独享的
  6. 寄存器 因为电脑的寄存器是物理的,每个线程去取值难道不一样吗?其实线程里存放的是副本,包括程序计数器PC

3.进程间通信

目的:
数据传输:一个进程需要将它的数据发送给另一个进程。
资源共享:多个进程之间共享同样的资源。
通知事件:一个进程需要向另一个或一组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。
进程控制:有些进程希望完全控制另一个进程的执行(如Debug进程),此时控制进程希望能够拦截另一个进程的所有陷入和异常,并能够及时知道它的状态改变。

  1. 管道:
    一个进程通过调用管程的一个过程进入管程。在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用。
    (1)无名管道(内存文件):是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程之间使用。进程的亲缘关系通常是指父子进程关系。
    (2)有名管道(FIFO文件,借助文件系统):也是半双工的通信方式,但是允许在没有亲缘关系的进程之间使用,先进先出的通信方式。
  2. 共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的IPC方式,它是针对其他进程间通信方式运行效率低而专门设计的。
  3. 消息队列:消息队列是有消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号:传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点
  4. 套接字socket:适用于不同机器间进程通信,在本地也可作为两个进程通信的方式。
  5. 信号:用于通知接收进程某个事件已经发生,比如按下ctrl + C就是信号。
  6. 信号量:是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种机制,实 现进程、线程的对临界区的同步及互斥访问。

4.线程间通信

线程间的同步方式包括互斥锁、信号量、条件变量、读写锁

5.了解的锁机制?(线程同步)

互斥锁:mutex,保证在任何时刻,都只有一个线程访问该资源,当获取锁操作失败时,线程进入阻塞,等待锁释放。
读写锁:rwlock,分为读锁和写锁,处于读操作时,可以运行多个线程同时读。但写时同一时刻只能有一个线程获得写锁
互斥锁和读写锁的区别
(a)读写锁区分读锁和写锁,而互斥锁不区分
(b)互斥锁同一时间只允许一个线程访问,无论读写;读写锁同一时间只允许一个线程写,但可以多个线程同时读。
3. 自旋锁:spinlock,在任何时刻只能有一个线程访问资源。但获取锁操作失败时,不会进入睡眠,而是原地自旋,直到锁被释放。这样节省了线程从睡眠到被唤醒的时间消耗,提高效率。(可用于中断,互斥锁会引起阻塞)
4. 条件锁:就是所谓的条件变量,某一个线程因为某个条件未满足时可以使用条件变量使该程序处于阻塞状态。一旦条件满足了,即可唤醒该线程(常和互斥锁配合使用)
5. 信号量:计数器,允许多个线程同时访问同一个资源。

6.讲一下死锁

如果一组进程中的每个进程都在等待一个事件,而这个事件只能由该组的另一个进程触发,这种情况会导致死锁。可以理解为:死锁就是两个线程同时占用两个资源,但又在彼此等待对方释放锁。比如两只羊过独木桥。进程比作羊,资源比作桥。若两只羊互不相让,争着过桥,就产生死锁。
产生死锁的本质原因

  1. 系统资源有限。
  2. 进程推荐顺序不合理

4个必要条件:

1.互斥:进程对所需求的资源具有排他性,若有其他进程请求该资源,请求进程只能等待。
2.占有且等待:一个进程本身占有资源,同时还有资源未得到满足,正在等待其他进程释放资源。
3.不可抢占:进程在所获得的资源未释放前,不能被其他进程强行夺走,只能自己释放。
4.循环等待:存在一种进程资源循环等待链,使得每个进程都占有下一个进程所需的至少一种资源。

预防死锁:

资源一次性分配(破坏请求和保持条件)
可剥夺资源:当某新进程的资源未满足时,释放已占用的资源(破坏不可剥夺条件)
资源有序分配法:系统给每类资源赋予一个编号,每一个进程按编号递增的顺序请求资源,释放则相反(破坏环路等待条件)

避免死锁:

预防死锁的几种策略,会严重的损害系统性能,因此在避免死锁时,要施加较弱的限制,从而获得比较满意的系统性能。由于在避免死锁的策略中,允许进程动态地申请资源;因此:系统在进行资源分配之前预先计算资源分配的安全性,若此次分配不会导致系统进入不安全状态,则将资源分配给进程;否则,进程等待。其中具有代表性的算法有银行家算法

检测死锁

首先为每个进程和每个资源指定一个唯一的号码;然后建立资源分配表和进程等待表

解除死锁

当发现有进程死锁后,便应立把它从死锁状态中解脱出来,常采用的方法有:
剥夺资源:从其他进程剥夺足够数量的资源给死锁进程,以解除死锁状态;
撤销进程:可以直接传销死锁进程或撤销代价最小的进程,直至有足够的资源可用,死锁状态消除为止;所谓代价是指优先级、运行代价、进程的重要性和价值等。

7.虚拟内存

虚拟内存是计算机系统内存管理的一种技术。它使得应用程序认为它拥有连续的可用的内存 (一个连续完整的地址空间),而实际上,它通常是被分隔成多个物理内存碎片,还有部分暂时存储在外部磁盘存储器上,在需要时进行数据交换

8.大文件传输

  1. 基于socket
    由于socket本身缓冲区的限制,大概一次只能发送4K左右的数据,所以在传输大数据时客户端就需要进行分包,在目的地重新组包。
  2. 使用现有的通讯中间件
  3. 基于共享文件、ftp(文件传输协议)、scp等
    ftp:使用 TCP 传输

scp

用于在Linux下进行远程拷贝文件的命令,scp传输是加密的
scp 被复制目标 复制存储的目录

五、linux

1.内核与系统组成

Linux内核主要由五个子系统组成:进程调度,内存管理,虚拟文件系统,网络接口,进程间通信。
Linux系统一般有4个主要部分:内核、shell、文件系统和应用程序。

2. linux用户态和内核态

用户程序运行在用户态,操作系统运行在内核态.

  1. 用户态:
    当进程在执行用户自己的代码时(应用程序),则称其处于用户态,这时cpu 访问资源有限,运行在用户态下的程序不能直接访问操作系统内核数据结构和程序。
  2. 内核态:
    当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核状态,这时cpu可以访问计算机的任何资源。
    特权指令,只能内核态使用:I/O指令、置终端屏蔽指令、停机、修改程序状态字、清内存、建存储保护、设置时钟指令
  3. 用户态的应用程序可以通过三种方式来访问内核态的资源:
    1)系统调用
    2)库函数
    3)Shell脚本
  4. 用户态到内核态切换可以通过三种方式:
    (1)系统调用
    用户态进程主动要求切换到内核态的一种方式,用户态进程通过系统调用申请使用操作系统提供的服务程序完成工作,
    (2)异常
    当CPU正在执行运行在用户态的程序时,突然发生某些预先不可知的异常事件,这个时候就会触发从当前用户态执行的进程转向内核态执行相关的异常事件,典型的如缺页异常。
    (3)外设中断(硬中断):当外围设备完成用户的请求操作后,会像CPU发出中断信号,此时,CPU就会暂停执行下一条即将要执行的指令,转而去执行中断信号对应的处理程序

3.系统调用与普通函数调用的区别

  1. 系统调用
    1.使用INT和IRET指令,内核和应用程序使用的是不同的堆栈,因此存在堆栈的切换,从用户态切换到内核态,从而可以使用特权指令操控设备。
    2.依赖于内核,不保证移植性
    3.在用户空间和内核上下文环境间切换,开销较大
    4.是操作系统的一个入口点
  2. 普通函数调用:
    1.使用CALL和RET指令,调用时没有堆栈切换
    2.平台移植性好
    3.属于过程调用,调用开销较小
    4.一个普通功能函数的调用

4.中断处理流程

请求中断→响应中断→关闭中断→保留断点→中断源识别→保护现场→中断服务子程序→恢复现场→中断返回。

5.大小端问题

  1. 大端:低位字节存放在高地址,高位字节存放在低地址;
  2. 小端:高位字节存放在高地址,低位字节存放在低地址;
    STM32是小端模式,从低字节->高字节开始存放,而大端模式是从高字节->低字节开始存放

6.如何对绝对地址赋值

(unsigned int)0x1000000 = 1234;

如何将程序跳转到指定地址执行

typedef void (*)() voidfunc;

*((voidfunc)0x1000000)();

另外一种实现:((void()()0x1000000)()

3.I/O多路复用(select、poll和epoll)

I/O复用指的是允许计算机执行或者阻塞在一组数据流上,直到某个到达唤醒阻塞的进程,此时的I/O信道不仅仅是通过一个数据流,而是一组,所以是复用。

阻塞和非阻塞:拿I/O为例子,
如果是阻塞模型,那么程序一直会等到有数据来的时候才会继续向下执行,否则会一直等待数据的到来;
如果是非阻塞模型,如果有数据,那么直接读取数据向下执行,没有数据也会继续向下执行,不过此时可能会进行一些其他的操作,比如Linux中设置一些错误的比特位等。

select、poll和epoll这三个函数是Linux系统中I/O多路复用的系统调用函数。

  1. select (水平触发LT)
    (1)使用select 可以在一个线程内同时处理多个socket的IO请求。用户可以注 册多个socket,然后不断地调用select读取被激活的socket,即可达到在同一个线程内同时处理多个IO请求的目的
    (2)select运行机制
    select()的机制中提供一种fd_set的数据结构,实际上是一个long类型的数组每一个数组元素都能与一打开的文件句柄(不管是Socket句柄,还是其他文件或命名管道或 设备句柄)建立联系,建立联系的工作由程序员完成,当调用select()时由内核根据IO状态修改fd_set的内容(通过轮询所有的文件描述符来检查是否有事件发生),由此来通知执行了select()的进程哪一Socket或文件可读。

每个select都要处理一个fd_set结构
fd_set简单地理解为一个长度是1024的比特位,每个比特位表示一个需要处理的FD,如果是1,那么表示这个FD有需要处理的I/O事件,否则没有

(3)select机制的缺点
=每次调用select,都需要把fd_set集合从用户态拷贝到内核态,如果fd_set集合很大时,那这个开销也很大
=每次调用select都需要在内核遍历传递进来的所有fd_set,如果fd_set集合很大时,那这个开销也很大
=为了减少数据拷贝带来的性能损坏,内核对被监控的fd_set集合大小做了限制,并且这个是通过宏控制的,大小限制为1024
(4)优点:
可移植性好;
连接数少并且连接都十分活跃的情况下,效率也不错。

  1. poll(水平触发LT)
    调用过程和select类似,时间复杂度:O(n),其和select不同的地方:采用链表的方式替换原有fd_set数据结构,而使其没有连接数的限制。所以poll的文件描述符没有最大数量的限制,但是依然采用轮询遍历的方式检查是否有事件发生。
  2. epoll,时间复杂度:O(1)
    (1)运行机制
    epoll是基于事件驱动的I/O方式,相对于select来说, epoll没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件 描述符的事件存放到内核的一个事件表中,这时候epoll_wait将会接收到消息,并且将数据拷贝到用户空间,这样在用户空间和内核空间的 copy只需一 次
    具体是通过红黑树和就绪链表实现的,红黑树存储所有的文件描述符,就绪链表存储有事件发生的文件描述符;
    (2)步骤:
    第一步:epoll_create()系统调用。建立一个epoll对象,此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。
    第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。
    第三步:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。
    (3)优点
    接口使用方便:虽然拆分成了三个函数,但是反而使用起来更方便高效,不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离
    数据轻量拷贝:只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频 繁(而 select / poll 都是每次循环都要进行拷贝),开销变的很小
    事件回调机制:避免使用遍历,而是使用回调函数的方法,将就绪的文件描述符加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪,这和操作时间复杂度是O(1),即使文件描述符很多,效率也不会受到影响。
    没有数量限制:文件描述符无上限
    表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
  3. epoll的两种工作方式
    1.水平触发(LT)2.边缘触发(ET)
    LT模式:若就绪的事件一次没有处理完要做的事件,就会一直去处理。即就会将没有处理完的事件继续放回到就绪队列之中(即那个内核中的链表),一直进行处理。
    ET模式:就绪的事件只能处理一次,若没有处理完会在下次的其它事件就绪时再进行处理。而若以后再也没有就绪的事件,那么剩余的那部分数据也会随之而丢失。
    由此可见:ET模式的效率比LT模式的效率要高很多。只是如果使用ET模式,就要保证每次进行数据处理时,要将其处理完,不能造成数据丢失,这样对编写代码的人要求就比较高。
    在这里插入图片描述
    epoll读到一半又有新事件来了怎么办?
      避免在主进程epoll再次监听到同一个可读事件,可以把对应的描述符设置为EPOLL_ONESHOT,效果是监听到一次事件后就将对应的描述符从监听集合中移除,也就不会再被追踪到。读完之后可以再把对应的描述符重新手动加上。

4.Socket

Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。把复杂的TCP/IP协议族隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
发送接收方式

1、异步

报文发送和接收是分开的,相互独立的,互不影响。这种方式又分两种情况:
(1)异步双工:接收和发送在同一个程序中,由两个不同的子进程分别负责发送和接收
(2)异步单工:接收和发送是用两个不同的程序来完成。

2、同步

报文发送和接收是同步进行,既报文发送后等待接收返回报文。
同步方式一般需要考虑超时问题,即报文发出去后不能无限等待,需要设定超时时间,超过该时间发送方不再等待读返回报文,直接通知超时返回。
在长连接中一般是没有条件能够判断读写什么时候结束,所以必须要加长度报文头。读函数先是读取报文头的长度,再根据这个长度去读相应长度的报文。

5.调试程序(gdb命令)

GDB调试:gdb调试的是可执行文件,在gcc编译时加入 -g ,告诉gcc在编译时加入调试信息,这样gdb才能调试这个被编译的文件
gcc -g tesst.c -o test
GDB命令格式:

  1. quit:退出gdb,结束调试
  2. list:查看程序源代码
    (1) list 5,10:显示5到10行的代码
    (2)list test.c:5, 10: 显示源文件5到10行的代码,在调试多个文件时使用
    (3) list get_sum: 显示get_sum函数周围的代码
    (4)list test,c get_sum: 显示源文件get_sum函数周围的代码,在调试多个文件时使用
  3. reverse-search:字符串用来从当前行向前查找第一个匹配的字符串
  4. run:程序开始执行
  5. help list/all:查看帮助信息
  6. 条件断点:break if 条件 以条件表达式设置断点
  7. break:设置断点
    (1)break 7:在第七行设置断点
    (2)break get_sum:以函数名设置断点
    (3)break 行号或者函数名 if 条件:以条件表达式设置断点
  8. watch 条件表达式:条件表达式发生改变时程序就会停下来
  9. next:继续执行下一条语句 ,会把函数当作一条语句执行
  10. step:继续执行下一条语句,会跟踪进入函数,一次一条的执行函数内的代码
  11. 多进程下如何调试:用set follow-fork-mode child 调试子进程
    或者set follow-fork-mode parent 调试父进程

6.查看IP、GPU、显卡

  1. 查看ip:输入ifconfig -a,然后回车
  2. 查看显卡信息: lspci | grep -i vga
  3. 查看GPU使用情况:nvidia-smi

7.查看cpu、内存、磁盘(IO)使用率(top命令)

  1. 查看内存free (总内存、使用、空闲)
  2. free -g # 以 G 为单位显示内存使用状况
  3. 查看磁盘使用率df
  4. io状态查询:iostat -d -k 2
    参数 -d 表示,显示设备(磁盘)使用状态;-k某些使用block为单位的列强制使用Kilobytes为单位;2表示,数据显示每隔2秒刷新一次。

8.查看进程状态(PS命令)

用来查看当前运行的进程状态,一次性查看,如果需要动态连续结果使用 top

  1. 进程的状态
    运行(正在运行或在运行队列中等待)
    中断(休眠中, 受阻, 在等待某个条件的形成或接受到信号)
    不可中断(收到信号不唤醒和不可运行, 进程必须等待直到有中断发生)
    僵死(进程已终止, 但进程描述符存在, 直到父进程调用wait4()系统调用后释放)
    停止(进程收到SIGSTOP, SIGSTP, SIGTIN, SIGTOU信号后停止运行运行)
  2. 工具标识进程的5种状态码:
    D 不可中断 uninterruptible sleep (usually IO)
    R 运行 runnable (on run queue)
    S 中断 sleeping
    T 停止 traced or stopped
    Z 僵死 a defunct (”zombie”) process
  3. 命令参数:
    -A 显示所有进程
    -a 显示现行终端机下的所有程序,包括其他用户的程序
    c 显示进程真实名称
    e 显示环境变量
    f 显示进程间的关系
    r 显示当前终端运行的进程
    -aux 显示所有包含其它使用的进程

1.显示当前所有进程环境变量及进程间关系
ps -ef
2.显示当前所有进程
ps -A
3.与grep联用查找某进程
ps -aux | grep apache
4.找出与 cron 与 syslog 这两个服务有关的 PID 号码
ps aux | grep ‘(cron|syslog)’

9.grep命令(文本搜索)

强大的文本搜索命令

//参数
-A n --after-context显示匹配字符后n行
-B n --before-context显示匹配字符前n行
-C n --context 显示匹配字符前后n行
-c --count 计算符合样式的列数
-i 忽略大小写
-l 只列出文件内容符合指定的样式的文件名称
-f 从文件中读取关键词
-n 显示匹配内容的所在文件中行数
-R 递归查找文件夹

//grep 的规则表达式:
^ #锚定行的开始 如:’^grep’匹配所有以grep开头的行。
$ #锚定行的结束,如:‘grep$‘匹配所有以grep结尾的行。
. 匹配一个非换行符的字符 如:‘gr.p’匹配gr后接一个任意字符,然后是p。
[] #匹配一个指定范围内的字符,如’[Gg]rep’匹配Grep和grep。
[^] #匹配一个不在指定范围内的字符,如:’[^A-FH-Z]rep’匹配不包含A-R和T-Z的一个字母开头,紧跟rep的行。
(…) #标记匹配字符,如’(love)’,love被标记为1。
< #锚定单词的开始,如:’<grep’匹配包含以grep开头的单词的行。
> #锚定单词的结束,如’grep>'匹配包含以grep结尾的单词的行。
x{m} #重复字符x,m次,如:'0{5}'匹配包含5个o的行。
x{m,} #重复字符x,至少m次,如:'o{5,}'匹配至少有5个o的行。
x{m,n} #重复字符x,至少m次,不多于n次,如:'o{5,10}'匹配5–10个o的行。
\w #匹配文字和数字字符,也就是[A-Za-z0-9],如:'G\w*p’匹配以G后跟零个或多个文字或数字字符,然后是p。
\W #\w的反置形式,匹配一个或多个非单词字符,如点号句号等。
\b #单词锁定符,如: '\bgrep\b’只匹配grep。

//查找指定进程
ps -ef | grep svn

//查找指定进程个数
ps -ef | grep svn -c

//从文件中读取关键词
cat test1.txt | grep -f key.log

//显示包含 ed 或者 at 字符的内容行
grep -E ‘ed|at’ test.txt

10.搜索文件(find)

find [path][options][expression]path

11.linux更改用户权限

chmod +/-rwx 文件名|目录名

七、设计模式

常见C++设计模式面试题和场景题
设计模式有 6 大设计原则:
单一职责原则:就一个类而言,应该仅有一个引起它变化的原因。
开放封闭原则:软件实体可以扩展,但是不可修改。即面对需求,对程序的改动可以通过增加代码来完成,但是不能改动现有的代码。
里氏代换原则:一个软件实体如果使用的是一个基类,那么一定适用于其派生类。即在软件中,把基类替换成派生类,程序的行为没有变化。
依赖倒转原则:抽象不应该依赖细节,细节应该依赖抽象。即针对接口编程,不要对实现编程。
迪米特原则:如果两个类不直接通信,那么这两个类就不应当发生直接的相互作用。如果一个类需要调用另一个类的某个方法的话,可以通过第三个类转发这个调用。
接口隔离原则:每个接口中不存在派生类用不到却必须实现的方法,如果不然,就要将接口拆分,使用多个隔离的接口。
设计模式分为三类
创造型模式:单例模式、工厂模式、建造者模式、原型模式
结构型模式:适配器模式、桥接模式、外观模式、组合模式、装饰模式、享元模式、代理模式
行为型模式:责任链模式、命令模式、解释器模式、迭代器模式、中介者模式、备忘录模式、观察者模式、状态模式、策略模式、模板方法模式、访问者模式

1.单例模式

单例模式:单例模式只允许创建一个活动的对象(实例),提供了对唯一实例的受控访问。
单例实现原理:将能够创建对象的函数都设置为private,通过静态成员返回一个实例。有两种方式,一个是懒汉式,一个是饿汉式。懒汉式需要考虑加锁。
懒汉模式:直到第一次用到类的实例时才去实例化。存在的问题:每次判断实例对象是否为空,都要被锁定,如果是多线程的话,就会造成大量线程阻塞。
饿汉模式:类定义的时候就实例化。
应用场景:

  1. 表示文件系统的类,一个操作系统一定是只有一个文件系统,因此文件系统的类的实例有且仅有一个。
  2. 打印机打印程序的实例,一台计算机可以连接好几台打印机,但是计算机上的打印程序只有一个,就可以通过单例模式来避免两个打印作业同时输出到打印机。

实现方式:
单例模式可以通过全局或者静态变量的形式实现,这样比较简单,但是这样会影响封装性,难以保证别的代码不会对全局变量造成影响。

  1. 默认的构造函数、拷贝构造函数、赋值构造函数声明为私有的,这样禁止在类的外部创建该对象;
  2. 全局访问点也要定义成 静态类型的成员函数,没有参数,返回该类的指针类型。因为使用实例化对象的时候是通过类直接调用该函数,并不是先创建一个该类的对象,通过对象调用。

八、项目经验

1.失真校准算法是如何实现的?

射频芯片中存在常见的两种误差对系统的性能会产生较大的影响,发射链路主要包括发射的本振泄露、IQ失配;接收和反馈链路的直流偏置和IQ失配;
失真校准算法就是针对链路存在的这些误差或干扰进行补偿,从而提高系统的性能;

  1. TX的本振泄露:一方面是基带的直流偏置信号和本振经过混频后与信号同时发射出去产品本振泄漏,第二个是由于本振信号通过耦合直接泄漏到射频域产生本振泄漏;
    算法实现:通过在发射端加入直流分量的数字信号和链路直流分量进行叠加,抵消链路直流信号,最终相互抵消得到无本振泄漏的射频信号;
  2. TX的IQ失配:一是由于基带信号分为IQ两路,它们通过不同的DAC以及模拟链路,当模拟链路频率响应的不同时,导致IQ两路信号到达混频器端口时产生失配;第二个是由于混频器本身的本振信号存在增益或相位偏差,与基带信号的IQ两路混频后得到的射频信号也会存在IQ失配,导致镜像信号。
    算法实现:问题1:在数字域采用数字滤波器对IQ两路信号线分别进行预补偿,来抵消链路的响应特性,那么在IQ解调前就可以得到正交IQ信号;问题2:在数字域对IQ两路信号进行处理,将Q路信号做增益调整,并引入I路信号来调整Q路的相位,得到信号S_Q,该信号自身存在增益和相位偏差,与混频器的本振信号进行混频后,当设定适当的参数时即可将本振增益和相位差抵消掉。
    2:

2.dsp软件工作部分

MTS项目中的DSP软件主要承担了.全链路失真校准算法的计算任务(初始化校准和跟踪校准)以及初始化&跟踪校准的流程调度。模拟链路控制和链路的切换开关是调用bsp封装的接口函数。
DSP代码从调度方向来说分为对内和对外两大类型。
对内接口主要是算法计算相关的数学运算、链路数据采集、提参、下参等。对外接口主要是可维可测类,主要用于在线调试和状态监测等。

4.给定一个数组,求取第k大的值。

使用堆,
或者快排,基准元素的下标我们知道了,基准元素是第几大我们就清楚了啊!如果基准元素的下标为Index,时间O(N),空间O(1)

while (index == k){
    if (index > k)
        抛弃index右边所有的元素,对左边的元素继续排序
    else if (index < k)
        抛弃index左边所有的元素,对右边的元素继续排序
}
参与评论 您还未登录,请先 登录 后发表或查看评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
©️2022 CSDN 皮肤主题:游动-白 设计师:我叫白小胖 返回首页

打赏作者

夏时桃

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值