携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第9天,点击查看活动详情
💦 虚函数
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
- 虚函数就是被 virtual 修饰的类成员函数,它跟虚继承共用了一个关键字 virtual。
- 注意虚继承和虚函数中的 virtual,并没有关联关系,就像取地址和引用没有半毛钱关系,并不是天下姓王的都是亲戚。
💦 虚函数的重写
//重写(覆盖)
class Person
{
public:
virtual void BuyTicket()
{
cout << "正常排队-全价买票" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "正常排队-半价买票" << endl;
}
};
//隐藏(重定义)
class A
{
public:
void fun()
{
cout << "fun()" << endl;
}
};
class B : public A
{
public:
void fun(int i)
{
cout << "fun(int i)" << endl;
}
};
-
构成多态的条件之一是虚函数的重写,而虚函数也有自己的规则,虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数,即派生类虚函数和基类虚函数的返回值类型、函数名、参数列表完全相同,就称子类的虚函数重写了基类的虚函数。
注意区分隐藏的概念,隐藏是只要基类函数名和派生类函数名相同即是隐藏或重定义。
-
虚函数要求三同,但是这三同有些例外,这就恶心了,具体例外看测试用例三四五。
✔ 测试用例一:
#include<iostream>
using namespace std;
//class A {};//AB为无关联的类
//class B {};
class A {};//AB为关联的父子类
class B : public A {};
class Person
{
public:
virtual A* BuyTicket()
{
cout << "正常排队-全价买票" << endl;
return new A;
}
protected:
int _age;
string _name;
};
class Student : public Person
{
public:
virtual B* BuyTicket()
{
cout << "正常排队-半价买票" << endl;
return new B;
}
protected:
//...
};
void Func(Person& ptr)
{
ptr.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
-
协变 (基类与派生类虚函数返回值类型不同),即重写的虚函数可以不同,但是返回值必须是父子类型指针或引用。
如果返回值是普通没有关联的类,那么它既不满足三同、也不满足协变,会编译报错。
如果返回值是有关联的父子类,那么虽然它不满足三同,但是它满足协变这个例外,所以能构成多态。
✔ 测试用例二:
#include<iostream>
using namespace std;
class Person
{
public:
//~Person()
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
//~Student()
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
//普通场景
Person p;
Student s;
//new对象的特殊场景
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;//p1->destructor() + operator delete(p1)
delete p2;//p2->destructor() + operator delete(p2)
return 0;
}
-
析构函数的重写 (基类与派生类析构函数的名字不同)
如果是普通的析构, 程序运行没有问题,这里生命周期结束,s 后定义,s 先析构,s 中分为为两个部分,先调用自己的析构,再去调用继承的父类的析构,随后再去调用 p 的析构;如果是虚函数的析构,可以看到结果同普通的析构。
虚函数的析构有什么意义 ❓
普通场景下,虚函数是否重写都是 ok 的;new 对象的特殊场景下,Person 的指针 p1 指向 Person 的对象、Person 的指针 p2 指向 Student 的对象、delete Person 的对象、delete Student 的对象。这里 new Person 调用 Person 的构造函数、new Student 调用 Studnet 的构造函数 + Person 的构造函数都没有问题;这里 delete p1 期望的是 delete 调用 Person 的析构函数、delete p2 调用 Student 的析构函数 + Person 的析构函数,但是在继承中我们说过,在子类中要去显示的调用父类的析构函数,需要指定作用域,因为所有类的析构函数名都被处理成了 destructor(),所以子类和父类的析构函数构成隐藏关系。为什么它要对析构函数名作单独处理呢,因为如果这里不构成多态,调用时看的是指针的类型,那么这里 p1 和 p2 调用的都是 Person 的析构函数,此时就不对了。p1 没问题,但是 p2 指向的是一个子类对象,子类对象应该先调用子类的析构函数,再去调用父类的析构函数,万一子类对象中又去 delete,那么 Student 的析构函数没调到就有可能会出现资源泄漏。
所以这里 delete p1/p2 是想达到多态的场景,Person* 指向父类调父类,指向子类调用子类,上面已经满足多态的条件之一,通过基类的指针或者引用调用虚函数;但是并没有满足多态的条件之二,被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写;要完成虚函数的重写有两个条件:它们必须是虚函数以及三同,析构函数没有返回值,也就不考虑协变了。这里的两个析构函数没有返回值、参数,函数名不相同,因为在这种场景下需要多态,所以编译器对它们进行了特殊处理,统一成 destructor(),所以这里我们对于这种场景是需要加上 virtual 的,所以 delete p1 指向父类,调用父类的虚函数,delete p2 指向子类,调用子类的虚函数,子类析构函数结束后,再调用父类的虚函数。
✔ 测试用例三:
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:
virtual void BuyTicket()
{
cout << "正常排队-全价买票" << endl;
}
protected:
int _age;
string _name;
};
class Student : public Person
{
public:
void BuyTicket()//可以不加virtual
{
cout << "正常排队-半价买票" << endl;
}
protected:
//...
};
void Func(Person* ptr)
{
ptr->BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(&ps);
Func(&st);
return 0;
}
-
其实严格来说这里还有一个例外,子类中的重写函数可以不加 virtual,但是通常不建议这样做。
为什么子类重写时可以不加 virtual ❓
因为它的理解是认为你是先继承下来的,我是在重写你,继承后你都有虚函数属性了,我去重写你,加与不加都无所谓。主要的实用场景还是测试用例四中的问题, 如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加 virtual 关键字,都与基类的析构函数构成重写,虽然基类与派生类的析构函数名不同,看起来违背了重写的规则,其实编译器对析构函数名统一处理成了 destructor()。也就是说如果支持子类不加虚函数也构成重写的话,那么只要父类中析构函数是虚函数,析构函数就一定构成重写,之后的问题就不存在了。
这种例外,无疑是让语法变的更重了,C++ 经常爱搞这种东西,已经见怪不怪了。
💦 静态多态和动态多态
有些书籍会把多态进行细分:
- 静态多态是函数重载,调用一个函数,传不同的参数,就有不同的行为。
- 动态的多态是调用一个虚函数,不同的对象去调用,就有不同的行为。