C++|多态
🌸心有所向,日复一日,必有精进
🌸专栏《C++修炼秘籍》
🌸作者:早凉
目录
多态概念
多态(英语:polymorphism)指为不同的实体提供统一的接口。多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上。
多态的定义和实现
多态的条件
- 必须通过基类的指针或者引用调用虚函数
- 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
虚函数
虚函数:即被virtual修饰的类成员函数称为虚函数
虚函数重写(覆盖)
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person{
public:
virtual void BuyTicket(){ cout << "全价购票" << endl; }
};
class Student :Person{
public:
virtual void BuyTicket(){ cout << "学生票" << endl; }
};
上述就是完成了对BuyTicket()的重写 (覆盖);
❗️ 注意:
- 1、子类虚函数重写可不加virtual,也将完成虚函数重写,基类必须加;
class Person{
public:
virtual void BuyTicket(){ cout << "全价购票" << endl; }
};
class Student :Person{
public:
void BuyTicket(){ cout << "学生票" << endl; }
};
- 2、协变(基类与派生类虚函数返回值类型不同)
当基类与派生类虚函数返回值类型不同,但是要求返回值必须是一个父子类关系的指针或引用;
- 3、析构函数的重写
当基类析构函数如果不写成虚函数会出现一定问题;
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;
return 0;
}
当基类指针指向开辟派生类的对象时,会发生切片,但是当发生析构时只能调用基类的析构函数,导致发生内存泄漏;但是函数重写的定义是返回值类型、函数名字、参数列表完全相同,但是
析构函数函数名字都不相同啊!
class Person{
public:
virtual ~Person(){ cout << "Person::析构函数" << endl; }
};
class Student : public Person{
public:
virtual ~Student(){ cout << "Student::析构函数" << endl; }
};
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor
普通调用:根据调用对象相关
class Person{
public:
//虚函数的重写/覆盖
void BuyTicket(){ cout << "全价购票" << endl; }
};
class Student : public Person{
public:
void BuyTicket(){ cout << "学生票" << endl; }
};
void Fun(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
Fun(p);
Fun(s);
return 0;
}
class Person{
public:
//虚函数的重写/覆盖
virtual void BuyTicket(){ cout << "全价购票" << endl; }
};
class Student : public Person{
public:
virtual void BuyTicket(){ cout << "学生票" << endl; }
};
void Fun(Person p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
Fun(p);
Fun(s);
return 0;
}
多态调用:根据指针或引用指向的对象有关;
class Person{
public:
//虚函数的重写/覆盖
virtual void BuyTicket(){ cout << "全价购票" << endl; }
};
class Student : public Person{
public:
virtual void BuyTicket(){ cout << "学生票" << endl; }
};
void Fun(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p;
Student s;
Fun(p);
Fun(s);
return 0;
}
多态具体原理
虚函数表:
class A
{
public:
void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
A b;
cout << sizeof A << endl;
return 0;
}
打印结果为:4(意料之中)
class A
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
int main()
{
A b;
cout << sizeof A << endl;
return 0;
}
打印为:8(出乎意料)(也可能是16,在32位下为8,64位下为16)
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,
class A
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
virtual void Func2()
{
cout << "Func2()" << endl;
}
void Func3()
{
cout << "Func3()" << endl;
}
private:
int _b = 1;
};
class B : public A
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
void Func3()
{
cout << "Func3()" << endl;
}
void Func4()
{
cout << "Func4()" << endl;
}
private:
int _c = 1;
};
int main()
{
A b;
B c;
return 0;
}
派生类对象c中也有一个虚表指针,c对象由两部分构成,一部分是基类继承下来的成员,虚
表指针也就是存在部分的另一部分是自己的成员。
基类b对象和派生类c对象虚表是不一样的,这里我们发现Func1完成了重写,所以c的虚表
中存的是重写的A::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数
的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
不是虚函数,所以不会放进虚表;
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
❗️ 注意:
虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是
他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
多态具体原理
通过上图发现,当基类引用分别指向基类对象和子类对象时形成多态,是分别根据所存储的虚基表来寻找的;
当想实现多态要遵守两个条件:一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。
当虚函数覆盖其实就是覆盖了虚表,通过虚表来完成函数查找,实现多态;
还有就是只有基类才能被基类和派生类赋值,这种赋值是天然的,普通赋值发生截断或者叫切片,
而当基类引用或指针引用或指向派生类,能访问父类部分,这样其实也就相当于实现了如果在多态条件下,调用函数出现相对应的不同的效果;
动态绑定和静态绑定
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
通过如下汇编:
当实现多态后是通过一系列的操作,拿到虚表中存放虚函数的地址来调用函数;而下方是没有实现多态的调用,在编译期间已经从符号表找到了函数地址
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实
现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
总结
- 什么是多态?
- 什么是重载、重写(覆盖)、重定义(隐藏)?
- 多态的实现原理?
- inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去。
- 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
- 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
- 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。参考本节课件内容
- 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
- 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
- C++菱形继承的问题?虚继承的原理?注意这里不要把虚函数表和虚基表搞混了。
- 什么是抽象类?抽象类的作用?
这些问题都掌握了,那么多态也就非常OK了