1. 综述
问题抛出: 如果子类定义了与父类中原型相同的函数时会发生什么?
函数重写:在子类中定义与父类中原型相同的函数,函数重写只发生在父类与子类之间。
父类中被重写的函数依然会继承给子类,默认情况下子类中重写的函数将隐藏父类中的函数,通过作用域分辨符::可以访问到父类中被隐藏的函数。
1.1 类型兼容性原则遇上函数重写--引出面向对象新需求
#include <iostream>
#include <stdlib.h>
using namespace std;
class Parent
{
public:
Parent(int a)
{
this->a = a;
cout << "Parent : a = " << a << endl;
}
void print()
{
cout << "Parent print: a = " << a << endl;
}
private:
int a;
};
class Child : public Parent
{
public:
Child(int b) : Parent(10)
{
this->b = b;
cout << "Parent : b = " << b << endl;
}
void print()
{
cout << "Child print: b = " << b << endl;
}
private:
int b;
};
void howtoPrint1(Parent *base)
{
base->print();
}
void howtoPrint2(Parent &base)
{
base.print();
}
void main()
{
Parent *base = NULL;
Parent p1(20);
Child c1(30);
/* 面向对象新需求的提出:
* 如果传一个父类对象,执行父类的print函数;
* 如果传一个子类对象,执行子类的print函数.
*/
/* 如下场景始终执行的是父类的print函数 */
{
/* 场景一 : 指针 */
base = &p1;
base->print(); // 执行父类的打印函数
base = &c1; // 父类指针指向子类对象
base->print(); // 发现执行的父类的打印函数--因此提出:面向对象新需求
}
{
/* 场景二 : 引用 */
Parent &base2 = p1;
base2.print();
Parent &base3 = c1; // base3是c1的别名
base3.print(); // 发现还是执行的是父类的打印函数
}
{
/* 场景三 : 函数调用 */
howtoPrint1(&p1);
howtoPrint1(&c1);
howtoPrint2(p1);
howtoPrint2(c1);
}
system("pause");
return;
}
1.2 面向对象新需求
编译器的做法不是我们所期望的,我们期望的是:
- 根据实际的对象类型来判断重写函数的调用
- 如果父类指针指向的是父类对象则调用父类中定义的函数
- 如果父类指针指向的是子类对象则调用子类中定义的重写函数
面向对象中的多态
1.3 解决方案
- C++ 通过 virtual 关键字对多态进行了支持
- 使用 virtual 声明的函数被重写后即可展现多态特性
注:这才是 virtual 真正的应用场景,而不是虚继承中。
2. 多态
2.1 多态实例
#include <iostream>
#include <stdlib.h>
using namespace std;
class HzeoFighter
{
public:
virtual int power()
{
return 10;
}
};
class EnemyFighter
{
public:
int attack()
{
return 15;
}
};
class AdvHzeoFighter : public HzeoFighter
{
public:
int power()
{
return 20;
}
};
// 使用多态的方法
void playobj(HzeoFighter *hf, EnemyFighter *ef)
{
// hf->power()将会有多态发生
if (hf->power() > ef->attack())
{
cout << "主角赢" << endl;
}
else
{
cout << "主角输" << endl;
}
}
// 使用多态的方法
void main()
{
HzeoFighter hf;
AdvHzeoFighter advhf;
EnemyFighter ef;
playobj(&hf, &ef);
playobj(&advhf, &ef);
}
void main01()
{
HzeoFighter hf;
AdvHzeoFighter advhf;
EnemyFighter ef;
// 这不是使用多态的案例
if (hf.power() > ef.attack())
{
cout << "主角赢" << endl;
}
else
{
cout << "主角输" << endl;
}
if (advhf.power() > ef.attack())
{
cout << "主角赢" << endl;
}
else
{
cout << "主角输" << endl;
}
system("pause");
return;
}
2.2 多态成立条件
2.2.1 间接赋值成立的三个条件
- 两个变量(通常一个实参,一个形参)
- 建立关系,实参取地址赋给形参
- *p 形参去间接修改实参的值
2.2.2 多态成立的三个条件(是面向对象领域的一个标准)
- 要有继承
- 要有虚函数重写(即用 virtual 修饰)
- 要有父类指针(或父类引用)指向子类对象
注:多态是设计模式的基础,是框架的基础。
2.3 多态的理论基础
2.3.1 静态联编和动态联编
- 联编是指一个程序模块、代码之间互相关联的过程。
- 静态联编(static binding),是程序的匹配、连接在编译阶段实现,也称为早期匹配。重载函数使用静态联编。
- 动态联编是指程序联编推迟到运行时进行,所以又称为晚期联编(迟绑定)。switch 和 if 语句是动态联编的例子。
2.3.2 理论联系实际
- C++ 和 C 相同,是静态编译型语言。
- 在编译时,编译器自动根据指针的类型判断指向的是一个什么样的对象;所以编译器认为父类指针指向的是父类对象。
- 由于程序没有运行,所以不可能知道父类指针指向的具体是父类对象还是子类对象。从程序安全的角度,编译器假设父类指针只指向父类对象,因此编译的结果为调用父类的成员函数。这种特性就是静态联编。
3. 多态原理探究
3.1 多态理论知识
- 当类中声明虚函数时,编译器会在类中生成一个虚函数表。
- 虚函数表是一个存储类成员函数指针的数据结构。
- 虚函数表示由编译器自动生成和维护的。
- virtual 成员函数会被编译器放入虚函数表中。
- 当存在虚函数时,每个对象都有一个指向虚函数表的指针(C++ 编译器给父类对象、子类对象提前布局 vptr 指针;当进行 howtoPrint(Parent *base) 函数时,C++ 编译器不需要区分子类对象或者父类对象,只需要在 base 指针中,找 vptr 指针即可)。
- vptr 一般作为类对象的第一个成员。
3.2 多态的实现原理
3.2.1 多态实现原理图例
多态实现原理图1
说明:通过虚函数表指针 vptr 调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能确定真正应该调用的函数。而普通成员函数是在编译时就确定了 应该调用的函数。在效率上,虚函数的效率要低很多。
多态实现原理图2
3.2.2 多态原理的探究例子
#include <iostream>
#include <stdlib.h>
using namespace std;
/* 多态成立的三个条件:
* 1、要有继承
* 2、要有虚函数重写
* 3、要有父类指针(或父类引用)指向子类对象
*/
class Parent
{
public:
Parent(int a = 0)
{
this->a = a;
cout << "Parent 执行" << endl;
}
virtual void print() // 1、为实现多态,可能动手脚的地方
{
cout << "我是你爹" << endl;
}
private:
int a;
};
class Child : public Parent
{
public:
Child(int a = 0, int b = 0) : Parent(a)
{
this->b = b;
cout << "Chiild 执行" << endl;
}
void print()
{
cout << "我是儿子" << endl;
}
private:
int b;
};
void howtoplay(Parent *base)
{
base->print(); // 2、动手脚
// 效果:传来 子类对象 执行子类的 print 函数;传来父类对象 执行父类的 print 函数
// C++编译器根本不需要区分是 子类对象 还是 父类对象
// 父类对象和子类对象都有一个 vptr 指针,根据该指针去找 虚函数表(每个对象都一个虚函数表),
// 最终找到函数的入口地址.
// 因此 迟绑定(运行时,才去判断调用的函数)
}
void main()
{
/* 3、动手脚 提前布局
* 用类定义对象的时候 C++ 编译器会在对象中添加一个 vptr 指针,该指针指向虚函数表
*/
Parent p1;
Child c1; // 子类中也有一个vptr指针
howtoplay(&p1);
howtoplay(&c1);
system("pause");
return;
}
3.2.3 如何证明 vptr 指针存在
#include <iostream>
using namespace std;
class A
{
public:
void printf()
{
cout << "aaa" << endl;
}
protected:
private:
int a;
};
class B
{
public:
virtual void printf()
{
cout << "aaa" << endl;
}
protected:
private:
int a;
};
void main()
{
// 加上 virtual 关键字 c++ 编译器会增加一个指向虚函数表的指针
printf("sizeof(a):%d, sizeof(b):%d
", sizeof(A), sizeof(B));
cout << "hello..." << endl;
system("pause");
return;
}
3.2.4 构造函数中能调用虚函数,实现多态吗?
注:这句话的意思是:定义一个子类对象,在子类对象的父类里面调用一个虚函数,问能产生多态吗?
-
对象中的 vptr 指针是什么时候被初始化?
对象在创建的时候,由编译器对 vptr 指针进行初始化,只有当对象的构造完全结束后 vptr 的指向才最终确定。父类对象的 vptr 指向父类的虚函数表,子类对象的 vptr 指向子类的虚函数表。 -
分析过程,如下图
构造函数中不能实现多态,如下例子:
#include <iostream>
#include <stdlib.h>
using namespace std;
/* 构造函数调用虚函数,能发生多态吗?
* 意思是:定义子类对象时,在子类对象的父类里面能执行虚函数,能实现多态吗?
*/
class Parent
{
public:
Parent(int a = 0)
{
this->a = a;
print(); // 在父类函数中执行虚函数
cout << "Parent 执行" << endl;
}
virtual void print()
{
cout << "我是你爹" << endl;
}
private:
int a;
};
class Child : public Parent
{
public:
Child(int a = 0, int b = 0) : Parent(a)
{
this->b = b;
cout << "Chiild 执行" << endl;
}
void print()
{
cout << "我是儿子" << endl;
}
private:
int b;
};
void howtoplay(Parent *base)
{
base->print();
}
void main()
{
Child c1; /* 定义子类对象,子类对象在创建中会调用父类的构造函数,那么在父类对象中
* 调用虚函数print,能发生多态吗
* 运行,调试发现:虽然定义的是子类对象,但是仍然执行的是父类的print函数。
* 因此,在此场景下,不能发生多态。
*/
system("pause");
return;
}
4. 多态相关知识
4.1 重载与重写
函数重载(属于静态联编):
- 必须在同一个类中进行
- 子类无法重载父类的函数,父类同名函数被将被名称覆盖
- 重载是在编译期间根据参数类型和个数决定函数调用
函数重写:
- 必须发生在父类和子类之间
- 并且父类与子类中的函数必须有完全相同的原型
- 使用 vitual 声明之后能够产生多态(如果不使用 virtual,那叫重定义)
注:多态是在运行期间根据具体对象的类型决定函数调用的。
4.2 类成员函数与虚函数
问: 是否可将类的每个成员函数都声明为虚函数,为什么?
答:虽然可以都声明为虚函数,但不建议这样做。因为通过虚函数表指针 vptr 调用重写函数是在程序运行时进行的,因此需要通过寻址操作才能真正确定应该调用的函数。而普通成员函数在编译时就确定了调用的函数。所以在效率上,虚函数的效率要低很多。因此,出于效率的考虑,没有必要将所有的成员函数都定义为虚函数。
4.3 为什么要定义虚析构函数?
在什么情况下应当声明虚函数?
- 构造函数不是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的虚函数。
- 析构函数可以是虚的。虚析构函数用于指引 delete 运算符正确析构动态对象。
普通析构函数在删除动态派生类对象的调用情况示例图:
问:为什么要定义虚析构函数?
答:想通过父类指针把所有子类对象的析构函数都执行一遍,释放所有的子类资源。
虚析构函数案例:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <stdlib.h>
using namespace std;
class A
{
public:
A()
{
p = new char[20];
strcpy(p, "obja");
cout << "A执行" << endl;
}
virtual ~A()
{
if (p != NULL)
{
delete[] p;
p = NULL;
cout << "~A执行" << endl;
}
}
private:
char *p;
};
class B : public A
{
public:
B()
{
p = new char[20];
strcpy(p, "objb");
cout << "B执行" << endl;
}
~B()
{
if (p != NULL)
{
delete[] p;
p = NULL;
cout << "~B执行" << endl;
}
}
private:
char *p;
};
class C : public B
{
public:
C()
{
p = new char[20];
strcpy(p, "objc");
cout << "C执行" << endl;
}
~C()
{
if (p != NULL)
{
delete[] p;
p = NULL;
cout << "~C执行" << endl;
}
}
private:
char *p;
};
void houtodelete(A *base)
{
delete base;
}
void main()
{
C *myC = new C;
houtodelete(myC);
system("pause");
}
4.4 父类指针和子类指针的步长
- 铁律1: 指针也是一种数据结构,C++ 类对象的指针 p++/p--,仍然可用;
- 指针运算是按照指针所指的类型进行的:
p++ 等价于 p = p + 1,即 p = (unsigned int)basep + sizeof(*p) 步长 - 结论:父类 p++ 与子类 p++ 步长不同,不要混搭,不要用父类指针 ++ 方式操作数组。
父类指针和子类指针的步长不一样的示例:
#include <iostream>
#include <stdlib.h>
using namespace std;
/* 构造函数调用虚函数,能发生多态吗?
* 意思是:定义子类对象时,在子类对象的父类里面能执行虚函数,能实现多态吗?
*/
class Parent
{
public:
Parent(int a = 0)
{
this->a = a;
cout << "Parent 执行" << endl;
}
virtual void print()
{
cout << "我是你爹" << endl;
}
private:
int a;
};
// 成功,一次偶然的成功,比必然的失败更可怕
class Child : public Parent
{
public:
Child(int b = 0) : Parent(0)
{
//this->b = b;
cout << "Chiild 执行" << endl;
}
virtual void print()
{
cout << "我是儿子" << endl;
}
private:
//int b; // 把该语句的注释撤销前,则此时父类和子类的指针的步长一样,指针++/--不会
// 使程序出现core dump;但是撤销后,则会使子类的步长与父类的步长不一致了,
// 再++/--则会出现core dump现象。
};
void howtoplay(Parent *base)
{
base->print();
}
void main()
{
Parent *pP = NULL;
Child *pC = NULL;
Child array[] = { Child(1), Child(2), Child(3) };
pP = array;
pC = array;
pP->print();
pC->print(); // 多态发生
pP++;
pC++;
pP->print();
pC->print(); // 多态发生
pP++;
pC++;
pP->print();
pC->print(); // 多态发生
system("pause");
return;
}
上述例子中两个类的内存分布: