18.2 标准库中的宏
标准头文件提供了一组用于描述实现能力和类型范围的宏,同时也声明相关类型。例如,INT_MAX 表示 int 的最大值,PRId64 提供适合 int64_t 的格式片段,offsetof 计算结构体成员的偏移。
1. 本章内容
- 18.2.1
<stddef.h>:常用类型、NULL和offsetof。 - 19.1
<stdint.h>:定宽整数类型及其范围宏。 - 13.6
<inttypes.h>:整数转换函数和格式宏。 - 19.2
<limits.h>:基本整数类型的范围。 - 19.3
<float.h>:浮点类型的特征与范围。 - 18.3 功能特性测试宏:判断语言版本和实现能力。
这些名称表达的是标准或具体实现给出的事实。不要用某个平台上的常见数值代替它们,也不要假定 int、long 等类型具有固定宽度。
2. 习题
下面这段程序把数值写死在代码里,在 64 位 Windows 的 MinGW-w64 上只有前两行的结果是对的:
#include <stdio.h>
int main(void) {
printf("int 最大 %d\n", 2147483647);
printf("unsigned int 最大 %u\n", 4294967295u);
printf("size_t 最大 %lu\n", 4294967295u);
printf("long 最大 %ld\n", 9223372036854775807L);
return 0;
}2
3
4
5
6
7
8
9
要求:
- 编译运行,抄下警告与输出。第 4 行为什么打印
-1?从字面量的类型和printf的格式串两方面解释。第 3 行更隐蔽:它打印出来的4294967295看起来正是一个「size_t 的最大值」,为什么这个值仍然是错的? - 改成全部使用头文件给出的宏:
INT_MAX、UINT_MAX、LONG_MAX来自<limits.h>,SIZE_MAX来自<stdint.h>,PRIuMAX来自<inttypes.h>。要求-Wall -Wextra零警告。注意PRIuMAX是字符串字面量,要和"%"相邻拼接,不要写进引号里。 - 在 64 位 Linux 上编译运行改写后的程序,与 Windows 的结果做成一张对照表。哪一项不同,原因是什么?
size_t的最大值在两边为什么相同? - 用一句话回答:正文说「不要用某个平台上的常见数值代替它们」,指的是这段程序的哪几行。
验收:改写后的程序在两套环境上都无警告;对照表里只有 long 的最大值不同——Windows 上 2147483647,Linux 上 9223372036854775807;size_t 在两套 64 位环境上都是 18446744073709551615。原程序第 3 行有两个问题:格式串与实参类型不匹配,写死的数值在 64 位上也是错的。
offsetof 得到的是成员相对结构体起始地址的字节偏移。用它在编译期把结构体的布局固定下来,是它最常见的工程用法。
要求:
- 写一个
protocol.h,用offsetof与_Static_assert断言下面这个结构体的布局在任何平台上都成立:tag在 0,len在 2,flags在 6,总大小 8 字节。
struct wire {
uint16_t tag;
uint32_t len;
uint16_t flags;
};2
3
4
5
- 直接编译这份头文件,记录三条断言的结果。为什么在默认选项下它们全部失败?用
sizeof与对齐要求解释每个成员之间的空隙从哪里来。 - 用
#pragma pack(push, 1)与#pragma pack(pop)把结构体包起来,确认三条断言通过。再用gcc -fpack-struct=1编译一份不加pragma的版本,比较两者结果。 - 把
struct wire改成「字节缓冲区 + 手工解码」:下面这 8 个字节按小端存放一条记录,写read_le16与read_le32两个函数把它解出来,打印tag=1234 len=100 flags=0001。
unsigned char buf[8] = {0x34, 0x12, 0x64, 0x00, 0x00, 0x00, 0x01, 0x00};在 -fpack-struct=1、-fpack-struct=2 与默认选项下各编译一次,确认三次输出相同。据此说明:把线上格式交给结构体布局,代价是什么。
提示:第 1 问的断言在默认选项下就是会失败,这正是它的用处——它把「实现恰好这样排」变成了「必须这样排」。
验收:第 2 问三条断言都报 static assertion failed;第 3、4 问编译通过;能说清 offsetof 的结果类型是 size_t,所以打印时用 %zu 或用 (unsigned long) 转换。
正文说「<stdio.h>、<stdlib.h>、<string.h> 也定义了 size_t、NULL 等常用名称」,又说 ptrdiff_t、max_align_t、offsetof 要显式包含 <stddef.h>。这两条说法在真机上的边界在哪里,值得逐个实测。
要求:
- 写一个脚本或手工做这张实验表:取
size_t、NULL、ptrdiff_t、max_align_t、offsetof五个名称,分别只包含<stdio.h>、<stdlib.h>、<string.h>,各编译一个只用到该名称的小程序,记录通过还是失败。
/* 示例:只测 ptrdiff_t 与 <string.h> */
#include <string.h>
int main(void) {
ptrdiff_t d = 0;
return (int)d;
}2
3
4
5
6
7
- 把结果做成 5×3 的表格。
size_t与NULL三列全通过;max_align_t与offsetof三列全失败。ptrdiff_t这一行在两套环境上的结果不同,把差异写下来。 - 对失败的那些,把完整诊断抄下来。GCC 会附一句
note: '……' is defined in header '<……>'; did you forget to '#include <……>'?,说明编译器凭什么知道该名字属于哪个头文件。 - 按诊断补上最小的一组
#include,让五个名称全部可用。再回答:ptrdiff_t在 64 位 Windows 上碰巧能用,能不能据此认定「只包含<stdio.h>就能用ptrdiff_t」? - 把第 1 步的实验改成只包含
<stdint.h>,重跑一次。SIZE_MAX、INT32_MAX能用,INT_MAX不能用——这一步说明了<stdint.h>与<limits.h>的分工。
验收:表格与上面的描述一致,ptrdiff_t 一行在 64 位 Windows 上三列全部通过、在 64 位 Linux 上三列全部失败;第 4 问的结论是「不能」,理由是可移植性只由标准保证的最小集合决定。
标准只规定了每种类型的最小范围,具体数值由实现给出。写一个 limits_contract.h,把「标准保证的下限」变成一组编译期断言,再用编译选项把「本机的事实」与「标准的保证」分开。
要求:
- 只用
_Static_assert写下面这批断言,覆盖<limits.h>、<stdint.h>、<float.h>三个头文件:
CHAR_BIT >= 8 SCHAR_MAX >= 127 UCHAR_MAX >= 255
SHRT_MAX >= 32767 USHRT_MAX >= 65535 INT_MAX >= 32767
UINT_MAX >= 65535 LONG_MAX >= 2147483647L
ULONG_MAX >= 4294967295UL MB_LEN_MAX >= 1
LLONG_MAX >= 9223372036854775807LL ULLONG_MAX >= 18446744073709551615ULL
FLT_RADIX >= 2 FLT_MANT_DIG >= 6 DBL_MANT_DIG >= 10
FLT_DIG >= 6 DBL_DIG >= 10 SIZE_MAX >= 65535
INT32_MAX == 21474836472
3
4
5
6
7
8
- 用
gcc -std=c17 -Wall -Wextra编译一次,要求全部通过。这批断言在 16 位、32 位、64 位上都必须成立,这就是它们的意义。 - 在同一个文件末尾追加三条「本机事实」,再编译:
CHAR_MIN < 0、sizeof(void *) == 8、sizeof(long) == 4。分别用-funsigned-char与-m32 -c各编译一次,记录哪条断言在哪个选项下失败。把这三条从「标准保证」里划出去,说明为什么它们不能写进公共头文件。 - 把
FLT_MAX >= 1e+37写成_Static_assert,能通过;把同一条件写成#if FLT_MAX >= 1e+37,编译器报floating constant in preprocessor expression。反过来,INT32_MAX == 2147483647与SIZE_MAX >= 65535这两条#if能正常求值。据此总结<limits.h>、<stdint.h>、<float.h>里哪些宏能进#if、哪些不能,规律是什么。 - 在
-ffreestanding下再编译一次,确认这批断言仍然全部成立。这说明这些宏在独立实现里同样可用,与 18.1 讲的宿主/独立之分无关。
验收:第 2、5 步零诊断;第 3 步 -funsigned-char 下 CHAR_MIN < 0 失败,-m32 -c 下 sizeof(void *) == 8 失败,sizeof(long) == 4 在 64 位 Windows 上通过、在 64 位 Linux 上失败;第 4 步的报错是 floating constant in preprocessor expression。