Cherno C++学习笔记 P28-29 虚函数与纯虚函数

在这一篇文章当中,我们会讲到有关于虚函数和纯虚函数的内容。

首先我们介绍虚函数。什么是虚函数?虚函数是定义在父类当中,允许我们在子类里面重新实现该方法的函数。这个函数对于类的继承是非常重要的。

比如我们举这样一个例子,父类是Entity,子类是Player,两个类当中都有一个名字叫做GetName的函数,其中Entity当中的函数只会返回Entity,而Player类的名字是由用户自己定义的。

class Entity {
public:
	std::string GetName() {
		return "Entity";
	}
};

class Player : public Entity {
public:
	std::string m_Name;
public:
	Player(const std::string& name)
		:m_Name(name){}
	std::string GetName() { return m_Name; }
};

然后我们分别实例化一个Entity对象和一个Player对象,然后打印它们的名字:

int main() {
	Entity* e = new Entity;
	Player* p = new Player("Cherno");
	std::cout << e->GetName() << std::endl;
	std::cout << p->GetName() << std::endl;
	std::cin.get();
}

输出结果如下所示:

很好,我们成功的打印了这两个名字。

接下来我们定义一个函数,输入的变量就是一个Entity类的指针,然后用来打印这两个对象的名字。

void Function(Entity* e) {
	std::cout << e->GetName() << std::endl;
}

int main() {
	Entity* e = new Entity;
	Player* p = new Player("Cherno");
	Function(e);
	Function(p);
	std::cin.get();
}

但是打印的结果如下所示,很明显并不正确。

为什么会出现这种情况?我们明明想要调用Player类当中的GetName,结果打印出来的居然是Entity。这个原因是,当我们正确使用一个函数的时候,那么在调用方法时,函数调用的一定是这个类型的方法。这也就导致了这个函数调用出来的是Entity类的GetName。

那么我们如何能够避免这个问题?我们希望C++的编译器能够意识到,虽然输入类型是父类的类型,但是我们想要调用子类的方法,那么这个问题的答案就是虚函数。

虚函数引入了dynamic dispatch(动态编联),通常会通过v table(虚函数表)来实现编译。在v表当中,包含了基类的所有虚函数的映射,可以在程序运行的时候找到对应的那个子类的映射,从而帮我们把正确的方法挑选出来。

定义虚函数的方式是在函数名前面加上virtual关键字。比如这个问题中,我们把Entity类改为

class Entity {
public:
	virtual std::string GetName() {
		return "Entity";
	}
};

那么输出结果就会恢复正常:

当然,在子函数这一边,我们也可以使用一个关键字override来标明这个函数目前是重写了父类当中的一个函数。可以选择不标注,不会影响到程序运行,但是通过这样的标注,可以让我们的代码看起来更加清晰和整洁。而且还可以防止出现bug,比如我们不小心把父类当中的虚函数变成普通函数了,那么这个override关键字就会提示我们这里出了问题。

std::string GetName() override { return m_Name; }

虚函数虽然很好,但是还是需要成本的,因为首先,虚函数会存下一个v表和v表指针,来保存对所有子类的映射;其次,需要遍历整个v表才能够决定使用哪个函数。但是实际使用当中,这个对于性能的影响不是很大,除非是嵌入式编程这类要求极致性能的,也许会考虑这样一个问题。但是在windows下写C++,这一点并没有太大影响。

另外有一个有趣的地方,如果我们不在Entity类内定义任何虚函数,我们输出它的大小,会发现它只占了一个byte的内存;但是如果我们在里面定义了一个虚函数再看它的大小,会发现它的大小变成了8byte,说明虚表和虚指针的存在确实增加了它的内存占用量。

那么如果我们定义了一个虚函数,但是在基类当中我们干脆就不实现它,只希望子类来实现它,这个可不可以?答案是可以的,我们把这类函数成为纯虚函数,而这样的类会被称为接口(interface)。在之前的例子当中,我们在父类中定义了虚函数,说明这个虚函数有了默认定义,但是有的时候我们不希望它有默认定义,我们希望这个函数必须由子类实现,那么我们就可以把父类中的这个函数改为纯虚函数,这也是C++当中的一种抽象方法。

需要注意的一点是,接口类里面有没有实现的方法,所以接口类是不可以实例化的;当然,如果子类也没有实现这个方法,同样的子类也是没有办法实例化的。

定义虚函数的方法:virtual + 类型 + 函数名() = 0

只有实现了这个虚函数,我们才可以实例化对应的类。

那么为什么我们要使用纯虚函数?因为比如有这样一个函数,它接受某种类型,而且希望这个类型一定会有某种方法,因为它会调用这个方法,那么我们只需要定义一个包含有这个方法作为纯虚函数的接口类,然后我们使用其它类来继承这个类,实例化这个方法,再把实例化的对象传入到这个函数中,就完成了我们的设计目标。

以下面这个Entity和Player类为例,这次我们定义一个接口Printable,表明是一个可以打印名字的接口。

#include<iostream>
#include<string>


class Printable {
public:
	virtual std::string PrintName() = 0;
};

class Entity : public Printable {
public:
	std::string PrintName() { return "Entity"; };
};

class Player : public Entity {
public:
	std::string m_Name;
public:
	Player(const std::string& name)
		:m_Name(name){}
};

void Function(Printable* p) {
	std::cout << p->PrintName() << std::endl;
}

int main() {
	Entity* e = new Entity;
	Player* p = new Player("Cherno");
	Function(e);
	Function(p);
	std::cin.get();
}

如果我们这样做,那么输出的结果如下所示:

我们再一次遇到了打印的都是Entity的情况,因为我们目前只是在Entity类当中实现了这个函数,但是在Player类当中并没有,所以我们重新写一下这两个类:

class Entity : public Printable {
public:
	virtual std::string PrintName() override { return "Entity"; };
};

class Player : public Entity {
public:
	std::string m_Name;
public:
	Player(const std::string& name)
		:m_Name(name){}
	std::string PrintName() override {
		return m_Name;
	}
};

这样我们打印出来的结果就是正确的了。

需要注意一个细节就是,其实在Entity类当中,我们无需将PrintName声明为虚函数,因为我们的输入其实是Printable类型,所以它自动的就会去找对应子类当中的实现方式。但是最后还是要注意的是,如果不定义父类当中的纯虚函数,那么我们就无法实例化。
以上就是关于虚函数和纯虚函数的全部内容了,希望大家喜欢!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值