函数声明与原型
函数声明(也称为函数原型)告诉编译器函数的名称、返回类型和参数类型,但不提供函数体。它让编译器在函数调用处进行类型检查,捕获参数类型不匹配、数量错误等问题。在 C99 中,调用未声明的函数是非法的(C89 允许隐式声明,但极其危险)。
基本语法
返回类型 函数名(参数类型列表);
/* 函数声明(原型) */
int add(int a, int b);
double sqrt(double x);
void print_array(const int arr[], int n);
/* 函数定义 */
int add(int a, int b)
{
return a + b;
}
/* 调用 */
int main(void)
{
int result = add(3, 5); /* 编译器根据原型检查参数 */
return 0;
}
为什么需要原型
没有原型时,编译器不知道函数的参数类型,无法进行类型检查:
/* 没有原型 */
int main(void)
{
double result = sqrt(2); /* C89:隐式声明 int sqrt() */
/* 参数 2(int)传递方式可能与 double 不同! */
/* 运行时可能崩溃或产生错误结果 */
return 0;
}
C89 允许隐式函数声明:遇到未声明的函数调用时,编译器假设它返回 int,参数类型根据调用时的实参推断。这导致大量难以发现的 Bug。
C99 禁止隐式声明,调用未声明的函数是编译错误。
原型中的参数名
原型中的参数名是可选的,但有助于文档化:
/* 只有类型 */
int add(int, int);
/* 带参数名(推荐) */
int add(int a, int b);
参数名在原型中不参与编译,只供人阅读。定义中的参数名可以与原型不同:
/* 原型 */
int add(int x, int y);
/* 定义:参数名不同,合法 */
int add(int a, int b)
{
return a + b;
}
旧式声明 vs 原型声明
C89 支持旧式(K&R)声明,不指定参数类型:
/* 旧式声明 */
int add();
/* 调用 */
int result = add(3, 5); /* 编译器不检查参数 */
/* 定义 */
int add(a, b)
int a;
int b;
{
return a + b;
}
旧式声明在 C99 中已废弃,不应使用。始终使用原型声明:
/* 原型声明 */
int add(int a, int b);
/* 调用 */
int result = add(3, 5); /* 编译器检查:两个 int 参数 */
/* 定义 */
int add(int a, int b)
{
return a + b;
}
可变参数函数的原型
printf、scanf 等可变参数函数用 ... 表示:
#include <stdarg.h>
/* 原型 */
int printf(const char *format, ...);
/* 自定义可变参数函数 */
double average(int count, ...)
{
va_list args;
va_start(args, count);
double sum = 0;
for (int i = 0; i < count; i++)
sum += va_arg(args, double);
va_end(args);
return sum / count;
}
可变参数函数至少需要有一个固定参数(如 printf 的 format),va_start 需要这个固定参数来确定可变参数的起始位置。
头文件中的声明
多文件项目中,函数原型放在头文件中,源文件包含头文件:
/* math_utils.h */
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
double divide(double a, double b);
#endif
/* math_utils.c */
#include "math_utils.h"
int add(int a, int b)
{
return a + b;
}
/* ... */
/* main.c */
#include "math_utils.h"
int main(void)
{
int result = add(3, 5); /* 编译器通过头文件知道原型 */
return 0;
}
常见错误
调用未声明的函数:
int main(void)
{
int result = add(3, 5); /* C99 错误:add 未声明 */
return 0;
}
原型与定义不匹配:
/* 原型 */
int add(int a, int b);
/* 定义:返回类型不匹配 */
double add(int a, int b) /* 错误 */
{
return a + b;
}
/* 定义:参数类型不匹配 */
int add(double a, double b) /* 错误 */
{
return (int)(a + b);
}
参数数量不匹配:
int add(int a, int b);
int result = add(1, 2, 3); /* 错误:参数过多 */
int result2 = add(1); /* 错误:参数不足 */
最佳实践
- 始终提供函数原型(C99 强制要求)
- 原型放在头文件中,源文件包含头文件
- 原型中的参数名与定义一致(便于阅读)
- 使用头文件保护防止重复包含
- 开启
-Wmissing-prototypes和-Wstrict-prototypes警告 - 不要依赖隐式声明(C89 的坏习惯)