目录
条款32: 确保你的 public 继承建模出 “ is-a ” 关系
第六章
条款32: 确保你的 public 继承建模出 “ is-a ” 关系
“ is-a ” 关系用于表示public 继承,意味着基类与派生类之间的 一般 / 特殊关系。 基类它是一般类, 它的行为可以在派生类中特殊化。基类 TPerson 中拥有所有人都具有的一般行为, 而派生类 TStudent 可以将这些一般行为特殊化, 以符合某个学生的需要。实现这样的特殊化,并未修改 TPerson的任何功能。
特殊化在这样的语境中并不止于此。为了满足派生类的要求, 派生类可以扩展(extension)或精化 (refinement)基类。即派生类可以在已提供的一个或多个基类方法中添加新的功能(扩展), 或者派生类还可以重新实现某些或全部的基类方法(精化)。
当派生类 public 继承 基类时,基类可以派上用场的地方,派生类一样可以派上用场。 意思就是说派生类对象可以当作基类对象来使用。反之不成立。 这个规则只对 public 继承才有效。
公共继承 和 is-a的 等价关系听起来很简单,但有时你的直觉会误导你。例如,企鹅是一只鸟这是一个事实,鸟类可以飞也是一个事实。如果我们天真地试图用C++来描述这一层关系, 例如:
class Bird
{
public:
virtual void fly() { cout << "it can fly." << endl; }
};
class Penguin : public Bird
{
// fly()被继承过来了,可以覆写一个Penguin 的fly()方法,也可以直接用 Bird 类的
};
int main()
{
Penguin p;
p.fly(); // 问题是企鹅并不会飞!
system("pause");
return 0;
}
通过该程序就知道“ is-a ” 关系不是表面看起来那么简单,大家可以学习其它的著作来了解。 我推荐一本书叫 《 C++ 面向对象高效编程 第二版 》, 该书中 有讲解 “ is-a ” 关系, “ has_a ” 关系的章节。
条款33:避免掩盖继承而来的名称
该条款重点讲的是名字查找的过程,以及在类内部中查找名称的查找规则。 我们首先看书上 的例子:
int x = 10; //global
void someFunc()
{
double x = .45f; //local
cout << x << endl; // 输出0.45
cout << ::x << endl; // 这时输出的就是外层的x,输出结果为10
}
int main()
{
someFunc();
system("pause");
return 0;
}
当我们在主调函数中 调用 someFunc() 时, 第一条 输出语句是0.45, 因为局部作用域的 x 隐藏了 全局作用域的x。如图:
当如果我们想输出外层作用域的x的时候, 我们可以在x的前面加上 “ ::” 运算符来显式地访问。
像这样的程序的名字查找(需找与所用名字最匹配的过程) 是相对简单的(注明:但是对于类的成员函数以及继承中的类作用域查找规则也有所区别), 像这样的程序采用以下规则:
- 首先, 在名字所在的块中寻找其声明语句, 只考虑在名字的使用之前出现的声明
- 如果没找到, 继续查找外层作用域。
- 如果最终没有找到匹配的声明, 则程序报错。
本人注( 关于这样的程序的作用域问题在 C++ Primer 第五版 第43页有介绍)
成员函数中使用的名字按照如下方式解析:
- 首先, 在成员函数内查找该名字的声明。和前面一样, 只有在函数使用之前出现的声明才被考虑。
- 如果在成员函数内没有找到, 则在类内继续查找, 这时类的所有成员都可以被考虑。
- 如果类内也没找到该名字的声明, 在成员函数定义之前的作用域内继续查找。
本人注( 关于这样的程序的作用域问题在 C++ Primer 第五版 第 255 页有介绍)
现在来看看 名称冲突与继承 中的情况。 在C++ 中继承层次结构中, 派生类可以重用定义 或者 覆盖其直接(或者 间接)基类中的名字; 如果是重用定义 的情况,此时定义在内层作用域 (即派生类) 的名字将隐藏( 它只会隐藏基类中能访问的名称,比如public 和 protected 区域的名称,它不会隐藏基类中的 private 区域的名称,即使派生类的该名称与基类中的名称相同,因为派生类本身就不能访问基类中的私有成员的。)定义在外层作用域(即基类)的名字 。
覆盖只发生在 派生类的同名函数跟基类中那个virtual 函数的函数原型一致, 这样我们就说派生类中的版本覆盖了基类中的相应函数版本。
下面首先说说派生类的名称隐藏基类中的名称:
struct Base
{
public:
Base() : mem(11)
{
cout << "调用的是基类中的构造函数!" << endl;
}
int get_mem()
{
return mem;
}
protected:
int mem;
};
struct Derived : public Base
{
public:
Derived(int i) : mem(i) // i 初始化Derived 中的 mem
{
cout << "调用的是派生类中的构造函数!" << endl;
}
int get_mem() // 隐藏了基类中的同名成员
{
return mem; // returns Derived::mem
}
protected:
int mem; // 隐藏了基类中的同名成员
};
int main()
{
Derived d(42);
cout << "输出派生类的mem的值:" << d.get_mem() << endl;
cout << "输出基类的mem的值:" << d.Base::get_mem() << endl;//可以显式使用作用域运算符来访问Base 中的 get_mem 成员函数
Derived *myDerived = &d;
cout << "\n输出派生类的mem的值:" << myDerived->get_mem() << endl;
cout << "输出基类的mem的值:" << myDerived->Base::get_mem() << endl;
Base *myBase = &d;
// 下面这个调用的都是基类中的函数版本,为什么呢? 因为调用的该函数不是虚函数,所以调用是在编译期就进行绑定
cout << "\n输出基类的mem的值:" << myBase->get_mem() << endl;
// cout << myBase->myDerived::get_mem() << endl; // 这样写是错误的,因为派生类的成员在其基类中可能有也可能没有
system("pause");
return 0;
}
输出结果为:
调用的是基类中的构造函数!
调用的是派生类中的构造函数!
输出派生类的mem的值:42
输出基类的mem的值:11
输出派生类的mem的值:42
输出基类的mem的值:11
输出基类的mem的值:11
下面这个程序演示这一点 —— 派生类中的成员隐藏基类中的成员,参数列表和返回类型不一致的情况 :
struct Base
{
void memfcn() { cout << "Base::memfcn" << endl; } // 该函数的参数列表跟派生类中的同名函数不一样
int memfcn1() 该函数跟派生类中的同名函数返回类型不一样
{
cout << "Base::memfcn1" << endl;
return 0;
}
};
struct Derived : Base
{
void memfcn(int i) { { cout << "Derived::memfcn" << endl; } } // 隐藏基类中的memfcn
void memfcn1()
{
cout << "Derived::memfcn1" << endl;
}
};
int main()
{
Derived d; Base b;
b.memfcn(); // calls Base::memfcn
d.memfcn(10); // calls Base::memfcn
// d.memfcn(); // 错误, 参数列表为空的 memfcn 被隐藏了
d.Base::memfcn(); // ok: calls Base::memfcn
cout << endl;
b.memfcn1(); // calls Base::memfcn1
d.memfcn1(); // calls Base::memfcn1
d.Base::memfcn1(); // ok: calls Base::memfcn1
cout << endl;
system("pause");
return 0;
}
输出结果为:
Base::memfcn
Derived::memfcn
Base::memfcn
Base::memfcn1
Derived::memfcn1
Base::memfcn1
“ d.memfcn() ” 该调用是错误的,为什么?
- 为了解析这条调用语句 , 编译器首先在 Derived 中查找名字 memfcn ; 因为Derived确实定义了一个名为memfcn的成员,所以查找过程终止。一旦名字找到, 编译器就不再继续查找了。Derived中的memfcn版本需要一个int实参, 而当前的调用语句无法提供任何实参, 所以该调用语句是错误的。
下面在说派生类的名称覆盖基类中的名称:
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int i)
{
cout << "调用的是Base的virtual mf1(int i) " << endl;
}
virtual void mf2()
{
cout << "调用的是Base的 virtual mf2() " << endl;
}
void mf3()
{
cout << "调用的是Base的 mf3() " << endl;
}
void mf3(double i)
{
cout << "调用的是Base的 mf3(double i) " << endl;
}
};
void Base::mf1() // 纯虚函数是可以有实现的, 但是必须在类外定义
{
cout << "调用的是Base的virtual mf1() = 0 " << endl;
}
class Derived : public Base
{
public:
virtual void mf1()
{
cout << "调用的是Derived 的virtual mf1() " << endl;
}
void mf3()
{
cout << "调用的是Derived 的virtual mf3() " << endl;
}
void mf4()
{
cout << "调用的是Derived 的virtual mf3() " << endl;
}
};
int main()
{
Derived d;
d.mf1(); // 正确,静态调用,调用的是Derived:: mf1()
//d.mf1(10); // 错误! 静态调用, 因为Derived :: mf1 隐藏了Base::mf1
d.mf2(); // fine, calls Base::mf2
d.mf3(); // fine, calls Derived::mf3
// d.mf3(10); // error! Derived::mf3 隐藏 Base::mf3
cout << endl;
Base *myBase = &d;
myBase->mf1(); // 虚调用,调用Derived::mf1()
myBase->Base::mf1(); // 静态调用,显式使用 :: 运算符调用 Base:: mf1()
myBase->mf1(10); // 虚调用,调用的是 Base:: mf1( int i)
myBase->mf3(); // 静态调用,调用的是 Base:: mf3(),因为调用的不是虚函数,静态绑定
myBase->mf3(20); // 静态调用,调用的是 Base:: mf3(double i),因为调用的不是虚函数,静态绑定
system("pause");
return 0;
}
输出结果为:
调用的是Derived 的virtual mf1()
调用的是Base的 virtual mf2()
调用的是Derived 的virtual mf3()
调用的是Derived 的virtual mf1()
调用的是Base的virtual mf1() = 0
调用的是Base的virtual mf1(int i)
调用的是Base的 mf3()
调用的是Base的 mf3(double i)
在C++语言中, 当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定。根据引用或指针所绑定的对象类型不同, 该调用可能执行基类的版本, 也可能执行某个派生类的版本。
那么在主函数中的 d 它是一个静态对象, 它调用虚函数,调用的时候不会根据所绑定的对象类型来调用的。然后,d 它的静态类型是 Derived, 所以说,当调用某一个函数的时候,首先查找的就是 Derived 类的作用域。 当在Derived 作用域查找到 mf1 和 mf3 函数时,将发生报错,因为Derived 中没有带参数的相应版本。
当我们用基类的指针指向派生类时,即该代码 “ Base *myBase = &d; ”,当调用 “ myBase->mf1(10); ”时, 它首先在 Derived 类查找,因为没有,所以调用的是 Base 类中的相应版本, 从输出结果可以看出。
如果现在你非要访问父类里面的方法,第一种方法是派生类中使用 using 声明:
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int i)
{
cout << "调用的是Base的virtual mf1(int i) " << endl;
}
virtual void mf2()
{
cout << "调用的是Base的 virtual mf2() " << endl;
}
void mf3()
{
cout << "调用的是Base的 mf3() " << endl;
}
void mf3(double i)
{
cout << "调用的是Base的 mf3(double i) " << endl;
}
};
void Base::mf1() // 纯虚函数是可以有实现的, 但是必须在类外定义
{
cout << "调用的是Base的virtual mf1() = 0 " << endl;
}
class Derived : public Base
{
public:
// 使用 using 声明把 mf1 和 mf3 在基类中的所有重载实例都添加到Derived作用域中,并且是public的
// 注意:派生类只能为基类中能访问的名称提供using 声明, 即不能为基类中私有成员提供using声明
// 如果下面这两条语句放在 protected 和 private 区域中, 结果还是跟原来一样,都是报错。
using Base::mf1;
using Base::mf3;
virtual void mf1()
{
cout << "调用的是Derived 的virtual mf1() " << endl;
}
void mf3()
{
cout << "调用的是Derived 的virtual mf3() " << endl;
}
void mf4()
{
cout << "调用的是Derived 的virtual mf3() " << endl;
}
};
int main()
{
Derived d;
d.mf1(); // 正确,静态调用,调用的是Derived:: mf1()
d.mf1(10); // 正确,调用 Base::mf1(int i)
d.mf2(); // fine, calls Base::mf2
d.mf3(); // fine, calls Derived::mf3
d.mf3(10); // 正确,调用 Base::mf3(double i)
cout << endl;
Base *myBase = &d;
myBase->mf1(); // 虚调用,调用Derived::mf1()
myBase->Base::mf1(); // 静态调用,显式使用 :: 运算符调用 Base:: mf1()
myBase->mf1(10); // 虚调用,调用的是 Base:: mf1( int i)
myBase->mf3(); // 静态调用,调用的是 Base:: mf3(),因为调用的不是虚函数,静态绑定
myBase->mf3(20); // 静态调用,调用的是 Base:: mf3(double i),因为调用的不是虚函数,静态绑定
system("pause");
return 0;
}
输出结果为:
调用的是Derived 的virtual mf1()
调用的是Base的virtual mf1(int i)
调用的是Base的 virtual mf2()
调用的是Derived 的virtual mf3()
调用的是Base的 mf3(double i)
调用的是Derived 的virtual mf1()
调用的是Base的virtual mf1() = 0
调用的是Base的virtual mf1(int i)
调用的是Base的 mf3()
调用的是Base的 mf3(double i)
现在原来那些 用 d 对象调用的函数,都正确了。
可以想象,有时您不并想继承基类中的所有重载函数。在公共继承下,不应该出现这种情况,因为它再次违反了基类和派生类之间的is-a关系。(这就是为什么上面的using声明在派生类的public部分中: 在基类中是公共的名称在公共派生类中也应该是公共的)。然而,在私有继承下,可以不需要承基类中的所有重载函数。
例如,假设 Derived 私有地从Base继承,并且Derived想要继承的唯一版本的mf1是不带参数的版本。 using声明在这里不会起作用,因为using声明使所有具有给定名称的继承函数在派生类中可见。可以在 派生类中写一个转交函数,然后在该函数调用基类中的相应版本。
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int i)
{
cout << "调用的是Base的virtual mf1(int i) " << endl;
}
virtual void mf2()
{
cout << "调用的是Base的 virtual mf2() " << endl;
}
void mf3()
{
cout << "调用的是Base的 mf3() " << endl;
}
void mf3(double i)
{
cout << "调用的是Base的 mf3(double i) " << endl;
}
};
void Base::mf1() // 纯虚函数是可以有实现的, 但是必须在类外定义
{
cout << "调用的是Base的virtual mf1() = 0 " << endl;
}
class Derived : private Base // 私有继承 Base
{
public:
virtual void mf1()
{
Base::mf1(); // 一个转交函数
}
void mf3()
{
cout << "调用的是Derived 的virtual mf3() " << endl;
}
void mf4()
{
cout << "调用的是Derived 的virtual mf3() " << endl;
}
};
int main()
{
Derived d;
d.mf1(); // 正确,静态调用,调用的是Derived:: mf1()
system("pause");
return 0;
}
建议: 除了覆盖继承而来的虚函数的名称之外, 派生类最好不要重定义在基类中的名字(不管是 成员函数还是数据成员)。
条款34:区分接口继承和实现继承
条款35:考虑虚函数以外的其他选择
条款36:绝不重新定义继承而来的non-virtual函数
class Base
{
public:
void func() { cout << "base function" << endl; }
};
class Drived : public Base
{
public:
void func() { cout << "drived function" << endl; }
};
int main()
{
Drived d;
Base* pb = &d;
pb->func();
Drived* pd = &d;
pd->func();
system("pause");
return 0;
}
当我们通过 pb 和 pd 调用 func 函数时,显示结果却不相同。当 func 被pd 调用时,它又可能调用的是自定义的版本,也可能调用的是基类中的函数版本。
现在这个现象的原因是在于 Base:: func与 Derived :: func都是静态绑定,所以调用的non-virtual函数都是各自定义的版本。 引用也是一样。
回顾下之前的条款,如果是public继承的话,那么:
- 适用于Base Class的行为一定适用于Derived Class,因为每一个Derived Class对象都是一个BaseClass对象;
- 如果Base Class里面有非虚函数,那么 Derived Class一定是既继承了接口,也继承了实现;
- 子类里面的同名函数会隐藏( 如果派生类中的同名函数与基类中的相应版本不是虚函数)父类的同名函数,这是由于名称查找规则导致的。
如果Derived Class重定义一个non-virtual函数,那么会违反上面列出的法则。以第一条为例,如果子类真的要重定义这个函数,那么说明父类的这个函数不能满足子类的要求,这就与每一个子类都是父类的原则矛盾了。 既然如此,派生类就不应该以public 的方式继承基类。
如果Derived 真的需要实现出于 Base 中不同版本的 func, 那么该函数应该声明为 virtual 函数。
可以总结一下了,无论哪一个观点,结论都相同:
- 任何情况下都不该重新定义一个继承而来的non-virtual函数。