目录
前言
多态的讲解涉及到了继承的知识,可以说是继承是实现多态的基础。同时多态的构成条件、使用场景与继承也关联在一起,因此观看本章内容之前如果需要,可以看看我上一篇关于继承的博客:https://blog.youkuaiyun.com/qq_63412763/article/details/125916475?spm=1001.2014.3001.5501
多态的定义
“多态”,顾名思义就是多种形态。C++的学习始终是面向对象的,对于不同的类产生不同的对象,做同一件事可能会产生不同的结果,造成不同的形态,这就是多态的通俗理解。
多态的构成条件
1. 必须通过基类的指针或者引用调用虚函数。
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写。
虚函数
在学习继承的时候,我们接触到了虚拟继承的概念,虚函数同样使用virtual关键字。
定义
virtual修饰的类成员函数称为虚函数。
格式
virtual + 返回值 函数名() { 函数主体 }
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
protected:
int b1;
};
虚函数的重写(覆盖)
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
//基类
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
protected:
int b1;
};
//派生类
class Derive : public Base1
{
public:
virtual void func1() { cout << "Derive::func1" << endl; } //进行虚函数的重写
protected:
int d1;
};
虚函数重写例外
1.协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。注意这里的返回值必须也要构成继承关系。
class A
{};
class B:public A
{};
class C
{
public:
virtual A* Func()
{
cout << "C::virtual A* Func()" << endl;
return nullptr;
}
};
class D :public C
{
public:
virtual B* Func()
{
cout << "D::virtual B* Func()" << endl;
return nullptr;
}
};
上面的代码虚函数Func函数虽然返回值不同,但是返回值为指针,且构成继承关系,因此也能完成多态的条件。
2.析构函数的重写(基类与派生类析构函数的名字不同)
两个不同的类的析构函数的名字肯定不同,但是编译器会对析构函数名进行特殊处理,处理成destrutor(),这时候再加上virtual关键字就会构成重写。
意义:
正确调用析构函数,避免出现切片后的指针或引用调用析构函数时,调用成了基类而不是派生类的析构函数。
class A
{
public:
virtual ~A()
{
cout << "~A()" << endl;
}
};
class B : public A
{
public:
virtual ~B()
{
cout << "~B()" << endl;
}
};
int main()
{
A* ptr = new B;//最后要处理B的空间
delete ptr;//构成重写后就会调用~B()。否则的话会调用~A(),造成析构错误。
}
基类的指针或者引用调用虚函数
很好理解,就是对派生类的对象进行切片,之后用切好的指针或者引用去调用重写后的虚函数。
为什么不包括基类对象呢?
原因在于逻辑层面上。假如现在我想要一个基类对象,无论我是通过派生类来进行切片操作,得到了一个基类对象,还是直接实例化一个基类对象,我的目的只是是得到一个基类对象,与过程无关。调用虚函数的时候我肯定也是要使用基类的虚函数,这时候如果还设定成通过切片得来的对象再调用虚函数会有多态的行为,即调用派生类的虚函数,这就与最初的设想调用基类自身虚函数的逻辑相悖了。因此,从设计的角度上来说,基类对象调用虚函数也是不会形成多态的。
override、final C++关键字
override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
final
修饰虚函数,表示该虚函数不能再被重写。
多态的应用
前面的多态条件搞清楚了之后,我们试着应用一下。
例1
代码
//基类
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
protected:
int b1;
};
//派生类
class Derive : public Base1
{
public:
virtual void func1() { cout << "Derive::func1" << endl; } //进行虚函数的重写
protected:
int d1;
};
void f(Base1* ptr) //用基类的指针接受,以满足多态条件
{
ptr->func1();
}
int main()
{
Base1 b;
Derive d;
f(&b);
f(&d);
return 0;
}
运行结果
例2
代码
//基类
class Base1
{
public:
virtual void func1(int val=1) { cout << "Base1::func1: " << val << endl; }
void test() { func1(); }
protected:
int _b1;
};
//派生类
class Derive : public Base1
{
public:
virtual void func1(int val=2) { cout << "Derive::func1: " << val << endl; } //进行虚函数的重写
protected:
int _d1;
};
int main()
{
Derive d;
d.test();
return 0;
}
运行结果
我们明明调用的是Derive的func1啊,为什么结果不是Derive::func1: 2呢?
原因在于重写!!!
可见虚函数的继承是继承了返回值、函数名、参数。重写的话就是函数的内容被替换了。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。上面的例2就很好的说明了多态是接口继承。
重载、重写、重定义(隐藏)对比
抽象类
定义
包含纯虚函数的类叫做抽象类(也叫接口类)。
纯虚函数:在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
特点
1.抽象类不能实例化出对象。(抽象与实例化是相对着的,逻辑上很好理解)
2.派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。
3.纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。(与override的功能相似)
代码测试
代码
//基类
class Base1
{
public:
virtual void func1(int val = 1) = 0;
protected:
int _b1;
};
//派生类
class Derive1 : public Base1 //Derive1没有对func1进行重写
{
protected:
int _d1;
};
class Derive2 : public Base1 //Derive2对func1进行重写
{
public:
virtual void func1(int val = 2) { cout << "Derive::func1: " << val << endl; }
protected:
int _d2;
};
int main()
{
Base1 b; //抽象类进行实例化
Derive1 d1; //非重写派生类进行实例化
Derive2 d2; //重写派生类进行实例化
Base1* ptr = &d2; //抽象类指针
Base1& ref = d2; //抽象类引用
return 0;
}
运行结果
可以发现与上面的特点是一致的。
多态的原理
接下来就深入到内存的程度去了解多态到底是怎么实现的。
虚函数表
首先我们知道多态的条件之一就是要有虚函数的重写,那么问题来了:虚函数放在了对象的哪里呢?——虚函数表。这里和前面继承的虚基表要分清楚。
一个含有虚函数的类中都至少都有一个虚函数表指针,因为所有虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
因此一个类的大小除了包括成员变量,还包括一个虚函数表指针。
代码测试
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
protected:
int b1;
};
class Derive : public Base1
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
protected:
int d1;
};
int main()
{
Base1 b;
Derive d;
cout << sizeof(d) << endl;
return 0;
}
运行结果
32位下理论上结果应该是8,毕竟只_b1和_d1两个成员变量,函数不计入大小的计算。事实上结果是12,原因在于还有一个虚函数表指针_vfptr。算上之后就刚好是12。
这里有一点问题,在监视窗口里只有两个虚函数,实际上d除了重写的func1、继承的func2,应该还有一个自身的虚函数func3。毕竟前面我们也说虚函数都要存在虚函数表里。这里其实是编译器处理了,实际上因该还有一个func3,我们通过内存去找它去。
代码测试
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
protected:
int b1;
};
class Derive : public Base1
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
protected:
int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%p,->", i+1, vTable[i]);
VFPTR f = vTable[i];
f();
}
cout << endl;
}
int main()
{
Derive d;
VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
PrintVTable(vTableb1);
return 0;
}
运行结果
可以看出确实是有第三个虚函数func3在虚函数表里的。
多态的原理
代码
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
protected:
int b1;
};
class Derive : public Base1
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
protected:
int d1;
};
int main()
{
Derive d;
Base1* ptr = &d;
ptr->func1();
return 0;
}
运行结果
动态绑定与静态绑定
静态绑定
又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态。比如:函数重载
动态绑定
又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
如下:
多态调用就是动态绑定,普通调用就是静态绑定。
多继承中的虚函数表
代码测试
class Base1
{
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
protected:
int b1;
};
class Base2
{
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
protected:
int b2;
};
class Derive : public Base1, public Base2
{
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
protected:
int d1;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
cout << " 虚表地址>" << vTable << endl;
for (int i = 0; vTable[i] != nullptr; ++i)
{
printf(" 第%d个虚函数地址 :0X%p,->", i+1, 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;
}
上面的多继承我们不难发现,Derive中继承重载了两个func1,这到底是不是同一个虚函数呢?
我们还是通过反汇编来看:
再来看看func2:
运行结果
小结
一
多继承中只要构成了重写,多态调用的时候调用的是同一个函数,其余的只要不构成重载,一律各存一份到虚表中。且继承几个基类,派生类就会有几个虚函数表。
二
上面的Derive::func3函数是在第一个虚表里出现的,故:
派生类中没有重写的虚函数同样会放到虚表中,只不过是放在第一个基类的虚表中。
总结
多态的重点在于虚函数相关知识的掌握,搞清楚虚函数表的作用和意义对于学习多态至关重要。