一、核心铁律:声明、定义、调用的顺序
1.1 编译器是"从上到下"读代码的
调用一个函数之前,编译器必须先"认识"它。认识的方式有两种:
- 见过完整的定义(函数体写在了调用之前)
- 见过函数原型声明(告诉编译器有这个函数,定义在后面)
1.2 基本模式
c
#include <stdio.h>
// ① 函数原型声明(完整原型!)
int add(int a, int b);
// ② main 中调用
int main(void)
{
int result = add(3, 5); // 编译器已经认识 add 了
printf("%d\n", result);
return 0;
}
// ③ 函数定义(实现)
int add(int a, int b)
{
return a + b;
}
如果定义写在调用之前,声明可以省略:
c
// add 的定义在 main 之前,main 已经认识它了
int add(int a, int b)
{
return a + b;
}
int main(void)
{
int result = add(3, 5); // 可以直接调用
return 0;
}
二、声明必须用完整的函数原型
2.1 什么是完整原型
c
// ❌ 不完整声明------C89 遗留风格,千万别用
int add(); // 没写参数,编译器不检查类型
void do_something(); // 同上
// ✅ 完整函数原型------C99 后必须这么写
int add(int a, int b); // 参数类型、返回值类型,明明白白
double divide(int, int); // 参数名可省,但类型必须有
2.2 不写完整原型的三个坑
坑一:参数个数不检查,多传少传都不报错
c
double divide(); // 不完整声明
int main()
{
divide(10, 3, 5); // 多传了一个参数!编译器沉默
return 0;
}
// 能编译通过,运行时才出问题
坑二:类型不匹配导致隐式转换出错
c
void print_value(); // 不完整声明
int main()
{
print_value(3.14); // 传了 double
return 0;
}
void print_value(int x) // 实际接收 int
{
printf("%d\n", x); // double 的二进制被当 int 解读,输出乱码
}
坑三:返回值默认为 int 的远古陷阱
c
do_something(); // 没写声明,C89 默认返回 int
char *do_something()
{
return "hello"; // 返回指针(8 字节)
}
// 编译器按 int(4 字节)接收返回值,高位截断,崩溃
2.3 规范总结
c
// ✅ 永远这样写
int process(const char *input, int flags, double *output);
// ^^^^^^^^^^^^^^^ ^^^^^^^^^ ^^^^^^^^^^^^^^
// 类型+参数名,一目了然
函数原型就是你和编译器的契约。白纸黑字写清楚,谁也别坑谁。
三、工程实践:头文件与源文件分离
当项目变大,代码必须分文件管理。这是 C/C++ 工程化的第一步。
3.1 标准项目结构
project/
├── main.c ← 主程序(调用方)
├── math_utils.h ← 头文件(声明,给外部看的接口)
└── math_utils.c ← 源文件(定义,内部实现)
3.2 math_utils.h ------ 头文件(只放声明)
c
#ifndef MATH_UTILS_H // 防止重复包含
#define MATH_UTILS_H
// 函数声明(完整原型)
int add(int a, int b);
int subtract(int a, int b);
double divide(int a, int b);
// 宏定义、常量也可以放这里
#define PI 3.14159
#endif // MATH_UTILS_H
3.3 math_utils.c ------ 源文件(放实现)
c
#include "math_utils.h" // 包含自己的头文件,检查声明定义是否一致
int add(int a, int b)
{
return a + b;
}
int subtract(int a, int b)
{
return a - b;
}
double divide(int a, int b)
{
if (b == 0) return 0.0;
return (double)a / b;
}
3.4 main.c ------ 调用方
c
#include <stdio.h>
#include "math_utils.h" // 只包含头文件就能用
int main(void)
{
printf("3 + 5 = %d\n", add(3, 5));
printf("10 / 3 = %.2f\n", divide(10, 3));
return 0;
}
编译命令:
bash
gcc main.c math_utils.c -o program
四、头文件的几个重要规范
4.1 防止重复包含(Include Guard)
c
// ✅ 传统写法
#ifndef MATH_UTILS_H
#define MATH_UTILS_H
// ... 内容 ...
#endif
// ✅ C++ 中也可以用(但有些编译器可能不支持)
#pragma once
4.2 用什么括号 include
c
#include <stdio.h> // 尖括号:系统头文件
#include "math_utils.h" // 双引号:自己写的头文件
4.3 头文件里尽量不写实现
c
// ❌ 别在头文件里定义函数
// math_utils.h
int add(int a, int b) {
return a + b; // 多个 .c 文件 include 会报重复定义
}
// ✅ 头文件只声明
int add(int a, int b);
// 定义放 .c 文件
五、命名规范
5.1 主流风格
| 风格 | 示例 | 常见于 |
|---|---|---|
| 蛇形命名 | get_user_name() |
Linux 内核、GTK、多数 C 项目 |
| 小驼峰 | getUserName() |
Windows API、部分 C++ 项目 |
| 大驼峰 | GetUserName() |
C#、部分 C++ 库 |
C 语言推荐蛇形命名,C++ 因项目而异。关键是一致。
5.2 命名原则
c
// ✅ 好:小写 + 下划线,动词在前,语义清晰
int calculate_total_price(int unit_price, int quantity);
void print_error_message(const char *msg);
bool is_valid_email(const char *email);
// ❌ 差:没有意义
int f(int a, int b);
void pem(const char *m); // 缩写莫名其妙
int a; // 单字母变量(循环里除外)
5.3 常用动词前缀
c
get_xxx() // 获取
set_xxx() // 设置
init_xxx() // 初始化
parse_xxx() // 解析
is_xxx() // 判断,返回 bool
has_xxx() // 拥有
create_xxx() // 创建
destroy_xxx() // 销毁
六、参数规范
6.1 参数顺序:输入 → 输出
c
// ✅ 输入参数在前,输出参数在后
void copy_string(const char *src, char *dest);
int read_file(const char *path, char *buffer, size_t *bytes_read);
6.2 不该改的参数加 const
c
// ✅ 明确承诺不修改 str
int count_vowels(const char *str);
// 调用者可以放心传只读数据
const char *message = "Hello";
int n = count_vowels(message); // 安全
// ❌ 不加 const,调用者得担心函数会不会改数据
int count_vowels(char *str);
6.3 参数不宜过多
c
// ❌ 超过 3-4 个参数,难读易错
void draw_rect(int x, int y, int w, int h, int color, int thickness, int style);
// ✅ 封装成结构体
typedef struct {
int x, y, w, h;
int color;
int thickness;
int style;
} RectConfig;
void draw_rect(const RectConfig *cfg);
七、返回值规范
7.1 用返回值表示成败
c
// ✅ 返回 0 表示成功,非 0 表示错误码
int read_file(const char *path, char *buffer);
// 调用时必须检查
if (read_file("data.txt", buf) != 0) {
fprintf(stderr, "读取失败\n");
return -1;
}
7.2 指针类型返回 NULL 表示失败
c
FILE *open_log(const char *name);
FILE *fp = open_log("log.txt");
if (fp == NULL) {
perror("打开失败");
exit(1);
}
7.3 绝对不要返回局部变量的地址
c
// ❌ 致命错误
int *create_array(void)
{
int arr[10]; // arr 是局部变量
return arr; // 函数返回后 arr 的内存已释放!
}
// ✅ 用 malloc 分配堆内存
int *create_array(void)
{
int *arr = malloc(10 * sizeof(int));
return arr;
}
// 调用方用完记得 free
八、一个完整的中型项目示例
calculator/
├── main.c ← 程序入口
├── calc.h ← 计算逻辑声明
├── calc.c ← 计算逻辑实现
├── io.h ← 输入输出声明
└── io.c ← 输入输出实现
calc.h
c
#ifndef CALC_H
#define CALC_H
int add(int a, int b);
int subtract(int a, int b);
int multiply(int a, int b);
double divide(int a, int b);
#endif
calc.c
c
#include "calc.h"
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }
int multiply(int a, int b) { return a * b; }
double divide(int a, int b)
{
return (b != 0) ? (double)a / b : 0.0;
}
io.h
c
#ifndef IO_H
#define IO_H
void print_menu(void);
int get_choice(void);
void get_operands(int *a, int *b);
#endif
io.c
c
#include <stdio.h>
#include "io.h"
void print_menu(void)
{
printf("======== 计算器 ========\n");
printf("1. 加法 2. 减法\n");
printf("3. 乘法 4. 除法\n");
printf("5. 退出\n");
printf("请选择: ");
}
int get_choice(void)
{
int choice;
scanf("%d", &choice);
return choice;
}
void get_operands(int *a, int *b)
{
printf("输入两个数: ");
scanf("%d %d", a, b);
}
main.c
c
#include <stdio.h>
#include "calc.h"
#include "io.h"
int main(void)
{
int choice, a, b;
while (1) {
print_menu();
choice = get_choice();
if (choice == 5) break;
get_operands(&a, &b);
switch (choice) {
case 1: printf("结果: %d\n", add(a, b)); break;
case 2: printf("结果: %d\n", subtract(a, b)); break;
case 3: printf("结果: %d\n", multiply(a, b)); break;
case 4: printf("结果: %.2f\n", divide(a, b)); break;
default: printf("无效选择\n");
}
}
printf("再见!\n");
return 0;
}
九、规范速查表
| 要素 | 规范 |
|---|---|
| 声明 | 完整函数原型,放头文件(.h) |
| 定义 | 放源文件(.c),#include 自己的 .h |
| 命名 | C: 小写+下划线+动词开头;C++: 因团队风格统一 |
| 参数 | 输入→输出;不改的加 const;超 4 个考虑结构体 |
| 返回值 | 整数表错误码;指针 NULL 表失败;绝不返局部变量地址 |
| include | 系统用 <>,自己的用 "",头文件必须防重复包含 |
核心一句话:头文件给接口(声明),源文件给实现(定义),调用者只需看头文件,声明必须用完整原型。