1.什么是virtual
virtual是一个C++的修饰符,用于修饰函数,被修饰的函数称为虚函数。
2.为什么需要?
在C++中,我们都知道要实现多态有2种方法,一种是重写,一种是重载,重载是横向的,意思是只发生在同一个类中,根据函数的参数个数,类型不同从而实现重载,而重写则是纵向的,发生在继承中,子类函数覆盖父类函数,父类指针指向子类实体时,应该实现运行时多态。
3.通常用在什么情形?
1.作为基类的类的析构函数
如果一个类做为父类,然后它被别人继承,当用子类指针指向子类时不会出现任何的问题,但是如果用父类指针指向子类时,若没有加析构函数,那只会析构父类的析构函数,这时我们需要用virtual修饰父类的析构函数。
2.需要实现多态的函数
若一个函数需要实现多态,即运行时多态。
1、定义在函数中的对象,在函数调用结束时,在对象释放前调用析构函数
2、定义在函数中的static对象,在函数调用结束时,不调用对象析构函数,在mian函数结束时,会调用对象析构函数
3、定义全局对象或者static全局对象,程序执行流程离开其作用域时,调用对象析构函数
4、用new运算法生成的对象,调用delete运算法释放该对象时,先调用对象析构函数
重写 覆盖的几个函数必须函数名、参数、返回值都相同;
case 1
#include <iostream>
using namespace std;
class Base{
public:
~Base() {cout<<"~B"<<endl;}
};
class Derived:public Base{
public:
~Derived() {cout<<"~D"<<endl;}
};
int main (){
Base *b = new Derived; //注意这里
delete b;
}
子类指向父类,查看打印日志:
~B
可见删除子类指向父类的指针,代表delete只删除了父类,并没有删除子类的相关内存,会产生内存泄漏。所以在多态的使用过程中,delete(指向父类的子类对象)需要格外注意是否会没有释放子类或者父类。
case 2
#include <iostream>
using namespace std;
class Base{
public:
~Base() {cout<<"~B"<<endl;}
};
class Derived:public Base{
public:
~Derived() {cout<<"~D"<<endl;}
};
int main (){
Derived *d = new Derived; //注意这里
delete d;
}
子类指向子类指针,查看打印日志:
~D
~B
可以得知,delete子类的析构顺序:是先释放子类,在释放父类。
这是我们期望的结果,所以没啥问题。。。
case 3
对于case 1当中的情况,如何去规避呢? 需要将父类的析构函数定义为virtual
#include <iostream>
using namespace std;
class Base{
public:
virtual ~Base() {cout<<"~B"<<endl;}
};
class Derived:public Base{
public:
~Derived() {cout<<"~D"<<endl;}
};
int main (){
Base *d = new Derived; //注意这里
delete d;
}
打印结果:
~D
~B
原理:delete d; //d虽然为Base*,但是它的析构函数是virtual的,所以它调用析构函数,其实是调用虚函数表中的函数。而Base* d指向的虚函数表在内存中其实是new Derived类的虚函数表。所以通过Base类的virtual函数 + Base* d指向派生类(Derived)对象,从而使得在调用delete d的时候实际上是调用的是delete Derived* d的意思,也就是第二种情况的效果。
4.延伸:虚函数/纯虚函数、override 关键字
虚函数分类:
- 纯虚函数要求派生类必须重写
- 普通虚函数有默认实现但可以被派生类重写
使用override和default关键字可以明确指示函数的重写和默认实现。
示例
virtual void Draw() const = 0; // 1) 纯虚函数
virtual void Error(const string& msg); // 2) 普通虚函数
virtual void Error(const string& msg) override = default; // 错误,= default只能用在默认构造、拷贝构造等默认函数中
virtual void Error(const string& msg) override;
9.问题汇总
9.1 非虚函数和虚函数都可以重写,那区别是啥?
virtual void Error(const string& msg);
void Error(const string& msg);
对于上述两个函数声明:
-
virtual void Error(const string& msg);
是一个虚函数的声明。虚函数是在基类中声明并带有 virtual
关键字的函数,它可以被派生类重写(覆盖)。虚函数可以通过基类指针或引用来实现动态绑定,即在运行时根据对象的实际类型来调用相应的函数。
-
void Error(const string& msg);
是一个非虚函数的声明。非虚函数是在类中声明但没有 virtual
关键字修饰的函数。非虚函数在派生类中也可以被重写,但它们不具备动态绑定的特性。当通过基类指针或引用调用非虚函数时,将根据指针或引用的类型来确定调用的函数,而不考虑指向的对象的实际类型。
因此,虚函数和非虚函数的区别在于它们的调用方式和多态性的支持。虚函数可以通过基类指针或引用实现多态性,而非虚函数的调用始终与指针或引用的类型相符,不会发生动态绑定。
9.2 基类虚函数/纯虚函数、子类有没有 override 的区别
override 的作用:用于显式地标识子类中的函数重写父类的虚函数(包括纯虚函数)的一种方式。
使用 override
关键字有以下几个好处:
-
明确表明意图:使用 override
关键字可以明确表示子类中的函数是对父类虚函数的重写,可以增强代码的可读性和可维护性。
-
静态检查错误:使用 override
关键字可以让编译器在子类中发现对父类虚函数的重写错误,例如函数签名不匹配等。
-
避免意外创建新函数:如果没有使用 override
关键字,子类可能会意外地创建一个新的函数,而不是重写父类的虚函数。
虽然使用 override
关键字可以提高代码的可读性和安全性,但在某些情况下,如果没有使用 override
关键字,编译器仍然可以进行正确的函数匹配。所以使用 override
关键字是一种良好的编程实践,但不是强制要求。
注意:如果基类不是虚函数,则无法使用override修饰。
9.2 override 和 override = default
在C++中,只有特殊成员函数(默认构造函数、拷贝构造函数、移动构造函数、析构函数和赋值运算符)才能使用 = default;
语法进行默认。普通的成员函数不能使用 = default;
进行默认。
class Shape {
public:
Shape(){}
virtual ~Shape(){ cout << "Parent class ~Shape :" <<endl; }
};
class Rectangle: public Shape{
public:
Rectangle(){ }
virtual ~Rectangle() override = default;
10.综合实例
#include <iostream>
using namespace std;
class Shape {
protected:
int width, height;
public:
Shape( int a=0, int b=0)
{
width = a;
height = b;
}
virtual ~Shape(){ cout << "Parent class ~Shape :" <<endl; }
int area()
{
cout << "Parent class area :" <<endl;
return 0;
}
virtual void test() // 普通虚函数
{
cout << "Parent class test :" <<endl;
}
virtual void test2() = 0; // 纯虚函数:子类必须实现
};
class Rectangle: public Shape{
public:
Rectangle( int a=0, int b=0):Shape(a, b) { }
virtual ~Rectangle() override = default;
int area ()
{
cout << "Rectangle class area :" <<endl;
return (width * height);
}
void test() override
{
cout << "Rectangle class test :" <<endl;
}
void test2() override
{
cout << "Rectangle class test2 :" <<endl;
}
};
class Triangle: public Shape{
public:
Triangle( int a=0, int b=0):Shape(a, b) { }
virtual ~Triangle(){ cout << "Parent class ~Triangle :" <<endl; }
int area ()
{
cout << "Triangle class area :" <<endl;
return (width * height / 2);
}
void test() //可以不写override,但是写了一目了然
{
cout << "Triangle class test :" <<endl;
}
void test2() //可以不写override,但是写了一目了然
{
cout << "Triangle class test2 :" <<endl;
}
};
// 程序的主函数
int main( )
{
Shape *shape;
Rectangle rec(10,7);
Triangle tri(10,5);
//普通函数:基类、子类:按类型
shape = &rec;
shape->area();//基类
shape = &tri;
shape->area();//基类
//虚函数
shape = &rec;
shape->test();//子类
shape = &tri;
shape->test();//子类
shape->Shape::test();//指定父类
//纯虚函数约定的是子类必须实现、override约定的是重写基类
return 0;
}