函数声明
函数声明用于告诉编译器:某个函数的名字、返回类型和形参类型是什么。它是跨翻译单元协作的接口契约,不是可有可无的注释。
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. 为什么原型重要
函数原型让编译器在调用点执行类型检查,提前发现实参类型不匹配问题。缺少原型时,历史兼容规则会引入默认提升,错误往往延迟到运行期。
3. 链接属性
函数若需要跨文件可见,按默认外部链接即可;若只在当前翻译单元使用,声明为 static 更稳妥。
static int helper(int x) {
return x * x;
}2
3
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
这样可以避免内部实现细节暴露到全局符号表。
4. 实践建议
头文件中的声明应保持最小且稳定,避免把内部依赖泄露给调用方。接口一旦发布,就应把“向后兼容”当作设计约束,而不是临时修补。
5. 旧式声明的兼容边界
C 早期存在未给出原型信息的旧式写法,但现代代码应避免继续使用。缺少完整原型会削弱编译期类型检查,尤其在可变参数或隐式转换路径上更容易埋下错误。
/* 推荐:给出完整原型 */
double norm2(double x, double y);2
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
只要接口面向跨文件调用,就应尽量保证“声明先于调用且类型完整可检查”。
6. 声明一致性检查
同一函数若在多个头文件或源文件里重复声明,类型必须保持一致。哪怕只有一个限定符或形参类型写错,都可能造成调用点和定义点理解不一致。
/* 头文件 A */
int parse(const char *s);
/* 头文件 B(错误示例) */
/* int parse(char *s); */2
3
4
5
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
把函数声明集中在单一头文件,并让所有调用方都包含它,是避免声明漂移的有效方式。
7. 形参数组声明的调整规则
函数形参中写成数组形式时,会调整为指针类型参与函数类型匹配。例如 void f(int a[10]); 与 void f(int *a); 在函数声明层面等价。这个规则经常导致“在声明里写了长度就以为接口会自动检查长度”的误解;长度约束若要生效,仍需由调用方协议或额外参数保证。
void fill(int a[10], int n); /* 声明层面 a 仍是 int * */
void fill(int *a, int n); /* 与上式兼容 */2
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
8. 头文件应自给自足
一个稳妥的头文件应包含它自身声明所需的全部前置声明与头依赖,而不是依赖“调用方恰好先包含了别的头”。这样任何源文件只要包含该头就能独立通过语义检查,接口边界也更清晰。把声明做成可独立编译单元,是长期维护里很关键的稳定性基础。