浅谈c++中继承体系中易错点

浅谈c++中继承体系中易错点

目前,就我工作经验来看,在c++中继承体系中总爱搞错的有两点:

  1. 不把基类中需要覆盖的方法为设置为虚方法。
  2. 不把基类中的析构函数设置为虚方法。

就第一点而言,如果基类中需要覆盖的方法没有设置为虚方法会怎么样呢?后果就是继承了该类的子类,子类想要覆盖父类的方法,是不会成功的,在发生动态绑定的时候,父类依旧是调用父类的方法,并未调用子类的方法。来看看实列:

#include<iostream>
class Father {
    
    
public:
	Father(){
    
    }
	~Father(){
    
    }
	void Show()
	{
    
    
		std::cout << "I'm your Father" << std::endl;
	}
};

class Son : public Father {
    
    
public:
	Son() {
    
    }
	~Son(){
    
    }
	void Show()
	{
    
    
		std::cout << "I'm your Son" << std::endl;
	}
};

int main()
{
    
    
	Father* father = new Son;
	father->Show();
	system("pause");
}

读完代码我们先思考一下到底输出啥呢,是调用父亲的show()呢,还是调用Son的Show()?
输出:
I’m your Father
请按任意键继续. . .
果然父亲铁青着脸,调用了自己方法。儿子始终得不到润泽,可怜的娃啊。
是时候做出改变了,现在我们把父类的show()方法变为虚方法(virtual void Show(); ),再瞧瞧效果。

#include<iostream>

class Father {
    
    
public:
	virtual void Show()
	{
    
    
		std::cout << "I'm your Father" << std::endl;
	}
};

class Son : public Father {
    
    
public:
	void Show()
	{
    
    
		std::cout << "I'm your Son" << std::endl;
	}
};

int main()
{
    
    
	... // 与上面一致
}

改变后的输出:
I’m your Son
请按任意键继续. . .
这种继承问题还是比较常见的。我们需要覆盖的方法一定要用virtual 关键子修饰,不然就算发生动态绑定,那么子类的方法也得不到调用,和我们心中预期相差甚远,甚至是错误。其实,要杜绝这类错误有一个很好的方法,只需在子类的方法上加上override即可。该关键字自动检测子类的方法是不是可以覆盖父类的方法,包括:父类的方法是不是虚方法,子类的方法是否和父类的方法一致。所以按照规范来写就是:

#include<iostream>
class Father {
    
    
public:
	virtual void Show()
	... // 与上面一致
};

class Son : public Father {
    
    
public:
	virtual void Show() override
	...
};

int main()
{
    
    
	... 
}

所谓无规矩不成方圆嘛,按照规范来总会减少犯错的机会 _

第二个易错点,也是面试馆最爱问的问题,就是继承体系中,基类的析构方法一定要设置为虚方法。多一句嘴,如果面试馆总爱这样问,那么我们把要点说一下就可以了:
把基类析构函数设置为虚方法是为了防止内存泄漏的,怎么说呢?假如我们的派生类申请了一些内存,并在析构函数中释放,那么当删除基类指针的时候就不会调用子类的析构函数,为什么不会呢,刚刚我们不是做了实验吗,子类覆盖父类的方法,如果不是虚方法,就不会发生动态绑定,那么自然也就只调用父类自己的方法,子类的方法永远得不到调用,方法都得不到调用,怎么可能释放内存嘛,所以就发生内存泄漏了。当然,子类自身也是没有释放的,变成了野指针。造成内存泄漏。
人狠话不多,来,直接上代码,代码才是灵魂的归宿。

#include<iostream>

class Base {
    
    
public:
	Base()
	{
    
    

	}
	//  故意不设置为虚方法
	~Base()
	{
    
    
		std::cout << " Base 析构完成..." << std::endl;
	}
	virtual void DoSomething() = 0;
};

class Derive : public Base {
    
    
public:
	Derive() :m_data(nullptr)
	{
    
    
		m_data = new char[m_lenth];
	}

	~Derive()
	{
    
    
		delete[] m_data;
		std::cout << " Derive 析构完成..." << std::endl;
	}
	virtual void DoSomething() override
	{
    
    
		// ...
	}
private:
	// 既然是狠人,我们就new 500M内存
	// 这儿为什么不直接写524288000呢? 其实多个嘴,
	// c++有编译和运行阶段,像这种常量,编译阶段能计算出来的,
	// 编译器早就优化好了,根本就等不到运行时才算,所以啊,
	// 常量我们尽量写的直白点,像直接写524288000 这种魔鬼数字,鬼懂哦
	const unsigned int m_lenth = 500 * 1024 * 1024;
	char* m_data;
};

int main()
{
    
    
	Base* base = new Derive;
	base->DoSomething();
	delete base;
	while (true)
	{
    
    
		// 方便我们在任务管理器里看看内存到底有没有被回收
	}
	system("pause");
	return 0;
}

来看一下输出,和任务管理器的内存情况。
输出:
Base 析构完成…
就打印了这样一句话,可怜啊,子类的析构函数没有运行。难哦。
任务管理器:
内存
瞧瞧500M 的内存还没有释放掉,这样的代码要是执行在服务器上,服务器那区区几百G的内存怎么够哦,几个循环就宕机。
我们在来改改代码,将父类的析构函数变为虚方法

#include<iostream>

class Base {
    
    
public:
	...
	virtual ~Base()
	{
    
    
		std::cout << " Base 析构完成..." << std::endl;
	}
	virtual void DoSomething() = 0;
};

class Derive : public Base {
    
    
public:
	... // 和上保持一致
private:
	...
};

int main()
{
    
    
	Base* base = new Derive;
	base->DoSomething();
	delete base;
	while (true)
	{
    
    
		// 方便我们在任务管理器里看看内存到底有没有被回收
	}
	system("pause");
	return 0;
}

再看下运行效果:
输出:
Derive 析构完成…
Base 析构完成…
任务管理器:
正确释放的内存
从输出看到父类调用了子类的析构函数,任务管理器里显示内存也已经被释放。从实践的效果来看,把父类的析构函数设为虚方法是很有必要,因为涉及到内存泄漏。本地运用到还好,内存耗尽重启就好了,服务器如果这样,直接要不成嘛。那时候影响的就不是一个人了,而是成千上万的用户。关于这方面的原理性讨论我就不说了,建议多看看:
《Primer c++ 第5版》第十五章
《Effective C++》07.多态基类声明virtual析构函数
最后再多句嘴,假如我们要继承第三方库的某个类,一定要长个心眼,看看这个基类是否有虚的析构函数。就比如我不可能继承一个std::string 吧,这直接是无稽之谈。他都没有虚的虚构函数。所以啊,万事得长个心眼。

猜你喜欢

转载自blog.csdn.net/qq_33944628/article/details/120444177