目录:
- 前言
- 多态
- 认识多态
- 多态的定义与实现
- 构成多态的条件
- 虚函数
- 1.协变(基类与派生类虚函数返回值不同)
- 2.析构函数的重写
- c++11.两个虚函数修饰关键字:final & override
- 重载、重写、重定义再理解
- 抽象类
- 抽象类的概念
- 接口继承与实现继承
- 多态的原理
- 虚函数表
- 打印虚函数表
- 原理剖析
- 经典例题
- 总结
前言
打怪升级:第61天 |
---|
多态
认识多态
所谓多态,通俗来讲就是多种形态,也就是当一件事情由不同的人去完成会表现出不同的形态,例如买车票:成人全价,学生半价,军人优先等;
再例如测量体重,不同的人去测量,体重仪的表现也会不同。
多态的定义与实现
构成多态的条件
下面我们先来“见一见猪跑”:
class Person
{
public:
virtual void BuyTicket()
{
cout << "成人,全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "学生,半价" << endl;
}
};
void Buy(Person& p)
{
p.BuyTicket();
}
void Test_p2()
{
Person p1;
Student t1;
Buy(p1);
Buy(t1);
}
虚函数
- 虚函数的定义
虚函数就是使用 virtual 修饰的成员函数。
class Person
{
public:
virtual void BuyTicket()
{
cout << "成人,全价" << endl;
}
};
- 虚函数的重写
**重写(覆盖)**的条件:
在子类中存在与父类完全相同的虚函数(三同:函数名、参数、返回值都必须相同),我们称为子类对父类的虚函数进行了重写。
class Person
{
public:
virtual void BuyTicket()
{
cout << "成人,全价" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "学生,半价" << endl;
}
};
上面,我们说的十分肯定 – 必须由三同才可构成重写,
然而,其实是有两个特例存在的 – 1.子类的重写返回值在特殊情况下可以不同;2.析构函数的重写
1.协变(基类与派生类虚函数返回值不同)
当子类和父类的虚函数返回值为有父子关系的类对象时,返回值也可以不同。
class A
{
};
class B :public A
{
};
class Person
{
public:
virtual Person& BuyTicket()
{
cout << "成人,全价" << endl;
return *this;
}
};
class Student : public Person
{
public:
virtual Student& BuyTicket()
{
cout << "学生,半价" << endl;
return *this;
}
};
可以让父类虚函数返回子类引用,子类虚函数返回父类引用吗?
– 不可,虚函数的重写实际上是对 从父类继承下来的虚函数的实现进行重写,声明部分是完全继承的,因此,
如果父类虚函数返回子类引用,就会使得子类中的虚函数使用父类对象初始化子类对象,(我们可以使用子类对象初始化父类对象 – 会进行切片,但是父类中不一定拥有子类的全部成员,无法完成对子类的初识化)。
2.析构函数的重写
如果基类的析构函数是虚函数,此时派生类的析构函数无论是否加 virtual,都与基类的析构函数构成重写。
这里虽然基类与派生类的析构函数函数名不同,看起来好像违反了 三同 的规则,
其实不然,这里是编译器在底层做了特殊处理:编译之后所有析构函数的名称都会被处理为destruction。
c++11.两个虚函数修饰关键字:final & override
final修饰父类虚函数:该虚函数不可再在子类中进行重写了。
override修饰子类虚函数:该虚函数必须是父类的虚函数的重写。
重载、重写、重定义再理解
也就是说:两个基类和派生类中的同名函数 不构成重写 就是重定义。
抽象类
抽象类的概念
在虚函数后面写上 = 0 ,这个虚函数就变成了纯虚函数, 包含纯虚函数的类称为抽象类(也叫接口类),包含纯虚函数的类无法实例化出对象,抽象类的派生类想要实例化出对象必须对纯虚函数实现重写,否则派生类也是抽象类。
接口继承与实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
所以如果不实现多态,不要把函数定义成虚函数。
多态的原理
多态,如果只看表面应用 – 不同的对象调用"同一个函数"表现也不一样,看起来感觉好像很神奇、很厉害,居然可以“进行判断?”,
那么到底是不是这样呢,让我们去底层一探究竟吧~。
(注:以下数据测试环境为 vs2022,x86)
虚函数表
class Base
{
public:
virtual void Print()
{
cout << "Base::Print" << endl;
}
int _bval;
};
void Test_p3()
{
Base b1;
cout << sizeof(b1) << endl;
}
我们来计算一下Base类的大小:
按照我们以前的知识:成员函数是放在代码段,对象中只有普通成员变量, 因此,Base的大小应该是4;
class Base
{
public:
virtual void Print1() {}
virtual void Print2() {}
int _bval = 1;
};
class Derive :public Base
{
public:
virtual void Print1() {}
virtual void Print3() {}
int _dval = 10;
};
void Test_p4()
{
Base b1;
Derive d1;
}
这里有一点我们需要注意:虚函数表存在哪里?虚函数又存在哪里?
虚函数表存在对象中,虚函数存在虚函数表中,吗?
不是的,对象中存的是一个虚函数表指针,虚函数表中存的也只是虚函数的指针,
至于虚函数表和虚函数,其实都存在于内存中的代码段。
打印虚函数表
typedef void(*VFPTR)(); // 定义 VFPTR为 void(*)() -- 函数指针类型
void VFTable(VFPTR*table)
{
/*while (*table)
{
(*table)();
++table;
}*/
for (int i = 0; table[i]; ++i)
{
printf("[%d]->", i);
table[i](); // 函数调用
}
cout << endl;
}
void Test_p4()
{
Base b1;
Derive d1;
// 要打印虚函数表,我就要先获取虚函数表的地址 -- 通过上面几次的查看我们可以看到 -- 虚函数表地址存放在对象的最前面
VFTable((VFPTR*)(*(int*)&b1));
VFTable((VFPTR*)(*(int*)&d1));
VFTable(*(VFPTR**)&b1);
VFTable(*(VFPTR**)&d1);
}
原理剖析
补充:
- 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
- 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
经典例题
- 1
class Base1 { public: int _b1; };
class Base2 { public: int _b2; };
class Derive : public Base1, public Base2 { public: int _d; };
int main()
{
Derive d;
Base1* p1 = &d;
Base2* p2 = &d;
Derive* p3 = &d;
return 0;
}
- 2
class AA
{
public:
virtual void Print(int a = 1)
{
cout << "a = " << a << endl;
}
virtual void Call() { Print(); }
};
class BB :public AA
{
public:
virtual void Print(int b = 0)
{
cout << "b = " << b << endl;
}
};
void Test_p1()
{
BB p;
p.Call();
}
总结
多态的重点
- 就是要了解多态构成的条件:父类的指针或引用;虚函数重写。
- 就是知道了解虚函数表的原理:存的是虚函数地址。
- 清楚多态实现的原理。