C语言关键字(static、const、inline、volatile、extern)——别再死记硬背了,一篇彻底理解!

如果你正在学习嵌入式、AUTOSAR、Linux驱动、MCU开发,或者准备ADAS面试,那么这些关键字几乎一定会遇到。

很多教程都是:

static 有三种用法……

const 有四种组合……

于是大家开始疯狂背诵。

但是几天之后,又全部忘掉。

为什么?

因为没有理解这些关键字为什么会存在。

今天,我们换一种方式学习。

按照Harsha Suryanarayana老师的教学思路:

先理解问题,再理解为什么有这个关键字,最后自然记住它。


一、程序到底在干什么?

先来看一个最简单的程序。

int main()
{
    int a = 10;

    return 0;
}

CPU真正执行的时候,大概经历下面几个阶段:

程序启动

↓

main()

↓

创建变量a

↓

使用a

↓

main结束

↓

释放a

这里出现了一个问题。

为什么程序结束以后,变量a就没了?

因为:

局部变量存放在栈(Stack)中。

函数退出,栈空间释放。

变量自然消失。

于是,C语言设计者开始思考:

有没有一种变量,函数退出以后还能保存?

于是……

static诞生了。


二、static——让变量"活得更久"

来看例子。

void func()
{
    int cnt = 0;

    cnt++;

    printf("%d\n",cnt);
}

调用三次:

func();
func();
func();

输出:

1
1
1

为什么?

因为每次进入函数:

创建cnt

↓

赋值0

↓

++

↓

释放

所以每次都是1。


如果改成:

void func()
{
    static int cnt = 0;

    cnt++;

    printf("%d\n",cnt);
}

输出:

1

2

3

为什么?

因为:

第一次进入

创建cnt

↓

以后一直存在

↓

再次进入

继续使用原来的cnt

所以:

static局部变量,只初始化一次,生命周期变成整个程序运行期间。

但是:

注意一点。

虽然生命周期变长了,

作用域没有变。

仍然只能在这个函数里面访问。


内存变化

普通变量:

Stack

↓

函数退出

↓

释放

static变量:

Data Segment

↓

程序结束才释放

所以:

static改变的是生命周期,不改变作用域。


三、为什么static还能修饰全局变量?

来看两个文件。

ACC.c

Brake.c

ACC.c

int VehicleSpeed;

Brake.c

extern int VehicleSpeed;

可以访问。

但是有一天,

项目越来越大。

几十个人一起开发。

张三:

VehicleSpeed

李四:

VehicleSpeed

王五:

VehicleSpeed

最后链接时报错:

重复定义

怎么办?

于是:

static int VehicleSpeed;

它告诉编译器:

这个变量,只允许当前.c文件使用。

其它文件:

看不见。

所以:

static修饰全局变量,本质就是"隐藏"。


四、为什么static还能修饰函数?

例如:

ACC.c

里面有:

CalcPID()

CalcTargetAcc()

CalcTargetTorque()

外部都可以调用。

但是实际上:

真正应该暴露出去的,

只有:

Acc_MainFunction()

其它都是内部实现。

怎么办?

于是:

static void CalcPID()

变成:

ACC.c

↓

自己可以调用

↓

其它文件不能调用

这就是:

模块封装(Encapsulation)。

AUTOSAR几乎所有模块都是这样写。

例如:

Acc.c

↓

static CalcTargetAcc()

↓

static CalcTorque()

↓

Acc_MainFunction()

外部永远只看到:

Acc_MainFunction()

五、const——为什么需要"只读"

假设有一个PID查表。

float table[9];

程序运行过程中,

有人误写:

table[0]=999;

那么:

整个ACC控制全部错误。

怎么办?

于是:

const float table[9];

变成:

只允许读取

↓

禁止修改

这样:

编译阶段就会报错。


六、最容易混淆:const指针

很多人都是死记:

const int *

int * const

const int * const

其实不用背。

记一句话:

const修饰谁,谁不能改。


第一种:

const int *p;

可以理解成:

const修饰int

所以:

数据不能改

↓

指针可以移动

例如:

*p

×

但是:

p=&b

√

第二种:

int * const p;

这次:

const修饰的是:

p

所以:

指针不能移动

↓

数据可以修改

第三种:

const int * const p;

两个都不能改。

一句口诀:

const靠谁,谁不能改。


七、inline——为什么函数调用会慢?

来看:

c = Max(a,b);

CPU真正执行:

保存现场

↓

跳到Max()

↓

执行

↓

返回

↓

恢复现场

整个过程:

Jump

Call

Return

都是有开销的。

如果:

Max()

ABS()

MIN()

MAX()

一天调用几百万次。

怎么办?

于是:

inline int Max()

编译器可能直接展开:

c = (a>b)?a:b;

没有函数调用。

速度更快。

但是:

注意:

inline只是:

建议

不是命令。

最终:

编译器决定。


八、volatile——最容易踩坑的关键字

来看一个中断。

主循环:

while(flag==0)
{

}

中断:

flag=1;

理论上:

程序应该退出循环。

但是:

编译器发现:

flag没有修改

于是优化:

一直认为flag是0

结果:

死循环

怎么办?

告诉编译器:

volatile int flag;

意思就是:

这个变量可能随时发生变化,每次都重新读取。

不要优化。


在车载开发中:

经常用于:

CAN接收标志

DMA完成标志

MCU寄存器

中断变量

共享变量

几乎每天都会写。


九、extern——跨文件共享变量

A.c

int VehicleSpeed;

B.c

extern int VehicleSpeed;

意思:

这个变量

↓

不是这里定义的

↓

去其它文件找

所以:

extern不是创建变量。

而是:

声明变量。


十、typedef——为什么不用unsigned char?

来看:

unsigned char

是不是很长?

于是:

typedef unsigned char uint8;

以后:

直接写:

uint8 speed;

AUTOSAR全部这样。

例如:

uint8

uint16

uint32

boolean

Std_ReturnType

统一规范。

提高可移植性。


十一、ADAS项目中的真实应用

例如ACC模块:

static void CalcTargetAcc(void);

隐藏内部函数。


PID查表:

const float kpTable[9];

防止修改。


CAN接收:

volatile uint8 CanRxFlag;

避免优化。


跨模块共享:

extern float VehicleSpeed;

读取车速。


AUTOSAR:

typedef uint8 boolean;

统一类型。


十二、一张图理解所有关键字

关键字 解决什么问题 典型用途
static 生命周期、封装 模块内部函数、局部变量保持状态
const 防止误修改 标定参数、查表数据
inline 减少函数调用开销 MAX、MIN、ABS等小函数
volatile 防止编译器优化 寄存器、中断、DMA、CAN标志
extern 跨文件共享变量 全局变量声明
typedef 统一数据类型 AUTOSAR标准类型

十三、总结

学习关键字,不要死记语法。

应该先问自己:

它到底解决了什么问题?

可以把今天学到的内容总结成一句话:

  • static:控制生命周期和可见性,实现模块封装。
  • const:保证数据不可修改,提高程序安全性。
  • inline:减少函数调用,提高执行效率。
  • volatile:告诉编译器,每次都必须重新读取变量。
  • extern:声明变量来自其它文件,实现模块共享。
  • typedef:给类型起别名,提高代码规范性和可移植性。

理解了这些关键字背后的设计思想,比背100条语法规则都更重要。


如果本文对你有所帮助,欢迎点赞、收藏和关注。后续我会继续用这种“理解原理,而不是死记语法”的方式,分享嵌入式、AUTOSAR、ADAS 和 C/C++ 系列知识。

Logo

免费领 150 小时云算力,进群参与显卡、AI PC 幸运抽奖

更多推荐