心有所向,日复一日,必有精进
专栏《C++修炼秘籍》
作者:早凉
目录
C++|多态
多态概念
多态的定义和实现
多态的条件
虚函数
虚函数重写(覆盖)
多态具体原理
虚函数表:
多态具体原理
动态绑定和静态绑定
抽象类
总结
多态(英语:polymorphism)指为不同的实体提供统一的接口。多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上。
虚函数:即被virtual修饰的类成员函数称为虚函数
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
class Person{
public:
virtual void BuyTicket(){ cout << "全价购票" << endl; }
};
class Student :Person{
public:
virtual void BuyTicket(){ cout << "学生票" << endl; }
};
上述就是完成了对BuyTicket()的重写 (覆盖);
❗️ 注意:
class Person{
public:
virtual void BuyTicket(){ cout << "全价购票" << endl; }
};
class Student :Person{
public:
void BuyTicket(){ cout << "学生票" << endl; }
};
当基类与派生类虚函数返回值类型不同,但是要求返回值必须是一个父子类关系的指针或引用;
当基类析构函数如果不写成虚函数会出现一定问题;
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 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生
类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实
现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
这些问题都掌握了,那么多态也就非常OK了