目录
一.背景介绍
虚函数重写:子类重新定义父类中有相同函数名,返回值和参数的虚函数
非虚函数重写:子类重新定义父类中有相同名称和参数的非虚函数
继承中的类型兼容性原则
1.子类对象可以作为父类对象使用
2.子类对象可以直接赋值给父类对象
3.子类对象可以初始化父类对象
4.父类指针可以直接指向子类对象
5.父类引用可以直接引用子类对象
当发生赋值兼容时,子类对象退化为父类对象,只能访问父类中定义的成员。
类型兼容规则是多态性的重要基础之一(子类就是特殊的父类)
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
int mi;
Parent() : mi(0) {}
void add(int i)
{
mi += i;
}
void add(int a, int b)
{
mi += (a + b);
}
};
class Child : public Parent
{
public:
int mi;
Child() : mi(0) {}
void add(int x, int y, int z)
{
mi += (x + y + z);
}
};
int main()
{
Parent p;
Child c;
c.mi = 100;
p = c; // p.mi = 0; 子类对象退化为父类对象
Parent p1(c); // p1.mi = 0; 同上
Parent & rp = c;
Parent * pp = &c;
rp.add(5);
pp->add(10, 20);
cout << "p.mi: " << p.mi << endl; // p.mi: 0;
cout << "p1.mi: " << p1.mi << endl; // p1.mi: 0;
cout << "c.Parent::mi: " << c.Parent::mi << endl; // c.Parent::mi: 35
cout << "rp.mi: " << rp.mi << endl; // rp.mi: 35
cout << "pp->mi: " << pp->mi << endl; // pp->mi: 35
return 0;
}
在面向对象的继承关系中,我们了解到子类可以拥有父类的所有属性和行为;但是,有时父类所提供的方法不能满足子类的要求,因此我们要在子类中重写父类的方法。尽管我们可以通过函数重写(非虚函数)解决这个问题,但是在类型兼容性原则当中也不能出现我们实际所期待的结果()不能够根据指针/引用所指向的实际对象类型去调用相应的重写函数),下面我们用代码去复现这个问题。
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
void print()
{
cout << "I'm Parent." << endl;
}
};
class Child : public Parent
{
public:
void print()
{
cout << "I'm Child." << endl;
}
};
void how_to_print(Parent * p)
{
p->print();
}
int main()
{
Parent p;
Child c;
how_to_print(&p); // I'm Parent // Expected to print: I'm Parent.
how_to_print(&c); // I'm Parent // Expected to print: I'm Child.
return 0;
}
为什么上述用父类指针指向子类对象时,子类对象调用父类的成员函数呢? 这是因为在编译器编译期间,编译器只能根据指针的类型去判断所指向的对象;根据赋值兼容,编译器认为父类指针指向的是父类的对象,所以编译结果可能是调用父类中定义的同名函数。
在调用这个函数时,编译器不可能知道指针究竟指向了什么。但是编译器没有理由报错,于是编译器认为最安全的做法就是调用父类中的print()函数。
要解决这个问题,就需要用到C++中的多态。那么如何实现C++的多态呢?
二.多态的实现原理
1.什么是多态?
概念:同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果,这就是多态性。 简单的说:就是用基类的引用指向子类的对象。
2.多态的分类
在编译器把函数或模板连接生产执行代码的过程中,有两种联编方式,一种是静态联编,另外一种是动态联编。
静态联编是在编译阶段就把函数连接起来,就可以确定调用哪个函数或者模板,而动态联编是指在程序运行时才能确定函数和实现的连接,才能确定调用哪个函数。
由编译器的联编方式,我们可以把多态分为静态多态和动态多态
#include <iostream>
#include <string>
using namespace std;
class Parent
{
public:
virtual void func()
{
cout << "Parent::void func()" << endl;
}
virtual void func(int i)
{
cout << "Parent::void func(int i) : " << i << endl;
}
virtual void func(int i, int j)
{
cout << "Parent::void func(int i, int j) : " << "(" << i << ", " << j << ")" << endl;
}
};
class Child : public Parent
{
public:
void func(int i, int j)
{
cout << "Child::void func(int i, int j) : " << i + j << endl;
}
void func(int i, int j, int k)
{
cout << "Child::void func(int i, int j, int k) : " << i + j + k << endl;
}
};
void run(Parent * p)
{
p->func(1, 2); // 展现多态的特性
// 动态联编
}
int main()
{
Parent p;
p.func(); // 静态联编
p.func(1); // 静态联编
p.func(1, 2); // 静态联编
cout << endl;
Child c;
c.func(1, 2); // 静态联编
cout << endl;
run(&p);
run(&c);
return 0;
}
3.多态的实现原理
虚函数表与vptr指针
1. 当类中声明虚函数时,编译器会在类中生成一个虚函数表;
2. 虚函数表是一个存储类成员函数指针的数据结构;
3. 虚函数表是由编译器自动生成与维护的;
4. virtual成员函数会被编译器放入虚函数表中;
5. 存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)。
多态执行过程:
1. 在类中,用 virtual 声明一个函数时,就会在这个类中对应产生一张 虚函数表,将虚函数存放到该表中;
2. 用这个类创建对象时,就会产生一个 vptr指针,这个vptr指针会指向对应的虚函数表;
3. 在多态调用时, vptr指针 就会根据这个对象 在对应类的虚函数表中 查找被调用的函数,从而找到函数的入口地址;
如果这个对象是 子类的对象,那么vptr指针就会在 子类的 虚函数表中查找被调用的函数
如果这个对象是 父类的对象,那么vptr指针就会在 父类的 虚函数表中查找被调用的函数
如何证明vptr指针?
由类的大小==类中定义成员变量的大小。在有虚函数的类中,类的大小 == 成员变量的大小 + vptr指针大小。
#include <iostream>
#include <string>
using namespace std;
class Demo1
{
private:
int mi; // 4 bytes
int mj; // 4 bytes
public:
virtual void print() {} // 由于虚函数的存在,在实例化类对象时,就会产生1个 vptr指针
};
class Demo2
{
private:
int mi; // 4 bytes
int mj; // 4 bytes
public:
void print() {}
};
int main()
{
cout << "sizeof(Demo1) = " << sizeof(Demo1) << " bytes" << endl; // sizeof(Demo1) = 16 bytes
cout << "sizeof(Demo2) = " << sizeof(Demo2) << " bytes" << endl; // sizeof(Demo2) = 8 bytes
return 0;
}
// 64bit(OS) 指针占 8 bytes
// 32bit(OS) 指针占 4 bytes
三.总结
多态实现面向对象重要的特性之一,掌握多态的具体实现原理,以及为什么引入多态,可以为我们日后使用多态提供基础。