欢迎光临
我们一直在努力

C语言嵌入式开发相关面试内容总结

本文总结了嵌入式面试常用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 #多文件

本人新手,如有错误,欢迎各位大佬指点。

赞(0)
未经允许不得转载:171主机测评 » C语言嵌入式开发相关面试内容总结
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址