GNU C 上

Tutorial: 卓越工程师 Category: 嵌入式 Published: 2026-10-23 11:05:29 Views: 1 Likes: 0 Comments: 0

353.2.1 GNU C(上)

本节只展示了课程思维导图和网页资料,没有展示或编写业务源码,因此不在标题下添加 C 代码块。课程主题虽然写作“GNU C 标准函数”,但这一节实际讲的是 C 标准版本、目标架构、编译器及其扩展之间的关系。

学习本节时应先分清四个层次:

层次回答的问题示例
ISO C 标准源代码遵循哪一版语言规则C90、C99、C11、C17、C23
编译器及方言哪个程序翻译源码,是否允许厂商扩展GCC 的 GNU C、Arm Compiler、IAR C/C++ Compiler、Keil C51
目标架构与 ABI生成哪种机器码,数据布局与函数调用如何约定8051、Arm Cortex-M、AArch32、AArch64
操作系统与 C 库程序能调用哪些运行环境接口裸机运行库、newlib、glibc、POSIX/Linux API

这四层会相互影响,但不能混为“不同种类的 C 语言”。

一、C 语言与 GNU C

ISO C 是标准化的语言和标准库规范;GNU C 通常指 GCC 实现的 C 语言,包括所选 ISO C 版本以及可选的 GNU 扩展;GNU C Library(glibc) 则是常见于 GNU/Linux 用户空间的 C 库实现。GNU C、GCC 和 glibc 不是同一个概念。

GCC 可通过 -std= 明确选择语言方言,例如:

gcc -std=c90 -pedantic-errors main.c
gcc -std=c99 -pedantic-errors main.c
gcc -std=gnu99 main.c
  • c99 表示尽量按 ISO C99 编译;配合 -pedantic-errors 可把所要求的标准诊断作为错误报告;
  • gnu99 表示以 C99 为基础,同时启用 GNU 扩展;
  • GCC 的默认方言会随版本变化,工程不应依赖“大家电脑上碰巧相同”的默认值,而应在构建系统中明确指定。

可用下列命令查看当前 GCC 方言预定义的 __STDC_VERSION__:

printf '' | gcc -dM -E -x c - | grep __STDC_VERSION__

C90 本身没有定义该宏,因此严格 C90 模式下可能没有输出;__GNUC__ 等宏表示编译器兼容的 GNU 版本接口,不能代替 C 标准版本判断。

1.1 C 语言版本的区别

原笔记把 8051 和 STM32 称为“非标 C 语言的两大类”,这是概念混淆:

  • 8051/MCS-51 是一种 8 位微控制器架构及其衍生器件家族;
  • STM32 是 ST 的微控制器产品家族,大多数 STM32 MCU 使用 Arm Cortex-M 内核;
  • ARM Linux 表示运行在 Arm 架构处理器上的 Linux 系统;
  • 它们描述的是处理器、芯片或运行环境,而不是 C 标准的不同分支。

同一份符合 ISO C 的源代码,可以由不同交叉编译器翻译成 8051、Arm Cortex-M 或 Arm Linux 的目标代码。真正影响可移植性的通常是以下内容:

  • 整数宽度、字节序、对齐及调用约定等实现或 ABI 差异;
  • 编译器专有关键字、属性和 #pragma;
  • 裸机、RTOS 与 Linux 所提供的库和系统 API 不同;
  • 对硬件寄存器、地址空间和中断机制的直接依赖。

C51 常指面向 8051 的 C 编译器/方言,例如 Keil C51。它以标准 C 为基础,又为 8051 的哈佛结构、多地址空间、位寻址和中断机制增加了实现相关扩展,如存储区限定符、位类型、特殊功能寄存器和中断声明。它不是一个与 ISO C 平级的国际标准。

A51/Ax51 更准确地说是 Keil 8051 汇编工具及相关源文件命名;程序所使用的是 8051 汇编语言,不能把“A51”理解为一种通用汇编语言标准。

ARM Linux 应用也不是使用所谓“ARM 分支的非标 C”。它通常使用 ISO/GNU C 源码,交叉编译器根据目标三元组生成相应机器码。例如:

常见工具链前缀典型目标
arm-none-eabi-Arm 裸机或 RTOS,通常不以 Linux 为运行目标
arm-linux-gnueabihf-32 位 Arm GNU/Linux,硬浮点 ABI
aarch64-linux-gnu-64 位 Arm GNU/Linux

工具链名称不仅表达 CPU 架构,也可能表达操作系统和 ABI;“都是 Arm”不代表生成的程序可以互换运行。

1.2 单片机 C 语言的编译器

视频列举了 Keil MDK、IAR 和 GNU C/GCC,但三者并非完全相同层级的名称:

  • Keil MDK 是 Arm 嵌入式开发工具套件,可包含 IDE、编译器、调试器和设备支持;具体编译器可能是 Arm Compiler for Embedded、Arm GNU Toolchain 等。面向 8051 的 Keil C51/PK51 又是另一套产品线。
  • IAR Embedded Workbench 也是集成工具链,其中包含 IAR C/C++ Compiler、链接器和调试器等组件。
  • GCC 是 GNU Compiler Collection;面向 Arm 嵌入式目标时通常使用相应的交叉工具链,而不是普通 x86 Linux 主机上的 gcc。

不同编译器之间的差异不一定只是“细微语法不同”,还可能包括:

  • 支持的 ISO C 版本和缺陷修正程度;
  • 专有关键字、属性、内建函数与预定义宏;
  • ABI、目标文件格式、链接脚本和启动文件;
  • 优化策略、标准库实现、调试信息和诊断行为;
  • 功能安全认证或特定芯片支持。

底层库经常使用条件编译选择不同编译器实现,例如依据 __GNUC__、__ICCARM__ 或 Arm Compiler 相关宏选择属性写法。其目标不是证明这些语言“都基于 GNU C 修改”,而是把同一抽象映射到不同编译器的扩展语法。Arm CMSIS 提供的 cmsis_compiler.h 就定义了 __PACKED、__ALIGNED、__WEAK 等统一宏,供上层源码尽量保持编译器无关。

视频以“单字节对齐”为例。更准确地说,这是结构体打包或对象对齐控制:

  • GCC 常使用属性或 #pragma 表达;
  • Arm Compiler、IAR 等可能采用不同关键字或 #pragma;
  • 打包会减少或取消结构体成员之间的填充,但可能产生未对齐访问;
  • 未对齐访问在某些内核上会降低效率,甚至触发异常,因此不能只为节省几个字节就随意打包结构体。

可移植项目应优先使用 CMSIS 等经过维护的适配层,或把编译器差异集中封装在少量头文件中,而不是让业务代码到处散布厂商宏。

二、GNU C 各个版本之间的差异

这里更准确的标题含义应是“ISO C 标准版本,以及 GCC 对这些版本和 GNU 扩展的支持”。C89、C99、C11 等不是 GNU 单独制定的版本。

2.1 C 语言的发展史

常用名称正式标准/时间关键说明
C89ANSI X3.159-1989ANSI X3J11 委员会从 1983 年开始标准化工作,最终标准于 1989 年发布;不能把 1983 年写成首个标准的发布年份
C90ISO/IEC 9899:1990ISO/IEC 采纳 C89 后形成首版国际标准,核心语言基本相同
C95ISO C90 的 1995 年修正案增补宽字符等内容,并引入 __STDC_VERSION__;常被课程简史省略
C99ISO/IEC 9899:1999正式年份是 1999,不是原笔记所写的 2000
C11ISO/IEC 9899:2011加入原子操作、线程支持、静态断言等重要内容
C17/C18ISO/IEC 9899:2018通常称 C17,也常因正式出版年份称 C18;主要整合 C11 的缺陷修正,并非大量新增语言特性
C23ISO/IEC 9899:2024常用名称沿用制定阶段的 C23,正式国际标准于 2024 年出版

“后一个版本只是不断叠加特性”也过于简单。新标准除了增加能力,还会澄清语义、修正缺陷、弃用或删除旧特性;具体编译器也可能只实现其中一部分。

视频称企业通常只使用 C89/C99、很少使用 C11/C17,这只能视为特定项目经验。实际选择取决于编译器支持、芯片工具链、认证要求、既有代码和团队规范:老旧嵌入式工程可能长期停留在 C90/C99,现代 Linux 和新工具链项目则经常使用 C11、C17,甚至开始评估 C23。判断项目标准应查看编译选项和编码规范,而不是根据行业印象推测。

2.2 C89 和 C99 的区别

C99 在 C90 基础上增加并规范了许多能力。视频重点提到的内容应这样理解:

特性准确含义初学者易错点
声明与语句混排C99 允许在代码块中的语句之后继续声明变量,也允许在 for 初始化部分声明循环变量不是“临时变量”才适用;严格 C90 要求块内声明出现在语句之前
// 单行注释C99 把 C++ 风格单行注释纳入标准/* ... */ 块注释仍然有效,并没有被替代;很多 C90 编译器也曾把 // 当扩展接受
变长数组(VLA)块作用域数组的长度可在进入声明处由运行时表达式确定数组创建后长度不会继续自动变化;它通常占用自动存储,尺寸过大有栈溢出风险;从 C11 起实现可以不提供完整 VLA 支持
复合字面量用带类型的字面形式创建结构体、联合体或数组对象不只用于函数内;对象生存期由所在作用域决定,不能简单等同于“马上消失的临时值”
灵活数组成员结构体最后一个成员可以写成不指定长度的数组,结构体还必须有其他具名成员sizeof 不包含数组后的额外元素,使用者须另外分配足够存储;它与复合字面量不是同一机制
_Bool 与 <stdbool.h>C99 增加内建类型 _Bool,并由 <stdbool.h> 提供 bool、true、false 等便捷名称视频把真假数值说反了:0 表示假,非 0 表示真;转换到 _Bool 后结果为 0 或 1,false 为 0、true 为 1
数学库增强C99 扩展 <math.h>,增加不同浮点类型版本、分类/比较宏及更多数学函数标准只规定接口和语义,不要求用汇编实现,也不保证一定比手写代码快;性能取决于库、编译器、优化和硬件

C99 还包括 long long、<stdint.h> 定宽整数类型、inline、restrict、指定初始化器、可变参数宏和复数类型等重要内容。是否能在某个 MCU 工程中使用,仍要检查对应编译器、标准库和编译选项。

在 GCC/Linux 环境中调用部分数学函数时,常见构建方式是在链接阶段加入 -lm;这属于具体工具链的链接要求,不是 C99 语法本身。库函数也未必会形成真正的函数调用:编译器可能把某些调用识别为内建操作并进行常量折叠或指令替换。

本节最重要的判断方法是:先确认代码要求的 ISO C 版本,再确认是否使用了 GNU/厂商扩展,最后检查 目标 ABI、运行环境和库。只看到代码运行在 STM32 或 ARM Linux 上,不能据此判断它属于哪一版 C 标准。

参考资料

Prev: 环境搭建 Next: GNU C下