目录
1.多态
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态,例如:不同身份的人,买票的价格不同;不同的动物,发出的叫声不同;不同的用户,抢到的红包可能金额不同……这些都是做同一种行为,对象不同产生的结果也不同。
2.多态的定义及实现
2.1 多态的构成条件
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象买票半价。
在继承中构成多态的两个条件:
1. 必须通过基类的指针或者引用调用虚函数
2.被调用的函数必须是虚函数,而且派生类必须对基类的虚函数进行重写
2.2 虚函数
虚函数:被virtual修饰的类成员函数称为虚函数
class Person
{
public:
virtual void Ticket()//虚函数
{
cout << "全票" << endl;
}
protected:
string _name = "1";
size_t _age = 1;
};
class Student : public Person
{
public:
virtual void Ticket()//虚函数
{
cout << "半票" << endl;
}
protected:
string _StuID = "10";
};
inline函数可以是虚函数,但编译器会忽略inline属性,这个函数就不再是inline,因为虚函数地址要放在虚表中去。
静态成员函数不可以是虚函数,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表
构造函数不可以是虚函数,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化。
析构函数可以是虚函数,并且最好把基类的析构函数定义成虚函数
2.3虚函数重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
在重写基类虚函数时,派生类的虚函数可以不加virtual关键字,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。
对于普通成员函数,基类与派生类的成员函数具有相同的函数名,构成隐藏关系
对于虚函数,基类与派生类的虚函数返回值类型、函数名字、参数列表相同,也构成一种特殊的隐藏关系
2.4 虚函数重写的两个例外(协变,析构函数的重写)
1. 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象(或基类对象)的指针或者引用时(要么同样是指针,要么同样是引用,返回类型可以是别的继承体系,也可以是本身的继承体系),称为协变。
class A
{};
class B : public A
{};
class C : public B
{};
class Person
{
public:
virtual A * Ticket()
{
cout << "全票" << endl;
return 0;
}
protected:
string _name = "1";
size_t _age = 1;
};
class Student : public Person
{
public:
virtual B* Ticket()
{
cout << "半票" << endl;
return 0;
}
protected:
string _StuID = "10";
};
2.析构函数的重写
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成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;
//如果不构成多态,调用person* 的指针delete调用的析构函数都会是Person的析构函数
delete p1;
delete p2;
return 0;
}
总结:多态调用,看指向对象类型,指向谁调用谁的虚函数(指向基类调用基类的虚函数,指向派生类调用派生类的虚函数)
普通调用,看调用者的类型,调用该类型的函数
2.5 接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数
3. c++11 override和final
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写
3.1 final
final用来修饰虚函数,表示该虚函数不能再被重写
3.2 override
override用来检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
4.抽象类
4.1 纯虚函数
虚函数的后面加上 =0, 这个函数成为纯虚函数。
4.2 抽象类的概念
包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
5. 多态的原理
5.1 虚函数表
A类的成员变量除了_a,通过监视窗口可以看见还多了一个_vfptr的指针变量放在对象的前面(注意有些平台可能放在对象的最后面,这个与平台有关),这个_vfptr指针叫做虚函数表指针,该指针指向虚函数表,虚函数表里面存放这个类所有虚函数的地址,一个含有虚函数的类中至少都有一个虚函数表指针,虚函数表也称虚表。
1. 派生类对象b也有一个虚函数指针,b对象由两部分构成,一部分是基类继承下来的成员,另一部分是自己的成员
2. 基类a对象和派生类b对象虚表是不一样的,这里我们发现func1完成了重写,所以b的虚表中存的是重写的B::func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法
3. 另外func2继承下来后是虚函数,所以放进了虚表,func3也继承下来了,但是不是虚函数,所以不会放进虚表
4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr
5. 派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
6.相同类型的不同对象通用一张虚表
7.虚函数指针存在虚表中;虚函数与普通函数一样存在代码段;对象中存的是虚表指针,不是存虚表;虚表通常存在于静态数据区或常量区,在编译阶段就生成
5.2 多态的原理
ptr指向s1对象时吗,p->Ticket()在s1的虚表中找到虚函数是Student::Ticket()。
ptr指向p1对象时吗,p->Ticket()在p1的虚表中找到虚函数是Person::Ticket()。
通过ptr指向的对象不同,用不同对象的虚表找到相应的虚函数,实现出不同对象去完成同一行为时,产生不同的形态。
对象访问普通函数快还是虚函数更快?
如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去找。
那么多态一定要达到两个条件呢?
满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。
不满足多态的函数调用时编译时确认好的。
class Person
{
public:
virtual void Ticket()
{
cout << "全票" << endl;
}
};
class Student : public Person
{
public:
virtual void Ticket()
{
cout << "半票" << endl;
}
};
void BuyTicket(Person* ptr)
{
ptr->Ticket();
}
int main()
{
Person p1;
Student s1;
BuyTicket(&p1);
BuyTicket(&s1);
return 0;
}
满足多态的情况,可以看一下汇编代码(X86环境下):
地址00E523C1:p存的是p1对象的指针,将p移动到eax中
地址00E523C4:[eax]取eax值指向的内容,相当于把p1对象的头四个字节(即虚表指针)移动到edx
地址00E523CB:[edx]就是取edx值指向的内容,相当于把虚表中的头四个字节存的虚函数指针移动到eax
地址00E523CD:eax中存虚函数的指针,call eax 这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后对象中取到的。
可以总结:多态的两个条件缺一不可。
不满足多态的情况:
Ticket()虽然是虚函数,但是直接用p1对象调用的方式不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call地址
5.3 动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态
6.单继承和多继承关系的虚函数表
6.1 单继承中的虚函数表
![](https://i-blog.csdnimg.cn/direct/84b8380a2d18490c9eb1782df4c1d0d5.png)
6.2 多继承中的虚函数表
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
7.一道选择题
class A
{
public:
virtual void func(int val = 1)
{
std::cout<<"A->"<< val <<std::endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
void func(int val=0)
{
std::cout<<"B->"<< val <<std::endl;
}
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确
答案:B
解析:继承对于成员来说,基类的成员并非拷贝到派生类去,而是需要用时会去基类查找,所以test()函数是A类的成员函数,test()的this指针是A*,已知func()重写,A *调用func()构成多态,调用的是B::func();
val是而不是0,这是因为虚函数的默认参数解析机制:
默认参数是在编译时进行解析的,而不是在运行时。这意味着,编译器在编译时会根据函数的声明来决定使用哪个默认参数。
当通过基类指针调用虚函数时,使用的是基类的默认参数。默认参数不是在调用时解析的,而是在编译时根据指针的类型来决定的。
这就导致了感觉就像,构成多态时:使用基类的接口,派生类的实现
再例如: