变量、指针和关键字
两个口诀:
- 变量变量,能变,就是能读能写,必定在内存(RAM)里
- 指针指针,保存的是地址,32 位处理器中的地址都是 32 位的,无论是什么类型的指针变量,都是 4 字节
指针
- 对于 32 位处理器里面,地址是 32 位的,所以指针的大小为 4 字节,
sizeof(p) = 4
,sizeof(*p) = 指针所指向的类型所占的空间
变量
- 只读的常量一般放在 flash 中,所以只读的变量加上
const
可以节省内存,但有时候为了优化也可能会放在内存里
extern 关键字
- 如果想在
a.c
中引用b.c
中的全局变量int b
,需要在a.c
中加入:extern int b
- 包含头文件
#include "b.h"
,然后在头文件中写extern int b
注意 extern int b 不能被赋值!!!这个是声明,表示 b 是什么
不建议使用 extern,可以使用函数来进行值传递
static 关键字
- 对于全局变量,如果不加
static
,全局变量的作用域为整个程序,加上 static 作用域就变为该文件了(函数前加static
也是同样的作用) - 对于在函数内定义的变量,加上
static
的变量仅会初始化一次,再次调用该函数时,仍为上一次函数调用的结果,不会再次初始化
volatile 关键字
-
不能自作主张优化变量,直接存取原始内存地址
-
应用场景:
- 中断服务程序中修改的供其他程序检测的变量,需要用
volitile
- 多任务环境中个任务间共享的标志加
volitile
- 存储器映射的硬件存储器要加
volitile
后面两个场景还没用到,等用到再具体补充
- 中断服务程序中修改的供其他程序检测的变量,需要用
结构体
- 结构体是不占内存的,是一种类型,用结构体定义了变量之后(实例化)才会分配内存空间
结构体对齐
为什么会有内存对齐
- 平台原因:不是所有的硬件平台都能访问任意内存地址上的任意数据,某些硬件平台只能在某些地址处取某些特定类型的数据,否则抛出硬件异常。为了同一个程序可以在多平台运行,需要内存对齐。
- 硬件原因:经过内存对齐后,CPU 访问内存的速度大大提升。
对齐规则
- 结构体每个成员相对结构体首地址的偏移量是对齐参数的整数倍,如有需要会在成员之间填充字节。编译器在为结构体成员开辟空间时,首先检查预开辟空间的地址相对于结构体首地址的偏移量是否为对齐参数的整数倍,若是,则存放该成员,若不是,则填充若干字节,以达到整数倍的要求。
这里的对齐参数取每个变量自身对齐参数和系统默认参数#pragma pack(n)(一般为 8)中较小的那个
- 结构体变量所占空间的大小是对齐参数大小的整数倍。如有需要会在最后一个成员末尾填充若干字节使得所占空间大小是对齐参数大小的整数倍。
这里的对齐参数取结构体中所有变量对齐参数的最大值和系统默认参数对比取较小的
- 即结构体变量所占的空间大小需要经过两次对齐
图解
| char | | | | 4字节
| int | int | int | int | 4字节
| short|short| | | 4字节
| char | |short|short| 4字节
| int | int | int | int | 4字节
实例
- 实例 1:
typedef struct
{
char c;
short d;
static int a;
}A;
| char | | 2字节
| short|short| 2字节
易错点:对于结构体中的 static int a
,静态数据成员存放位置与结构体实例的存储地址无关,不算在里面
只有 C++ 结构体中才有 static,C 语言中不允许有
- 实例 2:
typedef struct
{
double b;
int c;
}D;
typedef struct
{
bool a; // bool为1字节
D d;
double b;
int c;
}E;
|bool|-----------------------------------| 8字节
|--------------------D-------------------| 8字节
|--------------------D-------------------| 8字节
|------------------double----------------| 8字节
|---------int--------|-------------------| 8字节
易错点:D 与默认的 8 比,8 小,取 8 为对齐参数
变量赋值
a = 123
等价于
p = &a;
*p = 123; // 将a的值变为123
A.age = 20
等价于(A 为结构体)
pt = &A;
pt->age = 20; // pt为指针,取成员用"->",结构体用"."
*pt = A2; // 将A变成A2的值
结构体指针
typedef struct student{
char *name;
int age;
struct student *classmate; // 结构体中只能用指针,长度为4个字节(链表)
}student, *pstudent;
student zhangsan = {"zhangshan", 10, NULL};
student lili = {"lili", 20, NULL};
zhangsan.classmate = &lili; // 构成链表
name = zhangsan.classmate->name; // zhangsan为结构体,用"."取值,classmate为指针,用"->"取值
函数指针
void (*add){}; // 函数指针,变量,占4字节
typedef struct student{
char *name;
int age;
void (*good_work)(void); // 函数指针
struct student *classmate;
}student, *pstudent;
// add为函数指针,也可以写成&add
// 函数指针是变量,所以可以赋值为地址,函数不是变量,只能被调用
student ss[2] = {{"zhangshan", 10, add, NULL}, {"lili", 20, add, NULL}}; // 结构体数组
ss[1].good_work(); // 结构体调用函数指针,用"."
pstudent get(void){
int type = 1;
return &ss[type]; // 返回结构体指针
} // 应尽量避免使用全局变量,可以将变量封装在函数里
pstudent a;
a = get();
a->good_work(); //这里a为结构体指针,用"->"
全局变量与局部变量
-
全局变量:断电时无,运行时有,初值来自 Flash
- 有值时初始化:类似于
memcpy
,把 Flash 数据段整体拷贝到内存 - 初始值为 0/没有初始化的:这些变量在内存里都放到了 ZI 段,类似于
memset
,把 ZI 段全部清零
- 有值时初始化:类似于
-
局部变量:在栈里
参考资料
https://www.bilibili.com/video/BV1VM4y137Pm/?spm_id_from=333.999.0.0
https://blog.csdn.net/qq_41068271/article/details/83446623?spm=1001.2014.3001.5502