目录
一、多态的概念
二、多态的定义与实现
2.1 多态的构成条件
2.2 虚函数与虚函数重写
2.3 多态效果展示
2.4 多态调用与普通调用
2.5 虚函数重写两个例外
2.5.1 协变(基类与派生类虚函数返回值类型不同)
2.5.2 析构函数的重写(基类与派生类析构函数的名字不同)
2.6 C++11 override 和 final
2.7 重载、重写(覆盖)、重定义(隐藏)的对比
三、抽象类
3.1 纯虚函数
3.2 接口继承和实现继承
四、多态的原理
4.1 虚函数表
4.2 多态的原理
4.3 多继承中的虚函数表
4.4 动态绑定与静态绑定
五、多态习题
5.1 问答题
5.2 选择题
5.2.1 重载、重写和重定义问题
5.2.2 抽象类问题
5.2.3 虚表问题
5.2.4 虚函数问题
5.2.5 继承与多态综合题()
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
语言角度来说,就是不同类对象调用“同一函数”,结果不同!
举个栗子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人
买票时是优先买票。
1. 对象是具有继承关系的对象,即基类与派生类对象!
2. 被调用的函数必须是虚函数,且派生类必须对虚函数进行重写!
3.必须通过基类的指针或者引用调用虚函数!
虚函数:即被 virtual 修饰的类成员函数称为虚函数。
虚函数重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同,但函数实现功能不同!),称子类的虚函数重写了基类的虚函数。
class Person {
public:
//虚函数
virtual void BuyTicket() { cout << "Person->买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "Student->买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
class soldier : public Person
{
public:
//虚函数重写
virtual void BuyTicket() { cout << "soldier->买票-优先" << endl; }
};
void Func(Person& p)
{
//买票行为
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
soldier si;
Func(ps);
Func(st);
Func(si);
return 0;
}
普通调用(对象):通过类名区分调用不同类中的同样函数!
多态调用(引用、指针):满足上面三个条件!
多态的核心用武之地在切割后的调用!
class A {};
class B : public A {};
class Person {
public:
virtual A* f() { return new A; }
};
class Student : public Person {
public:
virtual B* f() { return new B; }
};
Q:继承中,为什么析构函数需要是多态?
下面我们来看一下,普通调用的结果:
多态调用结果:
我们来分析一下为什么会出现第一个情况:因为p2通过切割获指向了Person对象,未加虚函数调用,只是普通调用,所以delete p2时普通调用只根据类名调用对应的析构函数,即~person!
Q2: 为什么析构名字不同,依然满足重写?
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor
1. final:修饰虚函数,表示该虚函数不能再被重写
2.override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
典型例题:
一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
打印单继承虚表:
typedef void(*VFPtr)(); //函数指针 虚表中存储的是虚函数地址
void PrintVFPTable(VFPtr VFPTable[],int n)
{
for (int i = 0;i", i, VFPTable[i]);
VFPTable[i]();
}
cout << endl;
}
void test_6()
{
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;
}
virtual void Func3()
{
cout << "Derive::Func3()" << endl;
}
private:
int _d = 2;
};
Base b;
Derive d;
PrintVFPTable((VFPtr*)(*(int*)&b),2);
PrintVFPTable((VFPtr*)(*(int*)&d),3);
}
如何理解参数 (VFPtr*)(*(int*)&b) ?
总结一下派生类的虚表生成:
a.先将基类中的虚表内容拷贝一份到派生类虚表中
b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数
c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后
总结一下虚表与虚函数存储位置:
虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是
他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::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 d1;
};
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)));//切割指向 Base2& b=d; (VFPTR*)(*(int*)(&b))
PrintVTable(vTableb2);
return 0;
}
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
1. 什么是多态?答:见 4.4
2. 什么是重载、重写(覆盖)、重定义(隐藏)?答:见 2.73. 多态的实现原理?答:4.1 4.2
4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是
inline,因为虚函数要放到虚表中去。(多态调用忽略内联,普通则内联)
5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。(虚函数表 4.1)
6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。(见 2.5 )
8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。
9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
10. C++菱形继承的问题?虚继承的原理?答:参考继承博客。注意这里不要把虚函数表和虚基表搞混了。
11. 什么是抽象类?抽象类的作用?答:见 3 !抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系
1.关于重载、重写和重定义的区别说法正确的是(A F)【不定项选择】
A.重写和重定义都发生在继承体系中
B.重载既可以在一个类中,也可以在继承体系中
C.它们都要求原型相同
D.重写就是重定义
E.重定义就是重写
F.重写比重定义条件更严格
G.以上说法全错误
函数原型相同-> 函数名、参数列表、返回类型相同!
B 重载只能在一个类域中!
D、E 重写与重定义不是一回事,只能说函数满足重写则一定满足重定义,不能将两者等价!
1.假设A为抽象类,下列声明(B )是正确的
A.A fun(int);
B.A*p;
C.int fun(A);
D.A obj;
A.抽象类不能实例化对象,所以以对象返回是错误
B.抽象类可以定义指针,而且经常这样做,其目的就是用父类指针指向子类从而实现多态
C.参数为对象,所以错误
D.直接实例化对象,这是不允许的
1.关于虚表说法正确的是(D )
A.一个类只能有一张虚表
B.基类中有虚函数,如果子类中没有重写基类的虚函数,此时子类与基类共用同一张虚表
C.虚表是在运行期间动态生成的
D.一个类的不同对象共享该类的虚表
A.多继承的时候,就会可能有多张虚表
B.父类对象的虚表与子类对象的虚表没有任何关系,这是两个不同的对象
C.虚表是在编译期间生成的
D.一个类的不同对象共享该类的虚表,可以自行写代码验证之
2.下面函数输出结果是(A )
class A
{
public:
virtual void f()
{
cout<<"A::f()"<f();
A.B::f()
B.A::f(),因为子类的f()函数是私有的
C.A::f(),因为强制类型转化后,生成一个基类的临时对象,pa实际指向的是一个基类的临时对象
D.编译错误,私有的成员函数不能在类外调用
A.正确
B.虽然子类函数为私有,但是多态仅仅是用子类函数的地址覆盖虚表,最终调用的位置不变,只是执行函数发生变化
C.不强制也可以直接赋值,因为赋值兼容规则作出了保证
D.编译正确
1.关于不能设置成虚函数的说法正确的是(D )
A.友元函数可以作为虚函数,因为友元函数出现在类中
B.成员函数都可以设置为虚函数
C.静态成员函数不能设置成虚函数,因为静态成员函数不能被重写
D.析构函数建议设置成虚函数,因为有时可能利用多态方式通过基类指针调用子类析构函数
A.友元函数不属于成员函数,不能成为虚函数
B.静态成员函数就不能设置为虚函数
C.静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数
D.尤其是父类的析构函数强力建议设置为虚函数,这样动态释放父类指针所指的子类对象时,能够达到析构的多态
1. 以下程序输出结果是( C )
class A
{
public:
A() :m_iVal(0) { test(); }
virtual void func() { std::cout << m_iVal << endl; }
void test() { func(); }
public:
int m_iVal;
};
class B : public A
{
public:
B() { test(); }
virtual void func()
{
++m_iVal;
std::cout << m_iVal << endl;
}
};
int main(int argc ,char* argv[])
{
A*p = new B;
p->test();
return 0;
}
A.1 0
B.0 1
C.0 1 2
D.2 1 0
E.不可预期
F. 以上都不对
1. new B 会调用B的默认构造函数,但派生类构造前先调用基类A的构造函数!此时初始化列表初始化val=0和A的虚表指针->>A::test()->A::fun() 打印0!
2. 然后调用B的构造函数,此时完成了虚函数func重写覆盖形成B的虚表指针!调用A::test()(继承),函数内多态调用的func是B::func(),所以++val,打印1
3.p->test()先普通调用A::test(),然后多态调用B::func(),++val,打印2 。