基类接口契约必须显式声明为virtual,派生类需正确重写语义契约;C++不自动检查,依赖人工设计与测试驱动,关键包括虚函数声明、纯虚函数强制实现、虚析构函数、模板化契约测试及多态调用验证。

基类接口契约必须显式声明为 virtual
派生类是否遵守约定,本质是看它有没有正确重写基类的语义契约。C++ 不会自动检查这个,必须靠人设计 + 测试驱动。第一步是确保基类中所有可被重写的接口都声明为 virtual,否则派生类同名函数只是隐藏(hiding),不是重写(overriding),测试根本测不到真实行为。
常见错误现象:Base::process() 没加 virtual,派生类写了 Derived::process(),但通过基类指针调用时仍执行基类版本,测试用例看似“通过”,实则完全没验证派生逻辑。
- 纯虚函数用
= 0强制派生类实现(如virtual void validate() = 0;) - 带默认实现的虚函数要注明“此默认行为是契约一部分”,否则派生类重写后可能无意破坏语义
- 析构函数若基类有虚函数,
~Base()必须是virtual,否则delete base_ptr行为未定义,单元测试里一旦涉及对象销毁就可能崩溃
用 Google Test 写契约测试模板类
不能为每个派生类重复写一套测试逻辑,而是把基类契约抽象成模板测试:给定一个 Base*,验证它的行为是否符合预期。这样只要把不同派生类实例传进去,就能统一检验。
示例场景:基类 Shape 约定 area() 必须 > 0,且 clone() 返回的对象 area() 相等:
立即学习“C++免费学习笔记(深入)”;
template <typename T>
class ShapeContractTest : public ::testing::Test {
protected:
std::unique_ptr<Shape> instance;
void SetUp() override {
instance = std::make_unique<T>();
}
};
<p>using ShapeImplementations = ::testing::Types<Circle, Rectangle, Triangle>;
TYPED_TEST_SUITE(ShapeContractTest, ShapeImplementations);</p><p>TYPED_TEST(ShapeContractTest, area_is_positive) {
EXPECT_GT(this->instance->area(), 0.0);
}</p><p>TYPED_TEST(ShapeContractTest, clone_preserves_area) {
auto cloned = this->instance->clone();
EXPECT_DOUBLE_EQ(this->instance->area(), cloned->area());
}</p>关键点:测试不依赖具体类型名,只依赖基类接口;SetUp() 中构造具体派生类,保证多态调用真实实现。
覆盖虚函数调用路径,避免直接调用派生类方法
测试中如果写 derived.area(),就绕过了虚表机制,等于白测——你测的是派生类自己的函数,不是“作为 Base 被使用时的行为”。契约生效的前提是用户通过基类指针/引用调用。
- 永远用
Base* ptr = new Derived{...}; ptr->some_virtual();方式触发多态 - 避免在测试里
static_cast回派生类类型,那是在验证实现细节,不是契约 - 如果基类有非虚接口(如模板函数
template<typename T> void process_all(T&)),需额外测试该模板是否对派生类实例产生预期副作用
性能影响:虚函数调用有极小开销,但单元测试中可忽略;重点是确保测试路径和生产调用路径一致,否则覆盖率数字再高也没意义。
析构与资源管理契约最容易被忽略
很多基类约定隐含在生命周期里:比如“派生类析构时必须释放所有独占资源”“clone() 返回的对象必须可独立析构而不影响原对象”。这些不会在单个函数返回值里暴露,但一出错就是内存泄漏或 double-free。
实操建议:
- 在测试 fixture 的
TearDown()里用valgrind或 ASan 检查是否有泄漏(CI 中可加--gtest_filter=*Leak*单独跑) - 写一个“析构安全”测试:创建对象 → 复制一份指针 →
delete原指针 → 验证副本仍能安全调用area()(若契约允许)或至少不崩溃 - 如果基类有
std::shared_ptr<Resource>成员,契约应明确说明“派生类不得裸 delete 该资源”,测试里可尝试dynamic_cast并访问内部指针来破坏性验证
真正难的不是写测试,而是把隐含契约写进基类注释、Doxygen 和测试用例名里——比如 TEST_F(BaseContractTest, clone_returns_independent_object_with_valid_state) 这种名字,比代码还重要。


















