嵌入式八股文
运算符
算术运算符
- +:加法
- -:减法
- *:乘法
- /:除法
- %:取模
关系运算符
- ==:等于
- !=:不等于
>
:大于- <:小于
>=
:大于等于<=
:小于等于
逻辑运算符:
- &&:逻辑与
- ||:逻辑或
- !:逻辑非
位运算符
- &:按位与
- |:按位或
- ^:按位非
- ~:按位取反
- <<:左移
>>
:右移
赋值运算符
=
:简单赋值+=
:加后赋值-=
:减后赋值*=
:乘后赋值/=
:除后赋值%=
:取模后赋值<<=
:左移后赋值>>=
:右移后赋值&=
:按位与后赋值|=
:按位或后赋值^=
:按位异或后赋值
条件运算符
- ?::三元运算符
其他运算符
- sizeof:返回变量或者数据类型的字节数
- &:取地址运算符
- *:取值运算符
运算符优先级
C语言中的运算符优先级决定了表达式中运算的顺序。
- 括号()的优先级最高
- 算数运算符的优先级高于关系运算符
- 简单的记:()》 !》算术运算符》关系运算符》&& 》|| 》复制运算符
源码、补码、反码
在计算机中,源码、补码、反码是表示有符号整数的三种形式。他们只要用于解决计算机中负数的表示和运算问题。
- 源码:最直观的表示方式。用一个符号位和数值为表示一个有符号的整数。
- 反码:反码是为了解决源码中加减运算的问题而提出的。
- 正数的反码和源码相同
- 负数的符号位位1,数值按位取反。
- 补码:解决了反码中两个零的问题。
- 正数:补码与源码相同。
- 负数:符号位为1,数值按位取反加一。
在源码中,加减的运算规则:
- 判断符号位
- 如果符号位相同:直接对数值进行加减法运算。
- 如果不相同,需要比较两个数的绝对值,用较大的绝对值减去较小的绝对值,结果的符号位和绝对值较大的数相同。
- 如果加法运算中产生了进位,则需要调整结果。
在源码运算时,运算比较复杂,而且存在两个0的问题,在源码中存在+0和-0,导致比较时和判断时需要额外的处理。反码和补码通过改进负数的表示方法,简化了加减运算,成为现代计算机中表示有符号整数的标准方法。
在反码中,核心思想是将减法转换为加法。在反码中,A - B
可以转换为 A + (-B)
,其中 -B
是 B
的反码表示。
题目:
int a = 5,b =3; !a && (b++); printf("%d,%d\n", a, b); //5,3
在 C 语言中,当有符号整数和无符号整数进行运算时,**有符号整数会被隐式转换为无符号整数**,然后再进行运算。 11111111 11111111 11111111 11111001 (-7 的补码) +00000000 00000000 00000000 00000011 (3 的无符号表示) ------------------------------------- 11111111 11111111 11111111 11111100 (结果的二进制表示) - 如果解释为**无符号整数**,其值为 `4294967292`。 - 如果解释为**有符号整数**(补码),其值为 `-4`。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
!a是对a进行取反,a=5是正数,!逻辑非运算符,!a=0;
&&逻辑与,但是!a为false,所以(b++)不执行
所以最后a=5,b=3.!a是对a进行逻辑判断,不赋值。所以a不变。
* ```
#include <stdio.h>
int main()
{
int a = -7;
unsigned int b = 3;
if( (a + b) > 0) //如果打印‘+’说明 a+b > 0,即有符号转⽆符号运算的时候
{
printf("+\n"); //+
}
printf("%u\n",a + b);//4294967295
printf("%d\n",a + b);//-1
return 0;
}
关键字
Continue
在C语言中,continue是一个控制语句,主要用于循环结构(比如for、while、do-while循环)。它的作用是跳过当前循环的剩余部分,直接进行下一次的迭代。
Break
在C语言中,break是一个控制语句。
作用:
- 终止循环:在循环结构中,break语句会立即终止整个循环。
- 退出switch语句:在switch语句中,break用于结束当前的case块。
注意点:
- break在多层循环嵌套中,只退出当前循环。
return
用法和作用:
- 结束函数执行,return语句会立即结束当前函数的执行,并将控制前返回到调用函数的地方。
- 返回值:可以将一个值或者表达式的计算结果返回给调用者。如果函数声明了返回类型,通常返回一个同样类型的一个值。
- 多返回点:一个函数可以有多个return语句。
注意点:
- 必须返回值:如果函数声明了非void的返回类型,则在所有可能的执行路径上都需要有一个return语句,否则编译器可能会发出警告或错误。
- 返回类型一致性:返回的值或表达式必须与函数声明中的返回类型相匹配,或者通过隐式转换匹配。
- 资源管理:在return之前,确保释放或正确管理分配的资源(如内存、文件句柄等)。
- 优化:编译器可能会对return语句进行尾调用优化,特别是在递归函数中,这可以提高性能。
- 早期返回:有时为了减少嵌套或提高代码可读性,开发者会使用return提前结束函数执行,这被称为“提前返回”。
volatile
在C语言中,volatile是一个类型限定符,用于告知编译器某些变量的值可能会在代码的显示操作之外发生变化。
用法和作用:
- 防止优化:volatile 告诉编译器不要对该变量进行优化。编译器在处理非volatile变量时,可能会假设变量值在没有显式修改的情况下不会改变,从而进行优化(如寄存器缓存)。对于volatile变量,编译器每次使用时都会从内存中重新读取值。
- 硬件交互:常用于与硬件相关的编程,如访问内存映射的I/O设备、硬件寄存器、中断等,这些地方的值可能在软件控制之外被改变。
- 多线程环境:在多线程或多任务的系统中,一个线程可能修改一个变量,而另一个线程需要看到这个变化。volatile确保线程能够读取最新的值。
注意点:
- 不提供原子性:volatile 只保证编译器不优化对变量的访问,但不提供任何形式的原子操作或线程安全。如果需要这些保证,需要使用其他机制如互斥锁或原子操作。
- 不应滥用:滥用volatile可能导致性能下降,因为它会阻止编译器进行一些优化。应该只在确实需要的地方使用它。
- 与const一起使用:可以声明const volatile变量,这表示变量值可能被外部因素改变,但程序本身不会去改变它。
- 内存屏障:虽然volatile确保每次读取或写入变量时都会从内存获取或更新值,但它不是内存屏障。复杂的多线程场景可能需要使用内存屏障指令。
- 理解与volatile相关的优化:虽然volatile会阻止某些优化,但理解编译器的优化策略对于正确使用volatile非常重要。例如,编译器可能仍然会重排序指令,除非有显式的内存屏障。
题目:
1 | Volatile int a=20,b,c; |
- 如果没有volatile:
- 编译器读取 a 的值 20 到一个寄存器中(假设编译器决定这样做)。
- 编译器可以直接从寄存器中取值,将 20 赋给 b。这意味着没有必要再次从内存中读取 a。
- 同样,编译器可能会选择从寄存器中取值,而不是从内存中再次读取 a,因为没有 volatile 表明 a 的值可能在两次读取之间发生了变化。因此,c 也会得到 20。
- 如果有volatile:
- 都是从内存中读取a的值。
计算占用空间的大小问题
在C语言中,计算数据结构占用空间的大小是编程中一个常见任务。理解如何计算这些大小对于内存管理、性能优化和理解程序至关重要。
字节:C语言中,最小的可寻址内存单元通常为字节。
对齐:为了提高内存访问效率,编译器可能对数据进行对齐,使得数据的内存地址满足某些规则。
使用sizeof操作字符可以计算其参数的内存大小,返回是size—t类型。
对于不同的操作系统,个别数据类型大小不一样。
- char:通常是1字节
- short:通产是2字节
- int:可能是2、4、8字节,通产为4字节
- long:可能是4、8字节
- long long:通常为8字节
- float:通常为4字节
- double:通常为8字节
计算的时候需要考虑字节对齐的问题:
- 所占空间必须是成员变量中字节最大的整数被。
- 每个变量类型的偏移量必须是该变量类型的整数倍
- 对于来联合体来说,是所有变量成员公用一块内存,结构体则是内存的叠加。
题目:
以下哪个语句可以正确计算数组arr的总大小,假设arr是一个包含10个元素的int型数组? A) sizeof(arr[10]) B) sizeof(int) * 10 C) sizeof(arr) D) sizeof(arr) / 10
解答: * 联合体的内存大小取决于其成员最大的那个成员,因为所有成员共享一块内存。 * char占用1字节,int占用4字节,所以答案为4字节。1
2
3
4
5
6
7
8
9
10
解答:
* A选项,给出的是数组中第11个元素的大小,但是arr只有10个元素。
* B选项,结果是对的,但是过程不对。
* C选项,真确,sizeof作用于数组名时,返回的是整个数组的大小
* D选项,可以得到单个元素的大小。
* ```
联合体union { char c; int i; }的大小在大多数系统上是____字节。声明一个包含5个double型元素的数组,并使用sizeof运算符计算并打印出这个数组的总大小。
1
2
3
4
5
6
解答:
* ```
double arr[5];
printf("%zu", sizeof(arr)); // 使用%zu来打印size_t类型的值编写一个C程序,定义一个结构体,包含一个char,一个int和一个double。然后计算并打印这个结构体的大小,以及各个成员的单独大小,验证是否考虑了对齐。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
解答:
* ```
#include <stdio.h>
struct arr {
char a;
int b;
double c; // 修正了缺少的成员名
};
int main() {
printf("Structure size: %zu bytes\n", sizeof(struct arr));
printf("Size of char: %zu bytes\n", sizeof(char));
printf("Size of int: %zu bytes\n", sizeof(int));
printf("Size of double: %zu bytes\n", sizeof(double));
if (sizeof(struct arr) == 16) {
printf("字节对齐已考虑。\n");
} else {
printf("未考虑字节对齐或对齐不同。\n");
}
return 0;
}
Typedef和define
#define
定义:#define是C语言预处理指令,用于定义宏。宏定义允许你给一个名称或者表达式指定一个值或者代码片段,在编译前进行文本替换。
例子:
文本替换:宏定义只是简单的文本替换,不进行类型检查。
1
#define PI 3.1415926
宏函数:定义带参数的宏,即宏函数
1
#define SQUARE(X) ((X) * (X))
作用域:宏定义的作用域式从定义点开始,直到文件结束或者遇到#undef指令为止。
typedef
定义:用于存在的类型定义一个新的名称的关键字。它在编译时创建一个新的类型名,而不是进行文本替换。
类型别名
1
2typedef int myint;
myint number = 5;结构体和联合体的别名
1
2
3
4
5typedef struct {
int x;
int y;
} point;
point p;// 比写 struct { ... } p; 更简洁指针类型别名
1
2typedef char * string;
string str = "hello";
Const
用于声明常量。其主要用途是告知编译器某些值在程序的执行过程不应该再改变。
基本用法:
常量变量:const可以用来定义一个不能被改变的变量。
1
const int max = 100;//max是一个常量,并且不可被修改。
指针和常量
指向常量的指针。
1
2
3
4int a = 10;
const int *ptr = &a;
ptr = &b;
//const 修饰的是int *,所以ptr的类型不变。常量指针
1
2
3
4int a = 10;
int *const ptr = &a;
*ptr = 20;
//const修饰的ptr,所以变量ptr不可改变。指向常量的常量指针
1
2int a = 10;
const int * const ptr = &a;记忆技巧
- const在前(紧挨着类型名):表示指向的数据是常量。
- const在后(紧挨着指针名):表示指针本身是常量。
使用场景
接口设计:在函数参数中使用const可以保证函数不会修改传入的参数。
1
2
3void print_array(const int *arr, int size) {
// 保证函数不会修改arr的值
}保护数据:使用const来保护数据不会被轻易的改变。
1
const double PI = 3.14159; // 常量PI不应被修改
全局变量:定义全局变量,避免在多个文件中重复定义。
1
2
3
4
5// in a header file
extern const int MAX_USERS;
// in one source file
const int MAX_USERS = 1000;
注意事项
const必须在定义时初始化。
与volatile连用:const volatile 表示一个变化值不可由程序修改但是可能由硬件或者其他外部原因而改变。
1
const volatile int *ptr; // ptr指向的值可能由外部更改,但程序不能修改它
与宏定义连用:const提供了安全性。
分析如下const关键字的用法(刀哥版)
1
2
3
4const int* ptr;
int const* ptr;
int* const ptr;
const int* const ptr;1
2
3
4
5
6
7
8
9
10
11const int* ptr
指向常量的指针,指针指向的是常量,指针可变
int const* ptr
指向常量的指针
int * const ptr
指向整型的常量指针,指针指向的是整形的,可变;但是指针是常量,不可变
const int * const ptr
指向常量的常量指针,指针和指针指向的数据都是常量,不可变。
extern(外部链接)
定义:extern是一个关键字,用于声明变量或者函数。其主要目的是告知编译器这个变量或者函数在别处定义。
基本用法
变量
声明但不定义:extern用于在某个文件中声明一个变量,但是这个变量的定义在其他的文件中,这意味着你可以使用这个变量,但是编译器会从其他文件中寻找其实际的定义
1
2
3
4
5// file1.c
extern int global_var; // 声明
// file2.c
int global_var = 10; // 定义全局变量的多次声明:extern允许你在一个程序的多个文件中声明同一个全局变量,而不用担心重复定义的问题。
1
2
3
4
5
6
7
8
9
10
11
12
13
14// header.h
extern int shared_var;
// source1.c
#include "header.h"
void function1() {
shared_var = 5;
}
// source2.c
#include "header.h"
void function2() {
printf("%d\n", shared_var);
}
函数
函数声明:在函数的声明中添加extern可以明确的指出这个函数的定义在其他地方。
1
2
3
4
5
6
7// header.h
extern void my_function(void);
// source.c
void my_function(void) {
// 函数实现
}
注意事项
链接性:extern表示外部链接,这意味着变量或者函数可以被其他文件引用。如果不适用extern,在全局作用域下,声明的变量会自动具有外部链接,但是局部变量默认是内部链接。
初始化:在使用extern声明的变量不能在声明处初始化,因为这会变成一个定义,而不是声明。
1
2
3
4
5
6// 错误的用法
extern int x = 10; // 这实际上是定义了一个变量,而不是声明
// 正确的用法
extern int x; // 只声明
int x = 10; // 在其他地方定义并初始化与静态变量对比:extern对比static:
- extern变量可以在多个文件中被访问。
- static变量只在定义它的文件内可见(内部链接性),或在块作用域内只在定义它的块中可见。
static
静态局部变量
定义:当static引用于函数内的局部变量时,他改变了该变量的生命周期,使其在函数调用之间保持其值。
1
2
3
4
5void count() {
static int counter = 0; // 初始化只发生在第一次调用时
counter++;
printf("%d\n", counter);
}- 初始化只初始化一次,在第一次进入函数时。
静态全局变量
定义:当static用于全局变量时,它将全局变量的外部链接,改为内部链接。这意味着变量只能在本文件中使用。
1
2
3
4
5
6// file1.c
static int global_counter = 0; // 只能在file1.c中访问
void increment() {
global_counter++;
}
静态函数
定义:将函数使用static,同样改变其链接型,是函数只能在定义它的文件中使用。
1
2
3
4// file1.c
static void helper_function() {
// 只在file1.c中可用
}
注意事项
- 初始化:静态变量只初始化一次,如未显示初始化,则默认初始化为0.
- 生命周期:静态变量的生命周期于程序的生命周期相同,即他们在程序开始时分配内存,在程序结束时释放内存。
- 作用域和链接型:
- 静态局部变量的作用域任然时局部的,但是生命周期是全局的。
- 静态全局变量的作用域是该在文件作用域,但是他们是内部链接。
a++和++a的区别
功能:
- 两个都是将变量a自增加一。
- 区别在于两个的返回值和执行顺序。
区别:
- a++:先返回当前值,在将a的值加一。例如a的初始值为5,a++先返回5,然后a再加一变为6.
- ++a:先将a的值加1,再返回新的a值。例如,++aa先让a加一变为6,再返回。
注意点:
- 计算过程中着重考虑的是需要的是返回值还是自身。a++,返回值不变,自身加一;++a,返回值加一,自身也加一。