C++ 把函数声明、类接口写在头文件(.h / .hpp)里,把函数体写在对应的源文件(.cpp)里,再分别编译、链接成可执行程序。其他 .cpp 通过 #include 包含头文件,就能看到这些声明并调用函数。同一头文件可能被包含多次,因此要用 include guard 防止重复展开;非内联函数的定义在整个程序里只能出现一次。本节说明这种多文件组织,以及它与函数声明、定义的对应关系。

math_utils.hpp#include#includemain.cppmath_utils.cpp编译编译main.omath_utils.o链接可执行文件

main.cpp 与 math_utils.cpp 都包含同一头文件,各自编译后再链接成可执行文件。

简单拆分示例

头文件 math_utils.hpp:

#ifndef MATH_UTILS_HPP
#define MATH_UTILS_HPP

int square(int x);

#endif

源文件 math_utils.cpp:

#include "math_utils.hpp"

int square(int x) {
    return x * x;
}

主程序:

#include <iostream>
#include "math_utils.hpp"

int main() {
    std::cout << square(6) << std::endl;
    return 0;
}

在自己电脑上,要把 main.cpp 和 math_utils.cpp 一起交给编译器,例如:

g++ -std=c++17 main.cpp math_utils.cpp -o app

这样声明、定义和调用才会链成一个程序。本站在线编译一次只处理一份源码,不能同时提交这两个文件,所以下面把声明和定义写进同一个文件,用来验证 square(6) 的结果:

C++ 声明与定义(单文件)运行
#include <iostream>

int square(int x);

int main() {
    std::cout << square(6) << std::endl;
    return 0;
}

int square(int x) {
    return x * x;
}

因为 6×6 等于 36,所以打印 36。把声明放到 math_utils.hpp、定义放到 math_utils.cpp 之后,用上面的 g++ 命令在本地编译,输出仍应是 36。单文件写法只是为了在网页上能直接运行,和拆成多文件后的计算结果相同。

include guard 与 #pragma once

#ifndef / #define / #endif 构成 include guard,保证同一头文件在同一翻译单元只展开一次,避免重复声明导致的编译错误。许多编译器支持 #pragma once,效果相近;工程中选一种风格并统一。

声明与定义应放何处

位置典型内容
头文件函数声明、类定义、inline 函数、模板定义(模板常需放头文件)
源文件非内联函数定义、少量全局变量定义

若把普通函数的函数体、或全局变量的定义写进头文件(而不是只写声明),两个 .cpp 一旦都包含这个头,各自就会带上一份同样的定义。链接时编译器看到两份,就会报重复定义。因此非 inline 的函数和变量在整个程序里只能定义一次:函数体放在某一个 .cpp 里,头文件只保留声明。

系统头与本地头

尖括号形式 #include <iostream> 在编译器的系统目录里查找,标准库头文件走这条路径。引号形式 #include "math_utils.hpp" 通常先在当前工程目录里找,所以项目自己的头文件写成引号。习惯上先写系统头、再写项目头:若先包含项目头,里面的宏可能改掉随后系统头里用到的名字。

内联与 constexpr 函数

小函数可在头文件中定义为 inline,允许多个翻译单元含同一定义,链接时合并。C++17 起 inline 变量亦有类似用途。过度在头文件中放大型实现会增加编译时间;仅接口放头文件、实现放 .cpp 是常见分工。

前置声明

若头文件只需指针或引用指向某类型,而不使用其成员,可写 class Widget; 前置声明,避免包含完整定义,减少编译依赖。一旦需要对象大小、继承或调用成员,必须包含定义该类型的头文件。

常见错误

  • 头文件缺少 guard:间接重复包含同一头两次导致重声明。
  • 在头文件中定义全局变量:多源文件包含后链接冲突;若需共享,使用 extern 声明于头文件、定义于单一 .cpp。
  • 声明与定义签名不一致:链接失败或未定义行为,须保证参数类型、const、命名空间完全一致。

本节说明了头文件 guard、多文件编译流程、前置声明与 ODR 要点。下一章进入数组:内置数组的声明、初始化与边界意识。