HOWTO · C++
C++ 中的类类型重定义
本文讨论如何解决 C++ 中的类类型重新定义。
本页内容
C++ 类定义会确定类型名称、内存布局和成员声明。在同一作用域中再次定义该类是不符合语言规则的,因此编译器会报告类重定义错误。修复方法是找出重复定义并删除或合并它。继承和组合可以为不同的类型添加行为,但都不能让原类被定义两次。
理解 C++ 类重定义错误
类定义会引入一种类型。在同一作用域中,即使两个定义包含完全相同的成员,也不能把同一个类型名称再次定义为类。例如,在同一个翻译单元中两次定义 class DelftStack { };,会产生类似 redefinition of 'class DelftStack' 的诊断信息。具体措辞和源码位置会因编译器而异。
这与前向声明不同,例如 class DelftStack; 只声明类型,不定义其成员。之后可以再提供完整定义。这里的错误针对重复定义,并非每次重复提及类名都会出错。
查找重复定义,包括被包含的头文件
先根据诊断信息中的类名,检查报告的行号以及编译器指出的较早位置。在当前源文件中搜索其他定义,然后检查其中的 #include 指令:某个头文件可能定义了同一个类,也可能是两个被包含的头文件分别定义了同名类。
如果头文件没有使用头文件保护宏或 #pragma once,在同一个翻译单元中多次包含它可能会重复处理其中的内容。传统头文件保护宏是可移植的预处理器技术;#pragma once 是编译器普遍支持的指令,但不属于 ISO C++ 标准。这两种方法都能防止同一个头文件被重复包含,但都无法解决两个不同头文件各自定义了同一个类的问题。还要检查源文件是否既包含了定义该类的头文件,又重复写了类的主体。
通过保留一个定义来修复错误
选定一个权威定义,并删除意外产生的副本。如果两个副本都包含有用的成员,就把这些成员合并到同一个定义中,并按需更新调用代码。如果定义由头文件负责,就包含该头文件,不要再把类主体复制到源文件中。添加传统头文件保护宏,或使用编译器普遍支持的 #pragma once,以防止同一个头文件被重复处理。
只有当一个类确实代表用途不同的类型时,才应重命名它。给意外产生的重复类改名,可能会掩盖底层的设计或包含问题,并让程序留下两个相互竞争的类型。更改名称前,应先确认程序确实需要这两种类型,并一致地更新所有用法。
使用继承扩展现有类
当新类型确实是基类的子类型,并且应能通过基类的公共接口使用时,可以采用公有继承。派生类可以添加成员和函数,但不会重新定义基类。使用公有继承时,基类的公有成员仍为公有成员,受保护成员仍可供派生类代码访问。派生类不能直接访问基类的私有成员。私有继承和受保护继承会改变基类成员对派生类使用者的可见方式。
使用 C++ 编译器运行下面这个独立示例:
本节中的代码以及显示的标签或输出仍为英文。
#include <iostream>
class Greeter {
public:
void greet() const { std::cout << "Hello from DelftStack!\n"; }
};
class FriendlyGreeter : public Greeter {
public:
void greet_twice() const {
greet();
greet();
}
};
int main() {
FriendlyGreeter greeter;
greeter.greet_twice();
}
Hello from DelftStack!
Hello from DelftStack!
该程序会输出两行问候语。FriendlyGreeter 是一种专门的问候器,并复用了从基类继承的公有函数。如果这种子类型关系并不成立,继承可能会让接口产生误导。
通过组合添加行为
组合表达“拥有一个”的关系:一个类存储另一个类的对象,并把工作委托给它。这样,所包含类型的接口保持独立,而包含它的类可以自行选择要公开哪些操作。在这里,Report 存储一个 Formatter 成员,并将渲染工作委托给该成员;报告拥有格式化器,但报告本身不是格式化器。
本节中的代码以及显示的标签或输出仍为英文。
#include <string>
class Formatter {
public:
std::string format(const std::string& text) const {
return "Report: " + text;
}
};
class Report {
public:
std::string render(const std::string& text) const {
return formatter_.format(text);
}
private:
Formatter formatter_;
};
int main() {
Report report;
return report.render("Summary") == "Report: Summary" ? 0 : 1;
}
组合也使用彼此独立的类定义。如果成员对象所属的类定义在头文件中,就包含该头文件,并且只定义一次包含它的类。添加成员对象无法解决同一作用域其他位置存在的重复定义。
根据改动选择继承或组合
如果新类型应遵守基类型的契约,并且在任何需要基类型的地方都能安全地使用,就选择继承。如果新类型拥有或使用另一个对象作为组件,或者只应公开该对象部分行为,就选择组合。组合通常能让依赖关系和公有接口保持更独立;当确实需要可替代性时,继承很有用。
这两种方式都不能修复意外的重复类定义。应先为每种类型建立一个有效定义,再根据所需的设计关系决定是否使用继承或组合。
使用编译器验证修复
删除或合并重复定义后,编译受影响的翻译单元以及相关项目目标。对于名为 main.cpp 的单个源文件,常见命令是 g++ -std=c++17 main.cpp -o app;然后运行生成的程序:./app。上面的继承示例已经编译并运行,退出状态为 0,并输出了前面展示的两行问候语。重复定义诊断信息仅用于示意;所提供的执行证据针对继承示例,并未运行诊断示例。
如果诊断信息仍然存在,请检查编译器报告的两个位置,并再次追查被包含的头文件。如果重复定义只在某种构建配置下出现,请比较该配置的包含路径和预处理器定义。分享诊断信息时,应保留可选择的文本,因为措辞和行号取决于编译器及构建方式。