3.4、基类成员函数有缺省参数
继承类中重载的成员函数与基类相比可以有不同的缺省参数。参数使用依赖于声明变量的类型,而不是背后的对象。下面是一个简单的例子,继承类在重载的成员函数中提供了一个不同的缺省参数:
class Base
{
public:
virtual ~Base() = default;
virtual void go(int i = 2) { println("Base's go with i={}", i); }
};
class Derived : public Base
{
public:
void go(int i = 7) override { println("Derived's go with i={}", i); }
};
如果在Derived对象上调用go(),带有缺省参数7的Derived版本的go()执行。如果在Base对象上调用go(),带有缺省参数2的Base版本的go()执行。然而,最诡异的部分出现了,如果在实际上执行Derived对象的Base指针或Base引用上调用go(),调用的是Derived版本的go(),但是用的却是Base的缺省参数2.该行为展示在如下的示例中:
int main()
{
Base myBase;
Derived myDerived;
Base& myBaseReferenceToDerived{ myDerived };
myBase.go();
myDerived.go();
myBaseReferenceToDerived.go();
}
代码的输出如下:
Base's go with i=2
Derived's go with i=7
Derived's go with i=2
这种行为的原因是c++使用编译时表达式的类型来绑定缺省的参数,而不是运行时类型。缺省的参数没有在c++中“继承”。如果本例中的Derived类无法像其父类一样提供缺省参数给到go(),就无法在不传递参数给Derived对象的情况下调用go()。
注意:当重载一个拥有缺省参数的成员函数时,也应该提供一个缺省的参数,可能是同样的值。推荐使用缺省的命名常数,这样在继承类中也可以用同名的常数。
3.5、基类成员函数有不同的访问指标符
有两种改变成员函数访问标示符的方法:可以让它限制更多或更少。两者在c++中都没有什么道理,但是有几个合情合理的原因尝试去这样做。
在成员函数上(或数据成员上)加强紧密的限制,可以采取两个方法。一个方法是改变整个基类的访问标示符。本章后面会描述。另外一个方法是简单重新定义继承类中的访问,如下所示的Shy类:
class Gregarious
{
public:
virtual void talk() { println("Gregarious says hi!"); }
};
class Shy : public Gregarious
{
protected:
void talk() override { println("Shy reluctantly says hello."); }
};
在Shy中的talk()的受保护的版本恰当地重载的Gregarious::talk()成员函数。在Shy上尝试调用talk()的客户代码得到了一个编译错误:
Shy myShy;
myShy.talk(); // Error! Attempt to access protected member function.
然而,成员函数并不是全部受保护的。只要获得Gregarious引用或指针来访问你认为是受保护的成员函数:
Shy myShy;
Gregarious& ref{ myShy };
ref.talk();
代码的输出如下:
Shy reluctantly says hello.
它证明了使成员函数在继承类中受保护确实重载了成员函数(因为重载类版本被正确调用),但是它也证明如果基类使其为公共的,无法整体加强受保护的访问。
注意:没有合理的方式(或好的理由)来限制对公共的基类成员函数限制访问。
注意:前面例子重新定义了重载类中的成员函数,因为它想显示不同的信息。如果不想修改实现,只想改变成员函数的访问标识符,优选的方式是简单添加一个using声明在重载类定义中用期望的访问标示符。
在重载类中减少访问限制更容易(也更说得通)。最简单的方式是提供公共成员函数,在重载类中调用基类中的受保护的成员函数,如下所示:
class Secret
{
protected:
virtual void dontTell() { println("I'll never tell."); }
};
class Blabber : public Secret
{
public:
virtual void tell() { dontTell(); }
};
客户调用Blabber对象的public tell()成员函数高效地访问了Secret类的protected成员函数。当然了,这并不是真的修改了dontTell()的访问标示符;它只是提供了一个公共的访问方式。
也可以在Blabber中显式重载dontTell(),并且用public访问给一个新的行为。这要比减少访问标示更有道理,因为它在指向基类的引用或指针发生了什么是非常清楚的。例如,假设Blabber真的让dontTell()成员函数成为public:
class Blabber : public Secret
{
public:
void dontTell() override { println("I'll tell all!"); }
};
现在可以在Blabber对象上调用dontTell():
myBlabber.dontTell(); // Outputs "I'll tell all!"
如果不想修改重载成员函数的实现,只是修改访问标示符,可以使用using声明,如下所示:
class Blabber : public Secret
{
public:
using Secret::dontTell;
};
这也允许在Blabber对象上调用dontTell(),但是这一次输出会是“I’ll never tell”:
myBlabber.dontTell(); // Outputs "I'll never tell."
在上面的两个场景中,然而,基类的受保护的成员函数保持受保护状态,因为任何通过Secret指针或引用的尝试调用Secret的dontTell()成员函数都编译不成功:
Blabber myBlabber;
Secret& ref{ myBlabber };
Secret* ptr{ &myBlabber };
ref.dontTell(); // Error! Attempt to access protected member function.
ptr->dontTell(); // Error! Attempt to access protected member function.
唯一有用的修改成员函数访问标示符的方式是通过提供限制更小的访问符给到受保护的成员函数。