C++中的多态(关于虚...)
1.前置基础知识
1.1对象是如何存储在内存中的
#include <iostream>
#include <string>class Animal {
private:string name;int age;public:Animal(std::string name, int age) : name(name), age(age) {};~Animal();virtual void eat() {std::cout << "Animal在吃东西" << std::endl;}
};class Cat : public Animal {
private:int lives = 0;public:Cat(int lives) : Animal("cat", 18), lives(lives) {}virtual void eat() {std::cout << "cat在吃鱼" << std::endl;}
};int main() {Animal a("dog", 18); // 属性会单独的存储在一个连续的内存空间中,但是函数大家都是一样的,所以不会重复拷贝一份函数和属性存储在一起,是放在另一快内存空间,供大家一起调用Animal b("cat", 12); Cat c(8); // 会申请一片内存空间,从父类继承过来的属性以及虚函数表指针存储在子类的属性和虚函数表指针前,是连续的return 0;
}
1.2虚函数
#include <iostream>
#include <string>class Animal {
public:std::string name;int age;public:Animal(std::string name, int age) : name(name), age(age) {};~Animal();virtual void eat() {std::cout << "Animal在吃东西" << std::endl;}
};class Cat : public Animal {
private:int lives = 0;public:Cat(int lives) : Animal("cat", 18), lives(lives) {}virtual void eat() {std::cout << "cat在吃鱼" << std::endl;}
};int main() {Animal* c = new Cat(8);c->eat();return 0;
}
- 因为是Animal类型,所以调用的eat方法也就是Animal的eat方法
- 给函数加上virtual关键字就代表是虚函数,这个时候调用会根据子类的虚函数表指针去查,根据对象去判断使用父类的方法还是子类重写后的方法
1.3虚函数表
- 加上virtual关键字变为虚函数的函数都会存在虚函数表中,父类子类都有,一开始父类子类的虚函数表都是一样的,子类重写后,就会覆盖子类修函数表对应的位置上的内容
1.4虚函数表指针
-
虚函数表指针顾名思义就是一个指针,指向于虚函数表,与对象的属性存储在一起
-
虚函数 虚函数表 虚函数表指针关系
1.5动态绑定vs静态绑定
-
Animal* c = new Cat("helloKitty", 18);
上述这一行代码,为什么必须是指针或者引用?-
静态绑定+对象切片
-
因为如果
Animal c = new Cat("helloKitty", 18);
这样写,就声明了c是Animal类型的,但是Cat构造法方法中多了一个属性,且重写了,会直接截取对象,使得c对象缺少lives
这个属性和重写后的方法·。此时也为静态绑定,在编译的时候就根据对象类型已经确定了对应方法和属性,、。对象切片:当你将子类对象赋值给父类对象时,只有父类部分被复制,子类特有的部分被"切掉"了。 - 这意味着你失去了子类的特定信息和行为。
静态绑定: - 使用对象(而不是指针或引用)调用方法时,编译器会在编译时决定调用哪个函数版本。 - 这种绑定是基于变量的声明类型,而不是实际对象的类型。
-
-
动态绑定
- 使用父类对象的指针或引用指向子类对象,并不会发生对象切片。因为只是把子类对象的地址赋值给父类指针(把地址传给指向父类指针,由于是虚函数,此时编译器不知道是什么类型),后在运行的时候确定类型,并根据虚函数指针查找虚函数表确认函数地址。
-
-
总结
- 静态绑定(Static Binding):
- 发生时间:编译时
- 决定因素:变量的声明类型
- 工作原理:编译器根据变量的声明类型直接决定调用哪个函数
- 适用情况:非虚函数调用,或通过对象(非指针/引用)调用函数
- 优点:运行效率高,没有额外开销
- 缺点:不支持多态
- 动态绑定(Dynamic Binding):
- 发生时间:运行时
- 决定因素:指针或引用所指对象的实际类型
- 工作原理:通过虚函数表查找并调用正确的函数
- 适用情况:通过基类的指针或引用调用虚函数
- 优点:支持多态,提高代码的灵活性和可扩展性
- 缺点:相比静态绑定有轻微的性能开销
关键区别:
- 确定时机:
- 静态绑定在编译时确定调用哪个函数。
- 动态绑定在运行时确定调用哪个函数。
- 类型判断:
- 静态绑定基于编译时已知的类型信息。
- 动态绑定基于运行时对象的实际类型。
- 灵活性:
- 静态绑定对于编译器来说更直接,但缺乏运行时的灵活性。
- 动态绑定允许在运行时根据实际情况选择正确的函数版本。
- 一句话总结区别
- 静态绑定在编译的时候就确定了,效率较高、内存占用少(编译器默认都是静态绑定)。动态绑定是只有是虚函数才会发生,不然就根据声明类型编译时就确定了。
2.实现多态
2.1何为多态(分为静态多态和动态多态)
-
动态多态
-
动态多态:它允许我们通过基类的引用或指针来调用派生类的重写方法,这样就能在不修改代码的情况下实现多态行为,提高代码的复用性和扩展性。
我理解的多态是,在编写任何相关代码时,如果声明传入的对象是父类的,可以根据虚函数,传入子类从而调用子类重写的方法,达到极高的代码复用性。
-
-
静态多态
- 静态多态:在编译时就确定使用的方法,可以通过重载和或者模板
2.2动态多态必须具备的条件
- 首先必须具备一个具有虚函数的父类和重写虚函数的子类
- 父类指针或引用指向(绑定)子类对象
- 通过动态绑定实现多态
#include <iostream>
#include <string>class Animal {
public:std::string name;int age;public:Animal(std::string name, int age) : name(name), age(age) {};virtual void eat() { // 1.父类虚函数std::cout << "Animal在吃东西" << std::endl;}virtual ~Animal() {delete this;}
};class Cat : public Animal {
private:int lives = 0;public:Cat(int lives) : Animal("cat", 18), lives(lives) {}virtual void eat() { // 1.子类重写父类虚函数std::cout << "cat在吃鱼" << std::endl;}
};void sleep(Animal& a) { // 2.父类引用指向子类对象a.eat(); // 3.进行动态绑定实现多态
}int main() {Animal* c = new Cat(8);c->eat();sleep(*c);Cat* ca = new Cat(9); // 2.父类指针指向子类对象,这个例子可能要难理解些,因为嵌套了一层,显示一个指针再传为引用,引用依靠着这里的指针,如果指针是不满足条件的那么第二次引用也会出问题,类似于高楼大厦特别依靠地基sleep(*ca);return 0;
}
2.3静态多态
- 这里不过多介绍,仅介绍部分概念
- 静态多态通过函数重载和模板,函数重载常见的有运算符重载。模板可以自定义数据类型。
3.多态的注意点
3.1纯虚函数
-
虚函数=0
virtual ~Base() = 0
3.2虚析构函数
-
概念
-
虚基类: 指的是一个类由被其它类虚继承,那么该类就为虚基类。
-
虚基类派生的子类的子类只能接受一份关于虚基类的属性与方法
-
-
当父类指针或引用指向(绑定)子类对象,若虚基类中没有析构函数不是虚函数,那么只会调用父类的析构,不会调用子类的析构函数,可能会造成内存泄漏
#include <iostream>class Base { public:Base() { std::cout << "Base constructor\n"; }virtual ~Base() { std::cout << "Base destructor\n"; } // 虚析构函数 };class Derived : public Base { public:Derived() { std::cout << "Derived constructor\n"; }~Derived() { std::cout << "Derived destructor\n"; } };int main() {Base* obj = new Derived();delete obj; // 正确:会调用Derived的析构函数,然后再调用Base的析构函数return 0; }//输出结果 //Base constructor //Derived constructor //Derived destructor //Base destructor
3.3抽象类和纯析构函数
-
抽象类就是虚函数=0,抽象类必须被继承
#include <iostream>class Base { public:Base() { std::cout << "Base constructor\n"; }virtual ~Base() = 0 // 虚析构函数 };Base::virtual ~Base() {std::cout << "Base析构执行了" << std::endl; }class Derived : public Base { public:Derived() { std::cout << "Derived constructor\n"; }~Derived() { std::cout << "Derived destructor\n"; } };int main() {Derived* obj = new Derived();delete obj;return 0; }
-
纯析构是实现抽象类的一个方法,但是要注意也要实现一下纯析构函数,原因是纯虚析构函数确保基类的析构过程能被正确调用,以便释放派生类对象的资源。没有实现,析构过程会出现未定义行为。这个时候有人会问可是我加了个函数函数体但是是空的却不会有问题,因为纯虚析构和函数体为空是不同的
4多继承和菱形继承
-
多继承:
就是一个子类继承了多个类,注意要避免二义性,就是多个父类含有相同的命名的属性,这个时候需要通过作用域运算符进行访问"::"
-
菱形继承
可与看出子类齐天大圣的两个父类都继承了同一个父类,那么齐天大圣就会有两个关于地球的属性,这就重复了,所以就有了虚继承
-
虚继承: 如果多个基类共享一个公共基类,可以使用虚继承来避免菱形继承问题,确保公共基类只存在一个实例。
-
虚基类: 指的是一个类由被其它类虚继承,那么该类就为虚基类。
-
虚基类派生的子类的子类只能接受一份关于虚基类的属性与方法
-
如何使用
class Base { public:int base;Base() : base(0) {} };class Derived1 : virtual public Base {// 通过虚继承继承 Base };class Derived2 : virtual public Base {// 通过虚继承继承 Base };class Final : public Derived1, public Derived2 { public:void show() {std::cout << "Base::base = " << base << std::endl; // 这里访问 Base::base} };int main() {Final f;f.show(); // 正常输出 Base::basereturn 0; }