文章

C++ODR

ODR(One Definition Rule)要求每个变量、函数在程序中只能有一个定义,避免链接错误。

C++ODR

C++ODR

ODR = One Definition Rule,字面意思是:

“程序中每个实体(如变量、函数、类、模板)在整个程序中必须只有一个定义。”

ODR 适用的内容

  1. 变量(全局、类静态)
  2. 函数(包括普通函数和成员函数)
  3. 类、结构体、联合体、枚举
  4. 模板(类模板和函数模板)
  5. 类型别名(typedef / using)

ODR 的基本规则

1. 单个翻译单元中(一个 .cpp 文件加上它 #include 的头)

  • 只能有一次定义。重复定义会在编译阶段报错。

2. 多个翻译单元中(多个 .cpp 文件)

  • 可以多次定义,但前提是:
    • 它们内容必须完全相同(逐字节一致!)
    • 必须是 允许多定义的实体:如 inline 函数、模板、inline static 成员变量等
    • 编译器和链接器要能把它们“合并成一个”定义

举例

ODR 合法示例:inline 函数

1
2
3
4
5
// foo.h
inline void sayHello() {
    std::cout << "Hello\n";
}
// a.cpp + b.cpp 都 include foo.h

这不会报错,因为:

  • inline 函数可以在多个翻译单元中重复定义
  • 链接器会合并成一个定义

ODR 违规示例:多个定义但不 inline

1
2
// foo.h
void sayHello() { std::cout << "Hello\n"; } // 没有 inline ❌
1
// a.cpp 和 b.cpp 都 include foo.h

会导致链接错误:

1
multiple definition of `sayHello()`

全局变量放在头文件中定义

1
2
// foo.h
int g_counter = 0; // ❌ 多个 .cpp 引用会造成重复定义
1
2
3
// 推荐改为
extern int g_counter; // foo.h
// foo.cpp 中定义:int g_counter = 0;

模板类成员函数定义不一致

1
2
3
4
5
6
// foo.h
template <typename T>
class A {
public:
    void f() { std::cout << "A\n"; } // 若 a.cpp 和 b.cpp 中不一样,就违反 ODR
};

所有模板的定义都要在 header 中完全一致。否则多个 TU 中的版本不同,会违反 ODR。

ODR 相关的特殊关键字

关键字用途
inline多 TU 中定义合并
constexpr编译期常量,允许多定义
extern声明不定义
static限制作用域在当前 TU
inline staticC++17 起类静态成员类内定义合法
constinitC++20,防止未初始化,强制初始化

常见建议:

所有变量定义放在 .cpp,声明用 extern 放头文件。

模板定义全部放在 .h 文件中。

如果函数会在多个 .cpp 中使用,加上 inline

#pragma once 或 include guard 防止重复包含。

类静态变量推荐用 inline static(C++17 起)。

C++声明和定义为何要分开

这是 C++ 设计中的一个非常重要原则,声明(declaration)和定义(definition)分开的原因,主要是为了支持多文件编程、编译依赖管理和链接过程

什么是声明和定义

  • 声明(declaration):告诉编译器“这个东西存在”,但不分配内存或生成代码。例如:

    1
    2
    
    extern int x;       // 声明,告诉编译器变量 x 存在
    void foo();         // 函数声明,告诉编译器有个函数 foo
    
  • 定义(definition):告诉编译器“这个东西具体是什么”,并分配内存或生成代码。例如:

    1
    2
    
    int x = 10;         // 定义,同时分配空间并初始化
    void foo() { /*...*/ }  // 函数定义,包含函数体
    

为什么要分开

支持多文件编译
  • 一个大型项目通常分成多个 .cpp 文件(翻译单元,TU),每个文件独立编译。
  • 声明放在头文件(.h),让多个 .cpp 文件都能知道函数、变量的接口。
  • 定义放在某个 .cpp 文件,避免同一实体重复定义。

如果声明和定义写在一起,且放在头文件,多个 .cpp 文件包含该头文件时,就会重复定义,导致链接错误。

避免重复定义冲突
  • 例如全局变量,如果头文件既声明又定义:

    1
    2
    
    // bad.h
    int counter = 0;  // 定义
    
  • 多个 .cpp 包含 bad.h,就会在链接阶段报错:

    1
    
    multiple definition of `counter`
    
  • 改为声明定义分离:

    1
    2
    3
    4
    
    // good.h
    extern int counter;  // 仅声明
    // good.cpp
    int counter = 0;     // 定义一次
    
  • 这样链接器只会看到一个 counter 定义。

加快编译速度和模块化设计
  • 头文件只写声明,减少头文件内容,避免每次包含时编译大量代码。
  • 代码实现放在 .cpp,只需单独编译改动的文件,提高效率。
符合 C++ 的链接模型和语言规则
  • C++ 编译分为编译(单个翻译单元)和链接(多个翻译单元合并)。
  • 声明告诉编译器符号存在,使编译通过。
  • 定义告诉链接器符号实现在哪里,负责合并和分配资源。
本文由作者按照 CC BY 4.0 进行授权