嵌入式八股文(一)——C语言基础篇【理论干货,复习好用】运算符、关键字

前言​

根据各方大佬总结的 c 语言面试问题进行了收集,并根据自己的理解进行整理,本篇章属于知识点汇总,如果有需要的内容可以根据目录跳转。
另外八股文等知识梳理的文章,在下整理都要花费十数个小时,若觉得不错的话还请点赞、收藏、关注在下文章吧,感谢感谢!

一、运算符

(一)运算符优先级

问:运算符的优先级
答:成员运算符 > 单目运算符 >= 算数运算符 > 移位运算符 > 关系运算符 > 逻辑运算符 > 赋值运算符

  1. () 和 [] 优先级最高
  2. 成员运算符:. 成员选择(对象)、 -> 成员选择(指针)
  3. 单目运算符:~ 按位取反、- 负号、++ 自增、-- 自减、& 取值、! 逻辑非、(类型) 强制类型转换、sizeof 长度运算符
    ! 表示取非,对于整形变量,非 0 数字取非为 0,0 取非为 1
    ~ 表示取反,代表位取反& 表示取地址符
    * 作用于指针时表示访问指针所指向的对象
#include <stdio.h>

int main()
{
   /*  Write C code in this online editor and run it. */
   int a = 20;
   printf("%d\n",a);     // 20
   printf("%d\n",&a);    // a的地址值,比如0093F968

   int* b = &a;
   printf("%d\n",b);     // a的地址值
   printf("%d\n",*b);    // 20

   int c = !a;    
   printf("%d\n",c);     // 0

   int d = ~a;
   printf("%d\n",d);    // -21
   
   return 0;
}
  1. 算数运算符:+ 加法、- 减法、* 乘法、/ 除法、% 取余、++ 自增、-- 自减
  2. 移位运算符:<< 左移、>> 右移
  3. 关系运算符:> 大于、< 小于、== 等于、 != 不等于、 >= 大于等于、<= 小于等于
  4. 逻辑运算符:&& 逻辑与、|| 逻辑或、& 位与、| 位或、^ 异或
  5. 赋值运算符:? : 三目运算符、= 赋值、/= 除后赋值、*= 乘后赋值、%= 取模后赋值、+= 加后赋值、-= 减后赋值、<<= 左移后赋值、>>= 右移后赋值、 &= 位与后赋值、|= 位或后赋值、^=异或后赋值

% 操作要求左右的值均为整数


(二)原码、反码、补码

问:请说说原码、反码、补码

答: 整形数值在计算机的存储中,最左边的一位代表符号位,0 正 1 负

  • 原码:二进制的数,比如 10 的原码为 0000 1010
  • 反码:正数的反码与原码相同,比如 10 的原码为 0000 1010,反码则为 0000 1010;
    负数的反码 0 变 1,1 变 0(符号位不变),比如 -10 的原码为 1000 1010,反码则为 1111 0101。
  • 补码:正数的补码与原码相同,比如 10 的原码为 0000 1010,补码则为 0000 1010
    负数的补码为反码加1,比如 -10 反码为 1111 0101,补码为 1111 0110

二、关键字

(一)continue

  1. 作用: 跳过本次循环体中余下未执行的语句,立即进行下一次循环条件判定。
  2. 注意: continue 只能用在循环语句中,且并不会终止循环

(二)break

  1. 作用: 跳出本层循环
  2. 注意:
  • break 能用在循环语句和 switch 语句中,其作用是跳出当前循环或跳出当前 switch 语句体
  • 如果存在多重循环比如for(){switch case: break},则仅跳出 switch 语句,for循环不受影响
  • 使用 switch 一定要加 break,除非有特殊需要

(三)return

结束当前循环,退出函数,用在函数体中则返回特定值


(四)goto

无条件跳转,几乎不使用。


(五)volatile

  1. 作用: 用于修饰变量和指针,告诉编译器该变量是容易发生变化的,不能对该变量进行优化,每次取值必须从内存中取值而不是直接去取之前在寄存器中的值
  2. 注意: 首先要清楚在 c 语言中,变量是存储在内存中的,而编译器会将对内存的多次读取操作替换为单次读取操作,以减少对内存的访问次数。比如下列代码:
volatile int a = 20, b, c;
b = a;
c = a;

上述代码如果不加 volatile 编译器的执行流程如下:

  1. b=a; 先从a的内存中取值存放到寄存器,再把寄存器的值给存到b的内存
  2. c=a;把寄存器的值给存到b的内存

这里可以看出编译器对 c=a;进行了优化,不再执行从 a 的内存中取值,而是直接从寄存器中取值 如果这段时间内 a 发生了变化(比如中断触发、多线程共享变量修改了a的值),那么 c 就不能得到最新的值

这时候就需要使用 volatile 告诉编译器,不要对变量 a 进行优化,每次都是从内存中取出 a 的值

  1. 使用场景:
  • 多线程使用共享变量:因为多线程是多核操作,同时进行
  • 中断:中断程序会修改其他程序中使用的变量
  • 硬件寄存器:因为寄存器随时会被修改,好比AD转换的寄存器,随时会因 为电压变化而修改
  • 外部任务会修改变量

(六)struct 结构体

问:struct 与 typedef struct 的区别
答:typedef 是类型定义的意思,用在结构体中是给结构体取别名,定义方式如下

#include <stdio.h>
#include <string.h>

typedef struct Student
{
	char name[20];
	int age;
	int sex;
} stu; // stu 即结构体别名

struct Teacher
{
	char name[20];
	int age;
} Li; // 结构体成员

int main() {
	stu Zhangsan = {"zhangsan", 18, 1}; // 使用typedef struct可以直接使用结构体别名创建新的结构体成员
	stu Lisi;
	strcpy(Lisi.name, "lisi"); // 使用strcpy复制字符串
	Lisi.age = 17;
	Lisi.sex = 0;

	struct teacher Zhang; // 直接使用struct创建结构体成员

	return 0;
}

问:结构体大小如何计算
答:

  1. 对于不同的操作系统,个别数据类型的大小有区别,比如 long 在 32 位操作系统中为 4 字节,在 64 位中就是 8 字节
  2. 计算大小时需要考虑字节对齐问题:
    a. 结构体成员的每个变量的首地址,必须是成员变量中“最大基本数据类型成员所占字节数”的整数倍(对齐)
    b. 每个变量类型的偏移量必须是该变量类型的整数倍(对齐)
//最大基本数据类型为 double,占 8 字节,其他的结构体变量占用空间均为 8 的整数倍
struct data {
    double d1;    // 大小为8字节,从地址0开始,占用到地址8
    int d2[5];    // 大小为 4x5=20 字节,首地址为8,为8的倍数,占用到地址28
    char d3;      // 大小为 1,首地址为28,不是8的倍数,偏移地址到32,占用到地址33
};               // 当前结构体大小为33,不是8的倍数,取8的倍数,所以结构体大小为40


(七)union 联合体

union 的使用:
union 的用法和 struct 差不多,区别是同一个 union 成员的不同的成员变量共享同一块内存空间(起始地址相同),同一时刻只能存在一个成员变量
这边可以理解为张三和李四是穿同一条裤子的,张三穿了裤子,李四就没裤子穿了,同一时刻张三和李四最多只有一个人能穿裤子。
举例个代码:

#include <stdio.h>

union Age
{
	int zhangsan;//成员变量张三
	int lisi;//成员变量李四
}; 

int main() {
	//union Age age = {18, 20};//同一时刻只能存在一个成员变量,所以这里运行会报错
	union Age age;
	age.zhangsan = 18;
	age.lisi = 22;
	printf("%d, %d\n", age.zhangsan, age.lisi);	//运行结果 22, 22,说明后赋的值会将先赋的值覆盖
	printf("%p, %p\n", &age.zhangsan, &age.lisi);//获取成员变量的地址值,结果0x7ffc6b832f5c, 0x7ffc6b832f5c,说明起始地址相同
	printf("%d\n", sizeof(union Age));//获取联合体大小,结果为4,说明该联合体只有一个int类型大小
	return 0;
}

问:union 联合体和 struct 结构体的区别

答:

  1. 对于联合体同一成员的所有成员变量都共享一块内存,而结构体是将所有成员变量内存的叠加,需要考虑字节对齐问题。
  2. 对于联合体来说,只要你修改了里面的成员变量,其他的成员变量也会被修改,而结构体间的成员变量是互不影响的。

问:联合体一般可以用来做什么?

答:

  1. 获取数据的高低位,或检查大小端问题

在实际的编程当中,为了确定当前计算机使用的存储模式是大端存储还是小端存储,可以使用union进行简单高效的判断
大端存储模式:高字节存放在低位地址,低字节存放在高位地址
小端存储模式:低字节存放在低位地址,高字节存放在高位地址
ps:x86架构和多数ARM、DSP都为小端存储模式,keil C51则为大端存储模式,有些架构还可以自主切换

#include <stdio.h>

union GetByte
{
	short twoByte; //short 类型占 2 字节
	char myByte[2];//myByte[0] 和 myByte[1] 分别占用 1 字节
}getbit; 

int main() {
	getbit.twoByte = 0x1122;
	printf("%x, %x\n", getbit.myByte[0], getbit.myByte[1]);	//运行结果 22, 11;说明本计算机是低字节存放在低位,高字节存放在高位,为小端存储
	return 0;
}
  1. 节省内存空间(计算机早期内存寸土寸金时才考虑的事情)

如果有不理解的地方,可以看看孤烟大佬的视频


(八)enum

int main()
{
	enum { a,b=5, c, d=4, e };
	printf("%d, %d, %d, %d, %d",a, b, c, d, e);
	//输出结果0, 5, 6, 4, 5
}
  • 在枚举变量的值默认为前一个变量的值+1
  • 而如果第一个枚举变量没有被赋值,默认值为 0
  • b == e == 5 说明枚举的值是可以重复的

(九)typedef

问:typedef 和 #define 的区别
答:

  • #define 是 c 语言中定义的语法,是预处理指令,在预处理时进行简单而机械的字符串替换,不作正确性检查,只有在编译已被展开的源程序时才会发现可能的错误并报错。可作用于类型定义、常量、变量等。
  • typedef 是关键字,在编译时处理,有类型检查功能。它在自己的作用域内给一个已存在的类型一个别名,但不能在一个函数定义里面使用 typedef。用 typedef 定义数组、指针、结构等类型能够使程序书写简单,也能使意义明确,增强可读性。可作用于类型定义上。
#definetypedef
预处理指令关键字
在编译前不作检查,编译时可能检查到错误有类型检查功能
无作用域限制有作用域限制
可定义类型、常量、变量等的别名可定义类型的别名
  • 定义指针区别
#define myptr int* p
myptr a,b;//a 是 p 的指针变量,b 是 int 变量
typedef int* myptr;
myptr a,b;//a 是 int* a,    b是 int* b

(十)const

  1. const 定义变量,表示该变量是个常量(并不是真正意义上的常量),不允许被修改,如果存在修改会在编译时报错
const int a = 100;
int const b = 10;

a = 200; //报错

char str[b] = "abcdefg";//报错,因为被const修饰的变量b并不是真正意义上的常量,无法分配准确的大小给str
char num[b];//数组不初始化是不会报错的
  1. const 修饰函数参数,表示函数体内不能修改该参数的值
void test1(const int x, int y)
{
	x = 10; //编译时报错
	y = 10;
}
  1. const 修饰函数返回值,赋值返回值的变量需要被 const 修饰
const int * test(void)
{
	return 1;
}

int main(){
	const int *num1 = test();
	int *num2 = test();//编译时报错
}
  1. const 修饰指针
int main(){
	//*在const后,即常量指针,表示指针指向的内容不可以修改,但是指针的地址可以更改
	const * int a;
	int const *b;
	
	//*在const前,即指针常量,表示指针指向的内容可以修改,但是指针的地址不可以更改
	int * const c;

	//指针的地址和内容都不可以被修改
	const int * const d;
}
  1. const 修饰的变量存放位置
    对于const修饰的局部变量:存放在栈中,代码结束就会释放,在C语言中可以通过指针修改里面的值
    对于const修饰的全局变量(已初始化的)存放在只读数据段,不可以通过指针修改里面的值,未初始化的存放在.bss

(十一)extern 链接阶段

  1. extern 声明外部变量:(保存在数据段)
    在文件 a.c 定义变量并声明全局变量:int a = 10; 这时会建立存储空间
    在文件 b.c 对变量 a 进行声明,注意声明时不能初始化。
extern int a;
extern int a = 30; //错误,生命时不能初始化

注意:全局变量最好不要写在 .h 头文件中,如果写在了 .h 文件中,多个 .c 文件对这头文件进行包含,编译器会报错 multiple define 多个定义。

如果有些全局变量经常在多个文件中被需要,普遍的做法如下:

// file1.c 文件
int globalVariable = 10; // 定义全局变量
// golbal.h 文件
#ifndef GLOBAL_H_
#define GLOBAL_H_

extern int globalVariable; // 声明全局变量

#endif /* GLOBAL_H_ */
//file2.c 文件
#include "global.h"
#include <stdio.h>

int main() {
    printf("%d\n", globalVariable);
    return 0;
}

如上,在golbal.h文件中将常出现的变量进行声明,在需要全局变量的文件中包含即可,如果需要增加代码可读性和防止特殊错误,你也可以在 file2.c 中再进行一次声明。

extern 的作用主要是告诉编译器我在其他文件定义了个全局变量,并且已经分配了空间,不需要再为该变量申请空间

  1. extern 声明外部函数:方式和声明外部变量一样。
extern void function1(int x, int y);
  1. extern “C”
    该作用是实现在 c++ 中调用 c 语言代码,告诉编译器这部分代码要是有 c 编译

(十二)register

register 只能用于修饰变量(且不能是浮点数),被修饰的变量会被存储在 CPU 内部的寄存器中,而不是内存中。
寄存器有两个特点,一个是运算速度快,一个是不能取地址。
被 register 修饰的变量无法取地址,取地址会编译报错

(十三)auto

一般情况下,我们没有特别声明的局部变量,都默认为 auto 类型,存储在栈中

(十四)static

  1. static 修饰变量
    静态全局变量:作用域当前文件中,其他文件无法访问这个变量,程序运行时被初始化,有且只初始化一次。
    静态局部变量:作用域在当前函数中,函数外无法访问,函数被调用时被初始化,有且只初始化一次,生命周期不会随函数结束而结束,直到程序结束
#include <stdio.h>

int staticNum()
{
	static int num = 0;
	return ++num;
}

int main()
{
	int sum = 0;
	for(int i=0; i<10; i++)
	{
		sum = staticNum();
	}
	printf("%d\n", sum);	//打印结果为 10
	return 0;
}

问:静态全局变量与静态局部变量的区别
答:作用域不同,静态全局变量的作用域在当前文件中,静态局部变量的作用域在当前函数体中。

问:普通全局变量和静态全局变量的去呗
答:作用域不同,静态全局变量的作用域在当前文件中,普通全局变量的作用域为当前整个项目。


  1. static 修饰函数
    在函数返回值前加上 static 关键字,函数即被定义为静态函数,静态函数的作用域为当前文件,在其他文件也可使用同名的静态函数

(十五) switch case

注意:

  1. switch里面不能是浮点数、double,可以是表达式,但是结果不能是浮点数或double
  2. 要注意 case 语句后面是否有break,如果没有,就会从找到的case语句一直执行到停止
  3. case不能是 “abcdef” 这样的字符串,但是可以是 ‘a’ 这样的单字符,因为字符最终也是整数

(十六)do while

先执行 do {} 中的内容,在执行 while() 里的条件判断是否符合。

(十七) sizeof

问: sizeof() 和 strlen() 的区别
答:
1. sizeof 是关键字,strlen 是函数
2. sizeof 用于计算占用内存大小,strlen 用来计算字符串的长度
3. sizeof 会计算 \0 的空间,strlen 不需要

#include <stdio.h>
#include <string.h>

int main()
{
	char c[5] = {1,2,3};
	printf("%d\n", sizeof(c)); //5
	printf("%d\n", strlen(c)); //3
	
	int i[5] = {1,2,3};
	printf("%d\n", sizeof(i)); //20
	return 0;
}

4. sizeof 在编译时计算,strlen 在运行时计算


问:如何不适用 sizeof 求数据类型字节的大小
答:

//(char*)强转,便于在指针运算中以字节为单位进行计算。
//&value+1 则能够获取value结束后的地址
//&value 获取value的起始地址
#define	mysizeof(value)		(char*)(&value+1)-(char*)(&value)

问:strlen(“\0”) == sizeof(“\0”); 是否正确?
答:

printf("%d\n", sizeof("\0"));	//2
printf("%d\n", strlen("\0"));	//0

printf("%d\n", sizeof('\0'));	//4
printf("%d\n", strlen('\0'));	//报错空指针

问: sizeof(a++); a最后的结果会改变吗?
答:不会,sizeof 只会求所占内存大小,不会进行表达式运算

int a = 2;
printf("%d\n", sizeof(a++));	//4
printf("%d\n", a);	//2

问:计算数组大小
答:

	char ch[]  = "hello";
	char str[10] = {'h','e','l','l','o'};
	printf("%d\n",sizeof(ch));//6
	printf("%d\n",strlen(ch));//5
	
	printf("%d\n",sizeof(str));//10
	printf("%d\n",strlen(str));//5

问:sizeof(void); 结果
答:报错,或者为1


嵌入式C语言八股文是指在嵌入式系统开发中常见的基本知识点和技能要求的简要总结。下面是嵌入式C语言八股文的主要内容: 1. 数据类型:包括基本数据类型(如int、char、float等)和派生数据类型(如数组、结构体、枚举等),掌握各种数据类型的使用方法和特点。 2. 运算符:熟悉各种算术运算符、逻辑运算符、位运算符等,掌握它们的优先级和结合性,能够正确使用运算符完成各种计算任务。 3. 控制语句:包括条件语句(if-else语句)、循环语句(for、while、do-while循环)、选择语句(switch-case语句)等,掌握这些语句的使用方法和注意事项。 4. 函数:了解函数的定义和调用,能够编写函数并正确使用函数参数和返回值,理解函数的作用域和生命周期。 5. 数组和指针:掌握数组和指针的定义和使用,了解数组和指针在内存中的存储方式,能够通过指针进行数组的访问和操作。 6. 文件操作:了解文件操作的基本流程,包括文件的打开、读写和关闭,理解文件指针和文件访问模式的概念。 7. 中断处理:了解中断的基本概念和原理,能够编写中断服务程序(ISR)并正确处理中断请求。 8. 程序调试:掌握常用的调试技巧和工具,能够使用调试器进行程序的单步执行、观察变量值等操作,能够分析程序运行过程中的错误和异常。 以上是嵌入式C语言八股文的主要内容,掌握这些知识和技能,可以帮助你在嵌入式系统开发中更好地应对各种任务和挑战。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值