目录
多态的定义及实现
1、什么是多态?
当不同的对象去完成某个行为时,会产生出不同的结果。多态是:不同继承关系的类对象去调用同一函数时,产生了不同的行为。
例如:Student类继承了Person类。 Person对象买票全价,Student对象买票半价。这就是多态行为。
2、构成多态的两个必要条件
- 调用函数的对象必须是指针或者引用。 若不用指针或引用,则传一个对象给基类类型(切片),父类将自己的虚表改为子类的虚表,不合理。
- 被调用的函数必须是虚函数,且完成了虚函数的重写。
虚函数:在类的成员函数前面加关键字virtual。
虚函数的重写:派生类中有一个跟基类的完全相同虚函数,他们的函数名、参数、返回值都相同,我们就称子类的虚函数重写了基类的虚函数。另外虚函数的重写也叫作虚函数的覆盖。
实现一个简单的多态例子:
class Person
{
public:
virtual void BuyTicket()
{
cout << "买票全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "半价买票" << endl;
}
};
void Func(Person& people)
{
people.BuyTicket();
}
void Test()
{
Person p;
Func(p);
Student s;
Func(s);
}
3、重写
虚函数的重写中,派生类中重写的成员函数可以不加virtual关键字,因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性,我们只是重写了它。但这是非常不规范的,我们平时不要这样使用。
虚函数重写有一个例外:重写的虚函数的返回值可以不同,但必须分别是基类指针和派生类指针或者基类引用和派生类引用。这种行为叫做 协变。
//协变举例
class A{};
class B : public A{};
class Person
{
public:
virtual A* f()
{
return new A;
}
};
class Student : public Person
{
public:
virtual B* f()
{
return new B;
}
};
析构函数的重写问题:若基类的析构函数被写成了虚函数,那么继承下来的派生类中是否重写了析构函数?这里他们看起来函数名不相同,违背了重写的规则,但其实可以理解为编译器对析构函数的重写进行了特殊处理,编译后析构函数的名称统一处理成destructor。这也说明了基类的析构函数最好写成虚函数。
class Person
{
public:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
virtual ~Student()
{
cout << "~Student()" << endl;
}
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main() {
Person* p1 = new Person;
Person* p2 = new Student;
delete p1; //这里希望通过对象调析构,所以用虚函数
delete p2;
//结果为:
// ~Person()
// ~Student()
return 0;
}
为什么将析构函数写成虚函数?如果析构函数不使用virtual,使用动态绑定,则在析构的时候就会忽略掉派生类的部分。若我们在派生类中进行了空间的开辟,而在派生类的析构中对其进行释放,如过不调用派生类析构,会造成内存泄漏。
//基类的析构函数不是虚函数时,两个析构函数没有构成多态.
//在析构的时候,是根据类型析构,而不是根据对象析构,忽略了派生类的部分,会造成内存泄漏。
class Person
{
public:
~Person()
{
cout << "~Person()" << endl;
}
};
class Student : public Person
{
public:
~Student()
{
cout << "~Student()" << endl;
}
};
int main() {
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
//结果为:
// ~Person()
// ~Person()
//只对基类不部分进行了析构,而并没有对派生类部分进行析构
return 0;
}
4、接口继承和实现继承
实现继承:普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
接口继承:虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
继承达到的目的就是实现继承,多态就是接口继承。
5、重载、重写(覆盖)、重定义(隐藏)的对比
重载:
- 两个函数在同一个作用域中。
- 函数名相同,参数个数或类型不同。
重写(覆盖):
- 两个函数在两个不同的作用域中。
- 函数名,参数,返回值都相同(协变除外)。
- 两个函数都必须是虚函数。
重定义(隐藏):
- 两个函数在两个不同的作用域中。
- 函数名相同。
- 上述条件成立后,如果不是重写就一定构成了隐藏。
抽象类
纯虚函数:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。只对函数进行声明,不实现。在派生类中才对于函数进行实现。
纯虚函数的目的:强制重写虚函数。
抽象类:包含纯虚函数的类叫做抽象类(也叫接口类)。
- 抽象类不能实例化出对象,派生类继承后也不能实例化出对象,
- 当派生类中将继承的抽象类的纯虚函数都重写实现了,才可以实例化出对象。
- 更体现出了接口继承。
class A
{
public:
virtual void Func() = 0; //只声明,不实现
};
class A1 : public A
{
public:
virtual void Func() //继承后实现
{
cout << "A1" << endl;
}
};
class A2 : public A
{
public:
virtual void Func() //继承后实现
{
cout << "A2" << endl;
}
};
int main()
{
A* a1 = new A1;
A* a2 = new A2;
a1->Func();
a2->Func();
return 0;
}
C++11还提供了override 和 final 来修饰虚函数:
override:
虚函数的意义就是实现多态,如果没有重写,虚函数就没有意义。所以C++11中使用了 纯虚函数 + override 的方式来强制重写虚函数。
override 修饰的派生类虚函数没有重写会编译报错。
final:
final 修饰基类的虚函数不能被派生类重写 。
多态的原理
1、虚函数表(虚表)
虚函数表本质是一个存放 虚函数指针 的指针数组,这个数组最后面放了一个nullptr。虚函数指针就是类中虚函数的地址,这些虚函数的地址存放在这个指针数组中。
类对象中有着一个隐藏的成员指针_vfptr,我们叫做虚函数表指针,这个指针指向虚函数表。
派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
虚函数存在哪?
虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。
虚表存在哪?
在vs下验证后发现虚表存在代码段。
虚函数表指针存在哪?
虚函数指针是存放在对象中,所以虚函数指针的位置是跟着对象的位置走的对象在栈上被创建,虚函数指针存在于栈上;对象被创建在堆上,虚函数指针就存在于堆上。
2、多态的原理
实际上的多态就是不同的对象,在调用时查找其虚函数表,找到要调用的函数。
在派生类中,派生类的虚函数表已完成了重写,所以尽管调用的是同一个函数,但虚表却不同,完成的是不同的动作,展现出不同的形态。
满足多态后的函数调用,不是在编译时确定的,是运行起来以后到对象的中去找的。不满足多态的函数调用,是编译时确认好的。
3、动态绑定与静态绑定
1) 静态绑定(前期绑定/早绑定):在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2) 动态绑定(后期绑定/晚绑定):是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
单继承和多继承关系的虚函数表
1、单继承下的虚函数表
- 派生类的虚函数表先存放拷贝自基类的虚函数表,并用重写的函数将基类对应的函数覆盖。在表的后面,按派生类自己的声明顺序,加入自己的虚函数地址。
- 在vs的编译器下监视的窗口中在虚函数表中无法看到派生类自己的虚函数,可以通过打印虚函数表看到。
- 虚函数表指针存放在对象的前四个字节,以nullptr结尾,相当于一个函数指针数组 。
2、多继承下的虚函数表
- 多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中