文章

函数声明与函数定义

函数声明告诉编译器函数名和参数,函数定义提供具体实现,分开便于代码组织和复用。

函数声明与函数定义

函数声明与函数定义

概念

函数声明

告诉编译器函数的名称、返回类型、参数类型及(可选的)参数名,以便在调用处进行类型检查、布局调用约定等。通常放在头文件(.h)或源文件顶部。

1
返回类型 函数名(参数类型1 参数名1, 参数类型2 参数名2, );
1
2
3
// math_utils.h
int add(int a, int b);
double power(double base, int exp);

函数定义

给出函数的具体实现,包括函数体(大括号 {} 内的语句)。通常放在源文件(.cpp)中;如果函数体很短,也可以在头文件中(常用于 inline 函数)。

1
2
3
返回类型 函数名(参数列表) {
    // 函数体:具体执行语句
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// math_utils.cpp
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

double power(double base, int exp) {
    double result = 1.0;
    for (int i = 0; i < exp; ++i) {
        result *= base;
    }
    return result;
}

区别和联系

 函数声明函数定义
是否包含实现否,只有函数签名是,包含完整的函数体
编译器作用提供接口信息,检查调用合法性生成可执行代码
出现次数同一个函数可声明多次,但通常不超过几次每个函数只能定义一次(否则链接错误)
放置位置头文件(或源文件顶部)源文件(或头文件内的 inline 定义)

为什么要有函数声明

允许先调用后定义

C++ 是编译型语言,编译器从上到下顺序处理代码。如果在函数定义之前就使用了它(即先调用后定义),没有提前声明会导致编译错误

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>

int main() {
    int result = add(3, 4); // 错误:此时编译器不知道 add 是什么
    std::cout << result << std::endl;
    return 0;
}

int add(int a, int b) {
    return a + b;
}

正确写法(有函数声明):

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

int add(int a, int b); // 函数声明

int main() {
    int result = add(3, 4); // 编译器已经知道 add 的签名
    std::cout << result << std::endl;
    return 0;
}

int add(int a, int b) {
    return a + b;
}

支持多文件编程(模块化开发)

当把函数定义放在一个 .cpp 文件中,另一个文件想要用这个函数,就需要通过声明(通常放在 .h 文件中)来共享接口。

示例结构:

1
2
3
math_utils.h       // 函数声明
math_utils.cpp     // 函数定义
main.cpp           // 函数调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// math_utils.h
#ifndef MATH_UTILS_H
#define MATH_UTILS_H

int add(int a, int b);

#endif

// math_utils.cpp
#include "math_utils.h"

int add(int a, int b) {
    return a + b;
}

// main.cpp
#include <iostream>
#include "math_utils.h"

int main() {
    std::cout << add(1, 2) << std::endl;
    return 0;
}

实现了接口与实现分离

头文件中的函数声明可以被别人引用,而实现隐藏在 .cpp 中,有利于:

  • 信息封装
  • 编译依赖减小
  • 团队协作更清晰

支持递归函数或相互调用函数

比如函数 A 调用 B,B 又调用 A,这时就必须有声明:

1
2
3
4
5
6
7
8
9
void A(); // 声明 A

void B() {
    A(); // 可以调用 A,因为已经声明
}

void A() {
    B();
}

为什么导个头文件就能使用到定义

#include "math_utils.h" 看起来像是引入了 .cpp 文件里的函数,但实际上,它并不是直接“使用 cpp 文件”,而是通过函数声明 + 链接过程来实现跨文件调用的。这是 C++ 编译机制的核心之一。

C++ 编译的本质:“编译 + 链接” 两阶段

C++ 编译流程通常如下:

1
2
3
每个 .cpp 文件 --> 独立编译成 .obj / .o 文件(目标文件)
       
        所有目标文件和库 --> 链接器(Linker)组合为最终可执行文件

头文件(.h)的作用:提供声明(接口)

当在 main.cpp 里写:

1
#include "math_utils.h"

引入了函数的声明,告诉编译器:

“有一个叫 add(int, int) 的函数,返回 int,你先相信我,它确实存在。”

这允许编译器通过类型检查并成功生成目标文件 main.o

.cpp 文件的作用:提供定义(实现)

然后 math_utils.cpp 中真正写了这个函数:

1
2
3
int add(int a, int b) {
    return a + b;
}

这段代码单独被编译成 math_utils.o,包含了 add() 函数的机器码实现。

链接阶段:把引用和实现对上号

最后,链接器(linker)将:

  • main.o 中对 add()未解析引用
  • math_utils.oadd()实现

匹配起来,完成程序的拼装。

小结

角色作用举例
math_utils.h说:“add 这个函数存在,参数是啥”提供声明给 main.cpp
math_utils.cpp真正定义了这个函数是怎么做加法的提供函数体给链接器找定义
#include只是复制头文件的内容进当前文件相当于把声明粘贴进来了
编译器负责翻译语法,生成目标代码.cpp 文件编成 .o 文件
链接器把“你调用了什么”对上“我实现了什么”组合目标文件生成程序

为什么不直接导入 .cpp 源文件

也可以这样做:

1
#include "math_utils.cpp"  // 不推荐

但这是极不推荐的做法:

  • 会造成多重定义(多个 .cpp 都包含的话)
  • 无法实现编译单元分离(每个 .cpp 独立编译失效)
  • 会降低编译效率模块化结构

#include 用于头文件(声明),.cpp 文件应独立编译,不被直接包含。

为什么不能把函数定义写在头文件里

示例( 错误用法):

1
2
3
4
// math_utils.h
int add(int a, int b) {
    return a + b;
}

如果这样写,并在多个 .cpp 文件里都 #include "math_utils.h",会得到 “multiple definition of add” 链接错误

什么时候可以在头文件里写函数定义

使用 inline(内联函数)

1
2
3
4
// math_utils.h
inline int add(int a, int b) {
    return a + b;
}

inline 表示编译器可以将函数体复制到调用点,避免函数调用开销,并允许多个 .cpp 中包含而不报错。适合写短小函数(getter/setter、工具函数)。

使用 static(内部链接)

1
2
3
4
// math_utils.h
static int add(int a, int b) {
    return a + b;
}

static 表示这个函数对当前编译单元(.cpp 文件)私有,不参与链接。这样每个 .cpp 文件都有一个自己的 add() 副本,不冲突。但一般不推荐滥用。

本文由作者按照 CC BY 4.0 进行授权