多态
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种借口继承,派生类继承的是基类函数的借口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
区别**重载,覆盖(重写),隐藏(重定义)**的对比
重载:
- 两个函数在同一作用域
- 函数名相同,参数列表(参数类型,顺序,个数)不相同,返回值不做要求
覆盖(重写):
- 两个函数分别在基类和派生类的作用域
- 函数名,参数,返回值都必须相同(协变例外)
- 两个函数必须是虚函数
隐藏(重定义):
- 两个函数分别在基类和派生类的作用域
- 函数名相同
- 基类和派生类同名函数不构成覆盖(重写)就是隐藏(重定义)
抽象类
在虚函数后面写上=0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,纯虚函数更体现出接口继承。
纯虚函数的目的:体现接口继承,强制重写
class Person
{
public:
virtual void Read() = 0;
};
class Teacher :public Person
{
public:
virtual void Read()
{
cout << "教书" << endl;
}
};
class Student :public Person
{
public:
virtual void Read()
{
cout << "学习" << endl;
}
};
int main()
{
Person* pTeacher = new Teacher;
pTeacher->Read();
Person* pStudent = new Student;
pStudent->Read();
}
C++11 override和final
C++11中用override和final来修饰虚函数
实际中我们建议多使用 虚函数+override 的方式来强制重写函数,因为虚函数的意义就是实现多态,如果没有重写,虚函数就没有意义。
//用final关键字修饰基类虚函数,不能被派生类重写
class Person
{
public:
virtual void Read() final{}
};
class Teacher :public Person
{
public:
virtual void Read()
{
cout << "教书" << endl;
}
};
class Student :public Person
{
public:
virtual void Read()
{
cout << "学习" << endl;
}
};
int main()
{
Person* pTeacher = new Teacher;
pTeacher->Read();
Person* pStudent = new Student;
pStudent->Read();
}
class Person
{
public:
virtual void Read(){}
};
class Student :public Person
{
public:
virtual void Read() override
{
cout << "学习" << endl;
}
};
//override 修饰派生类虚函数强制完成重写,如果没有重写会编译报错
多态原理
虚函数表
一个栗子
class Base
{
public:
virtual void Func1()
{
cout << "Func" << endl;
}
private:
int _b = 1;
};
int main()
{
cout << sizeof(Base) << endl;
return 0;
}
//sizeof(Base)=8
除了_b成员,还多了一个 _vfptr成员放在对象前面(有些平台会放到对象的最后面,这和平台有关)
对象中的这个指针我们叫做虚函数表指针(v表示virtual,f表示function)。
一个含有虚函数的类中至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
举个例子
class Base
{
public:
virtual void Func1()
{
cout << "Base::Func1()" << endl;
}
virtual void Func2()
{
cout << "Base::Func2()" << endl;
}
void Func3()
{
cout << "Base::Func3()" << endl;
}
private:
int _b = 1;
};
class Derive :public Base
{
public:
virtual void Func1()
{
cout << "Derive::Func1()" << endl;
}
private:
int _d = 2;
};
int main()
{
Base b;
Derive d;
return 0;
}
[外链图片转存失败(img-jMKmzKY7-1564195986620)(E:\CODE\虚函数表指针1.png)]
派生类中d中也有一个虚表指针,d对象由两部分组成,一部分是父类继承下来的成员,虚表指针也是存在部分,另一部分是自己的成员。
基类b对象和派生类d对象虚表是不一样的,这里我们发现了Func1完成了重写,所以d的虚函数表中存的是重写的Derive::Func1,所以虚函数的重写也叫覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但不是虚函数,所以不会放进虚表。
虚函数的本质是一个存虚函数指针的指针数组,这个数组最后放了一个nullptr
虚表的生成:
先将基类中的虚表内容拷贝一份到派生类虚表中
如果派生类重写了基类函数中的某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
派生类自己新增加的虚函数按其在派生类声明次序增加到派生类虚表的最后
关于虚函数存在哪,虚表存在哪
虚函数和普通函数一样都是存在代码段中的
虚表存储在存储在代码段
虚表存的是虚函数的指针,不是虚函数
对象中存的是不是虚表,存的是虚表指针
[外链图片转存失败(img-Ytwnvi8d-1564195986622)(E:\CODE\虚函数表指针2.png)]
多态的原理
指向谁调用谁
多态的条件:虚函数重写,必须是指针和引用
为什么要完成虚函数重写?
只有虚函数重写虚表中才能完成覆盖,父类和子类的虚表中存储的是各自的虚函数
为什么是指针和引用?
指针指向父类函数,然后到父类函数的虚表中去找父类的虚函数;指向子类对象的时候,看到的是子类对象中父类的那一部分,然后再找到子类的虚函数;如果是对象的话,用切片将对象的成员拷过去并不会把虚表拷贝过去。
[外链图片转存失败(img-02aPvDV8-1564195986628)(E:\CODE\虚函数覆盖.png)]
普通调用:静态联编,编译时决议,即编译时就已经确定调用函数的地址
多态调用:动态联编,运行时决议,即在运行的时候才到对象虚表中去找
动态绑定与静态绑定
class Person
{
public:
void Func()
{
cout << "Person" << endl;
}
};
class Student :public Person
{
public:
void Func()
{
cout << "Student" << endl;
}
};
int main()
{
Student s;
Person *p = &s;//隐式类型转换
p->Func();
}
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如函数重载。
从编译角度上看:C++在编译的时候。要确定每个对象调用的函数地址,这称为早绑定,从内存上看如下图:
[外链图片转存失败(img-mcpfd3i8-1564195986630)(E:\CODE\父子类内存图.png)]
当我们构造Student类时,首先要调用Person类的构造函数去构造Person类的对象,然后才调用Student类的构造函数完成Student自身的部分的构造,从而拼接出一个完整的Student类,当我们将Student类型转化成Person类型时,该对象就被认为是原对象的整个上半部分,就是上图中“Person所占内存”,所以当我们用类型转换后的指针去调用它时,就调用它所在内存中的方法。
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
实现晚绑定,就要在基类声明函数的时候,使用virtual关键字,这样的函数我们就称之为虚函数,一旦某个函数在基类中声明为virtual,那么所有的派生类中该函数都是virtual,不需要显式的声明为virtual。
class Person
{
public:
void Func()
{
cout << "Person" << endl;
}
};
class Student :public Person
{
public:
virtual void Func()
{
cout << "Student" << endl;
}
};
int main()
{
Student s;
Person *p = &s;//隐式类型转换
p->Func();
}
单继承和多继承关系的虚函数表
单继承中的虚函数表
class Base{
public:
virtual void func1(){ cout << "Base::func1" << endl; }
virtual void func2(){ cout << "Base::func2" << endl; }
private:
int b;
};
class Derive :public Base
{
public:
virtual void func1(){ cout << "Derive::func1" << endl; }
virtual void func3(){ cout << "Derive::Func3" << endl;}
virtual void func4(){ cout << "Derive::Func4" << endl; }
private:
int d;
};
int main()
{
Base b;
Derive d;
return 0;
}
[外链图片转存失败(img-IiC9Zy2r-1564195986632)(E:\CODE\单继承虚函数表.png)]
图中只看见了func1和func2,是因为编译器将func3和func4隐藏了。
我们用以下一段代码来看一下虚函数的地址。
typedef void(*VFPTR)();
void PrintVTable(VFPTR vTable[])
{
cout << "虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf("第%d的虚函数地址:0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Base b;
Derive d;
//解析:取出b,d对象的头4个字节,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后放了一个nullptr
//1.先取b的地址,强转成一个int*的指针
//2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
//3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
//4.虚表指针传递给PrintVTable进行打印虚表
VFPTR* vTableb = (VFPTR*)(*(int*)&b);
PrintVTable(vTableb);
VFPTR* vTabled = (VFPTR*)(*(int*)&d);
PrintVTable(vTabled);
return 0;
}
[外链图片转存失败(img-hbtwMzUG-1564195986633)(E:\CODE\单继承的覆盖.png)]
多继承中的虚函数表
class Base1{
public:
virtual void func1(){ cout << "Base::func1" << endl; }
virtual void func2(){ cout << "Base::func2" << endl; }
private:
int b1;
};
class Base2{
public:
virtual void func1(){ cout << "Base::func1" << endl; }
virtual void func2(){ cout << "Base::func2" << endl; }
private:
int b2;
};
class Derive :public Base1,public Base2
{
public:
virtual void func1(){ cout << "Derive::func1" << endl; }
virtual void func3(){ cout << "Derive::Func3" << endl; }
private:
int d;
};
typedef void(*VFPTR)();
void PrintVTable(VFPTR vTable[])
{
cout << "虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf("第%d的虚函数地址:0X%x,->", i, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
PrintVTable(vTableb2);
return 0;
}
[外链图片转存失败(img-G2oMOhFO-1564195986635)(E:\CODE\多继承的覆盖.png)]
值得注意的
-
内联函数不能是虚函数,因为内联函数没有地址,无法把地址放在虚函数表中
-
静态成员不能是虚函数,因为静态成员没有this指针,只用类型::成员函数的调用方式无法访问虚函数表,所以静态成员无法放进虚函数表。
-
构造函数不可以是虚函数,因为对象中的得虚函数表指针是在构造函数初始化列阶段才初始化的。也就是说,没有构造函数就没有虚函数表,所以构造函数不能是虚函数。
-
析构函数可以是虚函数,并且最好把基类的析构函数定义为虚函数。
-
对象访问普通函数快还是虚函数更快?
如果是普通对象,一样快,如果是指针对象或者是引用对象,则调用的普通函数快,因为要构成多态,运行时调用虚函数要到虚函数表中去查找。
-
虚函数表是在编译阶段就生成的。
-
抽象类强制了函数重写,体现了接口继承。
-
区分虚基表与虚表,虚基表是虚拟继承解决菱形继承二义性产生的,而虚表则是多态中的