C/C++ 自定义函数的常用规范:从入门到工程实践

一、核心铁律:声明、定义、调用的顺序

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 系统用 <>,自己的用 "",头文件必须防重复包含

核心一句话:头文件给接口(声明),源文件给实现(定义),调用者只需看头文件,声明必须用完整原型。

相关推荐
发疯幼稚鬼1 小时前
二叉树的广度优先遍历
c语言·数据结构·算法·宽度优先
谭欣辰1 小时前
C++ DFS 与 BFS 剪枝方法详解
c++·算法·剪枝
c++之路1 小时前
C++ 预处理器
开发语言·c++
CN-Dust1 小时前
【C++专题】格式化输出与输入
开发语言·c++·算法
Titan20241 小时前
C++位图学习笔记
c++·笔记·学习
6Hzlia1 小时前
【Hot 100 刷题计划】 LeetCode 148. 排序链表 | C++ 归并排序自顶向下
c++·leetcode·链表
是个西兰花2 小时前
C++:异常
开发语言·c++·异常
cpp_25012 小时前
P1873 [COCI 2011/2012 #5] EKO / 砍树
数据结构·c++·算法·题解·二分答案·洛谷·csp
AbandonForce2 小时前
Map类:pair键值对|map的基本操作|operator[]
开发语言·c++·算法·leetcode