针对于嵌入式软件杂乱的知识点总结起来,提供给读者学习复习对下述内容的强化。
目录
2.关键字volatile有什么含意?并举出三个不同的例子?
8.new/delete与malloc/free的区别是什么?
9.strlen("\0")=? sizeof("\0")=?
1.C语言宏中"#“和"##"的用法
1.1.(#)字符串化操作符
功能:将宏参数转换为字符串字面量。
用法:# 操作符会将紧随其后的参数转换为一个带双引号的字符串。
#include <stdio.h>
#define STR(x) #x
int main() {
printf("%s\n", STR(Hello, World!)); // 输出:Hello, World!
printf("%s\n", STR(123)); // 输出:123
return 0;
}
1.2.(##)符号连接操作符
功能:将两个标记(Token)拼接为一个标记。
用法:## 操作符会将它两边的宏参数或标记拼接在一起,形成新的标记。
#include <stdio.h>
#define CONCAT(x, y) x##y
int main() {
int xy = 100;
printf("%d\n", CONCAT(x, y)); // 输出:100
return 0;
}
CONCAT(x, y)
将x
和y
拼接为xy
,因此xy
变量被正确解析。##
在生成代码时非常有用,例如动态生成变量名或函数名。
2.关键字volatile有什么含意?并举出三个不同的例子?
2.1.并行设备的硬件寄存器
存储器映射的硬件寄存器通常加volatile,因为寄存器随时可以被外设硬件修改。当声明指向设备寄存器的指针时一定要用volatile,它会告诉编译器不要对存储在这个地址的数据进行假设。
就比如我们常用的MDK中,你单纯给一个寄存器赋值,不加volatile会被优化掉,程序会略过这个内容去编译别的部分。
#define XBYTE ((volatile unsigned char*)0x8000) // 假设硬件寄存器的基地址
void set_register() {
XBYTE[2] = 0x55; // 写入 0x55
XBYTE[2] = 0x56; // 写入 0x56
XBYTE[2] = 0x57; // 写入 0x57
XBYTE[2] = 0x58; // 写入 0x58
}
- 如果未声明
volatile
,编译器可能优化为直接写入最后的值0x58
。 - 声明了
volatile
后,编译器会逐条生成机器代码,确保硬件设备能够接收到完整的写入操作序列。
2.2.中断服务程序中修改的变量
volatile提醒编译器,它后面所定义的变量随时都有可能改变。因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。如果没有volatile关键字,则编译器可能优化读取和存储,可能暂时使用寄存器中的值,如果这个变量由别的程序更新了的话,将出现不一致的现象。
当中断服务程序(ISR)修改一个变量,主程序可能在等待该变量的改变。在这种情况下,使用 volatile
避免主程序读取优化后的缓存值,确保从内存中获取最新值。
#include <stdbool.h>
volatile bool flag = false; // 用于主程序和中断之间的通信
void ISR() {
flag = true; // 中断触发时修改变量
}
void main() {
while (!flag) {
// 等待中断触发
}
// 中断触发后执行其他操作
}
- 如果未声明
volatile
,主程序可能会认为flag
始终未改变,从而陷入死循环。 - 使用
volatile
后,每次都会直接从内存读取flag
的值,确保中断修改可以被感知。
2.3.多线程中共享的变量
在多线程环境中,不同线程可能会访问或修改同一个变量。volatile
确保每个线程都能读取到变量的最新值,而不是被优化后的缓存值。
#include <pthread.h>
#include <stdbool.h>
volatile bool stop = false; // 多线程共享变量
void* thread_func(void* arg) {
while (!stop) {
// 执行线程操作
}
return NULL;
}
int main() {
pthread_t thread;
pthread_create(&thread, NULL, thread_func, NULL);
// 主线程控制其他操作
sleep(2);
stop = true; // 通知线程停止
pthread_join(thread, NULL);
return 0;
}
- 如果未声明
volatile
,线程可能会读取到未更新的stop
值,导致逻辑错误。 - 声明
volatile
后,线程每次都会从内存中读取stop
的值。
3.关键字static的作用是什么?
3.1.在函数体内定义静态变量
在函数体,只会被初始化一次,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
静态变量具有静态存储周期,只会被初始化一次,且在函数调用结束后其值不会丢失,而是保持到下一次函数调用。
#include <stdio.h>
void counter() {
static int count = 0; // 静态变量,仅初始化一次
count++;
printf("Count = %d\n", count);
}
int main() {
counter(); // 输出:Count = 1
counter(); // 输出:Count = 2
counter(); // 输出:Count = 3
return 0;
}
static
保证变量只初始化一次,即使函数被多次调用。- 变量在函数作用域内可见,但其值会在多次调用中保持。
3.2.在模块内定义静态变量
当在函数体外(即全局作用域)使用 static
时,变量的作用域被限制在当前文件,不能被其他文件中的代码访问。这种变量称为局部全局变量。
在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量(只能被当前文件使用)。
// file1.c
#include <stdio.h>
static int local_var = 10; // 静态全局变量
void print_local_var() {
printf("local_var = %d\n", local_var);
}
// file2.c
extern void print_local_var();
int main() {
print_local_var(); // 如果没有 static,local_var 可直接被访问
return 0;
}
- 限制全局变量的作用域,仅在当前文件中可见。
- 避免命名冲突,特别是在大型项目中。
3.3.在模块内定义静态函数
当函数使用 static
关键字修饰时,其作用域被限制在当前文件,不能被其他文件调用。这种函数被称为静态函数。
在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用(只能被当前文件使用)。
// file1.c
#include <stdio.h>
static void local_function() {
printf("This is a static function.\n");
}
void call_local_function() {
local_function();
}
// file2.c
extern void call_local_function();
int main() {
call_local_function(); // 正常调用 file1.c 的接口函数
// local_function(); // 错误:无法访问静态函数
return 0;
}
- 限制函数作用域,仅在当前文件中可见。
- 适合用于实现模块内部的辅助功能,避免函数命名冲突。
作用域 | static 的用途 |
---|---|
函数体内变量 | 变量只初始化一次,值在函数多次调用中保持不变。 |
模块内变量(全局变量) | 变量作用域仅限于当前文件,防止全局命名冲突。 |
模块内函数 | 函数作用域仅限于当前文件,适用于模块内部使用的辅助功能。 |
注意,我们很多时候是要避免各种全局变量的,因此我们除了利用结构体,就是利用第一点,函数体内变量这个办法。
4.在C语言中,为什么 static 变量只初始化一次?
静态变量 (static
) 存储在内存的 静态存储区(也称为 全局数据区)。
对于所有的对象(不仅仅是静态对象),初始化都只有一次,而由于静态变量具有“记忆“功能,初始化后,一直都没有被销毁,都会保存在内存区域中,所以不会再次初始化。存放在静态区的变量的生命周期一般比较长,它与整个程序“同生死、共存亡”,所以它只需初始化一次。而auto变量,即自动变量由于它存放在栈区,一旦函数调用结束,就会立刻被销毁。
类型 | 存储位置 | 生命周期 | 初始化次数 |
---|---|---|---|
静态变量 | 静态存储区 | 程序运行期间始终存在 | 1 次 |
自动变量 | 栈区 | 随函数调用创建,随函数结束销毁 | 每次重新初始化 |
5.extern"c”的作用是什么?
extern"℃"的主要作用就是为了能够正确实现C++代码调用其他C语言代码。加上extern"C"后,会指示编译器这部分代码按C语言的进行编译,而不是C++的。
extern "C"
的主要作用是实现 C++ 和 C 之间的兼容性:
- C++ 和 C 在函数符号(名称)处理上有本质区别。
- C++ 支持 函数重载,因此采用了 名称修饰(Name Mangling) 技术,使同名函数可以根据参数的类型和数量生成唯一的符号。
- C 不支持函数重载,函数名称在编译后直接对应符号表中的函数名。
- 如果 C++ 代码直接调用 C 的函数(或者反之),名称修饰会导致链接器无法找到正确的符号。
extern "C"
告诉编译器关闭 C++ 的名称修饰,按照 C 的方式处理符号表。
应用如下:
// C++ 文件
#include "example.h"
extern "C" {
#include "example.h"
}
int main() {
print_message("Hello from C++");
return 0;
}
在 C 头文件 中添加 extern "C"
包装,避免名称修饰问题:
#ifdef __cplusplus
extern "C" {
#endif
void function_in_c();
#ifdef __cplusplus
}
#endif
- 仅限在 C++ 环境中使用:
- C 编译器不支持
extern "C"
关键字,因此在混合编译时需要通过宏区分语言环境(__cplusplus
宏用于判断是否是 C++ 编译器)。
- C 编译器不支持
- 仅影响链接(Linking)阶段:
extern "C"
并不改变代码的编译方式,只是改变符号表的生成方式。
6.const有什么作用?
6.1.定义变量为常量
局部变量或全局变量 可以通过 const
来定义为常量,一旦赋值后,该常量的值就不能被修改。
const int N = 100; // 定义常量N,值为100
// N = 50; // 错误:常量的值不能被修改
const int n; // 错误:常量在定义时必须初始化
6.2.修饰函数的参数
使用 const
修饰函数参数,表示该参数在函数体内不能被修改。这样可以保证函数不会无意间修改传入的参数值,增加代码的可维护性。
void func(const int x) {
// x = 10; // 错误:x 是常量,不能修改
}
6.3.修饰函数的返回值
a. 返回指针类型并使用 const
修饰
- 当函数返回指针时,若用
const
修饰返回值类型,那么返回的指针所指向的数据内容不能被修改,同时该指针也只能赋值给被const
修饰的指针。
const char* GetString() {
return "Hello";
}
const char* str = GetString(); // 正确,str 被声明为 const
// char* str = GetString(); // 错误,str 未声明为 const,不能修改返回值
b. 返回普通类型并使用 const
修饰
- 如果
const
用于修饰普通类型的返回值,如int
,由于返回值是临时的副本,在函数调用结束后,返回值的生命周期也随之结束,因此将其修饰为const
是没有意义的。
const int GetValue() {
return 5;
}
int x = GetValue(); // 正确,返回值可以赋给普通变量
// const int y = GetValue(); // 不必要的,因为返回值会是临时变量,不会被修改
6.4.节省空间,避免不必要的内存分配
const
关键字还可以帮助优化内存管理。当你使用 const
来定义常量时,编译器会考虑将常量放入只读存储区,避免了额外的内存分配。对于宏(#define
)和 const
常量,它们在内存分配的方式上有所不同。
#define PI 3.14159 // 使用宏定义常量 PI
const double pi = 3.14159; // 使用 const 定义常量 pi
使用宏定义的常量(如 PI
)会在编译时进行文本替换,所有使用该宏的地方都会被替换为常量值,因此不会单独分配内存;而 const
常量则会在内存中分配空间,通常存储在只读数据区。
double i = PI; // 编译期间进行宏替换,不会分配内存
double I = pi; // 分配内存,存储常量 pi
宏定义常量的每次使用都会进行文本替换,因此会进行额外的内存分配。相反,const
常量只会分配一次内存。
#define PI 3.14159 // 宏定义常量 PI
double j = PI; // 这里会进行宏替换,不会再次分配内存
double I = PI; // 宏替换后再次分配内存
7.什么情况下使用const关键字?
序号 | 使用场景 | 示例 | 说明 |
---|---|---|---|
1 | 修饰一般常量 | const int x = 2; int const x = 2; | 定义只读的常量,const 位置灵活。 |
2 | 修饰常数组 | const int arr[8] = {1,2,3,4,5,6,7,8}; int const arr[8] = {1,2,3,4,5,6,7,8}; | 定义的数组内容不可修改。 |
3 | 修饰常对象 | const A obj; A const obj; | 定义的类对象不可被修改,且需立即初始化。 |
4 | 修饰指针相关 | - const int *p; (指向常量的指针,p 的内容不可变,p 本身可变)- int *const p; (指针常量,p 不可变,内容可变)- const int *const p; (指向常量的常量指针,p 和内容都不可变) | 不同组合修饰指针的行为。 |
5 | 修饰常引用 | void func(const int &ref); | 常引用绑定到变量后不能更改其指向对象的值,可保护传入变量不被函数修改。 |
6 | 修饰函数的常参数 | void func(const int var); | 参数不可在函数体内被修改。 |
7 | 修饰函数的返回值 | - const int func(); (返回的值不可修改)- const A func(); (返回的对象不可修改) | 表明返回值不可被外部代码修改。 |
8 | 跨文件使用常量 | extern const int i; | 在其他文件中使用 const 修饰的全局变量。 |
const
的位置:
const
可以放在类型前或类型后,如 const int
与 int const
表达同样的含义。
对于指针的 const
修饰,其位置决定了是修饰指针本身,还是指针指向的内容。
保护机制:
使用 const
的核心目的之一是防止数据被意外修改,提高代码的安全性和可读性。
类和对象:
对象或成员函数被 const
修饰后,只能调用其他 const
成员函数,确保不会修改对象的状态。
通过合理使用 const
,可以编写更安全、健壮的代码。
8.new/delete与malloc/free的区别是什么?
8.1.类型安全性
new
和 delete
:
new
是 C++ 的运算符,delete
也是运算符,具有类型安全性。new
会返回正确类型的指针,无需强制转换。使用时,编译器会自动计算所需内存的大小。delete
会释放通过new
分配的内存,并自动调用对象的析构函数。
int* p = new int; // 分配内存并返回指向 int 类型的指针
delete p; // 释放内存并调用析构函数
malloc
和 free
:
malloc
和free
是 C 标准库函数,malloc
返回的是void*
指针,必须显式转换为实际的类型指针。它没有类型安全性,容易导致错误。malloc
只是为内存分配空间,并不调用构造函数,而free
只是释放内存,并不调用析构函数。
int* p = (int*)malloc(sizeof(int)); // 需要手动转换类型
free(p); // 只释放内存
8.2.构造函数与析构函数
new
和 delete
:
- 当使用
new
分配内存时,会自动调用类的构造函数来初始化对象。 - 当使用
delete
释放内存时,会自动调用类的析构函数。
class MyClass {
public:
MyClass() { cout << "Constructor called" << endl; }
~MyClass() { cout << "Destructor called" << endl; }
};
MyClass* obj = new MyClass; // 自动调用构造函数
delete obj; // 自动调用析构函数
malloc
和 free
:
malloc
不会调用构造函数,仅分配内存;free
不会调用析构函数,仅释放内存。
MyClass* obj = (MyClass*)malloc(sizeof(MyClass)); // 不会调用构造函数
free(obj); // 不会调用析构函数
8.3.内存管理
new
和 delete
:
new
在分配内存时会计算所需内存的大小,并根据类型自动计算。delete
自动处理内存释放及相关清理工作。
malloc
和 free
:
malloc
需要明确指定需要分配的字节数,不会考虑对象的类型。free
只能释放malloc
或calloc
分配的内存,并且不能自动调用析构函数。
int* p = (int*)malloc(10 * sizeof(int)); // 需要手动计算内存大小
8.4.对象的内存对齐和初始化
new
和 delete
:
new
会调用类的构造函数进行初始化,并且会适当地进行内存对齐。delete
会释放内存并自动调用析构函数。
malloc
和 free
:
malloc
只分配原始内存,不会初始化对象。如果需要初始化对象,必须手动进行。free
只会释放内存,而不会调用析构函数。
int* p = new int(5); // 自动初始化
delete p; // 自动释放并调用析构函数
特性 | new /delete | malloc /free |
---|---|---|
语言 | C++ | C |
类型安全 | 类型安全,自动推导和转换 | 需要手动类型转换 |
构造函数/析构函数 | 自动调用构造函数/析构函数 | 不调用构造函数/析构函数 |
内存分配 | 自动计算内存大小 | 需要手动指定内存大小 |
内存初始化 | 支持初始化 | 不会初始化内存 |
使用方式 | 运算符,使用 new 和 delete | 函数,使用 malloc 和 free |
9.strlen("\0")=? sizeof("\0")=?
strlen("\0")=0 ,sizeof("\0")=2。
strlen用来计算字符串的长度(在C/C++中,字符串是以"0"作为结束符的),它从内存的某个位置(可以是字符串开头,中间某个位置,甚至是某个不确定的内存区域)开始扫描直到碰到第一个字符串结束符\0为止,然后返回计数器值sizeof是C语言的关键字,它以字节的形式给出了其操作数的存储大小,操作数可以是一个表达式或括在括号内的类型名,操作数的存储大小由操作数的类型决定。
strlen()
函数计算的是 字符串的长度,即从字符串的开头到 第一个空字符 (\0
) 之前的字符数。在这种情况下,字符串 "\0"
只有一个字符,它就是 空字符 \0
,因此它的长度为 0。
strlen("\0"); // 结果是 0,因为字符串仅包含一个 '\0' 终止符
sizeof()
计算的是 操作数的大小(通常是以字节为单位)。在 C 中,字符串字面量 "str"
的实际类型是 字符数组,并且这个数组总是包括一个额外的空字符 \0
作为结束符。因此,字符串 "\0"
实际上是一个包含两个字符的字符数组:'\0'
和 \0
终止符。所以 sizeof("\0")
结果是 2。
sizeof("\0"); // 结果是 2,因为字符串 "\0" 包含两个字符:'\0' 和 '\0' 终止符
10.sizeof和strlen有什么区别?
strlen与 sizeof的差别表现在以下5个方面,
1.sizeof是运算符(是不是被弄糊涂了?事实上,sizeof既是关键字,也是运算符,但不是函数)而strlen是函数。 sizeof后如果是类型,则必须加括弧,如果是变量名,则可以不加括弧。
2. sizeof运算符的结果类型是 size_t,它在头文件中 typedef 为 unsigned int类型。该类型保证能够容纳实现所建立的最大对象的字节大小
3. sizeof可以用类型作为参数,strlen只能用char*作参数,而且必须是以“0结尾的。 sizeof还可以以函数作为参数,如intg(),则 sizeof(g())的值等于 sizeof( int的值,在32位计算机下,该值为4。
4.大部分编译程序的 sizeof都是在编译的时候计算的,所以可以通过 sizeof(x)来定义数组维数。而 strlen则是在运行期计算的,用来计算字符串的实际长度,不是类型占内存的大小。例如,charstr[20]="0123456789",字符数组str是编译期大小已经固定的数组,在32位机器下,为sizeof(char)*20=20,而其 strlen大小则是在运行期确定的,所以其值为字符串的实际长度10.当数组作为参数传给函数时,传递的是指针,而不是数组,即传递的是数组的首地址。
11.不使用 sizeof,如何求int占用的字节数?
#include <stdio.h>
#define Mysizeof(value) ((char *)(&value + 1) - (char *)&value)
int main() {
int i;
double f;
double *q;
// 输出各个变量占用的字节数
printf("%d\n", Mysizeof(i)); // 输出 int 类型的字节数
printf("%d\n", Mysizeof(f)); // 输出 double 类型的字节数
printf("%d\n", Mysizeof(q)); // 输出 double* 类型的字节数
return 0;
}
(char *)(&value + 1)
:将 value
的地址向后移动一个 value
类型的单位(如 int
,移动 1 个 int
的大小)。
(char *)&value
:获取 value
的起始地址。
两者相减,即可得到 value
类型的字节数,因为指针的差值以 char
的大小为单位,而 char
是 1 字节。
&value + 1
是类型安全的,它表示从当前地址向后移动一个变量的单位。
将地址强制转换为 (char *)
,使得指针的差值以字节为单位。
12.C语言中 struct与 union的区别是什么?
比较项目 | struct (结构体) | union (联合体) |
---|---|---|
内存分配 | 每个成员有独立的存储空间,大小是所有成员大小的累加值(考虑字节对齐)。 | 所有成员共用同一块内存,大小等于最大成员的大小(考虑字节对齐)。 |
成员访问 | 所有成员可以独立访问且互不影响。 | 同一时刻只能访问一个成员,写入一个成员会覆盖其他成员的值。 |
用途 | 用于保存多个相关但独立的数据。 | 用于在同一存储区域保存多个数据(节省内存)。 |
字节对齐 | 根据成员类型和字节对齐规则进行分配。 | 最大成员决定内存分配,并根据字节对齐规则调整大小。 |
适用场景 | 常用于多种类型数据的组合使用。 | 常用于需要节省内存或多种数据类型共用时。 |
typedef union {
double i; // 8 bytes
int k[5]; // 5 × 4 bytes = 20 bytes
char c; // 1 byte
} DATE;
typedef struct data {
int cat; // 4 bytes
DATE cow; // 24 bytes (union, 8-byte alignment)
double dog; // 8 bytes
} too;
DATE max;
// sizeof(too) + sizeof(max)
-
DATE
的大小:
联合体的大小由 最大成员大小 决定,即 k[5]
,占用 20 字节。
为了满足 8 字节对齐,需要调整到 8 的倍数,实际占用 24 字节。
-
too
的大小:
int cat
: 4 字节。
DATE cow
: 24 字节(由前面计算)。
double dog
: 8 字节。
按 8 字节对齐,too
总大小 = 4 + 4(填充) + 24 + 8 = 40 字节。
-
max
的大小:
与 DATE
相同,占用 24 字节。
-
总大小:
sizeof(too) + sizeof(max) = 40 + 24 = 64
。
13.左值和右值是什么?
左值是指可以出现在等号左边的变量或表达式,它最重要的特点就是可写(可寻址)。也就是说,它的值可以被修改,如果一个变量或表达式的值不能被修改,那么它就不能作为左值。
int a = 10; // a 是左值
a = 20; // a 出现在赋值号的左边,可修改其值
右值是指只可以出现在等号右边的变量或表达式。它最重要的特点是可读。一般的使用场景都是把一个右值赋值给一个左值。
int b = a + 5; // (a + 5) 是右值,提供计算结果但无法修改
通常,左值可以作为右值,但是右值不一定是左值。
类别 | 左值(L-value) | 右值(R-value) |
---|---|---|
定义 | 表示内存中的一个地址,可出现在赋值运算符左侧 | 表示一个值,不占据内存地址,只能出现在赋值运算符右侧 |
特点 | 可寻址、可修改 | 不可寻址、只提供值,不能被修改 |
作用 | 提供一个持久的存储位置,可读写 | 提供数据,通常用于计算或赋值 |
示例 | 变量:int a; a = 5; | 常量或表达式:5; a + 3; |
内存分配 | 与具体内存地址绑定 | 通常是临时值,不绑定内存地址 |
使用场景 | - 出现在赋值号左侧 - 可作为右值 | - 出现在赋值号右侧 - 参与计算 |
互相关系 | 左值可以用作右值 | 右值不能用作左值 |
函数返回值 | 函数返回引用或指针是左值 | 函数返回具体值是右值 |
代码示例 | c<br>int a = 10; a = 20; | c<br>int b = a + 5; |
14.什么是短路求值?
短路求值(Short-Circuit Evaluation)是一种逻辑表达式的求值方式,在逻辑运算(&&
和 ||
)中,一旦可以确定整个表达式的最终结果,后续的部分就不会被执行。
- 逻辑或(
||
):- 如果左侧表达式为
true
,整个表达式为true
,后续表达式不会执行。 - 如果左侧表达式为
false
,需要计算右侧表达式。
- 如果左侧表达式为
- 逻辑与(
&&
):- 如果左侧表达式为
false
,整个表达式为false
,后续表达式不会执行。 - 如果左侧表达式为
true
,需要计算右侧表达式。
- 如果左侧表达式为
#include <stdio.h>
int main() {
int i = 6; // i = 6
int j = 1; // j = 1
if (i > 0 || (j++) > 0); // 短路求值在此发生
printf("%o\r\n", j); // 输出 j 的值
return 0;
}
条件判断:i > 0 || (j++) > 0
先计算 i > 0
,结果为 true
(因为 i = 6
,大于 0)。
因为 ||
运算中只需要一边为 true
就能确定整个表达式为 true
,因此不会执行右侧的 (j++) > 0
。
j++
不会被执行,j
的值保持不变。
输出:printf("%o\r\n", j);
j
仍然为 1,因此输出 1
(八进制表示为 1
)。
15.++a和a++有什么区别?两者是如何实现的?
++a
(前置自增):先对变量自增 1,再返回变量的值。
a++
(后置自增):先返回变量的值,再对变量自增 1。
a++
的实现过程
int a = 5;
int temp = a; // 保存当前值到临时变量 temp
a = a + 1; // 自增
return temp; // 返回保存的临时变量 temp
++a
的实现过程
int a = 5;
a = a + 1; // 自增
return a; // 返回自增后的值
标签:知识点,const,函数,int,C++,嵌入式,内存,sizeof,变量
From: https://blog.csdn.net/weixin_64593595/article/details/145191331