条款32:确定public继承塑模出is-a关系
is-a和has-a是C++类的两个重要关系描述,如果类D基于public方式继承于类B,则D类的实例 is-a B类的实例,适用于Base class身上的每一件事情也一定适用于Derived class身上。
条款33:避免遮掩继承而来的名称
C++编译器在编译的时候针对类内的成员函数也会按照变量域类似的规则进行搜索:即先在本类的作用域中搜索,如果找到对应的符号,则不会继续扩大搜索域,如果没有找到则会扩大搜索域,先扩展至其父类域,依次往上扩展。考察下面的示例代码:
#include <iostream>
class B {
public:
void mf1() {}
void mf2() {}
};
class D : public B {
public:
void mf3() {}
void mf1(int a) {}
};
int main()
{
D obj;
obj.mf3(); // 编译OK,调用D::mf3()
obj.mf1(2); // 编译OK,调用D::mf1(int)
obj.mf1(); // 编译不通过
obj.mf2(); // 编译通过,调用B::mf2
return 0;
}
- 在编译obj.mf3()时,直接就命中D类作用域定义的mf3函数,并且参数与定义的一致,编译通过。
- 在编译obj.mf1(2)时,因为在D类的作用域中有mf1函数,并且参数定义一致,编译通过。
- 在编译obj.mf1()时,因为D类作用域中有mf1函数,编译器不会扩大搜索域了,然后比较参数定义发现不一致,所以编译不通过。
- 在编译obj.mf2()时,由于D类作用域中没有这个函数符号,所以编译器扩大搜索域至其父类,即B类作用域,在B类作用域中发现此函数符号,并且参数定义检查一致,所以编译通过。
结论:在子类中需要避免定义与父类中同名的函数(虚函数除外),因为那样的话,子类对象实例就不能调用父类定义的该函数了,如果一定要调用,则需要在子类中使用using语句显式的声明父类被掩盖的函数,如下所示:
#include <iostream>
class B {
public:
void mf1() {}
void mf2() {}
};
class D : public B {
public:
using B::mf1; // 使用using语句声明父类中被掩盖的函数
void mf3() {}
void mf1(int a) {}
};
int main()
{
D obj;
obj.mf3(); // 编译OK,调用D::mf3()
obj.mf1(2); // 编译OK,调用D::mf1(int)
obj.mf1(); // 现在编译可以通过了
obj.B::mf1(); // 等同于上面的写法
obj.mf2(); // 编译通过,调用B::mf2
return 0;
}
条款34:区分接口继承和实现继承
- 纯虚函数意味着接口继承,子类如果要实例化,则必须要实现父类中定义的纯虚函数。
- 非纯虚函数意味着接口继承和继承一份默认的实现,该默认的实现在父类中给出(非纯虚函数必须在父类函数中给出一份默认实现),子类可以给出自己的实现以覆盖默认实现。
- 非虚函数意味着接口继承以及强制性实现继承,一般来说,如果在父类中定义了一个非虚函数,则子类中一般无需重新定义。
条款35:考虑virtual函数以外的其它选择
- 选择1:NVI(Non-virtual Interface)手法,采用非虚函数调用虚函数的方式,如下:
#include <iostream>
class B {
public:
void FuncWrapper() {std::cout << "Entering B::FooWrapper" << std::endl; Foo();}
private:
virtual void Foo() {std::cout << "Entering B::Foo" << std::endl;};
};
class D : public B {
private:
virtual void Foo() {std::cout << "Entering D::Foo" << std::endl;}
};
int main()
{
D obj;
obj.FuncWrapper();
return 0;
}
在基类B中定义了一个非虚函数FuncWrapper,然后定义了一个一般虚函数Foo,在FuncWrapper调用Foo,这样我们将Foo放在private域中,增强了类的封装性。
- 方法2:virtual函数替换为函数指针(或者std::function对象,C++11之后的特性),这是Strategy设计模式的具体形式。
#include <iostream>
#include <functional>
static void Func1()
{
std::cout << "Calling Func1" << std::endl;
}
static void Func2()
{
std::cout << "Calling Func2" << std::endl;
}
using MyFunc = std::function<void()>; // 使用using语句代替传统的typedef,定义一个函数类型
class MyClass {
public:
MyClass(MyFunc func) : f(func) {}
void DoSomething() {f();}
private:
MyFunc f;
};
int main()
{
MyClass obj1(Func1), obj2(Func2);
obj1.DoSomething();
obj2.DoSomething();
return 0;
}
在这个MyClass类中,定义了一个私有成员变量,它代表处理函数真正的执行动作,由于每个对象可能的执行动作不一样,所以可以在构造函数中传入具体的函数,注意:这里使用了C++11之后的std::function模板,这是比传统的函数指针更好用的一种C++方式。
条款36:绝不重新定义继承而来的非虚函数
一句话:子类切勿重新定义继承自父类中的非虚函数,如果你真的要这么做,请将其定义成虚函数。
条款37:绝不重新定义继承而来的缺省参数值
这是个C++考试中常考的一个知识点,考察以下示例代码:
#include <iostream>
class B {
public:
virtual void Foo(int val = 1) {std::cout << "val = " << val << std::endl;}
};
class D : public B {
public:
virtual void Foo(int val = 2) {std::cout << "val = " << val << std::endl;}
};
int main()
{
B* p = new D();
p->Foo();
return 0;
}
可以看到在调用子类的Foo虚函数版本中,参数却是用了父类中的缺省值。
简而言之:缺省参数的设置是静态的,而虚函数的执行是动态的,所以请不要重新定义继承而来的缺省值,否则会引起误解。
条款38:通过组合关系塑模出“has-a”
- 一些C++设计模式中推荐尽可能使用组合而非继承。
- 组合意味着“has-a”关系。
条款39:明智而谨慎的使用private继承
条款40:多重继承
以上两条不常用,也不推荐使用。