【八股文】嵌入式软件工程师-2025校招必备-详细整理

0 写在前面

       后续会不断更新,大家可以先关注我,点收藏!

       开头先分享一下自己的一些笔记,欢迎大家关注我的VX_公_众_号:嵌入式求职之路

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第1张图片

        我本科是普通双非院校毕业,硕士就读于某个中游211院校,本科和硕士专业都是机械工程

        然后在2024届秋招中,从8.15号开始准备秋招投递简历,到9.20号成功拿到5个嵌入式软件工程师的offer,最低的offer是在二线省会城市一个做工业机器人的企业,总包22万最高的offer是做新能源存储设备,总包36万,其他三个offer薪资总包在25万-30万之间。最后选择新能源行业做嵌入式电力设备开发。

        从机械-->嵌入式,相当于是跨半个专业,整个过程全靠自学,然后自己独立完成几个项目,从stm32到RTOS操作系统,再到Linux应用开发,中间也涉及到了驱动、QT上位机等等,中间遇到了很多困难,然后摸索出很多学习思路和解决办法。

        这一篇八股文文章,是我自己结合网络上他人的经验,自己详细整理出来的,我秋招过程中也是使用了自己这篇八股文文章,把它写出来,分享给大家。

        这篇文章包含的内容很多(会不断更新):

        第一部分(纯八股):C语言、C++、数据结构与算法、操作系统(RTOS)、ARM基础与架构、通讯协议、Linux应用知识点、Linux驱动知识点

        第二部分(个人简历):简历模板、做一份好的嵌入式简历、如何突出重点

        第三部分(经验分享):面试经历、面试经验、面试技巧

第一部分(纯八股)

1 C语言/C++

1.1 面试常问关键字

        面试不是死记硬背,你死记硬背,技术面试官能一下就瞧出来,所以要从本质上去理解一些内容。所以,八股有些地方我写的很详细,不要嫌我啰嗦,因为这些地方的八股必须要真的去理解,而不单单是背诵,相信大家看完后,就能理解相应的知识点了,而不仅仅是死记硬背。

1.1.1 volatile关键字

        volatile的意思是”易变的”,这个关键字主要是防止编译器对变量进行优化。即告诉编译器每次存取该变量的时候都要从内存去存取而不是使用它之前在寄存器中的备份。详细分析一下什么是编译器优化,以及为什么使用这个关键字。

(a)关于编译器优化

        首先理解CPU(寄存器)读取规则:

        如下面程序段:

int a, b; // 为a,b申请内存
a = 1;    // 1 -> CPU
          // CPU -> 内存(&a)
b = a;    // 内存(&a) -> CPU
          // CPU -> 内存(&b)

        如上图代码所示,a = 1这个程序,先将1写入CPU,再从CPU中将1写入a所在的内存地址中; b = a是先从内存中将a的值取出到CPU,再从CPU将值存入b的内存地址中。

int a = 1, b, c; // 为a,b,c申请内存并初始化
b = a;    // 内存(&a) -> CPU
          // CPU -> 内存(&b)
c = a;    // * 内存(&a) -> CPU *
          // CPU -> 内存(&c)

        如上图代码所示,上边的程序如果按第一段代码所说的顺序执行,则c = a语句在编译时是可以被编译器优化的,即注释部分(* 内存(&a) -> CPU *)的内容不被执行,因为在b = a这个语句中,a已经被移入过寄存器(CPU),那么在执行c = a时,就直接将a在寄存器(CPU)中传递给c。这样就减少了一次指令的执行,就完成了优化。
        上面就是编译器优化的原理过程,但是这个过程,有时会出现问题,而这个问题也就volatile存在的意义! 

(b)volatile的引入

        上边程序中,如果在执行完b = a后,a此时的值存放在CPU中。但是a在内存中又发生了变化(比如中断改变了a的值),但是存在CPU中的a是原来未变的a,按理应该是已经变化后的a赋值给c,但是此时却导致未变化的a赋值给了c

        这种问题,就是编译器自身优化而导致的。为了防止编译器优化变量a,引入了volatile关键字,使用该关键字后,程序在执行时c = a时,就会先去a的地址读出a到CPU,再从CPU将a的值赋予给c。这样就防止了被优化。

volatile int a = 1, b, c; // 为a,b,c申请内存并初始化
b = a;    // 内存(&a) -> CPU
          // CPU -> 内存(&b)
c = a;    // 内存(&a) -> CPU
          // CPU -> 内存(&c)
(c)哪些情况下使用volatile

        (1)并行设备的硬件寄存器。存储器映射的硬件寄存器通常加volatile,因为寄存器随时可以被外设硬件修改。当声明指向设备寄存器的指针时一定要用volatile,它会告诉编译器不要对存储在这个地址的数据进行假设。

        (2) 一个中断服务程序中修改的供其他程序检测的变量。volatile提醒编译器,它后面所定义的变量随时都有可能改变。因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。

        (3)多线程应用中被几个任务共享的变量。

1.1.2 static关键字

        面试问题1:static关键词的作用?

        static是被声明为静态类型的变量,存储在静态区(全局区)中,其生命周期为整个程序,如果是静态局部变量,其作用域为一对{  }内,如果是静态全局变量,其作用域为当前文件。静态变量如果没有被初始化,则自动初始化为0。

        面试问题2:为什么 static变量只初始化一次?

        对于所有的对象(不仅仅是静态对象),初始化都只有一次,而由于静态变量具有“记忆”功能,初始化后,一直都没有被销毁,都会保存在内存区域中,所以不会再次初始化。存放在静态区的变量的生命周期一般比较长,它与整个程序“同生死、共存亡”,所以它只需初始化一次。而auto变量,即自动变量,由于它存放在栈区,一旦函数调用结束,就会立刻被销毁。

        static修饰的全局变量,只能在本文件被调用;修饰的函数也只能在本文件调用。

1.1.3 const关键字

(a)定义变量(局部变量或全局变量)为常量,例如:

        图片在CSDN上带有水印,不太方便观看,后续会分享便于观看的电子版PDF文档 

 (b)修饰指针

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第2张图片

         第一种和第二种是常量指针;第三种是指针常量;第四种是指向常量的常指针。

(b1)面试问题1:什么是常量指针?

        (1)常量指针说的是不能通过这个指针改变变量的值,但是还是可以通过其他的方式来改变变量的值的。
        (2)常量指针指向的值不能改变,但是这并不是意味着指针本身不能改变,常量指针可以指向其他的地址。

         上图中,p1是定义的常量指针,p1指向a的地址,*p1 = 15是不行的,因为不能通过常量指针去改变变量的值,如果去掉const则是可以的。

        没有const时,利用*p1可以去对a的值进行修改,如下图所示。

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第3张图片

(b2)面试问题2:什么是指针常量? 

        指针常量是指指针本身是个常量,不能在指向其他的地址,需要注意的是,指针常量指向的地址不能改变,但是地址中保存的数值是可以改变的,可以通过其他指向该地址的指针来修改。

 (b3)面试问题3:什么是指向常量的常指针? 

        是指针常量与常量指针的结合,指针指向的位置不能改变并且也不能通过这个指针改变变量的值,但是依然可以通过其他的普通指针改变变量的值。

 (c)修饰函数的参数

        表示在函数体内不能修改这个参数的值。

 (d)修饰函数的返回值

        (d1)如果给用const修饰返回值的类型为指针,那么函数返回值(即指针)的内容是不能被修改的,而且这个返回值只能赋给被const修饰的指针。例如:

        (d2)如果用const修饰普通的返回值,如返回int变量,由于这个返回值是一个临时变量,在函数调用结束后这个临时变量的生命周期也就结束了,因此把这些返回值修饰为const是没有意义的。

1.1.4 typedef和 define有什么区别?

        typedef与define都是替一个对象取一个别名,以此来增强程序的可读性,但是它们在使用和作用上也存在着以下4个方面的不同。

(a)原理不同

        #define是C语言中定义的语法,它是预处理指令,在预处理时进行简单而机械的字符串替换,不做正确性检査,不管含义是否正确照样代入,只有在编译已被展开的源程序时,才会发现可能的错误并报错。 例如, # define Pl3.1415926 ,当程序执行 area=Pr * r 语句时,PI会被替换为3.1415926。于是该 语句被替换为 area=3.1415926*r*r 。如果把# define语句中的数字9写成了g,预处理也照样代入,而不去检查其是否合理、合法。 typedef是关键字,它在编译时处理,所以 typedef具有类型检查的功能。它在自己的作用域内给一个已经存在的类型一个别名,但是不能在一个函数定义里面使用标识符 typedef。例如,typedef int INTEGER ,这以后就可用 INTEGER来代替int作整型变量的类型说明了,例如:INTEGER a,b; 用 typedef定义数组、指针、结构等类型将带来很大的方便,不仅使程序书写简单而且使意义更为明确,因而增强了可读性。例如: typedef int a[10]; 表示a是整型数组类型,数组长度为10。然后就可用a说明变量,例如:语句a s1,s2;完全等效于语句 int s1[10],s2[10].同理, typedef void(*p)(void)表示p是一种指向void型的指针类型。

(b)功能不同

        typedef用来定义类型的别名,这些类型不仅包含内部类型(int、char等),还包括自定义类型(如 struct),可以起到使类型易于记忆的功能。

        例如: typedef int (*PF)(const char *, const char*) 定义一个指向函数的指针的数据类型PF,其中函数返回值为int,参数为 const char*。typedef还有另外一个重要的用途,那就是定义机器无关的类型。例如,可以定义一个叫REAL的浮点类型,在目标机器上它可以获得最高的精度: typedef long double REAL ,在不支持 long double的机器上,该 typedef 看起来会是下面这样: typedef double real ,在 double都不支持的机器上,该 typedef看起来会是这样: typedef float REAL 。 #define不只是可以为类型取别名,还可以定义常量、变量、编译开关等。

(c)作用域不同

        #define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而 typedef有自己的作用域。

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第4张图片

(d)对指针的操作不同

        INTPTR1 pl, p2和INTPTR2 p3,p4的效果截然不同。 INTPTR1 pl, p2进行字符串替换后变成 int*p1,p2 ,要表达的意义是声明一个指针变量p1和一个整型变量p2。

        而INTPTR2 p3,p4,由于 INTPTR2是具有含义的,告诉我们是一个指向整型数据的指针,那么p3和p4都为指针变量,这句相当于 int*pl,*p2 .从这里可以看出,进行宏替换是不含任何意义的替换,仅仅为字符串替换;而用 typedef 为一种数据类型起的别名是带有一定含义的。

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第5张图片

        上述代码中, const INTPTR1 p1表示p1是一个常量指针,即不可以通过p1去修改p1指向的内容,但是 p1可以指向其他内容。而对于 const INTPTR2 p2,由于 INTPTR2表示的是个指针类型,因此用 const去 限定,表示封锁了这个指针类型,因此p2是一个指针常量,不可使p2再指向其他内容,但可以通过p2修 改其当前指向的内容。 INTPTR2 const p3同样声明的是一个指针常量。

1.2 变量、数组、指针

1.2.1变量

(a)定义常量谁更好?# define还是 const?

        尺有所短,寸有所长, define与 const都能定义常量,效果虽然一样,但是各有侧重。

        define既可以替代常数值,又可以替代表达式,甚至是代码段,但是容易出错,而 const的引入可以增强程序的可读性,它使程序的维护与调试变得更加方便。具体而言,它们的差异主要表现在以下3个方面。

        (a1)define只是用来进行单纯的文本替换,define常量的生命周期止于编译期,不分配内存空间,它存在于程序的代码段,在实际程序中,它只是一个常数;而const常量存在于程序的数据段,并在堆栈中分配了空间,const常量在程序中确确实实存在,并且可以被调用、传递

        (a2)const常量有数据类型,而define常量没有数据类型。编译器可以对const常量进行类型安全检査,如类型、语句结构等,而define不行。

        (a3)很多IDE支持调试 const定义的常量,而不支持 define定义的常量由于const修饰的变量可以排除 程序之间的不安全性因素,保护程序中的常量不被修改,而且对数据类型也会进行相应的检查,极大地提高了程序的健壮性,所以一般更加倾向于用const来定义常量类型。

(b)全局变量和局部变量的区别是什么?

        (b1)全局变量的作用域为程序块,而局部变量的作用域为当前函数。

        (b2)内存存储方式不同,全局变量(静态全局变量,静态局部变量)分配在全局数据区(静态存储空间),后者分配在栈区。

        (b3)生命周期不同。全局变量随主程序创建而创建,随主程序销毁而销毁,局部变量在局部函数内部,甚至局部循环体等内部存在,退出就不存在了。

        (b4)使用方式不同。通过声明为全局变量,程序的各个部分都可以用到,而局部变量只能在局部使用。

(c)全局变量可不可以定义在可被多个.C文件包含的头文件中?为什么?

        可以,在不同的C文件中以static形式来声明同名全局变量

        可以在不同的C文件中声明同名的全局变量,前提是其中只能有一个C文件中对此变量赋初值,此时连接不会出错。

(d)局部变量能否和全局变量重名?

        能,局部会屏蔽全局。

        局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。 对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内。

1.2.2 数组

(a)数组指针

        数组指针就是指向数组的指针,它表示的是一个指针,这个指针指向的是一个数组,它的重点是指针。 例如, int(*pa)[8] 声明了一个指针,该指针指向了一个有8个int型元素的数组。下面给出一个数组 指针的示例。

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第6张图片

        程序的输出结果为 5。

        上例中,p是一个数组指针,它指向一个包含有4个int类型数组的指针,刚开始p被初始化为指向数组b 的首地址,++p相当于把p所指向的地址向后移动4个int所占用的空间,此时p指向数组{5,6,7,8},语句 *(++p); 表示的是这个数组中第一个元素的地址(可以理解p为指向二维数组的指针,{1,2,3,4}, {5,6,7,8},{9,10,11,12}。p指向的就是{1,2,3,4}的地址, *p 就是指向元素,{1,2,3,4}, **p 指向的就是1,语句**(++p)会输出这个数组的第一个元素5。

(b)指针数组

        指针数组表示的是一个数组,而数组中的元素是指针。下面给出另外一个指针数组的示例:

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第7张图片

        程序的输出结果为1234。

(c)数组下标可以为负数吗?

        可以,因为下标只是给出了一个与当前地址的偏移量而已,只要根据这个偏移量能定位得到目标地址即可。

1.2.3 指针

(a)函数指针

        如果在程序中定义了一个函数,那么在编译时系统就会为这个函数代码分配一段存储空间,这段存储空 间的首地址称为这个函数的地址。而且函数名表示的就是这个地址。既然是地址我们就可以定义一个指针变量来存放,这个指针变量就叫作函数指针变量,简称函数指针。

        这个语句就定义了一个指向函数的指针变量 p。首先它是一个指针变量,所以要有一个“*”,即 (*p); 其次前面的 int 表示这个指针变量可以指向返回值类型为 int 型的函数;后面括号中的两个 int 表示这个 指针变量可以指向有两个参数且都是 int 型的函数。所以合起来这个语句的意思就是:定义了一个指针变量 p,该指针变量可以指向返回值类型为 int 型,且有两个整型参数的函数。p 的类型为 int(*) (int,int) 。

        我们看到,函数指针的定义就是将“函数声明”中的“函数名”改成“(指针变量名)”。但是这里需要注意的是:“(指针变量名)”两端的括号不能省略,括号改变了运算符的优先级。如果省略了括号,就不是定义函数指针而是一个函数声明了,即声明了一个返回值类型为指针型的函数。

        重要:最后需要注意的是,指向函数的指针变量没有 ++ 和 -- 运算。

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第8张图片

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第9张图片

(b)指针函数

        首先它是一个函数,只不过这个函数的返回值是一个地址值。函数返回值必须用同类型的指针变量来接受,也就是说,指针函数一定有“函数返回值”,而且,在主调函数中,函数返回值必须赋给同类型的指针变量。

       类型名 *函数名(函数参数列表)

        其中,后缀运算符括号“()”表示这是一个函数,其前缀运算符星号“*”表示此函数为指针型函数,其函数值为指针,即它带回来的值的类型为指针,当调用这个函数后,将得到一个“指向返回值为…的指针(地址),“类型名”表示函数返回的指针指向的类型”。

        “(函数参数列表)”中的括号为函数调用运算符,在调用语句中,即使函数不带参数,其参数表的一对括号也不能省略。其示例如下:

        由于 “*” 的优先级低于“()”的优先级,因而pfun首先和后面的“()”结合,也就意味着,pfun是一个函数。 即:

        接着再和前面的 “*” 结合,说明这个函数的返回值是一个指针。由于前面还有一个int,也就是说,pfun是一个返回值为整型指针的函数。

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第10张图片

        共有三个学生的成绩,函数find()被定义为指针函数,其形参pointer是指针指向包含4个元素的一维数组 的指针变量。pointer+n指向score的第n+1行。*(pointer+1)指向第一行的第0个元素。pt是一个指针变 量,它指向浮点型变量。main()函数中调用find()函数,将score数组的首地址传给pointer。

(c)数组和指针的区别与联系是什么?
        (c1)存储方式

        数组通常存储在静态存储区或栈上;指针可以随时随地地指向任意类型的内存块。

        数组在内存中是连续存放的,开辟一块连续的内存空间。数组是根据数组的下标进行访问的;指针很灵活,它可以指向任意类型的数据。指针的类型说明了它所指向地址空间的内存。

        (c2)求sizeof
        数组:

                数组所占存储空间的内存sizeof(数组名)

                数组的大小:sizeof(数组名)/sizeof(数据类型)

        指针:

                在32位平台下,无论指针的类型是什么,sizeof(指针名)都是4,在64位平台下,无论指针的类型是什么,sizeof(指针名)都是8

        (c3)数据访问方面

        指针对数据的访问方式是间接访问,需要用到解引用符号(*数组名)。

        数组对数据的访问则是直接访问,可通过下标访问或数组名+元素偏移量的方式

        (c4)使用环境

        指针多用于动态数据结构(如链表,等等)和动态内存开辟。

        数组多用于存储固定个数且类型统一的数据结构(如线性表等等)和隐式分配。

(d)指针进行强制类型转换后与地址进行加法运算,结果是什么?

        假设在32位机器上,在对齐为4的情况下,sizeof(long)的结果为4字节,sizeof(char*)的结果为4字节, sizeof(short int)的结果与 sizeof(short)的结果都为2字节, sizeof(char)的结果为1字节, sizeof(int)的结果为4字节,由于32位机器上是4字节对齐,以如下结构体为例:

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第11张图片

        当p=0x100000; 则 p+0×200=? (ulong)p+0x200=? (char*)p+0x200=? 其实,在32位机器下, sizeof(struct BBB)=sizeof(*p)=4+4+2+2+1+3/*补齐*/+2*5+2/*补齐*/=24字节,而 p=0x100000 ,那么 p+0x200=0x1000000+0x200*24 指针加法,加出来的是指针所指类型的字节长度的整倍数,就是p偏移sizeof(p)*0x200。

        (ulong)p+0x200=0x10000010+0x200经过ulong后,已经不再是指针加法,而变成一个数值加法了。(char*)p+0x200=0x1000000+0×200*sizeof(char) 结果类型是char*。

(e)指针常量,常量指针,指向常量的常量指针有什么区别?
        (e1)指针常量

        先看const再看 * ,p是一个常量类型的指针,不能修改这个指针的指向,就是指针指向的地址不能修改,但是这个指针所指向的地址上存储的值可以修改。

        (e2)常量指针

先看*再看const,定义一个指针指向一个常量,不能通过指针来修改这个指针指向的值

        (e3)指向常量的常量指针

对于“指向常量的常量指针”,就必须同时满足上述1和2中的内容,既不可以修改指针的值,也不可以修改指针指向的值

(f)指针和引用的异同是什么?如何相互转换?(C++)

        (f1)相同

  1. 都是地址的概念,指针指向某一内存、内容是所指内存的地址;引用则是某块内存的别名
  2. 从内存分配上看:两者都占内存,程序为指针会分配内存,一般是4个字节;而引用的本质是指针常量,指向对象不能变,但指向对象的值可以变。两者都是地址概念,所以本身都会占用内存。

        (f2)区别

        1. 指针是实体,而引用是别名。

        2. 指针和引用的自增(++)运算符意义不同,指针是对内存地址自增,引用是对值的自增。

        3. 引用使用时无需解引用(*),指针需要解引用;

        4. 引用只能在定义时被初始化一次,之后不可变;指针可变。

        5. 引用不能为空,指针可以为空。

        6. “sizeof 引用”得到的是所指向的变量(对象)的大小,而“sizeof 指针”得到的是指针本身的大小,在32 位系统指针变量一般占用4字节内存。

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第12张图片

        由结果可知,引用使用时无需解引用(*),指针需要解引用;我用的是64位操作系统,“sizeof 指针”得到 的是指针本身的大小,及8个字节。而“sizeof 引用”得到的是的对象本身的大小及int的大小,4个字节。

       (f3) 转换

        1. 指针转引用:把指针用*就可以转换成对象,可以用在引用参数当中。

        2. 引用转指针:把引用类型的对象用&取地址就获得指针了。

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第13张图片

(g)野指针是什么?

        (g1) 野指针是指向不可用内存的指针,当指针被创建时,指针不可能自动指向NULL,这时,默认值是随机的,此时的指针成为野指针。

        (g2) 当指针被free或delete释放掉时,如果没有把指针设置为NULL,则会产生野指针,因为释放掉的仅仅是指针指向的内存,并没有把指针本身释放掉。

        (g3) 第三个造成野指针的原因是指针操作超越了变量的作用范围。

(h)如何避免野指针?
        (h1)对指针进行初始化。

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第14张图片

        (h2)指针用完后释放内存,将指针赋NULL。

        注:malloc函数分配完内存后需注意:

        1. 检查是否分配成功(若分配成功,返回内存的首地址;分配不成功,返回NULL。可以通过if语句来判断)

        2. 清空内存中的数据(malloc分配的空间里可能存在垃圾值,用memset或bzero 函数清空内存)

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第15张图片

(i)C++中的智能指针是什么?

        智能指针是一个类,用来存储指针(指向动态分配对象的指针)。

        C++程序设计中使用堆内存是非常频繁的操作,堆内存的申请和释放都由程序员自己管理。程序员自己管理堆内存可以提高了程序的效率,但是整体来说堆内存的管理是麻烦的,C++11中引入了智能指针的概念,方便管理堆内存。使用普通指针,容易造成堆内存泄露(忘记释放),二次释放,程序发生异常时内存泄露等问题等,使用智能指针能更好的管理堆内存。

(j)智能指针的内存泄漏如何解决?

        为了解决循环引用导致的内存泄漏,引入了弱指针 weak_ptr , weak_ptr 的构造函数不会修改引用计数的值,从而不会对对象的内存进行管理,其类似一个普通指针,但是不会指向引用计数的共享内存, 但是可以检测到所管理的对象是否已经被释放,从而避免非法访问。

(k)this指针是什么?

        this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第16张图片

        (k1)this指针指向当前对象,可以访问当前对象的所有成员变量。包括private、protected、public。

        (k2)this指针是const指针,一切企图修改该指针的操作,如赋值(改变指向)、增减都是不允许的!

        (k3)this指针只有在成员函数中才有定义。因此,在创建一个对象后,也不能通过对象使用this指针。所以,我们也无法知道一个对象的this指针的位置(只有在成员函数里才有this指针的位置)。当然,在成员函数里,你是可以知道this指针的位置的(可以&this获得),也可以直接使用的。

        (k4)只有创建对象后,this指针才有意义。

        (k5)static静态成员函数不能使用this指针。原因静态成员函数属于类,而不属于某个对象,所以static静态成员函数压根就没有this指针。

        (k6)this在成员函数的开始执行前构造的,在成员函数的执行结束后清除。至于如何清除的,由编译器实现,程序员不关心。this是通过函数参数的首参数来传递的。

1.3 内存

1.3.1 C语言中内存分配的方式有几种?

(a)静态存储区分配

内存分配在程序编译之前完成,且在程序的整个运行期间都存在,例如全局变量、静态变量等。 (b)栈上分配

在函数执行时,函数内的局部变量的存储单元在栈上创建,函数执行结束时这些存储单元自动释放。局部变量、函数内参数都在栈上。

(c) 堆上分配  New开辟的空间在堆上

1.3.2 堆与栈有什么区别?

(a)申请方式

        栈的空间由操作系统自动分配/释放,堆上的空间手动分配/释放。

(b)申请大小的限制栈空间有限。

        在Wind  ows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在WINDOWS下,栈的大小是2M(也有的说是1M,总之是 一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示 overflow。因此,能从栈获得的空间较小堆是很大的自由存储区。堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用 链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

(c)申请效率

        栈由系统自动分配,速度较快。但程序员是无法控制的。 堆是由new分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便.

1.3.3 栈在C语言中有什么作用?

(a)C语言中栈用来存储临时变量,临时变量包括函数参数和函数内部定义的临时变量。函数调用中和函数调用相关的函数返回地址,函数中的临时变量,寄存器等均保存在栈中,函数调动返回后从栈中恢复寄存器和临时变量等函数运行场景。

(b)多线程编程的基础是栈,栈是多线程编程的基石,每一个线程都最少有一个自己专属的栈,用来存储本线程运行时各个函数的临时变量和维系函数调用和函数返回时的函数调用关系和函数运行场景。 操作系统最基本的功能是支持多线程编程,支持中断和异常处理,每个线程都有专属的栈,中断和异常处理也具有专属的栈,栈是操作系统多线程管理的基石。

1.3.4 C语言函数参数压栈顺序是怎样的?

先理解入栈和出栈:

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第17张图片

栈的范围是由   ss * 10H   至   ss * 10H + sp

(ss)指堆栈寄存器:存放堆栈段起始地址的高16位(即16进制下五个数的前四个数)。

(sp)指堆栈指针:用于存放栈顶的逻辑偏移地址。

栈的栈底指针不变,栈顶的指针随sp的改变而改变。由于栈的栈底地址是高地址,栈顶地址是低地址。所以当栈存入数据时,会先将sp减去存入数据的字节数,然后再将数据存入。反之,当栈取出数据时,会将数据取出后将sp加上取出数据的字节数。(例如,当sp=0800H,ss=2360H时,若此时加入20个字节的数据,那么就要将sp - 20,此时的栈顶就是ss * 10H + sp)。

注:所谓高地址与低地址,前面的地址称为低地址,后面的地址称为高地址,例如23600H23E00H,此时23600H为低地址,23E00H为高地址

回答问题:从右至左。

        C语言参数入栈顺序的好处就是可以动态变化参数个数。自左向右的入栈方式,最前面的参数被压在栈底。除非知道参数个数,否则是无法通过栈指针的相对位移求得最左边的参数。这样就变成了左边参数的个数不确定,正好和动态参数个数的方向相反。因此,C语言函数参数采用自右向左的入栈顺序,主要原因是为了支持可变长参数形式

例如:    printfconst char* format,…

  1. printf函数是一个不定参函数。
  2. 编译器通过format的%占位符的个数来获取参数的个数。
  3. 假设函数压栈顺序是从左至右,format先入栈,各个参数再入栈,最后pc入栈。入栈完之后,想知道参数的个数就要读取format,但要读取format就得知道参数的个数,陷入了一个死循环。
  4. 但是,如果函数压栈顺序是从右至左,未知个数的参数先入栈,format再入栈,最后压pc入栈。这时候要想知道参数的个数只需要将栈顶指针加2即可读取到format

1.3.5 C++的内存管理是怎样的?

        在C++中,虚拟内存分为代码段、数据段、BSS段、堆区、文件映射区以及栈区六部分。

        代码段:包括只读存储区和文本区,其中只读存储区存储字符串常量,文本区存储程序的机器代码。

        数据段:存储程序中已初始化的全局变量和静态变量

        BSS段:存储未初始化的全局变量和静态变量(局部+全局),以及所有被初始化为0的全局变量和静态变量。

        堆区:调用new/malloc函数时在堆区动态分配内存,同时需要调用delete/free来手动释放申请的内存。

        映射区:存储动态链接库以及调用mmap函数进行的文件映射

        栈:使用栈空间存储函数的返回地址、参数、局部变量、返回值

1.3.6 什么是内存泄漏?

        简单地说就是申请了一块内存空间,使用完毕后没有释放掉。

        它的一般表现方式是程序运行时间越长,占用内存越多,最终用尽全部内存,整个系统崩溃。由程序申请的一块内存,且没有任何一个指针指向它,那么这块内存就泄露了。

1.3.7 如何判断内存泄漏?

        1. 良好的编码习惯,尽量在涉及内存的程序段,检测出内存泄露。当程式稳定之后,在来检测内存泄露时,无疑增加了排除的困难和复杂度。使用了内存分配的函数,一旦使用完毕,要记得要使用其相应的函数释放掉。

        2. 将分配的内存的指针以链表的形式自行管理,使用完毕之后从链表中删除,程序结束时可检查改链表。

        3. Boost 中的smart pointer。

        4. 一些常见的工具插件,如ccmalloc、Dmalloc、Leaky等等

1.3.8  new/delete与malloc/free的区别是什么?

        在C++中,申请动态内存和释放动态内存,用new/delete 和 malloc/free都可以,new和malloc动态申请的内存都位于堆中,无法被操作系统回收,需要对应的delete/free来释放空间。

        void *malloc(int size);

        说明:malloc向系统申请分配指定size个字节的内存空间。返回类型是 void* 类型。void* 表示未确定类型的指针。C,C++规定,void* 类型可以强制转换为任何其它类型的指针。

        对于类的对象而言,malloc/free无法满足动态对象的要求,对象在创建时要自动执行构造函数,在对象消亡之前要自动执行析构函数,而malloc/free 不在编译器控制权限之内,无法执行构造函数和析构函数。

        当然对于没有资源要清理的类,不调用析构函数也没有太大的问题,即使用free或delete没有区别。但万一有一些类的成员是指针,而这个指针又在堆上开辟了空间,这时不调用析构函数去释放这个指针指向的这段空间,就会造成内存泄漏。delete会调用析构函数,释放指针成员变量的空间,再销毁对象本身的空间;而free只释放了对象本身的空间,而指针成员所指向的空间没有被释放

        1)new 能够自动计算需要分配的内存空间,而malloc需要手工计算字节数。

        2) new与delete带具体类型的指针,malloc与free返回void类型的指针。

        3)new 将调用构造函数,而malloc不能;delete将调用析构函数,而free不能。

        4)malloc/free 需要库文件支持,而new/delete不需要库文件支持。

        5)new操作可以重载,可以自定义内存分配策略,不做内存分配,或者分配到非内存设备上。而malloc不能。

        delete和free被调用后,内存不会不会立即收回,指针也不会指向空,delete或free仅仅是告诉操作系统,这一块内存被释放啦,还可以做其他用途。由于没有对这块内存进行写操作,所以内存中的变量数值并没有发生变化,出现野指针的情况,因此,释放完内存后需要将指针向量置为空。

1.4 预处理

 1.4.1 预处理器标识#error的目的是什么?

        #error预处理指令的作用是,编译程序时,只要遇到#error就会生成一个编译错误提示消息,并停止编译。其语法格式为:#error error-message。

        下面举个例子: 程序中往往有很多的预处理指令

        当程序比较大时,往往有些宏定义是在外部指定的(如makefile),或是在系统头文件中指定的,当你 不太确定当前是否定义了 XXX 时,就可以改成如下这样进行编译:

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第18张图片

        这样,如果编译时出现错误,输出了XXX has been defined,表明宏XXX已经被定义了。

1.4.2 如何使用 define声明个常数,用以表明1年中有多少秒(忽略闰年问题)

        考虑到可能存在数据溢出问题,更加规范化的写法是使用长整型类型,即UL类型,告诉编译器这个常数是长整型数。

1.4.3  # include< filename. h>和#include" filename. h"有什么区别?

        对于 include< filename. h>,编译器先从标准库路径开始搜索filename.h,使得系统文件调用较快。而 对于# include“ filename.h”,编译器先从用户的工作路径开始搜索filename.h,然后去寻找系统路径,使得自定义文件较快。

1.4.4 头文件的作用有哪些?

        头文件的作用主要表现为以下两个方面:

        1. 通过头文件来调用库功能。出于对源代码保密的考虑,源代码不便(或不准)向用户公布,只要向用户提供头文件和二进制的库即可。用户只需要按照头文件中的接口声明来调用库功能,而不必关心接口是怎么实现的。编译器会从库中提取相应的代码。

        2. 头文件能加强类型安全检查。当某个接口被实现或被使用时,其方式与头文件中的声明不一致,编译器就会指出错误,大大减轻程序员调试、改错的负担。

1.4.5 在头文件中定义静态变量是否可行,为什么?

        不可行,如果在头文件中定义静态变量,会造成资源浪费的问题,同时也可能引起程序错误。因为如果 在使用了该头文件的每个C语言文件中定义静态变量,按照编译的步骤,在每个头文件中都会单独存在一个静态变量,从而会引起空间浪费或者程序错误所以,不推荐在头文件中定义任何变量,当然也包括静态变量

1.4.6 写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个

1.5 其他C语言面试问题

1.5.1 C语言宏中“#”和“##”的用法

(a)“#”字符串化操作符

        作用:#可以把一个宏参数直接转换成相应的字符串。比如有下面这个宏:

        则进行如下调用:

        最后其执行效果如下面程序一样:

        也就是说,最后输出的是宏参数的参数名。即:将宏参数直接转换成相应得字符串

(b)“##”符号连接操作符

作用:将宏定义的多个形参转换成一个实际参数名。

则下面第一个图的代码和第二个图的代码等价:

1.5.2 extern”C” 的作用是什么?

        extern "C"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern "C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第19张图片

1.5.3 strlen("\0") =? sizeof("\0")=? 两者结果与区别

        strlen("\0") =0,sizeof("\0")=2。

        strlen用来计算字符串的长度(在C/C++中,字符串是以"\0"作为结束符的),它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描直到碰到第一个字符串结束符\0为止,然后返回计数器值。sizeof是C语言的关键字,它以字节的形式给出了其操作数的存储大小,操作数可以是一个表达式或括在括号内的类型名,操作数的存储大小由操作数的类型决定。

1.5.4 C语言中 struct与 union的区别是什么?

        struct(结构体)与 union(联合体)是C语言中两种不同的数据结构,两者都是常见的复合结构,其区 别主要表现在以下两个方面。

        (a)结构体与联合体虽然都是由多个不同的数据类型成员组成的,但不同之处在于联合体中所有成员共 用一块地址空间,即联合体只存放了一个被选中的成员,而结构体中所有成员占用空间是累加的, 其所有成员都存在,不同成员会存放在不同的地址。在计算一个结构型变量的总长度时,其内存空 间大小等于所有成员长度之和(需要考虑字节对齐),而在联合体中,所有成员不能同时占用内存空间,它们不能同时存在,所以一个联合型变量的长度等于其最长的成员的长度。

        (b)对于联合体的不同成员赋值,将会对它的其他成员重写,原来成员的值就不存在了,而对结构体的 不同成员赋值是互不影响的。

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第20张图片

        假设为32位机器,int型占4个字节, double型占8个字节,char型占1个字节,而DATE是一个联合型变 量,联合型变量共用空间,uion里面最大的变量类型是int[5],所以占用20个字节,它的大小是20,而 由于 union中 double占了8个字节,因此 union是要8个字节对齐,所占内存空间为8的倍数。为了实现 8个字节对齐,所占空间为24.而data是一个结构体变量,每个变量分开占用空间,依次为 sizeof(int) + sizeof(DATE)+ sizeof( double)=4+24+8=36按照8字节对齐,占用空间为40,所以结果为 40+24=64。

1.5.5 左值和右值是什么?

        左值是指可以出现在等号左边的变量或表达式,它最重要的特点就是可写(可寻址)。也就是说,它的值可以被修改,如果一个变量或表达式的值不能被修改,那么它就不能作为左值。

        右值是指只可以出现在等号右边的变量或表达式。它最重要的特点是可读。一般的使用场景都是把一个右值赋值给一个左值。通常,左值可以作为右值,但是右值不一定是左值。

1.5.6 有符号数和无符号数的运算?   

        int a = -20, unsigned int b = 6,a+b是否大于6?

       有符号和无符号运算,强制转换为无符号,所有a+b会变成(unsigned int)a+b;

       (unsigned int)a 就会相当于无符号最大值-20,那么是一个非常大的值,这个值加上6,那么肯定是大于6的;

       最后的值是2^32-20+6=4294967282,肯定大于6

1.5.7 什么是短路求值?

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第21张图片

        输出结果为1。输出为什么不是2,而是1呢?

        其实,这里就涉及一个短路计算的问题。由于i语句是个条件判断语句,里 面是有两个简单语句进行或运算组合的复合语句,因为或运算中,只要参与或运算的两个表达式的值都为真,则整个运算结果为真,而由于变量i的值为6,已经大于0了,而该语句已经为true,则不需要执行后续的j+操作来判断真假,所以后续的j++操作不需要执行,j的值仍然为1。

        因为短路计算的问题,对于&&操作,由于在两个表达式的返回值中,如果有一个为假则整个表达式的值 都为假,如果前一个语句的返回值为 false,则无论后一个语句的返回值是真是假,整个条件判断都为 假,不用执行后一个语句,而a>b的返回值为 false,程序不执行表达式n=c>d,所以,n的值保持为初值2。

1.5.8 什么是大端和小端?

        大端:高地址存低字节,低地址存高字节
        小端:低地址存低字节,高地址存高字节

1.5.9 ++a和a++有什么区别?两者是如何实现的?

        a++的具体运算过程为: 先用a,再执行++

【八股文】嵌入式软件工程师-2025校招必备-详细整理_第22张图片

        ++a的具体运算过程为:  先执行++,再用a

1.6 C++部分

1.6.1 C++中类成员的访问权限?

        无论成员被声明为 public、protected 还是 private,都是可以互相访问的,没有访问权限的限制。在类的外部 (定义类的代码之外),只能通过对象访问成员,并且通过对象只能访问 public 属性的成员,不能访问 private、protected 属性的成员。

        (a)protected:受保护的,类内和子类可直接访问,也就是说,基类中有protected成员,子类继承于基类,那么也可以访问基类的protected成员,要是基类是private成员,则对于子类也是隐藏的,不可访问。

        (b)private:私有的,只有类内的成员函数才可以访问。

1.6.2 什么是构造函数?   

        构造函数是一种特殊的函数,用于创建和初始化对象。它在创建对象时被调用,用于设置对象的初始状态和属性。构造函数的名称通常与类的名称相同,且没有返回类型声明。

        构造函数可以有多个重载版本,每个版本允许接受不同类型和数量的参数。通过调用不同的构造函数,可以根据需要创建不同种类的对象。

        构造函数的主要功能包括:

        (a)分配内存空间:构造函数负责为对象分配足够的内存空间,以存储对象的数据成员。

        (b)初始化对象:构造函数可以对对象的数据成员进行初始化,确保对象的属性处于正确的初始状态。

        (c)设置默认值:构造函数可以为对象的属性设置默认值,以避免对象在创建时出现未定义的行为。

        在C++中,构造函数名称与类名称相同,没有返回类型声明,并且可以是公有、私有或受保护的。当创建对象时,会自动调用适当的构造函数来初始化对象。如果未明确定义构造函数,编译器将提供一个默认的无参数构造函数。

1.6.3 构造函数的分类是怎样的?

        (1)无参构造    Person( ) {}

        (2)有参构造      Person(int a) {}

        (3)拷贝构造函数     Person( const Person& p) {}

1.6.4 构造函数的调用规则是怎样的?

     C++编译器至少给一个类添加3个函数

        (a)默认构造函数(无参)

        (b)默认析构函数(无参)

        (c)默认拷贝构造函数,对属性进行值拷贝

                如果用户定义了有参构造函数,C++不再提供默认无参构造,但是会提供默认拷贝构造;如果用户定义拷贝构造函数,C++不会再提供其他构造函数。

1.6.5 什么是析构函数? 

        需要自己定义构造函数和析构函数的情况有以下几种:

        (a)当需要在对象创建时进行一些初始化操作时,可以定义构造函数来实现。比如,需要在对象创建时给成员变量赋初值或者打开一些资源。

        (b)当需要在对象销毁时进行一些清理操作时,可以定义析构函数来实现。比如,需要在对象销毁时释放一些资源或者关闭一些文件。

        (c)当需要控制对象的生命周期时,可以定义构造函数和析构函数来实现。比如,需要在对象创建时进行一些操作,在对象销毁时进行一些清理操作,这样可以确保对象的正确使用。

        总之,需要自己定义构造函数和析构函数的情况主要是为了实现一些特定的需求,比如初始化、清理、控制对象的生命周期等。

        注意:构造函数可以有参数,因此可以重载,析构函数不能有参数,因此不可以发生重载。

1.6.6 引用注意事项?       

        引用格式:   数据类型 &别名 = 原名

        引用必须初始化;引用在初始化后不可以改变;函数传参时,可以利用引用让形参修饰实参;引用可以作为函数的返回值,但是不要返回局部变量。引用的本质在C++内部实现一个指针常量。

1.6.7 函数重载是什么?

        重载满足条件:同一个作用域下;函数名称相同;函数参数类型不同或者个数不同或者顺序不同。

1.6.8 什么是深拷贝与浅拷贝?

浅拷贝:简单的赋值拷贝操作

深拷贝:在堆区重新申请空间,进行拷贝操作

       当在类里面涉及到指针操作时,如果采用浅拷贝,则执行拷贝构造函数后。会导致拷贝出两个指针指向同一个内存空间,则进行析构函数时,就会对该空间释放两次,然后导致报错。因此需要进行深拷贝,对于指针重新再开辟一段空间。

1.6.9 静态成员归纳

(a)静态成员变量:

       所有对象共享同一份数据; 在编译阶段分配内存;类内声明,类外初始化

(b)静态成员函数:

       所有对象共享同一个函数;静态成员函数只能访问静态成员变量

(c)关于两者内存:

       如果只声明了类而未定义对象,则类的一般成员变量是不占用内存空间的,只有在定义对象的时候,才为对象的成员变量分配空间。

       静态成员不占用类内空间;静态成员函数在类内声明,类外初始化。

1.7.0  继承是什么?

        语法:  class 子类 : 继承方式 父类     比如:class A :public B

       多继承语法:   

                  class 子类 : 继承方式   父类1,继承方式 父类1

       继承过程中,父类中的私有成员也被子类继承,只是由编译器隐藏后访问不到。

        继承同名成员处理方式:

        子类对象可以直接访问到子类中同名成员

       子类对象加作用域可以访问到父类同名成员

       当子类与父类拥有同名的成员函数,子类会隐藏父类中同名成员函数,加作用域可以访问到父类中同名函数。

1.7.1 菱形继承是什么?

        两个派生类继承同一个基类;又有某个类同时继承了两个派生类;这种继承称为菱形继承。

       羊继承了动物数据;马继承了动物数据;草泥马继承了羊和马的数据,则动物数据被继承了两份。

       采用虚继承的方法解决该问题。

1.7.2 虚函数是什么?

        虚函数只能是类的成员函数, 而不能将类外的普通函数声明为虚函数. 虚函数的作用是允许在派生类中对基类的虚函数重新定义 (函数覆盖), 只能用于类的继承层次结构中.

        虚函数能有效减少空间开销. 当一个类带有虚函数时, 编译系统会为该类构造一个虚函数表 (一个指针数组), 用于存放每个虚函数的入口地址.

        什么时候应该使用虚函数:

        判断成员函数所在的类是不是基类, 非基类无需使用虚函数

        成员函数在类被继承后有没有可能被更改的功能, 如果希望修改成员函数功能, 一般在基类中将其声明为虚函数;

        我们会通过对象名还是基类指针访问成员函数, 如果通过基类指针过引用去访问, 则应当声明为虚函数

1.7.3 静态函数和虚函数的区别?

        多态分为两类:静态多态和动态多态

       静态多态:函数重载和运算符重载属于静态多态、

       虚函数因为用了虚函数表机制,调用的时候会增加一次内存开销。

1.7.4 什么是多态?

        多态是指不同继承关系的类对象,去调用同一函数,产生了不同的行为。在继承中要想构成多态需要满足两个条件:

       (a)必须通过基类的指针或者引用调用虚函数

        (b)被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

1.7.5 纯虚函数是什么?

        纯虚函数不需要在父类中实现,必须在子类中实现

        在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容,因此可以将虚函数改为纯虚函数。

       纯虚函数语法:

                virtual 返回值类型 函数名 (参数列表) = 0

               当类中有了纯虚函数,这个类也称为抽象类

        抽象类特点:

               无法实例化对象

               子类必须重写抽象类中的纯虚函数,否则也属于抽象类。

1.7.6 重载和覆盖有什么区别?

        (a)覆盖是子类和父类之间的关系,垂直关系;重载同一个类之间方法之间的关系,是水平关系。

        (b)覆盖只能由一个方法或者只能由一对方法产生关系;重写是多个方法之间的关系。

        (c)覆盖是根据对象类型(对象对应存储空间类型)来决定的;而重载关系是根据调用的实参表和形参表来选择方法体的。

1.7.7 析构函数可以为 virtual 型,构造函数则不能,为什么?

        虚函数的主要意义在于被派生类继承从而产生多态。派生类的构造函数中,编译器会加入构造基类的代码,如果基类的构造函数用到参数,则派生类在其构造函 数的初始化列表中必须为基类给出参数,就是这个原因。虚函数的意思就是开启动态绑定,程序会根据对象的动态类型来选择要调用的方法。然而在构造函数运行的时候,这个对象的动态类型还不完整,没有办法确定它到底是什么类型,故构造函数不能动态绑定。

每天会不断更新。。。。。

你可能感兴趣的:(嵌入式,嵌入式硬件,求职招聘,linux,arm开发,c++)