14.6 非自动继承的函数
不是所有的函数都能自动地从基类继承到派生类中的。构造函数和析构函数用来处理对象的创建和析构操作,但它们只知道对它们的特定层次上的对象做些什么。所以,在该类以下各个层次中的所有的构造函数和析构函数都必须被调用,也就是说,构造函数和析构函数不能被继承,必须为每一个特定的派生类分别创建。
另外,operator=也不能被继承,因为它完成类似于构造函数的活动。这就是说,尽管我们知道如何由等号右边的对象初始化左边的对象的所有成员,但这并不意味着这个初始化在继承后仍然具有同样的意义。
在继承过程中,如果不亲自创建这些函数,编译器就会生成它们(至于构造函数,我们不能创建任何的构造函数,因为编译器创建默认的构造函数和拷贝构造函数),这在第6章中已经简要地讲过了。被生成的构造函数使用成员方式的初始化,而被生成的operator=使用成员方式的赋值。下面是由编译器创建的函数的例子。
GameBoard和Game中的构造函数和operator=都自己作了声明,所以我们能知道编译器何时使用它们。另外,operator Other()从Game对象到被嵌入的类Other的对象完成自动类型变换。类Chess简单地从Game继承,并没有创建函数(观察编译器如何反应)。函数f()接收一个Other对象以测试这个自动类型变换函数。
在main()中,调用了为派生类Class生成的默认构造函数和拷贝构造函数。调用这些构造函数的Game版本作为构造函数调用继承的一部分,尽管这看上去像是继承,但新的构造函数实际上是创建的。正如所预料的,自动创建带参数的构造函数是不可能的,因为这样对于编译器来说需要靠直觉知道太多东西。
在Chess中,使用成员函数赋值,operator=也被作为一个新的函数生成,(因此,调用了基类版本),这是因为该函数在新类中没有被显式地写出。当然,析构函数也会被编译器自动地生成。
鉴于有关处理对象创建的重写函数的所有原则,我们也许会觉得奇怪,为什么自动类型变换运算也能被继承。但其实这不足为奇—如果在Game中有足够的块建立一个Other对象,那么在从Game中派生出的任何东西中,这些块仍在原地,类型变换当然也就仍然有效(尽管实际上我们可能想重定义它)。
生成的operator=仅仅作用于同种类型对象。如果想把一种类型赋予另一种类型,则这个operator=必须由自己写出。
如果仔细地观察Game,将会看到拷贝构造函数和赋值运算符显式地调用了成员对象的拷贝构造函数和赋值运算符。我们通常会想这么做的,因为如果不这样做的话,将会代替拷贝构造函数调用默认的成员对象构造函数,至于赋值运算符,则根本就不会对成员对象有赋值操作执行。
最后,观察一下Checkers,它显示地写了默认构造函数、拷贝构造函数和赋值运算符。在默认构造函数中,默认的基类构造函数被自动地调用,这正是我们所希望的。但是,有一点很重要,一旦决定写自己的拷贝构造函数和赋值运算符,编译器就会假定我们已知道所做的一切,并且不再像在生成的函数中那样自动地调用基类版本。而如果想调用基类版本,那我们就必须亲自显式地调用它们。Checkers的拷贝构造函数中,这个调用出现在构造函数的初始化列表中:
至于Checkers赋值运算符,基类的调用在函数体的第一行中:
无论何时我们继承了一个类,这些调用都将成为我们使用的规范形式的一部分。
14.6.1 继承和静态成员函数
静态(static)成员函数与非静态成员函数的共同点:
1)它们均可被继承到派生类中。
2)如果我们重新定义了一个静态成员,所有在基类中的其他重载函数会被隐藏。
3)如果我们改变了基类中一个函数的特征,所有使用该函数名字的基类版本都将会被隐藏。
然而,静态(static)成员函数不可以是虚函数(virtual)(第15章将详细介绍这个主题)。