目录
- [1. 多态相关概念](#1. 多态相关概念)
-
- [1.1 简述:多态](#1.1 简述:多态)
- [1.2 概念汇总与补充](#1.2 概念汇总与补充)
- [2. 多态重写的底层原理](#2. 多态重写的底层原理)
-
- [2.1 虚函数存储的结构与位置](#2.1 虚函数存储的结构与位置)
- [2.2 重写覆盖](#2.2 重写覆盖)
1. 多态相关概念
1.1 简述:多态
1. 什么是多态
- 不同的对象去做同一个行为时,得到的结果不同。反应到编程语言中,即为不同类型的对象调用同一个函数,得到的返回值不同。
cpp
class Person
{
public:
virtual void BuyTicket()
{
cout << "全票" << endl;
}
};
class Children : public Person
{
public:
virtual void BuyTicket()
{
cout << "半票" << endl;
}
};
void BuyTicket(Person& p)
{
p.BuyTicket();
}
int main()
{
Person p1;
Children p2;
BuyTicket(p1);
BuyTicket(p2);
return 0;
}
2. 虚函数
- 虚函数:被关键字
virtual
修饰的函数
cpp
virtual void func1()
{
//...
}
3. 构成多态的条件
- 父类与子类的虚函数必须构成重写 关系(三同:函数名,函数参数,函数返回值相同)
- 必须用父类的指针或者引用去调用虚函数。
4. 多态的特殊情况
- 协变:父类与子类的虚函数返回值不同,也可以构成重写,只是返回值类型必须为父类/子类(不是同一父类/子类也可以)的指针或者引用。
- 析构函数即使函数名不同也构成重写,这时因为编译在编译时会将析构函数的名字统一处理为
destructor
。- 特殊的,父类的虚函数加virtual,子类的虚函数不加virtual也构成重写,子类的虚函数被视作实现重写(建议不要省略)。
cpp
class A
{
public:
virtual void func(int a = 1)
{
cout << "A->" << a << endl;
}
virtual void test()
{
func();
}
};
class B : public A
{
public:
virtual void func(int a = 0)
{
cout << "B->" << a << endl;
}
};
int main()
{
B().test();
return 0;
}
- <1> B类因为本身没有进行test函数的重写,所以调用test时会调用从A类继承而来的test。
<2> 调用继承而来的test的时需要使用父类指针进行调用,会发生赋值兼容转换从B类中截断出A类的部分。
<3> 调用的func函数为B类重写A类后的func函数,又因为虚函数的重写是实现重现,会直接继承父类的函数框架,只重写内部的实现。
5. 多态调用与普通调用
- 普通调用:根据指针/引用的类型,调用指针/引用类型的函数
- 多态调用:使用父类指针指向子类对象,根据指针/引用指向对象的类型,调用指向对象的函数。
- 即使父类与子类构成多态,但不采用多态调用的方式,也不会达到多态的效果。
- 普通函数会在编译时就将函数的地址写入符号表中,而重写的虚函数其地址存储在对象的虚表中,当我们使用多态调用对其进行调用时,则是在运行时从对象的虚表中获得对应虚函数的地址。
6. 补充语法
- 如何定义实现一个不能被继承的父类:
<1> 使用private
私有化构造函数
<2> 使用关键字final
修饰父类
cpp
class A final
{
//...
};
- 关键字
override
,检查子类是否重写了父类的虚函数,如果没有,会发生报错
cpp
class Person()
{
public:
virtual void func1()
{
//...
}
};
class Student : public Person
{
public:
virtual void func1() override
{
//...
}
}
7. 含有虚函数的类的大小
cpp
class Base
{
public:
virtual void func1()
{
cout << "hello world" << endl;
}
private:
int _b = 1;
char _c = 'a';
};
- 内存对齐:(VS最大对齐数为8)
<1> int(对齐数4),大小 + 4,地址相对位置起始处:0(小于8,取自己;大于8,取8)
<2> char(对齐数1),大小 + 1,地址相对位置起始处:4
<3> 最后空间大小取最大对齐数的整数倍处(取8),8字节- 虚函数表指针(虚表指针):指向虚函数表的指针,虚函数表内存储序函数的地址,虚函数表可以视作一个函数指针数组
- 含有虚函数的类内部,除开本身包含的成员变量外,还有额外包含一个虚表指针,虚表内存储着这个类所有虚函数的地址。
cpp
int main()
{
Base a;
return 0;
}
8. 抽象类
- 包含纯虚函数的类,这种类无法实例化出对象。
- 继承抽象类的子类必须要对纯虚函数进行重写,重写后子类才能够实例化出对象。
cpp
class Car
{
public:
virtual void Drive() = 0
{}
};
class Benz : public Car
{
public:
virtual void Drive()
{
cout << "舒适" << endl;
}
};
class BMW : public Car
{
public:
virtual void Drive()
{
cout << "操控" << endl;
}
};
1.2 概念汇总与补充
1. 重载,隐藏与重写
- 重载:会函数的重载,构成重载的函数必须在一个作用域中,且函数名相同,参数类型不同。
- 隐藏(覆盖):在父类与子类中的同名函数,在不同的作用域中,参数类型可以不同。
- 重写(重定义):在父类与子类中函数名相同,参数类型相同,返回值相同的虚函数(协变例外)。
- 注:分别在父类作用域与子类作用域的同名函数,不是重写就是隐藏。
2. 内联函数可以作为虚函数
- 想要能够作为一个虚函数的前提为必须是一个函数,拥有函数地址,可是内联函数在一般情况下不会创建栈帧,会直接在原地展开没有函数地址。
- 但,特殊的,内联函数在多态调用是不会展开,其也就不再具备内联属性,这里体现了内联函数的双向属性。
3. 静态成员函数与构造函数
- 静态成员函数没有this指针,而虚函数的调用需要通过父类指针来实现多态调用,运行时也就无法通过虚表指针进行调用,所以静态成员函数不能时虚函数。
- 构造函数只有在创建对象时的初始化列表中初始化对象,而虚表指针需要在编译阶段就完成初始化,所以构造函数不能是虚函数。
4. 虚函数的调用速度与抽象类
- 虚函数在进行普通调用时与普通函数的调用方式相同,只有在进行多态调用时,因为在运行时要通过虚表指针去搜索虚函数地址,所以会比普通函数的调用慢。
- 抽象类强制其子类进行虚函数重写,是一种接口继承的体现。
2. 多态重写的底层原理
2.1 虚函数存储的结构与位置
1. 虚函数的存储结构
cpp
class Base
{
public:
virtual void func1()
{
cout << "func1" << endl;
}
virtual void func2()
{
cout << "func2" << endl;
}
virtual void func3()
{
cout << "func3" << endl;
}
private:
int _b = 1;
char _c = 'a';
};
typedef void(*PTR)();
void Print(PTR* p)
{
for (int i = 0; p[i]; i++)
{
printf("p[%d] = %p\n", i, p[i]);
}
p[0]();
p[1]();
p[2]();
}
int main()
{
Base* p = new Base;
//相近类型才可以发生类型转换
Print((PTR*)(*((int*)p)));
return 0;
}
2. 虚函数与虚表指针在内存中的位置
cpp
class A
{
public:
virtual func()
{}
}
int main()
{
//栈
int a = 10;
//堆
int* b = new int;
//静态区
static int c = 0;
//常量区
const char* str = "hello world";
printf("栈:%p\n", &a);
printf("堆:%p\n", b);
printf("静态区:%p\n", &c);
printf("常量区:%p\n", str);
//函数指针
typedef void (*PTR)();
A d;
PTR* pd = (PTR*)(*((int*)(&d)));
printf("虚表指针:%p\n", pd);
printf("虚函数地址:%p\n", pd[0]);
return 0;
}
3. 补充
- 当子类拥有独属于自己的虚函数时,也会将此虚函数的函数指针记录至虚表中,但,监视窗口无法查看。
2.2 重写覆盖
cpp
class A
{
public:
virtual void func1()
{}
};
class B : public A
{
public:
//重写/未重写
//virtual void func1()
//{}
};
int main()
{
A* p1 = new A;
B* p2 = new B;
return 0;
}
- 语法上的重写,反映到底层实现上就是函数指针的覆盖。