C++ 多态
多态按字面的意思就是多种形态。
当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
在 C++ 中,多态(Polymorphism)是面向对象编程的重要特性之一。
C++ 多态允许使用基类指针或引用来调用子类的重写方法,从而使得同一接口可以表现不同的行为。
多态使得代码更加灵活和通用,程序可以通过基类指针或引用来操作不同类型的对象,而不需要显式区分对象类型。这样可以使代码更具扩展性,在增加新的形状类时不需要修改主程序。
以下是多态的几个关键点:
虚函数(Virtual Functions):
- 在基类中声明一个函数为虚函数,使用关键字
virtual
。 - 派生类可以重写(override)这个虚函数。
- 调用虚函数时,会根据对象的实际类型来决定调用哪个版本的函数。
动态绑定(Dynamic Binding):
- 也称为晚期绑定(Late Binding),在运行时确定函数调用的具体实现。
- 需要使用指向基类的指针或引用来调用虚函数,编译器在运行时根据对象的实际类型来决定调用哪个函数。
纯虚函数(Pure Virtual Functions):
- 一个包含纯虚函数的类被称为抽象类(Abstract Class),它不能被直接实例化。
- 纯虚函数没有函数体,声明时使用
= 0
。 - 它强制派生类提供具体的实现。
多态的实现机制:
- 虚函数表(V-Table):C++运行时使用虚函数表来实现多态。每个包含虚函数的类都有一个虚函数表,表中存储了指向类中所有虚函数的指针。
- 虚函数指针(V-Ptr):对象中包含一个指向该类虚函数表的指针。
使用多态的优势:
- 代码复用:通过基类指针或引用,可以操作不同类型的派生类对象,实现代码的复用。
- 扩展性:新增派生类时,不需要修改依赖于基类的代码,只需要确保新类正确重写了虚函数。
- 解耦:多态允许程序设计更加模块化,降低类之间的耦合度。
注意事项:
- 只有通过基类的指针或引用调用虚函数时,才会发生多态。
- 如果直接使用派生类的对象调用函数,那么调用的是派生类中的版本,而不是基类中的版本。
- 多态性需要运行时类型信息(RTTI),这可能会增加程序的开销。
实例 1
我们通过一个简单的实例来了解多态的应用:
实例 1
using namespace std;
// 基类 Animal
class Animal {
public:
// 虚函数 sound,为不同的动物发声提供接口
virtual void sound() const {
cout << "Animal makes a sound" << endl;
}
// 虚析构函数确保子类对象被正确析构
virtual ~Animal() {
cout << "Animal destroyed" << endl;
}
};
// 派生类 Dog,继承自 Animal
class Dog : public Animal {
public:
// 重写 sound 方法
void sound() const override {
cout << "Dog barks" << endl;
}
~Dog() {
cout << "Dog destroyed" << endl;
}
};
// 派生类 Cat,继承自 Animal
class Cat : public Animal {
public:
// 重写 sound 方法
void sound() const override {
cout << "Cat meows" << endl;
}
~Cat() {
cout << "Cat destroyed" << endl;
}
};
// 测试多态
int main() {
Animal* animalPtr; // 基类指针
// 创建 Dog 对象,并指向 Animal 指针
animalPtr = new Dog();
animalPtr->sound(); // 调用 Dog 的 sound 方法
delete animalPtr; // 释放内存,调用 Dog 和 Animal 的析构函数
// 创建 Cat 对象,并指向 Animal 指针
animalPtr = new Cat();
animalPtr->sound(); // 调用 Cat 的 sound 方法
delete animalPtr; // 释放内存,调用 Cat 和 Animal 的析构函数
return 0;
}
程序执行输出为:
Dog barks Dog destroyed Animal destroyed Cat meows Cat destroyed Animal destroyed
代码解释
基类 Animal
:
Animal
类定义了一个虚函数sound()
,这是一个虚函数(virtual
),用于表示动物发声的行为。~Animal()
为虚析构函数,确保在释放基类指针指向的派生类对象时能够正确调用派生类的析构函数,防止内存泄漏。
派生类 Dog
和 Cat
:
Dog
和Cat
类都从Animal
类派生,并各自实现了sound()
方法。Dog
的sound()
输出"Dog barks";Cat
的sound()
输出"Cat meows"。这使得同一个方法(sound()
)在不同的类中表现不同的行为。
主函数 main()
:
- 创建一个基类指针
animalPtr
。 - 使用
new Dog()
创建Dog
对象,将其地址赋给animalPtr
。此时,调用animalPtr->sound()
会输出"Dog barks",因为animalPtr
实际指向的是Dog
对象。 - 释放
Dog
对象时,先调用Dog
的析构函数,再调用Animal
的析构函数。 - 使用
new Cat()
创建Cat
对象并赋给animalPtr
,再调用animalPtr->sound()
,输出"Cat meows",显示多态行为。
关键概念
虚函数:通过在基类中使用
virtual
关键字声明虚函数,派生类可以重写这个函数,从而使得在运行时根据对象类型调用正确的函数。动态绑定:C++ 的多态通过动态绑定实现。在运行时,基类指针
animalPtr
会根据它实际指向的对象类型(Dog
或Cat
)调用对应的sound()
方法。虚析构函数:在具有多态行为的基类中,析构函数应该声明为
virtual
,以确保在删除派生类对象时调用派生类的析构函数,防止资源泄漏。
实例 2
下面的实例中,我们通过多态实现了一个通用的 Shape 基类和两个派生类 Rectangle 和 Triangle。
通过基类指针调用不同的派生类方法,展示了多态的动态绑定特性。
实例 2
当上面的代码被编译和执行时,它会产生下列结果:
Rectangle Area: Rectangle class area: 70 Triangle Area: Triangle class area: 25
代码分析
Shape 类的定义:
Shape
是一个抽象基类,定义了一个虚函数area()
。area()
是用来计算面积的虚函数,并使用了virtual
关键字,这样在派生类中可以重写该函数,进而实现多态。width
和height
是protected
属性,只能在Shape
类及其派生类中访问。
// 基类 Shape,表示形状 class Shape { protected: int width, height; // 宽度和高度 public: // 构造函数,带有默认参数 Shape(int a = 0, int b = 0) : width(a), height(b) { } // 虚函数 area,用于计算面积 virtual int area() { cout << "Shape class area: " << endl; return 0; } };
Rectangle 类的定义:
Rectangle
继承了Shape
类,并重写了area()
方法,计算矩形的面积。area()
方法使用了override
关键字,表示这是对基类Shape
的area()
方法的重写。Rectangle::area()
返回width * height
,即矩形的面积。
// 派生类 Rectangle,表示矩形 class Rectangle : public Shape { public: // 构造函数,使用基类构造函数初始化 width 和 height Rectangle(int a = 0, int b = 0) : Shape(a, b) { } // 重写 area 函数,计算矩形面积 int area() override { cout << "Rectangle class area: " << endl; return width * height; } };
Triangle 类的定义:
Triangle
类也继承自Shape
,并重写了area()
方法,用于计算三角形的面积。Triangle::area()
返回width * height / 2
,这是三角形面积的公式。
// 派生类 Triangle,表示三角形 class Triangle : public Shape { public: // 构造函数,使用基类构造函数初始化 width 和 height Triangle(int a = 0, int b = 0) : Shape(a, b) { } // 重写 area 函数,计算三角形面积 int area() override { cout << "Triangle class area: " << endl; return (width * height / 2); } };
主函数中的多态行为:
- 定义了一个基类指针
shape
,这个指针可以指向任何Shape
类的对象或其派生类的对象。 - 首先将
shape
指针指向Rectangle
对象rec
,然后调用shape->area()
。由于area()
是虚函数,此时会动态绑定到Rectangle::area()
,输出矩形的面积。 - 接着,将
shape
指针指向Triangle
对象tri
,调用shape->area()
时会动态绑定到Triangle::area()
,输出三角形的面积。
// 主函数 int main() { Shape *shape; // 基类指针 Rectangle rec(10, 7); // 矩形对象 Triangle tri(10, 5); // 三角形对象 // 将基类指针指向矩形对象,并调用 area 函数 shape = &rec; cout << "Rectangle Area: " << shape->area() << endl; // 将基类指针指向三角形对象,并调用 area 函数 shape = &tri; cout << "Triangle Area: " << shape->area() << endl; return 0; }
关键概念
虚函数:在基类
Shape
中定义了虚函数area()
。虚函数的作用是让派生类可以重写此函数,并在运行时根据指针的实际对象类型调用适当的函数实现。动态绑定:因为
area()
是虚函数,shape->area()
调用时会在运行时根据shape
实际指向的对象类型(Rectangle
或Triangle
)来调用相应的area()
实现。这种在运行时决定调用哪个函数的机制称为动态绑定,是多态的核心。基类指针的多态性:基类指针
shape
可以指向任何派生自Shape
的对象。当shape
指向不同的派生类对象时,调用shape->area()
会产生不同的行为,这体现了多态的特性。- 在基类中可以有实现。通常虚函数在基类中提供默认实现,但子类可以选择重写。
- 动态绑定:在运行时根据对象的实际类型调用相应的函数版本。
- 可选重写:派生类可以选择性地重写虚函数,但不是必须。
- 必须在基类中声明为
= 0
,表示没有实现,子类必须重写。 - 抽象类:包含纯虚函数的类不能直接实例化,必须通过派生类实现所有纯虚函数才能创建对象。
- 接口定义:纯虚函数通常用于定义接口,让派生类实现具体行为。
虚函数
虚函数是在基类中使用关键字 virtual 声明的函数。
虚函数允许子类重写它,从而在运行时通过基类指针或引用调用子类的重写版本,实现动态绑定。
我们想要的是在程序中任意点可以根据所调用的对象类型来选择调用的函数,这种操作被称为动态链接,或后期绑定。
特点:
实例
using namespace std;
class Animal {
public:
virtual void sound() { // 虚函数
cout << "Animal makes a sound" << endl;
}
};
class Dog : public Animal {
public:
void sound() override { // 重写虚函数
cout << "Dog barks" << endl;
}
};
int main() {
Animal *animal = new Dog();
animal->sound(); // 输出: Dog barks
delete animal;
}
以上代码中,sound 是 Animal 类的虚函数。通过 Animal* 指针 animal 调用 sound() 时,程序会根据实际对象类型(Dog)来选择调用 Dog::sound()。
纯虚函数
您可能想要在基类中定义虚函数,以便在派生类中重新定义该函数更好地适用于对象,但是您在基类中又不能对虚函数给出有意义的实现,这个时候就会用到纯虚函数。
纯虚函数是没有实现的虚函数,在基类中用 = 0 来声明。
纯虚函数表示基类定义了一个接口,但具体实现由派生类负责。
纯虚函数使得基类变为抽象类(abstract class),无法实例化。
特点:
我们可以把基类中的虚函数 area() 改写如下:
= 0 告诉编译器,函数没有主体,上面的虚函数是纯虚函数。
虚函数与纯虚函数的对比
特性 | 虚函数(Virtual Function) | 纯虚函数(Pure Virtual Function) |
---|---|---|
定义 | 基类中使用 virtual 声明,有实现 | 基类中使用 = 0 声明,无实现 |
子类重写 | 子类可以选择重写 | 子类必须实现 |
抽象性 | 可以实例化类 | 使类变为抽象类,无法实例化 |
用途 | 提供默认行为,允许子类重写 | 定义接口,强制子类实现具体行为 |
lqd052
172***[email protected]
参考地址
1、纯虚函数声明如下: virtual void funtion1()=0; 纯虚函数一定没有定义,纯虚函数用来规范派生类的行为,即接口。包含纯虚函数的类是抽象类,抽象类不能定义实例,但可以声明指向实现该抽象类的具体类的指针或引用。
2、虚函数声明如下:virtual ReturnType FunctionName(Parameter) 虚函数必须实现,如果不实现,编译器将报错,错误提示为:
3、对于虚函数来说,父类和子类都有各自的版本。由多态方式调用的时候动态绑定。
4、实现了纯虚函数的子类,该纯虚函数在子类中就变成了虚函数,子类的子类即孙子类可以覆盖该虚函数,由多态方式调用的时候动态绑定。
5、虚函数是C++中用于实现多态(polymorphism)的机制。核心理念就是通过基类访问派生类定义的函数。
6、在有动态分配堆上内存的时候,析构函数必须是虚函数,但没有必要是纯虚的。
7、友元不是成员函数,只有成员函数才可以是虚拟的,因此友元不能是虚拟函数。但可以通过让友元函数调用虚拟成员函数来解决友元的虚拟问题。
8、析构函数应当是虚函数,将调用相应对象类型的析构函数,因此,如果指针指向的是子类对象,将调用子类的析构函数,然后自动调用基类的析构函数。
lqd052
172***[email protected]
参考地址
ZZsprite
125***[email protected]
C++多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数;
形成多态必须具备三个条件:
1、必须存在继承关系;
2、继承关系必须有同名虚函数(其中虚函数是在基类中使用关键字Virtual声明的函数,在派生类中重新定义基类中定义的虚函数时,会告诉编译器不要静态链接到该函数);
3、存在基类类型的指针或者引用,通过该指针或引用调用虚函数;
ZZsprite
125***[email protected]
Coder
cm6***[email protected]
动态联编的实现机制 VTABLE
编译器对每个包含虚函数的类创建一个虚函数表VTABLE,表中每一项指向一个虚函数的地址,即VTABLE表可以看成一个函数指针的数组,每个虚函数的入口地址就是这个数组的一个元素。
每个含有虚函数的类都有各自的一张虚函数表VTABLE。每个派生类的VTABLE继承了它各个基类的VTABLE,如果基类VTABLE中包含某一项(虚函数的入口地址),则其派生类的VTABLE中也将包含同样的一项,但是两项的值可能不同。如果派生类中重载了该项对应的虚函数,则派生类VTABLE的该项指向重载后的虚函数,如果派生类中没有对该项对应的虚函数进行重新定义,则使用基类的这个虚函数地址。
在创建含有虚函数的类的对象的时候,编译器会在每个对象的内存布局中增加一个vptr指针项,该指针指向本类的VTABLE。在通过指向基类对象的指针(设为bp)调用一个虚函数时,编译器生成的代码是先获取所指对象的vtb1指针,然后调用vtb1所指向类的VTABLE中的对应项(具体虚函数的入口地址)。
当基类中没有定义虚函数时,其长度=数据成员长度;派生类长度=自身数据成员长度+基类继承的数据成员长度;
当基类中定义虚函数后,其长度=数据成员长度+虚函数表的地址长度;派生类长度=自身数据成员长度+基类继承的数据成员长度+虚函数表的地址长度。
包含一个虚函数和几个虚函数的类的长度增量为0。含有虚函数的类只是增加了一个指针用于存储虚函数表的首地址。
派生类与基类同名的虚函数在VTABLE中有相同的索引号(或序号)。
Coder
cm6***[email protected]
白菜
109***[email protected]
虚函数这里说的有些乱,因为 C++ 写法奇葩略多。其实可以简单理解。
虚函数可以不实现(定义)。不实现(定义)的虚函数是纯虚函数。
在一个类中如果存在未定义的虚函数,那么不能直接使用该类的实例,可以理解因为未定义 virtual 函数,其类是抽象的,无法实例化。将报错误:
这和其它语言的抽象类,抽象方法是类似的——我们必须实现抽象类,否则无法实例化。(virtual 和 abstract还是有些区别的)
也就是说,如果存在以下代码:
那么,在 main 方法中,我们不能使用 Base base; 这行代码,此时的 tall 没有实现,函数表(vtable)的引用是未定义的,故而无法执行。但我们可以使用 People people; 然后 people.tall(); 或 (&people)->tall(); 因为People实现或者说重写、覆盖了 Base 的纯虚方法 tall(),使其在 People 类中有了定义,函数表挂上去了,于是可以诞生实例了。
上述的是针对虚函数而言,普通的函数,即使我们只声明,不定义,也不会产生上述不可用的问题。
白菜
109***[email protected]
ILSYT
175***[email protected]
父类的虚函数或纯虚函数在子类中依然是虚函数。有时我们并不希望父类的某个函数在子类中被重写,在 C++11 及以后可以用关键字 final 来避免该函数再次被重写。
例:
以上程序运行结果:
如果不希望一个类被继承,也可以使用 final 关键字。
格式如下:
则该类将不能被继承。
ILSYT
175***[email protected]
初次
130***[email protected]
visual studio code1、多态的理念主要就是用到纯虚函数或者虚函数这个,这个c++的特殊处理函数利用父对象指针来访问子对象,然后根据子对象的函数来调用。
结果:
初次
130***[email protected]
泊宇
aiy***[email protected]
A 类继承 B 类,A 类重写了 B 类的虚函数 B2,而 B 类的 B1 中调用了 B2,则 A 类使用 B1 时,运行时 B1 中的 B2 将执行 A 类中重写的虚函数B2。
例程如下:
执行结果:
泊宇
aiy***[email protected]