C++学习笔记----9、发现继承的技巧(六)---- 有趣且令人迷惑的继承问题(6)

4、继承类中的拷贝构造函数与同仁操作符

        上一章解释了当在类中有动态分配的内存时必须提供拷贝构造函数与赋值操作符。当定义继承类时,需要小心对待拷贝构造函数与operator=。

        如果继承类没有特殊数据(通常是指针)需要非缺省拷贝构造函数或operator=,不需要硬来一个,而不管基类是否有一个。如果继承类省略了拷贝构造函数或operator=,在继承类中的数据成员会被提供缺省的拷贝构造函数或operator=,基类拷贝构造函数或operator=会被用于基类中指定的数据成员。

        另一方面,如果在继承类中确实指定了拷贝构造函数,需要显式调用父类的拷贝构造函数,如下代码所示。如果不这样做,缺省构造函数(不是拷贝构造函数)就会用于对象的父类部分。

class Base
{
public:
	virtual ~Base() = default;
	Base() = default;
	Base(const Base& src) { }
};

class Derived : public Base
{
public:
	Derived() = default;

	Derived(const Derived& src) : Base{ src } { }
};

        同样的,如果继承类重载了operator=,差不多也总是需要调用父类版本的operator=。唯一不需要这么做的情况是如果有某些奇怪的原因,只想当赋值发生的时候赋值部分对象。下面的代码展示了如何 在继承类中调用父类的赋值操作符:

Derived& operator=(const Derived& rhs)
{
	if (&rhs == this) {
		return *this;
	}
	Base::operator=(rhs); // Calls parent's operator=.
	// Do necessary assignments for derived class.
	return *this;
}

        警告:如果继承类没有指定它的拷贝构造函数或operator=,基类功能继续工作。然而,如果继承类确实提供了自己的拷贝构造函数或operator=,它需要显式地调用基类版本。

        注意:当需要在继承层次结构中的拷贝功能时,专业c++开发者通用的方法是实现多态clone()成员函数,因为单纯依赖标准拷贝构造函数与拷贝赋值操作符是不够的。多态clone()会在以后的章节中讨论。

5、运行时类型装备

        与其它面向对象编程语言相关,c++是面向编译时的。前面已经学到,重载成员函数好用的原因是在成员函数与其实现之间是间接层次,而不是对象有内建的自身类的知识。

        然而,c++的属性提供了对象的运行时观点。这些属性通常以叫做运行时类型信息(RTTI)属性集来分类。RTTI提供了一些有用的属性来工作于对象的类成员。dynamic_cast就是这样的一个属性,它允许在面向对象的层次结构中的类型的安全转换;这在本章前面也讨论过了。在没有vtable的类上使用dynamic_cast(),也就是说,没有任何virtual成员函数,会造成编译错误。

        第二个RTTI属性是typeid操作符,它让你在运行时查询类型。应用这个操作符的结果是一个指向std::type_info对象的引用,在<typeinfo>中定义。type_info类有一个成员函数叫做name()返回类型的编译依赖的名字。typeid操作符行为如下:

  • typeid(type):结果是指向type_info的引用,表示给定的类型。
  • typeid(expression)
  • 如果检测expression的结果是一个多态类型,那么 expression被检测,typeid操作符的结果是一个指向type_info对象的xhet ,表示检测过的expression的动态类型。
  • 否则,expression不被检测,结果是指向type_info对象的引用,表示静态类型。

        在大部分情况下,不需要使用typeid,因为任何代码基于对象类型有条件执行都会更好地处理,例如,virtual成员函数。

        下面的代码使用typeid打印基于对象类型的信息:

class Animal { public: virtual ~Animal() = default; };
class Dog : public Animal {};
class Bird : public Animal {};

void speak(const Animal& animal)
{
	if (typeid(animal) == typeid(Dog)) {
		println("Woof!");
	} else if (typeid(animal) == typeid(Bird)) {
		println("Chirp!");
	}
}

        当你看到这样的代码,马上考虑使用virtual成员函数来重新实现其功能。在这种情况下,更好的实现是在Animal基类中声明一个叫做speak()的virtual成员函数。Dog会重载成员函数打印“Woof”,而Bird会重载它打印“Chirp!”。这个方法更符合面向对象编程,与对象相关的功能给到那些对象。

        警告:只有在类有至少一个virtual成员函数时typeid操作符才会正确工作,也就是说,当类有一个vtable。还有,typeid操作符剥离了参数的引用与const指示符。

        typeid操作符的一个可能的使用场景是日志与查错目的。下面的代码使用typeid来做日志。logObject()函数用”loggable”对象作为参数。设计是任何从Loggable类继承的对象都可以被做日志,并且 支持叫做getLogMessage()的成员函数。

class Loggable
{
public:
	virtual ~Loggable() = default;
	virtual string getLogMessage() const = 0;
};

class Foo : public Loggable
{
public:
	string getLogMessage() const override { return "Hello logger."; }
};

void logObject(const Loggable& loggableObject)
{
	print("{}: ", typeid(loggableObject).name());
	println("{}", loggableObject.getLogMessage());
}

        logObject()首先打印对象类的名字到控制台,后面跟着日志信息。用这种方式,当你以后读日志时,可以看到哪个对象负责哪一行。下面是微软Visual C++2022生成的输出,当logObject()用Foo实例调用时:

class Foo: Hello logger.

        可以看到,typeid操作符返回的名字是“class Foo”。然而,这个名字由编译器决定。例如,如果用GCC编译并且运行同样的代码,输出就是下面这样:

3Foo: Hello logger.

        注意:如果不是出于日志与排错的目的使用typeid,考虑改变设计,例如,使用virtual成员函数。

猜你喜欢

转载自blog.csdn.net/weixin_71738303/article/details/143379342