嵌入式--C语言八股总结(一)
PS:这篇主要有关于c语言的关键字
预处理和关键字(define)
- 调用宏替换,替换文本被插入原来文本的位置(只替换文本)
- 宏与类型无关
- define常量的生命周期止于编译期,不分配内存空间,它存在于程序的代码段
宏定义是在编译的哪个阶段被处理的?
答:宏定义是在编译预处理阶段被处理的。
- 编译预处理:头文件包含、宏替换、条件编译、去除注释、添加行号。
写一个标准宏MIN,实现输入两个参数并返回较小的一个
#define MIN(A,B) ((A)<=(B)? (A) : (B))
- 用到三重条件操作符,在宏中要小心地把参数用括号括起来,并且整个宏也要用括号括起来,防止替换时出现错误。
- 注意若写“least = MIN(p++, b);”这句代码会产生副作用,将p++代入宏体,指针p会做两次自增操作
已知数组table,用宏求数组元素个数。
#define COUNT(table) (sizeof(table) / sizeof(table[0]))
- sizeof(table)得到数组长度,sizeof(table[0])得到数组元素长度,两者相除即可得到数组元素个数。
宏定义的作用和缺点
宏定义的作用:
-
简化代码:宏定义可以将一些重复的代码片段定义为宏,减少代码量,提高代码的可读性和可维护性。
-
提高代码的灵活性:宏定义可以根据不同的需求,定义不同的宏来实现不同的功能,提高代码的灵活性和可扩展性。
-
提高代码的执行效率:宏定义是在预处理阶段进行替换,不需要函数调用的开销,可以提高代码的执行效率。
宏定义的缺点:
-
可能导致代码可读性下降:宏定义会将代码片段替换为宏定义的内容,导致代码变得复杂,可读性下降。尤其是宏定义过长或者嵌套过深时,代码的可读性会更差。
-
可能引发错误:宏定义是在预处理阶段进行替换,没有类型检查和作用域限制,容易引发错误。例如,在宏定义中对参数进行多次计算,可能导致不符合预期的结果。
-
可能引起命名冲突:宏定义没有命名空间的概念,定义的宏可能与其他代码中的变量或函数名冲突,导致错误或不可预测的行为。
-
可能导致代码膨胀:宏定义会将代码片段替换为宏定义的内容,可能导致代码膨胀。特别是在宏定义中使用了大量的循环或条件语句时,会导致生成的代码量变大,影响代码的执行效率和可维护性。
带参宏和函数的区别?
(1)带参宏只是在编译预处理阶段进行简单的字符替换;而函数则是在运行时进行调用和返回。
(2)宏替换不占运行时间,只占编译时间;而函数调用则占运行时间(分配单元、保留现 场、值传递、返回)。
(3)带参宏在处理时不分配内存;而函数调用会分配临时内存。
(4)宏不存在类型问题,宏名无类型,它的参数也是无类型的;而函数中的实参和形参都要定义类型,二者的类型要求一致。
(5)而使用宏定义次数多时,宏替换后源程序会变长;而函数调用不使源程序变长。
typedef 和 define 都是用来为一种类型或对象取一个别名的,但是它们有以下区别:
- typedef 是一个关键字,而 define 是一个预处理指令。
- typedef 在编译阶段有效,有类型检查的功能,而 define 在预处理阶段有效,只是进行简单的字符串替换,不进行任何检查
- typedef 只能为类型起别名,不能为常量或变量等起别名,而 define 可以为任何对象起别名
- typedef 不能对宏类型名进行扩展,而 define 可以使用其他类型说明符对宏类型名进行扩展
- typedef 之后要带分号,而 define 之后不带分号
include<xxx.h>和inlcude"xx.h"区别
-
对于include<filename.h>,编译器先从标准库路径开始搜索filename.h,使得系统文件调用较快。
-
对于#include'“filename.h",编译器先从用户的工作路径开始搜索filename.h,然后去寻找系统路 径,使得自定义文件较快。
内联函数的优缺点和适用场景是什么?
(1)优点:内联函数与宏定义一样会在原地展开,省去了函数调用开销,同时又能做类型检查。
(2)缺点:它会使程序的代码量增大,消耗更多内存空间。
(3)适用场景:函数体内没有递归,没有循环(执行时间短)且代码简短(占用内存空间小)。
一个内联函数的例子。
int max(int a, int b) {
return a > b ? a : b;
}
如果我们想把这个函数定义成内联函数,我们可以在前面加上 inline 关键字:
inline int max(int a, int b) {
return a > b ? a : b;
}
内联函数和宏的区别主要有以下几点:
- 内联函数是由编译器控制实现的,而宏是由预处理器进行文本替换的
- 内联函数有类型检查和相关检查,而宏没有,因此内联函数更安全
- 内联函数是真正的函数,可以调试,而宏只是字符串替换,不能调试
- 内联函数可以处理一些特殊情况,例如带有自增或自减运算符的参数,而宏可能会出错
关键字volatile的作用是什么?给出三个不同的例子。
(1)作用:告诉编译器不要去假设(优化)这个变量的值,因为这个变量可能会被意想不到地改变。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。
(2)例子:
a[1]=0x11;
a[1]=0x14;
a[1]=0x10;
编译器会优化上述三句,认为只有a[1]=0x10,忽略前两句
- 中断服务程序中修改的供其它程序检测的变量需要加 volatile;
- 多任务环境下各任务间共享的标志应该加 volatile;
- 存储器映射的硬件寄存器通常也要加 volatile 说明;
- 多线程应用中被几个线程共享的变量(防止死锁)
如何用C语言实现读写寄存器变量?
#define rBANKCON0 (*(volatile unsigned long *)0x48000004)
rBANKCON0 = 0x12;
- 由于是寄存器地址,所以需要先将其强制类型转换为
volatile unsigned long
。 - 由于后续需要对寄存器直接赋值,所以需要解引用。
关键字static的作用是什么?
-
static修饰局部变量时:①改变了其存储位置,存储在静态区;②同时改变了其生命周期,不会随着函数的调用或结束而被创建或销毁,为整个源程序,因此它只被初始化一次,若没显式初始化则自动初始化为0。
-
static修饰全局变量时:改变了其作用域,只可以被文件内所用函数访问。
-
static修饰函数时:改变了其作用域,只可被这一文件内的其它函数调用。static变量在函数内部声明时,可以保持其值在函数调用之间不变
static变量初始化为什么初始化一次
- 静态变量具有”记忆"功能,初始化后,一直都没有被销毁,都会保存在内存区域中,所以不会再次初始化。存放在静态区的变量的生命周期一般比较长,它与整个程序"同生死、共存亡"。
下面是关键字const的使用示例
(1)const int a; // a是一个整形常量
int const a; // a是一个整形常量
(2)const int *a; // a是一个指向整型常量的指针变量
int * const a; // a是一个指向整型变量的指针常量
int const * const a = &b; // a是一个指向整型常量的指针常量
(3)char *strcpy(char *strDest, const char *strSrc); // 参数在函数内部不会被修改
const int strcmp(char *source, char *dest); // 函数的返回值不能被修改
- 定义变量为常量
- 修饰函数的参数,表示在函数体中不能修改这个参数的值
- 修饰函数的返回值
- 如果给用const修饰返回值的类型为指针,那么函数返回值的内容是不能修改的
- const可以避免不必要的内存的分配。如果我们使用const关键字修饰这个变量,那么编译器就会将这个变量的值视为常量,不再为它分配存储空间。也就是说,const变量的值在编译期间就已经确定了,不会在运行时改变。
一个参数既可以是const还可以是volatile吗?一个指针可以是volatile吗?
- 是的。一个例子是只读的状态寄存器,它是volatile因为它可能被意想不到地改变,它是const因为程序不应该试图去修改它。
- 是的。一个例子是当一个中断服务子程序修改一个指向一个缓冲区的指针时。
关键字auto的作用是什么?
- 用来定义自动局部变量,自动局部变量在进入声明该变量的语句块时被建立,退出语句块时被注销,仅在语句块内部使用。 其实普通局部变量就是自动局部变量,只是省略了auto这一关键字。
关键字register的作用是什么?使用时需要注意什么?
-
作用:编译器会将register修饰的变量尽可能地放在CPU的寄存器中,以加快其存取速度,一般用于频繁使用的变量。
-
注意:register变量可能不存放在内存中,所以不能用&来获取该变量的地址;只有局部变量和形参可以作为register变量;寄存器数量有限,不能定义过多register变量。
C语言编译过程中,关键字volatile和extern分别在哪个阶段起作用?
答案:volatile在编译阶段,extern在链接阶段。
解读:C语言编译过程分为预处理、编译、汇编、链接。
说一下源码到可执行文件的过程
预处理过程
●头文件展开:将#include包含的头文件内容展开到当前位 置。 ● 宏展开:展开所有的宏定义,并删除#define。 ● 条件编译:根据宏定义条件,选择要参与编译的分支代码,其 余的分支丢弃。 ● 删除注释。 ● 添加行号和文件名标识:编译过程中根据需要可以显示这些信 息。 ● 保留#pragma命令:该命令会在程序编译时指示编译器执行一 些特定行为。
编译过程
把预编译之后生成的xxx.i或xxx.ii文件,进行一系列词法分析、语法分析、语义分析及优化后,生成相应的汇编代码文件。 1、词法分析:利用类似于“有限状态机”的算法,将源代码程序输入到扫描机中,将其中的字符序列分割成一系列的记号。 2、语法分析:语法分析器对由扫描器产生的记号,进行语法分析,产生语法树。由语法分析器输出的语法树是一种以表达式为节点的树。 3、语义分析:语法分析器只是完成了对表达式语法层面的分析,语义分析器则对表达式是否有意义进行判断,其分析的语义是静态语义——在编译期能分期的语义,相对应的动态语义是在运行期才能确定的语义。 4、优化:源代码级别的一个优化过程。 5、目标代码生成:由代码生成器将中间代码转换成目标机器代码,生成一系列的代码序列——汇编语言表示。 6、目标代码优化:目标代码优化器对上述的目标机器代码进行优化:寻找合适的寻址方式、使用位移来替代乘法运算、删除多余的指令等。
汇编过程
是使用汇编器将前一阶段生成的汇编文件翻译成目标文 件。汇编器的主要工作就是参考ISA指令集,将汇编代码翻译成对应的 二进制指令,同时生成一些必要的信息,以section的形式组装到目标文件中,后面的链接过程会用到这些信息。如图4-5所示,汇编的流程 主要包括词法分析、语法分析、指令生成等过程
链接过程
- 将各个目标文件分段组装。链接器将编 译器生成的各个可重定位目标文件重新分解组装:将各个目标文件的 代码段放在一起,作为最终生成的可执行文件的代码段
- 链接器在链接程序时一般会基于某个链接地址link——addr进行链接,因为重新分解后地址发生变化,所以要更新全局符号表中的符号的值
- 重定位的核心工作就是修正指令中的符号地址,是链接过程中的最后 一步,也是最核心、最重要的一步
- 无论是代码段,还是数据段,只要这个段中有需要重定位的符 号 , 编 译 器 都 会 生 成 一 个 重 定 位 表 与 其 对 应 : .rel.text 或.rel.data。这些重定位表记录各个段中需要重定位的各种符号
关键字sizeof的作用是什么?函数strlen()呢?
-
sizeof关键字用来计算变量、数据类型所占内存的字节数。sizeof(数组名)得到数组所占字节数,sizeof(字符串指针名)得到指针所占字节数。
-
而strlen()函数则用来测试字符串所占字节数,不包括结束字符 ’\0’。strlen(字符数组名)得到字符串所占字节数,strlen(字符串指针名)得到字符串所占字节数。
数据类型
全局变量和局部变量区别
-
全局变量的作用域为程序块,而局部变量的作用域为当前函数
-
内存存储方式不同,全局变量分配在全局数据区,后者在栈区
-
生命周期不同 用变量a给出下面的定义
-
一个指向指针的的指针,它指向的指针是指向一个整型数的指针(二重指针):
int **a。
-
一个有10个指针的数组,这10个指针是指向整型数的(指针数组):
int *a[10]
-
一个指向有10个整型数数组的指针(数组指针):
int (*a)[10]
。
-(函数指针) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数:int (*a)(int)
。 -
(函数指针数组)一个有10个指针的数组,这10个指针均指向函数,该函数有一个整型参数并返回一 个整型数:
int (*a[10])(int)
。
负数和正数的反码,补码分别是什么?
- 负数的反码:对原码除符号位外的其余各位逐位取反就是反码
- 负数的补码:负数的补码就是对反码加1
- 正数的原码、反码、补码都一样
new和malloc有什么区别?
(1)new与delete是C++的操作符;而malloc与free是C/C++的标准库函数。
(2)C++允许重载new/delete操作符;而不允许重载malloc/free。
(3)new返回的是对象类型的指针,严格与对象匹配;而malloc返回的是void*类型的指针,需要进行强制类型转换。
(4)new可以自动计算所申请内存的大小;而malloc需要显式指出所需内存的大小。
(5)new操作符从自由存储区上动态分配内存;而malloc函数从堆上动态分配内存。
(6)new内存分配失败会抛出bac_alloc异常;malloc内存分配失败会返回NULL。
(7)new/delete会调用对象的构造函数/析构函数,以完成对象的构造/析构;而malloc不会。
结构体内存对齐原则
-
第一个成员的首地址(地址偏移量)为0。
-
成员对齐:以4字节对齐为例,如果自身类型小于4字节,则该成员的首地址是自身类型大小的整数倍;如果自身类型大于等于4字节,则该成员的首地址是4的整数倍。若内嵌结构体,则内嵌结构体的首地址也要对齐,只不过自身类型大小用内嵌结构体的最大成员类型大小来表示。数组可以拆开看做n个数组元素,不用整体看作一个类型。
-
最后结构体总体补齐:以4字节对齐为例,如果结构体中最大成员类型小于4字节,则大小补齐为结构体中最大成员类型大小的整数倍;如果大于等于4字节,则大小补齐为4的整数倍。内嵌结构体也要补齐。
-
如程序中有
#pragma pack(n)
预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再考虑当前类型以及最大结构体内类型。 -
结构体长度一定是最长数据元素的整数倍
-
GCC默认的最大对齐数为4,当一种数据类型大小超过4字节仍然按照4对齐。这是GCC和VC++6.0、Visual Studio、arm-linux-gcc等编译器不一样的地方
联合体对齐原则
- 联合体的整体大小:最大成员对齐模数或对齐模数的整数倍。
- 联合体的对齐原则:按照最大成员的对齐模数对齐
C语言中struct与union的区别是什么
- 在存储多个成员信息时,编译器会自动给struct每个成员分配存储空间,struct可以存储多个成员信息,而union每个成员会用同一个存储空间,只能存储最后一个成员的信息。
- 都是由多个不同的数据类型成员组成,但在任何同一时刻,union只存放了一个被先选中的成员,而结构体的所有成员都存在。对于联合体不同成员赋值,将会对它的其他成员重写
枚举变量
enum 枚举名{ 枚举值表 };
- 枚举值是常量,不是变量。不能在程序中用赋值语句再对它赋值
- 只能把枚举值赋予枚举变量,不能把元素的数值直接赋予枚举变量
- 枚举元素本身由系统定义了一个表示序号的数值,从0开始顺序定义为0,1,2… 如同结构和联合一样,枚举变量也可用不同的方式说明,即先定义后说明,同时定义说明或直接说明。 设有变量a,b,c被说明为上述的weekday,可采用下述任一种方式:
enum weekday{ sun,mou,tue,wed,thu,fri,sat };
enum weekday a,b,c;
或者为:
enum weekday{ sun,mou,tue,wed,thu,fri,sat }a,b,c;
或者为:
enum { sun,mou,tue,wed,thu,fri,sat }a,b,c;
extern “C”
extern "C"
是为了告诉C++编译器这部分代码使用了C的函数命名规则,并将其当作C语言代码去处理。- 使用
extern "C"
可以避免C++编译器对C语言代码进行名称改写,从而保持C语言代码的兼容性。这样,C++源文件就能够调用和使用C语言的全局变量、函数和宏定义。
指针相关
指针函数作用
-
动态内存分配:指针函数通常用于在堆(heap)上动态分配内存,并将指向该内存的指针返回给调用者。这样的函数用于创建动态数据结构,如链表、树等。通过返回指针,可以在函数外部使用和访问这些动态分配的内存。
-
字符串操作:在C语言中,字符串是以字符数组的形式表示的。指针函数经常用于返回指向字符串的指针,以便进行字符串的处理和操作,例如拷贝、连接和比较等。
-
复杂数据结构:指针函数可用于返回指向复杂数据结构(如结构体、类对象等)的指针。这样可以避免在函数之间频繁地复制大量的数据,提高程序的效率。
-
错误处理:有时,函数需要返回多个值,包括成功或失败的状态和其他信息。指针函数可以通过返回一个指向特定类型的指针来实现这一点,使得函数能够返回多个值。
野指针
- 1.野指针是指向不可用内存的指针,当指针被创建时,指针不可能自动指向NU儿L,这时,默认值是 随机的,此时的指针成为野指针。
- 2.当指针被free或delete释放掉时,如果没有把指针设置为NU儿L,则会产生野指针,因为释放掉的仅 仅是指针指向的内存,并没有把指针本身释放掉。
- 3.第三个造成野指针的原因是指针操作超越了变量的作用范围。
边学习边总结的嵌入式各种知识,八股,面经,量大管饱,最重要:免费开放,希望大家能共同进步。