6. 头文件与接口边界
头文件用于让多个翻译单元共享一致声明。本章第一次要求读者编写自己的 .h 文件;第一部分只使用标准头。
函数声明用于告诉编译器:某个函数的名字、返回类型和形参类型是什么。它是跨翻译单元协作的接口契约,不是可有可无的注释。
1. 声明与定义
函数定义提供实现,函数声明提供接口。一个函数可以声明多次,但定义通常只有一次。把声明放进头文件,把定义放进 .c 文件,是最稳定的组织方式。
/* math_ext.h */
#ifndef MATH_EXT_H
#define MATH_EXT_H
double add(double lhs, double rhs);
#endif2
3
4
5
6
7
/* math_ext.c */
#include "math_ext.h"
double add(double lhs, double rhs) {
return lhs + rhs;
}2
3
4
5
6
2. 链接属性
函数若需要跨文件可见,按默认外部链接即可;若只在当前翻译单元使用,声明为 static 更稳妥。
static int helper(int x) {
return x * x;
}2
3
这样可以避免内部实现细节暴露到全局符号表。
3. 声明一致性检查
同一函数若在多个头文件或源文件里重复声明,类型必须保持一致。哪怕只有一个限定符或形参类型写错,都可能造成调用点和定义点理解不一致。
/* 头文件 A */
int parse(const char *s);
/* 头文件 B(错误示例) */
/* int parse(char *s); */2
3
4
5
把函数声明集中在单一头文件,并让所有调用方都包含它,是避免声明漂移的有效方式。
4. 文件作用域与接口边界
文件作用域与 static 的可见性
文件作用域标识符若配合 static,只在当前翻译单元可见;不加 static 则可能形成外部链接,被其他翻译单元访问。
5. 前向声明 + 不完全类型(用于隐藏实现)
typedef struct list list;
/* 此处可以只暴露 list* 相关 API,而把 struct list 的定义放到 .c 文件中。 */2
3
6. 包含保护
头文件通常使用 #ifndef 保护,防止同一翻译单元重复包含:
#ifndef MY_LIB_H
#define MY_LIB_H
/* 声明 */
#endif2
3
4
5
6
包含保护不是可选装饰,而是头文件最基础的正确性要求。
把带内部链接的定义写进头文件,会使每个包含它的翻译单元各自拥有一份实体,尤其是 static 对象;除非接口明确要求每个翻译单元独立持有状态,否则不应这样设计。
7. #pragma once 的边界
#pragma once 受到绝大多数现代编译器支持,可以看作事实标准。它出现在头文件中时,表示这个头文件在同一翻译单元内无论被包含多少次,都只解析一次。
它的目标与传统的 #ifndef / #define 包含保护一致,但属于实现扩展。需要严格可移植时使用标准包含保护;选择 #pragma once 时,应把对目标工具链的要求写进工程约束。
8. 习题
写一个最小示例(分成两个 .c 文件):在一个文件里定义 static int x;,在另一个文件里尝试 extern int x; 并使用它。说明会发生什么,为什么。
解释:为什么“把 static 对象的定义写在头文件”通常是错误的库设计?会造成什么行为?
写一个“隐藏实现”的最小示例:在头文件里只暴露 typedef struct foo foo; 与 foo* foo_create(void);,并说明这样做的好处与代价。