什么是多态
多态,顾名思义,多种形态。
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象买票半价
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
//虚函数重写的两个例外:
//1. 协变(基类与派生类虚函数返回值类型不同)
//派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
//针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。(了解)
//2. 析构函数的重写(基类与派生类析构函数的名字不同)
//如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
//都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
//看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
//理,编译后析构函数的名称统一处理成destructor。
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
原理图
普通继承是子类继承父类的全部,是一种复用。
虚函数在多态情况下是子类复用父类的接口,重写父类的实现。
多态条件
举例(ps:下面就是通过修改多态的条件从而不满足多态)
void Func(Person p)
{
p.BuyTicket();
}
要求三同,假设我们这里让参数不同:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket(int ) { cout << "买票-半价" << endl; }
};
重写虚函数
Person* p=new Person;
delete p;
p = new Student;
delete p;
我们发现子类的析构函数没有被正确调用。
这是因为delete是根据指针类型来调用的。
但是person指针可能指向person,也可能指向student。
它的调用如下:先调用destrutor函数再调用析构函数。 destrutor是普通函数,普通函数调用看类型,因为是person指针类型,所以调用person析构。
我们不希望它是普通继承,因为如果是普通继承就不会调子类析构,会造成内存泄漏。
那我们怎么样让它变多态呢?第一:三同(参数相同,函数名相同,返回值相同)二:虚函数重写
~Person() ~Student()参数没有,返回值没有,函数名不同,但是为了满足多态进行了特殊处理。
现在只差虚函数重写这一项了,我们给析构加上virtual:
virtual ~Person()
{
cout << "~Person()" << endl;
}
};
virtual ~Student()
{
cout << "~Student()" << endl;
}
练习题
说出如下代码打印结果:
class Person
{
public:
virtual void func(int a = 1) { cout << "A->" << a << endl; }
virtual void test() { func(); }
};
class Student: public Person
{
void func(int a = 0) {cout << "B->" << a << endl;}
};
int main()
{
Student* B=new Student;
B->test();
return 0;
}
结果:
B->1
解析:
那如果是这样呢?我不访问test(),我直接访问func*()
class Person
{
public:
virtual void func(int a = 1) { cout << "A->" << a << endl; }
virtual void test() { func(); }
};
class Student: public Person
{
public:
void func(int a = 0) {cout << "B->" << a << endl;}
};
int main()
{
Student* B=new Student;
B->func();
return 0;
}
答案很明显,不构成多态,因为多态需要父类指针,而下面是子类的指针,不构成多台那就是普通的,普通的话就会隐藏,会访问子类的。
要想访问父类我们指定父类域就可以了:
#include <iostream>
using namespace std;
class Person
{
public:
virtual void func(int a = 1) { cout << "A->" << a << endl; }
virtual void test() { func(); }
};
class Student : public Person
{
public:
void func(int a = 0) {
Person::func(1); // 调用基类Person的func函数
cout << "B->" << a << endl;
}
};
int main()
{
Student* B = new Student;
B->Person::func();
return 0;
}
要想形成多态我们改为父类指针就行了:
class Person
{
public:
virtual void func(int a = 1) { cout << "A->" << a << endl; }
virtual void test() { func(); }
};
class Student: public Person
{
public:
void func(int a = 0) {cout << "B->" << a << endl;}
};
int main()
{
Person* B = new Person;
B=new Student;
B->func();
return 0;
}
抽象类
虚函数+“=0”就是纯虚函数包含纯虚函数的类就叫抽象类,抽象类不能实例化:
class A
{
public:
virtual void fun() = 0;
};
那抽象类要怎么才能实例化呢?不能实例化这个类就没有意义,实例化要通过虚函数重写:
如下,指针指向谁就访问谁
class A
{
public:
virtual void fun() = 0;
};
class B: public A
{
void fun()
{
cout << "B->舒适" << endl;
}
};
class C : public A
{
void fun()
{
cout << "B->巴适" << endl;
}
};
void fun(A* p)
{
p->fun();
}
int main()
{
fun(new B);
fun(new C);
return 0;
}
虚函数表
如下代码求出来的字节大小为多少:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
void BuyTicket() { cout << "买票-半价" << endl; }
private:
int _a;
};
int main()
{
cout << sizeof(Student) << endl;
return 0;
}
答案:
原因,只要有虚函数,大小就要把多算一个虚函数指针的大小,如果没有虚函数,那它的大小就为它的成员int_a的大小,就是4:
虚函数指针存放在 虚函数表里面。虚函数表我们称为vf
ptr(virtaul function ptr table ),而vfptr在代码段里存放着。
现在在子类中再加个char _b成员变量,然后在64位操作系统下运行,此刻的大小为多少呢?
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
void BuyTicket() { cout << "买票-半价" << endl; }
private:
int _a;
char _b;
};
int main()
{
Student b;
cout << sizeof(Student) << endl;
return 0;
}
答案:
因为64位操作系统按8进行内存对齐。
如下,如果fun1,fun2是虚函数,fun3不是虚函数,那么fun3就不在vfptr里面。
虚函数实际上在代码段里面,只是把地址放在vfptr里面而已。
class Student : public Person {
public:
virtual void fun1() { cout << "fun1()" << endl; }
virtual void fun2(){ cout << "fun2()" << endl; }
void fun3(){ cout << "fun3()" << endl; }
};
int main()
{
Student b;
return 0;
}
多态是怎么来的
观察下面地址和代码,我们发现子类只是把父类的BuyTicket()函数重写了,func()函数没有重写,还是同一个func()。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
virtual void fun(){}
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
调用BuyTick()函数的时候它也不知道调父类的BuyTick()函数还是子类的BuyTick()函数,但是它会去调vfptr,如果调父类就通过虚函数表去调父类的BuyTick(),如果调子类就对父类的vfptr切片,仍然通过vfptr调BuyTick(),不过此时BuyTick()已经是子类的BuyTick()了。
多态的实现原理
【C++面试题】请简述多态实现的原理_哔哩哔哩_bilibili
多态分为静态多态和动态多态
静态多态
静态就是在编译期间确定的。
表现形式:函数重载。
函数重载:运行在同一个作用域内声明多个功能相似的同名函数。
我们不能通过函数返回值类型来确定函数重载。那么我们可以通过什么来确定函数重载呢?
1.参数类型
2.参数个数
函数重载是怎么实现的呢?是利用函数名修饰来实现的。
函数名修饰实现过程:
编译过程:
预编译:把函数声明拷贝到源文件,避免编译过程中找不到函数定义。
编译阶段:语法分析,进行函数汇总。
汇编阶段:生成函数名到函数地址的映射,方便之后通过函数名找到函数定义的位置,从而执行函数。
链接阶段:将多个文件中的符号表汇总合并。
例1
写两个关于sum函数的重载:
#include<iostream>
class A
{
public:
int sum(int a,int b)
{
return a+b;
}
double sum(double a,double b)
{
return a+b;
}
};
int main()
{
A a;
a.sum(1,2);
a.sum(1.0,2.0);
return 0;
}
首先编译一下,生成目标文件:
g++ test.cc -o test.o
然后输入objdump -t命令:
objdeump -t test.o
我们可以在符号表里找到我们的int sum(int a,int b),double sum(double a,double b)函数,发现它们的函数名分别被修饰为了_ZN1A3sumEii和 _ZN1A3sumEdd:
函数名修饰规则:
_zn+类名长度+类名+函数名长度+函数名+E+参数类型首字母
动态多态
动态是在运行期间确定的。
它的表现方式为虚函数重写。
1.首先在基类的函数前加上virtual关键字,然后在派生类中重写该函数。
2.运行时会根据对象类型来调用对应的函数。如果是基类类型就调用基类的函数,否之调用子类的。
早绑定和晚绑定
在编译期间已经确定了对象调用的函数地址,这就是早绑定,例如函数名重载。在运行期间根据对象类型来确定调用哪个函数的就是晚绑定,例如虚函数重载。
晚绑定,若使用virtual函数,则会为类生成虚函数表,虚函数表存放了虚函数地址,类对象构造时会初始化该虚函数表指针。
例2
#include<iostream>
class Base
{
public:
void func()
{
std::cout<<"Base func()"<<std::endl;
}
};
class Drive:public Base
{
public:
void func()
{
std::cout<<"Drive func()"<<std::endl;
}
};
int main()
{
Base *p1=new(Base);
Base *p2=new(Drive);
p1->func();
p2->func();
return 0;
}
我们可以看到运行结果是全调用的基类的func()。这是因为我们在声明类对象时声明了其类型为基类类型,那么在编译的时候就会调用基类的函数:
例3
我们可以给基类func()函数加上virtual关键字:
class Base
{
public:
virtual void func()
{
std::cout<<"Base func()"<<std::endl;
}
};
实现原理
首先,我们可以取出虚函数表指针。因为这个虚函数表实际上就是一个一维数组,我们访问首元素:
#include<iostream>
class Base
{
public:
virtual void func()
{
std::cout<<"Base func()"<<std::endl;
}
};
class Drive:public Base
{
public:
void func()
{
std::cout<<"Drive func()"<<std::endl;
}
};
int main()
{
typedef void (*Func)(void);
Base *p1=new(Base);
Base *p2=new(Drive);
long* vptr1=(long*)*(long*)p1;
Func f1=(Func)vptr1[0];
f1();
long* vptr2=(long*)*(long*)p2;
Func f2=(Func)vptr2[0];
f2();
// p1->func();
// p2->func();
return 0;
}