头文件只放声明,函数实现须在.cpp中;模板和内联函数可放头文件;用#include guards或#pragma once防重复包含;源文件只包含直接需要的头文件;C++17起用inline变量替代extern全局变量。

头文件里只放声明,别写函数实现
很多人一上来就在 .h 文件里写 void foo() { /* 一堆代码 */ },结果多个 .cpp 包含它,链接时报 multiple definition of 'foo'。这是因为每个编译单元都生成了一份定义,违反了 C++ 的 One Definition Rule(ODR)。
正确做法是:头文件只放 class 声明、extern 变量、函数声明、内联函数(inline 或定义在类内)、模板定义。
- 普通函数实现必须放在
.cpp文件里 - 如果非要写在头里,加
inline(C++17 起对constexpr函数也隐式 inline) - 模板函数/类的定义通常得全放在头文件——因为实例化发生在使用点,编译器需要看到完整定义
用 include guards 或 #pragma once 防止重复包含
没加防护的头文件被间接包含多次(比如 A.h → B.h → A.h),会导致语法重定义错误,比如 redefinition of 'struct X'。
#pragma once 简单直接,主流编译器都支持;但严格跨平台或极端旧环境(如某些嵌入式工具链)仍建议用传统 include guard:
立即学习“C++免费学习笔记(深入)”;
#ifndef MY_HEADER_H #define MY_HEADER_H <p>// 头文件内容</p><h1>endif // MY_HEADER_H
-
#pragma once不依赖宏名,不易冲突,但不是 C++ 标准特性(只是事实标准) - include guard 的宏名要唯一,推荐用路径转大写下划线格式,比如
UTILS_STRING_UTILS_H - 不要混用两种方式——既写
#pragma once又写 guard,没必要,还可能干扰某些静态分析工具
源文件里只 include 真正需要的头文件
在 main.cpp 里无脑 #include "everything.h" 看起来省事,实际会让编译变慢、隐藏依赖、增加重构风险。更糟的是,如果某个头文件改了,所有包含它的 .cpp 都得重编译。
原则是:每个 .cpp 只 #include 它自己直接用到的类型/函数声明。比如:
- 如果
widget.cpp只用了std::string,就#include <string>,别靠其他头文件“顺带”提供 - 如果
widget.h已经前向声明了class Config;,而widget.cpp里只用指针/引用,那widget.cpp就不需要#include "config.h" - 第三方库头文件尽量用
<xxx>,项目头文件用"xxx.h",方便构建系统区分系统路径和本地路径
全局变量和 inline 变量的声明/定义要分清
C++17 之前,想在头文件里定义一个全局常量,容易误写成 const int kMax = 100; ——这会在每个包含它的编译单元生成一份定义,链接失败。C++17 引入 inline 变量才真正解决这个问题。
常见写法对比:
- 旧写法(不安全):
extern const int kMax;声明在头文件,const int kMax = 100;定义在某个.cpp里 - C++17 推荐:
inline constexpr int kMax = 100;直接放在头文件,安全且高效 - 非 constexpr 变量(如
inline std::mutex g_mutex;)也适用inline,但要注意初始化时机是首次 ODR-use
没开 C++17 的项目,老老实实用 extern + 单点定义,别试图用 static 或匿名命名空间绕过——那会为每个编译单元创建独立副本。
头文件和源文件怎么组织,本质上是在平衡编译速度、链接正确性、依赖清晰度三件事。最容易被忽略的,是把“能跑通”当成“组织合理”——比如模板全塞头文件能跑,但不意味着它适合拆分或测试;比如加了 #pragma once 不报错,不代表头文件本身没有隐式耦合。









