多态
- 多态的概念
- 概念
- 多态的定义及实现
- 多态的构成条件
- 虚函数
- 虚函数的重写
- 虚函数重写的两个例外
- C++11override和final
- 重载,覆盖,隐藏的对比
- 抽象类
- 概念
- 接口继承和实现继承
- 多态的原理
- 虚函数表
- 多态的原理
- 动态绑定与静态绑定
- 单继承和多继承关系中的虚函数表
- 单继承中的虚函数表
- 多继承中的虚函数表
多态的概念
概念
多态即多种形态,当不同对象去执行同一个行为时,会产生不同的状态
多态的定义及实现
多态的构成条件
- 必须通过基类的指针/引用去调用虚函数(
virtual
所修饰) - 被调用的函数必须是虚函数,并且派生类必须对基类的虚函数进行重写
class Person
{
public:virtual void Buyticket(){cout << "Person:买票-全价" << endl;}
};class Student:public Person
{
public:virtual void Buyticket(){cout << "Student:买票-半价" << endl;}
};//多态调用
void Display(Person& p)
{p.Buyticket();
}int main()
{Person pn;Display(pn);Student st;Display(st);return 0;
}
普通调用:和调用对象的类型有关
多态调用:和指针/引用指向的对象有关
class Person
{
public:virtual void Buyticket(){cout << "Person:买票-全价" << endl;}
};class Student:public Person
{
public:virtual void Buyticket(){cout << "Student:买票-半价" << endl;}
};//普通调用
void Display(Person p)
{p.Buyticket();
}int main()
{Person pn;Display(pn);Student st;Display(st);return 0;
}
虚函数
虚函数:被 virtual
修饰的类成员函数称作虚函数
class Person
{
public:virtual void Buyticket(){cout << "Person:买票-全价" << endl;}
};
虚函数的重写
虚函数的重写:派生类中有一个和基类完全相同的虚函数,称派生类的虚函数重写了基类的虚函数
虚函数重写的两个例外
- 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。也就是基类虚函数返回基类对象的指针/引用,派生类虚函数返回派生类对象的指针/引用
class Person
{
public:~Person(){cout << "~Person()" << endl;delete[] _p;}
protected:int* _p = new int[10];
};class Student :public Person
{
public:~Student(){cout << "~Student()" << endl;delete[] _s;}
protected:int* _s = new int[10];
};int main()
{Person pn;Student st;return 0;
}
- 析构函数的重写(基类与派生类析构函数名不同)
先观察如果析构函数不设置为虚函数
class Person
{
public:~Person(){cout << "~Person()" <<endl;delete[] _p;}
protected:int* _p = new int[10];
};class Student :public Person
{
public:~Student(){cout << "~Student()" << endl;delete[] _s;}
protected:int* _s = new int[20];
};int main()
{Person pn;Student st;return 0;
}
运行起来似乎没有什么问题,做如下修改,结果又是如何
int main()
{Person* ptr1 = new Person;Person* ptr2 = new Student;delete ptr1;delete ptr2;return 0;
}
由运行结果可知,发生了内存泄漏,Student
的资源并没有释放;delete
的行为是:使用指针调用析构函数,由于是普通调用,函数与调用对象的类型有关,所以造成了内存泄漏;如果将函数设置为虚函数结构会怎么样呢?
class Person
{
public:virtual ~Person(){cout << "~Person()" << endl;delete[] _p;}
protected:int* _p = new int[10];
};class Student :public Person
{
public:virtual ~Student(){cout << "~Student()" << endl;delete[] _s;}
protected:int* _s = new int[20];
};
内存泄漏的问题完美地解决
虽然函数名不同,这里其实是构成了虚函数的重写,编译器对析构函数的名称做了处理,编译后的析构函数的名称统一处理成 destructor
- 子类虚函数可以不加
virtual
修饰(不推荐)
C++11override和final
- final:修饰虚函数,表示该虚函数不能被重写
- override:检查派生类虚函数是否重写了某类的某个虚函数,如果没有重写编译报错
重载,覆盖,隐藏的对比
抽象类
概念
在虚函数的后面加上=0
,称这个函数为纯虚函数,包含纯虚函数的类称作抽象类,抽象类不能实例化对象;派生类继承后也不能实例化对象,只有重写纯虚函数,派生类才能实例化对象;纯虚函数规定了派生类必须重写
class Car
{
public:virtual void Drive()=0;
};int main()
{Car c;return 0;
}
纯虚函数不可以实例化
class Car
{
public:virtual void Drive() = 0;
};class NIO : public Car
{
public:virtual void Drive(){cout << "安全驾驶" << endl;}
};int main()
{NIO et7;et7.Drive();return 0;
}
接口继承和实现继承
普通函数的继承是是实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现;虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,构造多态,继承的是接口
多态的原理
虚函数表
class Base
{
public:virtual void Test(){cout << "Test()" << endl;}
private:int _a = 0;
};int main()
{Base b;cout << sizeof(b) << endl;return 0;
}
按照以往的方式计算 Base
类的大小:类中成员变量只有 int
类型大小四个字节,成员函数 Test
不占空间,所以类的大小应该是四个字节
运行结果如下
运行结果和预算的结果不一样,这是怎么回事呢?难道是成员函数也要计算大小吗?接下来,通过监视一探究竟
原来对象类中除了成员变量之外,还有_vfptr
,也就是接下来要学习的虚函数表指针,想要学习指针,就先来了解虚函数表
虚函数表:用来存放虚函数,就如上面[0]
中存放的就是Test
的地址;本质就是函数指针数组
class Car
{
public:virtual void Test1(){cout << "Car:Test1()" << endl;}virtual void Test2(){cout << "Car:Test2()" << endl;}
private:int _a = 0;
};class NIO : public Car
{
public:virtual void Test1(){cout << "NIO:Test1()" << endl;}private:int _b = 0;
};int main()
{Car c;NIO et7;return 0;
}
完成重写的虚函数,表函数表对应位置覆盖成重写的虚函数
多态的原理
int main()
{Car c;NIO et7;//多态调用Car* ptr = &c;ptr->Test1();ptr = &et7;ptr->Test1();//普通调用ptr = &c;ptr->Test2();ptr = &et7;ptr->Test2();return 0;
}
运行结果如下
普通调用由调用对象类型决定:Test2()
函数不是虚函数,调用类型是Car
;多态调用由指针/引用指向的对象决定:Test1()
函数是虚函数,两次调用指向的对象都不同
再通过汇编进行观察
普通调用在编译时就确定好的(静态);多态调用在编译时是不确定的(动态)运行之后在,根据调用对象指向的类型,在表函数表中找到对应的函数进行调用
动态绑定与静态绑定
- 静态绑定,在程序编译期间就已经确定程序的行为,也称静态多态
- 动态绑定,在程序运行期间,根据具体的类型确定程序具体的行为,调用具体的函数,也称动态多态
单继承和多继承关系中的虚函数表
单继承中的虚函数表
观察下列代码
class Car
{
public:virtual void test1(){cout << "Car test1()" << endl;}virtual void test2(){cout << "Car test2()" << endl;}private:int _a;
};class NIO:public Car
{
public:virtual void test1(){cout << "NIO test1()" << endl;}virtual void test3(){cout << "NIO test3()" << endl;}private:int _b;
};int main()
{Car c;NIO et7;return 0;
}
通过监视窗口能够发现:在派生类对象et7
中重写了test1
,继承了test2
,但是本身的test3
却没有,在内存窗口看到一个未知的地址,可以猜测是test3
函数,现在就是想办法将其打印出来进行验证
上面了解到虚函数表本质是函数指针数组,接下来通过函数指针数组将虚函数表打印出来
typedef void(*_vfptr)();void Print_vfptr(_vfptr vft[])
{for (int i = 0; vft[i] != nullptr; i++){printf("[%d]:%p->", i, vft[i]);vft[i]();}cout << endl;
}
通过将对象进行取址再强转 int*
获取前四个字节,在类型转换 _vfptr*
传递给函数进行打印
Print_vfptr((_vfptr*)*(int*)&c);Print_vfptr((_vfptr*)*(int*)&et7);
完整代码如下
class Car
{
public:virtual void test1(){cout << "Car test1()" << endl;}virtual void test2(){cout << "Car test2()" << endl;}private:int _a;
};class NIO:public Car
{
public:virtual void test1(){cout << "NIO test1()" << endl;}virtual void test3(){cout << "NIO test3()" << endl;}private:int _b;
};typedef void(*_vfptr)();void Print_vfptr(_vfptr vft[])
{for (int i = 0; vft[i] != nullptr; i++){printf("[%d]:%p->", i, vft[i]);vft[i]();}cout << endl;
}int main()
{Car c;Print_vfptr((_vfptr*)*(int*)&c);NIO et7;Print_vfptr((_vfptr*)*(int*)&et7);return 0;
}
打印结果和预期一致
多继承中的虚函数表
class NIO
{
public:virtual void test1(){cout << "NIO test1()" << endl;}virtual void test2(){cout << "NIO test2()" << endl;}private:int _a;
};class XPENG
{
public:virtual void test1(){cout << "XPENG test1()" << endl;}virtual void test2(){cout << "XPENG test2()" << endl;}private:int _b;
};class NEA :public NIO, public XPENG
{
public:virtual void test1(){cout << "NEA test1()" << endl;}virtual void test3(){cout << "NEA test3()" << endl;}private:int _c;
};int main()
{NIO et7;XPENG p7;NEA car;return 0;
}
通过监视会发现与上面类似的疑问,派生类 NEA
对象本身的虚函数存放在第一个基类 NIO
中还是第二个基类 XPENG
中呢?
接下来再次通过函数指针数组进行验证
typedef void(*_vfptr)();void Print_vfptr(_vfptr vft[])
{for (int i = 0; vft[i] != nullptr; i++){printf("[%d]:%p->", i, vft[i]);vft[i]();}cout << endl;
}class NIO
{
public:virtual void test1(){cout << "NIO test1()" << endl;}virtual void test2(){cout << "NIO test2()" << endl;}private:int _a;
};class XPENG
{
public:virtual void test1(){cout << "XPENG test1()" << endl;}virtual void test2(){cout << "XPENG test2()" << endl;}private:int _b;
};class NEA :public NIO, public XPENG
{
public:virtual void test1(){cout << "NEA test1()" << endl;}virtual void test3(){cout << "NEA test3()" << endl;}private:int _c;
};int main()
{NIO et7;Print_vfptr((_vfptr*)*(int*)&et7);XPENG p7;Print_vfptr((_vfptr*)*(int*)&p7);NEA car;//NIO虚函数表Print_vfptr((_vfptr*)*(int*)&car);XPENG* ptr = &p7;//XPENG虚函数表Print_vfptr((_vfptr*)*(int*)ptr);return 0;
}
打印结果显示:派生类本身的虚函数是存放在第一个基类的虚函数表中的