最近正在准备秋招,在此每天记录一些面试常问的八股文,希望可以和大家一起进步。(持续更新中……)
目录
1. const关键字的作用
const意味着只读,用来定义常量,如果一个变量被const修饰,那么它的值就不能再被改变。
1) 定义变量为常量
const可定义变量(局部变量或全局变量)为常量。
2) 增强程序的健壮性
#define也可以用来定义常量,但#define只是对值进行简单的替换,并不会进行类型检查;而const可以保护被修饰的东西,防止意外修改,增强程序的健壮性。
3) 节省空间,提高编译效率
编译器通常不为普通const常量分配存储空间,而是将它们保存在符号表中,这使得它成为一个编译期间的常量,没有了存储与读内存的操作,使得它的效率也很高。
const int N = 100; /* 定义一个常量N */
N = 50; /* 错误,不能修改const修饰的值 */
const int n; /* 错误,常量在定义的时候必须初始化 */
const char GetString() /* 定义一个函数 */
char* str = GetString() /* 错误,如果const修饰的函数返回值类型为指针,则指针指向的内容不能被修改,且只能被赋值给被const修饰的指针(我用VS2022使用char *str = GetString()没发现报错) */
const char* str = GetString() /* 正确 */
#define A 9 /* 定义一个常量 */
const int B = 10; /* 此时并未给B分配内存 */
int i = A; /* 预处理期间进行宏替换,分配内存 */
int i = B; /* 为A分配内存,之后不再分配 */
int j = A; /* 预处理期间再次进行宏替换,分配内存 */
int j = B; /* 不再分配内存 */
2. const关键字的用法
1) 修饰局部变量
比如 const int n=5 和 int const n=5 这两种写法一样,表示变量n的值不能被改变了;也可以修饰常量静态字符串,如const char* str="fdsafdsa",后续若对str进行了误修改,编译期间就会发现,进而避免程序的逻辑错误。
2) 修饰常量指针、指针常量、指向常量的常指针
修饰常量指针:不能使用这个指针改变变量的值,但指针可以指向其他地址。
/* 修饰常量指针 */
int a = 100;
int b = 50;
const int* n = &a; /* 或int const* n = &a; */
*n = 10; /* 错误,不可以修改指针指向的值 */
n = &b; /* 正确,可以修改指针指向其他地址 */
void StringCopy(char *strDestination, const char *strSource); /* 不能使用*strSource改变指针strSource指向的内容 */
修饰指针常量:指针指向的地址不能变,但地址中保存的数据是可以变的。
/* 修饰指针常量 */
int a = 100;
int b = 50;
int* const c = &a;
c = &b; /* 错误,不能修改指针指向其他地址 */
*c = 10; /* 正确,可以修改指针指向的值 */
void swap ( int * const p1 , int * const p2 ) /* 指针p1和p2指向的地址都不能修改,但是可以使用*p1和*p2对值进行修改 */
修饰指向常量的常指针:指针指向的地址不能改变,也不能通过这个指针改变变量的值,但是依然可以通过其他指针改变这个变量的值。
int m = 100;
int n = 50;
const int* const p = &m;
int* q = &m; /* q指针也指向变量m */
p = &n; /* 错误,指针指向的地址不能修改 */
*p = 10; /* 错误,不能使用该指针修改变量的值 */
*q = 60; /* 正确,可以使用其他指针修改变量值 */
3) 修饰全局变量
全局变量的作用域是整个工程,我们应该尽量避免使用全局变量,因为一旦有一个函数改变了全局变量的值,它也会影响到其他引用这个变量的函数,导致出了bug后很难发现,如果一定要用全局变量,我们应该尽量使用const进行修饰,以防止不必要的人为修改,使用的方法与局部变量是相同的。
3. 关键字static的作用
1)在函数体中:一个被声明为静态的变量在这一函数被调用过程中其值维持不变。静态变量的生命周期和程序相同,在main 函数之前初始化,在程序退出时销毁。
2) 在模块内(但在函数体外):一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是 一个本地的全局变量,只能被当前文件使用。
3) 在模块内:一个被声明为静态的函数只可被这一模块内的其它函数调用,即这个函数被限制在声明它的模块的本地范围内使用。
大多数应试者能正确回答第一部分,一部分能正确回答第二部分,但是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处和重要性。
4. 关键字volatile的作用
volatile是一个类型修饰符,提醒编译器它后面所定义的变量随时都有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,编译器不会对该变量进行优化,而是直接从变量内存地址中读取数据,从而可以提供对特殊地址的稳定访问。在以下几种情况中会用到volatile:
1) 并行设备的硬件寄存器(如状态寄存器)
存储器映射的硬件寄存器通常要加volatile,因为每次对他读写都可能有不同的意义。比如对一个设备进行初始化,此设备的某个寄存器为0xff800000,如下所示:
int* output = (unsigned int*)0xff800000 /* 定义一个IO端口 */
int init(void)
{
int i;
for(i = 0; i < 10; i++)
*output = i;
}
如果不使用volatile修饰寄存器地址,那么编译器优化后的程序只对该寄存器进行了一次配置操作,对此地址只做了一次读操作,如下所示:
int init(void)
{
*output= 9;
}
2) 中断服务程序中修改的供其他程序检测的变量
中断服务函数中对某变量进行修改时,若主程序中没有修改该变量,则编译器优化后可能只从内存中读取到寄存器中一次,之后每次只从寄存器中读取变量的副本,导致中断服务程序的操作短路,所以需要使用volatile对变量进行修饰,告诉编译器不对其优化。
3) 多任务环境下个任务间共享标志,应该加volatile
多个任务都可以修改共享标志的值,编译器优化后会把变量读取到寄存器中,之后再取变量值时都是从寄存器读取,当内存变量或寄存器变量因别的线程而改变了值时,该寄存器的值不会改变,若不使用volatile修饰,会导致应用程序读取的值与实际的变量值不一致。
5. 关于volatile的面试题
1) 一个参数既可以是const还可以是volatile吗?
可以,比如只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2) 一个指针可以是volatile吗?
可以,当一个中服务子程序修改一个指向buffer的指针时。
3) 下面的函数有什么错误?
int square(volatile int* ptr)
{
return *ptr * *ptr;
}
程序的目的是返回指针ptr指向的值的平方,但ptr指向的是volatile型的变量,所以编译器将产生类似于如下效果:
int square(volatile int* ptr)
{
int a, b;
a = *ptr;
b = *ptr;
return a * b;
}
由于*ptr的值可能会被意想不到的改变,所以a和b的值可能不同,导致结果与预期不一致,正确的代码如下所示:
long square(volatile int* ptr)
{
int a;
a = *ptr;
return a * a;
}
6. typedef和#define的区别
1) 原理不同
#define是预处理指令,预处理时只进行简单而机械的字符串替换,不做正确性检查;typedef是关键字,在编译处理时会做正确性检查,它在自己的作用域内给一个已经存在的类型一个别名,但不能在函数定义里面使用typedef。用typedef定义数组、指针、结构等类型会带来很大的方便,不仅使程序书写简单,也使意义明确,增强可读性。
2)功能不同
typedef用来定义类型的别名,起到类型易于记忆的功能。另一个功能是定义与机器无关的类型,如定义目标机器上最高精度的浮点类型REAL;而#define不只可以为类型取别名,还可以定义常量、变量、编译开关等。
typedef long double REAL; /* 在支持long double的机器上 */
typedef double REAL; /* 在不支持long double的机器上 */
typedef float REAL; /* 在不支持double的机器上 */
3)作用域不同
#define没有作用域的限制,只要是之前预定义过的宏,在以后的程序中都可以使用,而typedef有自己的作用域。
4)对指针的操作不同
#define INTPTR1 int*
typedef int* INTPTR2;
INTPTR1 p1, p2; /* 声明一个指针变量p1和一个整型变量p2 */
INTPTR2 p3, p4; /* 声明两个指针变量p3和p4 */
int a = 1;
int b = 2;
int c = 3;
const INTPTR1 p1 = &a; /* 定义一个常量指针p1,不能通过p1改变其指向的内容,但p1可以指向其他位置 */
const INTPTR2 P2 = &b; /* 定义一个指针常量p2,即p2不能再指向其他位置 */
intptr2 const p3 = &c; /* 定义一个指针常量p3 */
7. 找出下面中断处理程序(ISR)的错误
__interrupt double compute_area(double radius)
{
double area PI * radius * radius;
printf("Area = %f\n", area);
return area;
}
1) ISR不能有返回值,也不能传递参数;
2)ISR应该快进快出,所以不推荐进行浮点运算,并且在许多编译器中浮点一般都是不可重入的,不允许在ISR中进行浮点运算。
3) printf()是不可重入函数,不能在ISR中使用。printf()函数采用的缓冲机制,这个缓冲区是共享的,相当于一个 全局变量,第一层中断来时,它向缓冲里面写入一些部分内容,恰好这时来了个优先级更高的中断,它同样调用了printf()函数, 也向缓冲里面写入一些内容,这样缓冲区的内容就错乱了。
8. C语言编译的四个步骤
1) 预处理:展开宏和包含头文件,生成.i文件;
2) 编译:编译器对将预处理后的源代码进行词法分析、语法分析、语义分析和优化,最后翻译成汇编码,生成.s文件;
3)汇编:汇编器将汇编码转换为机器码,生成.o文件;
4) 链接:链接器将多个目标和库文件等组合在一起,并解决外部引用,最终生成可执行文件。
9. 自己实现mystrcpy函数
char* mystrcpy(char* strDest, const char *strSrc) /* 3分,const修饰带拷贝字符串,提高代码健壮性 */
{
assert((strDest != NULL) && (strSrc != NULL)); /* 2分,检查指针的有效性,提高代码的健壮性 */
char* address = strDest; /* 1分 */
while((*strDest++ = *strSrc++) != 0); /* 3分 */
return address; /* 1分,支持链式表达式,提高函数可用性 */
}
该函数将address返回,是为了实现链式表达式,比如: int length = strlen( mystrcpy(str, “Hello World”) )。
10. 无符号整型与有符号整型进行运算
void foo(void)
{
unsigned int a = 6;
int b = -20;
(a + b > 6) ? puts(">6") : puts("<6");
}
答案输出为:“>6”,当表达式中存在有符号类型和无符号类型时,所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。嵌入式系统中频繁用到无符号整形的数据,务必注意。
11. C语言内存分配方式有几种
C语言有三种内存分配方式,分别为: 静态存储区分配、栈上分配、堆上分配。
静态存储分配:内存在编译程序时就已经分配好了,这块内存在程序的整个运行期间都存在,比如全局变量、静态变量等;
栈上分配:在执行函数时,函数内局部变量的存储单元都可以在栈上创建,栈空间充足的话则正常分配,栈空间不足则提示栈溢出错误;函数执行结束时这些存储单元自动被释放;
从堆上分配:也叫做动态内存分配,程序运行时使用malloc()或new()函数申请的内存就是从堆上分配的,程序员可以自定义申请内存的大小,并且要自己负责在何时调用free()或者delete()函数释放内存,要注意内存泄露的问题。
12. 堆和栈的区别
1) 申请方式不同:栈由系统分配和释放,存放函数的参数值、局部变量的值等;而堆需要程序员自己申请和释放,堆中的具体内容也由程序员自己安排;
2) 申请大小的限制:栈空间有限,栈是向下生长的一块连续的内存区域,栈顶的地址和栈的容量已经设定好了,若申请的空间大于栈的剩余空间,则会报栈溢出错误。堆是向上生长的不连续的内存区域,系统中使用链表来管理空闲的内存地址,堆的大小受限于计算机系统中有效的的虚拟内存(链表的遍历方向是由低地址向高地址,当系统收到程序的申请时,会遍历链表,寻找第一个空间大于所申请空间的堆节点,然后将节点从内存空闲节点链表中删除,并将该节点的空间分配给程序)。可见,堆的空间比较灵活,通常比栈的空间要大。
3) 申请效率:栈由系统分配,效率较高,但程序员无法控制;相比之下堆的申请效率就比较低,而且容易产生内存碎片,但是用起来比较方便。
13. 栈的作用
栈的作用有:函数调用、中断处理、程序调试、操作系统中的任务调度,介绍如下:
1) 函数调用:当一个函数被调用时,程序会把该函数的参数、返回地址、局部变量等信息存入栈中,当函数执行完成后,栈会依次弹出这些信息,并恢复调用函数前的状态(栈先进后出);
2) 中断处理:系统发生硬中断或软中断时,操作系统会自动保存当前进程的执行现场(包括CPU状态、程序计数器、寄存器等信息)到栈中,然后将CPU控制权转交给中断处理函数,中断处理函数执行完成后,栈会弹出之前保存的执行现场信息,恢复到中断前的状态;
3) 程序调试:调试程序时,程序员可根据栈中的内容(如变量的值、函数调用栈、异常处理等)来查看当前程序的执行状态;
4) 线程调度:栈是多线程编程的基石,每个线程都必须有一个自己的栈,用来存放本线程运行时各个函数的临时变量,和维护线程中函数调用和函数返回时的函数调用关系以及保存函数运行现场。
14. 调用函数时的入栈顺序
1) 第一个入栈的是主函数中调用函数处的下一条执行语句的地址,也就是函数的返回地址;
2) 接着入栈的是函数的各个参数,入栈顺序为从右向左(为了适应可变参函数的参数个数不确定的情况)依次将参数入栈;
3) 然后是函数内部的局部变量(注意,static变量是不入栈的);
4) 函数执行完成后按照先进后出的顺序依次出栈。
15. 为什么static变量只初始化一次
静态局部变量、静态全局变量和全局变量都保存在静态存储区中,所以这些变量都具有“记忆”功能,初始化一次后不会被销毁,生命周期和程序相同,所以只需要初始化一次。
16. sizeof()和strlen()的区别
1) 数据类型不同:sizeof是操作符(sizeof既是关键字,也是操作符),而strlen是库函数。
2) 操作的参数类型不同:sizeof 只关注占⽤内存空间的⼤⼩,不关注内存中存放什么数据,strlen()只能用char*类型变量做参数,计算以“ \0 ”结尾的字符串长度,而sizeof后可接变量名、数据类型、函数等,sizeof后接变量名时可以不加括号,示例如下:
int fun(void)
{
return 10;
}
int main(void)
{
int num = 100;
printf("%d\n", sizeof(num)); /* 结果为:4 */
printf("%d\n", sizeof(int)); /* 结果为:4 */
printf("%d\n", sizeof num); /* 结果为:4 */
printf("%d\n", sizeof(fun())); /* 结果为:4,结果取决于函数返回值的类型*/
printf("%d\n", sizeof("abcdef")); /* 结果为:7,包含字符串最后的'/0' */
printf("%d\n", strlen("abcdef")); /* 结果为:6,不包含字符串结尾的'/0' */
printf("%d\n", sizwof("\0")); /* 结果为:2,字符串"\0"还有一个字符串结束符'\0' */
printf("%d\n",strlen("\0")); /* 结果为:0,strlen用来计算以"\0"作为结束符的字符串长度 */
printf("%d\n", sizeof('\0')); /* 结果为:4,'\0'的ASCLL代码值为0,等价于数字0 */
return 0;
}
3) 执行时间不同:sizeof()通常是在编译程序时进行计算的,而strlen()函数是在程序运行时进行计算的,示例如下,
int main(void)
{
char str[20] = "0123456789";
char* str2 = "abcdef";
printf("strlen(str) = %d\n", strlen(str)); /* 结果为字符串的长度: 10 */
printf("sizeof(str) = %d\n", sizeof(str)); /* 结果为数组的大小: 20 */
printf("strlen(str2) = %d\n", strlen(str2)); /* 结果为字符串的长度: 6 */
printf("sizeof(str2) = %d\n", sizeof(str2)); /* 结果为指针的大小(64为系统):8 */
return 0;
}
17. 不使用sizeof(),求变量占用的字节数
如下所示:(char*)&value是value的地址,(char*)(&value + 1)是value的下一个地址,二者之差就是value类型数据的大小。
#define MySizeof(value) (char*)(&value + 1) - (char*)&value
int main(void)
{
int i;
double f;
double* q;
printf("%d\n", MySizeof(i)); /* 结果为:4 */
printf("%d\n", MySizeof(f)); /* 结果为:8 */
printf("%d\n", MySizeof(q)); /* 结果为:8 */
return 0;
}
18. 短路求值
#include <stdio.h>
int main(void)
{
int i = 6;
int j = 1;
if((i > 0) || ((j++) > 0))
printf("%d\n", j); /* 结果为1,i>0条件成立后直接往下执行,右边的((j++) > 0)被短路,&&同理 */
}
19. 结构体和联合体的区别
结构体:结构体中各成员拥有自己的内存,各自互不干涉,结构体的总长度为所有结构体成员长度之和,遵循内存对齐原则;
联合体:联合体中各成员共用一块内存,共用一个内存首地址,并且同时只有一个成员可以得到这块内存的使用权,联合体的大小至少能容纳最大的成员变量,并且为所有成员变量类型大小的整数倍。
区别:内存利用方式不同,结构体中各成员拥有自己的内存,互不干涉;联合体中各成员共用一块内存;对结构体的成员赋值时,各变量互不影响,对联合体的变量赋值时,其他成员的值也将被改写。
20. 什么是内存泄漏
内存泄漏是指程序中已经分配的内存未能成功释放,导致可用内存逐渐减少的现象。在程序运行过程中,如果反复发生内存泄漏,最终可能会导致系统可用内存耗尽,从而影响程序的性能或导致程序崩溃。
21. 内存泄露的原因
1) 未释放动态分配的内存:如使用malloc
, calloc
, realloc
和 new
等函数分配内存后,未使用对应的 free
或 delete
来释放内存;
2) 资源占用:除了内存外,程序可能申请其他系统资源(如文件句柄、数据库连接等),未正确释放这些资源也会导致类似内存泄漏的问题;
3) 数据结构错误:链表、树等数据结构若未正确处理其元素的删除操作,可能导致部分节点成为不可达的,从而造成内存泄漏。
22. 如何判断内存泄漏
1) 检查程序:通过检查程序来寻找可能未释放内存的地方,特别关注那些有动态内存分配的函数或模块。所以我们要养成良好的编程习惯,定期检查代码,使用内存分配函数后,一定要记得使用相应的函数释放掉。如果程序运行后再反过头检查内存泄漏是很麻烦的,难度很大。也可以在代码中添加日志输出,特别是在分配和释放资源的地方,可以帮助追踪内存的使用和释放。
2) 使用第三方工具:在Linux下,可使用Valgrind等编程工具帮助检查内存泄漏;在Windows下,Visual Studio提供了内置的内粗泄露检测工具。除此之外,还有一些常见的工具插件,如ccmalloc、Dmalloc、Leaky等。
3) 链表管理:将分配的内存指针以链表的形式进行管理,使用完毕之后从链表中将其删除,程序运行结束时可检查该链表,判断是否将申请的内存全部释放完成;
23. 什么是大端模式和小端模式
计算机系统中以字节(8bit)为存储单元,也就是每个地址单元对应一个字节,C语言中的int、short等类型数据大小大于一字节,所以就要考虑到多字节类型数据在内存中的字节排序问题,也就是大端模式和小端模式,介绍如下:
小端模式:数据的低位存放在低地址中,数据的高位存放在高地址中。如存储0x12 34 56 78,小端存储时情况如下:
低地址 --------------------> 高地址
0x12 | 0x34 | 0x56 | 0x78
大端模式:数据的高位存放在低地址中,数据的低位存放在高地址中。如存储0x12 34 56 78,大端存储时情况如下:
低地址 --------------------> 高地址
0x78 | 0x56 | 0x34 | 0x12
24. 如何判断当前系统的大小端模式
无论是大端存储还是小端存储,读取数据时都是从低地址开始读取的,所以下面示例程序中 &i 是 i 的低位地址。
void judge_bigend_littleend()
{
int i = 12; /* 16进制为:0x 00 00 00 0C */
int* p = &i; /* p中存储i的地址,也就是i的低地址 */
char c = 0;
c = *((char*)p); /* c等于i低地址处保存的一字节数据 */
if (c == 0XC) /* i的低位数据为0XC */
printf("系统为小端模式\n");
else
printf("系统为大端模式\n");
}
25. 指针数组和数组指针的区别
指针数组:它是一个数组,数组元素为指针类型。比如int *p[3]声明了一个指针数组([]的优先级高,先与p结合,所以p是一个数组),p+1时指向下一个数组元素。
int main(void)
{
int i;
int *p[4]; /* 定义一个指针数组,该数组有4个元素,每个元素为int类型 */
int a[4] = {1,2,3,4};
p[0] = &a[0];
p[1] = &a[1];
p[2] = &a[2];
p[3] = &a[3];
for(i = 0; i < 4; i++)
printf("%d",*p[i]); /* 最终的输出结果为1234 */
printf("\n");
return 0;
}
数组指针:它是一个指针,指向一个数组。比如 int (*p)[4]声明了一个数组指针(()的优先级高,先与p结合,所以p是一个指针),p+1时要跨越4个整型数据的长度。
int main(void)
{
int b[12] = {1,2,3,4,5,6,7,8,9,10,11,12};
int (*p)[4]; /* 定义一个数组指针,指向的数组大小为4个int类型数据的大小 */
p = (int (*)[4])b;
printf("%d\n", **(++p)); /* ++p跨越的长度为4个int类型数据的大小,结果为第5个元素的值 */
return 0;
}
将上面程序中,将数组b的地址赋值给p,因为p是一个指向包含有4个int类型数据的数组的指针,所以可以理解为p是指向二维数组的指针,该二维数组为:{1,2,3,4},{5,6,7,8},{9,10,11,12} ,p指向的是 {1,2,3,4}的地址,所以++p指向的是{5,6,7,8}的地址,*(++p)就是二维数组元素{5,6,7,8},**(++p) 则是该数组的第一个元素,所以结果为5。
26. 指针函数和函数指针的区别
指针函数:它是一个函数,函数的返回值是一个地址。比如 int *pfun(int, int) 声明了一个指针函数。
#include <stdio.h>
int* fun(int* x) /* 传入指针 */
{
int* tmp = x; /* 指针tmp指向x */
return tmp; /* 返回tmp指向的地址 */
}
int main()
{
int b = 2;
int* p = &b; /* p指向b的地址 */
printf("%d",*fun(p)); /* 输出p指向的地址的值,输出结果为2,很简单,不解释了 */
return 0;
}
函数指针:它是一个指针,指向函数的地址。程序中定义一个函数后,在编译程序时会为这个函数分配一段内存空间,内存空间的首地址就是函数的地址,函数名表示的就是函数的地址,函数指针指向的就是这个地址。比如 int(*p)(int, int) 声明了一个函数指针。
# include <stdio.h>
int Max(int,int); /* 函数声明 */
int main(void)
{
int(*p)(int,int); /* 定义一个函数指针 */
int a, b,c;
p = Max; /* 把函数Max赋给指针变量p,使p指向Max函数 */
printf("please enter a and b:");
scanf("%d%d",&a,&b);
c=(*p)(a,b); /* 通过函数指针调用Max函数 */
printf("a=%d\nb =%d\nmax=%d\n",a,b,c);
return 0;
}
int Max(int x,int y) /* 定义Max函数 */
{
int z;
if(x> y)
z = x;
else
z = y;
return z;
}
补充:函数指针数组:
int (*a[4])(int) /* 定义一个数组函数指针数组,数组中的元素为参数和返回值都为int类型的函数 */
27. 数组和指针的区别
1)数据保存:指针保存的是数据的地址,内存访问偏移量为4个字节(32位处理器),无论指向的数据类型如何,都以地址类型进行解析。数组保存的是数据,数组名表示第一个数组元素的地址,内存偏移量取决于数据类型。对数组名取地址(&数组名)时表示整个数组,此时内存偏移量为整个数组的大小(sizeof(数组名))。
2) 数据访问:指针对数据是间接访问,需要使用解引用符号 *,数组对数据是直接访问,可通过数组名[下标]的形式来访问。
3) 使用环境:指针多用于动态数据结构(如链表等)和动态内存开辟;数组多用于存储固定个数且数据类型统一的数据结构(如线性表等)和隐式分配。
28. 什么是野指针,如何避免野指针
野指针是指向不可用内存的指针,当指针被free或delete释放掉时,此时只是释放掉了指针指向的内存,如果没将指针设置为NULL,则会产生野指针;当指针访问越界时也会产生野指针。避免野指针的方法如下:
1) 创建指针时,对其设置一个初始值或NULL
2) 指针用完后释放内存,并记得将其赋值为NULL
29. 头文件中是否可以定义静态变量
不可以,在头文件中定义静态变量会造成资源浪费,因为这样会导致每个包含该头文件的源文件都有一个独立的静态变量,甚至会导致程序出现错误,所以不推荐在头文件中定义任何变量,当然也包含静态变量。推荐在源文件中定义静态变量,然后在头文件中使用extern关键字进行声明。extern 关键字表示该变量在其他地方有定义,而在当前头文件中不进行实际定义,这样就只会在源文件中进行一次内存分配,使其成为全局唯一实例。
30. 局部变量和全局变量是否可以同名
可以,局部变量会屏蔽全局变量。
31. C语言中函数是如何调用的
大多数CPU上的程序使用栈来实现函数调用,调用函数时,栈被用来传递函数的参数、存储返回值信息和存储局部变量等,函数调用所使用的栈叫做栈帧,每个函数调用都有自己的栈帧结构。栈帧结构由两个指针来指定,分别为:帧指针(指向起始位置)、栈指针(指向栈顶)。函数执行的过程中,栈指针esp会数据的入栈和出栈而移动,多以函数大多数时间都是基于帧指针来访问数据,示意图如下:
32. 程序分为几个段
程序通常被分为四个段,分别为:代码段、数据段、堆栈段、bss段
代码段:用于存储程序的可执行指令,通常是只读的,且位于地地址的区域;
数据段:存储程序中已经初始化的全局变量和静态变量,通常位于代码段的后面;
堆栈段:栈中保存局部变量、函数调用的参数及返回值等,堆中保存由malloc()等申请的动态内存;
bss段:保存已经定义,但还未被初始化的全局变量和静态变量。
33. 数组下标可以是负数么
可以,数组下标只是与当前位置的偏移量,示例如下:
#include <stdio.h>
int main(void)
{
int i;
int a[5] = {0, 1, 2, 3, 4};
int *p = &a[4];
for(i = -4; i <= 0; i++)
printf("%d %x\n", p[i], &p[i]);
return 0;
}
/* 运行结果 */
0 bd5b5f00
1 bd5b5f04
2 bd5b5f08
3 bd5b5f0c
4 bd5b5f10
34. strcpy与memcpy的区别
这两个都是标准C库函数,介绍如下:
strcpy:用于字符串拷贝,将源字符串中的内容复制到目标字符串中,直到遇到字符串结束符 ‘\0’,需要注意的是,目标字符串必须有足够的空间来存储被复制的内容,否则可能导致缓冲区溢出。
memcpy:用于字节级的内存拷贝,memcpy对拷贝的内容没有限制,不仅限于字符串,还可以拷贝结构体、数组等数据。需要注意的是,使用memcpy函数拷贝字符串时不会检查字符串结束符,就是根据指定的字节数来进行拷贝。
区别介绍如下:
1) 复制的内容不同:strcpy只能复制字符串,而memcpy可以复制任意内容,例如字符数组、整型、结构体、类等;
2) 复制的方法不同:strcpy不需要指定长度,它遇到被复制字符的串结束符"\0"才结束,所以容易溢出。memcpy可通过第3个参数指定复制的长度,但要注意源区域和目标区域不要发生重叠,否则可能会导致数据损坏;
3) 应用场景不同:复制字符串时通常使用strcpy函数,复制其他类型数据时通常使用memcpy函数。
35. 如何使用两个栈实现一个队列
栈先进后出,队列先进先出,所以可以使用一个栈实现入队操作,另一个栈实现出队操作。假如现在有两个栈:栈A和栈B,要对1、2、3、4、5、6这6个元素进行入队和出队操作,流程如下:
① 假如此时要将1、2、3、4这4个元素入队,可将这四个元素压入栈A中,此时栈A和栈B的内容如下:
② 此时要将最先入队的1出队,此时就要现将栈A中的元素依次出栈,再压入栈B中,最后从栈B中弹出元素1,如下所示:
③ 若此时将元素2和元素3出队,接着元素5和元素6入队,最后将所有元素出队,流程如下所示:
最后再梳理一下思路:使用两个栈模拟一个队列,栈A用来实现入队操作,栈B用来实现出队操作。入队时比较简单,就是将数据压入栈A中。出队时要先判断栈B是否为空:若栈B不为空,则将栈B中的元素依次出栈。若栈B为空,则检查栈A是否为空。若栈A不为空,则将栈A的元素依次出栈,并依次压入到栈B中,接着再从栈B中将元素依次出栈;若栈A为空,则提示错误信息,此时队列为空。
36. 什么是位域,有什么作用
在存储一些信息时,有时不需要存储一个完整的字节,可能只需要使用一个或几个二进制位,比如定义一个开关变量,只有0和1两种状态,也就是只使用1个二进制位即可。这时就可以使用位域来进行处理,用来节省存储空间。位域是C语言提供的一种数据结构,也叫做位段,位域就是把一个字节中的二进制位划分为不同的区域,并指定每个区域的二进制位数和域名,在程序中就可通过域名来操作对应的区域。位域的定义方式与结构体类似,示例如下:
int main(void){
struct bs{
unsigned char a:1; /* 域名为a,占用1个二进制位 */
unsigned char b:3;
unsigned char :4; /* 无名位域,不能使用,一般用来作填充或者调整成员位置 */
unsigned char c:4;
}bit;
bit.a=1; /* 位域赋值方式和结构体一样,注意赋值不能超过该位域的允许范围 */
bit.b=5;
bit.c=9;
printf("%d\n", sizeof(struct bs)); /* 输出结果为:2 */
printf("%d,%d,%d\n", bit.a,bit.b,bit.c); /* 输出结果为:1,5,9 */
return 0;
}
位域有两个特征:位域不能跨字节存储,位域不能跨类型存储,示例如下:
/* 位域不能跨字节存储 */
int main(void)
{
struct bs {
unsigned char a : 3;
unsigned char b : 4;
unsigned char c : 2; /* a和b已经占用了7个二进制位,本字节还剩1个二进制位,存不下成员c,所以成员c直接从下个字节开始存储,跳过当前字节的bit8 */
//unsigned char d : 9; /* 错误,位域的宽度不能大于当前数据类型的宽度,unsigned char为8位数据,所以d的位宽不能大于8 */
return 0;
}
/* 位域不能跨类型存储 */
int main(void)
{
struct bs {
char a : 1; /* 内存对齐,所以预留3个字节,即占1+3个字节 */
int b : 1; /* 占4个字节 */
};
printf("%d\n", sizeof(struct bs)); /* 输出结果为:8 */
return 0;
}
最后再看一个笔试题,如下所示:
int mian(void)
{
unsigned char puc[4];
struct tagPIM{
unsigned char ucPim1;
unsigned char ucData0 : 1;
unsigned char ucData1 : 2;
unsigned char ucData2 : 3;
}*pstPimData;
pstPimData = (struct tagPIM*)puc;
memset(puc, 0, 4); /* 将数组初始化为0 */
pstPimData->ucPim1 = 2; /* 占一个字节,所以puc[0]的值为2 */
pstPimData->ucData0 = 3; /* 3的二进制为0000 0011,而ucData0的位域宽度为1,所以ucData0得到值为1(二进制) */
pstPimData->ucData1 = 4; /* 4的二进制为0000 0100,而ucData1的位域宽度为2,所以ucData1得到值为00(二进制) */
pstPimData->ucData2 = 5; /* 5的二进制为0000 0101,而ucData1的位域宽度为3,所以ucData1得到值为101(二进制) */
/* ucData0、ucData1、ucData2一共占7个二进制位,所以上面三条语句都是在给puc[1]进行赋值,赋值结果为0010 1001,对应的16进制就是0x29 */
printf("%02x %02x %02x %02x\n", puc[0], puc[1], puc[2], puc[3]); /* 输出结果为02 29 00 00 */
return 0;
}
37. 什么是内联函数,有什么用
在程序中,如果某些函数会被频繁的调用,会导致频繁的函数入栈出栈,造成栈空间的大量消耗,甚至导致栈空间枯竭。为了解决这个问题,引入了inline修饰符,用来修饰内联函数。inline
关键字可以提示编译器将被修饰的内联函数的代码直接复制到调用处,而不是使用正常的函数调用机制。从而减少函数调用的开销,并提高程序的执行效率。示例如下:
inline int add(int a, int b) {
return a + b;
}
下面介绍几个内联函数的注意事项:
1) 程序中每一处内联函数的调用都会复制函数代码,所以如果函数代码量太大的话,将其修饰为内联函数会导致程序的总代码量增大,消耗更多的内存空间,所以推荐只有当函数的代码量在10行之内才将其修饰为内联函数;
2) inline只是一种请求,编译器有可能不允许这种请求,并且只在release版本起作用,在debug版本不起作用;
3)如果函数中出现while、switch等复杂的控制语句,或者该函数是递归函数时,就不适合将其修饰为内联函数。
38. const与#define的区别
1) const是一种编译器关键字,而#define是预处理器指令;
2) const在编译阶段进行处理,而#define在预处理阶段进行处理;
3) #define只是简单的字符串替换,没有类型检查,而const有对应的数据类型,编译器会进行类型检查;
4) 若有多个地方使用#define时, 会在内存中产生多个备份,而const定义的只读变量在程序运行过程中只有一份备份;
5) const常量可以进行调试的而#define不能进行调试,因为在预编译阶段就已经替换掉了。
39. 防止头文件被多次包含
#ifndef TEST_H
#define TEST_H
/* 头文件内容 */
#endif
40. float x 与“零值”比较的if语句
计算机中的浮点数通常不能表示所有数,存储的通常是实际的的近似值,比如1.0可能被存储为0.99999。无论是float还是double类型的变量,都有精度限制,所以一定要避免将浮点变量用“==”或“!=”与数字比较,应该设法转化成“>=”或“<=”形式。
if (x == 0.0) /* 错误 */
if ((x>=-EPSINON) && (x<=EPSINON)) /* 正确,其中EPSINON是一个很小的值,也就是允许的误差(即精度),比如0.00001 */
41. 给绝对地址 0x100000 赋值,并跳转执行
(unsigned int*)0x100000 = 1234; /* 给绝对地址0x100000赋值 */
*(void(*)()0x100000)(); /* 跳转到地址0x100000处执行 */
这里再解释下跳转的原理,其实就是将地址0x100000强制转换成函数指针类型,也就是(void(*)())0x100000,再调用该函数*((void(*)())0x100000)(),STM32进行IAP时就是使用这个方法跳转的,相关程序如下:(参考正点原子)
typedef void (*iapfun)(void); /* 定义一个函数类型的参数 */
iapfun jump2app; /* 定义一个函数指针变量 */
/**
* @brief 跳转到应用程序段(执行APP)
* @param appxaddr : 应用程序的起始地址
* @retval 无
*/
void iap_load_app(uint32_t appxaddr)
{
if (((*(volatile uint32_t *)appxaddr) & 0x2FFE0000) == 0x20000000) /* 检查栈顶地址是否合法.可以放在内部SRAM共64KB(0x20000000) */
{
/* 用户代码区第二个字为程序开始地址(复位地址) */
jump2app = (iapfun) * (volatile uint32_t *)(appxaddr + 4);
/* 初始化APP堆栈指针(用户代码区的第一个字用于存放栈顶地址) */
sys_msr_msp(*(volatile uint32_t *)appxaddr);
/* 跳转到APP */
jump2app();
}
}