嵌入式八股汇总

1.1.1 volatile关键字

volatile的意思是”易变的”,这个关键字eeeeeeeeeeee+

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

(a)关于编译器优化

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

如下面程序段:

1.  int a, b; // 为a,b申请内存
    
2.  a = 1;    // 1 -> CPU
    
3.            // CPU -> 内存(&a)
    
4.  b = a;    // 内存(&a) -> CPU
    
5.            // CPU -> 内存(&b)
    

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

1.  int a = 1, b, c; // 为a,b,c申请内存并初始化
    
2.  b = a;    // 内存(&a) -> CPU
    
3.            // CPU -> 内存(&b)
    
4.  c = a;    // * 内存(&a) -> CPU *
    
5.            // 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。这样就防止了被优化。

1.  volatile int a = 1, b, c; // 为a,b,c申请内存并初始化
    
2.  b = a;    // 内存(&a) -> CPU
    
3.            // CPU -> 内存(&b)
    
4.  c = a;    // 内存(&a) -> CPU
    
5.            // 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)定义变量(局部变量或全局变量)为常量,例如:

img

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

(b)修饰指针

img

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

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

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

img

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

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

img

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

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

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

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

(c)修饰函数的参数

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

(d)修饰函数的返回值

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

img

(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有自己的作用域。

img

(d)对指针的操作不同

img

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

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

img

上述代码中, 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型元素的数组。下面给出一个数组 指针的示例。

img

程序的输出结果为 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)指针数组

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

img

程序的输出结果为1234。

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

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

1.2.3 指针

(a)函数指针

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

img

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

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

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

img

img

(b)指针函数

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

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

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

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

img

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

img

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

img img

共有三个学生的成绩,函数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字节对齐,以如下结构体为例:

img

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

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

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

img

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

** (e2)常量指针**

img

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

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

img

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

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

** (f1)相同**

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

** (f2)区别**

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

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

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

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

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

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

img img

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

** (f3) 转换**

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

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

img

(g)野指针是什么?

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

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

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

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

img

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

img

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

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

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

img

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

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

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

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

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

(k)this指针是什么?

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

img

(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语言函数参数压栈顺序是怎样的?

先理解入栈和出栈:

img

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

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

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

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

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

**回答问题:**从右至左。

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

例如: printf``(``const 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、递归函数定义没有问题,递归深层次后易引发什么问题?

(1)影响执行效率

(2)栈溢出。

因为每一次调用函数是,栈区都要给函数分配空间,而且上一次调用并没有结束,调用的次数太多,栈区的内存不够分配了,便会出现栈溢出的情况。

2、堆与栈的区别?

(1)栈的空间是系统自动分配和回收,堆的空间是用户手动分配回收(malloc,calloc,realloc,free)

(2)栈的空间较小,堆的空间较大

(3)栈的地址空间往地址向下增长,堆的地址空间是由低地址到高地址

(4)栈的存储效率更高

3、循环控制条件关键字goto被经常使用,但是goto的使用场合为什么受到局限?

因为goto会破坏程序的栈逻辑。

4、循环控制条件关键字goto的使用场景有哪些?

(1)常用来跳出死循坏;

(2)打印错误;

(3)goto被经常使用,只是使用场合受到局限,因为他会破坏程序的栈逻辑。

5、字节对齐的理解

5.1 什么是字节对齐?

字节对齐主要是针对结构体而言的,通常编译器会自动对其成员变量进行对齐,以提高数据存取的效率;

5.2 字节对齐的两种方式

默认对齐方式、指定对齐方式;

(1)默认对齐方式内存分配满足以下三个条件:

  • 结构体第一个成员的地址和结构体的首地址相同;
  • 结构体每个成员地址相对于结构体首地址的偏移量(offset)是该成员大小的整数倍,如果不是则编译器会在成员之间添加填充字节;
  • 结构体总的大小要是其成员中最大size的整数倍,如果不是编译器会在其末尾添加填充字节。

如char是1字节,short是2字节,int是4字节...

(2)指定对齐方式使用以下方式声明:

1.  //注:通过#pragma pack(n)改变C编译器的字节对齐方式
    
2.  #pragma pack(4)         //安装4字节的对齐方式
    

指定对齐方式内存分配满足以下几个条件:

  • 结构体第一个成员的地址和结构体的首地址相同
  • 结构体每个成员的地址偏移需要满足:N大于等于该成员的大小,那么该成员的地址偏移需满足默认对齐方式(地址偏移是其成员大小的整数倍);N小于该成员的大小,那么该成员的地址偏移是N的整数倍。
  • 结构体总的大小需要时N的整数倍,如果不是需要在结构体的末尾进行填充。
  • 如果N大于结构体成员中最大成员的大小,则N不起作用,仍然按照默认方式对齐。

注:在使用#pragma pack设定对齐方式一定要是2的整数幂,也就是(1,2,4,8,16,…),不然不起作用的,仍然按照默认方式对齐。

例1:结构体使用字节对齐为1

 `2.  // date:2022年 11月 08日 星期二 19:35:36 CST
    
3.  // author: HeiBaiYe
    
4.  // path: /mnt/hgfs/CD2206/02-c语言
    
5.  #include <stdio.h>
    

7.  #pragma pack(1)     //通过#pragma pack(n)改变C编译器的字节对齐方式 在C语言中,结构是一种复合数据类型
    
8.  struct s1{
    
9.      char ch;    // 1
    
10.     int a;      //4
    
11.     double b;   //8
    
12.     char c1;    //1
    
13. };
    

15. #pragma pack(1) 
    
16. struct s2{
    
17.     char ch;    //1
    
18.     int a;      //4
    
19.     double b;   //8
    
20. };
    

22. int main()
    
23. {
    
24.     printf("s1的大小:%ld\n ",sizeof(struct s1));
    
25.     printf("s2的大小:%ld\n ",sizeof(struct s2));
    

27.     return 0;
    
28. }`

![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

结果: s1的大小:14 s2的大小:13

例2:结构体使用默认字节对齐方式,m值

`

1.  // date:2022年 11月 08日 星期二 19:35:36 CST
    
2.  // author: HeiBaiYe
    
3.  // path: /mnt/hgfs/CD2206/02-c语言
    
4.  #include <stdio.h>
    
5.  struct s1{
    
6.      char ch;    // 1
    
7.      int a;        //4
    
8.      double b;    //8
    
9.      char c1;    //1
    
10. };
    

12. struct s2{
    
13.     char ch;    //1
    
14.     int a;        //4
    
15.     double b;    //8
    
16. };
    

18. int main()
    
19. {
    
20.     printf("s1的大小:%ld\n ",sizeof(struct s1));
    
21.     printf("s2的大小:%ld\n ",sizeof(struct s2));
    

23.     return 0;
    
24. }
    

`

![](https://csdnimg.cn/release/blogv2/dist/pc/img/newCodeMoreWhite.png)

结果: s1的大小:24 s1的大小:16

参考链接:https://blog.csdn.net/wdl20170204/article/details/109386825

6、局部变量和全局变量可以重名吗?

(1)能,局部变量会屏蔽全局变量。C++中要用全局变量,需要使用 "::"(域解析符) 。C语言中局部变量可以与全局变量同名,在函数内引用这个变量时,会用到同名的局部变量,而不会用到全局变量。

(2)对于有些编译器而言,在同一个函数内可以定义多个同名的局部变量,比如在两个循环体内都定义一个同名的局部变量,而那个局部变量的作用域就在那个循环体内。

7、UNIX系统中fsync函数的作用?

fsync()负责将参数fd 所指的文件数据, 由系统缓冲区写回磁盘, 以确保数据同步。

头文件:#include

定义函数:int fsync(int fd);

函数说明:fsync()负责将参数fd 所指的文件数据, 由系统缓冲区写回磁盘, 以确保数据同步.

返回值:成功则返回0, 失败返回-1, errno 为错误代码。

参考链接:https://blog.csdn.net/Michaelwubo/article/details/41210547

8、const关键字使用有哪些?

8.1 修饰变量

const的 常规用法,在变量初次定义时赋初,并用关键字const修饰,使变量只可访问,不能重新赋值修改变量。

8.2 修饰指针

(1)限制指针变量修饰:指针变量指向的位置不能被修改。定义时,被 const 修饰的指针变量指针只能在定义时初始化,不能定义之后重新指向新的数据。

(2)限制指针变量指向的数据修饰【指针的解引用】:修饰的指针变量指向的变量的值不能被修改,但是该指针可以指向其它空间。

(3)同时限制指针变量和指针变量指向的变量的值修饰:指针变量指向的位置不能被修改,并且指针变量指向变量的值也不能被修改。

(4)修饰函数形参【指针】:函数形参可以利用const关键字进行限制,来防止在函数内部修改指针指向的数据。

9、内存布局中有哪些段?

文本段(.text)、数据段(.data)、.bss段、堆(heap)、栈(stack)

img

图 虚拟空间的各个部分

10、volatile关键字的作用?

(1)裸机编程时,某变量是指向寄存器中某一特定地址,添加volatile的变量不进行优化处理;

(2)某函数与中断函数共享全局变量时,加上volatile,让编译器不要省略该变量的访问;

(3)多线程中修饰共享全局变量,让编译器不要省略该变量的访问。

11、sizeof()与strlen()的区别?

(1)sizeof是运算符,计算能容纳实现所建立的最大对象的字节大小,参数可以是数组、指针、类型、对象、函数等;

(2)strlen是函数,功能是返回字符串的长度,参数必须是字符型指针(char*)。

12、内存泄漏和内存溢出是什么?

(1)内存溢出:指程序申请内存时,没有足够的内存供申请者使用。或者说,给了你一块存储int类型数据的存储空间,但是你却存储long类型的数据,那么结果就是内存不够用,此时就会报错Out Of Memory,即所谓的内存溢出。

(2)内存泄漏:是指程序在申请内存后,无法释放已申请的内存空间。一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出。

13、定义一个指针赋值字符串与定义一个数组赋值字符串有什么区别?

(1)指针赋值字符串是指向一定内存的指针,只不过是指向字符串常量的指针,指针中的数据不能修改。

(2)数组赋值字符串是一片char型的数组,可以理解为缓冲区,只不过是赋值为了字符串。

14、malloc()与calloc分配空间有什么不一样?

(1)malloc申请后空间的值是随机的,并没有进行初始化;而calloc却在申请后,对空间逐一进行初始化,并设置值为0;

(2)malloc要申请的空间大小,需要我们手动的去计算;calloc并不需要人为的计算空间的大小。

15、实现循环的方式?

while、for 、do while 、goto 循环。

16、全局变量和局部变量在内存中有什么不同?

(1)全局变量保存在内存的全局存储区中,占用静态的存储单元;

(2)局部变量保存在栈中,只有在所在函数被调用时才动态地为变量分配存储单元。

17、预处理的作用是什么?

预处理器可以删除注释、包含其他文件以及执行宏(宏macro是一段重复文字的简短描写)替代。

18、编译器的作用?

编译器就是将一种语言(通常为高级语言)翻译为另一种语言(通常为低级语言)的程序。一个现代编译器的主要工作流程:源代码(.c)→ 预处理器(.i) → 编译器 (.s)→ 目标代码 (.o)→ 链接器 → 可执行程序 。

19、.ELF文件是什么?

.ELF是C语言在linux中的可执行文件。

20、C语言程序编译的流程是什么?

img

图 编译过程

(1)预处理:根据以字符#开头的命令修给原始的C程序,结果得到另一个C程序,通常以.i作为文件扩展名。主要是进行文本替换、宏展开、删除注释这类简单工作。

对应的命令:linu

gcc -E hello.c hello.i

(2)编译:编译器将文本文件hello.i翻译成hello.s,包含相应的汇编语言程序。

对应的命令:linu

gcc -S hello.c hello.s

(3)汇编:将.s文件翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件.o中(把汇编语言翻译成机器语言的过程)。

把一个源程序翻译成目标程序的工作过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。

对应的命令:linu

gcc -c hello.c hello.o

(4)链接:将静态库和动态库的库函数连接到可执行程序中。静态库是指编译链接时,把库文件的代码全部加入到可执行文件中,因此生成的文件比较大,但在运行时也就不再需要库文件了。其后缀名一般为.a。动态库与之相反,在编译链接时并没有把库文件的代码加入到可执行文件中,而是在程序执行时由运行时链接文件加载库,这样可以节省系统的开销。动态库一般后缀名为.so,gcc在编译时默认使用动态库。

原文链接:https://blog.csdn.net/daide2012/article/details/73065204

21、如何用C语言实现C++的类?

(1)由于C语言是面向过程,而C++是面向对象,所以在定义数据时,可以用C的结构体成员充当C++类的成员定义;

(2)由于结构体只能定义变量,不能够定义函数,所以通过函数指针的方法来实现其类函数的定义。

参考链接:https://blog.csdn.net/forever__1234/article/details/61429870

1.1 关键字

(参考”嵌入式及Linux那些事“以及众多帖子汇总而成)

volatile

当声明指向设备寄存器的指针时一定要用volatile,它会告诉编译器不要对存储在这个地 址的数据进行假设。

中断服务程序中修改的供其他程序检测的变量。 中断中直接从变量地址中读取数据,而不是从寄存器中读取。

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

static

1、函数体内的变量,这个变量只被声明一次。

2、在模块内的变量,表示只能被模块内函数使用,不能被模块外函数访问,表示本地全局变量

3、模块内的函数,限制在模块内使用,同上。

extern

1、引用同一文件变量

使用在声明之前时,可以使用关键字extern,让声明在程序任意位置。

2、引用另一个文件中的变量

extern可以引用其他文件中的全局变量,而且extern只需要指明数据类型和

extern int num=4; 这样不行。

3、引用另一个文件中的函数

可以不用包含头文件引用函数。

new/delete malloc/free

1、new/delete是操作符,malloc/free是库函数

2、new/delete可以调用构造函数/析构函数,m/f 只是分配内存。

struct 和 union区别

1、联合体公用一块地址空间,联合体变量长度等于最长的成员的长度

2、对不同成员赋值,会将其他成员重写。

const

1、定义变量为常量

2、修饰参数为常量

3、修饰返回值为常量

总结:只读

sizeof和strlen

1、sizeof是运算符,strlen是函数

2、sizeof可以用类型、函数作为参数,strlen只能计���char*,还必须以/0结尾

3、sizeof编译的时候计算,strlen是运行期计算,表示字符串长度,不是内存大小。

typedef和 define

1、都是替对象去一个别名,增强程序的可读性

2、define为预处理指令,不做正确性检查,只有带入之后才能发现

3、typedef用来定义类型别名,不止包含内部类型还包含自定义类型(与机器无关),方便记忆

4、define不仅可以给类型取别名,还能定义常量、变量、编译开关。

5、define没有作用域限制,typedef有。

# define还是 const ,谁定义常量最好

1、define只是文本替换,声明周期止于编译期,不分配内存空间,存在于代码段。const常量存在于数据段,堆栈中分配了空间。

2、const有数据类型,编译器可以对const进行安全检查。

3、const有保护常量不被修改的作用,提高程序的健壮性。

总结:一般倾向于用const定义常量

1.2 内存

C语言内存分配方式

1、静态储存区分配

2、栈上分配

3、堆上分配

C++内存管理是怎样的

分为代码段、数据段、BSS段、堆区、栈区、文件映射区

代码段:分为只读区和文本区,只读取储存字符串常量,文本区储存机器代码。

数据段:储存以及初始化的全局变量和静态变量

BSS段:储存未初始化的全局变量和静态变量,以及初始化为0的全局和静态。

堆区:手动分配的内存

栈:局部变量参数返回值等

映射区:储存动态链接库,mmap函数的文件映射

堆和栈的区别

1、申请方式。 栈为操作系统自动分配/释放,堆为手动

2、申请大小,栈空间有限,向低地址拓展的连续区域,堆是向高地址拓展的不连续区域,链表储存的空闲地址。

3、申请效率,栈是系统自动分配,速度快,不可控。堆是由new分配,速度比较慢,容易产生内存碎片。

栈的作用

1、储存临时变量

2、多线程编程的基础。每个线程至少有一个栈用来存储临时变量和返回的值。

内存泄漏

申请了没有释放,由程序申请的一块内存,没有任何指针指向它,这个内存就泄露了。

避免内存泄漏方法

1、分配的内存以链表管理,使用完毕后从链表删除,程序结束的时候检查链表

2、良好的编程习惯,在设计内存的程序段,检验出内存泄漏,使用了内存分配的函数,使用完毕后将使用的相应函数释放掉

3、smart pointer

指针

数组指针和指针数组

int (*p)[20]; 数组指针,本质是一个指针,指向一个数组

int *p[20]; 指针数组,本质是一个数组,里面装的是指针。

函数指针和指针函数

1、函数指针 int(*p)(int,int);本身是一个指针,指向一个函数的地址

2、指针函数 int *p(int,int); 指针函数表示一个函数,返回数是指针。

数组名和指针区别

1、指针保存的是地址,数组保存的是数据,单数组名是第一个元素的地址

2、指针间接访问,数组直接下标或者偏移量

3、sizeof 有区别,指针为指针大小,数组为全体数据大小

指针常量,常量指针、指向常量的指针

1、int *const p 指向地址不变,地址值可变

2 int const *p 指向地址可变,地址值不能边

3、const int * const p 都不能变

指针与引用区别

1、都是地址,指针是地址,应用是别名

2、引用本质是指针常量,对象不变,对象的值可变

3、++不同,指针是地址自增,引用是对象自增

4、指针需要解引用

5、指针可为空,引用不行

6、sizeof不同 一个是指针大小一个是对象大小

野指针

1、指向不可用内存的指针,指针被创建时如果没有初始化就是野指针

2、指针被free、delete时没有指向NULL就是野指针

3、指针超出了变量的地址范围

智能指针

C++智能指针是指一个类,用来存储指针

1.3 预处理

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

1、遇到#error就会生成一个编译错误提示信息,并停止编译

define声明一年多少秒

#define SECOND_OF_PER_YEAR (3652460*60)UL

#include"" 和 include<>区别

<>号先搜索标准库搜索系统文件比较快,“”号先搜索工作路径搜索自定义文件比较快

头文件作用

1、通过文件调用库功能,源码保护

2、头文件加强类型安全检查,编译器报错

头文件定义静态变量

1、资源浪费,每个头文件都会单独存在一个静态变量

不使用流程控制语句,打印1~1000数字

`#include<stdio.h>
#define A(x) x;x;x;x;x;x;x;x;x;x;
int main()
{
    int n=1;
    A(A(A(printf("%d",n++))));
    return 0;
}` 

1.4 变量

全局变量和静态变量

1、全局变量作用域为程序块,局部变量为当前函数

2、全局变量储存在静态区,后者为栈

3、全局变量生命周期为主函数,局部变量生命周期在局部函数中,甚至循环体内

1.5 函数

写个函数在main函数执行前执行

1、attribute可以设置函数属性

`#include <stdio.h>
void before() __attribute__((constructor));
void after() __attribute__((destructor));
void before() {
  printf("this is function %s\n",__func__);
  return;
}
void after(){
  printf("this is function %s\n",__func__);
  return;
}
int main(){
  printf("this is function %s\n",__func__);
  return 0;
}
// 输出结果
// this is function before
// this is function main
// this is function after` 

为什么析构函数必须是虚函数

1、基类指针指向子类时,释放基类指针也能释放掉子类的空间,防止内存泄漏。

2、最好是作为父类的类的析构函数作为虚函数

为什么C++默认的析构函数不是虚函数?

1、虚函数有额外的虚函数表和虚指针表,占用额外的内存,对于那些不会被继承的类当然也不需要虚函数作为析构函数。

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

1、静态函数编译时确定运行时机

2、虚函数运行时动态绑定,并且使用虚函数表,内存开销增加

重载与覆盖

1、覆盖是子类和父类的关系,垂直关系,重载是一个类之间的关系,水平关系

2、覆盖一对一,重载多个方法

3、覆盖由对象类型决定,重载根据调用的参数表决定

虚函数表实现多态方法

原理: 虚函数表示一个类的地址表,子类创建时,按照函数声明吮吸会将函数的地址存在虚函数表中。子类重写父类虚函数的时候,父类虚函数表中的位置会被子类虚函数地址覆盖。

C语言函数调用方法

1、使用栈来支持函数调用操作,栈被用来传递参数,返回值,局部变量等。

2、函数调用主要操作栈帧结构

select函数

`int select(int maxfdp,fd_set *readfds,fd_set *writefds,fd_set *errorfds,struct timeval *timeout);` 

fork wait exec函数

1、附近产生的子进程使用fork拷贝出一个父进程的副本

数组的下标可以为负数吗?

可以,数组下标指地址偏移量,根据偏移量能定位得到目标地址。

inline函数和宏定义的区别

1、内联函数在编译时展开,而宏在预编译时展开。

2、在编译的时候,内联函数直接被嵌入到目标代码中去,而宏只是一个简单的文本替换。

3、内联函数可以进行诸如类型安全检查、语句是否正确等编译功能,宏不具有这样的功能。

4、宏不是函数,而inline是函数。

5、宏在定义时要小心处理宏参数,一般用括号括起来,否则容易出现二义性。而内联函数不会出现二义性。

6、inline可以不展开,宏一定要展开。因为inline指示对编译器来说,只是一个建议,编译器可以选择忽略该建议,不对该函数进行展开。

7、宏定义在形式上类似于一个函数,但在使用它时,仅仅只是做预处理器符号表中的简单替换,因此它不能进行参数有效性的检测,也就不能享受C++编译器严格类型检查的好处,另外它的返回值也不能被强制转换为可转换的合适的类型,这样,它的使用就存在着一系列的隐患和局限性。

宏和函数的优缺点

1、函数调用时,先求出实参表达式的值,然后带入形参。而使用带参数的函数只是进行简单的字符替换

2、函数调用实在程序运行时处理的,分配的临时的内存单元;而宏展开则是在编译时进行的,在展开时不分配i内存单元,不进行值的传递,也没有"返回值的概念"

3、函数实参形参都要定义类型,二者要求一致 ,宏不存在类型问题,宏没有类型,宏的参数只是一个符号代表,展开时代入指定的字符就行,宏定义时字串可以是任意内心的数据

4、函数只可以得到一个返回值,宏可以设法得到多个

5、使用宏次数多时,展开后源程序长,每次展开都使程序增长,而函数调用不使源程序变长。

6、宏的替换不占用时间,只占用编译时间,函数调用占用运行时间。

简单回答:宏由编译计算,增加编译时间,函数运行的时候计算,增加运行时间;函数的返回值入口参数有数据类型,宏只是简单的符号加减。

ASSERT()作用

ASSERT()是一个调试程序时经常使用的宏,在程序运行时它计算括号内的表达式,如果表达式为FALSE (0), 程序将报告错误,并终止执行。如果表达式不为0,则继续执行后面的语句。

strcpy()和memcpy()的区别

1、复制的内容不同。strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等。 2、复制的方法不同。strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy则是根据其第3个参数决定复制的长度。 3、用途不同。通常在复制字符串时用strcpy,而需要复制其他类型数据时则一般用memcpy

1.6 位操作

求解整型类二进制表示1的个数

`int func(int x)
{
    int countx = 0;
    while(x)
    {
        countx++;
        x = x&(x-1);
    }
    return countx;
}` 

求解整型类二进制表示0的个数

`int CountZeroBit(int num)
{
    int count = 0;
        while (num + 1)
        {
            count++;
            num |= (num + 1); //算法转换
        }
    return count;
}` 

给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在 以上两个操作中,要保持其它位不变。

`void clearbit3(int a)
{
    a&=~(1<<3);
}
void setbit3(int a)
{
    a|=1<<3;
}` 

1.11数据结构

链表和数组的区别

  1. 数组在内存中栈上按顺序存储的,而链表是在堆上随机存储的。
  2. 要访问数组中的元素可以按下标索引来访问,速度比较快,如果对他进行插入操作的话,就得移动很多元素,所以对数组进行插入操作效率很低. 由于连表是随机存储的,链表在插入,删除操作上有很高的效率(相对数组)
  3. 如果要访问链表中的某个元素的话,那就得从链表的头逐个遍历,直到找到所需要的元素为止,所以链表的随机访问的效率就比数组要低

2、ARM体系与架构

2.1 硬件基础

NAND FLASH 和NOR FLASH异同?

类别 读 写 擦除 可靠性 容量 用途 价格

NOR 快 慢 非常慢 比较高 小 保存代码 高

NAND 快 快 快 低 大 保存数据 低

CPU,MPU,MCU,SOC,SOPC联系与差别?

1、CPU:是一台计算机的运算核心和控制核心

2、MPU: 微处理器稍强的CPU

3、MCU:将计算机的CPU、RAM、ROM、定时计数器和多种I/O接口集成在一片芯片上。

4、SOC: 系统级芯片不单单是放简单的代码,可以放系统级的代码,也就是说可以运行操作系统

CPU中cache的作用?cache的基本组织结构?

(1)高速缓冲存储器Cache是位于CPU与内存之间的临时存储器,它的容量比内存小但交换速度快。

在Cache中的数据是内存中的一小部分,但这一小部分是短时间内CPU即将访问的,当CPU调用大量数据时,就可避开内存直接从Cache中调用,从而加快读取速度。由此可见,在CPU中加入Cache是一种高效的解决方案,这样整个内存储器(Cache+内存)就变成了既有Cache的高速度,又有内存的大容量的存储系统了。 (2)全相连映射,直接映射,组相连映射

交叉编译

在一种计算机环境中运行的编译程序,能编译出在另外一种环境下运行的代码,我们就称这种编译器支持交叉编译。这个编译过程就叫交叉编译。

C/C++的编译包括几个部分

1、预编译:预处理器对c程序进行一些预处理工作,例如对宏定义的变量进行替换;

1)将所有的#define删除,并展开所有的宏定义;

2)处理所有的预编译指令,例如:#if,#elif,#else,#endif;

3)处理#include预编译指令,将被包含的文件插入到预编译指令的位置;

4)添加行号信息文件名信息,便于调试;

5)删除所有的注释:// /**/;

6)保留所有的#pragma编译指令,因为在编写程序的时候,我们经常要用到#pragma指令来设定编译器的状态或者是指示编译器完成一些特定的动作;

最后生成.i文件;

总的来说,包括(1)去注释 (2)宏替换 (3)头文件展开 (4)条件编译

2、编译:编译器将c语言程序翻译成汇编语言程序;

1)扫描,语法分析,语义分析,源代码优化,目标代码生成,目标代码优化;

2)生成汇编代码;

3)汇总符号;

4)生成.s文件;

3、汇编:汇编语言通过汇编器编译成可重定位目标程序.o,与之相反称为反汇编;

1)根据汇编指令和特定平台,把汇编指令翻译成二进制形式;

2)合并各个section,合并符号表;

3)生成.o文件;

4、链接:将目标文件和所需的库函数用链接器进行链接,常见的链接器有Unix;

1)合并各个.obj文件的section,合并符号表,进行符号解析;

2)符号地址重定位;

3)生成可执行文件;

描述一下嵌入式基于ROM的运行方式和基于RAM的运行方式有什么区别?

基于RAM

1、将硬盘或者其介质的代码加载到ram中。

2、速度快但是可用RAM少,因为自身的空间要存一部分代码

基于ROM:

1、将部分代码搬到RAM中去,所以可用RAM资源比基于RAM的多。

2.2 中断与异常

中断与异常区别

1、中断是指外部硬件产生的一个电信号从CPU的中断引脚进入,打断CPU的运行,异常是指软件运行过程中发生了一些必须作出处理的事件,CPU自动产生一个陷入来打断CPU的运行。

2、异常处理的时候要考虑与处理器的时钟同步,异常被称为同步中断

中断能不能睡眠 为什么?

1、一般说中断上下文中不能睡眠,这个中断是指硬件事件发生,触发CPU停止当前活动转而去处理硬件请求.

2、根据硬件请求响应处理逻辑的实时紧要与否,将整个中断处理过程分为上半部和下半部.上半部也就是所谓的硬中断处理逻辑,其要求cpu在收到硬件请求后必须马上处理的事情,比如网卡收到数据包了,把数据包从网卡缓存拷贝到主存(可以由DMA完成,但寄存器的修改以及资源设定还是要由cpu去做)的逻辑就需要cpu立即去做,不做的话,网络新来的数据包就可能丢失.所以这些紧要操作逻辑为硬中断处理. 3、下半部有很多种机制,其中就包括软中断,还有tasklet,workqueue等,软中断只是其中的一种,由于历史的原因,有时候是混淆称呼下半部和软中断的. 4、而可以看到软中断逻辑不属于任何进程,所以才不能睡眠,因为一旦睡眠,cpu切换出去,就切不回来了。

简单说就是:唤醒函数针对进程而言的,下半部的中断不属于进程,所以无法被唤醒

中断的响应执行流程是什么?

cpu接受中断->保存中断上下文跳转到中断处理历程->执行中断上半部->执行中断下半 部->恢复中断上下文。

写一个中断服务需要注意哪些?如果中断产生之后要做比较多的事情你是怎么做的?

1、快进快出,在中断服务函数里尽量快速采集信息。

2、中断中不能有阻塞操作

3、中断服务函数注意返回值,使用操作系统定义的宏,而不是自己定义的。

4、做的事情较多,将这些任务放在后半段tasklet处理。

中断和轮询哪个效率高?怎样决定是采用中断方式还是采用轮询方式去实现驱动?

1、中断是CPU处于被动状态下来接受设备的信号,而轮询是CPU主动去查询该设备是否有请求。 2、请求设备是一个频繁请求cpu的设备,或者有大量数据请求的网络设备,那么轮询的效率是比中断高。

3、如一般设备,并且该设备请求cpu的频率比较低,则用中断效率要高一些。主要是看请求频率。

2.3 通讯协议

异步传输与同步传输?

异步传输:是一种典型的基于字节的输入输出,数据按每次一个字节进行传输,其传输速度低。 同步传输:需要外界的时钟信号进行通信,是把数据字节组合起来一起发送,这种组合称之为帧,其传输速度比异步传输快。

RS232和RS485区别?

  1. 传输方式不同。 RS232采取不平衡传输方式,即所谓单端通讯。 而RS485则采用平衡传输,即差分传输方式。
  2. 传输距离不同。RS232适合本地设备之间的通信,传输距离一般不超过20m。而RS485的传输距离 为几十米到上千米。
  3. 设备数量。RS232 只允许一对一通信,而RS485 接口在总线上是允许连接多达128个收发器。
  4. 连接方式。RS232,规定用电平表示数据,因此线路就是单线路的,用两根线才能达到全双工的目的;而RS485, 使用差分电平表示数据,因此,必须用两根线才能达到传输数据的基本要求,要实现全双工,必需用4根线

SPI协议

SPI:高速全双工串行总线。

接口:输出线、输入线、时钟线、片选信号线

1、片选信号线由高到低是SPI的起始信号 ,从机检测到自己的NSS线起始信号之后就知道自己被选中了,然后由低到高是停止信号。

2、SPI 使用 MOSI 及 MISO 信号线来传输数据,使用 SCK 信号线进行数据同步。 在时钟线上升沿触发输出,在下降沿被采样。

IIC协议

1、IIC协议是由数据线SDA和时钟SCL构成的串行总线,可发送和接收数据,是一个多主机的半双工通信方式

2、空闲状态

SDA与SCL都处于高电平,就是空闲状态。

2、起始信号

时钟线为高,数据线由高到低就是启动信号,只能由主机发起空闲状态下才能启动该信号

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HEH6Q7Kx-1669710623167)(C:\Users\61769\AppData\Roaming\Typora\typora-user-images\image-20220601093452355.png)]

3、停止信号

时钟为高,数据线由低到高就是停止信号

4、传输数据格式

SCL为高就会获取SDA数据值,SDA在这期间必须稳定

SCL为低便是SDA电平变化状态,在此期间SDA可以自由变化

可以主动拉低SCL让IIC进入等待状态知道处理结束再释放SCL数据传输会继续

5、ACK应答信号

发送方在第9个时钟脉冲奇迹爱你释放SDA数据,当接收方接收成功时,会输出一个应答信号,低电平有效

6、写操作

start信号–设备地址–方向(读、写)。回应(确定这个设备是否存在)–发送数据–回应–发送完之后主芯片发送一个停止信号。

白色主到从、灰色从到主。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-GWFkECVR-1669710623168)(C:\Users\61769\AppData\Roaming\Typora\typora-user-images\image-20220601095654891.png)]

7、读操作

除了数据需要主到从,其余差不多。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IMX1TAyk-1669710623169)(C:\Users\61769\AppData\Roaming\Typora\typora-user-images\image-20220601100048451.png)]

嵌人式编程中,什么是大端?什么是小端?

大端模式:低位字节存在高地址上,高位字节存在低地址上。 小端模式:高位字节存在高地址上,低位字节存在低地址上。

`//第一种
union w{
    int a;
    char b;
}c;
c.a = 1;
if(c.b==1) printf("小端");
else printf("大端");
//第二种
int a = 0x12345678;
char *p = (char *)&a;
if(*p==0x78)printf("小端");
else printf("大端");` 

3.3 文件系统

什么是根文件系统?

1、内核启动时所挂载(mount)的第一个文件系统,内核代码的映像文件保存在根文件系统中。

2、挂载之后会把一些初始化脚本和服务加载到内存中去运行。

根文件系统为啥这么重要?

1、根文件系统包含系统启动时所必须的目录和关键性的文件,以及使其他文件系统得以挂载(mount)所必要的文件。比如shell命令程序必须运行在根文件系统上,譬如ls、cd等命令。

2、一套linux体系,只有内核本身是不能工作的,必须要rootfs(上的etc目录下的配置文件、/bin /sbin等目录下的shell命令,还有/lib目录下的库文件等)相配合才能工作。

3.4 中断

硬中断 / 软中断是什么?有什么区别?

1、硬中断是由硬件产生的,软中断是执行中断指令产生的。

2、硬中断可以直接中断CPU,软中断并不会直接中断CPU。也只有当前正在运行的代码(或进程)才会产生软中断。

3、硬中断可屏蔽、软中断不可屏蔽

4、硬中断又称上半部,要快速完成任务

中断为什么要区分上半部和下半部?

1、调用过程:外部中断产生->发送中断信号到中断控制器->通知处理器产生中断的中断号

2、为了能被新的中断打断。将中断处理一分为二,上半部登记新的中断,处理快速简单的任务,复杂耗时的任务给下半段处理,所以下半段可以被打断。

3、中断下半部一般使用tasklet或工作队列实现

linux中断的响应执行流程?

cpu接受中断->保存中断上下文跳转到中断处理历程->执行中断上半部->执行中断下半部->恢复中断上下文。

4、操作系统

什么是进程?什么是线程?

进程是资源分配的基本单位,它是程序执行时的一个实例,在程序运行时创建。 线程是程序执行的最小单位,是进程的一个执行流,一个线程由多个线程组成的。

进程和线程有什么区别?

1、进程是资源分配的基本单位,线程是程序运行的基本单位

2、进程有自己的资源空间,线程是共享进程中的数据,所以进程切换开销更大一点

3、线程通讯要简单一些,因为共享全局变量等

4、线程执行开销小,进程执行开销大。

5、多线程中一个线程死掉整个进程也死了,一个进程死掉不会影响其他进程,因为它有独立的地址空间。

何时使用多进程,何时使用多线程?

对资源的管理和保护要求高,不限制开销和效率时,使用多进程。 要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。

进程有几种状态?

  1. 创建状态
  2. 就绪状态
  3. 运行状态
  4. 阻塞状态
  5. 终止状态

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IS9rtzcd-1669710623170)(C:\Users\61769\AppData\Roaming\Typora\typora-user-images\image-20220601205944952.png)]

进程间通信方式

管道(pipe) 管道这种通讯方式有两种限制,一是半双工的通信,数据只能单向流动,二是只能在具有亲缘关系的进程间使用

信号量(semophore) 信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。

消息队列(message queue) 消息队列是由消息组成的链表,存放在内核中并由消息队列标识符标识。

共享内存(shared memory)

共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以 访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与 其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。

套接字(socket)

套解字也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同机器间的进程通信。

线程间的通讯方式

1)通过条件变量进行线程间的通信

(2)通过标志位来通知线程间的通信

(3)通过std::furture来进行线程间的通信

线程间同步方法有哪些?

  1. **临界区:**如果有多个线程试图访问共享资源,那么当有一个线程进入后,其他试图访问共享资源的线程将会被挂起,并一直等到进入临界区的线程离开,临界在被释放后,其他线程才可以抢占。
  2. 互斥量:为协调对一个共享资源的单独访问而设计,只有拥有互斥量的线程,才有权限去访问系统 的公共资源,因为互斥量只有一个,所以能够保证资源不会同时被多个线程访问。
  3. **信号量:**为控制一个具有有限数量的用户资源而设计。它允许多个线程在同一个时刻去访问同一个 资源,但一般需要限制同一时刻访问此资源的最大线程数目
  4. 事件:用来通知线程有一些事件已发生,从而启动后继任务的开始。

什么是僵尸进程,孤儿进程,守护进程?

僵尸进程是 一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用wait或waitpid获取子 进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵死进程。

孤儿进程是因为父进程异常结束了,然后被1号进程init收养。 守护进程是创建守护进程时有意把父进程结束,然后被1号进程init收养

请你回答一下fork和vfork的区别?

  1. fork( )的子进程拷贝父进程的数据段和代码段;vfork( )的子进程与父进程共享数据段
  2. fork( )的父子进程的执行次序不确定;vfork( )保证子进程先运行,在调用exec或exit之前与父进程 数据是共享的,在它调用exec或exit之后父进程才可能被调度运行。
  3. vfork( )保证子进程先运行,在它调用exec或exit之后父进程才可能被调度运行。如果在调用这两个 函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
  4. 当需要改变共享数据段中变量的值,则拷贝父进程。

堆和栈

什么是代码段,数据段,bss段,堆,栈?

代码段:存放程序执行代码的一块区域,通常是只读

数据段:已初始化的全局变量已初始化为非0的静态变量

BSS段:未初始化的全局变量和未初始化的静态变量或者初始化为0的静态变量

数据段和BSS段本质上都是静态区,存放全局变量和静态变量的

堆:堆是用来存放进程中被动态分配的内存段。

栈:是用户存放程序临时创建的变量。

为什么堆的空间是不连续的?

1、堆包含一个链表来维护已用和空闲的内存块。

2、分配的空间在逻辑地址(虚拟地址)上是连续的,但在物理地址上是不连续的

什么是用户栈和内核栈?

内核栈 :内存中属于操作系统空间的一块区域。

作用

1、保存中断现场

2、保存调用的参数、返回值、函数局部变量

用户栈:

用户进程空间的一块区域,用于保存用户空间子程序间调用的参数,返回值以及局部变量。

为什么不能共用一个栈:

1、系统栈(内核栈)大小有限用户程序调用次数可能很多。

2、用户栈空间不能提供相应保护措施

线程是否有相同的堆栈?

每个线程有自己的堆栈。

信号、并发和互斥

驱动里面为什么要有并发、互斥的控制?如何实现?讲个例子?

并发:多个执行单元同时对共享资源操作,容易导致竞态。

互斥:一个执行单元在访问共享资源的时候、其他执行单元都被禁止访问。访问共享资源的代码区被称为临界区,临界区需要某种互斥机制加以保护。

自旋锁是什么?信号量是什么?二者有何异同?

自旋锁:一个执行单元在操作资源时,另一个执行单元不能操作。自旋锁只能短期持有

信号量:资源标量,使用完了就不允许操作了。会有信号告诉需要等多久。适合长期持有的时候用

区别:

1、长时间持有锁使用信号量,短时间持有使用自旋锁。

2、信号量可以睡眠,其他人需要时也会进入睡眠。

3、信号量代码可以被抢占。

自旋锁和信号量可以睡眠吗?为什么?

自旋锁不能睡眠,信号量可以。

原因:

1、自旋锁自旋锁禁止处理器抢占;而信号量不禁止处理器抢占。

自旋锁和信号量可以用于中断中吗?

信号量不能用于中断中,因为信号量会引起休眠,中断不能休眠。

自旋锁可以用于中断。在获取锁之前一定要先禁止本地中断(也就是本CPU中断,对于多核SOC来说会 有多个CPU核),否则可能导致锁死现象的发生

产生死锁的原因是什么?

多个并发进程因争夺系统资源而产生相互等待的现象。即:一组进程中的每个进程都在等待某个事件发 生,而只有这组进程中的其他进程才能触发该事件,这就称这组进程发生了死锁。

原因:

1、系统资源有限

2、进程推进顺序不合理

如何避免死锁?

1、线程按一定顺序加锁

2、获取锁时加上时限,也就是说超过时间则放弃获取。

3、死锁检测

内存

在1G内存的计算机中能否malloc(1.2G)?为什么?

malloc能够申请的空间大小与物理内存的大小没有直接关系,仅与程序的虚拟地址空间相关。

内存管理的方法

1、块式管理

分成一大块一大块,只需要几个字节也给一大块。造成浪费、但方便管理。

2、页式管理

划分地址空间为若干大小区域,被称为页。优点便于管理,缺点页长与逻辑大小没有关系。

3、段式管理和段页式管理

按照程序的自然分界划分的并且长度可以动态改变的区域,每段可以定义一组相对完整的逻辑信息。段与段在内存中可以不相邻接,也实现了离散分配。

段页式管理:

用分段方法来分配和管理虚拟存储器。用分页方法来分配和管理内存

什么是虚拟内存?

它使得应用程序认为它拥有连续可用的内存(一个连续完整的地址空间),允许程序员编写并运行比实际系统拥有的内存大得多的程序

好处:

1、扩大了地址空间

2、地址保护

3、公平分配内存:每个进程相当于有了同样大小的额外内存

解释下内存碎片,内碎片,外碎片?

1、内存碎片:内存碎片是由于多次进行内存分配造成的,空白段太小无法进行下次分配

2、内碎片:分配给程序的存储空间没有用完,有一部分是程序不使用(没用完),但其他程序也没法用的空间。

3、外碎片:空间太小,小到无法给任何程序分配(不属于任何进程)的存储空间。

解释下虚拟地址、逻辑地址、线性地址、物理地址?

1、虚拟地址、逻辑地址:由程序产生的由段选择符段内偏移地址组成的地址这两部分组成的地址并没有直接访问物理内存,而是通过分段地址的变换处理后才会对应到相应的物理内存地址。

2、线性地址:指虚拟地址到物理地址变换之间的中间层,是处理器可寻址的内存空间(称为线性地址空间)中的地址。

3、物理地址:是指现在CPU外部地址总线上的寻址物理内存的地址信号,是地址变换的最终结果。

系统调用是什么,你用过哪些系统调用,和库函数有什么区别?

系统调用:系统调用是通向操作系统本身的接口,是面向底层硬件的。通过系统调用,可以使得用户态运行的进程与硬件设备(如CPU、磁盘、打印机等)进行交互,是操作系统留给应用程序的一个接口。

库函数:库函数(Library function)是把函数放到库里,供别人使用的一种方式。.方法是把一些常用到的函数编完放到一个文件里,供不同的人进行调用。一般放在.lib文件中。系统调用是为了方便使用操作系统的接口,而库函数则是为了人们编程的方便。

区别:

1、库函数是语言或应用程序的一部分,系统调用是内核提供的接口。

2、库函数在用户地址进行,系统调用在内核地中空间执行

3、库函数有缓冲、系统调用无缓冲。

4、系统调用依赖平台,库函数不用

上下文

上下文有哪些?怎么理解?

上下文简单说来就是一个环境。

  1. 用户级上下文: 正文、数据、用户堆栈以及共享存储区;
  2. 寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
  3. 系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、 pte)、内核栈

为什么会有上下文这种概念?

系统调用中用户空间会传递很多数据给内核空间,保存上下文以便系统调用结束后回到用户空间继续执行。

最后修改:2023 年 11 月 10 日
如果觉得我的文章对你有用,请随意赞赏