C++中的ODR全称为One Definition Rule,也就是定义一次规则,是C++标准中明确规定的核心规则之一。它的核心要求是,同一个实体(包括变量、函数、类、模板等)在整个程序的所有编译单元中,只能有一个唯一定义,否则在链接阶段就会出现重复定义的错误。

ODR的具体要求
ODR的适用范围覆盖C++中的大部分实体,不同实体的要求略有区别:
- 对于非内联的全局函数、全局变量,在整个程序中只能有一个定义,多个编译单元中定义相同名称的全局函数或变量就会触发错误。
- 对于类类型,同一个类的所有定义必须在词法上完全一致,否则属于未定义行为。
- 对于模板,同一个模板的定义在不同编译单元中也需要保持完全一致,否则同样不符合ODR要求。
常见的重复定义错误场景
1. 头文件中定义全局变量
很多开发者会在头文件中直接定义全局变量,然后在多个源文件中包含这个头文件,此时每个包含该头文件的编译单元都会有一个该变量的定义,链接时就会报错。
比如下面的头文件config.h:
// config.h int global_count = 0; // 直接定义全局变量,错误做法
如果a.cpp和b.cpp都包含config.h,编译后链接时就会出现multiple definition of 'global_count'的错误。
2. 头文件中定义非内联函数
非内联的普通函数如果在头文件中定义,被多个源文件包含后也会出现重复定义。比如:
// utils.h
void print_hello() { // 非内联函数直接定义在头文件,错误做法
printf("hellon");
}
3. 多个源文件中定义同名实体
即使没有包含头文件,如果两个不同的源文件中定义了同名的全局变量或函数,链接时也会触发重复定义错误。
避免重复定义错误的方法
1. 全局变量使用声明+单一定义
头文件中只声明全局变量,在一个源文件中完成定义,其他文件通过extern关键字引用。示例如下:
// config.h 头文件,只做声明
extern int global_count;
// config.cpp 源文件,做唯一的定义
int global_count = 0;
// a.cpp 其他源文件引用
#include "config.h"
void func_a() {
global_count++;
}
2. 函数定义放在源文件中,头文件只放声明
非内联的函数定义统一放在源文件中,头文件中只保留函数声明,避免多个编译单元出现重复定义。
// utils.h 头文件,只放声明
void print_hello();
// utils.cpp 源文件,放函数定义
#include <stdio.h>
void print_hello() {
printf("hellon");
}
3. 需要使用内联的场景加inline关键字
如果确实需要在头文件中定义短小的函数,可以添加inline关键字,内联函数允许在多个编译单元中有相同的定义,符合ODR要求。
// utils.h
inline int add(int a, int b) { // 内联函数,允许在头文件定义
return a + b;
}
4. 类的定义放在头文件,成员函数定义区分场景
类的定义本身可以放在头文件中,因为ODR允许类的多个定义完全一致。类的成员函数如果在类内定义,默认是内联的,没问题;如果在类外定义,非内联的成员函数要放在源文件中,内联的成员函数可以放在头文件中。
// user.h
class User {
public:
int get_id() const { // 类内定义,默认内联,允许放在头文件
return id;
}
void set_id(int new_id); // 类外非内联声明
private:
int id;
};
// user.cpp
#include "user.h"
void User::set_id(int new_id) { // 非内联成员函数定义放在源文件
id = new_id;
}
5. 模板定义放在头文件中
模板的定义通常需要放在头文件中,因为编译器需要在实例化模板时看到完整的定义,只要保证模板在所有编译单元中的定义完全一致,就符合ODR要求。
// array_utils.h
template <typename T>
T get_first(T* arr) { // 模板定义放在头文件,符合要求
return arr[0];
}
总结
ODR是C++中避免实体重复定义的核心规则,理解它的要求能够帮助开发者规避大部分链接阶段的重复定义错误。实际开发中只要遵循头文件只放声明、全局变量单一定义、需要头文件定义的函数加inline、模板定义放头文件且保持一致这些原则,就能有效避免相关问题,提升代码的规范性和稳定性。