GNU C下
354.2.2 GNU C(下)
/* ===== 视频中实际编译的文件:main.c ===== */
/* 引入标准输入输出函数的声明,供 printf 使用。 */
#include <stdio.h>
/* 引入 C99 布尔宏;本文件最终版本没有使用其中的名称。 */
#include <stdbool.h>
/* 程序从 main 函数开始执行。 */
int main()
{
/* 输出字符串;\r 是回车,\n 是换行。 */
printf("Hello ssh\r\n");
/* 定义并初始化整型变量;该变量随后没有被使用。 */
int i = 0;
/* 返回 0,表示程序正常结束。 */
return 0;
}
/* ===== 课程配套文件:c89_example.c ===== */
/* 条件为 0,预处理时排除下面的第一个示例。 */
#if 0
/* 引入 printf 的声明。 */
#include <stdio.h>
/* 第一个示例的程序入口。 */
int main() {
/* C90 要求本代码块的声明出现在语句之前。 */
int i;
/* 使用已声明的 i 完成十次循环。 */
for (i = 0; i < 10; i++) {
/* 输出当前的 i,并在数字后输出一个空格。 */
printf("%d ", i);
}
/* 输出换行符。 */
printf("\n");
/* 返回 0,表示程序正常结束。 */
return 0;
}
/* 结束第一个示例的条件编译区域。 */
#endif
/* 引入 printf 的声明。 */
#include <stdio.h>
/* 引入 round、trunc、remainder、fmax 和 fmin 的声明。 */
#include <math.h>
/* 数学函数示例的程序入口。 */
int main() {
/* 定义正浮点数 x。 */
double x = 3.5;
/* 定义负浮点数 y。 */
double y = -4.2;
/* 输出 x 以及 round(x) 的结果。 */
printf("Round of %f is %f\n", x, round(x));
/* 输出 y 以及 trunc(y) 的结果。 */
printf("Truncate of %f is %f\n", y, trunc(y));
/* 输出 x 除以 y 的 IEEE 风格余数。 */
printf("Remainder of %f divided by %f is %f\n", x, y, remainder(x, y));
/* 输出 x 与 y 中较大的数。 */
printf("Maximum of %f and %f is %f\n", x, y, fmax(x, y));
/* 输出 x 与 y 中较小的数。 */
printf("Minimum of %f and %f is %f\n", x, y, fmin(x, y));
/* 返回 0,表示程序正常结束。 */
return 0;
}
/* ===== 课程配套文件:c99_example.c ===== */
/* 条件为 0,预处理时排除前三个示例。 */
#if 0
/* 引入 printf 的声明。 */
#include <stdio.h>
/* for 循环声明示例的程序入口。 */
int main() {
/* C99 允许在 for 的初始化部分声明循环变量 i。 */
for (int i = 0; i < 10; i++) {
/* 输出当前的 i,并在数字后输出一个空格。 */
printf("%d ", i);
}
/* 输出换行符。 */
printf("\n");
/* 返回 0,表示程序正常结束。 */
return 0;
}
/* 引入 printf 的声明;同一条件区中的重复包含是允许的。 */
#include <stdio.h>
/* 接收运行时给出的数组元素个数 n。 */
void print_array(int n) {
/* 定义含 n 个 int 元素的变长数组。 */
int arr[n];
/* 依次访问数组的每个有效下标。 */
for (int i = 0; i < n; i++) {
/* 把当前下标值写入对应元素。 */
arr[i] = i;
}
/* 再次依次访问数组的每个有效下标。 */
for (int i = 0; i < n; i++) {
/* 输出当前数组元素,并在数字后输出一个空格。 */
printf("%d ", arr[i]);
}
/* 一组数组元素输出完毕后换行。 */
printf("\n");
}
/* 变长数组示例的程序入口。 */
int main() {
/* 创建并打印含 5 个元素的变长数组。 */
print_array(5);
/* 创建并打印含 10 个元素的变长数组。 */
print_array(10);
/* 返回 0,表示程序正常结束。 */
return 0;
}
/* 引入 printf 的声明;头文件保护机制会避免重复定义。 */
#include <stdio.h>
/* 定义一个具有 x、y 两个整型成员的结构体别名 Point。 */
typedef struct {
/* 保存横坐标。 */
int x;
/* 保存纵坐标。 */
int y;
/* 结束结构体定义,并把该类型命名为 Point。 */
} Point;
/* 复合字面量示例的程序入口。 */
int main() {
/* 用 Point 类型的复合字面量初始化局部变量 p。 */
Point p = (Point) {10, 20};
/* 读取并输出 p 的两个成员。 */
printf("Point: (%d, %d)\n", p.x, p.y);
/* 返回 0,表示程序正常结束。 */
return 0;
}
/* 结束前三个 C99 示例的条件编译区域。 */
#endif
/* 引入 printf 的声明。 */
#include <stdio.h>
/* 引入 malloc 和 free 的声明。 */
#include <stdlib.h>
/* 定义带灵活数组成员的结构体。 */
typedef struct {
/* 记录后续 data 区域中计划存放的元素个数。 */
int length;
/* 声明灵活数组成员;它必须位于结构体末尾。 */
int data[];
/* 结束结构体定义,并把该类型命名为 Array。 */
} Array;
/* 按传入的元素个数申请一个 Array 对象。 */
Array *create_array(int length) {
/* 输出结构体基部和 int 的大小;这里保留课程源码的格式写法。 */
printf("Array size:%ld, int size:%ld \r\n", sizeof(Array), sizeof(int));
/* 一次申请结构体基部及 length 个 int 所需的连续空间。 */
Array *arr = malloc(sizeof(Array) + length * sizeof(int));
/* 只有申请成功后才能解引用 arr。 */
if (arr != NULL) {
/* 保存元素个数,供调用者控制访问边界。 */
arr->length = length;
}
/* 返回新对象的地址;申请失败时返回空指针。 */
return arr;
}
/* 灵活数组成员示例的程序入口。 */
int main() {
/* 申请可容纳 5 个 int 元素的 Array 对象。 */
Array *arr = create_array(5);
/* 先确认内存申请成功。 */
if (arr != NULL) {
/* 在记录的元素范围内逐项访问 data。 */
for (int i = 0; i < arr->length; i++) {
/* 把当前下标的两倍写入对应元素。 */
arr->data[i] = i * 2;
/* 输出刚刚写入的元素值。 */
printf("%d ", arr->data[i]);
}
/* 所有元素输出完毕后换行。 */
printf("\n");
/* 释放由 malloc 取得的整块连续内存。 */
free(arr);
}
/* 返回 0,表示程序正常结束。 */
return 0;
}
/* 条件为 0,默认排除布尔类型示例。 */
#if 0
/* 引入 printf 的声明。 */
#include <stdio.h>
/* 引入 bool、true 和 false 宏。 */
#include <stdbool.h>
/* 布尔类型示例的程序入口。 */
int main() {
/* 定义值为真的布尔变量。 */
bool is_true = true;
/* 定义值为假的布尔变量。 */
bool is_false = false;
/* 值非零时进入该分支。 */
if (is_true) {
/* 输出 is_true 为真的提示。 */
printf("is_true is true\n");
}
/* 对假值取逻辑非会得到真,因此进入该分支。 */
if (!is_false) {
/* 输出 is_false 为假的提示。 */
printf("is_false is false\n");
}
/* 返回 0,表示程序正常结束。 */
return 0;
}
/* 结束布尔类型示例的条件编译区域。 */
#endif
/* 条件为 0,默认排除数学函数示例。 */
#if 0
/* 引入 printf 的声明。 */
#include <stdio.h>
/* 引入所用数学函数的声明。 */
#include <math.h>
/* C99 数学函数示例的程序入口。 */
int main() {
/* 定义正浮点数 x。 */
double x = 3.5;
/* 定义负浮点数 y。 */
double y = -4.2;
/* 输出 x 以及 round(x) 的结果。 */
printf("Round of %f is %f\n", x, round(x));
/* 输出 y 以及 trunc(y) 的结果。 */
printf("Truncate of %f is %f\n", y, trunc(y));
/* 输出 x 除以 y 的 IEEE 风格余数。 */
printf("Remainder of %f divided by %f is %f\n", x, y, remainder(x, y));
/* 输出 x 与 y 中较大的数。 */
printf("Maximum of %f and %f is %f\n", x, y, fmax(x, y));
/* 输出 x 与 y 中较小的数。 */
printf("Minimum of %f and %f is %f\n", x, y, fmin(x, y));
/* 返回 0,表示程序正常结束。 */
return 0;
}
/* 结束数学函数示例的条件编译区域。 */
#endif
上面依次收录了视频最终实际编译的 main.c,以及课程资料中的 c89_example.c、c99_example.c。配套文件用 #if 0 保存多个互斥的 main 示例;视频演示时会临时改变这些开关。一次编译只能让一组同名函数定义处于有效状态,否则会发生重复定义。
一、GNU C
1.1 查看编译器 C 语言版本
视频在终端执行的是:
gcc -E -dM - </dev/null | grep "STDC_VERSION"
其中:
-E表示只进行预处理;-dM要求预处理器输出所有有效的宏定义;-表示从标准输入读取源文件,</dev/null则提供空输入;grep只保留名称中含有STDC_VERSION的行。
为了明确告诉 GCC 输入是 C 源码,也可以写成:
printf '' | gcc -dM -E -x c - | grep __STDC_VERSION__
视频得到:
#define __STDC_VERSION__ 201710L
这里需要纠正原讲解:201710L 不是 GCC 的版本号,也不能单独证明该 GCC 所能支持的最高 C 标准。它是本次预处理所选择的 C 方言对 __STDC_VERSION__ 宏给出的值,表示该模式宣称对应 C17;末尾的 L 是整型常量的 long 后缀。
常见取值如下:
| 宏值 | 对应标准 |
|---|---|
199409L | C90 的 1995 年修正案 |
199901L | C99 |
201112L | C11 |
201710L | C17 |
202311L | C23 |
严格 C90 模式没有定义这个宏,因而没有输出也是正常现象。要判断编译器程序本身的版本,应运行 gcc --version;要检查某一标准能否使用,应明确添加相应的 -std= 选项进行编译。默认方言可能随 GCC 版本变化,工程构建不应依赖机器上的默认值。
1.2 编译程序
视频先把 main.c 中的 _Bool a = true; 删除,再执行:
gcc main.c -o main
./main
第一条命令把 main.c 编译并链接成名为 main 的可执行文件;-o 是 output file 选项,后面必须跟输出文件名。第二条命令中的 ./ 表示从当前目录寻找并运行 main。原笔记中的 man.c、man 是识别错误。
视频最终的 main.c 仍在 printf 语句之后声明了 int i = 0;。这种“语句与声明混合”的写法由 C99 标准化;若按严格 C90 编译,不能据此声称代码符合 C90。该变量没有被使用,开启警告后通常会看到提示:
gcc -std=c99 -Wall -Wextra main.c -o main
选择不同标准时,建议同时使用严格诊断:
gcc -std=c89 -pedantic-errors source.c -o program
gcc -std=c99 -pedantic-errors source.c -o program
-std=c89 与 -std=c90 在 GCC 中是同义选项;-pedantic-errors 可把所选 ISO 标准要求诊断的扩展用法报告为错误。若使用 gnu89、gnu99 等 GNU 方言,GCC 还会接受相应 GNU 扩展,因此“能被 GCC 编过”不等于“符合严格 ISO C”。
1.3 C89 和 C99 差异
视频用 #if 0 与 #endif 在一个文件里切换多段示例。这是预处理条件编译,不是 C 语言注释,更不是企业项目中普遍优于注释的固定规范:
#if 0
/* 这部分预处理后不进入翻译单元。 */
#endif
#if 0使其控制的预处理组不被送入后续编译阶段;#if 1则把该组纳入编译。它决定代码是否参与编译,不是决定代码在运行时“是否执行”。- 每个条件指令必须正确配对。视频漏写一个
#endif后出现unterminated #if,正是因为预处理条件没有闭合。 - 被排除的文本也不是可以任意破坏的“垃圾区”:注释和字符串仍须正确结束,嵌套的条件指令也必须配平。
- 临时保留一整段实验代码时可使用这种方法;长期无用代码应交给版本控制管理,功能差异则应使用含义明确的特性宏,不宜堆积大量
#if 0。
C99 把 // 单行注释纳入标准,/* ... */ 块注释在 C89/C90 和 C99 中都可使用。// 遇到换行即结束,不会因为用户“按回车”本身导致编译错误;真正的风险通常是下一行本应属于注释,却没有继续写 //。
1.3.1 临时变量定义
C90 要求一个代码块中的声明出现在该块的语句之前;需要晚些定义变量时,可以另开一个内部代码块。C99 开始允许声明与语句交错,也允许在 for 初始化部分声明循环变量:
for (int i = 0; i < 10; i++) {
printf("%d ", i);
}
这里的 i 只在该 for 语句及其循环体的作用域内有效。对应的 C90 写法是先声明再使用:
int i;
for (i = 0; i < 10; i++) {
printf("%d ", i);
}
课程的 C99 文件还包含 // 注释和其他 C99 语法。若整份文件用 -std=c89 -pedantic-errors 编译,可能同时出现多类诊断,不能把所有错误都归因于 for 中的变量声明。要验证单个特性,应制作只包含该特性的最小示例。
1.3.2 变长数组
变长数组(variable length array,VLA)的长度不是整型常量表达式,而是在程序执行到声明处时求值。例如配套源码中的:
void print_array(int n) {
int arr[n];
/* ... */
}
arr 进入作用域时根据 n 确定长度,在该数组的生存期内不会自动伸缩。课程调用 print_array(5) 和 print_array(10),分别打印 0~4 与 0~9。
使用 VLA 时还应注意:
- 本例中的
n必须大于 0;零或负的数组界限不满足 VLA 的运行时约束。 - 这种块作用域数组具有自动存储期。常见实现把它放在栈上,但 C 标准并不规定必须使用“栈”;长度过大会耗尽有限的自动存储空间。
- VLA 是 C99 的必备特性;从 C11 起,实现可以选择不支持相关类型和求值语义。因此面向多个编译器或资源受限系统时,应确认工具链能力,必要时改用定长上限或经过检查的动态分配。
- 用
-std=c89编译课程的整份 C99 示例时,for (int i = ...)等语法也会报错;这样的测试不能单独证明错误来自 VLA。
1.3.3 复合字面量
C99 的复合字面量语法是“带类型的花括号初始化列表”,它产生一个对象,可在表达式中直接使用:
Point p = (Point){10, 20};
课程写作 (Point) {10, 20},空格不影响含义。真正属于 C99 的关键部分是 (Point){...} 这个复合字面量表达式;原讲解所说“C89 只能在全局给结构体赋初值”并不正确。C90 本来就允许在局部变量的声明处使用聚合初始化:
Point p = {10, 20};
C99 的优势在于复合字面量可以出现在更一般的表达式位置,例如直接传给函数:
draw_point((Point){10, 20});
复合字面量不是普通的类型转换,也不等于每次都在堆上申请内存。其存储期由出现位置决定:文件作用域下具有静态存储期,块作用域下具有与所在块关联的自动存储期。
1.3.4 灵活数组成员
灵活数组成员(flexible array member)是在结构体最后声明的、不写元素个数的不完整数组成员:
typedef struct {
int length;
int data[];
} Array;
它必须是结构体的最后一个成员,而且结构体在它之前还要有其他具名成员。data 不是指针:结构体中没有保存一枚指向别处的地址,sizeof(Array) 也不包含任何 data 元素的存储空间。实现可能在成员之间或结构体末尾加入对齐填充,所以视频在当时平台上输出 Array size:4, int size:4,不能当作所有平台的固定结果。
课程通过一次 malloc 取得“结构体基部 + 额外元素”的连续空间:
低地址 高地址
+----------------------+------------------------------------------+
| length 与可能的填充 | data[0] | data[1] | ... | data[length-1] |
+----------------------+------------------------------------------+
^ Array *arr ^ offsetof(Array, data)
课程中的申请式为:
malloc(sizeof(Array) + length * sizeof(int))
申请成功后,编译器把 arr->data[i] 解释为访问同一对象末尾的附加存储。length 只是程序自行维护的元数据,C 运行时不会根据它自动检查下标,也不会让数组自动扩容。
这份课程源码适合展示语法,但生产代码还缺少几项防护:
- 应拒绝负的
length,并在乘法、加法前检查是否超过SIZE_MAX,否则转换为size_t或整数溢出可能导致分配尺寸错误; malloc失败时必须保持当前的空指针检查;sizeof的结果类型是size_t,可移植的printf格式是%zu。源码中的%ld与类型不严格匹配;free(arr)会一次释放结构体基部和附加元素,因为它们来自同一次分配。
视频实际运行时给 5 个元素依次写入 i * 2,所以打印 0 2 4 6 8。
1.3.5 C99 布尔类型
C99 增加了内建类型 _Bool,并在 <stdbool.h> 中提供更易读的宏:
bool展开为_Bool;true展开为整数常量1;false展开为整数常量0。
赋给 _Bool 的标量值会归一化为 0 或 1。因此课程中的 is_true 条件为真,而 !is_false 也为真,两条提示都会输出。
视频最初在 main.c 写了 _Bool a = true;。这行混合使用了关键字 _Bool 和 <stdbool.h> 提供的 true 宏,本身在 C99 及后续相应模式下有效;随后它被删除,所以标题下保留的是最终真正参与编译的版本。C89/C90 没有标准化 _Bool、<stdbool.h>、bool、true 或 false。
1.3.6 C99 数学函数
课程示例调用了 <math.h> 中由 C99 标准化的几个函数:
| 函数 | 准确含义 | 本例结果 |
|---|---|---|
round(x) | 舍入到最近整数;恰好位于中点时向远离 0 的方向取值 | round(3.5) 为 4.0 |
trunc(x) | 丢弃小数部分,即向 0 方向取整 | trunc(-4.2) 为 -4.0 |
remainder(x, y) | 计算 x - n*y,其中 n 是最接近 x/y 的整数,中点按偶数规则选择 | remainder(3.5, -4.2) 约为 -0.7 |
fmax(x, y) | 返回较大值,并按标准规则处理 NaN | fmax(3.5, -4.2) 为 3.5 |
fmin(x, y) | 返回较小值,并按标准规则处理 NaN | fmin(3.5, -4.2) 为 -4.2 |
round 不能笼统解释为对所有数都按日常口语中的“四舍五入”,因为负数中点的方向尤其容易混淆;remainder 也不是整数 % 运算,更不等同于 fmod。若一个参数是 NaN 而另一个是数值,fmax、fmin 返回数值参数;两个参数都是 NaN 时返回 NaN。
这些名称是标准库函数,不应一概称为“编译器内置函数”或断言“内部一定由汇编实现”。实现可以使用库代码、编译器内建、硬件指令或它们的组合,只要满足标准语义。在常见 GNU/Linux 工具链中,链接数学库通常写作:
gcc -std=c99 c99_math.c -o c99_math -lm
链接器通常从左向右处理输入,因此把 -lm 放在源文件或目标文件之后更稳妥。课程的 c89_example.c 默认启用的恰好也是这些 C99 数学函数;文件名只是示例组织方式,不能证明该活动代码符合 C89。