本文总结了嵌入式面试常用C语言基础内容。
1.static关键字的作用
修饰全局变量或函数:将其外部链接域限制在内部,这样仅当前源文件可访问,避免多文件编译时的命名冲突,防止两个文件都定义了同名全局变量或函数造成问题。
static int __init hello_init(void) {
printk(KERN_INFO "Hello Kernel!\\n");
return 0;
}
修饰函数内部的局部变量:存储位置将从栈区转移到静态存储区,仅在程序运行初期初始化一次,常用作函数调用计数器,用于记录函数被调用的总次数。
2.extern关键字的作用
声明该变量或函数的定义在其他源文件中,让编译器去其他文件找这个变量或函数的定义。函数默认就是extern,所以几乎省略。
3.const关键字的作用
修饰普通变量:约束变量为只读,编译阶段将会阻止直接赋值修改,但有可能通过指针绕过。下图代码的答案有可能为20,也有可能直接崩溃。
const int a = 10;
int *p = (int*)&a;
*p = 20;
printf("a=%d\\n",a);
修饰指针变量:分成两种情况。
下面这种是表示p指针指向的内容为只读,p指针可以改变指向的内容,但是p指向的内容是不可以修改的。
const int *p;
下面这种情况就是指针本身只读,p不能指向其他内容,但是p指向的内容是可以修改的。
int *const p;
修饰函数参数:函数内部不可以修改传入的参数值。
void print_str(const char *str) {
//str[0] = 'A'; 错误操作,不可以修改
}
4.volatile关键字的作用
volatile表示该变量的取值可能被当前程序流之外的因素修改,通常是硬件中断或其他线程导致。所以禁止编译器对这个变量进行优化,每次使用不许从内存里取出变量的值,不能从寄存器里取。下面的代码输出的是20,但是,如果没有volatile且函数并不在同一个文件中,就有可能输出15。
int func(int a){
return a+10;
}
void change_value(volatile int *p) {
*p = 10;
}
int main() {
volatile int a = 5;
change_value(&a);
printf("func(a)=%d\\n", func(a));
return 0;
}
5.typedef关键字的作用
主要作用是将类型进行重命名,主要是为简化命名,尤其是被unsigned和signed修饰的,或者是struct,union,enum定义的新类型。这个要重点区分typedef和#define,typedef是类型别名,由编译器处理,而#define是文本替换,由预处理器处理。
typedef unsigned int uint;
uint a = 10;
#define NUM 10
unsigned int b = NUM;
6.sizeof运算符的作用
主要作用是计算操作数的类型,变量或字面量在当前编译环境下占用的字节数。同时注意sizeof是运算符而不是函数,因为函数名必须有地址,而sizeof没有。
注意当sizeof计算的对象是指针时,如果是32位计算机,答案就是4字节,如果是64位计算机的话,答案就是8字节。
int main(){
int arr[10] = {0};
int *p = arr;
printf("%zu\\n",sizeof(arr)); //40=4*10
printf("%zu\\n",sizeof(p)); //8或4,指针
return 0;
}
这个也要注意区分sizeof()和strlen(),sizeof()计算字符串的字节数,会加上‘\\0';而strlen()只计算字符数。
int main(){
char *str = "guan";
char arr[5] = "guan";
printf("%zu\\n",sizeof(str));//8,指针
printf("%zu\\n",sizeof(arr));//5,4个字符+\\0
printf("%zu\\n",strlen(str));//4,4个字符
printf("%zu\\n",strlen(arr));//4,4个字符
return 0;
}
7.预处理指令
C语言预处理指令有很多,首先是#define,主要是文本替换,也可以加上参数,但是要注意表达式的整体要加上括号,避免文本替换时的优先级问题。
#define MAX(a,b) (a>b ? a : b)
然后是#include ,要注意如果还库文件,要使用<>,但如果是自己定义的,要使用双引号。
#include <stdlib.h>
#include "myheadfile.h"
还有条件编译相关的命令,主要是#ifdef,#endif,#define配合使用,下面的代码中就是通过定义DEBUG这个宏,来控制Dubug这个函数能不能使用,也可以控制一个函数中国那个部分执行。
#define DEBUG
#ifdef DEBUG
void Debug(){
printf("[DEBUG] pattern start…\\n");
}
#endif
int main(){
#ifdef DEBUG
Debug();
#endif
return 0;
}
8.函数指针和回调函数
函数指针的本质就是函数名是函数的首地址,就像数组一样。所以可以用指针调用函数。可以看代码例子,这个展示了函数指针的基本使用方法。
int func(int a,int b){
return a+b;
}
int main(){
int(*p)(int,int) = func;
printf("%d\\n",p(2,3));
return 0;
}
Linux应用层中的pthread_create()就是使用函数指针的典型函数。这个函数的第三个参数是一个函数指针,第四个参数将作为第三个函数指针指向的函数的参数。同时要注意函数指针的类型转换,函数原型如下:
int pthread_create(pthread_t *thread,const pthread_attr_t *attr,
void *(*start_routine)(void *),void *arg);
注意函数指针数组的声明方式如下:
//假设有两个函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a – b; }
//函数指针数组(两种都可以)
int (*fparr[2])(int, int) = {add, sub};
int (*fparr[])(int, int) = {add, sub};
9.函数的递归调用
递归本质就是函数在自身内部调用自己。其核心思路就是没有自己动手写一个栈结构,而巧妙利用系统本身就是利用栈调用函数,从而大大简化代码。
typedef struct TreeNode{
int val;
struct TreeNode *left;
struct TreeNode *right;
}TreeNode;
void PrintTree(TreeNode *root){
if(root == NULL) return;
PrintTree(root->left);
PrintTree(root->right);
printf("%d\\t",root->val);
}
上面是一个经典的二叉树的遍历算法,分为三种前序遍历,中序遍历,后序遍历。上面展示后序遍历算法,其他两种遍历方法类似。但是使用递归算法一定要注意,若递归太深,可能导致栈溢出。
10.位运算
C语言提供了大量位运算操作,包括&,|,^,~,<<,>>,分别是按位与,或,异或,非,左移,右移。下面介绍几种有趣的操作:
(x >> n) & 1; //取出x的第n位
x |= (1 << n); //把x的第n位设置为1
x &= ~(1 << n); //把x的第n位设置为0
x ^= (1 << n); //把x的第n位反转
左移一位相当于这个数乘2,右移一位相当于这个数除2。下面是计算一个数二进制多少位的函数。
int bit_len(unsigned int x){
int len = 0;
while(x > 0){
x = x >> 1;
len++;
}
return len;
}
下面是一个取绝对值的函数,利用了负数存储的是补码,且最高位是符号位。
int abs(int x){
int mask = x >> 31; //正数mask为0,负数mask为-1
return (x + mask) ^ mask; //-1其实是0xFFFF FFFF
}
11.内存相关的问题
首先介绍不同类型所占的字节数,char为1字节,short位2字节,int为4字节,long为8字节,而且不论是signed还是unsigned。
下面讲下malloc(0)的问题,标准允许,但是结果由编译器和库决定。可能有两个返回值,第一个是返回NULL,第二个则是返回一个“非空但是不能读写”的指针。但是可以free这个指针。
int main(){
void *p = malloc(0);
free(p);
p = NULL;
return 0;
}
12.C语言编译流程
C程序编译流程一共有4步,分别是预处理,编译,汇编,链接。
| 预处理:处理#开头的预处理指令 |
| 编译:把C代码编译为汇编代码,文件后缀为 .s |
| 汇编:把汇编代码汇编为二进制目标文件,文件后缀为 .o |
| 链接:把多个 .o文件+系统库/静态库/动态库链接在一起,生成可执行文件 |
相关GCC指令如下:
#预处理
gcc -E test.c -o test.i
#编译
gcc -S test.i -o test.s
#汇编
gcc -c test.s -o test.o
#链接
gcc test.o -o test #单文件
gcc test1.o test2.o test3.o -o test #多文件
本人新手,如有错误,欢迎各位大佬指点。





