详解c++多态中的析构与构造函数

首先简单介绍一下多态。

多态是面向对象编程中的概念,它允许我们使用基类类型的指针或引用来调用派生类对象的方法。C++中实现多态主要依靠虚函数动态绑定

那怎么使用多态呢?

基类指针或引用指向派生类对象。

在我学习过程中,这些概念耳熟能详,但是为什么要有多态呢,先看下面这段代码

class Animal
{
public:
	void speak()
	{
		cout << "动物在说话" << endl;
	}
};

class Cat :public Animal
{
public:
	void speak()
	{
		cout << "小猫在说话" << endl;
	}
};

void doSpeak(Animal &animal)
{
	animal.speak();
}

void test()
{
	Cat cat;
	doSpeak(cat);
}

int main()
{
	test();
}

Cat是Animal的派生类,在doSpeak函数中传入派生类对象的引用,最终的输出如下:

那么为什么结果是这个呢,刚学完继承的时候觉得子类重写的方法会覆盖父类,现在又搞不懂了。

当调用 doSpeak(cat) 时,参数 cat 实际上是一个 Cat 类型的对象,但编译器会进行切片操作,将  Cat 对象切割成一个 Animal 对象传递给 doSpeak() 函数。这意味着在 doSpeak() 函数中,实际上只能访问到 Animal 对象的部分,即使传入的是 Cat 对象。

那么切片是什么呢?

将一个派生类对象(如 Cat 对象)赋值给一个基类对象(如 Animal 对象)时,会发生切片。

也就是用基类类型的指针或引用来调用派生类对象时,会发生切片。

在 C++ 中,当通过基类指针指向派生类对象时,这个指针只能访问到基类中定义的成员变量和成员函数,而无法直接访问派生类特有的成员变量和成员函数。这就是所谓的“切片”(slicing)问题。
这意味着只有基类的部分成员和方法会被保留,而派生类特有的成员和方法会被丢弃

在调用 doSpeak(cat) 函数时,实际上是将Cat 对象传递给了一个接受 Animal 引用的函数。由于 doSpeak() 函数的参数类型是 Animal &,因此 Cat 对象会被切片为一个 Animal 对象,只有 Animal 类的部分内容被传递给了函数,导致无法访问到 Cat 类特有的内容。

因此,在没有使用虚函数的情况下,如果直接将派生类对象赋值给基类对象或传递给基类引用/指针,会导致切片问题,使得只有基类的部分功能可用,而派生类特有的功能则无法访问。

那我们之所以将基类指针指向派生类对象,目的是为了实现派生类中独有的方法,于是乎就有了多态。

---------------------------------------------------------

关于如何使用多态,网上有很多优秀的帖子,本文不过多赘述,直接切入主题——多态中的构造和析构函数。

先看代码

class Animal
{
public:
	Animal()
	{
		cout << "Animal构造函数调用" << endl;
	}
	
	virtual void speak() = 0;

    ~Animal()
	{
		cout << "Animal析构函数调用" << endl;
	}
};

class Cat : public Animal
{
public:
	Cat(string name)
	{
		cout << "Cat构造函数调用" << endl;
		m_Name = new string(name);
	}

	virtual void speak()
	{
		cout << *m_Name << "小猫在说话" << endl;
	}

	string* m_Name;

	~Cat()
	{
		if (m_Name != NULL)
		{
			cout << "Cat析构函数调用" << endl;
			delete m_Name;
			m_Name = NULL;
		}
	}
};

void test01()
{
	// 使用 new 运算符创建对象时,会分配内存并自动调用对象的构造函数来初始化对象,确保对象在创建时被正确初始化。
	Animal* animal = new Cat("tom");
	animal->speak();
	delete animal;// 删除父类指针的时候并不会走子类的析构函数
}

int main()
{
	test01();
}

结果如下

只有4行输出,从继承的角度来说应该有5行输出。
子类创建对象时有五个步骤:

  1. 先调用父类的构造函数
  2. 再调用子类的构造函数
  3. 实现子类重写的成员方法(如果有的话)
  4. 子类对象销毁时调用子类的析构函数
  5. 最后调用父类的析构函数

图中的输出结果少了一个第4步,我们来分析一下输出的每一步的由来。

首先在test01()中new Cat("Tom"),此时Cat对象在堆区被创建,自动执行1和2步,得到“Animal构造函数调用”和“Cat构造函数调用”。

然后调用子类重写的speak()方法,通过多态的方式得到“tom小猫在说话”。

最后删除父类指针时,为什么只调用了父类的析构函数,没调用子类的析构函数呢?
其实跟上文所说的切片有关。
尽管是通过 Animal* animal = new Cat("tom"); 这样的语句创建了一个 Cat 对象,并将其地址赋给了一个 Animal 类型的指针 animal,但实际上这只是将 Cat 对象的地址存储在了一个 Animal 类型的指针中。由于切片作用,编译器在这种情况下只会关注指针的类型,即基类类型 Animal,而不会考虑指针所指向的实际对象的类型。
所以销毁animal指针时,只会调用父类的析构函数,因为父类指针指向子类,经过切片,这个指针只能访问到基类中定义的成员变量和成员函数,而无法直接访问派生类特有的成员变量和成员函数。

那么这个时候怎么才能调用子类的析构函数呢,没错,是virtual。

至于为什么加了virtual就可以使用子类,相信看到这篇帖子的伙伴都知道虚函数表指针,很容易理解。

于是在delete animal时,调用了子类的析构函数,但是由于子类继承父类,子类销毁时在自动调用析构函数后,会自动调用父类的析构函数,也就出现了“Cat析构函数调用”之后输出“Animal析构函数调用”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值