我的C++实践(13):多态化的构造函数和非成员函数

    我们知道构造函数和非成员函数函数不可能是虚函数,但有时候当我们使用一个继承体系时,又希望这些函数具有多态行为,能根据基类指针(或引用)自动地使用子类的实现。其实,通过一些设计技术,我们可以达到这样的目的。
    1、虚构造函数: 完成对象的创建,并返回基类指针的函数。它的功能就是创建对象,但具有了多态行为,这种虚构造函数有很多的应用。
    设想写一个时事通讯(newsletter)的程序,组成NewsLetter的元素是文本TextBlock和图片Graphic,所有元素都从抽象基类NLComponent派生。图片和文本的内容在硬盘(或网络)上,这样TextBlock和Graphic的构造函数需要根据输入流来构造对象。代码如下:

 

    这里的constructComponent就是所谓的虚构造函数。我们在NewLetter中需要创建所有的数据成员(各个元素对象),并把指针压入components列表中。但components列表中只需压入NLComponent*型的指针,无需知道这个指针所指对象的确切子类型,这就需要各个子类的构造函数具有多态行为,希望它们能返回基类指针。但我们知道类的构造函数不能为虚函数,因此我们把对象的创建工作封装成constructComponent函数,它根据所读取的不同数据来创建不同的子类对象,并返回一个NLComponent*指针。为避免封装带来的效率损失,我们把它声明为inline。constructComponent中一般有一个分支语句,根据所读取数据的不同,创建相应的对象。constructComponent充当了真正构造函数的多态版本,它具有多态行为,因此可称为虚构造函数。这里constructComponent是全局函数,我们也可以把它实现为NewsLetter类的静态成员函数。如果我们不想动态分配资源,则在constructComponent中不用new,返回时用基类引用即可。
    虚构造函数的实现思想: 在继承体系中定义一个全局函数,它用来根据一个标志构造相应的子类对象,并返回指向它的基类指针。这样这个全局函数相当于一个构造函数,又具有了多态行为。
    2、虚拷贝构造函数: 当我们要对基类指针所指对象进行拷贝时,可能不知道指针所指向的到底是那个子类对象,因此无法调用子类的拷贝构造函数。这就需要在继承体系的基类中指定一个统一的接口(一般是名为clone的虚函数),用来实现拷贝功能。每个子类重写clone(),它直接调用实际的拷贝构造函数拷贝*this,返回这个副本的指针。子类中的clone()虽然返回本子类的指针,但根据C++标准,它仍然是重写了基类的clone版本(基类中的版本返回基类指针)。clone()就相当于虚拷贝构造函数,它具有多态行为,因为可以用基类指针来指向这个返回的副本。
    例如对于NewsLetter的拷贝,我们需要拷贝components列表,由于里面存放的是NLComponent*指针,因此要作深拷贝。即我们要把指针所指的各个对象拷贝过来,这需要调用对象的拷贝构造函数,但我们不并知道对象的具体子类型,因此需要在继承体系中提供clone()函数来实现这个功能。

    在继承体系中,我们添加了一个clone()虚函数,各个子类都要实现clone()函数。现在,即使指针不知道所指向的到底是那个子类对象,但它知道所有的子类都有一个clone()函数,用它可以完成拷贝功能,因此只要调用clone()就可以了,clone()相当于拷贝构造函数的多态版本,同时它又是内联的,基本上没有性能损失。事实上,在Java、C#这样的语言中就使用了这种技术(它们的类继承体系中有一个clone函数)。
    思想总结: 从中我们可以看出,多态机制最大的好处就是让我们可以用统一的标记(基类指针或引用)来关联不同的行为(各个子类中重写的虚函数),但它也会产生副作用。它隐藏了指针(或引用)所指对象的类型,使得我们有时候不能对所指对象进行一些具体的操作。这就需要我们在继承体系的基类中指定一个统一的接口,各个子类必须重写这个接口,完成我们需要的具体操作。有了这个契约,即使我们不知道对象的具体类型,但我们知道每个类都有一个统一的接口来完成相应的操作。
    多态还有一些其他的副作用。比如把多态应用于数组时,就会出问题。如果我们用基类指针(或引用)来指向一个包含派生类对象的数组,编译器就会认为数组中的每个对象的大小都和基类一样,在对数组进行遍历时,指针的移动(array[i]相当于*(array+i),对指针进行运算)会导致获得的对象地址不准确。另外,在用delete[]删除数组也同样有这样的问题,因为delete[]也要遍历数组的各个元素来调用它们的析构函数,这同样通过作指针移动运算来计算偏移地址。一般地,多态和指针运算就是不能用在一起,在数组中最好不要使用多态。要避免这个问题,我们在做设计时应尽量少从具体类派生出具体类,而让一个类派生自抽象基类,这样我们就不太可能犯这种把多态应用于数组的错误。另一种方法是少用或不用数组,多用std::vector、std::list等容器。当我们需要数组多态时,使用容器来存放基类指针,这还能让它们指向不同子类的对象。在上面的实现中,我们就使用了std::list来存放NLComponent*型指针,这样就可以实现安全的多态行为。
    3、虚的非成员函数: 非成员函数不可能是虚函数,但根据函数的参数(是某个基类指针或引用),可以让非成员函数具有多态行为。只要在参数类型的各个子类中用统一的虚函数来完成实际任务,然后让非成员函数调用这个虚函数即可。由于虚函数是多态的,这就导致非成员函数也有了多态行为。
    比如我们希望时事通讯的各个元素类具有输出功能,能够输出文本或图片的内容。显而易见的做法是让它们重载输出运算符<<,只要在抽象基类NLComponent中声明operator<<这个统一的虚接口,所有子类就必须实现它。在应用时只要写成"cout<<基类引用",就可以自动调用相应子类的operator<<,输出相应的内容。但是我们知道operator<<通常并不把它重载为成员函数,如果要重载为成员函数,就只能写成“ostream& operator<<(ostream& out)”的形式,使用的形式是“t<<cout“,t为TextBlock或Graphic对象,这与传统使用习惯正好相反。因此operator<<一般重载为非成员函数,但又要让它具有多态行为,怎么办呢?我们可以在继承体系中实现一个统一的虚函数接口用于打印输出,然后让operator<<非成员函数调用它即可。

 

    这里的统一接口print函数完成实际的打印输出功能,它其实就相当于成员函数版的operator<<,只不过函数名改成了print。非成员函数operator<<直接根据基类引用调用print,这就获得了多态行为。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值