常考面试题
-
int main(int argc, char ** argv)函数中,参数argc和argv分别代表什么意思?⭐⭐⭐⭐
第一个参数,
int
型的argc
,为整型,用来统计程序运行时发送给main
函数的命令行参数的个数。第二个参数,
char*
型的argv[]
,为字符串数组,用来存放指向字符串的指针元素,每一个指针元素指向一个字符串参数。各成员含义如下:-
argv[0]
指向程序运行的全路径名 -
argv[1]
指向在DOS命令行中执行程序名后的第一个字符串 -
argv[2]
指向执行程序名后的第二个字符串。。。。。。
-
argv[argc-1]
指向执行程序名后的最后一个字符串 -
argv[argc]
为NULL
-
-
结构体和共用体的区别⭐⭐⭐⭐⭐
- struct和union都是由多个不同的数据类型成员组成。 struct的所有成员都存在;但在任何同一时刻, union中只存放了一个被选中的成员。
- 在不考虑字节对齐的情况下,struct变量的总长度等于所有成员长度之和。Union变量的长度等于最长的成员的长度。
- struct的不同成员赋值是互不影响的;而对于union的不同成员赋值, 将会对其它成员重写, 原来成员的值就不存在了。
-
使用共用体读写成员时需要注意什么?⭐⭐⭐⭐
共用体是共用内存空间,所以每个成员都是读写同一个内存空间,那么内存空间里面的内容不停的被覆盖,而同一时刻,都只能操作一个成员变量。否则会出现读错误。
-
do…while(0)的作用⭐⭐⭐⭐
- do…while(0)使复杂的宏在展开时,能够保留初始的语义,从而保证程序正确的逻辑。
- 避免使用goto控制程序流。由于goto不符合软件工程的结构化,而且有可能使得代码难懂,不倡导使用,这个时候我们可以使用do{...}while(0)来做同样的事情。
常考面试题
-
简述C++有几种传值方式,之间的区别是什么?⭐⭐⭐⭐
传参方式有这三种:值传递、引用传递、指针传递
-
值传递:形参即使在函数体内值发生变化,也不会影响实参的值;
-
引用传递:形参在函数体内值发生变化,会影响实参的值;
-
指针传递:在指针指向没有发生改变的前提下,形参在函数体内值发生变化,会影响实参的值;
-
-
为什么值传递不改变实参的值?⭐⭐⭐⭐
因为在函数传参的过程中,函数会为形参申请新的内存空间,并将实参的值复制给形参。形参的改变当然不会影响实参的值。
要想影响实参的值,可以使用指针传递。在C++中,可以使用引用传递。
-
全局变量和局部变量的区别⭐⭐⭐⭐
-
作用域不同:全局变量的作用域为整个程序,而局部变量的作用域为当前函数或循环等
-
内存存储方式不同:全局变量存储在全局数据区中,局部变量存储在栈区
-
生命期不同:全局变量的生命期和主程序一样,随程序的销毁而销毁,局部变量在函数内部或循环内部,随函数的退出或循环退出就不存在了
-
使用方式不同:全局变量在声明后程序的各个部分都可以用到,但是局部变量只能在局部使用。函数内部会优先使用局部变量再使用全局变量。
-
-
全局变量和局部变量如何初始化?⭐⭐⭐⭐
当局部变量被定义时,系统不会对其初始化,必须自行对其初始化。定义全局变量时,系统会自动初始化为下列值:
数据类型 初始化默认值 int 0 char '\0' float 0 double 0 pointer NULL 正确地初始化变量是一个良好的编程习惯,否则有时候程序可能会产生意想不到的结果。
-
请说说原码、反码、补码⭐⭐⭐⭐
整型数值在计算机的存储里,最左边的一位代表符号位,0代表正数,1代表负数。
(1)原码:为二进制的数,如:10 原码为0000 1010
(2)反码:正数的反码与原码相同:如:10 原码为0000 1010,反码为0000 1010
负数为原码0变1,1变0,(符号位不变):如:-10 原码为1000 1010,反码为1111 0101
(3)补码:正数的补码与原码相同:如:10 原码为0000 1010,补码为0000 1010
负数的补码为反码加1:如:-10 反码为1111 0101,补码为1111 0110
-
32位机器下,sizeof (char *)的大小是多少?64位机器下呢?⭐⭐⭐⭐
4个字节。
8个字节。
常考面试题
-
说说数组与指针⭐⭐⭐⭐⭐
-
数组是相同类型数据的集合。
引入数组就不需要在程序中定义大量的变量,大大减少了程序中变量的数量,使程序精炼,而且数组含义清楚,使用方便,明确地反映了数据间的联系。
许多好的算法都与数组有关,如洗牌算法、冒泡排序等。同时数组也是一种数据结构,它的特点就是可以常数时间复杂度O(1)地访问元素,但是插入与删除元素是O(n)的时间复杂度,所以当需要频繁插入删除元素时,尽量不用数组,或对数组进行一些改进优化,比如C++ vector容器就是在数组的基础上进行改进优化,提高了数组操作效率。
-
指针也是一种变量,但它和普通的变量的区别是,普通的变量存放的是实际的数据,而指针变量包含的是内存中的一块地址,这块地址指向某个变量或者函数。
指针是C/C++语言的核心的概念,大大提高了程序的灵活性,但是同时也隐藏着危机,如内存泄露、非法内存访问、野指针等。所以为了规避这些问题,在后来的C++11引入了智能指针帮助程序员。
-
-
说说数组和指针的区别⭐⭐⭐⭐⭐
-
概念:
(1)数组:数组是用于储存多个相同类型数据的集合。 数组名是首元素的地址。
(2)指针:指针相当于一个变量,但是它和普通变量不一样,它存放的是其它变量在内存中的地址。指针名指向了内存的首地址。
-
区别:
(1)赋值:同类型指针变量可以相互赋值;数组不行,只能一个一个元素的赋值或拷贝
(2)存储方式:
数组:数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下进行访问的,数组的存储空间,不是在静态区就是在栈上。
指针:指针本身就是一个变量,作为局部变量时存储在栈上。
(3)求sizeof:
数组所占存储空间的内存大小:sizeof(数组名)/sizeof(数据类型)
在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8。
(4)初始化:
//数组 int a[5] = {0}; char b[]={"Hello"};//按字符串初始化,大小为6. char c[]={'H','e','l','l','o','\0'};//按字符初始化 int *arr = new int[n];//创建一维数组 //指针 //指向对象的指针 int *p=new int(0) ; delete p; //指向数组的指针 int *p=new int[n]; delete[] p; //指向类的指针: class *p=new class; delete p;
-
-
数组指针与指针数组的区别⭐⭐⭐⭐⭐
数组指针是一个指针变量,指向了一个一维数组, 如
int (*p)[4]
,(*p)[4]
就成了一个二维数组,p也称行指针;指针数组是一个数组,只不过数组的元素存储的是指针变量, 如int *p[4]
。 -
指针函数与函数指针的区别⭐⭐⭐⭐⭐
(1)定义不同 指针函数本质是一个函数,其返回值为指针。 函数指针本质是一个指针,其指向一个函数。
(2)写法不同
指针函数:int *fun(int x,int y); 函数指针:int (*fun)(int x,int y);
(3)用法不同
指针函数返回一个指针。 函数指针使用过程中指向一个函数。通常用于函数回调的应用场景。
常考面试题
-
请说说内存分布模型⭐⭐⭐⭐⭐
如上图,从低地址到高地址,一个程序由代码段、数据段、BSS段、堆栈段组成。
-
代码段:存放程序执行代码的一块内存区域。只读,不允许修改,代码段的头部还会包含一些只读的常量,如字符串常量字面值(注意:const变量虽然属于常量,但是本质还是变量,不存储于代码段)。
-
数据段data:存放程序中已初始化的全局变量和静态变量的一块内存区域。
-
BSS 段:存放程序中未初始化的全局变量和静态变量的一块内存区域。
-
可执行程序在运行时又会多出两个区域:堆区和栈区。
**堆区:**动态申请内存用。堆从低地址向高地址增长。
栈区:存储局部变量、函数参数值。栈从高地址向低地址增长。是一块连续的空间。
-
最后还有一个文件映射区(共享区),位于堆和栈之间。
-
堆和栈的区别⭐⭐⭐⭐⭐
- 堆栈空间分配不同。栈由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等,栈有着很高的效率;堆一般由程序员分配释放,堆的效率比栈要低的多。
- 堆栈缓存方式不同。栈使用的是一级缓存, 它们通常都是被调用时处于存储空间中,调用完毕立即释放;堆则是存放在二级缓存中,速度要慢些。
- 空间大小: 栈的空间大小并不大,一般最多为2M,超过之后会报Overflow错误。堆的空间非常大,理论上可以接近3G。(针对32位程序来说,可以看到内存分布,1G用于内核空间,用户空间中栈、BSS、data又要占一部分,所以堆理论上可以接近3G,实际上在2G-3G之间)。
- 能否产生碎片: 栈的操作与数据结构中的栈用法是类似的。‘后进先出’的原则,以至于不可能有一个空的内存块从栈被弹出。因为在它弹出之前,在它上面的后进栈的数据已经被弹出。它是严格按照栈的规则来执行。但是堆是通过new/malloc随机申请的空间,频繁的调用它们,则会产生大量的内存碎片。这是不可避免地。
-
请你说说野指针⭐⭐⭐⭐⭐
野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
野指针不同于空指针,空指针是指一个指针的值为null,而野指针的值并不为null,野指针会指向一段实际的内存,只是它指向哪里我们并不知情,或者是它所指向的内存空间已经被释放,所以在实际使用的过程中,我们并不能通过指针判空去识别一个指针是否为野指针。
出现野指针的情况:
- 指针变量的值未被初始化: 声明一个指针的时候,没有显示的对其进行初始化,那么该指针所指向的地址空间是乱指一气的。如果指针声明在全局数据区,那么未初始化的指针缺省为空,如果指针声明在栈区,那么该指针会随意指向一个地址空间。
- 指针所指向的地址空间已经被free或delete:在堆上malloc或者new出来的地址空间,如果已经free或delete,那么此时堆上的内存已经被释放,但是指向该内存的指针如果没有人为的修改过,那么指针还会继续指向这段堆上已经被释放的内存,这时还通过该指针去访问堆上的内存,就会造成不可预知的结果,给程序带来隐患。
- 指针操作超越了作用域
-
如何避免野指针⭐⭐⭐⭐⭐
(1)初始化置NULL
(2)申请内存后判空:malloc申请内存后需要判空,而在现行C++标准中,如C++11,使用new申请内存后不用判空,因为发生错误将抛出异常。
(3)使用时不要超出指针作用域。
(4)指针释放后置NULL
(5)使用智能指针。
-
请你说说内存泄露⭐⭐⭐⭐⭐
简单地说就是申请了一块内存空间,使用完毕后没有释放掉。
(1)new和malloc申请资源使用后,没有用delete和free释放;
(2)子类继承父类时,父类析构函数不是虚函数。
(3)比如文件句柄、socket、自定义资源类没有使用对应的资源释放函数。
(4)shared_ptr共享指针成环,造成循环引用计数,资源得不到释放。
有以下几种避免方法:
第一:良好的编码习惯,使用了内存分配的函数,一旦使用完毕,要记得使用其相应的函数释放掉。
第二:将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。
第三:使用智能指针。
第四:一些常见的工具插件可以帮助检测内存泄露,如ccmalloc、Dmalloc、Leaky、Valgrind等等。
-
在函数中申请堆内存需要注意什么?⭐⭐⭐⭐⭐
(1)不要错误地返回指向“栈内存”的指针,因为该内存在函数结束时自动消亡。即函数内申请的临时数组,不要指望能够拿到数组内的内容,因为函数执行完成后,数组消亡。
(2)不要返回了常量区的内存空间。因为常量字符串,存放在代码段的常量区,生命期内恒定不变,只读不可修改。不可修改拿到也没有什么意义。
(3)通过传入一级指针不能解决,因为函数内部的指针将指向新的内存地址。
解决办法:
(1)使用二级指针
(2)通过指针函数解决,返回用malloc新申请的堆内存空间的地址,这样才能拿到内存内容。
-
请你说说内存碎片⭐⭐⭐⭐⭐
内存碎片通常分为内部碎片和外部碎片:
(1)内部碎片是由于采用固定大小的内存分区,当一个进程不能完全使用分给它的固定内存区域时就产生了内部碎片,通常内部碎片难以完全避免;
(2)外部碎片是由于某些未分配的连续内存区域太小,以至于不能满足任意进程的内存分配请求,从而不能被进程利用的内存区域。再比如堆内存的频繁申请释放,也容易产生外部碎片。
解决方法:
(1)段页式管理
(2)内存池
-
请你说说malloc内存管理原理⭐⭐⭐⭐
当开辟的空间小于 128K 时,调用 brk()函数;
当开辟的空间大于 128K 时,调用mmap()。
malloc采用的是内存池的管理方式,以减少内存碎片。先申请大块内存作为堆区,然后将堆区分为多个内存块。当用户申请内存时,直接从堆区分配一块合适的空闲快。采用隐式链表将所有空闲块连接,每一个空闲块记录了一个未分配的、连续的内存地址。
-
什么是内存池⭐⭐⭐⭐
内存池也是一种对象池,我们在使用内存对象之前,先申请分配一定数量的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。当不需要此内存时,重新将此内存放入预分配的内存块中,以待下次利用。这样合理的分配回收内存使得内存分配效率得到提升。
-
说说new和malloc的区别,各自底层实现原理⭐⭐⭐⭐⭐
- new是操作符,而malloc是函数。
- new在调用的时候先分配内存,在调用构造函数,释放的时候调用析构函数;而malloc没有构造函数和析构函数。
- malloc需要给定申请内存的大小,返回的指针需要强转;new会调用构造函数,不用指定内存的大小,返回指针不用强转。
- new可以被重载;malloc不行
- new分配内存更直接和安全。
- new发生错误抛出异常,malloc返回null
-
说说使用指针需要注意什么?⭐⭐⭐⭐⭐
- 定义指针时,先初始化为NULL。
- 用malloc或new申请内存之后,应该立即检查指针值是否为NULL。防止使用指针值为NULL的内存。在现行C++标准中,如C++11,使用new申请内存后不用判空,因为发生错误将抛出异常。
- 不要忘记为数组和动态内存赋初值。防止将未被初始化的内存作为右值使用。
- 避免数字或指针的下标越界,特别要当心发生“多1”或者“少1”操作
- 动态内存的申请与释放必须配对,防止内存泄漏
- 用free或delete释放了内存之后,立即将指针设置为NULL,防止“野指针”
-
初始化为0的全局变量在bss还是data⭐⭐⭐⭐⭐
BSS段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。
常考面试题
-
你怎么理解C语言和C++的区别?⭐⭐⭐⭐⭐
- C语言是C++的子集,C++可以很好兼容C语言。但是C++又有很多新特性,如引用、智能指针、auto变量等。
- C++是面向对象的编程语言,C++引入了新的数据类型——类,由此引申出了三大特性:(1)封装。(2)继承。(3)多态。而C语言则是面向过程的编程语言。
- C语言有一些不安全的语言特性,如指针使用的潜在危险、强制转换的不确定性、内存泄露等。而C++对此增加了不少新特性来改善安全性,如const常量、引用、cast转换、智能指针、try—catch等等;
- C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL(Standard Template Library)。STL的一个重要特点是数据结构和算法的分离,其体现了泛型化程序设计的思想。C++的STL库相对于C语言的函数库更灵活、更通用。
-
简述下C++语言的特点⭐⭐⭐⭐⭐
- C++在C语言基础上引入了面向对象的机制,同时也兼容C语言。
- C++有三大特性(1)封装。(2)继承。(3)多态;
- C++语言编写出的程序结构清晰、易于扩充,程序可读性好。
- C++生成的代码质量高,运行效率高,仅比汇编语言慢10%~20%;
- C++更加安全,增加了const常量、引用、四类cast转换(static_cast、dynamic_cast、const_cast、reinterpret_cast)、智能指针、try—catch等等;
- C++可复用性高,C++引入了模板的概念,后面在此基础上,实现了方便开发的标准模板库STL(Standard Template Library)。
- 同时,C++是不断在发展的语言。C++后续版本更是发展了不少新特性,如C++11中引入了nullptr、auto变量、Lambda匿名函数、右值引用、智能指针。
-
简述C++从代码到可执行二进制文件的过程⭐⭐⭐⭐
C++和C语言类似,一个C++程序从源码到执行文件,有四个过程,预编译、编译、汇编、链接。
-
预编译:这个过程主要的处理操作如下:
(1) 将所有的#define删除,并且展开所有的宏定义
(2) 处理所有的条件预编译指令,如#if、#ifdef
(3) 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。
(4) 过滤所有的注释,如//、/**/
(5) 添加行号和文件名标识。
-
编译:这个过程主要的处理操作如下:
(1) 词法分析:将源代码的字符序列分割成一系列的记号。
(2) 语法分析:对记号进行语法分析,产生语法树。
(3) 语义分析:判断表达式是否有意义。
(4) 代码优化:
(5) 目标代码生成:生成汇编代码。
(6) 目标代码优化:
-
汇编:这个过程主要是将汇编代码转变成机器可以执行的指令。
-
链接:将不同的源文件产生的目标文件进行链接,从而形成一个可以执行的程序。
我们以douya.cpp和main.cpp为例,两者从预编译、编译、汇编、链接的整个过程和linux指令如下:
-
最后我们就生成了可执行目标文件douya。
-
说说include头文件的顺序以及双引号""和尖括号<>的区别⭐⭐⭐⭐
-
区别:
(1)尖括号<>的头文件是系统文件,双引号""的头文件是自定义文件。
(2)编译器预处理阶段查找头文件的路径不一样。
-
查找路径:
(1)使用尖括号<>的头文件的查找路径:编译器设置的头文件路径-->系统变量。
(2)使用双引号""的头文件的查找路径:当前头文件目录-->编译器设置的头文件路径-->系统变量。
-
-
知道动态链接与静态链接吗?两者有什么区别⭐⭐⭐⭐
链接分为静态链接和动态链接。
-
静态链接,是在链接的时候就已经把要调用的函数或者过程链接到了生成的可执行文件中,就算你在去把静态库删除也不会影响可执行程序的执行;生成的静态链接库,Windows下以.lib为后缀,Linux下以.a为后缀。
-
而动态链接,是在链接的时候没有把调用的函数代码链接进去,而是在执行的过程中,再去找要链接的函数,生成的可执行文件中没有函数代码,只包含函数的重定位信息,所以当你删除动态库时,可执行程序就不能运行。生成的动态链接库,Windows下以.dll为后缀,Linux下以.so为后缀。
区别
-
静态链接是将各个模块的obj和库链接成一个完整的可执行程序;而动态链接是程序在运行的时候寻找动态库的函数符号(重定位)
-
静态链接运行快、可独立运行;动态链接运行较慢(事实上,动态库被广泛使用,这个缺点可以忽略)、不可独立运行。
-
静态链接浪费空间,存在多个副本,同一个函数的多次调用会被多次链接进可执行程序,当库和模块修改时,main也需要重编译;动态链接节省空间,相同的函数只有一份,当库和模块修改时,main不需要重编译。
-
-
导入C函数的关键字是什么,C++编译时和C有什么不同?⭐⭐⭐⭐⭐
-
关键字:在C++中,导入C函数的关键字是extern,表达形式为extern “C”, extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
-
编译区别:由于C++支持函数重载,因此编译器编译函数的过程中会将函数的参数类型也加到编译后的代码中,而不仅仅是函数名;而C语言并不支持函数重载,因此编译C语言代码的函数时不会带上函数的参数类型,一般只包括函数名。
-
-
请你说说什么是宏?
#define
命令是一个宏命令,它用来将一个标识符定义为一个字符串,该标识符被称为宏名,被定义的字符串称为替换文本。该命令有两种格式:一种是不带参数的宏定义,另一种是带参数的宏定义。
-
为什么要少使用宏?C++有什么解决方案?⭐⭐⭐⭐⭐
-
由程序编译的四个过程,知道宏是在预编译阶段被展开的。在预编译阶段是不会进行语法检查、语义分析的,宏被暴力替换,正是因为如此,如果不注意细节,宏的使用很容易出现问题。比如在表达式中忘记加括号等问题。
-
正因为如此,在C++中为了安全性,我们就要少用宏。
不带参数的宏命令我们可以用常量const来替代,比如
const int PI = 3.1415
,可以起到同样的效果,而且还比宏安全,因为这条语句会在编译阶段进行语法检查。而带参数的宏命令有点类似函数的功能,在C++中可以使用内联函数或模板来替代,内联函数与宏命令功能相似,是在调用函数的地方,用函数体直接替换。但是内联函数比宏命令安全,因为内联函数的替换发生在编译阶段,同样会进行语法检查、语义分析等,而宏命令发生在预编译阶段,属于暴力替换,并不安全。
-
-
请你说说内联函数,为什么使用内联函数?需要注意什么?⭐⭐⭐⭐⭐
-
C++ 内联函数是通常与类一起使用。如果一个函数是内联的,那么在编译时,编译器会把该函数的代码副本放置在每个调用该函数的地方。
如果想把一个函数定义为内联函数,则需要在函数名前面放置关键字 inline。
-
为什么使用内联函数?
函数调用是有调用开销的,执行速度要慢很多,调用函数要先保存寄存器,返回时再恢复,复制实参等等。
如果本身函数体很简单,那么函数调用的开销将远大于函数体执行的开销。为了减少这种开销,我们才使用内联函数。
-
内联函数使用的条件
-
以下情况不宜使用内联:
(1)如果函数体内的代码比较长,使用内联将导致内存消耗代价较高。
(2)如果函数体内出现循环,那么执行函数体内代码的时间要比函数调用的开销大。
-
内联不是什么时候都能展开的,一个好的编译器将会根据函数的定义体,自动地取消不符合要求的内联。
-
-
-
说说内联函数和宏函数的区别⭐⭐⭐⭐⭐
- 宏定义不是函数,但是使用起来像函数。预处理器用复制宏代码的方式代替函数的调用,省去了函数压栈退栈过程,提高了效率;而内联函数本质上是一个函数,内联函数一般用于函数体的代码比较简单的函数,不能包含复杂的控制语句,while、switch,并且内联函数本身不能直接调用自身。
- 宏函数是在预编译的时候把所有的宏名用宏体来替换,简单的说就是字符串替换 ;而内联函数则是在编译的时候进行代码插入,编译器会在每处调用内联函数的地方直接把内联函数的内容展开,这样可以省去函数的调用的开销,提高效率
- 宏定义是没有类型检查的,无论对还是错都是直接替换;而内联函数在编译的时候会进行类型的检查,内联函数满足函数的性质,比如有返回值、参数列表等
-
什么是字节对齐?为什么要字节对齐?⭐⭐⭐⭐⭐
-
为了使CPU能够对变量进行快速的访问,变量的起始地址应该具有某些特性,即所谓的“对齐”,比如4字节的int型,其起始地址应该位于4字节的边界上,即起始地址能够被4整除,也即“对齐”跟数据在内存中的位置有关。如果一个变量的内存地址正好位于它长度的整数倍,他就被称做自然对齐。
-
为什么要字节对齐?
(1)需要字节对齐的根本原因在于CPU访问数据的效率问题。
(2)一些系统对对齐要求非常严格,比如sparc系统,如果取未对齐的数据会发生错误,而在x86上就不会出现错误,只是效率下降。
(3)各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。比如有些平台每次读都是从偶地址开始
-
-
说说内联函数和函数的区别,内联函数的作用。⭐⭐⭐⭐⭐
- 内联函数比普通函数多了关键字inline
- 内联函数避免了函数调用的开销;普通函数有调用的开销
- 普通函数在被调用的时候,需要寻址(函数入口地址);内联函数不需要寻址。
- 内联函数有一定的限制,内联函数体要求代码简单,不能包含复杂的结构控制语句;普通函数没有这个要求。
内联函数的作用:内联函数在调用时,是将调用表达式用内联函数体来替换。避免函数调用的开销。
常考面试题
-
说说const和define的区别⭐⭐⭐⭐⭐
const用于定义常量;而define用于定义宏,而宏也可以用于定义常量。都用于常量定义时,它们的区别有:
- const生效于编译的阶段;define生效于预处理阶段。
- const定义的常量,在C语言中是存储在内存中、需要额外的内存空间的;define定义的常量,运行时是直接的操作数,并不会存放在内存中。
- const定义的常量是带类型的;define定义的常量不带类型。因此define定义的常量不利于类型检查。
-
说说const的作用⭐⭐⭐⭐⭐
-
const修饰普通类型的变量,告诉编译器某值是保持不变的。
-
const 修饰指针变量,根据const出现的位置和出现的次数分为三种
-
指向常量的指针:指针指向一个常量对象,目的是防止使用该指针来修改指向的值。
-
常指针:将指针本身声明为常量,这样可以防止改变指针指向的位置。
-
指向常量的常指针:一个常量指针指向一个常量对象。
-
-
const修饰参数传递,可以分为三种情况。
- 值传递的 const 修饰传递,一般这种情况不需要 const 修饰
- 当 const 参数为指针时,可以防止指针被意外篡改。
- 自定义类型的参数传递,需要临时对象复制参数,对于临时对象的构造,需要调用构造函数,比较浪费时间,因此我们采取 const 外加引用传递的方法。
-
const修饰函数返回值,分三种情况。
- const 修饰内置类型的返回值,修饰与不修饰返回值作用一样。
- const 修饰自定义类型的作为返回值,此时返回的值不能作为左值使用,既不能被赋值,也不能被修改。
- const 修饰返回的指针或者引用,是否返回一个指向 const 的指针,取决于我们想让用户干什么。
-
const修饰成员函数
const 修饰类成员函数,其目的是防止成员函数修改被调用对象的值,如果我们不想修改一个调用对象的值,所有的成员函数都应当声明为 const 成员函数。
-
-
const修饰函数的三个位置⭐⭐⭐⭐⭐
//修饰返回值 const int func(void); //修饰参数,说明不希望参数在函数体内被修改 int func(const int i); //修饰成员函数,其目的是防止成员函数修改被调用对象的值 int func(void) const;
-
说说
const int *a
,int const *a
,const int a
,int *const a
,const int *const a
分别是什么,有什么特点。⭐⭐⭐⭐⭐1. const int a; //指的是a是一个常量,不允许修改。 2. const int *a; //a指针所指向的内存里的值不变,即(*a)不变 3. int const *a; //同const int *a; 4. int *const a; //a指针所指向的内存地址不变,即a不变 5. const int *const a; //都不变,即(*a)不变,a也不变
-
说说静态局部变量,全局变量,局部变量的特点,以及使用场景⭐⭐⭐⭐
-
首先从作用域考虑:C++里作用域可分为6种:全局,局部,类,语句,命名空间和文件作用域。
全局变量:全局作用域,可以通过extern作用于其他非定义的源文件。
静态全局变量 :全局作用域+文件作用域,所以无法在其他文件中使用。
局部变量:局部作用域,比如函数的参数,函数内的局部变量等等。
静态局部变量 :局部作用域,只被初始化一次,直到程序结束。
-
从所在空间考虑:除了局部变量在栈上外,其他都在静态存储区。因为静态变量都在静态存储区,所以下次调用函数的时候还是能取到原来的值。
-
生命周期:局部变量在栈上,出了作用域就回收内存;而全局变量、静态全局变量、静态局部变量都在静态存储区,直到程序结束才会回收内存。
-
使用场景:从它们各自特点就可以看出各自的应用场景,不再赘述。
-
-
说说静态变量什么时候初始化?⭐⭐⭐⭐
对于C语言的全局和静态变量,初始化发生在任何代码执行之前,属于编译期初始化。
而C++标准规定:全局或静态对象当且仅当对象首次用到时才进行构造。
-
说说 static关键字的作用⭐⭐⭐⭐⭐
-
定义静态函数或全局变量:当我们同时编译多个文件时,在函数返回类型或全局变量前加上static关键字,函数或全局变量即被定义为静态函数或静态全局变量。静态函数或静态全局变量只能在本源文件中使用。这就是static的隐藏属性。
-
static 的第二个作用是保持变量内容的持久:在变量前面加上static关键字。初始化的静态变量会在数据段分配内存,未初始化的静态变量会在BSS段分配内存。直到程序结束,静态变量始终会维持前值。只不过全局静态变量和局部静态变量的作用域不一样。
-
static 的第三个作用是默认初始化为 0:全局变量也具备这一属性,因为全局变量也存储在静态数据区。在静态数据区,内存中所有的字节默认值都是 0x00 。
最后对 static 的三条基本作用做一句话总结。首先 static 的最主要功能是隐藏,其次因为 static 变量存放在静态存储区,所以它具备持久性和默认值0。
-
在c++中,static关键字可以用于定义类中的静态成员变量:使用静态数据成员,它既可以被当成全局变量那样去存储,但又被隐藏在类的内部。类中的static静态数据成员拥有一块单独的存储区,而不管创建了多少个该类的对象。所有这些对象的静态数据成员都共享这一块静态存储空间。
-
在c++中,static关键字可以用于定义类中的静态成员函数:与静态成员变量类似,类里面同样可以定义静态成员函数。只需要在函数前加上关键字static即可。如静态成员函数也是类的一部分,而不是对象的一部分。所有这些对象的静态数据成员都共享这一块静态存储空间。
-
-
为什么静态成员函数不能访问非静态成员⭐⭐⭐⭐⭐
静态成员函数不属于任何一个对象,因此C++规定静态成员函数没有this指针。既然它没有指向某一对象,也就无法对一个对象中的非静态成员进行访问。
-
静态成员函数与普通成员函数的区别⭐⭐⭐⭐⭐
- 静态成员函数没有 this 指针,只能访问静态成员(包括静态成员变量和静态成员函数)。
- 普通成员函数有 this 指针,可以访问类中的任意成员;而静态成员函数没有 this 指针。
常考面试题
-
说说volatile和mutable⭐⭐⭐⭐⭐
mutable是为了突破const的限制而设置的。被mutable修饰的变量,将永远处于可变的状态,即使在一个const函数中,甚至结构体变量或者类对象为const,其mutable成员也可以被修改。mutable在类中只能够修饰非静态数据成员。
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器每次会从内存里重新读取这个变量的值,而不是从寄存器里读取。特别是多线程编程中,变量的值在内存中可能已经被修改,而编译器优化优先从寄存器里读值,读取的并不是最新值。这就是volatile的作用了。
-
说说volatile的应用⭐⭐⭐⭐⭐
Volatile主要有三个应用场景:
(1)外围设备的特殊功能寄存器。
(2)在中断服务函数中修改全局变量。
(3)在多线程中修改全局变量。
-
在多线程中修改全局变量存在什么问题?怎么解决?⭐⭐⭐⭐⭐
在多线程中修改全局变量,编译器会优化代码,导致优先从寄存器里读值,读取的并不是最新值,而内存里的值可能已经改变。
可以使用volatile关键字修饰变量。
-
说说原子操作⭐⭐⭐⭐
原子操作(atomic operation)指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
原子操作类似互斥锁,但是原子操作比锁效率更高,这是因为原子操作更加接近底层,它的实现原理是基于总线加锁和缓存加锁的方式。
-
互斥锁有什么缺点?可以用什么替代?⭐⭐⭐⭐
互斥锁主要缺点是效率会低一些。可以使用原子锁替代。
原子操作(atomic operation)指的是由多步操作组成的一个操作。如果该操作不能原子地执行,则要么执行完所有步骤,要么一步也不执行,不可能只执行所有步骤的一个子集。
原子操作类似互斥锁,但是原子操作比锁效率更高,这是因为原子操作更加接近底层,它的实现原理是基于总线加锁和缓存加锁的方式。
-
说说引用和指针的区别⭐⭐⭐⭐⭐
(1)指针是实体,占用内存空间;引用是别名,与变量共享内存空间。
(2)指针不用初始化或初始化为NULL;引用定义时必须初始化。
(3)指针中途可以修改指向;引用不可以。
(4)指针可以为NULL;引用不能为空。
(5)sizeof(指针)计算的是指针本身的大小;而sizeof(引用)计算的是它引用的对象的大小。
(6)如果返回的是动态分配的内存或对象,必须使用指针,使用引用会产生内存泄漏。
(7)指针使用时需要解引用;引用使用时不需要解引用‘*’。
(8)有二级指针;没有二级引用。
-
说说左值和右值⭐⭐⭐⭐
C++ 中有两种类型的表达式:
- **左值(lvalue):**指向内存位置的表达式被称为左值(lvalue)表达式。左值可以出现在赋值号的左边或右边。
- **右值(rvalue):**术语右值(rvalue)指的是存储在内存中某些地址的数值。右值是不能对其进行赋值的表达式,也就是说,右值可以出现在赋值号的右边,但不能出现在赋值号的左边。
变量是左值,因此可以出现在赋值号的左边。数值型的字面值是右值,因此不能被赋值,不能出现在赋值号的左边。
-
说说右值引用的作用⭐⭐⭐⭐⭐
C++11引入右值引用主要是为了实现移动语义和完美转发。
移动语义为了避免临时对象的拷贝,为类增加移动构造函数。
完美转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征,那么它就是完美的。
-
说说移动语义的原理⭐⭐⭐⭐⭐
移动语义为了避免临时对象的拷贝,为类增加移动构造函数。移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间同时将要拷贝的对象复制过来,而是"拿"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr
-
多线程编程修改全局变量需要注意什么⭐⭐⭐⭐⭐
多线程编程中,变量的值在内存中可能已经被修改,而编译器优化优先从寄存器里读值,读取的并不是最新值。
解决办法:
-
全局变量加关键字volatile
-
使用原子操作,效率比锁高
-
使用互斥锁
常考面试题
-
说说面对对象和面对过程的区别?⭐⭐⭐
两者的区别就在于:面向过程的编程思想,就是关注问题解决的过程,按顺序一步一步执行解决问题。而面向对象的编程思想,是把构成问题的各个事务分解成各个对象,即问题建模。建立对象的目的不是为了完成一个步骤,而是为了描述一个事务在解决问题中经过的步骤和行为。
-
说说类的访问权限有几种⭐⭐⭐⭐
类中成员访问属性有三种:
(1)私有成员(变量和函数)只限于类成员访问,由
private
限定;(2)公有成员(变量和函数)允许类成员和类外的任何访问,由
public
限定;(3)受保护成员(变量和函数)允许类成员和派生类成员访问,不允许类外的任何访问。所以
protected
对外封闭,对派生类开放。 -
对象是值传递还是引用传递⭐⭐⭐⭐⭐
-
引用传递对象
通常,使用对象作为参数的函数时,应按引用而不是按值来传递对象,这样可以有效的提高效率。
-
原因
因为按值传递的时候,将会涉及到调用拷贝构造函数生成临时的拷贝,然后又调用析构函数,这在大型的对象上要比传递引用花费的时间多的多。当我们不修改对象的时候,应当将参数声明为const引用。
-
实例
void goodGay(Building &building){//引用传递 函数体 } void goodGay(Building building){//值传递 函数体 }
-
-
拷贝构造函数的参数类型为什么必须是引用⭐⭐⭐⭐⭐
如果拷贝构造函数中的参数不是一个引用,即形如CClass(const CClass c_class),那么就相当于采用了传值的方式(pass-by-value),而传值的方式会调用该类的拷贝构造函数,从而造成无穷递归地调用拷贝构造函数。因此拷贝构造函数的参数必须是一个引用。否则无法完成拷贝,而且栈也会满。
-
判断构造次数和析构次数⭐⭐⭐⭐⭐
如下面的例子,判断构造次数和析构次数
#include <iostream> using namespace std; class Myclass{ public: Myclass(int n){number=n;} //构造函数 Myclass(Myclass &other) {number=other.number;}//拷贝构造函数 ~Myclass(){}//析构函数 private: int number; }; Myclass fun(Myclass p){ Myclass temp(p); return temp; } int main(){ Myclass obj1(10),obj2(0); Myclass obj3(obj1); obj2=fun(obj3); return 0; }
解析:
(1)Myclass obj1(10),obj2(0); 这条语句调用了两次构造函数
(2)Myclass obj3(obj1); 这条语句直接调用了一次拷贝构造函数
(3)obj2=fun(obj3); 这条语句调用了三次拷贝构造函数,第一次是参数按值传递,使用拷贝构造函数创建了一个临时对象。第二次是函数内部使用拷贝构造函数初始化局部对象temp,第三次是按值返回需要用拷贝构造函数创建临时对象。
所以一共六次构造,六次析构。
-
说说友元函数⭐⭐⭐
-
定义
类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。
友元可以是一个函数,该函数被称为友元函数;
-
声明方式
如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend
-
调用方式
可以直接调用友元函数,不需要通过对象或指针。
-
缺陷
友元函数有权访问类的所有私有(private)成员和保护(protected)成员。破坏了类的封装性,并不建议使用友元
-
-
说说初始化列表的使用场景⭐⭐⭐⭐⭐
-
成员类型是没有默认构造函数的类。若没有提供显示初始化式,则编译器隐式使用成员类型的默认构造函数,若类没有默认构造函数,则编译器尝试使用默认构造函数将会失败。
-
const 成员或引用类型的成员。因为 const 对象或引用类型只能初始化,不能对他们赋值。
-
-
构造函数和初始化列表谁效率高?⭐⭐⭐⭐⭐
初始化列表效率高。因为比构造函数构造对象时,少一次对象拷贝。
-
下面这个例题,Student1有几个受保护的成员?⭐⭐⭐⭐⭐
class Student{ public: void display(); protected: int num; string name; char sex; }; class Student1:protected Student{ public: void display1(); private: int age; string addr; };
**保护继承中,基类的公有成员和保护成员被派生类继承后变成保护成员。**所以Student1有4个受保护的成员.
-
深拷贝与浅拷贝的区别⭐⭐⭐⭐⭐
浅拷贝只复制指向某个对象的指针,而不复制对象本身,新旧对象还是共享同一块内存。
但深拷贝会另外创造一个一模一样的对象,新对象跟原对象不共享内存,修改新对象不会改到原对象。
-
实现一个string类⭐⭐⭐⭐⭐
#include <iostream> #include <cstring> #include <vector> using namespace std; class MyString{ public: //构造函数 MyString(const char* str = nullptr) { if (str != nullptr) { m_data = new char[strlen(str) + 1]; strcpy(m_data, str); } else { m_data = new char[1]; *m_data = '\0'; } } // 拷贝构造函数 MyString(const MyString& other) { m_data = new char[strlen(other.m_data) + 1]; strcpy(m_data, other.m_data); } // 拷贝赋值函数 =号重载 MyString& operator=(const MyString& other) { if (this == &other) // 避免自我赋值!! return *this; delete[] m_data; m_data = new char[strlen(other.m_data) + 1]; strcpy(m_data, other.m_data); return *this; } ~MyString() { delete[] m_data; m_data = NULL; } private: char* m_data; };
-
说说this指针⭐⭐⭐⭐⭐
在每一个成员函数中都包含一个特殊的指针,这个指针的名字是固定的,称为this指针。它是指向本类对象的指针,它的值是当前被调用的成员函数所在的对象的起始地址。
对于一个类的实例来说,你可以看到它的成员函数、成员变量,但是实例本身呢?
this是一个指针,它时时刻刻指向你这个实例本身。
-
说说 C++中 struct 和 class 的区别⭐⭐⭐⭐
-
struct 一般用于描述一个数据结构集合,而 class 是对一个对象数据的封装;
-
struct 中默认的访问控制权限是 public 的,而 class 中默认的访问控制权限是 private 的,例如:
struct A{ int iNum; // 默认访问控制权限是 public } class B{ int iNum; // 默认访问控制权限是 private }
-
在继承关系中,struct 默认是公有继承,而 class 是私有继承;
-
class 关键字可以用于定义模板参数,就像 typename,而 struct 不能用于定义模板参数,例如:
template<typename T, typename Y> // 可以把typename 换成 class int Func(const T& t, const Y& y) { //TODO }
-
-
说说C++结构体和C结构体的区别⭐⭐⭐⭐
(1)C的结构体内不允许有函数存在,C++允许有内部成员函数,且允许该函数是虚函数。
(2)C的结构体对内部成员变量的访问权限只能是public,而C++允许public,protected,private三种。
(3)C语言的结构体是不可以继承的,C++的结构体是可以从其他的结构体或者类继承过来的。
(4)C 中使用结构体需要加上 struct 关键字,或者对结构体使用 typedef 取别名,而 C++ 中可以省略 struct 关键字直接使用。
-
nullptr调用成员函数可以吗?为什么?⭐⭐⭐⭐
能。
原因:因为在编译时对象就绑定了函数地址,和指针空不空没关系。
答案解析
//给出实例 class animal{ public: void sleep(){ cout << "animal sleep" << endl; } void breathe(){ cout << "animal breathe haha" << endl; } }; class fish :public animal{ public: void breathe(){ cout << "fish bubble" << endl; } }; int main(){ animal *pAn=nullptr; pAn->breathe(); // 输出:animal breathe haha fish *pFish = nullptr; pFish->breathe(); // 输出:fish bubble return 0; }
原因:因为在编译时对象就绑定了函数地址,和指针空不空没关系。
pAn->breathe();
编译的时候,函数的地址就和指针pAn绑定了;调用breath(*this)
, this就等于pAn。由于函数中没有需要解引用this的地方,所以函数运行不会出错,但是若用到this,因为this=nullptr
,运行出错。
常考面试题
-
析构函数必须为虚函数吗?构造函数可以为虚函数吗?⭐⭐⭐⭐⭐
C++默认析构函数不是虚函数,因为申明虚函数会创建虚函数表,占用一定内存,当不存在继承的关系时,析构函数不需要申明为虚函数。
若存在继承关系时,析构函数必须申明为虚函数,这样父类指针指向子类对象,释放基类指针时才会调用子类的析构函数释放资源,否则内存泄漏。
构造函数不能为虚函数,当申明一个函数为虚函数时,会创建虚函数表,那么这个函数的调用方式是通过虚函数表来调用。若构造函数为虚函数,说明调用方式是通过虚函数表调用,需要借助虚表指针,但是没构造对象,哪里来的虚表指针?但是没有虚表指针,怎么访问虚函数表从而调用构造函数呢?这就成了一个先有鸡还是先有蛋的问题。
-
当类存在继承的情况下,我们需要注意什么?⭐⭐⭐⭐⭐
若存在继承关系时,析构函数必须申明为虚函数,这样父类指针指向子类对象,释放基类指针时才会调用子类的析构函数释放资源,否则内存泄漏。
-
说说继承类型和访问属性⭐⭐⭐⭐
当一个类派生自基类,该基类可以被继承为 public、protected 或 private 几种类型。
我们几乎不使用 protected 或 private 继承,通常使用 public 继承。当使用不同类型的继承时,遵循以下几个规则:
- 公有继承(public):当一个类派生自公有基类时,基类的公有成员也是派生类的公有成员,基类的保护成员也是派生类的保护成员,基类的私有成员不能直接被派生类访问,但是可以通过调用基类的公有和保护成员来访问。
- 保护继承(protected): 当一个类派生自保护基类时,基类的公有和保护成员将成为派生类的保护成员。
- 私有继承(private):当一个类派生自私有基类时,基类的公有和保护成员将成为派生类的私有成员。
总结: 不管是哪种继承方式,派生类中新增成员可以访问基类的公有成员和保护成员,无法访问私有成员。但是只有公有继承中,派生类的对象能访问基类的公有成员。使用友元(friend)可以访问保护成员和私有成员。
-
构造与析构的顺序⭐⭐⭐⭐⭐
构造顺序:基类构造函数》对象成员构造函数》子类构造函数
析构顺序:子类析构函数》对象成员析构函数》基类析构函数
从里向外构造,从外向里析构
答案解析
我们给一个例子:
#include <iostream> using namespace std; class A{ public: A(){cout<<"A::constructor"<<endl;}; ~A(){cout<<"A::deconstructor"<<endl;}; }; class B{ public: B(){cout<<"B::constructor"<<endl;}; ~B(){cout<<"B::deconstructor"<<endl;}; }; class C : public A{ public: C(){cout<<"C::constructor"<<endl;}; ~C(){cout<<"C::deconstructor"<<endl;}; private: // static B b; B b; }; class D : public C{ public: D(){cout<<"D::constructor"<<endl;}; ~D(){cout<<"D::deconstructor"<<endl;}; }; int main(void){ C* pd = new D(); delete pd; return 0; }
运行结果如下:
A::constructor B::constructor C::constructor D::constructor C::deconstructor B::deconstructor A::deconstructor
-
请说说你对多态的理解⭐⭐⭐⭐⭐
利用虚函数,基类指针指向基类对象时就使用基类的成员(包括成员函数和成员变量),指向派生类对象时就使用派生类的成员。
换句话说,基类指针可以按照基类的方式来做事,也可以按照派生类的方式来做事,它有多种形态,或者说有多种表现方式,我们将这种现象称为多态(Polymorphism)。
-
重载和重写的区别⭐⭐⭐⭐⭐
-
重载(overload)
函数名相同,参数列表不同(参数类型、参数顺序),不能用返回值区分。
特点:
(1)作用域相同;
(2)函数名相同;
(3)参数列表必须不同,但返回值无要求;
特殊情况:若某一重载版本的函数前面有virtual关键字修饰,则表示它是虚函数,但它也是重载的一个版本。
作用效果:编译器根据函数不同的参数列表,将函数与函数调用进行早绑定,重载与多态无关,与面向对象无关,它只是一种语言特性。
-
重写(override)
派生类重定义基类的虚函数,既会覆盖基类的虚函数(多态)。
特点:
(1)作用域不同;
(2)函数名、参数列表、返回值相同;
(3)基类函数是virtual;
特殊情况:若派生类重写函数是一个重载版本,那么基类的其他同名重载函数将在子类中隐藏。
作用效果:父类指针和引用指向子类的实例时,通过父类指针或引用可以调用子类的函数,这就是C++的多态。
-
-
请你说说虚函数的工作机制⭐⭐⭐⭐⭐
C++实现虚函数的原理是虚函数表+虚表指针。
当一个类里存在虚函数时,编译器会为类创建一个虚函数表,虚函数表是一个数组,数组的元素存放的是类中虚函数的地址。
同时为每个类的对象添加一个隐藏成员,该隐藏成员保存了指向该虚函数表的指针。该隐藏成员占据该对象的内存布局的最前端。
所以虚函数表只有一份,而有多少个对象,就对应多少个虚函数表指针。
-
虚函数表在什么时候创建?每个对象都有一份虚函数表吗?⭐⭐⭐⭐⭐
当一个类里存在虚函数时,编译器会为类创建一个虚函数表,发生在编译期。
虚函数表只有一份,而有多少个对象,就对应多少个虚函数表指针。
-
函数重载是怎么实现的?⭐⭐⭐⭐⭐
在编译后,函数签名已经都不一样了,自然也就不冲突了。这就是为什么C++可以实现重名函数,但实际上编译后的函数签名是不一样的。
签名命名的方式是:_z+函数名字符个数+函数参数列表。
比如四个函数:
void display(char str) { cout << str << endl; }; void display(int i) { cout << i << endl; }; int display(double j) { cout << j << endl; }; double display(short k) { cout << k << endl; };
反编译后,对应的函数签名如下:
00000000004009df g F .text 0000000000000045 main 0000000000400926 g F .text 000000000000002d _Z7displayc 0000000000400953 g F .text 000000000000002a _Z7displayi 000000000040097d g F .text 0000000000000034 _Z7displayd 00000000004009b1 g F .text 000000000000002e _Z7displays
其中, 前缀 _z 是GCC的规定,7 是函数名display的字符个数,参数类型转换规则:int-->i,long-->l,char-->c,short-->s
可以看出来,函数重载与返回值类型没有关系。
-
纯虚函数了解吗?什么情况下使用?⭐⭐⭐⭐
包含纯虚函数的类称为抽象类(Abstract Class)。之所以说它抽象,是因为它无法实例化,也就是无法创建对象。原因很明显,纯虚函数没有函数体,不是完整的函数,无法调用,也无法为其分配内存空间。
抽象类通常是作为基类,让派生类去实现纯虚函数。派生类必须实现纯虚函数才能被实例化。
-
请说说操作符重载⭐⭐⭐⭐
我们可以重定义或重载大部分 C++ 内置的运算符。这样,就能使用自定义类型的运算符。
重载的运算符是带有特殊名称的函数,函数名是由关键字
operator
和其后要重载的运算符符号构成的。在理解时可将operator和运算符(如operator=)视为一个函数名。与其他函数一样,重载运算符有一个返回类型和一个参数列表。 -
为什么要使用操作符重载?⭐⭐⭐⭐
对于C++提供的所有操作符,通常只支持对于基本数据类型(如int、float)和标准库中提供的类(如string)的操作,而对于用户自己定义的类,如果想要通过该操作符实现一些基本操作(比如比较大小,判断是否相等),就需要用户自己来定义关于这个操作符的具体实现了。
-
哪些操作符不能重载?⭐⭐⭐⭐
下面是不可重载的运算符列表:
- .:成员访问运算符
- .*, ->*:成员指针访问运算符
- :::域运算符
- sizeof:长度运算符
- ?::条件运算符
- #: 预处理符号
因为这部分操作符如果重载,会造成语法、语义的混淆,因此不能被重载。
操作符被重载的基本前提:
1、只能为自定义类型重载操作符;
2、不能对操作符的语法(优先级、结合性、操作数个数、语法结构)、语义进行颠覆;
3、不能引入新的自定义操作符。
-
请说说多重继承的二义性⭐⭐⭐⭐
当类存在**菱形继承**时,如:
类 A 派生出类 B 和类 C,类 D 继承自类 B 和类 C。
这个时候类 A 中的成员变量和成员函数继承到类 D 中变成了两份,一份来自 A-->B-->D 这条路径,另一份来自 A-->C-->D 这条路径。
假如类 A 有一个成员变量 a,那么在类 D 中直接访问 a 就会产生歧义,编译器不知道它究竟来自 A -->B-->D 这条路径,还是来自 A-->C-->D 这条路径。
**解决方法**:**虚继承**。使得在派生类中只保留一份间接基类的成员。
15. #### 可以通过引用实现多态吗?⭐⭐⭐⭐⭐
可以。
引用也是可以的。我们给出实例:
```c++
#include <iostream>
using namespace std;
class Parent {
public:
Parent() {}
virtual void func() { cout << "Parent" << endl; }
};
class Child :public Parent {
public:
Child() {}
void func() { cout << "Child" << endl; }
};
int main() {
Parent parent;
Child child;
Parent &rp = parent;
Parent &rc = child;
rp.func();
rc.func();
system("Pause");
return 0;
}
```
运行结果如下:
```c++
Parent
Child
```
由于引用类似于常量,只能在定义的同时初始化,并且以后也要**从一而终**,不能再引用其他数据,所以本例中必须要定义两个引用变量,一个用来引用基类对象,一个用来引用派生类对象。
从运行结果可以看出,当基类的引用指代基类对象时,调用的是基类的成员,而指代派生类对象时,调用的是派生类的成员。
不过引用不像指针灵活,指针可以随时改变指向,而引用只能指代固定的对象,在多态性方面缺乏表现力,所以以后我们再谈及多态时一般是说**指针**。本例的主要目的是让你知道,除了指针,**引用也可以实现多态**。
16. #### 编译期间如何实现多态?执行期间如何实现多态?⭐⭐⭐⭐⭐
重载。
虚函数
常考面试题
-
迭代器和指针有什么区别?有了指针干嘛还要迭代器?⭐⭐⭐⭐⭐
迭代器不是指针,是类模板,表现的像指针。它只是模拟了指针的一些功能,通过重载了指针的一些操作符,如
-->
、*
、++
、--
等。迭代器封装了指针,是一个“可遍历STL容器内全部或部分元素”的对象,本质是封装了原生指针,是指针概念的一种提升,相当于智能指针。而迭代器的访问方式就是把不同集合类的访问逻辑抽象出来,使得不用暴露集合内部的结构而达到循环遍历集合的效果。这就是迭代器产生的原因。
-
前置 ++i 与后置 i++ 的区别⭐⭐⭐⭐⭐
- 赋值顺序不同:++ i 是先加后赋值;i ++ 是先赋值后加;++i和i++都是分两步完成的。
- 效率不同:后置++执行速度比前置的慢。
- i++ 不能作为左值,而++i 可以
- 两者都不是原子操作。
C语言是汇编层面的实现,后置++的汇编代码比前置++多了一行,那么执行就会多花一点时间。但是随着编译器的不断发展,这样的区别已经微乎其微了。
但是迭代器前置 ++i 与后置 i++ 的效率就有区别了。后置++要多生成一个局部对象 tmp,这个对象有可能包含很多的成员,因此执行速度比前置的慢。在次数很多的循环中,++i和i++可能就会造成运行时间上可观的差别了。
-
请你说说STL⭐⭐⭐
STL 是“Standard Template Library”的缩写,中文译为“标准模板库”。STL 是 C++ 标准库的一部分,不用单独安装。C++ 对模板(Template)支持得很好,STL 就是借助模板把常用的数据结构及其算法都实现了一遍,并且做到了数据结构和算法的分离。例如,vector 的底层为顺序表(数组),list 的底层为双向链表,deque 的底层为双端队列,set 的底层为红黑树,hash_set 的底层为哈希表。
通常认为,STL 是由容器、算法、迭代器、函数对象、适配器、内存分配器这 6 部分构成,其中后面 4 部分是为前 2 部分服务的
STL的组成 含义 容器 一些封装数据结构的模板类,例如 vector 向量容器、list 列表容器等。 算法 STL 提供了非常多(大约 100 个)的数据结构算法,它们都被设计成一个个的模板函数,这些算法在 std 命名空间中定义,其中大部分算法都包含在头文件 中,少部分位于头文件 中。 迭代器 在 C++ STL 中,对容器中数据的读和写,是通过迭代器完成的,迭代器就是容器和算法之间的桥梁。 函数对象 如果一个类将 () 运算符重载为成员函数,这个类就称为函数对象类,这个类的对象就是函数对象(又称仿函数)。 适配器 可以使一个类的接口(模板的参数)适配成用户指定的形式,从而让原本不能在一起工作的两个类工作在一起。值得一提的是,容器、迭代器和函数都有适配器。 内存分配器 为容器类模板提供自定义的内存申请和释放功能,由于往往只有高级用户才有改变内存分配策略的需求,因此内存分配器对于一般用户来说,并不常用。 -
vector如何正确删除重复元素⭐⭐⭐⭐⭐
主要是要防止删除元素时,迭代器失效的问题,在使用erase时要返回下一个元素的迭代器。
vector<int>::iterator iter=vec.begin(); for(; iter!=vec.end();){ if(*iter == 3) iter = vec.erase(iter); else ++iter; }
-
迭代器删除的问题⭐⭐⭐⭐⭐
vector主要是要防止删除元素时,迭代器失效的问题,在使用erase时要返回下一个元素的迭代器。
而map,set则不一样,map,set的数据结构采用的红黑树,删除当前元素时,不会影响到下一个元素的迭代器,所以在调用erase之前,记录下一个元素的迭代器即可。
而对于list来说,它的数据结构是链表,使用了不连续分配的内存,并且它的erase方法也会返回下一个有效的迭代器,因此两种方式都可采用。
-
请你说说函数模板与模板函数⭐⭐⭐⭐
函数模板的重点是模板。表示的是一个模板,专门用来生产函数。
模板函数是函数模板的一个实例化。
-
请你说说智能指针,智能指针为什么不用手动释放内存了?⭐⭐⭐⭐⭐
使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等。
正是因为指针存在这样的问题,C++便引入了智能指针来更好的管理堆内存。智能指针是利用了一种叫做RAII(资源获取即初始化)的技术对普通的指针进行封装,这使得智能指针实质是一个对象,行为表现的却像一个指针。
因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,自动释放资源。这样程序员就不用再担心内存泄露的问题了。
C++里面有四个指针:auto_ptr、unique_ptr、shared_ptr、weak_ptr,后面三个是C++11支持的,第一个被C++11弃用。
-
auto_ptr有什么样的问题⭐⭐⭐⭐⭐
看如下代码:
auto_ptr<string> p1 (new string ("I am jiang douya.")); auto_ptr<string> p2; p2 = p1; //auto_ptr不会报错.
auto指针存在的问题是,两个智能指针同时指向一块内存,就会两次释放同一块资源,存在潜在的内存崩溃问题!因此auto指针被C++11弃用。应该用unique指针替代auto指针。
-
unique_ptr指针实现原理⭐⭐⭐⭐⭐
unique指针规定一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存,编译报错。
我们只需要将拷贝构造函数和赋值拷贝构造函数申明为private或delete。不允许拷贝构造函数和赋值操作符
-
shared_ptr实现原理,来手撕一下⭐⭐⭐⭐⭐
实现原理:有一个引用计数的指针类型变量,专门用于引用计数,使用拷贝构造函数和赋值拷贝构造函数时,引用计数加1,当引用计数为0时,释放资源。
#include <iostream> #include <stdlib.h> using namespace std; template <typename T> class mysharedPtr { public: mysharedPtr(T* p = NULL); ~mysharedPtr(); mysharedPtr(const mysharedPtr<T>& other); mysharedPtr<T>& operator=(const mysharedPtr<T>& other); private: T* m_ptr; unsigned int* m_count; }; template <typename T> mysharedPtr<T>::mysharedPtr(T* p) { m_ptr = p; m_count = new unsigned int(0); ++(*m_count); cout << "Constructor is succeed!" << endl; } template <typename T> mysharedPtr<T>::~mysharedPtr() { --(*m_count); if ((*m_count) == 0) { delete[] m_ptr; m_ptr = NULL; delete[] m_count; m_count = NULL; cout << "Destructor is succeed!" << endl; } } template <typename T> mysharedPtr<T>::mysharedPtr(const mysharedPtr<T>& other) { m_ptr = other.m_ptr; m_count = other.m_count; ++(*m_count); cout << "Copy constructor is succeed!" << endl; } template <typename T> mysharedPtr<T>& mysharedPtr<T>::operator=(const mysharedPtr<T>& other) { // 《C++ primer》:“这个赋值操作符在减少左操作数的使用计数之前使other的使用计数加1, // 从而防止自身赋值”而导致的提早释放内存 ++(*other.m_count); --(*m_count); // 将左操作数对象的使用计数减1,若该对象的使用计数减至0,则删除该对象 if ((*m_count) == 0) { delete[] m_ptr; m_ptr = NULL; delete[] m_count; m_count = NULL; cout << "Left side object is deleted!" << endl; } m_ptr = other.m_ptr; m_count = other.m_count; cout << "Assignment operator overloaded is succeed!" << endl; return *this; }
常考面试题
-
shared_ptr会不会出现内存泄露?怎么解决?⭐⭐⭐⭐⭐
会出现内存泄露问题。
共享指针的循环引用计数问题:当两个类中相互定义shared_ptr成员变量,同时对象相互赋值时,就会产生循环引用计数问题,最后引用计数无法清零,资源得不到释放。
可以使用weak_ptr,weak_ptr是弱引用,weak_ptr的构造和析构不会引起引用计数的增加或减少。我们可以将其中一个改为weak_ptr指针就可以了。比如我们将class B里shared_ptr换成weak_ptr。
-
说说智能指针⭐⭐⭐⭐⭐
因为智能指针就是一个类,当超出了类的作用域时,类会自动调用析构函数,自动释放资源。这样程序员就不用再担心内存泄露的问题了。
C++里面有四个指针:auto_ptr、unique_ptr、shared_ptr、weak_ptr,后面三个是C++11支持的,第一个被C++11弃用。
-
auto指针存在的问题是,两个智能指针同时指向一块内存,就会两次释放同一块资源,存在潜在的内存崩溃问题!因此auto指针被C++11弃用。应该用unique指针替代auto指针。
-
unique指针规定一个智能指针独占一块内存资源。当两个智能指针同时指向一块内存,编译报错。
我们只需要将拷贝构造函数和赋值拷贝构造函数申明为private或delete。不允许拷贝构造函数和赋值操作符
-
shared_ptr实现共享式拥有概念。多个智能指针可以指向相同对象,该对象和其相关资源会在“最后一个引用被销毁”时候释放。
-
shared_ptr存在共享指针的循环引用计数问题。weak_ptr是弱引用,weak_ptr的构造和析构不会引起引用计数的增加或减少。我们可以将其中一个改为weak_ptr指针就可以了。比如我们将class B里shared_ptr换成weak_ptr
-
-
父子类相互转换可能存在什么问题?⭐⭐⭐⭐⭐
向上转换:指子类向基类转换。
向下转换:指基类向子类转换。
这两种转换,因为子类包含父类,子类转父类是可以任意转的。但是当父类转换成子类时可能出现非法内存访问的问题。
-
说一说cast类型转换⭐⭐⭐⭐⭐
C++为了将强制类型转换变得更加明确、控制强制转换的过程,主要将强制转换细化为四种cast转换方式:const_cast、static_cast、dynamic_cast、reinterpret_cast。
- const_cast用于强制去掉不能被修改的常数特性,但需要特别注意的是const_cast不是用于去除变量的常量性,而是去除指向常数对象的指针或引用的常量性,其去除常量性的对象必须为指针或引用。
- static_cast用于将一种数据类型强制转换为另一种数据类型。什么都可以转,最常用。
- dynamic_cast只能用于含有虚函数的类转换,用于类向上和向下转换。dynamic_cast通过判断变量运行时类型和要转换的类型是否相同来判断是否能够进行向下转换。
- reinterpret_cast主要有三种强制转换用途:改变指针或引用的类型、将指针或引用转换为一个足够长度的整形、将整型转换为指针或引用类型。
-
说一说lambda⭐⭐⭐⭐
在我们编程的过程中,我们常定义一些只会调用一次的函数,这样我们就还得老老实实写函数名、写参数,其实还挺麻烦的.
但是C++ 11 引入Lambda 表达式用于定义并创建匿名的函数对象,就可以简化我们的编程工作了。
Lambda 的语法形式如下:
[函数对象参数] (操作符重载函数参数) mutable 或 exception 声明 -> 返回值类型 {函数体}
-
lambda什么原理⭐⭐⭐⭐
原理:编译器会把一个lambda表达式生成一个匿名类的匿名对象,并在类中重载函数调用运算符
()
。我们举个简单的例子:
auto print = []{cout << "Douya" << endl; };
那么编译器生成一个匿名类的匿名对象,形式可能如下:
//用给定的lambda表达式生成相应的类 class print_class{ public: void operator()(void) const{ cout << "Douya" << endl; } }; //用构造的类创建对象,print此时就是一个函数对象 auto print = print_class();
可以看到匿名类里重载了函数调用运算符
()
。还生成了一个函数对象,那么我们就直接可以使用这个函数对象了。 -
lambda值捕获可以修改吗⭐⭐⭐⭐
默认情况下,按值捕获的变量是不可以被修改的。非要修改的话,可以在参数列表后加关键字mutable。
-
sizeof(lambda表达式)等于多少⭐⭐⭐⭐
既然编译器为lambda创建匿名类,那当然是sizeof(匿名类)的大小了呀。sizeof(类)=sizeof(所有成员变量)
常考面试题
-
操作系统的功能⭐⭐⭐
操作系统主要包括以下几个方面的功能 :
(1)CPU管理:其工作主要是进程调度,在单用户单任务的情况下,处理器仅为一个用户的一个任务所独占,进程管理的工作十分简单。但在多道程序或多用户的情况下,组织多个作业或任务时,就要解决处理器的调度、分配和回收等问题。
(2)存储管理,分为几种功能:存储分配、存储共享、存储保护、存储扩张。
(3)设备管理,分为以下功能:设备分配、设备传输控制、设备独立性。
(4)文件管理:文件存储空间的管理、目录管理、文件操作管理、文件保护。
(5)作业管理,是负责处理用户提交的任何要求。
-
请你说说CPU工作原理⭐⭐⭐⭐⭐
CPU的运行原理就是:控制单元在时序脉冲的作用下,将程序计数器里所指向的指令地址送到地址总线上去,然后CPU将这个地址里的指令读到指令寄存器进行译码。对于执行指令过程中所需要用到的数据,会将数据地址也送到地址总线,然后CPU把数据读到CPU的内部存储单元(就是内部寄存器)暂存起来,最后命令运算单元对数据进行处理加工。这个过程不断重复,直到程序结束。
-
请你说说CPU流水线⭐⭐⭐⭐⭐
CPU执行一条指令时,分为几个步骤,CPU并不会等一条指令完全执行完才执行下一条指令,而是像流水一样。
经典MIPS五级流水线将执行的生命周期分为五个部分:
-
取指
-
译码
-
执行
-
访存
-
写回
-
内核态与用户态的区别⭐⭐⭐⭐⭐
-
内核态与用户态:内核态(系统态)与用户态是操作系统的两种运行级别。内核态拥有最高权限,可以访问所有系统指令;用户态则只能访问一部分指令。
-
什么时候进入内核态:共有三种方式:a、系统调用。b、异常。c、设备中断。其中,系统调用是主动的,另外两种是被动的。
-
为什么区分内核态与用户态:在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。所以区分内核态与用户态主要是出于安全的考虑。
-
-
什么是系统调用⭐⭐⭐⭐⭐
Linux内核中设置了一组用于实现各种系统功能的子程序,称为系统调用。用户可以通过系统调用命令在自己的应用程序中调用它们。从某种角度来看,系统调用和普通的函数调用非常相似。区别仅仅在于,系统调用由操作系统核心提供,运行于核心态;而普通的函数调用由函数库或用户自己提供,运行于用户态。
系统调用的机制其核心还是使用了操作系统为用户特别开放的一个中断来实现,该中断是程序人员自己开发出的一种正常的异常,这个异常具体就是调用int $0x80的汇编指令,这条汇编指令将产生向量为0x80的编程异常。
产生中断(软中断)后,调用中断处理程序,调用System_call函数,就完成操作系统内核态的调用了。
-
请你说说并发和并行⭐⭐⭐⭐⭐
-
并发:对于单个CPU,在一个时刻只有一个进程在运行,但是线程的切换时间则减少到纳秒数量级,多个任务不停来回快速切换。
-
并行:对于多个CPU,多个进程同时运行。
-
区别。通俗来讲,它们虽然都说是"多个进程同时运行",但是它们的"同时"不是一个概念。并行的"同时"是同一时刻可以多个任务在运行(处于running),并发的"同时"是经过不同线程快速切换,使得看上去多个任务同时都在运行的现象。
-
-
请你说说物理内存层次⭐⭐⭐⭐
物理内存有四个层次,分别是寄存器、高速缓存、主存、磁盘。
寄存器:速度最快、量少、价格贵。
高速缓存:次之。
主存:再次之。
磁盘:速度最慢、量多、价格便宜。
操作系统会对物理内存进行管理,有一个部分称为内存管理器(memory manager),它的主要工作是有效的管理内存,记录哪些内存是正在使用的,在进程需要时分配内存以及在进程完成时回收内存。
-
说说存储类型⭐⭐⭐⭐⭐
ROM 只读存储器(Read-Only Memory) 是一种只能读出事先所存数据的固态半导体存储器。其特性是一旦储存资料就无法再将之改变或删除。
RAM 随机存取存储器(random access memory)又称作“随机存储器” 是与CPU直接交换数据的内部存储器,也叫主存(内存)。它可以随时读写,而且速度很快。
更多的存储类型如下表:
存储器类型 简介 作用 ROM 只读存储器(Read-Only Memory) 是一种只能读出事先所存数据的固态半导体存储器。其特性是一旦储存资料就无法再将之改变或删除。 RAM 随机存取存储器(random access memory)又称作“随机存储器” 是与CPU直接交换数据的内部存储器,也叫主存(内存)。它可以随时读写,而且速度很快。 SRAM 静态随机存取存储器(Static Random-Access Memory) 随机存取存储器的一种。所谓的“静态”,是指这种存储器只要保持通电,里面储存的数据就可以恒常保持。然而,当电力供应停止时,SRAM储存的数据还是会消失。 DRAM 动态随机存取存储器(DRAM) DRAM里面所储存的数据就需要周期性地更新。要刷新充电一次,否则内部的数据即会消失。 EPROM (Erasable Programmable ROM),可擦除可编程ROM 芯片通过紫外线可重复擦除和写入,解决了PROM芯片只能写入一次的弊端。EPROM芯片在写入资料后,还要以不透光的贴纸或胶布把窗口封住,以免受到周围的紫外线照射而使资料受损。使用并不方便。 PSRAM 全称Pseudo static random access memory。指的是伪静态随机存储器。 内部的内存颗粒跟SDRAM的颗粒相似,但外部的接口跟SDRAM不同,不需要SDRAM那样复杂的控制器和刷新机制,PSRAM的接口跟SRAM的接口是一样的。PSRAM 内部自带刷新机制。 EEPROM (electrically erasable, programmable, read-only )是一种电可擦除可编程只读存储器 其内容在掉电的时候也不会丢失。在平常情况下,EEPROM与EPROM一样是只读的,需要写入时,在指定的引脚加 上一个高电压即可写入或擦除,而且其擦除的速度极快 Flash - 它的主要特点是在不加电的情况下能长期保持存储的信息。就其本质而言,Flash Memory属于EEPROM(电擦除可编程只读存储器)类型。它既有ROM的特点,又有很高的存取速度,而且易于擦除和重写,功耗很小。 NOR Flash - NOR Flash的特点是芯片内执行(XIP, eXecute In Place),这样应用程序可以直接在flash闪存内运行,不必再把代码读到系统RAM中。NOR Flash的传输效率很高,在1~4MB的小容量时具有很高的成本效益,但是很低的写入和擦除速度大大影响了它的性能。 NAND Flash - NAND Flash结构能提供极高的单元密度,可以达到高存储密度,并且写入和擦除的速度也很快。应用NAND Flash的困难在于flash的管理需要特殊的系统接口。
常考面试题
-
说说IO设备输入输出的三种方式⭐⭐⭐
- 循环
- 中断
- DMA
-
说说中断流程⭐⭐⭐
中断是指当出现需要时,CPU暂时停止当前进程的执行,转而执行处理新情况的中断处理程序。当执行完该中断处理程序后,则重新从刚才停下的位置继续当前进程的运行。
为了区分不同的中断,每个设备有自己的中断号。系统有0-255一共256个中断。系统有一张中断向量表,用于存放256个中断的中断服务程序入口地址。每个入口地址对应一段代码,即中断服务程序。
-
说说嵌入式中断的流程⭐⭐⭐⭐
IRQ中断和FIQ中断都属于ARM的异常模式。在ARM系统中,一旦有中断发生,不管是外部中断,还是内部中断,正在执行的程序都会停下来。接下来通常会按照如下步骤处理中断:
-
保存现场。保存当前的PC值到R14,寄存器R14常用作链接寄存器(LR,Link Register),当进入子程序时,常用来保存PC(Program Counter,程序计数器) 的返回值。保存PC值后,接着保存当前的程序运行状态到SPSR(Storage Program Status Register,程序状态备份寄存器)。
-
模式切换。根据发生的中断类型,进入IRQ模式或FIQ模式。
-
获取中断源。以异常向量表保存在低地址处为例,若是IRQ中断,则PC指针跳动0x18处(
0x18:LDR PC, IRQ_ADDR
);若是FIQ中断,则跳到0x1C处(0x1c:LDR PC, FIQ_ADDR
)。IRQ和FIQ的异常向量地址处一般保存的是中断服务子程序的地址,所以接下来PC指针跳入中断服务子程序处理中断。 -
中断处理。
-
中断返回,恢复现场。当完成中断服务子程序后,将SPSR中保存的程序运行状态恢复到CPSR(Current Program Status Register,当前程序状态寄存器)中,R14中保存的被中断程序的地址恢复到PC中,继续执行被中断的程序。
-
-
说说ARM的七种模式⭐⭐⭐⭐
模式 意义 模式 模式 用户模式(usr,User Mode) ARM处理器正常的程序执行状态 非特权模式 普通模式 快速中断模式(FIQ,Fast Interrupt Request Mode) 用于高速数据传输或通道处理。当触发快速中断时进入此模式 特权模式 异常模式 外部中断模式(IRQ,Interrupt Request Mode) 用于通用的中断处理。当触发外部中断时进入此模式 特权模式 异常模式 管理模式(svc,Supervisor Mode) 操作系统使用的保护模式。在系统复位或执行软件中断指令SWI时进入 特权模式 异常模式 数据访问中止模式(abt,Abort Mode) 当数据或指令预取中止时进入该模式,可用于虚拟存储及存储保护 特权模式 异常模式 系统模式(sys,System Mode) 运行具有特权的操作系统任务 特权模式 普通模式 未定义指令中止模式(und,Undefined Mode) 当未定义的指令执行时进入该模式,可用于支持硬件协处理器的软件仿真 特权模式 异常模式 -
User模式和Supervisor模式有什么区别⭐⭐⭐
用户模式user是用户程序的工作模式,它运行在操作系统的用户态,它没有权限去操作其它硬件资源,只能执行处理自己的数据,也不能切换到其它模式下,要想访问硬件资源或切换到其它模式只能通过软中断或产生异常。
管理模式Supervisor是CPU上电后默认模式,因此在该模式下主要用来做系统的初始化,软中断处理也在该模式下。当用户模式下的用户程序请求使用硬件资源时,通过软件中断进入该模式。相比与IRQ和FIQ通过硬件触发,Supervisor优先级最低,而且是通过软件触发。
-
说说软中断⭐⭐⭐
Linux 将中断处理过程分成了两个阶段,也就是上半部和下半部:
-
上半部用来快速处理中断,它在中断禁止模式下运行,主要处理跟硬件紧密相关的或时间敏感的工作。(硬中断)
-
下半部用来延迟处理上半部未完成的工作,通常以内核线程的方式运行。(软中断)
比如:网卡接收到数据包后,会通过硬件中断的方式,通知内核有新的数据到了。这时,内核就应该调用中断处理程序来响应它。
-
对上半部来说,既然是快速处理,其实就是要把网卡的数据读到内存中,然后更新一下硬件寄存器的状态(表示数据已经读好了),最后再发送一个软中断信号,通知下半部做进一步的处理。
-
而下半部被软中断信号唤醒后,需要从内存中找到网络数据,再按照网络协议栈,对数据进行逐层解析和处理,直到把它送给应用程序。
所以,这两个阶段你也可以这样理解:
-
上半部直接处理硬件请求,也就是我们常说的硬中断,特点是快速执行;
-
而下半部则是由内核触发,也就是我们常说的软中断,特点是延迟执行。
-
-
说说DMA⭐⭐⭐⭐
-
概念
为I/O使用一种特殊的直接存储器访问(Direct Memory Access,DMA)芯片,它可以直接控制外围设备的数据流,而无需持续的CPU干预。这样效率就很高了,但对应成本就相对高些,因为DMA是由专门的硬件( DMA)控制。
-
使用场景
DMA传送主要用于需要高速大批量数据传送的系统中,以提高数据的吞吐量。如磁盘存取、图像处理、高速数据采集系统、同步通信中的收/发信号等方面应用甚广。通常只有数据流量较大(kBps或者更高)的外设才需要支持DMA能力,这些应用方面典型的例子包括视频、音频和网络接口。
-
设置
因为无需CPU干预,那么DMA要进行数据传输就必须有两个条件:数据从哪传(源地址),数据传到哪里去(目的地址)。通过软件设置,设置好源地址和目的地址。在一个重要的条件就是触发源是什么,就是说什么时候进行DMA数据传输呢?这叫触发信号。也可以通过软件编程设置具体时间,具体条件来触发DMA数据传输。
-
-
Linux中查看进程运行状态的指令、查看内存使用情况的指令、tar解压文件的参数。⭐⭐⭐
-
查看进程运行状态的指令:ps命令。“ps -aux | grep PID”,用来查看某PID进程状态
-
查看内存使用情况的指令:free命令。“free -m”,命令查看内存使用情况。
-
tar解压文件的参数:
五个命令中必选一个 -c: 建立压缩档案 -x:解压 -t:查看内容 -r:向压缩归档文件末尾追加文件 -u:更新原压缩包中的文件 这几个参数是可选的 -z:有gzip属性的 -j:有bz2属性的 -Z:有compress属性的 -v:显示所有过程 -O:将文件解开到标准输出
答案解析
//ps使用示例 //显示当前所有进程 ps -A //与grep联用查找某进程 ps -aux | grep apache //查看进程运行状态、查看内存使用情况的指令均可使用top指令。 top
-
-
文件权限怎么修改⭐⭐⭐
Linux文件的基本权限就有九个,分别是owner/group/others三种身份各有自己的read/write/execute权限
修改权限指令:chmod
答案解析
举例:文件的权限字符为 -rwxrwxrwx 时,这里总共会有10个字符,第一个字符表示文件类型,如文件(
-
表示),文件夹(d
表示),链接文件(l
表示),块设备(b
表示),字符设备(c
表示),后面9个字符按照三个一组分。其中,我们可以使用数字来代表各个权限。各权限的分数对照如下:
r w x 4 2 1 每种身份(owner/group/others)各自的三个权限(r/w/x)分数是需要累加的,
例如当权限为: [-rwxrwx---] ,则分数是:
owner = rwx = 4+2+1 = 7
group = rwx = 4+2+1 = 7
others= --- = 0+0+0 = 0
所以我们设定权限的变更时,该文件的权限数字就是770!变更权限的指令chmod的语法是这样的:
[root@www ~]# chmod [-R] xyz 文件或目录 选项与参数: xyz : 就是刚刚提到的数字类型的权限属性,为 rwx 属性数值的相加。 -R : 进行递归(recursive)的持续变更,亦即连同次目录下的所有文件都会变更 # chmod 770 douya.c //即修改douya.c文件的权限为770
-
说说常用的Linux命令⭐⭐⭐⭐⭐
命令 | 功能 |
---|---|
man | 帮助命令 |
ls命令 | 查看当前文件与目录信息 |
cd命令 | 用于切换当前目录 |
pwd命令 | 用于显示工作目录。 |
mkdir命令 | mkdir 命令用于创建文件夹。 |
rm命令 | 删除文件或文件夹命令 |
rmdir 命令 | 从一个目录中删除一个或多个子目录项 |
mv命令 | 移动文件或文件夹命令 |
cp命令 | 复制命令 |
cat命令 | 查看文件内容;连接文件 |
more命令 | more 会以一页一页的显示文件内容 |
less命令 | less 与 more 类似,但使用 less 可以随意浏览文件 |
grep命令 | 该命令常用于分析一行的信息,若当中有我们所需要的信息,就将该行显示出来,该命令通常与管道命令一起使用,用于对一些命令的输出进行筛选加工。 |
ps命令 | 查看进程情况 |
top命令 | 可以查看操作系统的信息,如进程、CPU占用率、内存信息等 |
kill命令 | 向进程发送终止信号 |
-
说说如何以root权限运行某个程序。⭐⭐⭐
sudo chown root app(文件名) sudo chmod u+s app(文件名)
输入上面两条指令后即可
-
说说常见信号有哪些,表示什么含义?⭐⭐⭐
常见信号如下:
信号代号 信号名称 说 明 1 SIGHUP 该信号让进程立即关闭.然后重新读取配置文件之后重启 2 SIGINT 程序中止信号,用于中止前台进程。相当于输出 Ctrl+C 快捷键 8 SIGFPE 在发生致命的算术运算错误时发出。不仅包括浮点运算错误,还包括溢出及除数为 0 等其他所有的算术运算错误 9 SIGKILL 用来立即结束程序的运行。本信号不能被阻塞、处理和忽略。般用于强制中止进程 14 SIGALRM 时钟定时信号,计算的是实际的时间或时钟时间。alarm 函数使用该信号 15 SIGTERM 正常结束进程的信号,kill 命令的默认信号。如果进程已经发生了问题,那么这 个信号是无法正常中止进程的,这时我们才会尝试 SIGKILL 信号,也就是信号 9 17 SIGCHLD 子进程结束时, 父进程会收到这个信号。 18 SIGCONT 该信号可以让暂停的进程恢复执行。本信号不能被阻断 19 SIGSTOP 该信号可以暂停前台进程,相当于输入 Ctrl+Z 快捷键。本信号不能被阻断 其中最重要的就是 "1"、"9"、"15"、"17"这几个信号。
-
Linux里如何查看带有关键字的日志文件?⭐⭐⭐
- cat 路径/文件名 | grep 关键词
# 返回test.log中包含http的所有行 cat test.log | grep "http"
- grep -i 关键词 路径/文件名 (与方法一效果相同,不同写法而已)
# 返回test.log中包含http的所有行(-i忽略大小写) grep -i "http" ./test.log
-
说说你对grep命令的了解?⭐⭐⭐
grep 命令。强大的文本搜索命令,grep(Global Regular Expression Print) 全局正则表达式搜索。
grep 的工作方式是这样的,它在一个或多个文件中搜索字符串模板。如果模板包括空格,则必须被引用,模板后的所有字符串被看作文件名。搜索的结果被送到标准输出,不影响原文件内容。
-
Linux修改主机名的命令是什么?⭐⭐⭐
-
如果只需要临时更改主机名,可以使用hostname命令。
sudo hostname <new-hostname> # 例如: sudo hostname myDebian #myDebian为修改名
-
如果想永久改变主机名,可以使用hostnamectl命令
sudo hostnamectl set-hostname myDebian #myDebian为修改名
-
-
Linux开机自动执行命令如何实现?⭐⭐⭐
-
方法 #1 - 使用 cron 任务
除了常用格式(分 / 时 / 日 / 月 / 周)外,cron 调度器还支持 @reboot 指令。这个指令后面的参数是脚本(启动时要执行的那个脚本)的绝对路径。
然而,这种方法需要注意两点:
a) cron 守护进程必须处于运行状态(通常情况下都会运行),同时
b) 脚本或 crontab 文件必须包含需要的环境变量。
-
方法 #2 - 使用 /etc/rc.d/rc.local
这个方法对于 systemd-based 发行版 Linux 同样有效。不过,使用这个方法,需要授予 /etc/rc.d/rc.local 文件执行权限:
# chmod +x /etc/rc.d/rc.local
然后在这个文件底部添加脚本。
-
-
Linux中,如何通过端口查进程,如何通过进程查端口?⭐⭐⭐
-
linux下通过进程名查看其占用端口: (1)先查看进程pid
ps -ef | grep 进程名
(2)通过pid查看占用端口
netstat -nap | grep 进程pid
-
linux通过端口查看进程:
netstat -nap | grep 端口号
-
-
说说top指令⭐⭐⭐⭐
显示当前系统正在执行的进程的相关信息,分为五行:
第一行,任务队列信息
第二行,Tasks — 任务(进程)
第三行,cpu状态信息
第四行,内存状态
第五行,swap交换分区信息
**前五行是当前系统情况整体的统计信息区。**
1. 第一行,**任务队列信息**,同 uptime 命令的执行结果,具体参数说明情况如下:
00:12:54 — 当前系统时间
up ?days, 4:49 — 系统已经运行了?天4小时49分钟(在这期间系统没有重启过)
21users — 当前有1个用户登录系统
load average: 0.06, 0.02, 0.00 — load average后面的三个数分别是1分钟、5分钟、15分钟的负载情况。load average数据是每隔5秒钟检查一次活跃的进程数,然后按特定算法计算出的数值。如果这个数除以逻辑CPU的数量,结果高于5的时候就表明系统在超负荷运转了。
2. 第二行,**Tasks — 任务(进程)**,具体信息说明如下:
系统现在共有256个进程,其中处于运行中的有1个,177个在休眠(sleep),stoped状态的有0个,zombie状态(僵尸)的有0个。
3. 第三行,**cpu状态信息**,具体属性说明如下:
0.2%us — 用户空间占用CPU的百分比。
0.2% sy — 内核空间占用CPU的百分比。
0.0% ni — 改变过优先级的进程占用CPU的百分比
99.5% id — 空闲CPU百分比
0.0% wa — IO等待占用CPU的百分比
0.0% hi — 硬中断(Hardware IRQ)占用CPU的百分比
0.0% si — 软中断(Software Interrupts)占用CPU的百分比
4. 第四行,**内存状态**,具体信息如下:
2017552 total — 物理内存总量
720188 used — 使用中的内存总量
197916 free — 空闲内存总量
1099448 cached — 缓存的总量
5. 第五行,**swap交换分区信息**,具体信息说明如下:
998396 total — 交换区总量
989936 free — 空闲交换区总量
8460 used — 使用的交换区总量
1044136 cached — 缓冲的交换区总量
13. #### 请你说说ping命令?⭐⭐⭐
Linux ping命令用于检测主机。
**执行ping指令会使用ICMP传输协议,发出要求回应的信息,若远端主机的网络功能没有问题,就会回应该信息,因而得知该主机运作正常。**
常考面试题
-
说说你对进程的理解⭐⭐⭐
程序是指令、数据及其组织形式的描述,而进程则是程序的运行实例,包括程序计数器、寄存器和变量的当前值。
Linux的进程结构,一般分为三部分:代码段、数据段(.data与.bss)和堆栈段。
-
代码段用于存放程序代码,如果有多个进程运行相同的一个程序,那么它们可以使用同一个代码段。代码段还会存储一部分常量,如字符串常量字面值。
-
数据段则存放程序的全局变量和静态变量。
-
堆栈段中的栈用于函数调用,存放着函数的参数、局部变量。
-
-
进程有哪五种状态,如何转换?⭐⭐⭐⭐⭐
进程有五种状态:创建、就绪、执行、阻塞、终止。
答案解析
创建状态 一个应用程序从系统上启动,首先就是进入创建状态,需要获取系统资源创建进程管理块(PCB:Process Control Block)完成资源分配。
就绪状态 在创建状态完成之后,进程已经准备好,处于就绪状态,但是还未获得处理器资源,无法运行。
运行状态 获取处理器资源,被系统调度,当具有时间片开始进入运行状态。如果进程的时间片用完了就进入就绪状态。
阻塞状态 在运行状态期间,如果进行了阻塞的操作,如耗时的I/O操作,此时进程暂时无法操作就进入到了阻塞状态,在这些操作完成后就进入就绪状态。等待再次获取处理器资源,被系统调度,当具有时间片就进入运行状态。
终止状态 进程结束或者被系统终止,进入终止状态
-
请你说说Linux的fork的作用⭐⭐⭐⭐⭐
fork函数用来创建一个子进程。对于父进程,fork()函数返回新创建的子进程的PID。对于子进程,fork()函数调用成功会返回0。如果创建出错,fork()函数返回-1。
答案解析
fork()函数,其原型如下:
#include <unistd.h> pid_t fork(void);
fork()函数不需要参数,返回值是一个进程标识符PID。返回值有以下三种情况:
(1) 对于父进程,fork()函数返回新创建的子进程的PID。 (2) 对于子进程,fork()函数调用成功会返回0。 (3) 如果创建出错,fork()函数返回-1。
fork()函数创建一个新进程后,会为这个新进程分配进程空间,将父进程的进程空间中的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,并且和父进程共享代码段。这时候,子进程和父进程一模一样,都接受系统的调度。因为两个进程都停留在fork()函数中,最后fork()函数会返回两次,一次在父进程中返回,一次在子进程中返回,两次返回的值不一样,如上面的三种情况。
-
说说写时复制⭐⭐⭐⭐⭐
当创建新进程时,连数据段和堆栈段都不再立马复制了,而是等到需要修改数据段或堆栈段的数据时再复制,这就是写时复制。
这样更加节省了进程空间,效率更高。
-
说说什么是守护进程,如何创建?⭐⭐⭐⭐
守护进程是运行在后台的一种生存期长的特殊进程。它独立于控制终端,处理一些系统级别任务。
创建过程如下:
-
创建子进程,终止父进程。
-
调用setsid()创建一个新会话。
-
将当前目录更改为根目录。
-
重设文件权限掩码。
-
关闭不再需要的文件描述符。
-
-
说说孤儿进程与僵尸进程,如何解决僵尸进程⭐⭐⭐⭐⭐
- 孤儿进程,是指一个父进程退出后,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并且由init进程对它们完成状态收集工作。
- 僵尸进程,是指一个进程使用fork函数创建子进程,如果子进程退出,而父进程并没有调用wait()或者waitpid()系统调用取得子进程的终止状态,那么子进程的进程描述符仍然保存在系统中,占用系统资源,这种进程称为僵尸进程。
所以两者的区别是:孤儿进程是父进程已退出,子进程未退出;而僵尸进程是父进程未退出,子进程已退出。
-
如何解决僵尸进程:
(1)一般,为了防止产生僵尸进程,在fork子进程之后我们都要及时使用wait系统调用;同时,当子进程退出的时候,内核都会给父进程一个SIGCHLD信号,所以我们可以建立一个捕获SIGCHLD信号的信号处理函数,在函数体中调用wait(或waitpid),就可以清理退出的子进程以达到防止僵尸进程的目的。
(2)使用kill命令。
打开终端并输入下面命令:
$ ps aux | grep Z
会列出进程表中所有僵尸进程的详细内容。
然后输入命令:
$ kill -s SIGCHLD pid(父进程pid)
这样子进程退出后,父进程就会收到信号了。
或者可以强制杀死父进程:
$ kill -9 pid(父进程pid)
这样父进程退出后,这些子进程将成为孤儿进程。孤儿进程将被init进程(进程号为1)所收养,并且由init进程对它们完成状态收集工作。
-
说说wait()函数的作用⭐⭐⭐⭐⭐
wait函数是用来及时回收我们的进程资源的。
进程一旦调用了wait函数,就立即阻塞自己本身,然后由wait函数自动分析当前进程的某个子进程是否已经退出,当找到一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞,直到有一个出现为止。函数原型如下:
#include<sys/types.h> #include<sys/wait.h> pid_t wait(int* status);
子进程的结束状态值会由参数status返回,而子进程的进程识别码也会一起返回。如果不需要结束状态值,则参数status可以设成 NULL。
常考面试题
-
说说进程通信的方式有哪些?⭐⭐⭐⭐⭐
进程间通信主要包括管道、系统IPC(包括消息队列、信号量、信号、共享内存)、套接字socket。
-
管道:包括无名管道和命名管道,无名管道半双工,只能用于具有亲缘关系的进程直接的通信(父子进程或者兄弟进程),可以看作一种特殊的文件;命名管道可以允许无亲缘关系进程间的通信。
-
系统IPC
-
消息队列:消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。
-
信号量semaphore:是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步。
-
信号:用于通知接收进程某个事件的发生。
-
内存共享:使多个进程访问同一块内存空间。
-
-
套接字socket:用于不同主机直接的通信。
-
-
说说进程同步的方式?⭐⭐⭐⭐⭐
- 信号量semaphore:是一个计数器,可以用来控制多个进程对共享资源的访问。信号量用于实现进程间的互斥与同步。P操作(递减操作)可以用于阻塞一个进程,V操作(增加操作)可以用于解除阻塞一个进程。
- 管程:一个进程通过调用管程的一个过程进入管程。在任何时候,只能有一个进程在管程中执行,调用管程的任何其他进程都被阻塞,以等待管程可用。
- 消息队列:消息的链接表,放在内核中。消息队列独立于发送与接收进程,进程终止时,消息队列及其内容并不会被删除;消息队列可以实现消息的随机查询,可以按照消息的类型读取。
- 互斥锁
-
进程通信中的管道实现原理是什么?⭐⭐⭐⭐⭐
操作系统在内核中开辟一块缓冲区(称为管道)用于通信。管道是一种两个进程间同一时刻进行单向通信的机制。因为这种特性,管道又称为半双工管道,所以其使用是有一定的局限性的。半双工是指同一时刻数据只能由一个进程流向另一个进程(一端负责读,一端负责写);如果是全双工通信,需要建立两个管道。
管道分为无名管道和命名管道,无名管道只能用于具有亲缘关系的进程直接的通信(父子进程或者兄弟进程),可以看作一种特殊的文件,管道本质是一种文件;命名管道可以允许无亲缘关系进程间的通信。
管道原型如下:
#include <unistd.h> int pipe(int fd[2]);
管道两端可分别用描述字fd[0]以及fd[1]来描述。注意管道的两端的任务是固定的,即一端只能用于读,由描述字fd[0]表示,称其为管道读端;另 一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。如果试图从管道写端读取数据,或者向管道读端写入数据都将发生错误。一般文件的 I/O 函数都可以用于管道,如close()、read()、write()等。
通信是指两个进程之间的信息交互,而pipe()函数创建的管道处于一个进程中间,单个进程中的管道几乎没有任何用处。因此一个进程在由 pipe()创建管道后,一般再使用fork() 建立一个子进程,然后通过管道实现父子进程间的通信。父子进程都有读端和写端,子进程的是从父进程复制过来的。
具体步骤如下:
-
父进程调用pipe开辟管道,得到两个文件描述符指向管道的两端。
-
父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
-
父进程关闭管道读端,子进程关闭管道写端。父进程可以往管道里写,子进程可以从管道里读,管道是用环形队列实现的,数据从写端流入从读端流出,这样就实现了进程间通信。
-
说说什么是信号量,有什么作用?⭐⭐⭐⭐⭐
-
概念:信号量本质上是一个计数器,用于多进程对共享数据对象的读取,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻可以有多个进程访问。
-
原理:由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),具体的行为如下:
(1)P(sv)操作:如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行(信号量的值为正,进程获得该资源的使用权,进程将信号量减1,表示它使用了一个资源单位)。
(2)V(sv)操作:如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1(若此时信号量的值为0,则进程进入挂起状态,直到信号量的值大于0,若进程被唤醒则返回至第一步)。
-
作用:用于多进程对共享数据对象的读取,它主要是用来保护共享资源(信号量也属于临界资源),使得资源在一个时刻可以有多个进程访问。
-
-
多进程内存共享可能存在什么问题?如何处理?⭐⭐⭐⭐⭐
内存共享。fork父子进程代码段共享,专门设立一块数据内存让两个进程共享,就实现了两个进程的信息互通
但是也会出现一个问题,并发时,一个修改数据,一个正在读数据,自然会出bug。
还要考虑共享内存时访问的同步问题。比如加入互斥锁或者信号量实现同步。
常考面试题
-
一个线程占多大内存?⭐⭐⭐
一个linux的线程大概占8M内存。
linux的栈是通过缺页来分配内存的,不是所有栈地址空间都分配了内存。因此,8M是最大消耗,实际的内存消耗只会略大于实际需要的内存(内部损耗,每个在4k以内)。
-
32位系统能访问4GB以上的内存吗?⭐⭐⭐
正常情况下是不可以的。原因是计算机使用二进制,每位数只有0或1两个状态,32位正好是2的32次方,正好是4G,所以大于4G就没办法表示了,而在32位的系统中,因其它原因还需要占用一部分空间,所以内存只能识别3G多。要使用4G以上就只能换64位的操作系统了。
但是使用PAE技术就可以实现 32位系统能访问4GB以上的内存。
Physical Address Extension(PAE)技术最初是为了弥补32位地址在PC服务器应用上的不足而推出的。我们知道,传统的IA32架构只有32位地址总线,只能让系统容纳不超过4GB的内存,这么大的内存,对于普通的桌面应用应该说是足够用了。可是,对于服务器应用来说,还是显得不足,因为服务器上可能承载了很多同时运行的应用。PAE技术将地址扩展到了36位,这样,系统就能够容纳2^36=64GB的内存。
-
说说进程、线程、协程是什么,区别是什么?⭐⭐⭐⭐⭐
- 进程:程序是指令、数据及其组织形式的描述,而进程则是程序的运行实例,包括程序计数器、寄存器和变量的当前值。
- 线程:微进程,一个进程里更小粒度的执行单元。一个进程里包含多个线程并发执行任务。
- 协程:协程是微线程,在子程序内部执行,可在子程序内部中断,转而执行别的子程序,在适当的时候再返回来接着执行。
区别:
-
线程与进程的区别:
(1)一个线程从属于一个进程;一个进程可以包含多个线程。
(2)一个线程意外死亡,可能导致进程挂掉;一个进程挂掉,不会影响其他进程。
(3)进程是系统资源调度的最小单位;线程CPU调度的最小单位。
(4)进程系统开销显著大于线程开销;线程需要的系统资源更少。
(5)进程在执行时拥有独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。
(6)进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈,线程切换时只需要切换硬件上下文和内核栈。
(7)通信方式不一样。
(8)进程适应于多核、多机分布;线程适用于多核
-
线程与协程的区别:
(1)协程执行效率极高。协程直接操作栈基本没有内核切换的开销,所以上下文的切换非常快,切换开销比线程更小。
(2)协程不需要多线程的锁机制,因为多个协程从属于一个线程,不存在同时写变量冲突,效率比线程高。
(3)一个线程可以有多个协程。
-
互斥量能不能在进程中使用?⭐⭐⭐⭐⭐
能。
不同的进程之间,存在资源竞争或并发使用的问题,所以需要互斥量。
进程中也需要互斥量,因为一个进程中可以包含多个线程,线程与线程之间需要通过互斥的手段进行同步,避免导致共享数据修改引起冲突。可以使用互斥锁,属于互斥量的一种。
-
协程是轻量级线程,轻量级表现在哪里?⭐⭐⭐⭐⭐
-
协程调用跟切换比线程效率高:协程执行效率极高。协程不需要多线程的锁机制,可以不加锁的访问全局变量,所以上下文的切换非常快。
-
协程占用内存少:执行协程只需要极少的栈内存(大概是4~5KB),而默认情况下,线程栈的大小为1MB。
-
切换开销更少:协程直接操作栈基本没有内核切换的开销,所以切换开销比线程少。
-
-
说说线程间通信的方式有哪些?⭐⭐⭐⭐⭐
线程间的通信方式包括互斥量、信号量、条件变量、读写锁:
- 互斥量:采用互斥对象机制,只有拥有互斥对象的线程才可以访问。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
- 信号量:计数器,允许多个线程同时访问同一个资源。
- 条件变量:通过条件变量通知操作的方式来保持多线程同步。
- 读写锁:读写锁与互斥量类似。但互斥量要么是锁住状态,要么就是不加锁状态。读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率就比互斥锁要高。
-
说说线程同步方式有哪些?⭐⭐⭐⭐⭐
线程间的同步方式包括互斥锁、信号量、条件变量、读写锁:
- 互斥锁:采用互斥对象机制,只有拥有互斥对象的线程才可以访问。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
- 信号量:计数器,允许多个线程同时访问同一个资源。
- 条件变量:通过条件变量通知操作的方式来保持多线程同步。
- 读写锁:读写锁与互斥量类似。但互斥量要么是锁住状态,要么就是不加锁状态。读写锁一次只允许一个线程写,但允许一次多个线程读,这样效率就比互斥锁要高。
-
互斥量和信号量的区别⭐⭐⭐⭐⭐
-
互斥量用于线程的互斥,信号量用于线程的同步。
-
互斥量值只能为0/1,信号量值可以为非负整数。
-
互斥锁保证资源同一时间只有一个线程访问;信号量可以多个线程访问同一资源
-
-
有了进程,为什么还要有线程?⭐⭐⭐⭐⭐
-
原因
进程在早期的多任务操作系统中是基本的执行单元。每次进程切换,都要先保存进程资源然后再恢复,这称为上下文切换。但是进程频繁切换将引起额外开销,从而严重影响系统的性能。为了减少进程切换的开销,人们把两个任务放到一个进程中,每个任务用一个更小粒度的执行单元来实现并发执行,这就是线程。
-
线程与进程对比
(1)**进程间的信息难以共享。**由于除去只读代码段外,父子进程并未共享内存,因此必须采用一些进程间通信方式,在进程间进行信息交换。
但多个线程共享进程的内存,如代码段、数据段、扩展段,线程间进行信息交换十分方便。
(2)调用 fork() 来创建进程的代价相对较高,即便利用写时复制技术,仍然需要复制诸如内存页表和文件描述符表之类的多种进程属性,这意味着 fork() 调用在时间上的开销依然不菲。
**但创建线程比创建进程通常要快 10 倍甚至更多。**线程间是共享虚拟地址空间的,无需采用写时复制来复制内存,也无需复制页表。
-
-
单核机器上写多线程程序,是否要考虑加锁,为什么?⭐⭐⭐⭐⭐
在单核机器上写多线程程序,仍然需要线程锁。
原因:因为线程锁通常用来实现线程的同步和通信。在单核机器上的多线程程序,仍然存在线程同步的问题。因为在抢占式操作系统中,通常为每个线程分配一个时间片,当某个线程时间片耗尽时,操作系统会将其挂起,然后运行另一个线程。如果这两个线程共享某些数据,不使用线程锁的前提下,可能会导致共享数据修改引起冲突。
-
说说多线程和多进程的不同?⭐⭐⭐⭐⭐
(1)一个线程从属于一个进程;一个进程可以包含多个线程。
(2)一个线程意外死亡,可能导致进程挂掉,多线程也可能挂掉;一个进程挂掉,不会影响其他进程,多进程稳定。
(3)进程系统开销显著大于线程开销;线程需要的系统资源更少。
(4)多个进程在执行时拥有各自独立的内存单元,多个线程共享进程的内存,如代码段、数据段、扩展段;但每个线程拥有自己的栈段和寄存器组。
(5)多进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈;多线程切换时只需要切换硬件上下文和内核栈。
(6)通信方式不一样。
(7)多进程适应于多核、多机分布;多线程适用于多核
-
简述互斥锁的机制,互斥锁与读写的区别?⭐⭐⭐⭐⭐
-
互斥锁机制:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入阻塞,等待锁释放。
-
互斥锁和读写锁:
(1) 读写锁区分读者和写者,而互斥锁不区分
(2)互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。
-
-
一个线程正在进行读。另一个线程尝试加写锁,写锁优先级高于读锁,那这个正在读的线程会让出来资源吗?⭐⭐⭐⭐⭐
不会。一次只有一个线程可以对其加锁,不论是加读锁还是加写锁。
-
写锁优先级高于读锁什么意思⭐⭐⭐⭐⭐
优先级的意思是,当有两个线程处于阻塞状态时,一个尝试读,一个尝试写,写锁优先级高于读锁,当锁可以获取时,那么尝试写的线程先加锁。
-
说说线程池的设计思路,线程池中线程的数量由什么确定?⭐⭐⭐⭐
-
设计思路:
实现线程池有以下几个步骤: (1)设置一个生产者消费者队列,作为临界资源。
(2)初始化n个线程,并让其运行起来,加锁去队列里取任务运行
(3)当任务队列为空时,所有线程阻塞。
(4)当生产者队列来了一个任务后,先对队列加锁,把任务挂到队列上,然后使用条件变量去通知阻塞中的一个线程来处理。
-
线程池中线程数量:
线程数量和哪些因素有关:CPU,IO、并行、并发
如果是CPU密集型应用,则线程池大小设置为:CPU数目+1 如果是IO密集型应用,则线程池大小设置为:2*CPU数目+1 最佳线程数目 = (线程等待时间与线程CPU时间之比 + 1)* CPU数目
所以线程等待时间所占比例越高,需要越多线程。线程CPU时间所占比例越高,需要越少线程。
-
为什么要创建线程池:
创建线程和销毁线程的花销是比较大的,这些时间有可能比处理业务的时间还要长。这样频繁的创建线程和销毁线程,再加上业务工作线程,消耗系统资源的时间,可能导致系统资源不足。同时线程池也是为了提升系统效率。
-
线程池的核心线程与普通线程:
任务队列可以存放100个任务,此时为空,线程池里有10个核心线程,若突然来了10个任务,那么刚好10个核心线程直接处理;若又来了90个任务,此时核心线程来不及处理,那么有80个任务先入队列,再创建核心线程处理任务;若又来了120个任务,此时任务队列已满,不得已,就得创建20个普通线程来处理多余的任务。 以上是线程池的工作流程。
-
-
进程和线程相比,为什么慢?⭐⭐⭐⭐⭐
- 进程系统开销显著大于线程开销;线程需要的系统资源更少。
- 进程切换开销比线程大。多进程切换时需要刷新TLB并获取新的地址空间,然后切换硬件上下文和内核栈;多线程切换时只需要切换硬件上下文和内核栈。
- 进程通信比线程通信开销大。进程通信需要借助管道、队列、共享内存,需要额外申请空间,通信繁琐;而线程共享进程的内存,如代码段、数据段、扩展段,通信快捷简单,同步开销更小。
常考面试题
-
简述GDB常见的调试命令,什么是条件断点,多进程下如何调试。⭐⭐⭐⭐
GDB调试:gdb调试的是可执行文件,在gcc编译时加入 -g ,告诉gcc在编译时加入调试信息,这样gdb才能调试这个被编译的文件 gcc -g tesst.c -o test
GDB命令格式:
-
quit:退出gdb,结束调试
-
list:查看程序源代码
list 5,10:显示5到10行的代码
list test.c:5, 10: 显示源文件5到10行的代码,在调试多个文件时使用
list get_sum: 显示get_sum函数周围的代码
list test,c get_sum: 显示源文件get_sum函数周围的代码,在调试多个文件时使用
-
reverse-search:字符串用来从当前行向前查找第一个匹配的字符串
-
run:程序开始执行
-
help list/all:查看帮助信息
-
break:设置断点
break 7:在第七行设置断点
break get_sum:以函数名设置断点
break 行号或者函数名 if 条件:以条件表达式设置断点
-
watch 条件表达式:条件表达式发生改变时程序就会停下来
-
next:继续执行下一条语句 ,会把函数当作一条语句执行
-
step:继续执行下一条语句,会跟踪进入函数,一次一条的执行函数内的代码
**条件断点:**break if 条件 以条件表达式设置断点
**多进程下如何调试:**用set follow-fork-mode child 调试子进程
或者set follow-fork-mode parent 调试父进程
-
-
说说进程调度算法有哪些?⭐⭐⭐⭐⭐
- 先来先服务调度算法
- 短作业(进程)优先调度算法
- 高优先级优先调度算法
- 时间片轮转法
- 多级反馈队列调度算法
-
简述LRU算法及其实现方式。⭐⭐⭐⭐⭐
-
LRU算法:LRU算法用于缓存淘汰。思路是将缓存中最近最少使用的对象删除掉
-
实现方式:利用链表和hashmap。
当需要插入新的数据项的时候,如果新数据项在链表中存在(一般称为命中),则把该节点移到链表头部,如果不存在,则新建一个节点,放到链表头部,若缓存满了,则把链表最后一个节点删除即可。
在访问数据的时候,如果数据项在链表中存在,则把该节点移到链表头部,否则返回-1。这样一来在链表尾部的节点就是最近最久未访问的数据项。
-
代码实现
我们给出C++的具体实现,代码一看就懂了。
class LRUCache { list<pair<int, int>> cache;//创建双向链表 unordered_map<int, list<pair<int, int>>::iterator> map;//创建哈希表 int cap; public: LRUCache(int capacity) { cap = capacity; } int get(int key) { if (map.count(key) > 0){ auto temp = *map[key]; cache.erase(map[key]); map.erase(key); cache.push_front(temp);//把该节点移到链表头部 map[key] = cache.begin();//映射头部 return temp.second; } return -1; } void put(int key, int value) { if (map.count(key) > 0){ cache.erase(map[key]); map.erase(key); } else if (cap == cache.size()){//若缓存满了,则把链表最后一个节点删除 auto temp = cache.back(); map.erase(temp.first); cache.pop_back(); } cache.push_front(pair<int, int>(key, value));//新建一个节点,放到链表头部 map[key] = cache.begin();//映射头部 } }; /** Your LRUCache object will be instantiated and called as such: LRUCache* obj = new LRUCache(capacity); int param_1 = obj->get(key); obj->put(key,value); */
-
-
什么是页表,为什么要有?⭐⭐⭐⭐⭐
页表是虚拟内存的概念。操作系统虚拟内存到物理内存的映射表,就被称为页表。
原因:不可能每一个虚拟内存的 Byte 都对应到物理内存的地址。这张表将大得真正的物理地址也放不下,于是操作系统引入了页(Page)的概念。进行分页,这样可以减小虚拟内存页对应物理内存页的映射表大小。
我们举个形象的例子,就如同一本书,如果把整个书给摊开,那占地面积太大了,所以书才要分页,节约空间。
-
简述操作系统中的缺页中断。⭐⭐⭐⭐⭐
-
缺页异常:malloc和mmap函数在分配内存时只是建立了进程虚拟地址空间,并没有分配虚拟内存对应的物理内存。当进程访问这些没有建立映射关系的虚拟内存时,处理器自动触发一个缺页异常,引发缺页中断。
-
缺页中断:缺页异常后将产生一个缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,将其调入内存。
-
-
简述一下虚拟内存和物理内存,为什么要用虚拟内存,好处是什么?⭐⭐⭐⭐⭐
-
物理内存:物理内存有四个层次,分别是寄存器、高速缓存、主存、磁盘。
寄存器:速度最快、量少、价格贵。
高速缓存:次之。
主存:再次之。
磁盘:速度最慢、量多、价格便宜。
操作系统会对物理内存进行管理,有一个部分称为内存管理器(memory manager),它的主要工作是有效的管理内存,记录哪些内存是正在使用的,在进程需要时分配内存以及在进程完成时回收内存。
-
虚拟内存:操作系统为每一个进程分配一个独立的地址空间,却是虚拟内存。虚拟内存与物理内存存在映射关系,通过页表寻址完成虚拟地址和物理地址的转换。
-
为什么要用虚拟内存:因为早期的内存分配方法存在以下问题:
(1)进程地址空间不隔离。会导致数据被随意修改。
(2)内存使用效率低。
(3)程序运行的地址不确定。操作系统随机为进程分配内存空间,所以程序运行的地址是不确定的。
-
使用虚拟内存的好处:
(1)扩大地址空间。每个进程独占一个4G空间,虽然真实物理内存没那么多。
(2)内存保护:防止不同进程对物理内存的争夺和践踏,可以对特定内存地址提供写保护,防止恶意篡改。
(3)可以实现内存共享,方便进程通信。
(4)可以避免内存碎片,虽然物理内存可能不连续,但映射到虚拟内存上可以连续。
-
使用虚拟内存的缺点:
(1)虚拟内存需要额外构建数据结构,占用空间。
(2)虚拟地址到物理地址的转换,增加了执行时间。
(3)页面换入换出耗时。
(4)一页如果只有一部分数据,浪费内存。
-
-
虚拟地址到物理地址怎么映射的?⭐⭐⭐⭐⭐
操作系统为每一个进程维护了一个从虚拟地址到物理地址的映射关系的数据结构,叫页表。页表的内容就是该进程的虚拟地址到物理地址的一个映射。页表中的每一项都记录了这个页的基地址。
三级页表转换方法:(两步)
(1)逻辑地址转线性地址:段起始地址+段内偏移地址=线性地址
(2)线性地址转物理地址:
每一个32位的线性地址被划分为三部分:页目录索引(10位)、页表索引(10位)、页内偏移(12位)
- 从cr3中取出进程的页目录地址(操作系统调用进程时,这个地址被装入寄存器中)
- 页目录地址 + 页目录索引 = 页表地址
- 页表地址 + 页表索引 = 页地址
- 页地址 + 页内偏移 = 物理地址
-
说说什么是死锁,产生的条件,如何解决?⭐⭐⭐⭐⭐
-
死锁: 是指多个进程在执行过程中,因争夺资源而造成了互相等待。此时系统产生了死锁。比如两只羊过独木桥,若两只羊互不相让,争着过桥,就产生死锁。
-
产生的条件:死锁发生有四个必要条件: (1)互斥条件:进程对所分配到的资源不允许其他进程访问,若其他进程访问,只能等待,直到进程使用完成后释放该资源;
(2)请求保持条件:进程获得一定资源后,又对其他资源发出请求,但该资源被其他进程占有,此时请求阻塞,而且该进程不会释放自己已经占有的资源;
(3)不可剥夺条件:进程已获得的资源,只能自己释放,不可剥夺;
(4)环路等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
-
如何解决:
(1)资源一次性分配,从而解决请求保持的问题
(2)可剥夺资源:当进程新的资源未得到满足时,释放已有的资源;
(3)资源有序分配:资源按序号递增,进程请求按递增请求,释放则相反。
-
-
简述互斥锁的机制,互斥锁与读写的区别?⭐⭐⭐⭐⭐
-
互斥锁机制:mutex,用于保证在任何时刻,都只能有一个线程访问该对象。当获取锁操作失败时,线程会进入阻塞,等待锁释放。
-
互斥锁和读写锁:
(1) 读写锁区分读者和写者,而互斥锁不区分
(2)互斥锁同一时间只允许一个线程访问该对象,无论读写;读写锁同一时间内只允许一个写者,但是允许多个读者同时读对象。
-
-
简述自旋锁和互斥锁的使用场景⭐⭐⭐⭐⭐
- 互斥锁用于临界区持锁时间比较长的操作,比如下面这些情况都可以考虑
(1)临界区有IO操作
(2)临界区代码复杂或者循环量大
(3)临界区竞争非常激烈
(4)单核处理器
- 自旋锁就主要用在临界区持锁时间非常短且CPU资源不紧张的情况下。
-
公平锁与非公平锁⭐⭐⭐⭐⭐
公平锁:就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己 非公平锁:非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。 非公平锁的优点在于吞吐量比公平锁大。
-
死锁与活锁⭐⭐⭐⭐⭐
死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。 活锁:是指线程1可以使用资源,但它很礼貌,让其他线程先使用资源,线程2也可以使用资源,但它很绅士,也让其他线程先使用资源。这样你让我,我让你,最后两个线程都无法使用资源。
-
说说sleep和wait的区别?⭐⭐⭐
(1)sleep是一个延时函数,让进程或线程进入休眠。休眠完毕后继续运行。
(2)wait是父进程回收子进程PCB(Process Control Block)资源的一个系统调用。
-
请介绍一下5种IO模型⭐⭐⭐⭐⭐
IO(Input/Output,输入/输出)即数据的读取(接收)或写入(发送)操作。 通常用户进程中的一个完整IO分为两阶段:用户进程空间与内核空间之间的相互切换、内核空间与设备空间的相互切换(磁盘、网络等)。我们通常说的IO是指网络IO和磁盘IO两种。 Linux中进程无法直接操作I/O设备,其必须通过系统调用请求内核来协助完成I/O动作;内核会为每个I/O设备维护一个缓冲区。 对于一个输入操作来说,进程IO系统调用后,内核会先看缓冲区中有没有相应的缓存数据,没有的话再到设备中读取,因为设备IO一般速度较慢,需要等待;内核缓冲区有数据则直接复制到进程空间。 所以,对于一个网络输入操作通常包括两个不同阶段:
- 等待网络数据到达网卡→读取到内核缓冲区,数据准备好;
- 从内核缓冲区复制数据到进程空间。
5种IO模型如下:
- 阻塞IO:进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。调用者将一直等待,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
- 非阻塞IO:进程发起IO系统调用后,进程被阻塞,内核数据还没好,不想让进程等待,就返回一个错误,这样进程就不阻塞了。进程每隔一段时间就发起IO系统调用去检查IO事件是否就绪。这样就实现非阻塞了。每个进程都有一个时间片,轮询的时候读取IO,时间片到了就要换另一个进程做其他事情了,这样就做到了每隔一段时间发起IO系统调用。
- IO多路复用:Linux用select/poll函数实现IO复用模型,这两个函数也会使进程阻塞,但是和阻塞IO所不同的是这两个函数可以同时阻塞多个IO操作。而且可以同时对多个读操作、写操作的IO函数进行检查。select/poll会监听所有的IO,直到有数据可读或可写时,才真正调用IO操作函数。
- 信号驱动IO:Linux用套接口进行信号驱动IO,安装一个信号处理函数,进程继续运行并不阻塞,当IO事件就绪,进程收到SIGIO信号,然后处理IO事件。这个好理解,这个信号直接通知进程数据到了。
- 异步IO:进程发起IO系统调用后立即返回,当内核将数据拷贝到缓冲区后,再通知应用程序。用户可以直接去使用数据。具体操作是进程调用aio_read函数告诉内核描述字缓冲区指针和缓冲区的大小、文件偏移及通知的方式,然后立即返回。
前四种属于同步IO,原因就在于进程发起IO系统调用读取数据时,这个真正拿到数据的过程依然是阻塞的,直到完成数据读取还要把数据拷贝到用户空间中,进程才能继续做其他事。 而异步IO就不一样了,进程完全做自己的事情,数据都不需要它读取,而是由内核读取数据并将数据拷贝到缓冲区后,再通知应用程序。用户可以直接去使用数据。
-
简述同步与异步的区别,阻塞与非阻塞的区别?⭐⭐⭐⭐⭐
-
同步与异步的区别:
同步:所有的操作都做完,才返回给用户结果。即写完数据库之后,再响应用户,用户体验不好。双方的动作是经过双方协调的,步调一致的。
异步:不用等所有操作都做完,就响应用户请求。即先响应用户请求,然后慢慢去写数据库,用户体验较好。双方并不需要协调,都可以随意进行各自的操作。
-
阻塞与非阻塞的区别:
阻塞:调用者调用了某个函数,等待这个函数返回,期间什么也不做,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
非阻塞:非阻塞等待,每隔一段时间就去检查IO事件是否就绪。没有就绪就可以做其他事情。
-
-
BIO、NIO有什么区别?⭐⭐⭐⭐⭐
BIO、NIO的区别:
BIO(Blocking I/O):阻塞IO。进程发起IO系统调用后,进程被阻塞,转到内核空间处理,整个IO处理完毕后返回进程。操作成功则进程获取到数据。调用者将一直等待,不停的检查这个函数有没有返回,必须等这个函数返回后才能进行下一步动作。
NIO(New I/O):同时支持阻塞与非阻塞模式,NIO的做法是叫一个线程不断的轮询每个IO的状态,看看是否有IO的状态发生了改变,从而进行下一步的操作。
-
说说多路IO复用技术有哪些,区别是什么?⭐⭐⭐⭐⭐
-
select,poll,epoll都是IO多路复用的机制,I/O多路复用就是通过一种机制,可以监视多个文件描述符,一旦某个文件描述符就绪(一般是读就绪或者写就绪),能够通知应用程序进行相应的读写操作。
-
区别:
(1)poll与select不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。
(2)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。
(3)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把当前进程往设备等待队列中挂一次,而epoll只要一次拷贝,而且把当前进程往等待队列上挂也只挂一次,这也能节省不少的开销。
-
-
说说epoll的原理⭐⭐⭐⭐⭐
epoll提供了三个函数,epoll_create、epoll_ctl和epoll_wait。 首先创建一个epoll对象,然后使用epoll_ctl对这个对象进行操作(添加、删除、修改),把需要监控的描述符加进去,这些描述符将会以epoll_event结构体的形式组成一颗红黑树,接着阻塞在epoll_wait,进入大循环,当某个fd上有事件发生时,内核将会把其对应的结构体放入一个链表中,返回有事件发生的链表。
-
简述epoll和select的区别,epoll为什么高效?⭐⭐⭐⭐⭐
-
区别:
(1)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大;而epoll保证了每个fd在整个过程中只会拷贝一次。
(2)每次调用select都需要在内核遍历传递进来的所有fd;而epoll只需要轮询一次fd集合,同时查看就绪链表中有没有就绪的fd就可以了。
(3)select支持的文件描述符数量太小了,默认是1024;而epoll没有这个限制,它所支持的fd上限是最大可以打开文件的数目,这个数字一般远大于2048。
-
epoll为什么高效:
(1)select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。
(2)select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把当前进程往设备等待队列中挂一次,而epoll只要一次拷贝,而且把当前进程往等待队列上挂也只挂一次,这也能节省不少的开销。
-
-
epoll水平触发与边缘触发的区别?⭐⭐⭐⭐⭐
LT模式(水平触发)下,只要这个fd还有数据可读,每次 epoll_wait都会返回它的事件,提醒用户程序去操作;
而在ET(边缘触发)模式中,它只会提示一次,直到下次再有数据流入之前都不会再提示了,无论fd中是否还有数据可读。
常见面试题
-
说说计算机网络有哪两种通信方式?⭐⭐⭐
-
第一种方式:**客户-服务器方式。**这种传统的方式是互联网上最常见的方式。客户是服务请求方,服务器是服务提供方。
C/S模型有一个特例,那就是B/S(Browse/Server)模型,即浏览器/服务器模式,也叫B/S结构。它只安装维护一个服务器(Server),而客户端采用浏览器(Browse)运行软件。B/S结构是随着Internet技术的兴起,对C/S结构的变化和改进。它和C/S并没有本质区别。
-
第二种通信方式:**对等连接(P2P)方式。**是指两台主机在通信时并不区分哪一个是服务请求方哪一个是服务提供方。只要两台主机都运行了对等连接软件(P2P软件),他们就可以进行对等连接通信。
-
-
什么是分组交换?优缺点?⭐⭐⭐
分组交换采用存储转发技术,把一个报文划分为几个分组后再进行传送。分组的首部非常重要,包含了目的地址和源地址等重要控制信息,这样每一个分组才能在互联网中独立地选择传输路径,并被正确地交付到分组传输的终点。
优点:
(1)高效,在分组传输时动态分配带宽,对通信链路逐段占用。
(2)灵活,为每一个分组独立地选择最合适的转发路由。
(3)迅速,以分组为单位,可以不先建立连接就能向主机发送数据。
(4)可靠,分布式多路由的分组交换网,使传输鲁棒性强。
缺点:
(1)分组在路由器存储转发时需要排队,有时延。
(2)分组必须携带控制信息(头部)也造成了开销。
-
路由器的作用⭐⭐⭐
路由器就是用来转发分组的,即进行分组交换。路由器收到一个分组,先暂时存储一下,检查其首部,查找转发表,按照首部中的目的地址,找到合适的接口转发出去,把分组交给下一个路由器。这样一步一步地存储转发的方式,把分组交付给最终的目的主机。
-
什么是路由?⭐⭐⭐
路由是指路由器从一个接口上收到数据包,根据数据包的目的地址进行定向并转发到另一个接口的过程。
-
数据在TCP/IP分层模型中的流转形式⭐⭐⭐
在应用层的数据称为报文(message);在传输层的数据称为段(segment);在网络层的数据叫 分组包(packet),网络接口层(链路层)的数据称为帧(frame)。
-
请说说OSI七层协议模型?⭐⭐⭐
-
请说说TCP/IP四层分层模型?⭐⭐⭐⭐⭐
常见面试题
-
TCP为什么要三次握手,能两次吗?⭐⭐⭐⭐⭐
不能两次
假如只进行两次握手,客户端发送连接请求后,会等待服务器端的应答。但是会出现的问题是,假如客户端的SYN迟迟没有到达服务器端,此时客户端超时后,会重新发送一次连接,假如重发的这次服务器端收到了,且应答客户端了,连接建立了。
但是建立后,第一个SYN也到达服务端了,这时服务端会认为这是一个新连接,会再给客户端发送一个ACK,这个ACK当然会被客户端丢弃。但是此时服务器端已经为这个连接分配资源了,而且服务器端会一直维持着这个资源,会造成资源浪费。
两次握手的问题在于服务器端不知道SYN的有效性,所以如果是三次握手,服务器端会等待客户端的第三次握手,如果第三次握手迟迟不来,服务器端就会释放相关资源。
-
TCP为什么要四次挥手,能三次吗?⭐⭐⭐⭐⭐
不能三次。
第二次挥手和第三次挥手不能合并在一起,这是因为第二次挥手后,服务器端可能还在传输数据,需要等待数据传输完毕后再进行第三次挥手。
-
说说TCP三次握手的过程。⭐⭐⭐⭐⭐
三次握手:
- Client将标志位SYN置为1,随机产生一个值seq=J,并将该数据包发送给Server,等待Server确认。
- Server收到数据包后由标志位SYN=1知道Client请求建立连接,Server将标志位SYN和ACK都置1,ack=J+1,随机产生一个值seq=K,并将该数据包发给Client以确认连接请求。
- Client收到确认后,检测ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给Server。完成三次握手,随后Client与Server之间可以开始传输数据了。
-
说说TCP四次挥手的过程。⭐⭐⭐⭐⭐
四次挥手:
- 数据传输结束后,Client的应用进程发出连接释放报文段FIN,并停止发送数据,此时Client依然可以接收Server发送来的数据。
- Server接收到FIN后,发送一个ACK给Client,确认序号为收到的序号+1。
- 当Server没有数据要发送时,Server发送一个FIN报文,等待Client的确认。
- Client收到Server的FIN报文后,给Server发送一个ACK报文,确认序列号为收到的序号+1。此时Client进入TIME_WAIT状态,等待2MSL(MSL:报文段最大生存时间),然后关闭连接。
-
为什么第四次挥手后,客户端需要等待2MSL? ⭐⭐⭐⭐⭐
这是为了保证客户端发送的最后一个ACK报文段能够到达服务器。如果客户端不等待2MSL,这个ACK报文段可能丢失,因而使得处在LAST-ACK状态的服务器收不到ACK报文段的确认,导致服务器无法正常关闭。而如果客户端等待2MSL,服务器就会超时重传FIN报文段,而客户端就能在2MSL时间内收到这个重传的FIN-ACK报文段。接着客户端重传一个确认,重新启动2MSL计时器。当服务器收到最后一个ACK后就可以正常关闭了。
2MSL的意义是,经过2MSL后,所有的报文都会消失,不会影响下一次连接。最后客户端和服务器端都能正常进入到CLOSED状态。
-
什么是洪泛攻击?怎么避免?⭐⭐⭐⭐⭐
A(攻击者)发送TCP SYN,SYN是TCP三次握手中的第一个数据包,而当这个服务器返回ACK以后,A不再进行确认,那这个连接就处在了一个挂起的状态,也就是半连接的意思,那么服务器收不到再确认的一个消息,还会重复发送ACK给A。
这样一来就会更加浪费服务器的资源。A就对服务器发送非法大量的这种TCP连接,由于每一个都没法完成握手的机制,所以它就会消耗服务器的内存最后可能导致服务器死机,就无法正常工作了。更进一步说,如果这些半连接的握手请求是恶意程序发出,并且持续不断,那么就会导致服务端较长时间内丧失服务功能——这样就形成了DoS攻击。这种攻击方式就称为SYN泛洪攻击。
避免方法:
最常用的一个手段就是优化主机系统设置。
(1)比如降低SYN timeout时间,使得主机尽快释放半连接的占用。
(2)或者采用SYN cookie设置,就是给每一个请求连接的IP地址分配一个Cookie,如果短时间内连续受到某个IP的重复SYN报文,就认定是受到了攻击,以后从这个IP地址来的包会被丢弃。Cookie是当浏览某网站时,由Web服务器置于硬盘上一个非常小的文本文件,用来记录用户ID,密码,浏览过的网页,停留时间等信息。当我们认为受到了攻击,合理的采用防huo墙(汉字竟然会被屏蔽,有点意思,只能打拼音了)设置等外部网络进行拦截。
(3)使用长连接。在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。
-
如何应对短连接、高并发的场景?⭐⭐⭐⭐⭐
-
针对于大量短连接同时高并发的情况: 最常用的一个手段就是优化主机系统设置。
(1)比如降低SYN timeout时间,使得主机尽快释放半连接的占用。
(2)或者采用SYN cookie设置,就是给每一个请求连接的IP地址分配一个Cookie,如果短时间内连续受到某个IP的重复SYN报文,就认定是受到了攻击,以后从这个IP地址来的包会被丢弃。Cookie是当浏览某网站时,由Web服务器置于硬盘上一个非常小的文本文件,用来记录用户ID,密码,浏览过的网页,停留时间等信息。当我们认为受到了攻击,合理的采用防huo墙(汉字竟然会被屏蔽,有点意思,只能打拼音了)设置等外部网络进行拦截。
(3)使用长连接。在使用长连接的情况下,当一个网页打开完成后,客户端和服务器之间用于传输HTTP数据的 TCP连接不会关闭,如果客户端再次访问这个服务器上的网页,会继续使用这一条已经建立的连接。
-
而针对于服务器高并发的场景,有以下处理手段: (1)采用多IO复用模型,如select、epoll,甚至采用异步IO
(2)采用队列进行削峰、缓存
(3)采用多服务器负载均衡手段
(4)数据库层面我们可以采用分库分表、读写分离等措施。
(5)还可以采用缓存的方式
-
-
说说TCP的可靠机制。⭐⭐⭐⭐⭐
TCP保证可靠性:
-
序列号、确认应答、超时重传
数据到达接收方,接收方需要发出一个确认应答,表示已经收到该数据段,并且确认序列号,序列号说明了它下一次需要接收的数据序列号,保证数据传输有序。如果发送方迟迟未收到确认应答,那么可能是发送的数据丢失,也可能是确认应答丢失,这时发送方在等待一段时间后进行重传。
-
窗口控制
TCP会利用窗口控制来提高传输速度,意思是在一个窗口大小内,不用一定等到应答才能发送下一段数据,窗口大小就是无需等待确认而可以继续发送数据的最大值。如果不使用窗口控制,每一个没收到确认应答的数据都要重发。
使用窗口控制,如果数据段1001-2000丢失,后面数据每次传输,确认应答都会不停发送序号为1001的应答,表示我要接收1001开始的数据,发送端如果收到3次相同应答,就会立刻进行重发;数据一旦丢失,接收端会一直提醒。
-
拥塞控制
如果把窗口定的很大,发送端连续发送大量的数据,可能造成网络的拥堵。为了防止拥堵,进行拥塞控制。
(1)慢启动:定义拥塞窗口,一开始将该窗口大小设为1,之后每次收到一次确认应答(一次成功来回传输),将拥塞窗口大小 乘以2,呈指数增长。
(2)拥塞避免:设置慢启动阈值,一般开始都设为65536。拥塞避免是指当拥塞窗口大小达到这个阈值,拥塞窗口的值不再指数上升,而是+1,让其缓慢增加。
(3)快恢复:将报文段的超时重传看做拥塞,则一旦发生超时重传,我们就将阈值设为当前窗口大小的一半,并且窗口大小也变为原来窗口大小一半,如果收到新的ACK,表明重传的包成功了,那么退出快速恢复算法,进入拥塞避免算法
(4)快速重传:数据一旦丢失,接收端会一直提醒。发送3次重复确认应答,发送端收到后立即重传数据包,不用等待超时。
-
-
请说说TCP的ACK机制,有什么好处?⭐⭐⭐⭐⭐
由于通信过程的不可靠性,传输的数据不可避免的会出现丢失、延迟、错误、重复等各种状况,TCP协议为解决这些问题设计了一系列机制。这个机制的核心,就是发送方向接收方发送数据后,接收方要向发送方发送ACK(回执)。如果发送方没接收到正确的ACK,就会重新发送数据直到接收到ACK为止。
比如:发送方发送的数据序号是seq,那么接收方会发送seq + 1作为ACK,这样发送方就知道接下来要发送序号为seq + 1的数据给接收方了。
-
如何让UDP也变得可靠?⭐⭐⭐⭐⭐
加入TCP可靠性机制:
-
序列号、确认应答、超时重传
数据到达接收方,接收方需要发出一个确认应答,表示已经收到该数据段,并且确认序列号,序列号说明了它下一次需要接收的数据序列号,保证数据传输有序。如果发送方迟迟未收到确认应答,那么可能是发送的数据丢失,也可能是确认应答丢失,这时发送方在等待一段时间后进行重传。
-
窗口控制
TCP会利用窗口控制来提高传输速度,意思是在一个窗口大小内,不用一定等到应答才能发送下一段数据,窗口大小就是无需等待确认而可以继续发送数据的最大值。如果不使用窗口控制,每一个没收到确认应答的数据都要重发。
使用窗口控制,如果数据段1001-2000丢失,后面数据每次传输,确认应答都会不停发送序号为1001的应答,表示我要接收1001开始的数据,发送端如果收到3次相同应答,就会立刻进行重发;数据一旦丢失,接收端会一直提醒。
-
拥塞控制
如果把窗口定的很大,发送端连续发送大量的数据,可能造成网络的拥堵。为了防止拥堵,进行拥塞控制。
(1)慢启动:定义拥塞窗口,一开始将该窗口大小设为1,之后每次收到一次确认应答(一次成功来回传输),将拥塞窗口大小 乘以2,呈指数增长。
(2)拥塞避免:设置慢启动阈值,一般开始都设为65536。拥塞避免是指当拥塞窗口大小达到这个阈值,拥塞窗口的值不再指数上升,而是+1,让其缓慢增加。
(3)快恢复:将报文段的超时重传看做拥塞,则一旦发生超时重传,我们就将阈值设为当前窗口大小的一半,并且窗口大小也变为原来窗口大小一半,如果收到新的ACK,表明重传的包成功了,那么退出快速恢复算法,进入拥塞避免算法
(4)快速重传:数据一旦丢失,接收端会一直提醒。发送3次重复确认应答,发送端收到后立即重传数据包,不用等待超时。
-
-
什么是负载均衡?⭐⭐⭐⭐⭐
当一台服务器的单位时间内的访问量越大时,服务器压力就越大,大到超过自身承受能力时,服务器就会崩溃。为了避免服务器崩溃,让用户有更好的体验,我们通过负载均衡的方式来分担服务器压力。
什么是负载均衡:我们可以建立很多很多服务器,组成一个服务器集群,当用户访问网站时,先访问一个中间服务器,再让这个中间服务器在服务器集群中选择一个压力较小的服务器,然后将该访问请求引入该服务器。如此以来,用户的每次访问,都会保证服务器集群中的每个服务器压力趋于平衡,分担了服务器压力,避免了服务器崩溃的情况。
负载均衡有几种方式实现
(1)轮询(默认) 请求依次轮流往每个应用服务器上进行分配,分配策略比较简单。 缺点:不均匀,可能会出现,某些服务器接受的请求较重,负载压力重,有些负荷小,不可控。另外服务器之间需要进行session同步。
(2)权重轮询(权重越高,进入的几率越大) 优点:可以根据情况进行调整。可控,仍然需要进行session同步。
(3)IP-Hash 优点:采用hash的方式来映射服务器。无需进行session同步,固定IP会固定访问一台服务器。 缺点:恶意攻击,会造成某台服务器压垮。提供的服务不同,面向的地区不同,IP可能会出现集中,造成不均匀,不可控。
(4)Fair 这种相当于自适应,会根据服务器处理请求的速度进行负载均衡分配。处理请求最早结束的,拿到下一个请求。看上去是不是很好。但是一般都不使用,说是考虑到网络不稳定因素。还有待研究。这种也需要进行session同步。
(5)URL-Hash 这种是根据URL进行hash,这样某些请求永远打某台服务器。利于利用服务器的缓存,但是可能由于URL的哈希值分布不均匀,以及业务侧重造成某些服务器压力大,某些负荷低。这种也需要进行session同步。
-
Session和cookie的区别?⭐⭐⭐⭐⭐
由于HTTP是一种无状态协议,服务器没有办法单单从网络连接上面知道访问者的身份,为了解决这个问题,就诞生了Cookie
Cookie实际上是一小段的文本信息。客户端请求服务器,如果服务器需要记录该用户状态,就使用response向客户端浏览器颁发一个Cookie
客户端浏览器会把Cookie保存起来。当浏览器再请求该网站时,浏览器把请求的网址连同该Cookie一同提交给服务器。服务器检查该Cookie,以此来辨认用户状态。服务器还可以根据需要修改Cookie的内容。
实际就是颁发一个通行证,每人一个,无论谁访问都必须携带自己通行证。这样服务器就能从通行证上确认客户身份了。这就是Cookie的工作原理
cookie 可以让服务端程序跟踪每个客户端的访问,但是每次客户端的访问都必须传回这些Cookie,如果 Cookie 很多,这无形地增加了客户端与服务端的数据传输量,
而 Session 的出现正是为了解决这个问题。同一个客户端每次和服务端交互时,不需要每次都传回所有的 Cookie 值,而是只要传回一个 ID,这个 ID 是客户端第一次访问服务器的时候生成的, 而且每个客户端是唯一的。这样每个客户端就有了一个唯一的 ID,客户端只要传回这个 ID 就行了,这个 ID 通常是 NANE 为JSESIONID 的一个 Cookie。
区别:
1、数据存放位置不同:cookie数据存放在客户的浏览器上,session数据放在服务器上。
2、安全程度不同:cookie不是很安全,别人可以分析存放在本地的COOKIE并进行COOKIE欺骗,考虑到安全应当使用session。
3、性能使用程度不同:session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。
4、数据存储大小不同:单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie,而session则存储与服务端,浏览器对其没有限制。
5、会话机制不同 session会话机制:session会话机制是一种服务器端机制,它使用类似于哈希表(可能还有哈希表)的结构来保存信息。
cookies会话机制:cookie是服务器存储在本地计算机上的小块文本,并随每个请求发送到同一服务器。 Web服务器使用HTTP标头将cookie发送到客户端。在客户端终端,浏览器解析cookie并将其保存为本地文件,该文件自动将来自同一服务器的任何请求绑定到这些cookie。
-
网络调试的工具?⭐⭐⭐⭐⭐
-
Ping命令:ping属于一个通信协议,是TCP/IP协议的一部分。利用“ping”命令可以检查网络是否通畅或者网络连接速度,很好地分析和判定网络故障。
它的原理是:利用网络上机器IP地址的唯一性,给目标IP地址发送一个数据包,通过对方回复的数据包来确定两台网络机器是否连接相通,时延是多少。
-
Nslookup(name server lookup)是一个用于查询因特网域名信息或诊断DNS 服务器问题的工具.
-
Fiddler(中文名称:小提琴)是一个HTTP的调试代理,以代理服务器的方式,监听系统的Http网络数据流动,Fiddler可以也可以让你检查所有的HTTP通讯,设置断点,以及Fiddle所有的“进出”的数据
-
网站压力测试工具——webbench
-
常见面试题
-
请说说socket网络编程的步骤。⭐⭐⭐⭐
(1)服务器根据地址类型( ipv4, ipv6 )、 socket 类型、协议创建 socket。
(2)服务器为 socket 绑定 IP 地址和端口号。
(3)服务器 socket 监听端口号请求,随时准备接收客户端发来的连接,这时候服务器的socket 并没有被打开 。
(4)客户端创建 socket。
(5)客户端打开 socket,根据服务器 IP 地址和端口号试图连接服务器 socket。
(6)服务器 socket 接收到客户端 socket 请求,被动打开,开始接收客户端请求,直到客户端返回连接信息 。这时候 socket 进入阻塞状态,所谓阻塞即accept()方法一直到客户端返回连接信息后才返回,开始接收下一个客户端连接请求 。
(7)客户端连接成功,向服务器发送连接状态信息 。
(8)服务器 accept 方法返回,连接成功 。
(9)客户端向 socket 写入信息 。
(10)服务器读取信息 。
(11)客户端关闭 。
(12)服务器端关闭 。
-
请说说socket网络编程的接口。⭐⭐⭐⭐
-
socket函数创建接口
-
bind函数绑定IP地址和端口号
-
listen函数监听
-
accept接受客户端请求
-
close函数关闭socket
-
connect函数是客户端请求
-
read和write函数用来读取和发送数据
-
-
什么是TCP粘包现象?⭐⭐⭐⭐⭐
在socket网络程序中,
- 发送端为了将多个发往接收端的包,更有效的发到对方,使用了优化方法(Nagle算法),将多次间隔较小、数据量小的数据,合并成一个大的数据块,然后进行封包。
- 或者接收端接收不及时,造成TCP缓冲区中存放多段数据。
这两种情况都会出现TCP粘包问题。
-
为什么会出现粘包现象?如何解决?⭐⭐⭐⭐⭐
- 由Nagle算法造成的发送端粘包。Nagle算法是一种改善网络传输效率的算法,但也可能造成困扰。这就造成了粘包。
- 接收端接收不及时造成的接收端粘包。TCP会把接收到的数据存在自己的缓冲区中,然后通知应用层取数据。当应用层由于某些原因不能及时取出TCP的数据,就会造成TCP缓冲区中存放多段数据。
解决办法是:科学封包和解包。
-
简述一下Nagle算法⭐⭐⭐
Nagle算法简单的说,当提交一段数据给TCP时,TCP并不立刻发送此段数据,而是等待一段时间,看看在等待期间是否还有要发送的数据,若有则会一次把多段数据发送出去。
-
为什么UDP不粘包⭐⭐⭐⭐⭐
对于UDP,不会使用块的合并优化算法,不存在封包,再加上UDP本身是一个“数据包“协议,也就是两段数据是有界限的。从TCP和UDP的头部结构体就可以很明显的看到,UDP头部是记录了数据的长度的,而TCP头部里面并没有记录数据长度的变量。
-
什么是封包和解包?⭐⭐⭐⭐⭐
封包就是给一段数据加上包头,这样一来数据包就分为包头和包体两部分内容了(可加上包尾)。包头其实是一个大小固定的结构体,其中有个结构体成员变量表示包体的长度,这是个很重要的变量,其他的结构体成员可根据需要自己定义。根据固定的包头长度以及包头中含有的包体长度变量值就能正确的拆分出一个完整的数据包。
利用底层的缓冲区来进行解包时,由于TCP也维护了一个缓冲区,所以可以利用TCP的缓冲区来解包,也就是循环不停地接收包头给出的数据,直到收够为止,这就是一个完整的TCP包。
常见面试题
-
请说说HTTP的工作原理。⭐⭐⭐⭐⭐
HTTP协议采用了请求/响应模型。客户端向服务器发送一个请求报文,请求报文包含请求的方法、URL、协议版本、请求头部和请求数据。服务器以一个状态行作为响应,响应的内容包括协议的版本、成功或者错误代码、服务器信息、响应头部和响应数据。
-
在浏览器地址栏键入URL,按下回车之后会经历哪些流程?⭐⭐⭐⭐⭐
- 浏览器向 DNS 服务器请求解析该 URL 中的域名所对应的 IP 地址;
- 解析出 IP 地址后,根据该 IP 地址和默认端口 80,和服务器建立TCP连接;
- 浏览器发出读取文件(URL 中域名后面部分对应的文件)的HTTP 请求,该请求报文作为 TCP 三次握手的第三个报文的数据发送给服务器;
- 服务器对浏览器请求作出响应,并把对应的 html 文本发送给浏览器;
- 释放 TCP连接;
- 浏览器解析html代码,并请求html代码中的资源,最后对页面进行渲染呈现给用户。
-
请你说说HTTP请求包含哪些内容?⭐⭐⭐⭐
请求报文包含请求的方法、URL、协议版本、请求头部和请求数据。
-
请说说有哪些请求方法?⭐⭐⭐⭐⭐
请求方法,GET和POST是最常见的HTTP方法,除此以外还包括DELETE、HEAD、OPTIONS、PUT、TRACE。
-
get和post的区别是什么?⭐⭐⭐⭐⭐
- get将数据放在url后面,post将数据放在报文体
- url长度会受到特定的浏览器及服务器的限制,如IE对URL长度的限制是2083字节(2K+35)。而报文体长度没有限制
- get将数据放在url后面,信息并不安全;post方法将数据放在报文体中,更安全。
-
请你说说HTTP状态码⭐⭐⭐⭐⭐
状态码 意义 解释 301 Permanently Moved 被请求的资源已永久移动到新位置,新的URL在Location头中给出,浏览器应该自动地访问新的URL。301为永久重定向。 302 Found 请求的资源现在临时从不同的URL响应请求。302为临时重定向。 200 OK 表示从客户端发来的请求在服务器端被正确处理 304 Not Modified 告诉浏览器可以从缓存中获取所请求的资源。 400 bad request 请求报文存在语法错误 403 forbidden 表示对请求资源的访问被服务器拒绝 404 not found 表示在服务器上没有找到请求的资源 500 internal sever error 表示服务器端在执行请求时发生了错误 503 service unavailable 表明服务器暂时处于超负载或正在停机维护,无法处理请求 -
请说说HTTP响应头有哪些内容?⭐⭐⭐⭐
HTTP响应也是由三个部分组成,分别是:状态行、消息报头、响应正文。
-
请说说HTTP协议的特点⭐⭐⭐⭐
-
支持客户/服务器模式
HTTP协议规定,请求从客户端发出,最后服务器端响应该请求并返回。换句话说,肯定是先从客户端开始建立通信的,服务器端在没有接收到请求之前不会发送响应。
-
简单快速
客户向服务器请求服务时,只需传送请求方法和路径。请求方法常用的有GET、HEAD、POST。由于HTTP协议简单,使得HTTP服务器的程序规模小,因而通信速度很快。
-
灵活
HTTP允许传输任意类型的数据对象。正在传输的类型由Content-Type(Content-Type是HTTP包中用来表示内容类型的标识)加以标记。
-
无连接
无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
-
无状态
无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。即我们给服务器发送 HTTP 请求之后,服务器根据请求,会给我们发送数据过来,但是,发送完,不会记录任何信息。
-
-
HTTP的无连接是什么意思?⭐⭐⭐⭐
无连接的含义是限制每次连接只处理一个请求。服务器处理完客户的请求,并收到客户的应答后,即断开连接。采用这种方式可以节省传输时间。
-
HTTP的无状态是什么意思?⭐⭐⭐⭐
无状态是指协议对于事务处理没有记忆能力,服务器不知道客户端是什么状态。即我们给服务器发送 HTTP 请求之后,服务器根据请求,会给我们发送数据过来,但是,发送完,不会记录任何信息。
-
HTTP1.0、HTTP1.1的区别⭐⭐⭐⭐
- 长连接(Persistent Connection):HTTP1.1支持长连接和请求的流水线处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟。
- 节约带宽:HTTP1.0中存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能。HTTP1.1支持只发送header信息(不带任何body信息),如果服务器认为客户端有权限请求服务器,则返回100,客户端接收到100才开始把请求body发送到服务器;如果返回401,客户端就可以不用发送请求body了节约了带宽。
- HOST域:在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname),HTTP1.0没有host域。HTTP1.1的请求消息和响应消息都支持host域,且请求消息中如果没有host域会报告一个错误(400 Bad Request)。
- 缓存处理:在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。
-
什么是长连接?⭐⭐⭐⭐⭐
HTTP1.1规定了默认保持长连接(HTTP persistent connection ,也有翻译为持久连接),数据传输完成了保持TCP连接不断开(不发RST包、不四次握手),等待在同域名下继续用这个通道传输数据;相反的就是短连接。长连接的好处是效率高,缺点是占用资源。
-
HTTP2.0有哪些改动?⭐⭐⭐⭐
- 多路复用允许同时通过单一的 HTTP/2 连接发起多重的请求-响应消息。
- 二进制分帧:在应用层(HTTP/2)和传输层(TCP or UDP)之间增加一个二进制分帧层。将所有传输的信息分割为更小的消息和帧(frame),并对它们采用二进制格式的编码 ,其中 HTTP1.x 的首部信息会被封装到 HEADER frame,而相应的 Request Body 则封装到 DATA frame 里面。
- 首部压缩:http1.x的header由于cookie和user agent很容易膨胀,而且每次都要重复发送。http2.0使用encoder来减少需要传输的header大小
- 服务端推送:http2.0能通过push的方式将客户端需要的内容预先推送过去
-
HTTPS的加密原理⭐⭐⭐⭐⭐
HTTPS采用对称密钥加密和非对称密钥加密两种方式,两者混合加密。两者都有各自的优点。对称密钥加密处理速度快,但密钥无法安全发送给对方;非对称密钥加密处理速度慢,但密钥能够安全交换。但如果我们将两种加密方式一起使用,则两种加密方式就能互补。
也就是说,利用非对称密钥加密方式安全地交换在稍后的对称密钥加密中要使用的密钥,在确保密钥安全前提下,使用对称密钥加密方式进行通信。
-
什么是对称加密?什么是非对称加密?两者区别?⭐⭐⭐⭐⭐
-
对称密钥加密:加密与解密使用同一个密钥
也就是说在加密的同时,也会把密钥发送给对方。在发送密钥过程中可能会造成密钥被窃取
-
非对称密钥加密
非对称密钥有两把密钥,一把叫私有密钥,另一把叫公有密钥,私有密钥不让任何人知道,公有密钥随意发送。
也就是说,发送密文时,使用对方的公有密钥进行加密,对方接受到信息后,使用私有密钥进行解密。在不使用私有密钥情况下很难还原信息。
-
-
对称加密有哪些?非对称加密有哪些?⭐⭐⭐⭐⭐
一种是对称密钥加密例如:DES、AES-GCM、ChaCha20-Poly1305等,一种是非对称密钥加密,例如:RSA、DSA、ECDSA、 DH、ECDHE
-
数字证书用来干嘛的?⭐⭐⭐⭐⭐
服务器会给客户端发出数字证书来证明自己的身份。客户端在接受到服务端发来的SSL证书时,会对证书的真伪进行校验。证书中包含的具体内容有:
- 证书的发布机构CA
- 证书的有效期
- 公钥
- 证书所有者
- 签名
这样我们通过数字证书,就可以安全交换对称秘钥了,既解决了公钥获取问题,又解决了黑客冒充问题,一箭双雕。
-
HTTPS为什么比HTTP更安全⭐⭐⭐⭐⭐
在HTTP基础上我们通过加密后,又衍生出了新的通信协议——HTTPS。HTTPS 协议(HyperText Transfer Protocol over Secure Socket Layer):可以理解为HTTP+SSL/TLS, 即 HTTP 下加入 SSL 层,HTTPS 的安全基础是 SSL,因此加密的详细内容就需要 SSL,用于安全的 HTTP 数据传输。
-
HTTPS和HTTP的区别⭐⭐⭐⭐⭐
- http是是明文传输,https则是具有安全性的tls加密传输协议。
- https除了三次握手以外,还要进行ssl握手,协商加密使用对称密钥
- https需要服务端申请证书,浏览器端安装根证书
- 端口也不一样,前者是80,后者是443
-
HTTPS的通信建立过程⭐⭐⭐⭐⭐
-
在使用HTTPS是需要保证服务端配置正确了对应的安全证书
-
客户端发送请求到服务端
-
服务端返回公钥和数字证书到客户端
-
客户端接收后会验证证书的安全性,如果通过,则会随机生成一个随机数,用公钥对其加密,发送到服务端
-
服务端接受到这个加密后的随机数后,会用私钥对其解密得到真正的随机数,随后用这个随机数当做对称加密密钥对需要发送的数据进行对称加密
-
客户端在接收到加密后的数据对称加密密钥与服务器通信。
-
SSL加密建立
-
常见面试题
-
请说说你对嵌入式的理解。⭐⭐⭐⭐
嵌入式系统是指以应用为中心,以计算机技术为基础,软件硬件可剪裁,适应应用系统对功能、成本、体积、功耗严格要求的专用计算机系统。
嵌入式系统主要由嵌入式微处理器、外围硬件设备、嵌入式操作系统以及用户应用软件等部分组成
-
精简指令集和复杂指令集的区别⭐⭐⭐⭐
- CISC(Complex Instruction SetComputer)是“复杂指令集”。自PC机诞生以来,处理器都采用CISC指令集方式。这种指令系统的指令不等长,指令的数目非常多,编程和设计处理器时都较为麻烦。
- RISC(Reduced Instruction SetComputing)是“精简指令集”。研究人员在对CISC指令集进行测试时发现,各种指令的使用频度相当悬殊,其中常使用的是一些比较简单的指令,它们仅占指令总数的20%,但在程序中出现的频度却占80%。RISC正是基于这种思想提出的。
-
请说说CPU的内部架构和工作原理⭐⭐⭐⭐⭐
CPU从逻辑上可以划分成3个部分,分别是控制单元、运算单元和存储单元,这三部分由CPU内部总线连接起来。
控制单元(CU, Control Unit)**:**控制单元是整个CPU的指挥控制中心,由程序计数器PC (Program Counter)、 指令寄存器IR (Instruction Register)组成。程序计数器包含当前正在执行的指令的地址,在每个指令被获取之后,程序计数器指向顺序中的下一个指令。指令寄存器用于暂存当前正在执行的指令。
运算单元(ALU, Arithmetic Logic Unit):是运算器的核心。可以执行算术运算 (包括加减乘数等基本运算及其附加运算)和逻辑运算 (包括移位、逻辑测试或两个值比较)。
存储单元:包括CPU片内缓存和寄存器组,是CPU中暂时存放数据的地方,里面保存着那些等待处理的数据,或已经处理过的数据,CPU访问寄存器所用的时间要比访问内存的时间短。采用寄存器,可以减少CPU访问内存的次数,从而提高了CPU的工作速度。
CPU的运行原理就是:控制单元在时序脉冲的作用下,将程序计数器里所指向的指令地址送到地址总线上去,然后CPU将这个地址里的指令读到指令寄存器进行译码。对于执行指令过程中所需要用到的数据,会将数据地址也送到地址总线,然后CPU把数据读到CPU的内部存储单元(就是内部寄存器)暂存起来,最后命令运算单元对数据进行处理加工。这个过程不断重复,直到程序结束。
-
请说说CPU的内核态与用户态⭐⭐⭐⭐⭐
多数CPU都有两种模式,即内核态与用户态。通常,在**PSW(Program Status Word,程序状态字寄存器)**中有一个二进制位控制这两种模式。当在内核态运行时,CPU可以执行指令集中的每一条指令,并且使用硬件的每一种功能。相反的,用户程序在用户态下运行,仅允许执行整个指令集的一个子集和访问所有功能的一个子集。相对应的,内核态与用户态也是操作系统的两种运行级别。内核态拥有最高权限,可以访问所有系统指令;用户态则只能访问一部分指令。
在CPU的所有指令中,有一些指令是非常危险的,如果错用,将导致整个系统崩溃。比如:清内存、设置时钟等。所以区分内核态与用户态主要是出于安全的考虑。
-
请说说CPU的流水线工作原理⭐⭐⭐
CPU执行一条指令时,也分为几个步骤,如指令取指(InstrucTIon Fetch)、指令译码(InstrucTIon Decode)、指令执行(Execute)、访存(Memory Access)、写回(Write-Back),CPU并不会等一条指令完全执行完才执行下一条指令,而是像流水一样。
-
嵌入式流水线工作有什么不同?⭐⭐⭐⭐
而嵌入式CPU为了更高的效率,采用的是超流水线结构。在普通流水线中,CPU执行一条指令被拆成了五个步骤,每个步骤执行时间可能都是1ns,但是有一个长指令,拆分成五个步骤时,指令执行(Execute)这个步骤却需要2ns,那整个流水线的效率就受制于指令执行(Execute)这个步骤了。
所以为了提高效率,我们可以把指令执行(Execute)这个步骤再拆分成两组(寄存器+组合逻辑),每组执行时间为1ns,这样我们的普通流水线成了六级流水线了,这就是超流水线。Intel的 i7 处理器有16级流水线,AMD的速龙64系列CPU流水线为20级。
-
什么是超流水线,为什么?⭐⭐⭐⭐
而嵌入式CPU为了更高的效率,采用的是超流水线结构。在普通流水线中,CPU执行一条指令被拆成了五个步骤,每个步骤执行时间可能都是1ns,但是有一个长指令,拆分成五个步骤时,指令执行(Execute)这个步骤却需要2ns,那整个流水线的效率就受制于指令执行(Execute)这个步骤了。
所以为了提高效率,我们可以把指令执行(Execute)这个步骤再拆分成两组(寄存器+组合逻辑),每组执行时间为1ns,这样我们的普通流水线成了六级流水线了,这就是超流水线。Intel的 i7 处理器有16级流水线,AMD的速龙64系列CPU流水线为20级。
-
什么是乱序执行?⭐⭐⭐⭐
乱序执行(out-of-order execution)是指CPU采用了允许将多条指令不按程序规定的顺序分开发送给各相应电路单元处理的技术。比方Core乱序执行引擎说程序某一段有7条指令,此时CPU将根据各单元电路的空闲状态和各指令能否提前执行的具体情况分析后,将能提前执行的指令立即发送给相应电路执行。在各单元不按规定顺序执行完指令后还必须由相应电路再将运算结果重新按原来程序指定的指令顺序排列后才能返回程序。
-
请说说CPU的两种体系结构,有什么区别⭐⭐⭐⭐⭐
计算机系统可以分为冯·诺依曼结构和哈佛结构。
冯·诺依曼结构具有公用的数据存储空间和程序存储空间,它们共享存储器总线。
哈佛结构则具有分离的数据和程序空间以及分离的访问总线。哈佛结构在指令执行时,取指和取数可以并行,因此具有更高的执行效率。
-
说说ROM和RAM的区别⭐⭐⭐⭐⭐
存储器类型 简介 作用 ROM 只读存储器(Read-Only Memory) 是一种只能读出事先所存数据的固态半导体存储器。其特性是一旦储存资料就无法再将之改变或删除。 RAM 随机存取存储器(random access memory)又称作“随机存储器” 是与CPU直接交换数据的内部存储器,也叫主存(内存)。它可以随时读写,而且速度很快。 -
说说你了解有哪些存储器类型⭐⭐⭐⭐
存储器类型 简介 作用 ROM 只读存储器(Read-Only Memory) 是一种只能读出事先所存数据的固态半导体存储器。其特性是一旦储存资料就无法再将之改变或删除。 RAM 随机存取存储器(random access memory)又称作“随机存储器” 是与CPU直接交换数据的内部存储器,也叫主存(内存)。它可以随时读写,而且速度很快。 SRAM 静态随机存取存储器(Static Random-Access Memory) 随机存取存储器的一种。所谓的“静态”,是指这种存储器只要保持通电,里面储存的数据就可以恒常保持。然而,当电力供应停止时,SRAM储存的数据还是会消失。 DRAM 动态随机存取存储器(DRAM) DRAM里面所储存的数据就需要周期性地更新。要刷新充电一次,否则内部的数据即会消失。 EPROM (Erasable Programmable ROM),可擦除可编程ROM 芯片通过紫外线可重复擦除和写入,解决了PROM芯片只能写入一次的弊端。EPROM芯片在写入资料后,还要以不透光的贴纸或胶布把窗口封住,以免受到周围的紫外线照射而使资料受损。使用并不方便。 PSRAM 全称Pseudo static random access memory。指的是伪静态随机存储器。 内部的内存颗粒跟SDRAM的颗粒相似,但外部的接口跟SDRAM不同,不需要SDRAM那样复杂的控制器和刷新机制,PSRAM的接口跟SRAM的接口是一样的。PSRAM 内部自带刷新机制。 EEPROM (electrically erasable, programmable, read-only )是一种电可擦除可编程只读存储器 其内容在掉电的时候也不会丢失。在平常情况下,EEPROM与EPROM一样是只读的,需要写入时,在指定的引脚加 上一个高电压即可写入或擦除,而且其擦除的速度极快 Flash 非易失闪存技术 它的主要特点是在不加电的情况下能长期保持存储的信息。就其本质而言,Flash Memory属于EEPROM(电擦除可编程只读存储器)类型。它既有ROM的特点,又有很高的存取速度,而且易于擦除和重写,功耗很小。 NOR Flash - NOR Flash的特点是芯片内执行(XIP, eXecute In Place),这样应用程序可以直接在flash闪存内运行,不必再把代码读到系统RAM中。NOR Flash的传输效率很高,在1~4MB的小容量时具有很高的成本效益,但是很低的写入和擦除速度大大影响了它的性能。 NAND Flash - NAND Flash结构能提供极高的单元密度,可以达到高存储密度,并且写入和擦除的速度也很快。应用NAND Flash的困难在于flash的管理需要特殊的系统接口。 -
说说你了解有哪些嵌入式操作系统,各自有什么特点?⭐⭐⭐
-
嵌入式Linux
嵌入式Linux(Embedded Linux)是标准Linux经过小型化裁剪处理之后的专用Linux操作系统,能够固化于容量只有几KB或者几MB的存储器芯片或者单片机中,适合于特定嵌入式应用场合。目前已经开发成功的嵌入式系统中,大约一半的系统使用嵌入式Linux。
-
VxWorks
VxWorks操作系统是美国WindRiver公司于1983年设计开发的一种嵌入式实时操作系统(RTOS),VxWorks具有以下优点。
(1)实时性好。
(2)可靠性高
(3)集成开发环境完备
但是,由于VxWorks源码不公开,它部分功能的更新(如网络功能模块)滞后。VxWorks的开发和使用都需要交高额的专利费,这就大大增加了用户开发的成本。
-
QNX
QNX独特的微内核和消息传递结构使其运行和开发时非常方便。QNX具有非常好的伸缩性,用户可以把应用程序代码和QNX内核直接编译在一起,使之为简单的嵌入式应用生成单一的映像。
-
Windows CE
Windows CE是微软公司开发的一个开放的、可升级的32位嵌入式操作系统,是基于掌上型电脑类的电子设备操作系统。Windows CE的图形用户界面相当出色,Windows CE具有模块化、结构化、基于Win32应用程序接口以及与处理器无关等特点。
-
Palm OS
Palm OS在PDA领域有着很大的用户群,一度占领PDA操作系统90%以上市场份额。Plam OS明显的特点是精简,它的内核只有几千个字节,同时用户也可以方便地开发、定制,具有较强的可操作性。
-
uC/OS
源代码公开,代码结构清晰、明了,注释详尽,组织有条理,可移植性好,可裁剪,系统短小精悍,是研究和学习实时操作系统的首选,但在工程应用领域使用较少。
-
-
什么是DMA⭐⭐⭐⭐⭐
DMA是指外部设备不通过CPU而直接与系统内存交换数据的接口技术。这样数据的传送速度就取决于存储器和外设的工作速度。DMA的出现就是为了解决批量数据的输入/输出问题。DMA的一个周期是指存储周期。
-
FreeRTOS、uCOS的区别⭐⭐⭐⭐
- 内核ROM和耗费RAM都比uCOS 小,特别是RAM。 这在单片机里面是稀缺资源,uCOS至少要5K以上, 而freeOS用2~3K也可以跑的很好。
- freeRTOS 可以用协程(Co-routine),减少RAM消耗(共用STACK)。uCOS只能用任务(TASK,每个任务有一个独立的STACK)。
- freeRTOS 可以有优先度一样的任务,这些任务是按时间片来轮流处理,uCOSII 每个任务都只有一个独一无二的优先级。因此,理论上讲,freeRTOS 可以管理超过64个任务,而uCOS只能管理64个。
- freeRTOS 是在商业上免费应用。uCOS在商业上的应用是要付钱的。
freeRTOS 不如uCOS的地方:
- 比uSOS简单,任务间通讯freeRTOS只支持Queque, Semaphores, Mutex。 uCOS除这些外,还支持Flag, MailBox.
- uCOS的支持比freeRTOS 多。除操作系统外,freeRTOS只支持TCPIP, uCOS则有大量外延支持,比如FS, USB, GUI, CAN等的支持
-
说说Linux proc下面有什么文件⭐⭐⭐
/proc/cmdline文件:这个文件给出了内核启动的命令行。它和用于进程的cmdline项非常相似。
/proc/cpuinfo文件:这个文件提供了有关系统CPU的多种信息。这些信息是从内核里对CPU的测试代码中得到的。
/proc/devices文件:这个文件列出字符和块设备的主设备号,以及分配到这些设备号的设备名称。
/proc/dma文件:这个文件列出由驱动程序保留的DMA通道和保留它们的驱动程序名称。
/proc/interrupts文件:这个文件的每一行都有一个保留的中断。
/proc/ioports文件:这个文件列出了诸如磁盘驱动器,以太网卡和声卡设备等多种设备驱动程序登记的许多I/O端口范围。
/proc/kcore文件:这个文件是系统的物理内存以core文件格式保存的文件。
-
说说中断使用场景和注意事项⭐⭐⭐⭐⭐
-
使用场景
在嵌入式领域中,针对实时性要求较高的场景下,可以使用中断来处理外部信号,以做到及时响应。如按键等。
-
注意事项
- 中断不能进行参数传递,没有返回值,不能调用中断函数
- 对于实时性要求不高的场景,尽量不设置中断
- 中断内,代码应少,这样中断才能尽快结束
- 对于频繁响应的场景,不应使用中断,因为会频繁打断主流程的运行
- 中断里面尽量不要放延时,CPU利用率低。如果非要延时,那不要使用踏步延时,应使用变量计数的方法达到延时的目的。
-
常见面试题
-
请说说ARM微处理器的特点。⭐⭐⭐
采用RISC架构的ARM微处理器一般具有如下特点:
- 体积小、低功耗、低成本、高性能;
- 支持Thumb(16位)/ARM(32位)双指令集,能很好的兼容8位/16位器件;
- 大量使用寄存器,指令执行速度更快;
- 大多数数据操作都在寄存器中完成;
- 寻址方式灵活简单,执行效率高;
- 指令长度固定;
除此以外,ARM体系结构还采用了一些特别的技术,在保证高性能的前提下尽量缩小芯片的面积,并降低功耗:
- 所有的指令都可根据前面的执行结果决定是否被执行,从而提高指令的执行效率。
- 可用加载/存储指令批量传输数据,以提高数据的传输效率。
- 可在一条数据处理指令中同时完成逻辑处理和移位处理。
- 在循环处理中使用地址的自动增减来提高运行效率。
- 支持协处理器来扩展ARM的功能
-
请说说你了解哪些ARM系列,都应用在什么地方?⭐⭐⭐
ARM7、9、11,Cortex系列、SecurCore系列性能各有千秋,被用于不同的领域之中。
-
SecurCore系列和Cortex系列各自有什么优势?⭐⭐⭐
- SecurCore系列:SecurCore系列涵盖了SC100、SC110、SC200和SC210微处理器核。该系列处理器主要针对新兴的安全市场,以一种全新的安全处理器设计为智能卡和其他安全IC开发提供独特的32位系统设计,并具有特定的反伪造方法,从而有助于防止对硬件和软件的盗版。
- Cortex系列:Cortex-M3处理能力相当于ARM7,处理器结合了多种突破性技术,令芯片供应商提供超低费用的芯片。该处理器还集成了许多紧耦合系统外设,令系统能满足下一代产品的控制需求。Cortex的优势应该在于低功耗、低成本、高性能3者(或2者)的结合。
-
ARM处理器模式有哪几种?简要介绍一下。⭐⭐⭐⭐⭐
-
用户模式(Usr,User Mode):ARM处理器正常的程序执行状态。
-
快速中断模式(FIQ,Fast Interrupt Request Mode):用于高速数据传输或通道处理。当触发快速中断时进入此模式。
-
外部中断模式(IRQ,Interrupt Request Mode):用于通用的中断处理。当触发外部中断时进入此模式。
-
管理模式(Svc,Supervisor Mode):操作系统使用的保护模式。在系统复位或执行软件中断指令SWI时进入。
-
数据访问中止模式(abt,Abort Mode):当数据或指令预取中止时进入该模式,可用于虚拟存储及存储保护。
-
系统模式(sys,System Mode):运行具有特权的操作系统任务。
-
未定义指令中止模式(und,Undefined Mode):当未定义的指令执行时进入该模式,可用于支持硬件协处理器的软件仿真。
除了用户模式之外,其余六种模式都是特权模式。除了用户模式和系统模式之外,其余五种模式都是异常模式。
-
-
说说ARM处理器几种模式切换的过程⭐⭐⭐⭐⭐
- 执行软中断(SWI)或复位命令(Reset)指令。如果在用户模式下执行SWI指令,CPU就进入管理(Supervisor)模式。
- 有外部中断发生。如果发生了外部中断,CPU就会进入IRQ或FIQ模式。
- CPU执行过程中产生异常。最典型的异常是由于MMU保护所引起的内存访问异常,此时CPU会切换到Abort模式。如果是无效指令,则会进入Undefined模式。
- 有一种模式是CPU无法自动进入的,这种模式就是System模式,要进入System模式必须由程序员编写指令来实现。因此一般情况下,操作系统在通过SWI进入Supervisor模式后,做一些操作后,就进入System模式。
-
说说嵌入式中断的流程⭐⭐⭐⭐
IRQ中断和FIQ中断都属于ARM的异常模式。在ARM系统中,一旦有中断发生,不管是外部中断,还是内部中断,正在执行的程序都会停下来。接下来通常会按照如下步骤处理中断:
-
保存现场。保存当前的PC值到R14,寄存器R14常用作链接寄存器(LR,Link Register),当进入子程序时,常用来保存PC(Program Counter,程序计数器) 的返回值。保存PC值后,接着保存当前的程序运行状态到SPSR(Storage Program Status Register,程序状态备份寄存器)。
-
模式切换。根据发生的中断类型,进入IRQ模式或FIQ模式。
-
获取中断源。以异常向量表保存在低地址处为例,若是IRQ中断,则PC指针跳动0x18处(
0x18:LDR PC, IRQ_ADDR
);若是FIQ中断,则跳到0x1C处(0x1c:LDR PC, FIQ_ADDR
)。IRQ和FIQ的异常向量地址处一般保存的是中断服务子程序的地址,所以接下来PC指针跳入中断服务子程序处理中断。 -
中断处理。
-
中断返回,恢复现场。当完成中断服务子程序后,将SPSR中保存的程序运行状态恢复到CPSR(Current Program Status Register,当前程序状态寄存器)中,R14中保存的被中断程序的地址恢复到PC中,继续执行被中断的程序。
-
-
特权模式有哪些?异常模式有哪些?⭐⭐⭐⭐⭐
除了用户模式之外,其余六种模式都是特权模式。除了用户模式和系统模式之外,其余五种模式都是异常模式。
-
说说DMA⭐⭐⭐⭐
-
概念
为I/O使用一种特殊的直接存储器访问(Direct Memory Access,DMA)芯片,它可以直接控制外围设备的数据流,而无需持续的CPU干预。这样效率就很高了,但对应成本就相对高些,因为DMA是由专门的硬件( DMA)控制。
-
使用场景
DMA传送主要用于需要高速大批量数据传送的系统中,以提高数据的吞吐量。如磁盘存取、图像处理、高速数据采集系统、同步通信中的收/发信号等方面应用甚广。通常只有数据流量较大(kBps或者更高)的外设才需要支持DMA能力,这些应用方面典型的例子包括视频、音频和网络接口。
-
设置
因为无需CPU干预,那么DMA要进行数据传输就必须有两个条件:数据从哪传(源地址),数据传到哪里去(目的地址)。通过软件设置,设置好源地址和目的地址。在一个重要的条件就是触发源是什么,就是说什么时候进行DMA数据传输呢?这叫触发信号。也可以通过软件编程设置具体时间,具体条件来触发DMA数据传输。
-
-
中断和异常的区别是什么?⭐⭐⭐⭐⭐
异常与中断不同,它在产生时必须考虑与处理器时钟同步。实际上,异常也常常称为同步中断。在处理器执行到由于编程失误而导致的错误指令(例如被0除)的时候,或者在执行期间出现特殊情况(例如缺页),必须靠内核来处理的时候,处理器就会产生一个异常。
中断的工作方式与之类似,其差异只在于中断是由硬件而不是软件引起的。
-
请你说说大端模式和小端模式⭐⭐⭐⭐⭐
小端模式:低的有效字节存储在低的存储器地址。常用的X86结构是小端模式。很多的ARM,DSP都为小端模式。
大端模式:高的有效字节存储在低的存储器地址。KEIL C51则为大端模式。
-
ARM是大端模式还是小端模式?51单片机呢?
小端模式:低的有效字节存储在低的存储器地址。常用的X86结构是小端模式。很多的ARM,DSP都为小端模式。
大端模式:高的有效字节存储在低的存储器地址。KEIL C51则为大端模式。
-
什么是MMU?工作原理是什么?⭐⭐⭐⭐⭐
-
MMU(Memory Management Unit)主要用来管理虚拟内存、物理内存的控制线路,同时也负责虚拟地址映射为物理地址。
如果处理器没有MMU,或者有MMU但没有启用,CPU执行单元发出的内存地址将直接传到芯片引脚上,被内存芯片(以下称为物理内存,以便与虚拟内存区分)接收,这称为物理地址(Physical Address,以下简称PA)
如果处理器启用了MMU(一般是在bootloader中的eboot阶段的进入main()函数的时候启用),CPU执行单元发出的内存地址将被MMU截获,从CPU到MMU的地址称为虚拟地址(Virtual Address,以下简称VA),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将VA映射成PA
大多数使用MMU的机器都采用分页机制。虚拟地址空间以页为单位进行划分(对于32位的CPU,通常一页为4k),而相应的物理地址空间也被划分,其使用的单位称为页帧,页帧和页必须保持相同,因为内存与外部存储器之间的传输是以页为单位进行传输的。
-
MMU的作用:一是扩大地址空间,二是内存保护与共享,三是避免内存碎片
-
-
说说TLB⭐⭐⭐⭐⭐
TLB(Translation Lookaside Buffer),也就是高速后备缓冲区,在TLB中可以存放一部分段表中的内容。当CPU访问内存时,先访问TLB查找访问的内存映射关系是否存放在TLB中。如果在,则从TLB中读取映射关系,否则再去访问内存中的段表,从段表中读取映射关系,并把相应的结果添加到TLB中,更新它的内容。这样CPU下一次又需要该地址映射关系的话,就可以直接从TLB取得。因为TLB的访问速度比内存的访问速度要快得多,如果采取一定的技术,使访问TLB的命中率比较高的话,就可以大大提高访问内存的速度。
简单来说,TLB就是映射表的缓存。
-
说说DSP⭐⭐⭐⭐⭐
DSP(digital singnal processor)是一种独特的微处理器,是以数字信号来处理大量信息的器件。其工作原理是接收模拟信号,转换为0或1的数字信号,再对数字信号进行修改、删除、强化,并在其他系统芯片中把数字数据解译回模拟数据或实际环境格式。采用哈佛结构,将程序和数据空间分开,可以同时访问指令和数据;DSP优势在于其有独特乘法器,一个指令就可以完成乘加运算。并且同时访问指令和数据,大大提高效率。
-
说说DSP和ARM有什么区别?⭐⭐⭐⭐⭐
ARM与DSP的区别:ARM处理器广泛的使用在许多嵌入式系统。ARM处理器的特点有指令长度固定,执行效率高,低成本,低功耗等。ARM内部资源丰富,外部外设接口丰富,通用性好。偏重控制。DSP偏重运算,有独特乘法器,一个指令就可以完成乘加运算,适合于信号、图像等需要大量数字运算的地方。
-
说说STM32⭐⭐⭐
STM32系列基于专为要求高性能、低成本、低功耗的嵌入式应用专门设计的ARM控制器。
我们以STM32F103为例,其具备以下资源:
- 内核:ARM32位Cortex-M3 CPU,最高工作频率72MHz,1.25DMIPSMHz。单周期乘法和硬件除法。
- 存储器:片上集成512KB的Flash存储器。6-64KB的SRAM存储器。
- 低功耗:3种低功耗模式:休眠,停止,待机模式。
- 调试模式:串行调试(SWD)和JTAG接口。
- DMA:12通道DMA控制器。
- 支持的外设:定时器,ADC,DAC,SPI,IIC和UART。
- 2个12位的us级的AD转换器(16通道):AD测量范围:0-3.6 V。双采样和保持能力。片上集成一个温度传感器。
- 2通道12位DA转换器:STM32F103xC,STM32F103xD,STM32F103xE独有。
- 最多高达112个的快速IO端口
- 最多多达11个定时器:
- 2个看门狗定时器(独立看门狗和窗口看门狗)。
- 最多多达13个通信接口:2个IIC接口(SMBusPMBus)。5个USART接口(ISO7816接口,LIN,IrDA兼容,调试控制)。3个SPI接口(18 Mbits),两个和IIS复用。CAN接口(2.0B)。USB 2.0全速接口。SDIO接口。
-
说说51单片机⭐⭐⭐
51单片机是对所有兼容Intel 8031指令系统的单片机的统称。
我们以STC51单片机为例,其具备以下资源:
- 8位CPU,4kbytes程序存储器(ROM) (52为8K)
- 128bytes的数据存储器(RAM) (52有256bytes的RAM)
- 32条I/O口线(P1,P2,P3,P4),111条指令,大部分为单字节指令
- 2个可编程定时/计数器,5个中断源,2个优先级(52有6个)
- 一个全双工串行通信口
- 单一+5V电源供电
51单片机是大端模式。
-
单片机如何选型⭐⭐⭐
最大准则应该是面向项目需求选型。
- 选择的单片机至少要能满足项目需求,需要几个I2C、SPI、UART?需要多大内存?多少算力?是否需要A/D、D/A转换?
- 在满足基本的需求后,就要考虑选择的单片机不要太冷门,最好是比较普遍使用的,资料多、服务全,缩短开发周期。同事发生故障容易更换
- 然后要考虑成本,当然是选择尽量便宜的单片机了。
- 还要考虑应用场景,有的场景要求低功耗,有的场景要求可靠性高,有的场景干扰大等等。
-
你用过什么单片机?⭐⭐⭐
用过STM32。
STM32系列基于专为要求高性能、低成本、低功耗的嵌入式应用专门设计的ARM控制器。
STM32拥有丰富的接口资源。以STM32F103为例,其具备:
- 片上集成512KB的Flash存储器。6-64KB的SRAM存储器。
- 3种低功耗模式:休眠,停止,待机模式。
- DMA:12通道DMA控制器
- 支持的外设:定时器,ADC,DAC,SPI,IIC和UART
- 最多高达112个的快速IO端口
- 最多多达11个定时器:
- 最多多达13个通信接口
我使用STM32具体做了XXXX项目/比赛(接下来说说用这个单片机做了什么事情)
-
你用过什么传感器?⭐⭐⭐
我用过金属应变片传感器,是在电子设计大赛中使用来制作一个电子秤。这个传感器的工作原理是:通过电阻丝受外力而产生的形变来计算所受外力的大小。
传感器有几个比较重要的指标:量程、线性度、灵敏度、分辨力、漂移。
对于金属应变片来说,会受到温度、湿度、震动等影响,产生零位漂移现象,通常我们可以采用电桥来补偿。
-
C语言结构体怎么定义节省内存⭐⭐⭐⭐⭐
-
在保证值域足够的情况下,用小字节变量代替大字节变量,如用short替代int,float替代double
-
将各成员按其所占字节数从小到大声明,以尽量减少中间的填补空间(字节对齐)。
-
可以取消字节对齐,
#pragma pack(1)
,当然这会牺牲效率,谨慎采用。
-
单片机main函数之前做了哪些工作⭐⭐⭐⭐⭐
主要完成两部分工作,一个是硬件执行环境,如建立中断向量表、初始化堆栈寄存器、关闭看门狗、初始化内部或者外部RAM等,另一个是软件环境,如加载C库环境、ZI(未初始化的内存变量)、初始化堆栈指针等。
-
程序是怎么编译成bin文件的
GCC的objcopy工具可以实现这个功能,具体方法是:
-
先用gcc把douya.c编译成douya.o:
gcc -c douya.c -o douya.o
-
将douya.o转为bin文件:
objcopy -O binary douya.o douya.bin
常见面试题
-
说一说什么是BootLoader⭐⭐⭐⭐⭐
BootLoader就是在运行操作系统内核之前所运行的一段小程序。通过这段小程序,可以对系统的硬件设备进行初始化、建立内存空间的映射图,从而将系统的软、硬件设置成一个合适的环境,以便为最终调用操作系统内核做好准备。嵌入式BootLoader的核心任务就是引导嵌入式操作系统运行起来。
-
同一个BootLoader程序可以运行在不同的板子上吗?为什么?⭐⭐⭐⭐
可能不行。
BootLoader不仅依赖于CPU的体系结构,也依赖于具体的嵌入式板级设备的配置,比如硬件地址分配,RAM芯片的类型及其他外设的类型等。也就是说,对于两块不同的嵌入式板而言,即使它们是基于同一种CPU而构建的,如果它们的硬件资源和配置不一致,要想让运行在一块板子上的BootLoader程序也能运行在另一块板子上,还需要对BootLoader进行裁剪和移植。
-
BootLoader的启动有哪两个阶段?⭐⭐⭐⭐⭐
BootLoader的stage1通常包括以下步骤:
- 硬件设备初始化。
- 为加载BootLoader的stage2准备RAM空间。
- 复制BootLoader的stage2到RAM空间中。
- 设置好堆栈。
- 跳转到stage2的C入口点。
BootLoader的stage2通常包括以下步骤:
- 初始化本阶段要使用的硬件设备。
- 检测系统内存映像。
- 将kernel映像和根文件系统映像从Flash上读到RAM空间中。
- 为内核设置启动参数。
- 调用内核。
-
说一说你熟悉的BootLoader⭐⭐⭐
U-Boot,全称Universal Boot Loader,是遵循GPL条款的开放源码项目。其源码目录、编译形式与Linux内核很相似,
但是U-Boot不仅仅支持嵌入式Linux系统的引导,还支持相当多的操作系统,这是U-Boot中Universal的一层含义;其U-Boot主要有如下优点:
- 开放源码;
- 支持多种嵌入式操作系统内核,如Linux、NetBSD, VxWorks, QNX,RTEMS, ARTOS, LynxOS;
- 支持多个处理器系列,如PowerPC、ARM、x86、MIPS、XScale;
- 较高的可靠性和稳定性;[插图] 高度灵活的功能设置,适合U-Boot调试、操作系统不同引导要求、产品发布等;
- 丰富的设备驱动源码,如串口、以太网、SDRAM、Flash、NVRAM、EEPROM、LCD、键盘等;
- 较为丰富的开发调试文档与强大的网络技术支持。
-
说一说BootLoader的两种模式⭐⭐⭐
启动加载(BootLoading)模式:这种模式也称为“自主”(autonomous)模式。也即BootLoader从目标机上的某个固态存储设备上将操作系统加载到RAM中运行,整个过程并没有用户的介入。这种模式是BootLoader的正常工作模式,因此在嵌入式产品发布时,BootLoader显然必须工作在这种模式下。
下载(DownLoading)模式:在这种模式下,目标机上的BootLoader将通过串口连接或网络连接等通信手段从主机(host)下载文件,比如:下载更新的BootLoader、下载内核映像和根文件系统映像等。
-
说一说什么是驱动程序⭐⭐⭐⭐⭐
一个驱动程序就是一个函数和数据结构的集合,它的目的是建立内核和实际硬件之间的连接,从而提供通过内核访问底层硬件的上层API接口。内核用这个接口请求驱动程序控制设备的I/O操作。
-
说一说设备的种类,各自有什么特点。⭐⭐⭐⭐⭐
Linux支持3种不同类型的设备:字符设备(character devices)、块设备(blockdevices)和网络接口(network interfaces)。
- 字符设备以字节为单位进行数据处理,一般不使用缓存技术。大多数字符设备仅仅是数据通道,只能按顺序读/写(但是也有些字符设备可以实现随机读/写)。典型的字符设备有:鼠标、键盘、I/O设备等。字符设备驱的源码一般在目录drivers/char中。
- 块设备数据可以按可寻址的块为单位进行处理,块的大小通常为512B到32 KB不等。大多数块设备允许随机访问,而且常常采用缓存技术。块设备有:硬盘、光盘驱动器等。文件系统一般都要求能随机访问,因此通常采用块设备。
- 网络接口用于网络通信,可能针对某个硬件,如网卡或纯软件,如loopback。网络接口只是面向数据包而不是数据流,所以内核的处理也不同,没有映射成任何设备文件,而是按照UNIX标准给它们分配一个唯一的名字。
-
什么是交叉编译?为何要有交叉编译⭐⭐⭐⭐⭐
交叉编译,就是:在一种平台上编译,编译出来的程序,是放到别的平台上运行即编译的环境,和运行的环境不一样,属于交叉的,此所谓cross,此所谓:在x86平台上编译,在ARM平台上运行
之所以要有交叉编译,主要原因是:嵌入式系统中的资源太少。没办法进行本地编译
-
说说Linux设备驱动开发有哪些API?⭐⭐⭐
open() close() read() write()等
常见面试题
-
说一说I2C的时序图,如何传输数据⭐⭐⭐⭐⭐
I2C总线只有两根双向信号线。一根是数据线SDA,另一根是时钟线SCL。
SDA:双向数据线,为OD (Open Drain,漏极输出) 门,与其它任意数量的OD与OC (Open Collector,集电极开路) 门成\线与\关系。
SCL:上升沿将数据输入到每个I2C从设备中;下降沿驱动I2C从设备输出数据。(边沿触发)
-
空闲状态:I2C总线总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。
-
起始信号:当SCL为高电平期间,SDA由高到低的跳变;
-
停止信号:当SCL为高电平期间,SDA由低到高的跳变;
然后我们来看看如何传输数据的,如图:
-
数据传输以字节为单位。
主设备在SCL线上产生每个时钟脉冲,将在SDA线上传输一个数据位,当一个字节按数据位从高位到低位的顺序传输完后,紧接着从设备将拉低SDA线,回传给主设备一个应答位(ACK),此时才认为一个字节真正的被传输完成。
当然,并不是所有的字节传输都必须有一个应答位,比如:当从设备不能再接收主设备发送的数据时,从设备将回传一个否定应答位。
-
I2C如何选择从设备⭐⭐⭐⭐
系统中的所有外围器件通常具有一个7位的从器件专用地址码,其中高4位为器件类型,由生产厂家制定,低3位为器件引脚定义地址,由使用者定义(I2C还支持10位寻址)。
-
说一说SPI总线⭐⭐⭐⭐⭐
SPI是串行外设接口。一 种同步串行接口技术,是一种高速的,全双工,同步的通信总线。
SPI的通信原理很简单,它以主从方式工作,这种模式通常有一个主设备和一个或多 个从设备,需要至少4根线,事实上3根也可以(单向传输时)。也是所有基于SPI的设备共有的,它们是SDI (数据输入)、SDO (数据输出)、SCLK (时钟)、CS (片选)。
- SDO/MOSI – 主设备数据输出,从设备数据输入;
- SDI/MISO – 主设备数据输入,从设备数据输出;
- SCLK – 时钟信号,由主设备产生;
- CS/SS – 从设备使能信号,由主设备控制。当有多个从设备的时候,因为每个从设备上都有一个片选引脚接入到主设备机中,当我们的主设备和某个从设备通信时需要将从设备对应的片选引脚电平拉低或者是拉高。
-
说说SPI的四种工作模式,由什么决定?⭐⭐⭐
SPI总线有四种工作方式(SP0, SP1, SP2, SP3),其中使用的最为广泛的是SPI0和SPI3方式。SPI模块为了和外设进行数据交换,根据外设工作要求,其输出串行同步时钟的极性和相位可以进行配置,如表格所示:
SPI模式 时钟极性 (CPOL) 时钟相位 (CPHA) 空闲状态下的时钟极性 采样 SP0 0 0 逻辑低电平 第一个跳变沿 (上升或下降) 数据被采样 SP1 0 1 逻辑低电平 第二个跳变沿 (上升或下降) 数据被采样 SP2 1 1 高电平 第二个跳变沿 (上升或下降) 数据被采样 SP3 1 0 高电平 第一个跳变沿 (上升或下降) 数据被采样 时钟极性 (CPOL) 对传输协议没有重大的影响。如果CPOL=0,串行同步时钟的空闲状态为低电平;如果CPOL=1,串行同步时钟的空闲状态为高电平。
时钟相位 (CPHA) 能够配置用于选择两种不同的传输协议之一进行数据传输。如果 CPHA=0,在串行同步时钟的第一个跳变沿 (上升或下降) 数据被采样;如果CPHA=1,在串行同步时钟的第二个跳变沿 (上升或下降) 数据被采样。
SPI主模块和与之通信的外设时钟相位和极性应该一致。
SPI接口有四种不同的数据传输时序,取决于CPOL和CPHL这两位的组合。
-
说说UART的数据通信格式⭐⭐⭐⭐⭐
在串口通讯的协议层中,规定了数据包的内容,它由起始位,主体数据,校验位以及停止位组成,通讯双方的数据包格式以及波特率要约定一致才能正常收发数据。
其中各位的意义如下:
- 起始位:先发出一个逻辑”0”信号,表示传输字符的开始。
- 数据位:可以是5~8位逻辑”0”或”1”。如ASCII码(7位),扩展BCD码(8位)。小端传输
- 校验位(可选):数据位加上这一位后,使得“1”的位数应为偶数(偶校验)或奇数(奇校验)。不过可以不选。
- 停止位:它是一个字符数据的结束标志。可以是1位、1.5位、2位的高电平。
- 空闲位:处于逻辑“1”状态,表示当前线路上没有资料传送。
-
说说串口的使用场景,你在什么场景下使用的串口?⭐⭐⭐
串口是计算机设备的通用接口,如鼠标、键盘等。
我一般是用来调试嵌入式系统时使用。
-
你一般使用串口,波特率设置多少?⭐⭐⭐
波特率:每秒传输的码元数。常用的波特率有4800、9600、19200、115200。
我平时使用的9600。
-
说说常用的数据校验算法:⭐⭐⭐⭐⭐
常用的数据校验算法:
- **奇偶校验:**奇偶校验是检测错误的最古老的方法。用于检查数据传输的完整性。校验方法非常简单,只需要在数据上添加一个额外的位,这个额外的位称为奇偶校验位。若用奇校验,则当接收端收到这组代码时,校验“1”的个数是否为奇数,从而确定传输代码的正确性。对于偶校验,校验位就定义为1;对于奇校验,则相反。
- 代码和校验:是发送方将所发数据块求和(或各字节异或),产生一个字节的校验字符(校验和)附加到数据块末尾。接收方接收数据时同时对数据块(除校验字节外)求和(或各字节异或),将所得的结果与发送方的“校验和”进行比较,相符则无差错,否则即认为传送过程中出现了差错。LRC属于其中一种。
- LRC:纵向冗余校验(Longitudinal Redundancy Check,LRC)是一个逐字节奇偶校验计算,将数据字的所有字节一起异或,创建一个字节的结果,也称为 XOR 校验和。
-
说说串口通信中的硬件流量控制⭐⭐⭐
这里的“流”,指的数据流,即流量控制。
数据在两个串口之间传输时,常常会出现丢失数据的现象,如:
- 两台计算机的处理速度不同(台式机与单片机之间的通讯)
- 接收端数据缓冲区已满,则此时继续发送来的数据就会丢失。
现在我们在网络上通过Modem(调制解调器)进行数据传输,这个问题就尤为突出。流控制能解决这个问题,当接收端数据处理不过来时,就发出“不再接收”的信号,发送端就停止发送,直到收到“可以继续发送”的信号再发送数据。
因此流控制可以控制数据传输的进程,防止数据的丢失。 PC机中常用的是硬件流控制(包括RTS/CTS、DTR/DSR等)
硬件流控制必须将相应的电线连上,数据终端设备(如计算机)使用RTS来请求发送数据,而数据通讯设备(如调制解调器)则用CTS来清除和暂停来自计算机的数据流。
这种硬件握手方式的过程为:在编程时,根据接收端缓冲区大小设置一个高位标志(可为缓冲区大小的75%)和一个低位标志(可为缓冲区大小的25%),当缓冲区内数据量达到高位时,我们在接收端将CTS线置低电平(送逻辑0),当发送端的程序检测到CTS为低后,就停止发送数据,直到接收端缓冲区的数据量低于低位而将CTS置高电平。RTS则用来标明接收设备有没有准备好接收数据。
引脚 意义 电平 CTS 停止发送数据 0 CTS 可以发送数据 1 RTS 停止接收数据 0 RTS 可以接收数据 1 -
说一说CAN总线的仲裁机制⭐⭐⭐⭐⭐
CAN总线为多主工作方式,网络上任意一节点均可在任意时刻主动向网络上的其它节点同时发送消息。若两个或两个以上的节点同时开始传送报文,就会产生总线访问冲突,根据逐位仲裁原则,借助帧开始部分的标识符,优先级低的节点主动停止发送数据,而优先级高的节点继续发送信息。
在仲裁期间,CAN总线作“与”运算,每一个节点都对节点发送的电平与总线电平进行比较,如果电平相同,则节点可以继续发送。如规定0 的优先级高,当某一个节点发送1而检测到0 时,此节点知道有更高优先级的信息在发送,它就停止发送消息,直到再一次检测到网络空闲。
-
说一说CAN总线的优点⭐⭐⭐⭐⭐
CAN(Controller Area Network)即控制器局域网,是一种能够实现分布式实时控制的串行通信网络。
- 可以多主方式工作,网络上任意一个节点均可以在任意时刻主动地向网络上的其他节点发送信息,而不分主从,通信方式灵活。
- 网络上的节点可分成不同的优先级,可以满足不同的实时要求。
- 采用非破坏性位仲裁总线结构机制,当两个节点同时向网络上传送信息时,优先级低的节点主动停止数据发送,而优先级高的节点可不受影响地继续传送数据。
- 可以点对点,一点对多点及全局广播几种传送方式接收数据。
- 直接通信距离最远可达10km
- 通信速率最高可达1MB/s
-
说一说UART和USART的区别⭐⭐⭐⭐⭐
UART与USART都是单片机上的串口通信,他们之间的区别如下:
UART:universal asynchronous receiver and transmitter通用异步收/发器
USART: universal synchronous asynchronous receiver and transmitter通用同步/异步收/发器
从名字上可以看出,USART在UART基础上增加了同步功能,即USART是UART的增强型。
其实当我们使用USART在异步通信的时候,它与UART没有什么区别,但是用在同步通信的时候,区别就很明显了:大家都知道同步通信需要时钟来触发数据传输,也就是说USART相对UART的区别之一就是能提供主动时钟。
-
说说你了解的总线⭐⭐⭐⭐⭐
总线 线 半双工/全双工 串行/并行 同步/异步 速率 通信距离 区分主从 I2C SCL(时钟线)和SDA(数据线) 半双工 串行 同步 标准(100kbit/s),快速(400kbit/s)和高速(3.4Mbit/s) 近 区分 SPI SDI(串行数据输入)、SDO(串行数据输出)、SCK(串行移位时钟)、CS(从使能) 全双工 串行 同步 一般传输速度能达到20Mbp/s 近 区分 UART 至少三根:RX(接收)、TX(发送)和GND 全双工 串行 异步 一般为9.6kbit/s 近 区分 CAN CAN_H和CAN_L 半双工 串行 异步 最高可达1MB/s 远 不区分 -
说一说RS232和RS485的区别⭐⭐⭐⭐
- RS232接口的信号电平值较高,易损坏接口电路的芯片,又因为与TTL 电平不兼容故需使用电平转换电路方能与TTL电路连接;RS-485接口信号电平比RS-232降低了,就不易损坏接口电路的芯片,且该电平与TTL电平兼容,可方便与TTL 电路连接。
- RS232传输速率较低,在异步传输时,波特率为20Kbps;RS-485的数据最高传输速率为10Mbps 。
- RS232接口使用两根信号线和一根地线返回而构成共地的传输形式,容易产生共模干扰,所以抗噪声干扰性弱;RS-485接口是采用平衡驱动器和差分接收器的组合,抗共模干能力增强,即抗噪声干扰性好。
- RS232传输距离有限,最大传输距离标准值为50英尺,实际上也只能用在50米左右;RS-485接口的最大传输距离标准值为4000英尺,实际上可达3000米
-
说一说串行和并行的区别⭐⭐⭐⭐⭐
- 数据传送方式不同:串行口传输方式为数据排成一行、一位一位送出数据;并行口传输8位数据一次送出。
- 针脚不同:串行口针脚少,消耗IO资源少;并行口针脚多,消耗IO资源多。
- 用途不同:串行口主要用在速度要求不高、有一定距离的传输场景,如UART,I2C通信;并行口多用于传输速率要求高、吞吐量大的场景,如FSMC(Flexible Static Memory Controller,可变静态存储控制器),DVP(Digital Video Port,数字视频接口)等接口。
-
说一说同步和异步的区别⭐⭐⭐⭐⭐
- 同步通信要求接收端时钟频率和发送端时钟频率一致,发送端发送连续的比特流,字节与字节之间没有间隙;异步通信时不要求接收端时钟和发送端时钟同步,发送端发送完一个字节后,可经过任意长的时间间隔再发送下一个字节。
- 同步通信效率高,异步通信效率较低。
- 同步通信较复杂,双方时钟的允许误差较小;异步通信简单,双方时钟可允许一定误差。
- 同步通信可用于多点对多点,异步通信只适用于点对点。
常见面试题
-
说说一个算法有哪些时间复杂度?归并算法时间复杂度是多少?⭐⭐⭐
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n)
归并算法时间复杂度是O(nlogn)
-
说说数组时间复杂度,什么场景下使用?⭐⭐⭐⭐⭐
从渐进趋势来看,数组插入和删除操作的时间复杂度是O(n)。而数组是有序的,可以直接通过下标访问元素,十分高效,访问时间复杂度是O(1)(常数时间复杂度)。
如果某些场景需要频繁插入和删除元素时,这时候不宜选用数组作为数据结构。
频繁访问的场景下,可以使用数组。
-
说说vector的实现原理⭐⭐⭐⭐⭐
vector是数组的进一步封装,它是一个类。可以比数组更加灵活的处理内存空间。
vector采用的数据结构是线性的连续空间,它以两个迭代器start和finish分别指向配置得来的连续空间中目前已将被使用的空间。迭代器end_of_storage指向整个连续的尾部。
vector是动态空间,随着元素的加入,它的内部机制会自动扩充空间以容纳新的元素。vector在增加元素时,如果超过自身最大的容量Capacity,vector则将自身的容量扩充为原来的两倍。扩充空间需要经过的步骤:重新配置空间,元素移动,释放旧的内存空间。一旦vector空间重新配置,则指向原来vector的所有迭代器都失效了,因为vector的地址改变了。
-
实现一个vector⭐⭐⭐⭐⭐
#ifndef _MY_VECTOR_HPP_ #define _MY_VECTOR_HPP_ template<typename T> class MyVector{ public: // 构造函数 MyVector(){ //这里默认数组大小为10 //但是vector文件中默认构造的方式是0,之后插入按照1 2 4 8 16 二倍扩容。注GCC是二倍扩容,VS13是1.5倍扩容 data = new T[10]; _capacity = 10; _size = 0; } ~MyVector(){ delete[] data; } //reserve只是保证vector的空间大小(_capacity)最少达到它的参数所指定的大小n。在区间[0, n)范围内,预留了内存但是并未初始化 void reserve(size_t n){ if(n>_capacity){ data = new T[n]; _capacity = n; } } //向数组中插入元素 void push_back(T e){ //如果当前容量已经不够了,重新分配内存,均摊复杂度O(1) if (_size == _capacity){ resize(2 * _capacity);//两倍扩容 } data[_size++] = e; } //删除数组尾部的数据,同时动态调整数组大小,节约内存空间 T pop_back(){ T temp = data[_size]; _size--; //如果容量有多余的,释放掉 if (_size == _capacity / 4){ resize(_capacity / 2); } return temp; } //获取当前数组中元素的个数 size_t size(){ return _size; } //判断数组是否为空 bool empty(){ return _size==0?1:0; } //重载[]操作 int &operator[](int i){ return data[i]; } //获取数组的容量大小 size_t capacity(){ return _capacity; } //清空数组,只会清空数组内的元素,不会改变数组的容量大小 void clear(){ _size=0; } private: T *data; //实际存储数据的数组 size_t _capacity; //容量 size_t _size; //实际元素个数 //扩容 void resize(int st){ //重新分配空间,在堆区新开辟内存,然后将以前数组的值赋给他,删除以前的数组 T *newData = new T[st]; for (int i = 0; i < _size; i++){ newData[i] = data[i]; } //实际使用时是清除数据,但不会释放内存 delete[] data; data = newData; _capacity = st; } }; #endif //_MY_VECTOR_HPP_
-
分析一下push_back() 的时间复杂度⭐⭐⭐⭐⭐
采用均摊分析的方法,公式如下:
公式里面,假定有 n 个元素,倍增因子为 m(我们设定为2),那么完成这 n 个元素往一个 vector 中的push_back操作,需要重新分配内存的次数大约为 log2(n),第 i 次重新分配将会导致复制 2^i (也就是当前的vector.size() 大小)个旧空间中元素,因此 n 次 push_back 操作所花费的总时间:
然后 n 次push_back操作,每次分摊O(1),即为常数时间复杂度了。
-
来手写一个链表⭐⭐⭐⭐⭐
struct ListNode { // 链表结构体 int data; ListNode *next; }; ListNode * initLink(){ // 创建一个链表(1,2,3,4) ListNode * p=(ListNode*)malloc(sizeof(ListNode));//创建一个头结点 ListNode * temp=p;//声明一个指针指向头结点,用于遍历链表 //生成链表 for (int i=1; i<5; i++) { ListNode *node=(ListNode*)malloc(sizeof(ListNode)); node->data=i; node->next=NULL; temp->next=node; temp=temp->next; } return p; } int selectElem(ListNode * p,int elem){ // 链表中查找某结点 ListNode * t=p; int i=1; while (t->next) { t=t->next; if (t->data==elem) { return i; } i++; } return -1; } ListNode * insertElem(ListNode * p,int elem,int add){ // 插入结点 ListNode * temp=p;//创建临时结点temp //首先找到要插入位置的上一个结点 for (int i=1; i<add; i++) { if (temp==NULL) { printf("插入位置无效\n"); return p; } temp=temp->next; } //创建插入结点node ListNode * node=(ListNode*)malloc(sizeof(ListNode)); node->data=elem; //向链表中插入结点 node->next=temp->next; temp->next=node; return p; } ListNode * delElem(ListNode * p,int add){ ListNode * temp=p; //temp指向被删除结点的上一个结点 for (int i=1; i<add-1; i++) { temp=temp->next; if (temp->next == NULL) // 判断add的有效性 return p; } ListNode *del=temp->next;//单独设置一个指针指向被删除结点,以防丢失 temp->next=temp->next->next;//删除某个结点的方法就是更改前一个结点的指针域 free(del);//手动释放该结点,防止内存泄漏 return p; }
-
总结一下数组与链表的区别⭐⭐⭐⭐⭐
- 数组内存连续、有序;链表内存可以不连续
- 数组可以直接访问下标,访问时间复杂度O(1);链表需要通过下一级指针层层递进访问,访问时间复杂度O(n)
- 数组插入或删除元素时需要移动后面的元素,时间复杂度O(n);而链表插入或删除元素时,时间复杂度O(1)
- 频繁访问元素的场景用数组;频繁插入或删除的场景用链表
-
栈和队列的区别⭐⭐⭐⭐⭐
主要区别就是规定不同。
栈规定:元素先入后出(First In Last Out, 简称FILO)。
队列规定:元素先入先出(First In First Out, 简称FIFO)。
-
用数组或链表来实现一个栈⭐⭐⭐⭐
实现的原理都是类似的,用游标变量来控制元素的位置。
栈只需要设置一个游标变量来控制栈顶元素的位置
大家一定要掌握,面试很容易手撕代码。
-
数组实现栈:
#include<iostream> using namespace std; typedef int dataType; class Stack{ private: dataType* arrays; // 成员数组 int top; // 栈顶 int size_a; // 数组大小 public: Stack(); ~Stack(); void push(dataType data); // 入栈 void pop(); // 出栈 dataType f_top(); // 访问栈顶元素 bool isEmpty(); // 判空 bool isFull(); // 判满 }; Stack::Stack():size_a(10), top(-1){ // 初始化 arrays = new dataType[size_a]; // 申请10个内存大小的空间 } Stack::~Stack(){ delete[] arrays; arrays = nullptr; } void Stack::push(dataType data){ if(isFull()) cout << "The stack is full!" << endl; else arrays[++top] = data; } void Stack::pop(){ if(isEmpty()) cout << "The stack is empty!" << endl; else top--; } dataType Stack::f_top(){ // 返回栈顶元素 if(isEmpty()){ cout << "The stack is empty!" << endl; return -1; } else return arrays[top]; } bool Stack::isEmpty(){ return (top == -1); } bool Stack::isFull(){ return (top >= size_a-1); // 当top为9的时候栈已满 } int main (){ // 测试用例 Stack s; for (int i=0; i<11; i++){ s.push(i); cout << s.f_top() << endl; } for (int i=0; i<11; i++){ s.pop(); cout << s.f_top() << endl; } return 0; }
-
链表实现栈:
#include<iostream> #include<stdlib.h> using namespace std; typedef int dataType; struct node{ //链栈节点 dataType data; //数值 node *next; //指针域 }; class Lstack{ public: Lstack(); ~Lstack(); void push(dataType var); //压栈 void pop(); //出栈 dataType f_top(); //取栈顶元素 bool isEmpty(); //判断是否为空 private: node *top; //栈顶指针,top等于NULL表示空栈 }; Lstack::Lstack(){ top = NULL; //top等于NULL表示栈为空 } Lstack::~Lstack(){ node *ptr = NULL; while(top != NULL){ ptr = top->next; delete top; top = ptr; } } void Lstack::push(dataType a){ node *ptr = new node; ptr->data = a; //新栈顶存值 ptr->next = top; //新栈顶指向旧栈顶 top = ptr; //top指向新栈顶 } void Lstack::pop(){ if (!isEmpty()){ //判空 node *ptr = top->next; //预存下一节点的指针 delete top; //释放栈顶空间 top = ptr; //栈顶变化 } else cout << "The stack is empty!" << endl; } dataType Lstack::f_top(){ //判空 if(isEmpty()){ cout << "The stack is empty!" << endl; return -1; } else return top->data; //返回栈顶元素 } bool Lstack:: isEmpty(){ return top == NULL; //栈顶为NULL表示栈空 } int main (){ // 测试代码 Lstack s; for (int i=0; i<11; i++){ s.push(i); cout << s.f_top() << endl; } for (int i=0; i<11; i++){ s.pop(); cout << s.f_top() << endl; } //system("Pause"); return 0; }
-
-
用数组或链表来实现一个队列⭐⭐⭐⭐
队列需要设置两个游标元素一前一后分别控制队头和队尾的位置。
-
数组实现队列:
#include<iostream> using namespace std; typedef int dataType; class Queue{ private: dataType* arrays; // 成员数组 int front; // 队前 int end; // 队后 int size_a; // 数组大小 public: Queue(); ~Queue(); void push(dataType data); dataType pop(); bool isEmpty(); bool isFull(); }; Queue::Queue():size_a(10),front(0),end(0){ // 初始化 arrays = new dataType[size_a]; // 申请10个内存大小的空间 } Queue::~Queue(){ delete[] arrays; arrays = nullptr; } void Queue::push(dataType data){ if(isFull()) cout << "The queue is full!" << endl; else{ arrays[end] = data; end = (end+1) % size_a; // 循环队列,如果队前有空间,元素放队前 } } dataType Queue::pop(){ if(isEmpty()){ cout << "The queue is empty!" << endl; return -1; } else{ dataType t = arrays[front]; front = (front+1) % size_a; return t; } } bool Queue::isEmpty(){ return (front == end); } bool Queue::isFull(){ return (front == ((end+1) % size_a)); } int main (){ // 测试用例 Queue q; for (int i=0; i<11; i++){ q.push(i); } for (int i=0; i<11; i++){ cout << q.pop() << endl; } return 0; }
-
链表实现队列:
#include<iostream> using namespace std; typedef int dataType; struct node{ //链栈节点 dataType data; //数值 node *next; //指针域 }; class Queue{ public: Queue(); ~Queue(); void push(dataType var); dataType pop(); int size(); bool isEmpty(); private: node* front; // 队前 node* end; // 队后 int size_a; // 内存大小 }; Queue::Queue():size_a(0),end(NULL){ // 初始化 front = new node; front->next = NULL; } Queue::~Queue(){ node *ptr = NULL; while(front != NULL){ ptr = front->next; delete front; front = ptr; } } void Queue::push(dataType a){ // end创建节点连接至front后面 if (end == NULL){ end = new node; end->data = a; end->next = NULL; front->next = end; } else{ node* pTail = end; // 在end与front之间插入节点 end = new node; end->data = a; pTail->next = end; end->next = NULL; } size_a++; } dataType Queue::pop(){ if (isEmpty()){ cout << "The queue is empty!" << endl; return -1; } dataType data = front->next->data; // 删除front后一个节点 node* pCurr = front->next; front->next = pCurr->next; delete pCurr; if (isEmpty()){ end = NULL; } size_a--; return data; } int Queue::size(){ return size_a; //返回队列大小 } bool Queue:: isEmpty(){ return (size() == 0); } int main (){ // 测试用例 Queue q; for (int i=0; i<11; i++){ q.push(i); } for (int i=0; i<12; i++){ cout << q.pop() << endl; } return 0; }
-
-
说说二叉堆⭐⭐⭐⭐⭐
堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
- 最大堆。最大堆的任何一个父节点的值, 都大于或等于它左、 右孩子节点的值。
- 最小堆。最小堆的任何一个父节点的值, 都小于或等于它左、 右孩子节点的值
-
说说哈希表⭐⭐⭐⭐⭐
哈希表是一个非常有用的数据结构。可以实现常数时间复杂度的查找。
哈希表的做法是,将键值对放入数组中。因为数组可以通过下标实现常数时间复杂度的查找。这样相当于就找了键,也就找到了对应的值。
键值对通过取模或位运算操作来获取哈希值,这个哈希值就对应数组的下标。数组一开始会申请一定数量的内存空间,当键值对多起来后,就需要两倍扩容。在获取哈希值的过程中,可能出现哈希冲突,解决办法有开发地址法、二次探测法、链地址法。
常见面试题
-
说说满二叉树和完全二叉树?⭐⭐⭐⭐⭐
二叉树还有两种特殊形式, 一个叫作满二叉树, 另一个叫作完全二叉树。
一个二叉树的所有非叶子节点都存在左右孩子, 并且所有叶子节点都在同一层级上, 那么这个树就是满二叉树。
完全二叉树的条件没有满二叉树那么苛刻: 满二叉树要求所有分支都是满的; 而完全二叉树只需保证最后一个节点之前的节点都齐全即可。
-
说说树的遍历有几种方式?⭐⭐⭐⭐⭐
树的遍历有四种,前序遍历、中序遍历、后序遍历、层序遍历。
区别在于访问节点的顺序不同:前序遍历(根-->左-->右)、中序遍历(左-->根-->右)、后序遍历(左-->右-->根)、层序遍历(按层从左至右访问)
-
用栈实现一下树的前序遍历,说说思路⭐⭐⭐⭐⭐
前序遍历,口诀很好记:根-->左-->右。所以根节点先入栈并输出。然后访问左子树,打印完左节点出栈。
根-->左访问完了,开始访问右,因为我们的栈顶保存了根节点,通过获取栈顶元素,也就可以访问根节点的右边了,所以此时获取根节点后,让根节点出栈。
重复以上的步骤。直到栈为空,整棵树也就完成输出了。
class Solution { public: vector<int> preorderTraversal(TreeNode* root) { // 前序遍历,口诀很好记:根-->左-->右。 vector<int> res; if (root == nullptr) { return res; } stack<TreeNode*> stk; TreeNode* node = root; while (!stk.empty() || node != nullptr) { while (node != nullptr) { res.emplace_back(node->val); // 前序遍历,先访问根 stk.emplace(node); node = node->left; // 接下来进入左子树 } if (!stk.empty()){ node = stk.top(); stk.pop(); node = node->right; // 接下来进入右子树 } } return res; } }; // 时间复杂度:O(n) // 空间复杂度:O(n)
-
用队列实现树的层序遍历,说说思路?⭐⭐⭐⭐⭐
层序遍历,就是一层一层按顺序访问节点。所以根节点先入队列并输出,输出完就要让根节点出队列。然后就判断根节点是否有左右子节点,如果有,就让左右子节点入队列。
接下来就是依次取队列前面的元素了,取出子节点,让子节点出队并输出子节点,然后判断子节点是否有左右子节点,如果有,就让左右子节点入队列。
又取出剩余子节点,重复以上步骤,直到队列没有元素可取了,此时整棵树也输出完毕了。
class Solution { public: vector<vector<int>> levelOrder(TreeNode* root) { if (root == NULL) return {}; queue<TreeNode*> que; // 创建队列 que.push(root); // 根节点先入队 vector<vector<int>> res; while (!que.empty()){ vector<int> vec; int len = que.size(); for (int i=0; i<len; i++){ TreeNode* node = que.front(); vec.push_back(node->val); // 访问根节点 que.pop(); // 根节点出队列 if (node->left != NULL) que.push(node->left); // 左节点入队 if (node->right != NULL) que.push(node->right); // 右节点入队 } res.push_back(vec); } return res; } }; // 时间复杂度:O(n) // 空间复杂度:O(n)
-
说说二叉堆⭐⭐⭐⭐⭐
二叉堆本质上是一种完全二叉树, 它分为两个类型。
- 最大堆。最大堆的任何一个父节点的值, 都大于或等于它左、 右孩子节点的值。
- 最小堆。最小堆的任何一个父节点的值, 都小于或等于它左、 右孩子节点的值
二叉堆可以用来实现堆排序。堆排序是一个效率要高得多的选择排序,首先把整个数组变成一个最大堆,然后每次从堆顶取出最大的元素,这样依次取出的最大元素就形成了一个排序的数组。堆排序的核心分成两个部分,第一个是新建一个堆,第二个是弹出堆顶元素后重建堆。
建堆过程的时间复杂度是O(n),堆排序的时间复杂度是O(nlogn)
-
说说堆排序的时间复杂度,建堆的时间复杂度⭐⭐⭐⭐⭐
建堆过程的时间复杂度是O(n),堆排序的时间复杂度是O(nlogn)
-
说说哈希表的实现原理⭐⭐⭐⭐⭐
- 哈希表是一个非常有用的数据结构。可以实现常数时间复杂度的查找。
- 哈希表通过键值对来实现,将键值对放入数组中,键和值一一对应,找到键也就找到了值。
- 哈希表哈希函数来获取键值对的哈希值,对应数组的下标。
- 哈希冲突,有几种方法解决:
-
开放地址法(线性探测法):如果得到的哈希地址冲突(该位置上已存储数据)的话 ,我们就是将这个数据插到下一个位置,要是下个位置也已存储数据 ,就继续到下一个,直到找到正确的可以插入的数据 。
-
二次探测法:当遇到冲突后 ,下次所找到的位置为当前的位置加上n的二次方
-
链地址法:如果得到的哈希地址冲突, 只需要插入到对应的链表中即可。
-
-
哈希表如何解决哈希冲突⭐⭐⭐⭐⭐
-
开放地址法(线性探测法):如果得到的哈希地址冲突(该位置上已存储数据)的话 ,我们就是将这个数据插到下一个位置,要是下个位置也已存储数据 ,就继续到下一个,直到找到正确的可以插入的数据 。
-
二次探测法:当遇到冲突后 ,下次所找到的位置为当前的位置加上n的二次方
-
链地址法:如果得到的哈希地址冲突, 只需要插入到对应的链表中即可。
-
-
哈希表的初始数组容量一般为多少,为什么?⭐⭐⭐⭐⭐
C++中哈希表的初始数组容量一般为质数,通常为2、3、5、7等小质数,以此为基数进行扩容。具体的初始容量可以根据实际应用需求和数据规模进行选择,通常建议选择一个适当的质数作为初始容量,然后根据负载因子的大小进行扩容。
假如我们写个代码来测试一下:
#include <iostream> #include <unordered_map> int main() { std::unordered_map<int, int> map; std::cout << "Size of unordered_map with default initial bucket count: " << map.bucket_count() << std::endl; std::unordered_map<int, int> map2(100); std::cout << "Size of unordered_map with initial bucket count of 100: " << map2.bucket_count() << std::endl; return 0; }
结果是:
Size of unordered_map with default initial bucket count: 1 Size of unordered_map with initial bucket count of 100: 103
从输出中我们可以看出,如果没有指定初始数组容量,哈希表的默认初始容量为 1。在第二个哈希表中,我们指定了初始容量为 100,但实际上容量是 103,这是因为哈希表实际上使用的是比指定值更大的质数来作为容量。
1还是太小了,所以我们在初始化哈希表时最好给一个合适的质数,建议给11。这是因为如果太小,扩容频繁;数值太大,浪费空间。
-
哈希表的负载因子为什么是0.75?⭐⭐⭐⭐⭐
哈希表的负载因子是指哈希表中已存储元素数量与哈希表数组容量的比值。通常情况下,哈希表的负载因子取值范围为 0.5 ~ 0.8,其中 0.75 是比较常见的一个值。
负载因子的取值与哈希表的性能有关。当负载因子太小时,哈希表的利用率不高,浪费空间;当负载因子太大时,哈希冲突的概率会增加,导致查询的效率下降。经过实践,当负载因子为 0.75 时,哈希表的空间利用率和查询效率都较为理想,因此被广泛采用。
当哈希表中已有元素数量达到哈希表容量的 75% 时,通常会触发哈希表扩容操作,将哈希表的容量扩大为原来的两倍,以保证哈希表的查询效率和空间利用率。
-
说说红黑树⭐⭐⭐⭐⭐
红黑树是一种近似平衡的二叉查找树,它能够确保任何一个节点的左右子树的高度差不会超过二者中较低那个的一倍。具体来说,红黑树是满足如下条件的二叉查找树(binary search tree):
- 每个节点要么是红色,要么是黑色。
- 根节点必须是黑色
- 红色节点不能连续(也即是,红色节点的孩子和父亲都不能是红色)。
- 对于每个节点,从该点至
null
(树尾端)的任何路径,都含有相同个数的黑色节点。 - 最长的路径长度不会超过任意路径的两倍。
在树的结构发生改变时(插入或者删除操作),往往会破坏上述条件3或条件4,需要通过调整(左旋或右旋)使得查找树重新满足红黑树的条件。
-
什么是平衡树?⭐⭐⭐
平衡树(Balanced Tree)是一种数据结构,也叫自平衡二叉搜索树(Self-Balancing Binary Search Tree)。它在二叉搜索树的基础上增加了自平衡的操作,保证在插入或删除节点时任意节点的子树的高度差都小于等于1。
-
说说map和set的区别⭐⭐⭐⭐
在C++中,容器map和set都是用红黑树实现的,但是两者也有区别,具体如下:
(1)map:经过排序了的二元组的集合,map中的每个元素都是由两个值组成,其中的key(键值,一个map中的键值必须是唯一的) 是在排序或搜索时使用,它的值可以在容器中重新获取;而另一个值是该元素关联的数值。
(2)set:包含了经过排序了的数据,这些数据的值(value)必须是唯一的。和 map容器不同,使用 set 容器存储的各个键值对,要求键 key 和值 value 必须相等。所以我们可以认为set就是元素的集合,如 {'a','b','c'} 。
(3)map和set的底层实现机制:红黑树(RB-Tree)。
常见面试题
-
说说什么是稳定的排序?⭐⭐⭐⭐⭐
- 稳定:如果a原本在b前面,而a=b,排序之后a仍然在b的前面。
- 不稳定:如果a原本在b的前面,而a=b,排序之后 a 可能会出现在 b 的后面。
冒泡排序、归并排序是稳定排序;快速排序是不稳定排序。
-
说说动态规划算法⭐⭐⭐⭐⭐
暴力解法有很多重复计算,动态规划就是为了帮助我们减少重复计算,所以动态规划提前存储前面计算的值,后面的值来源于已经存储的值,或者只需要在已经存储的值的基础上简单的叠加,这就是动态规划的本质,空间换时间。 三个非常重要的领悟: 1、动态规划应用于存在重复子结构的问题中,避免重复计算法 2、动态规划空间换时间 3、动态规划提前存储前面计算的值,后面的值来源于已经存储的值,或者只需要在已经存储的值的基础上简单的叠加
-
手撕归并排序,说说原理⭐⭐⭐⭐⭐
归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。
class Solution { void mergeSort(vector<int>& nums, vector<int>& temp, int l, int r){ if (l >= r) return; int mid = (l + r) / 2; mergeSort(nums, temp, l, mid); mergeSort(nums, temp, mid+1, r); int i=l,j=mid+1; int t = 0; while (i<=mid && j<=r){ if (nums[i] <= nums[j]) temp[t++] = nums[i++]; else temp[t++] = nums[j++]; } while (i <= mid) temp[t++] = nums[i++]; while (j <= r) temp[t++] = nums[j++]; for (int i=l,t=0; i<=r; i++) nums[i] = temp[t++]; } public: vector<int> sortArray(vector<int>& nums) { if (nums.empty()) return {}; vector<int> temp(nums.size(), 0); mergeSort(nums, temp, 0, nums.size()-1); return nums; } }; // 时间复杂度:O(nlogn) // 空间复杂度:O(n)
-
手撕快速排序,说说原理⭐⭐⭐⭐⭐
快速排序的基本思想:也是采用分治的思想,将数组拆分成一个个的子序列,针对每个子序列进行排序,排序的方法是,设立一个“基准元素”,比“基准元素”小的数字放左边,比“基准元素”大的数字放右边。
//快排递归实现 class Solution { void quickSort(vector<int>& nums, int l, int r){ if (l >= r) return; int mark = nums[l]; int mark_ptr = l; for (int i=l; i<=r; i++){ if (nums[i] < mark){ mark_ptr++; swap(nums[i], nums[mark_ptr]); } } swap(nums[mark_ptr], nums[l]); quickSort(nums, l, mark_ptr-1); quickSort(nums, mark_ptr+1, r); } public: vector<int> sortArray(vector<int>& nums) { if (nums.empty()) return {}; quickSort(nums, 0, nums.size()-1); return nums; } }; // 时间复杂度:O(nlogn) // 空间复杂度:O(logn) //快排非递归实现,利用栈,BFS的思想,存储前后指针 class Solution { void quickSort(vector<int>& nums, int l, int r){ using Pair = pair<int,int>; stack<Pair> stk; stk.push(Pair(l, r)); while (!stk.empty()){ auto index = stk.top(); stk.pop(); int l = index.first; int r = index.second; if (l >= r) continue; int mark = nums[l]; int mark_ptr = l; for (int i=l; i<=r; i++){ if (nums[i] < mark){ mark_ptr++; swap(nums[i], nums[mark_ptr]); } } swap(nums[mark_ptr], nums[l]); stk.push(Pair(l, mark_ptr-1)); stk.push(Pair(mark_ptr+1, r)); } } public: vector<int> sortArray(vector<int>& nums) { if (nums.empty()) return {}; quickSort(nums, 0, nums.size()-1); return nums; } }; // 时间复杂度:O(nlogn) // 空间复杂度:O(n)
-
手撕插入排序⭐⭐⭐⭐⭐
class Solution { void straightSort(vector<int> &arr){ int N = arr.size(); for (int i=1; i<N; i++){ for (int j=i-1; j>=0&&arr[j+1]<arr[j]; j-=1){ swap(arr[j+1], arr[j]); } } } public: vector<int> sortArray(vector<int>& nums) { straightSort(nums); return nums; } }; // 时间复杂度:O(n^2) // 空间复杂度:O(1)
-
手撕堆排序,说说原理⭐⭐⭐⭐⭐
堆排序(Heapsort)是指利用二叉堆这种数据结构所设计的一种排序算法。二叉堆是一个近似完全二叉树的结构,并同时满足堆的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
class Solution { void heap(vector<int>& nums, int p){ // 调整堆 for (int parent=p; parent*2+1<nums.size();){ int child = parent*2+1; if (child+1<nums.size() && nums[child] > nums[child+1]) child++; if (nums[child] > nums[parent]) break; swap(nums[child], nums[parent]); parent = child; } } void buildheap(vector<int>& nums){ // 建堆 for (int i=nums.size()/2; i>=0; i--) heap(nums, i); } public: vector<int> sortArray(vector<int>& nums) { if (nums.empty()) return {}; buildheap(nums); // 建堆 vector<int> temp(nums.size(), 0); for (int i=0; i<temp.size(); i++){ temp[i] = nums[0]; nums[0] = nums.back(); nums.pop_back(); // 破坏堆 heap(nums, 0); // 调整堆 } return temp; } }; // 时间复杂度:O(nlogn) // 空间复杂度:O(n),我们这里复制了原数组,如果在原数组上进行操作,空间复杂度为O(1)
-
手撕二分查找⭐⭐⭐⭐⭐
class Solution { public: int search(vector<int>& nums, int target) { int pivot, left = 0, right = nums.size() - 1; while (left <= right) { pivot = left + (right - left) / 2; if (nums[pivot] == target) return pivot; if (target < nums[pivot]) right = pivot - 1; else left = pivot + 1; } return -1; } }; // 时间复杂度:O(logn) // 空间复杂度:O(n)
-
快排最差时间复杂度,堆排最差时间复杂度?⭐⭐⭐⭐⭐
快排平均时间复杂度为O(nlogn)。而被排序的数组刚好是倒序的情况下,最差时间复杂度为O(n^2)
建堆的时间复杂度为O(n)。堆排平均时间复杂度为O(nlogn)。最差时间复杂度为O(nlogn),也即每个结点都进行了一次比较。
-
说说各排序算法的时间复杂度?⭐⭐⭐⭐⭐
-
说说DFS和BFS的区别?⭐⭐⭐⭐⭐
DFS(Deep First Search)深度优先搜索。
BFS(Breath First Search)广度优先搜索。
-
深度优先搜索的步骤分为
(1)递归下去
(2)回溯上来。
顾名思义,深度优先,则是以深度为准则,先一条路走到底,直到达到目标。这里称之为递归下去。
否则既没有达到目标又无路可走了,那么则退回到上一步的状态,走其他路。这便是回溯上来。
-
广度优先搜索较之深度优先搜索之不同在于,深度优先搜索旨在不管有多少条岔路,先一条路走到底,不成功就返回上一个路口然后就选择下一条岔路,而广度优先搜索旨在面临一个路口时,把所有的岔路口都记下来,然后选择其中一个进入,然后将它的分路情况记录下来,然后再返回来进入另外一个岔路,并重复这样的操作
-
区别
-
数据结构上的运用
DFS用递归的形式,用到了栈结构,先进后出。
BFS选取状态用队列的形式,先进先出。
-
复杂度
DFS的复杂度与BFS的复杂度大体一致,不同之处在于遍历的方式与对于问题的解决出发点不同,DFS适合目标明确,而BFS适合大范围的寻找。
-
思想
思想上来说这两种方法都是穷竭列举所有的情况。
-
-
数据库面试题
关于数据库,通过浏览牛客网的面经可以发现,很多C++软件开发的面经里没有涉及数据库的考题,所以豆芽在这章的考量是,就不具体展开讲解数据库了。但是又得防身,所以豆芽对于这章,直接给出面试题。
-
数据库事务以及四个特性⭐⭐⭐⭐⭐
数据库事务( transaction)是访问并可能操作各种数据项的一个数据库操作序列,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。事务由事务开始与事务结束之间执行的全部数据库操作组成。数据库事务是数据库最小逻辑单元。
四个特性:
-
原子性(Atomicity):原子性是指事务包含的所有操作要么全部成功,要么全部失败回滚。失败回滚的操作事务,将不能对事务有任何影响。
-
一致性(Consistency):一致性是指事务必须使数据库从一个一致性状态变换到另一个一致性状态,也就是说一个事务执行之前和执行之后都必须处于一致性状态。例如:A和B进行转账操作,A有200块钱,B有300块钱;当A转了100块钱给B之后,他们2个人的总额还是500块钱,不会改变。
-
隔离性(Isolation):隔离性是指当多个用户并发访问数据库时,比如同时访问一张表,数据库每一个用户开启的事务,不能被其他事务所做的操作干扰(也就是事务之间的隔离),多个并发事务之间,应当相互隔离。
例如同时有T1和T2两个并发事务,从T1角度来看,T2要不在T1执行之前就已经结束,要么在T1执行完成后才开始。将多个事务隔离开,每个事务都不能访问到其他事务操作过程中的状态;就好比上锁操作,只有一个事务做完了,另外一个事务才能执行。
-
持久性(Durability):持久性是指事务的操作,一旦提交,对于数据库中数据的改变是永久性的,即使数据库发生故障也不能丢失已提交事务所完成的改变。
-
-
数据库三大范式⭐⭐⭐⭐⭐
- 第一范式(1NF):数据表中的每一列(每个字段)必须是不可拆分的最小单元,也就是确保每一列的原子性;(比如“姓名与年龄“,我们应该拆分成两个字段:“姓名“、“年龄“。)
- 第二范式(2NF):满足1NF后,要求表中的所有列,都必须依赖于同一个主键,而不能有任何一列与主键没有关系,也就是说一个表只描述一件事情;(比如我们主字段是教师的“姓名“,那么字段“年龄“、“工号“、“工资“、“电话“都是与教师相关的,而字段“销售额“跟我们教师没有关系,就要去掉。)
- 第三范式(3NF):必须先满足第二范式(2NF),要求:表中的每一列只与主键直接相关而不是间接相关;(如果某一属性依赖于其他非主键属性,而其他非主键属性又依赖于主键,那么这个属性就是间接依赖于主键)
-
事务的隔离级别⭐⭐⭐⭐⭐
事务的隔离性就是指,多个并发的事务同时访问一个数据库时,一个事务不应该被另一个事务所干扰,每个并发的事务间要相互进行隔离。
-
读未提交(Read Uncommitted):因为没有行级共享锁,会出现脏读。
脏读就是,比如两个事务都在操作同一个表,A事务修改了C字段的值没有提交该事务;而B事务也在读取C字段的值,就在这时A事务发生了失败回滚,那么C字段的值返回原来的值,所以B事务发生了脏读。整个过程结束,数据库没有发生任何改变,B事务却读到了奇怪的值,这就是脏读。解决办法是行级共享锁。
-
读提交(Read Committed):通过行级共享锁,解决了脏读问题,但因为事务没有加锁,导致前后读取数据不一致,即不可重复读。
不可重复读就是,比如两个事务都在操作同一个表,A事务先读取了C字段的值后继续操作下一个字段;而B事务在这个时候修改了C字段的值。A事务再读取C字段的值,发现同一个事务下,竟然前后两次读取不一样,这就很怪异了,这就是不可重复读。解决办法是对事务进行加锁。
-
可重复读(Repeated Read):通过事务加锁,解决了不可重复读问题,但因为表没有加锁,会出现幻读的情况,比如多了一行数据。
幻读就是,比如两个事务都在操作同一个表,A事务先读取了表的行数;而B事务在这个时候为表格新插入了一行数据。A事务再读取表的行数,发现竟多了一行,这就很怪异了,这就是幻读。解决办法是对表进行加锁。
-
串行化(Serializable):可解决脏读、不可重复读、幻读问题,通过对表直接加锁的方式,但数据库的读取效率降低。
mysql默认隔离级别为可重复读。
-
-
什么是数据库索引⭐⭐⭐⭐⭐
索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。与在表中搜索所有的行相比,索引有助于更快地获取信息。唯一、不为空、经常被查询的字段适合建立索引。
-
索引类型与索引模型⭐⭐⭐⭐⭐
- 普通索引:哈希表、红黑树;
- 唯一索引:比如设定学号为索引;
- 主键索引:设定主键作为索引;
- 联合索引:多个字段共同索引;
- 全文索引:查找关键字。
索引模型:哈希表、红黑树、B树、B+树
-
什么情况下数据库索引会失效⭐⭐⭐⭐⭐
-
如果条件中有or,即使其中有条件带索引也不会使用(要想使用or,又想让索引生效,只能将or条件中的每个列都加上索引)
-
不符合最左匹配原则
-
like查询是以%开头
-
如果mysql估计使用全表扫描要比使用索引快,则不使用索引
-
-
引起慢查询的常见原因及一些解决方案⭐⭐⭐⭐⭐
(1)没有索引或者没有用到索引(这是查询慢最常见的问题,是程序设计的缺陷)
(2)锁或者死锁(这也是查询慢最常见的问题,是程序设计的缺陷)
(3)查询语句不好,没有优化
(4)网络速度慢
(5)内存不足
解决方案:
(1)根据查询条件,建立索引,优化 索引、优化访问方式
(2)优化锁。可以使用乐观锁、读写锁
(3)优化查询语句
(4)提升网络速度
(5)扩大服务器内存
-
聚簇索引与非聚簇索引⭐⭐⭐⭐⭐
聚簇索引:将数据存储与索引放到了一块,找到索引也就找到了数据
非聚簇索引:将数据存储于索引分开结构,索引结构的叶子节点指向了数据的对应行
-
数据库主键和外键⭐⭐⭐⭐⭐
-
主键是能确定一条记录的唯一标识,比如,一条记录包括身份正号,姓名,年龄。
身份证号是唯一能确定你这个人的,其他都可能有重复,所以,身份证号是主键。
-
外键用于与另一张表的关联。是能确定另一张表记录的字段,用于保持数据的一致性。
比如,A表中的一个字段,是B表的主键,那他就可以是A表的外键。
-
-
mysql知道哪些存储引擎,它们的区别⭐⭐⭐⭐⭐
MyISAM、InnoDB。从MySQL5.5版本之后,MySQL的默认内置存储引擎是InnoDB
-
InnoDB 支持事务、支持所有隔离级别、行级锁、外键关联、灾难恢复性好、索引和数据是在一起的、用的B+树。不保存行列的信息,使用select需要扫描全表,查找慢些。
-
MyISAM 不支持事务、不支持行级锁、外键关联,灾难恢复性差、在读写的时候需要锁住整个表,效率低些。但是查找的时候保持行列信息,通过select可以查找相对比较快,索引和数据是分开的、用的B+树。
-
-
关系型数据库和非关系型数据库的区别⭐⭐⭐⭐⭐
-
关系型数据库:指采用了关系模型来组织数据的数据库。 关系模型指的就是二维表格模型,而一个关系型数据库就是由二维表及其之间的联系所组成的一个数据组织。比如mysql就是关系型数据库。
-
非关系型数据库:指非关系型的,分布式的,且一般不保证遵循
ACID
原则的数据存储系统。非关系型数据库结构
非关系型数据库以键值对存储,且结构不固定,每一个元组可以有不一样的字段,每个元组可以根据需要增加一些自己的键值对,不局限于固定的结构,可以减少一些时间和空间的开销。
如典型的Redis就是非关系型数据库。
-
-
数据库垂直与水平拆分怎么做⭐⭐⭐⭐⭐
垂直拆分:一个数据库由很多表的构成,每个表对应着不同的业务,垂直切分是指按照业务将表进行分类,分布到不同的数据库上面 水平拆分:同一个表拆到不同的数据库中,可以理解为将表中的某些行切分到一个数据库,而另外的某些行又切分到其他的数据库中
-
什么是内联接、左联接、右联接⭐⭐⭐⭐⭐
内联接(Inner Join):匹配2张表中字段相等的记录。 左联接(Left Outer Join):返回左表,并且返回左表与右表字段相等的记录 右联接(Right Outer Join):返回右表,并且返回右表与左表字段相等的记录
-
乐观锁与悲观锁⭐⭐⭐⭐⭐
悲观锁:总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。 乐观锁:顾名思义,就是很乐观,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。 要明确一下:无论是悲观锁还是乐观锁,都是人们定义出来的概念,可以认为是一种思想。其实不仅仅是关系型数据库系统中有乐观锁和悲观锁的概念,像memcache、hibernate、tair等都有类似的概念。所以,不应该拿乐观锁、悲观锁和其他的数据库锁等进行对比。 实现方式: 悲观锁的实现可以依靠数据库里的锁机制,如排它锁。 乐观锁的实现Compare and Swap(CAS):当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。 乐观锁每次在执行数据的修改操作时,都会带上一个版本号,一旦版本号和数据的版本号一致就可以执行修改操作并对版本号执行+1操作,否则就执行失败。因为每次操作的版本号都会随之增加,所以不会出现ABA问题,因为版本号只会增加不会减少。除了version以外,还可以使用时间戳,因为时间戳天然具有顺序递增性。 ABA:如果另一个线程修改V值假设原来是A,先修改成B,再修改回成A。当前线程的CAS操作无法分辨当前V值是否发生过变化。关于ABA问题举一个例子:在你非常渴的情况下你发现一个盛满水的杯子,你一饮而尽。之后再给杯子里重新倒满水。然后你离开,当杯子的真正主人回来时看到杯子还是盛满水,他当然不知道是否被人喝完重新倒满。解决这个问题的方案的一个策略是每一次倒水假设有一个自动记录仪记录下,这样主人回来就可以分辨在她离开后是否发生过重新倒满的情况。这也是解决ABA问题目前采用的策略。用悲观锁就可以解决这个问题。
-
MVCC原理⭐⭐⭐⭐⭐
MVCC的英文全称是 Multiversion Concurrency Control ,中文意思是多版本并发控制技术。原理是,通过数据行的多个版本管理来实现数据库的并发控制,简单来说就是保存数据的历史版本。可以通过比较版本号决定数据是否显示出来。读取数据的时候不需要加锁可以保证事务的隔离效果。 MVCC 可以解决什么问题?
- 读写之间阻塞的问题,通过 MVCC 可以让读写互相不阻塞,读不相互阻塞,写不阻塞读,这样可以提升数据并发处理能力。
- 降低了死锁的概率,这个是因为 MVCC 采用了乐观锁的方式,读取数据时,不需要加锁,写操作,只需要锁定必要的行。
- 解决了一致性读的问题,当我们朝向某个数据库在时间点的快照是,只能看到这个时间点之前事务提交更新的结果,不能看到时间点之后事务提交的更新结果。
-
B树与B+树的区别⭐⭐⭐⭐⭐
-
B树每个节点都存储数据,所有节点组成这棵树。B+树只有叶子节点存储数据(B+数中有两个头指针:一个指向根节点,另一个指向关键字最小的叶节点),叶子节点包含了这棵树的所有数据,所有的叶子结点使用链表相连,便于区间查找和遍历,所有非叶节点起到索引作用。
-
B树中叶节点包含的关键字和其他节点包含的关键字是不重复的,B+树的索引项只包含对应子树的最大关键字和指向该子树的指针,不含有该关键字对应记录的存储地址。
-
B树查找,中途内部节点处可能停止返回,所以B树查找时间复杂度不稳定;B+树中查找,无论查找是否成功,每次都是一条从根节点到叶节点的路径,查找时间复杂度稳定。
-
B+树的叶子节点通过链表相连,所以B+树范围查找比B树效率高。 B树和B+树的共同优点 考虑磁盘IO的影响,它相对于内存来说是很慢的。数据库索引是存储在磁盘上的,当数据量大时,就不能把整个索引全部加载到内存了,只能逐一加载每一个磁盘页(对应索引树的节点)。所以我们要减少IO次数,对于树来说,IO次数就是树的高度,而“矮胖”就是b树和B+树的特征之一,m的大小取决于磁盘页的大小。
-
-
mysql的四种日志⭐⭐⭐
- 错误日志:记录mysql运行过程ERROR,WARING等信息,系统出错或某条记录出问题可查看ERROR日志。
- 日常运行日志:记录mysql中每条请求数据。
- 二进制日志:binlog,包含一些事件,数据库的改动等。
- 慢查询日志:用于mysql的性能调优。
-
mysql主从复制⭐⭐⭐⭐⭐
主从复制(也称 AB 复制)允许将来自一个MySQL数据库服务器(主服务器)的数据复制到一个或多个MySQL数据库服务器(从服务器)。实现数据库的读写分离,主数据库主要进行写操作,而从数据库负责读操作。同时数据库有多个副本,也可保证数据库安全,主服务器如果出问题,可以将从服务器升级为主服务器。
三种复制方式
-- 基于SQL语句的复制(statement-based replication, SBR),
-- 基于行的复制(row-based replication, RBR),
-- 混合模式复制(mixed-based replication, MBR)。
-
怎么优化查询⭐⭐⭐⭐⭐
索引优化(选择合适索引、索引避免失效),分库分表,读写分离
-
mysql如何建立和删除索引⭐⭐⭐⭐
create index my_index on table //为表格table的name字段创建一个名为my_index的索引 drop index my_index on [table] //删除表格table的名为my_index的索引
-
group by和where⭐⭐⭐⭐
- group表示分组,BY后面写字段名,就表示根据哪个字段进行分组,如果有用Excel比较多的话,group by比较类似Excel里面的透视表。
- where:数据库中常用的是where关键字,用于在初始表中筛选查询。它是一个约束声明,用于约束数据,在返回结果集之前起作用。
- group by:对select查询出来的结果集按照某个字段或者表达式进行分组,获得一组组的集合,然后从每组中取出一个指定字段或者表达式的值。
- having:用于对where和group by查询出来的分组经行过滤,查出满足条件的分组结果。它是一个过滤声明,是在查询返回结果集以后对查询结果进行的过滤操作。
执行顺序 select –>where –> group by–> having–>order by
-
数据库笛卡尔乘积⭐⭐⭐⭐
设A,B为集合,用A中元素为第一元素,B中元素为第二元素构成有序对,所有这样的有序对组成的集合叫做A与B的笛卡尔积,记作AxB.
-
什么是Redis⭐⭐⭐⭐⭐
Redis 是一个基于内存的高性能key-value数据库。
- 整个数据库统统加载在内存当中进行操作,定期通过异步操作把数据库数据flush到硬盘上进行保存。
- 因为是纯内存操作,Redis的速度快。支持丰富数据类型,如string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。支持事务。
Redis的主要缺点是数据库容量受到物理内存的限制,不能用作海量数据的高性能读写,因此Redis适合的场景主要局限在较小数据量的高性能操作和运算上。
-
Redis怎么实现高效缓存⭐⭐⭐⭐⭐
为了提供网站的负载能力,需要将一个访问频路较高,且经过复杂计算或者IO资源消耗较大的操作的结果缓存起来,并设置一个失效时间。每次用户访问的时候,先检查该键是否存在,如果存在直接获取该元素并返回,如果不存在,则经过一系列计算并将结果缓存,设置失效时间,在返回给用户
-
Redis持久化有哪几种方式,怎么选⭐⭐⭐⭐⭐
Redis是一种高级key-value数据库。它跟memcached类似,不过数据可以持久化。Redis的所有数据都是保存在内存中,然后不定期的通过异步方式保存到磁盘上。redis提供两种方式进行持久化,一种是RDB持久化(原理是将Reids在内存中的数据库记录定时dump到磁盘上的RDB持久化),另外一种是AOF(append only file)持久化(原理是将Reids的操作日志以追加的方式写入文件)。
RDB是定期将数据写入磁盘,当redis出现故障时,有一部分内存数据肯定会丢失。而AOF是以日志的方式追加,数据丢失会少很多。对于同一份数据来说,AOF 日志文件通常比 RDB 数据快照文件更大,数据恢复慢一些。
-
不要仅仅使用 RDB,因为那样会导致你丢失很多数据
-
也不要仅仅使用 AOF,因为那样有两个问题,第一,你通过 AOF 做冷备,没有 RDB 做冷备,来的恢复速度更快; 第二,RDB 每次简单粗暴生成数据快照,更加健壮,可以避免 AOF 这种复杂的备份和恢复机制的 bug。
-
redis 支持同时开启开启两种持久化方式,我们可以综合使用 AOF 和 RDB 两种持久化机制,用 AOF 来保证数据不丢失,作为数据恢复的第一选择; 用 RDB 来做不同程度的冷备,在 AOF 文件都丢失或损坏不可用的时候,还可以使用 RDB 来进行快速的数据恢复。
-
-
Redis对于过期键的清除策略⭐⭐⭐⭐⭐
- 被动删除:当读/写一个已经过期的key时,会触发惰性删除策略,直接删除掉这个过期key
- 主动删除:由于惰性删除策略无法保证冷数据被及时删掉,所以Redis会定期主动淘汰一批已过期的key
- 当前已用内存超过maxmemory限定时,触发主动清理策略
-
Redis单线程为什么快⭐⭐⭐⭐⭐
- 完全基于内存,绝大部分请求是纯粹的内存操作,非常快速。
- 采用单线程,避免了不必要的上下文切换和竞争条件,也不存在多进程或者多线程导致的切换而消耗 CPU,不用去考虑各种锁的问题,不存在加锁释放锁操作,没有因为可能出现死锁而导致的性能消耗
- 使用多路I/O复用模型,非阻塞IO
-
Redis如何实现高可用⭐⭐⭐⭐⭐
- 主从复制数据:实现数据库的读写分离,主数据库主要进行写操作,而从数据库负责读操作。
- 哨兵机制:监控(哨兵(sentinel)会不断地检查你的Master和Slave是否运作正常)、提醒(当被监控的某个Redis出现问题时, 哨兵(sentinel)可以通过 API 向管理员或者其他应用程序发送通知)、自动故障迁移(当一个Master不能正常工作时,哨兵(sentinel)会开始一次自动故障迁移操作,它会将失效Master的其中一个Slave升级为新的Master)
-
Redis缓存穿透、缓存击穿、缓存雪崩⭐⭐⭐⭐⭐
-
缓存穿透:是指查询一个数据库一定不存在的数据。正常的使用缓存流程大致是,数据查询先进行缓存查询,如果key不存在或者key已经过期,再对数据库进行查询,并把查询到的对象,放进缓存。如果数据库查询对象为空,则不放进缓存。参数传入对象主键ID根据key从缓存中获取对象如果对象不为空,直接返回如果对象为空,进行数据库查询如果从数据库查询出的对象不为空,则放入缓存(设定过期时间)想象一下这个情况,如果传入的参数为-1,会是怎么样?这个-1,就是一定不存在的对象。就会每次都去查询数据库,而每次查询都是空,每次又都不会进行缓存。假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库。
解决办法:如果从数据库查询的对象为空,也放入缓存,只是设定的缓存过期时间较短,比如设置为60秒
-
缓存击穿:是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。
解决办法:让缓存永不过期。
-
缓存雪崩:是指在某一个时间段,缓存集中过期失效。那么访问查询都落到了数据库上,对于数据库而言,就会产生周期性的压力波峰。
解决办法:热门key缓存时间长一些,冷门key缓存时间短一些
-
-
Redis渐进式rehash⭐⭐⭐⭐⭐
在redis的具体实现中,使用了一种叫做渐进式哈希(rehashing)的机制来提高字典的缩放效率,避免 rehash 对服务器性能造成影响,渐进式 rehash 的好处在于它采取分而治之的方式, 将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上, 从而避免了集中式 rehash 而带来的庞大计算量。
-
Redis相比memcached有哪些优势⭐⭐⭐⭐⭐
- memcached所有的值均是简单的字符串,redis作为其替代者,支持更为丰富的数据类型
- redis的速度比memcached快很多
- redis可以持久化其数据
C++软件与嵌入式软件面经解析大全(蒋豆芽的秋招打怪之旅)
本章讲解知识点
- 1.1 HR心理复盘
- 1.2 HR常问问题——学校的表现怎么样啊?
- 1.3 HR常问问题——了解我们公司吗?
- 1.4 HR常问问题——个人情况
- 1.5 HR常问问题——业余生活
- 1.6 HR常问问题——薪资待遇
- 1.7 HR常问问题——人性考察
故事背景
**蒋 豆 芽:**小名豆芽,芳龄十八,蜀中人氏。卑微小硕一枚,科研领域苟延残喘,研究的是如何炒好一盘豆芽。与大多数人一样,学习道路永无止境,间歇性踌躇满志,持续性混吃等死。会点编程,对了,是面对对象的那种。不知不觉研二到找工作的时候了,同时还在忙论文,豆芽都秃了,不过豆芽也没头发啊。
**隔壁老李:**大名老李,蒋豆芽的好朋友,技术高手,代码女神。给了蒋豆芽不少的人生指导意见。
**导 师:**蒋豆芽的老板,研究的课题是每天对豆芽嘘寒问暖。
故事引入
隔壁老李:(凝视前方)
蒋 豆 芽:老李,你在干嘛,又在思考人生嘛?
隔壁老李:豆芽,不知不觉,我们已经走到秋招的末尾了,有点感慨。
蒋 豆 芽:是啊,老李,从三月份的准备,白天忙论文,晚上搞面经。老李你陪着我度过了夏天,迎来了秋天。真的感谢你,在我煎熬的时候给我信心与力量。
隔壁老李:(愣)怎么突然这么煽情了?好男儿不可效儿女状,哈哈。陪伴豆芽的日子,见证了豆芽的努力和成长,杀不死你的,终会使你更加强大,而豆芽你也有了自己的收获。恭喜恭喜。
蒋 豆 芽:(嘻嘻)说来也是惭愧,投了接近两百家公司,笔试一百场,面试几十场,可谓身经百战,拿到了三家中公司offer,两家大公司offer。
隔壁老李:说到这个,今天我们就要奉献最后一篇面经了,那就是HR面经。豆芽,你既然面试了这么多家公司,对于HR面有什么感悟啊?
1.1 HR心理复盘
蒋 豆 芽:(胸有成竹)这个我还是很有体会的。回答HR的问题,最高宗旨就是:保持真我的情况下,说出HR想要的那个答案。在你的回答中体现出正能量。
隔壁老李:啧啧啧,精辟啊!
蒋 豆 芽:(嘻嘻)其实不难理解,HR面试就是和HR之间的博弈。我们学会换位思考,那么一切都很好理解了。如果我是HR,那我肯定想找优秀的、工作能力强的、积极向上的员工为公司创造价值。而对于校招,必须要综合考虑面试者的意愿,我才考虑是否发offer。
找工作就像谈恋爱一样,我喜欢对方,那我也要考虑对方是否喜欢我啊?喜欢我的情况下,我才能付出真心,否则就会受到伤害,你说对吧。
HR也是一样,他需要充分考虑面试者意愿到底怎么样?**那到底如何知道面试者的意愿呢?自然就是通过问问题来旁敲侧击了呀!**但是可别忘了,在保持真我的情况下,说假话就没必要了。
隔壁老李:(撇撇嘴)你这个例子举得不错,哈哈,看样子你也是受过了不少的伤,才有如此深刻的体会啊。
蒋 豆 芽:(苦笑)说多了都是累。
隔壁老李:好了,回到正题,我们来看看HR都会问什么问题吧!
1.2 HR常问问题——学校的表现怎么样啊?
蒋 豆 芽:HR总是会问问我们在学校的表现,如有没有拿过奖学金啊?参加过什么活动实践啊?在校期间最有成就感的一件事啊?说说你的在校经历啊?遇到过什么困难吗,怎么解决的?
等等这类问题,都可以归结到校园经历上。
隔壁老李:如果你是HR,你想要什么答案?
蒋 豆 芽:如果我是HR,我想找成绩好的学生、在校活动实践较丰富的学生、积极参加课外实践的学生、在校努力学习积极思考的学生。
蒋 豆 芽:我本科获得过两次校级二等奖学金、国奖励志奖学金、中国教育机器人国家一等奖、优秀毕业生、优秀毕业设计;研究生获得过两次国家一等奖学金、国家奖学金、省优秀研究毕业生、两次校级优秀研究生称号。发表了两篇一区SCI论文。
我本科成绩专业第一,保送至湖南大学;研究生成绩班级第一。
在校期间最有成就感的一件事就是双非学校保送至名牌大学,圆了自己的名牌大学梦。今后还要继续加油!
我在校经历是,我本科努力学习专业知识,获得过两次校级二等奖学金、国奖励志奖学金,成绩在专业前5%;我积极参加课外实践,获得过电子设计大赛二等奖、中国教育机器人国家一等奖;我加入过年级委员会,组织元旦晚会活动,服务院学生工作;最后以专业第一名保送至湖南大学,成绩班级第一,在湖南大学发表了两篇一区SCI论文,获得过两次国家一等奖学金、国家奖学金、省优秀研究毕业生、两次校级优秀研究生称号。我的座右铭是:脚踏实地、仰望星空,再攀高峰!
我曾经在科研的时候遇到过科研瓶颈,找不到科研思路,那个时候我就和师兄师姐多多讨论,看能不能学到了方法,然后再去大量参考文献,困苦了两周时间,当时实在痛苦,我觉得有点累,就给自己放了两天假。然后接着参考文献,和老师他们多讨论,找思路,最后也就找到了解决办法。
(大家可以根据自己的情况来回答,前提是保证真实,真的假不了,假的真不了)
隔壁老李:那有的同学没那么丰富的经历,要怎么回答呢?
蒋 豆 芽:也可以根据自己的情况来回答,最高宗旨,在回答中体现自己的努力、思考,给出HR想要的答案。
1.3 HR常问问题——了解我们公司吗?
蒋 豆 芽:HR总是会问,你了解我们公司吗?为什么想来我们公司?如何看待我们的公司文化?接受加班吗?有什么职业规划?
等等这类问题,都可以归结到公司上。
隔壁老李:如果你是HR,你想要什么答案?
蒋 豆 芽:如果我是HR,我想找认同公司文化的同学,接收加班、努力为公司创造价值的同学,有自己职业规划的同学、来我们公司意愿强烈的同学。
蒋 豆 芽:我对贵公司有一个简单的了解,公司具体的业务是消费者BG、企业BG、运营商BG三大块,我很认同贵公司的“以客户为中心,以奋斗者为本”的价值理念,服务好客户是根本,多劳多得,天经地义,当然我理解得也不深,期待能入职贵公司去深刻的体会公司文化。
我接受加班,没有问题,我在研究生期间基本就是加班状态,我每天工作到晚上九点,有事就忙,没事也要坚持学习,所以加班对我来说是提升自我的过程。
我的职业规划是,期望入职贵公司,在一年内能尽快熟悉业务尽快为公司创造价值,在三五年内成为领域的小牛人,我希望能长期扎根公司,和公司一同成长。
(还是一样的哦,大家说出自己真实情况和真实想法就行了,真的,别骗自己,有的人接受不了加班,那就别去互联网大公司,追逐自己的内心就好)
1.4 HR常问问题——个人情况
蒋 豆 芽:HR总是会问我们的一些个人情况,你家住哪里啊?介意地域吗?有对象嘛?对象对你找工作什么意见?
等等这类问题,都可以归结到个人情况上。
隔壁老李:如果你是HR,你想要什么答案?
蒋 豆 芽:如果我是HR,我想找不介意地域的同学、服从公司安排的同学、来我们公司意愿强烈的同学。
蒋 豆 芽:我是四川人,但是我对地域没有要求,这跟我个人经历有关,我本科在广西读书,在深圳实习,在湖南长沙读研,所以早已习惯在外面的生活,我个人愿意去沿海大城市发展。对我来说,年轻就是要打拼。
我有对象,她尊重我的选择。
(还是一样的哦,大家说出自己真实情况和真实想法就行了)
1.5 HR常问问题——业余生活
蒋 豆 芽:HR总是会问我们的业余生活,平时喜欢干嘛啊?有什么兴趣爱好啊?喜欢运动吗?
等等这类问题,都可以归结到业余生活上。
隔壁老李:如果你是HR,你想要什么答案?
蒋 豆 芽:如果我是HR,我想找有一项健康的兴趣爱好的同学,这样的同学心理更健康,也懂得缓解生活压力,能更好适应工作的压力。
蒋 豆 芽:我平时喜欢看书看电影,可以放松自我。我也喜欢写技术博客,能够总结自我。平时有运动,身体是工作的本钱,我喜欢跑步,每周两次。
(还是一样的哦,大家说出自己真实情况和真实想法就行了)
1.6 HR常问问题——薪资待遇
蒋 豆 芽:HR总是会问我们的薪资待遇,拿了几个offer啊?找公司看重什么啊?薪资预期是多少啊?除了我们公司以外最想去哪家公司。
等等这类问题,都可以归结到薪资待遇上。
隔壁老李:如果你是HR,你想要什么答案?
蒋 豆 芽:如果我是HR,我想找目前仅有一两个offer的同学、手里的offer没有我公司好的同学、薪资预期符合公司标准的同学、来我们公司意愿强烈的同学。
蒋 豆 芽:我目前手上就一个offer,我找公司看重三点:一是企业文化是否让我认同;二是有没有健全的培养制度;三是薪资待遇。
我也不知道这个岗位的薪资应该是多少,一般公司都有自己的薪资标准,我服从公司的薪资标准,我相信公司当然不会亏待应聘者。
(这道题有点难回答)除了贵公司,我之前最想去BAT,不过最近我对贵公司进行了了解,改变了我的这一想法。我十分认同贵公司的“以客户为中心,以奋斗者为本”的价值理念,我之前有读过《下一个倒下的,会不会是华为》一书,更加坚定了我想融入贵公司的想法。我对贵公司的意愿很强烈。
(大家可以自己查查offershow)
1.7 HR常问问题——人性考察
蒋 豆 芽:HR有时候会对我们进行人性测试,说说你的个人优势和劣势?请你评价一下你自己?说说别人对你的评价?
等等这类问题,都可以归结到人性考察上。
隔壁老李:这个题有点难回答了呀。如果你是HR,你想要什么答案?
蒋 豆 芽:如果我是HR,我想找能够正确认识自我的同学。
蒋 豆 芽:每个人都有优点和缺点,我的优点是吃苦耐劳、肯花时间做好一件事,我本科学习在专业前5%,我每天会看书两小时,我也积极参加课外实践,获得了中国教育机器人国家一等奖,最后保研至名牌大学,这是我很骄傲的一件事。在研究生阶段,为了写好论文,我每天工作到晚上九点,发表了两篇一区SCI论文。
我也有缺点,我的缺点是比较固执,我认为自己是对的,我就可能听不进别人的建议,但是后来我慢慢发现,这个缺点不好,因为别人的建议有时候也是好的,将自己的想法与别人的想法综合一下,往往能得到更好的结果。所以我也一直在改正这个缺点。
我对自己的评价是,我觉得自己是一个努力上进的boy,很多事情我都要求自己尽可能做好,我每天保持学习,每天进步一些,长期就是很大的成长,这得益于我的自律。
我倒是没问过别人,主要是我不太好意思,但是通过亲近的人的聊天中,可以得到别人对我的大致看法。一致认为我努力上进、比较优秀、自律,心事重,想得多、烦恼多。
(大家可以根据自己情况说,宗旨就是体现出正能量。大家一定要好好体会一下我的回答,都是很讲究的)
隔壁老李:(击掌)bingo!豆芽,今天我就将HR面试的要点一网打尽了,祝贺你成长很快啊!
蒋 豆 芽:嘿嘿。