条款21:GotW#5 改写虚拟函数(Overriding Virtual Functions)

本文深入解析了一段C++代码,揭示了隐藏、重载与重写的区别,讨论了虚析构函数的重要性,以及默认参数的不当修改。同时,提出了关于main函数、析构函数、函数重写及默认参数的若干设计准则。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

当你审查公司的旧代码时,可能偶然遇到不知哪个程序员编写的程序片段。写的人似乎对C++的特质有相当认识。那么这个程序员期望打印什么结果呢?实际结果如何呢?

#include <iostream>
#include <complex>
using namespace std;

class Base
{
public:
	virtual void f(int);
	virtual void f(double);
	virtual void g(int i = 10);
};

void Base::f(int)
{
	cout << "Base::f(int)" <<endl;
}


void Base::f(double)
{
	cout << "Base::f(double)" <<endl;
}

void Base::g(int i)
{
	cout << i <<endl;
}

class Derived : public Base
{
public:
	void f(complex<double>);
	void g(int i = 20);
};

void Derived::f(complex<double>)
{
	cout << "Derived::f(complex<double>)" <<endl;
}

void Derived::g(int i)
{
	cout << "Derived::g()" <<endl;
}

void main()
{
	Base b;
	Derived d;
	Base* pb = new Derived;
	b.f(1.0);
	d.f(1.0);
	pb->f(1.0);
	b.g();
	d.g();
	pb->g();
	delete pb;
}

【解答】

首先,让我们考虑某些风格问题,以及一些真正的错误。

1. “void main()” 不是标准写法,因此没有太多移植性。

虽然void main()不是标准写法,但许多编译器接受它,不过即使你的编译器接受了void main(),并不达标你可以将它移植到另一个编译器上,因此你最好习惯使用以下两个标准写法之一:

int main();
int main(int argc,char* argv[]);

main函数没有写return 0语句,虽然标准的main()应该传回一个int,但是没写编译器默认返回的是return0。

2.“delete pb;”是不安全的。

这看起来无害,事实上也无害,前提是Base得提供虚析构函数。本例删除的是一个point-to-base,而该base class没有虚析构函数,这是一种有害行为。你所预期到只能是程序崩溃,因为错误的析构函数将被调用,而且还会错误使用对象大小来调用operator delete()。

设计准则:让base class的析构函数成为virtual(除非你确定不会有人通过基类指针来删除子类对象)。

下面三个很重要,用来区分三个常见的术语:

  • 重载:函数名相同,作用域相同,参数类型不同的函数。
  • 重写:在派生类中提供一个相同名字,相同参数函数,并且基类的函数有virtual。
  • 隐藏:在派生类中提供一个相同名字,参数类型不同。

3.Derived::f 不是重载函数。

Derived::f并不会重载Base::f,而是隐藏了Base::f。这个区别非常重要,因为意味着Base::f(int)和Base::f(double)在Derived作用域内不可见。

如果Derived的编写者打算隐藏Base类的名为f的函数,这是没问题的。然而,通常情况下,这种屏蔽是无意为之的。将这些名字放进Derived作用域中正确的方法是"using Base::f"。

设计准则:当你提供一个函数,其名称和继承而来的函数同名,但参数不同,如果你不想因此遮掩了继承而来的函数,那么应该使用using声明来确保继承函数在作用域中。

4.Derived::g重写了Base::g,但是改变了默认参数。

void g(int i = 20);

这是对用户而言是不友好的写法,不要改变默认参数,除非你确实像迷惑别人。

设计准则:绝不要重写继承函数的默认参数。

现在我们已经谈论完了奇怪的风格问题,让我们回头看看代码,并看看它们的行为是否符合你的预期:

void main()
{
	Base b;
	Derived d;
	Base* pb = new Derived;
	b.f(1.0);

以上没问题,第一次调用了Base::f(double),正如预期那样。

d.f(1.0);

这里调用的是Derived::f(complex<double>)。因为没有重写。

pb->f(1.0);

这里调用的是Base::f(double)。虽然Base*指向的是一个Derived对象,因为重载根据静态类型决定,而不是动态类型决定的。

同样的理由,如果你调用的pb->f(complex<double>),将无法通过编译,因为在Base接口中没有符合的函数。

b.g();

打印出的是10,简单调用的基类的g(int i = 10)。

d.g();

打印的是20,简单调用了派生类的g(int i = 20)。

pb->g();

打印的10,因为默认参数是静态类型的,不是动态类型的。实际调用的函数根据动态类型来决定的。

delete pb;

造成内存泄露,因为派生类的部分对象没删除,导致切割问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值