面试题汇总
常考面试题
-
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];
-
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
- 本专栏适合于C/C++已经入门的学生或人士,有一定的编程基础。 - 本专栏特点: 本专刊囊括了C语言、C++、操作系统、计算机网络、嵌入式、算法与数据结构、数据库等一系列知识点,总结出了高频面试考点(附有答案)共计309道,事半功倍,为大家春秋招助力。 - 本专栏内容分为七章:共计309道高频面试题(附有答案)