(1)基本条件
多态有两个必要的构成条件:
- 必须通过基类的指针或者引用调用虚函数 (不能用对象直接调用)
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
(2)虚函数
被 virtual
修饰的类成员函数称为虚函数
- virtual关键字只在声明时加上,在类外实现时不能加
- 友元函数不属于成员函数,不能成为虚函数
- 静态成员函数就不能设置为虚函数。没有this指针无法拿到虚表,就无法实现多态,因此不能设置为虚函数
- 只有在需要实现多态时,才需要将成员函数设置成虚函数,否则没有必要
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
(3)重写
重写也叫覆盖。当派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同) ,我们称子类的虚函数重写了父类的虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
}
[特例]:
派生类的虚函数不加virtual
关键字时仍然可以构成重写(不建议这样写)
[原因]:
普通函数的继承是一种实现继承,即派生类继承了基类函数的具体实现。而虚函数的继承是一种接口继承,派生类继承的是基类虚函数的
接口
,目的是为了重写父类虚函数的具体实现。
派生类继承了基类的函数接口声明,与父类虚函数具有相同的属性,所以自动带有虚函数的属性。
[面试题]:下列程序输出的输出结果是 B→1
class A
{
public:
virtual void func(int val = 1)
{
std::cout<<"A->"<< val <<std::endl;
}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val = 0)
{
std::cout<<"B->"<< val <<std::endl;
}
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
[分析]:
- p 调用 test() 不符合多态的构成条件,因此只是对父类函数的继承。
- test() 函数里对 func() 函数的调用,实际上是
(&A)→func()
,因此满足多态的两个构造条件——即通过基类的指针调用虚函数,并且派生类构成重写。- 重写只是对父类虚函数具体实现的重写,但仍然保留父类虚函数的属性。因此子类 func 函数仍然是虚函数,且缺省值仍然是父类虚函数的缺省值。
(4)虚函数重写的两个例外
-
协变(基类与派生类虚函数返回值类型不同)
返回值为父子类的指针或引用时,称为协变。
class Person { public: virtual Person* Person() { cout << "~Person()" << endl; return this; } }; class Student : public Person { public: virtual Student* Student() { cout << "~Student()" << endl; return this; } };
-
析构函数的重写(基类与派生类析构函数的名字不同)
编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。为什么要这样处理呢?我们来看下面的这个例子 :
class Person{ public: ~Person() {cout << "~Person()" << endl;} }; class Student:public Person{ public: ~Student() {cout << "~Student()" << endl;} }; int main() { Person* ptr = new Person(); delete ptr; ptr = new Student(); delete ptr; return 0; }
[分析]:
正常情况下,调用的析构函数类型取决于变量或指针的类型,但是这样可能存在内存泄漏的风险,正如上面这个例子。所以我们期待实现析构函数的多态调用。 为了实现多态调用,派生类的虚函数必须与基类的虚函数名字相同。所以编译后,将析构函数的名称统一处理成destructor。