virtual

在class类中由于继承关系,很多时候容易将函数重写,从而写成一篇不符合多态的不合格函数方式。导致在引用上进行错误的引用从而输出错误的结果。那么virtual可以将函数设置为虚函数,此时如果函数被重写,程序将重新定义到一个正确的函数调用

  • 在性能消耗上,虚函数的使用将会加大内存花销,首先是需要用额外的内存来存储虚函数表以便于程序识别,其次是程序会遍历虚函数表来寻找虚函数。实际上花销不是特别大(根据项目情况具体抉择)。

具体例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <iostream>

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

class Example2 : public Example1
{
private:
std::string m_Name;
public:
Example2(const std::string& name)
:m_Name(name)
{}
std::string GetName(){return m_Name;}
};

int main()
{
Example1* p1= new Example1();
std::cout << p1->GetName() << std::endl;

Example2* p2= new Example2("Example2");
std::cout << p2->GetName() << std::endl;

Example1* p3=p2;
std::cout << p3->GetName() << std::endl;
//这里用example1的指针指向了example2的实例
}

输出结果如下:

image-20240917113204537

更加明显的使用区分如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
void PrintString(Example1* p)
{
std::cout << p->GetName() << std::endl;
}

int main()
{
Example1* p1= new Example1();
Example2* p2= new Example2("Example2");

PrintString(p1);
PrintString(p2);
}

输出结果如下:

image-20240917114238745

即如上述例子所示,我们在这里使用了Example1的指针指向一个Example2的实例,由于函数名称相同,则使用了Example1的函数,其实这里的表现更像是一个完全不合格的重载函数的写法。那么要想避免函数错用的情况我们就必须使用virtual,防止函数被重写覆盖我们原本想要的内容。

修改如下:

只需要将第一个class中的std::string GetName()函数更改为virtual std::string GetName(){return “Example1”;}即可

1
2
3
4
5
6
7
8

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

纯虚函数

纯虚函数允许我们定义一个在基类中没有实现的函数让子类强制去实现这个函数,例如上方例子中我们的Example1中的方法GetName直接返回了一个默认值,在很多时候这种方法是没有意义的,很多时候是让子类为某个特定函数提供自己的定义。这种无实现的方法通常称为“接口”,即接口就是只包含未实现方法并作为模板的一个类,由于不包含实现方法,所以我们无法实例化这个类。只有当子类函数发现使用了这个函数时,子类才能创造实例,否则也会报错。重点是纯虚函数必须被发现

例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Example1
{
public:
virtual std::string GetName()=0;
};

class Example2 : public Example1
{
private:
std::string m_Name;
public:
Example2(const std::string& name)
:m_Name(name)
{}
std::string GetName(){return m_Name;}
};

int main()
{
// Example1* p1= new Example1();//此时这行代码会报错

Example2* p2= new Example2("Example2");//这行代码则会正常使用
//如果我们将Example2中的函数方法去掉则同样不能将类实例化。
std::cout << p2->GetName() << std::endl;
}

创建一个接口示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class PrintName
{
public:
virtual std::string Print() = 0;
};

class A : public PrintName//提供Print接口确保A类有特定方法
{
std::string Print() override {return "A"};
}

int main ()
{
A* a=new A;
std::cout<< a->Print() <<std::endl;
return 0;
}

override

在上述过程中virtual的使用中基本可以排除掉复写函数的影响,但是为了增加代码的可读性和维护性,我们还可以使用override关键字,它可以明确标注出你的函数是否是复写的,并且会检查你的复写函数是否与原函数同名字,也就是你的复写函数名字是否正确,有纠错的能力,与此同时如果没有virtual关键字它同样也会报错,当然它的存在与否不影响程序的运行,即使不加也不会影响程序的正常运行。但是在虚函数使用中子类的函数重写必须加上override

使用方法如下

1
返回值类型 函数() override {}

举例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//依旧使用上述的class类举例

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

class Example2 : public Example1
{
private:
std::string m_Name;
public:
Example2(const std::string& name)
:m_Name(name)
{}
std::string GetName() override {return m_Name;}//override只能放在小括号后,大括号前
};

资料参考

YouTube上Thecherno的cpp系列