目录
一、故事开场:你给自己取了个名字
二、标识符——你给东西取的名字
定义
起名规则(必须遵守,否则编译器报错)
起名建议(不会报错但会被人骂)
三、关键字——C语言自己保留的"禁词"
定义
C语言标准关键字清单(C99/C11共37个)
用关键字起名会怎样?
四、预定义标识符——站在巨人的肩膀上
定义
常见的预定义标识符
和标识符、关键字的区别
五、三角关系对比(一张表说清)
一句话记忆法
六、实战踩坑经验
坑1:在STM32项目里重新定义了标准库函数
坑2:在头文件里误用了关键字
坑3:下划线开头的标识符
坑4:main 其实是个预定义标识符
七、总结
一、故事开场:你给自己取了个名字
假设你转学到了一个新班级。
开学第一天,老师说:"每个人给自己取一个英文名,方便大家叫你。"
你给自己取了"Alex"。
但老师接着说:"有几个名字不能取——班长、校长、班主任。这些是固定的职位称呼,你不能拿来当自己的名字。"
接着你又发现,班里有一些"公共称呼":"值日生"、"课代表"——谁当了谁用,不专属于某个人。
这个场景,恰好对应了C语言里的三个基本概念:
| 班级场景 | C语言概念 |
|---|---|
| 你给自己取名Alex | 标识符——你自己起的名字 |
| 不能用的名字:班长、校长 | 关键字——C语言保留的词 |
| 公共称呼:值日生、课代表 | 预定义标识符——系统预置的通用名字 |
是不是一下子就通了?下面一个个展开说。
二、标识符——你给东西取的名字
定义
标识符(Identifier)就是你给变量、函数、结构体、宏等起的名字。
写C代码的时候,你几乎每时每刻都在起名:
int led_pin = 13; // led_pin 是标识符 void delay_ms(int t) { ... } // delay_ms 是标识符, t 也是标识符 #define BUTTON_PIN 12 // BUTTON_PIN 是标识符 struct SensorData { ... }; // SensorData 是标识符起名规则(必须遵守,否则编译器报错)
| 规则 | 说明 | ✅ 正确 | ❌ 错误 |
|---|---|---|---|
| 只能包含字母、数字、下划线 | 不能有空格、@、#、$等特殊符号 | led_pin | led pin |
| 不能以数字开头 | 必须以字母或下划线开头 | pin1 | 1pin |
| 不能是关键字 | 不能和C语言保留字重名 | my_int | int |
| 大小写敏感 | LED和led是两个不同的标识符 | — | — |
起名建议(不会报错但会被人骂)
虽然编译器只检查上面的硬规则,但写代码是给人看的,建议习惯养成:
int a; // ❌ 鬼知道 a 是什么意思 int led_pin; // ✅ 一看就知道是LED引脚 int timer_count; // ✅ 清清楚楚 void func1(); // ❌ 哪个func1?干啥的? void uart_send(); // ✅ 明确说是串口发送嵌入式开发小贴士:STM32的HAL库里大量使用匈牙利命名法和下划线风格,比如
HAL_GPIO_WritePin()。跟着项目风格走,不要自创一套。
三、关键字——C语言自己保留的"禁词"
定义
关键字(Keywords)是C语言标准里规定好、有特殊含义的词,你不能拿来当标识符。
这些词是C语言的"语法砖块"——编译器拿它们来识别程序结构。
C语言标准关键字清单(C99/C11共37个)
按功能分类看着更清晰:
数据类型(8个):
char short int long float double signed unsigned控制语句(12个):
if else switch case default for while do break continue goto return存储类型(5个):
auto register static extern const类型定义(3个):
typedef struct union enum // 4个,加上面共12? // 准确说是这些: typedef struct union enum sizeof void volatile标准C关键字完整清单(按字母顺序,方便查阅):
auto break case char const continue default do double else enum extern float for goto if int long register return short signed sizeof static struct switch typedef union unsigned void volatile whileC99新增:inline、_Bool、_Complex、_Imaginary
用关键字起名会怎样?
int int = 5; // ❌ 编译错误:int 是关键字,不能用 float return; // ❌ 编译错误:return 是关键字编译器直接甩一个语法错误。这就好比你在班级里非要给自己取名叫"校长"——老师说不行。
嵌入式相关的常见关键字:
| 关键字 | 在STM32开发中的典型用法 |
|---|---|
volatile | __IO宏展开就是这个,告诉编译器"这个变量可能被中断/硬件修改,别优化" |
static | 限制函数的作用域在当前文件,或让局部变量在两次调用间保持值 |
const | 定义只读数据(比如查表),防止意外修改 |
extern | 声明在其他文件中定义的全局变量(比如 stm32f4xx.h 用这个链接寄存器地址) |
踩坑经验:刚学的时候最容易误用
void——void main()在某些编译器下可以通过,但标准C不允许。正确的写法是int main(void)。
四、预定义标识符——站在巨人的肩膀上
定义
预定义标识符(Predefined Identifiers)是编译器或标准库已经声明好的名字,你可以用,但不能重新定义(或者说,重新定义会有严重后果)。
它们跟关键字的区别在于:关键字你不能拿来用;预定义标识符你可以用,但不建议自己再定义一个同名的。
常见的预定义标识符
标准库函数名:
printf scanf malloc free memset memcpy strlen这些名字已经在<stdio.h>、<stdlib.h>、<string.h>等头文件里声明过了。你如果自己在代码里写:
int printf = 5; // ❌ 编译能过,但链接器报错,或者覆盖了库函数STM32 HAL库里的预定义标识符:
HAL_GPIO_WritePin() // HAL库的GPIO写函数 HAL_UART_Transmit() // HAL库的串口发送函数 TIM2 // 定时器2的外设句柄 GPIOA // GPIOA端口这些名字是ST公司写HAL库时已经定义好的。你在main.c里如果写:
int HAL_GPIO_WritePin; // ❌ 语法上允许,但会导致链接错误编译器不会报错(因为它不是关键字),但链接器会懵:你到底想用我的HAL_GPIO_WritePin还是自己的那个int?
C语言预定义的宏(编译器和语言标准内置的):
__FILE__ // 当前文件名 __LINE__ // 当前行号 __DATE__ // 编译日期 __TIME__ // 编译时间 __func__ // 当前函数名(C99起)这些也是预定义标识符的一种——你没法改它们的值,但可以直接拿来用:
printf("发生错误,文件:%s,行号:%d\n", __FILE__, __LINE__); // 输出:发生错误,文件:main.c,行号:42这在嵌入式调试中非常实用——程序崩溃时,通过串口打印出错的文件和行号,定位问题快得多。
和标识符、关键字的区别
| 概念 | 你能用它命名吗? | 你能修改它的含义吗? |
|---|---|---|
| 关键字 | ❌ 绝对不行 | ❌ 本来就是语法的一部分 |
| 预定义标识符 | ✅ 语法允许,但强烈不建议 | ❌ 改了会导致严重问题 |
| 普通标识符 | ✅ 随便取 | ✅ 按你的需求来 |
五、三角关系对比(一张表说清)
| 对比维度 | 关键字 | 预定义标识符 | 用户自定义标识符 |
|---|---|---|---|
| 谁定义的 | C语言标准 | 编译器/标准库/框架 | 你(程序员) |
| 能否用作变量名 | ❌ 编译器直接报错 | ⚠️ 语法允许,但后果严重 | ✅ 随便用 |
| 能否修改含义 | ❌ | ❌(改了会出问题) | ✅ |
| 数量 | 固定(C89:32个,C99:37个) | 随库函数增加而增加 | 无限(只要符合规则) |
| 典型例子 | intifreturn | printfmallocHAL_GPIO_WritePin | led_pinmy_func |
| 新手常见错误 | 拿关键字当变量名 | 拿库函数名当变量名 | 取名太随意(abtmp) |
一句话记忆法
关键字"不能碰"——编译器拦着你不让你用。
预定义标识符"可以碰但别碰"——语法上允许,但后果自负。
自定义标识符"大胆用"——但在用之前想清楚这个名字别人看得懂吗。
六、实战踩坑经验
坑1:在STM32项目里重新定义了标准库函数
// ❌ 错误示范 char* memset; // 声明了一个名叫 memset 的变量然后你在别处调用了memset(buffer, 0, 100)——链接器报错,说找不到memset的函数定义。排查了半天才发现是变量声明把函数名给"挡住"了。
坑2:在头文件里误用了关键字
// ❌ 错误示范 #define int my_int // 试图把 int 替换成 my_int预处理器会先处理#define,然后编译器看到的是my_int my_var;——这倒不会报错,但你所有用到int的地方全废了。这种宏定义是灾难性的。
坑3:下划线开头的标识符
C语言标准规定:以下划线开头的标识符通常保留给编译器/标准库使用。
int _my_var; // ⚠️ 语法允许,但建议不要这样写 int __my_var; // ❌ 双下划线开头的绝对不要用在STM32的HAL库里你经常看到__HAL_RCC_GPIOA_CLK_ENABLE()—— 这是ST公司自己用的,他们作为库开发者可以用。你要写自己的应用代码,别用下划线开头。
坑4:main其实是个预定义标识符
// ❌ 虽然语法上main不是关键字,但你不能在别处用main当变量名 int main; // 编译能过,但链接器会疯掉main是C语言规定的程序入口,它有特殊的地位。严格来说它是预定义标识符,而不是关键字(因为它可以出现在某些表达式中而不报错)。但请你把它当成关键字来看待——别用它命名其他东西。
七、总结
回到开头的班级比喻:
- 标识符≈ 你自己取的名字 → 大胆取,但要取得好
- 关键字≈ "校长""班长"这些禁词 → 编译器不让你用
- 预定义标识符≈ "值日生""课代表" → 你可以用,但你最好别用来命名自己的东西,否则整个班就乱套了
给初学者的三个习惯建议:
- 起名要有意义——
cnt比c好,timer_overflow_count比cnt更好 - 不要挑战关键字——编译器报错了别问为什么,就是不能这么用
- 不要覆盖库函数——你觉得
printf这个名字好,但C标准库已经用了
最后送一句:
起个好名字,代码就成功了一半。另一半是别踩上面那些坑。