《More Effictive C++》学习笔记 — 杂项

条款33 — 将非尾端类设计为抽象类

假设我们正在做一个项目,以软件来处理动物。大部分动物的实现都一致,其中有两种动物需要特殊处理:蜥蜴和鸡,在此情况下,这几个类之间的关系可能是这样的:
在这里插入图片描述
Animal 类负责处理所有的动物的共同特征,LizardChicken 负责两种动物的特化处理。

1、赋值操作符的切割问题

class Animal
{
public:
	Animal& operator=(const Animal& rhs)
	{
		...
		return *this;
	}
};

class Lizard : public Animal
{
public:
	Lizard& operator=(const Lizard& rhs) 
	{
		...
		return *this;
	}
};

class Chicken : public Animal
{
public:
	Chicken& operator=(const Chicken& rhs)
	{
		...
		return *this;
	}
};

考虑这样的调用代码:

Lizard lizard1;
Lizard lizard2;

Animal* animal1 = &lizard1;
Animal* animal2 = &lizard2;

*animal1 = *animal2;

很明显,这将会发生切割问题,其主要原因是 animal1 的静态类型和动态类型不一致,而编译器并不知道。

2、使用虚函数赋值

如果我们想让编译器知道它们的静态类型,可以让赋值操作符成为虚函数:

class Animal
{
public:
	virtual Animal& operator=(const Animal& rhs)
	{
		...
		return *this;
	}
};

class Lizard : public Animal
{
public:
	virtual Lizard& operator=(const Lizard& rhs)
	{
		...
		return *this;
	}
};

虚函数支持返回值类型协变,却不支持参数的动态转换。事实上,只要我们在派生类虚函数后面加上 override 关键字就可以很轻松地检查出来这个问题。因此,虚函数应该是下面这样:

class Animal
{
public:
	virtual Animal& operator=(const Animal& rhs)
	{
		...
		return *this;
	}
};

class Lizard : public Animal
{
public:
	virtual Lizard& operator=(const Animal& rhs) override
	{
		...
		return *this;
	}
};

class Chicken : public Animal
{
public:
	virtual Chicken& operator=(const Animal& rhs) override
	{
		...
		return *this;
	}
};

这解决了前面的问题,但是又引入了一个新的问题:

Lizard lizard1;
Chicken chicken1;

Animal* animal1 = &lizard1;
Animal* animal2 = &chicken1;

*animal1 = *animal2;

两个操作数的类型竟然可以不同。这可不是我们想要的。我们希望不同类型的对象之间不可以相互赋值。

3、使用RTTI

借助动态类型转换,我们得以这样识别出右操作数的类型:

Lizard& Lizard::operator=(const Animal& rhs) override
{
	const Lizard& lizard = dynamic_cast<const Lizard&>(rhs);
	...
	return *this;
}

当然,在类型不匹配的情况下此函数会抛出异常。

除此之外,我们发现,这种修改导致两个 Lizard 对象之间的赋值也要进行类型检查。这有点不好。我们考虑增加普通的赋值函数:

class Lizard : public Animal
{
public:
	virtual Lizard& operator=(const Animal& rhs) override;

	Lizard& operator=(const Lizard& rhs);
};

那么第一个赋值操作也可以简化为:

virtual Lizard& Lizard::operator=(const Animal& rhs) override
{
	return operator=(dynamic_cast<const Lizard&>(rhs));
}

这样做对用户的要求就是他们每次调用赋值操作时都需要使用 try-catch 语句进行异常捕获。这并不合适,增加了代码的复杂度。

4、阻止赋值操作符被访问

综上来说,我们如果执意使用 operator= 进行赋值(如果愿意实现 clone 方法以拷贝自然另当别论),那么并没有很好的办法解决前面遇到的所有问题。因此,最好的选择就是阻止这种赋值动作:

class Animal
{
private:
	Animal& operator=(const Animal& rhs)
	{
		return *this;
	}
};

class Lizard : public Animal
{
public:
	Lizard& operator=(const Lizard& rhs);
};

class Chicken : public Animal
{
public:
	Chicken& operator=(const Chicken& rhs);
};

那么下面的行为就正如我们期望那样:

Lizard lizard1, lizard2; 
lizard1 = lizard2; // valid

Chicken chicken1, chicken2;
chicken1 = chicken2; // valid

Animal* animal1 = &lizard1;
Animal* animal2 = &chicken1;

*animal1 = *animal2; // invalid

然而,这将导致 animal 对象之间的相互赋值不合法。同时,LizardChicken 的赋值操作符的实现也变得很困难,因为它们无法调用相应的基类版本。针对第二个问题,我们可以将 Animal 的赋值操作符变为 protected。但是,第一个问题仍悬而未决。

5、消除对象相互赋值的需要

最简单的办法就是消除允许 Animal 对象相互赋值的需要,而完成此事的最简单做法就是让 Animal 成为一个抽象类。那些 Animal 的实例怎么办呢?可以提取抽象的功能到 AbstractAnimal 中,让 AnimalLizardChicken 都继承于它:
在这里插入图片描述
各个类的定义如下:

class AbstractAnimal
{
protected:
	AbstractAnimal& operator=(const AbstractAnimal& rhs)
	{
		return *this;
	}

public:
	virtual ~AbstractAnimal() = 0;
};

class Animal : public AbstractAnimal
{
public:
	Animal& operator=(const AbstractAnimal& rhs)
	{
		return *this;
	}
};

class Lizard : public AbstractAnimal
{
public:
	Lizard& operator=(const Lizard& rhs);
};

class Chicken : public AbstractAnimal
{
public:
	Chicken& operator=(const Chicken& rhs);
};

6、总结

上述对于”通过基类的指针进行赋值动作“的讨论,是以”具体基类含有成员数据“的假设作为基础。如果具体基类中没有数据成员,也许你会觉得就没问题了。其实并不是,一方面,我们要以发展的眼光看待问题。在具体类中,未来是可能出现数据的。另一方面,没有成员数据的类不就该是抽象类吗?

使用抽象基类代替具体基类,最大的好处在于强迫我们发现有用的抽象性质。如果有个具体类 C1 和 C2,而你希望 C2 以公有方式继承 C1。你应该将原本的双继承体系变为三继承体系,正如我们对 Animal 所做的。这种转变的主要价值在于,它强迫我们验明抽象类A。很显然,C1 和 C2 有某些共同的东西,所以它们之间的关系是公有继承。如果采用上述转变,我们就必须鉴定出何为共同特征并将抽取称C++类。

面向对象的设计目标是辨识处一些有用的抽象性,并强迫它们成为抽象类。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值