GNU C下

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

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 后缀。

常见取值如下:

宏值对应标准
199409LC90 的 1995 年修正案
199901LC99
201112LC11
201710LC17
202311LC23

严格 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)返回较大值,并按标准规则处理 NaNfmax(3.5, -4.2) 为 3.5
fmin(x, y)返回较小值,并按标准规则处理 NaNfmin(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。

参考资料