在 C++ 中,重新定义继承而来的 non-virtual 函数(非虚函数)通常是不推荐的,特别是在遵循《Effective C++》条款 36 的建议时。以下是条款 36 的核心内容和原因解释:
条款 36:绝不重新定义继承而来的 non-virtual 函数
1. non-virtual 函数的定义和行为
non-virtual 函数是在基类中定义的普通成员函数,不带有 virtual
关键字。在子类中定义同名同参数的函数时,实际上是隐藏(隐藏是指基类函数名被隐藏,无法在子类作用域中直接访问,但可以通过作用域解析操作符调用)了基类的函数,而不是覆盖它。这意味着通过基类指针或引用调用该函数时,调用的是基类版本,而不是子类版本。
2. 为何不重新定义 non-virtual 函数
重新定义继承而来的 non-virtual 函数可能会导致以下问题:
- 函数调用行为的困惑: 由于 non-virtual 函数的多态性不适用于子类,使用基类指针或引用调用函数时,总是调用基类版本的函数。这与虚函数的行为不同,容易让开发者混淆和误解。
- 隐藏问题: 如果子类中定义了与基类同名的 non-virtual 函数,它会隐藏基类中的所有同名函数,无论这些函数的参数列表是否相同。这可能导致基类中的函数无法在子类中被正常访问,造成潜在的问题。
- 可维护性和可读性: 重定义 non-virtual 函数会导致代码可读性和维护性变差,因为非专业的行为和意图不容易被理解。尤其在大型代码库中,随着时间推移和人员更替,代码的可维护性会受到严重影响。
3. 推荐的做法
- 保持 non-virtual 函数的独特性: 继承的 non-virtual 函数不应被重定义。如果需要修改函数的行为,应当使用虚函数机制。
- 使用虚函数实现多态: 如果基类中的某个函数可能在派生类中需要不同实现,应当将其声明为虚函数。虚函数使得调用能够根据对象的实际类型(而非指针或引用的静态类型)选择适当的函数版本。
4. 总结
避免重新定义继承而来的 non-virtual 函数的原则有助于保持代码行为的一致性和可预测性,减少混乱和错误的可能性。虚函数的使用提供了一种明确而安全的多态机制,使代码更易于理解和维护。
遵循这些原则可以帮助开发者编写更健壮和可维护的 C++ 代码。
5.补充(动态绑定和静态绑定)
只有虚函数才会被动态绑定, 非虚函数是静态绑定
动态绑定与静态绑定
1. 静态绑定(Static Binding)
静态绑定发生在编译时,函数的调用与函数的定义在编译时就已经确定。非虚函数通常是静态绑定的。这意味着,通过对象或指针调用非虚函数时,编译器会在编译时决定调用哪个函数版本。无论实际对象的类型是什么,调用的始终是指针或引用的静态类型所指向的类中的那个非虚函数。
示例:
cpp
复制代码
class Base {
public:
void nonVirtualFunction() {
std::cout << "Base::nonVirtualFunction" << std::endl;
}
};
class Derived : public Base {
public:
void nonVirtualFunction() {
std::cout << "Derived::nonVirtualFunction" << std::endl;
}
};
Base* b = new Derived();
b->nonVirtualFunction(); // 输出 "Base::nonVirtualFunction"
在上面的例子中,无论 b
实际上指向 Derived
对象,nonVirtualFunction
的调用都会静态绑定到 Base
类中的实现。
2. 动态绑定(Dynamic Binding)
动态绑定发生在运行时,它是通过虚函数实现的。当基类中声明函数为 virtual
时,在子类中可以覆盖它。调用该函数时,系统会根据实际对象的类型在运行时选择相应的函数版本,即使调用是通过基类指针或引用进行的。
示例:
cpp
复制代码
class Base {
public:
virtual void virtualFunction() {
std::cout << "Base::virtualFunction" << std::endl;
}
};
class Derived : public Base {
public:
void virtualFunction() override {
std::cout << "Derived::virtualFunction" << std::endl;
}
};
Base* b = new Derived();
b->virtualFunction(); // 输出 "Derived::virtualFunction"
在这个例子中,由于 virtualFunction
是虚函数,调用 b->virtualFunction()
时会动态绑定到 Derived
类中的实现,即使 b
的静态类型是 Base*
。
总结
- 静态绑定:编译时确定函数调用,与对象的实际类型无关,适用于非虚函数。
- 动态绑定:运行时确定函数调用,根据对象的实际类型选择函数版本,适用于虚函数。