🎩 欢迎来到技术探索的奇幻世界👨💻
📜 个人主页:@一伦明悦-CSDN博客
✍🏻 作者简介: C++软件开发、Python机器学习爱好者
🗣️ 互动与支持:💬评论 👍🏻点赞 📂收藏 👀关注+
如果文章有所帮助,欢迎留下您宝贵的评论,点赞加收藏支持我,点击关注,一起进步!
目录
前言
在面向对象编程中,多态(Polymorphism)是面向对象编程中的一个重要概念,它允许不同对象对同一消息作出不同的响应。在C++中,多态性通过虚函数和动态绑定实现。当基类指针或引用调用一个虚函数时,程序会在运行时根据对象的实际类型来确定调用哪个版本的函数,而不是根据指针或引用的类型来确定。这样,同样的操作可以根据对象的实际类型产生不同的行为,从而实现多态性。
正文
01-多态的基本概念
多态(Polymorphism)是面向对象编程中的一个核心概念,它允许不同对象对同一消息作出不同的响应。在C++中,多态性通过虚函数和动态绑定实现。
具体来说,多态性可以通过以下步骤实现:
定义基类(父类):定义一个包含虚函数的基类,这个虚函数可以在派生类中被重写。
创建派生类(子类):创建一个或多个派生类,这些派生类继承了基类的虚函数,并可以重写这些函数以实现特定行为。
使用基类指针或引用:在需要多态行为的地方,使用基类的指针或引用来指向派生类的对象。
调用虚函数:通过基类指针或引用调用虚函数。在运行时,程序会根据指针或引用指向的实际对象类型来确定调用哪个版本的函数。
以下是一个简单的示例:在这个例子中,基类 Shape
声明了一个虚函数 draw()
,而派生类 Circle
和 Rectangle
分别重写了这个虚函数以实现绘制不同形状的功能。在 main()
函数中,基类指针 shape1
和 shape2
分别指向 Circle
和 Rectangle
对象,通过调用虚函数 draw()
实现了多态性,即同样的操作根据对象的实际类型产生了不同的行为。
#include <iostream>
// 基类 Shape
class Shape {
public:
// 声明虚函数
virtual void draw() {
std::cout << "绘制形状" << std::endl;
}
};
// 派生类 Circle
class Circle : public Shape {
public:
// 重写基类的虚函数
void draw() override {
std::cout << "绘制圆形" << std::endl;
}
};
// 派生类 Rectangle
class Rectangle : public Shape {
public:
// 重写基类的虚函数
void draw() override {
std::cout << "绘制矩形" << std::endl;
}
};
int main() {
// 创建基类指针,指向派生类对象
Shape* shape1 = new Circle();
Shape* shape2 = new Rectangle();
// 调用虚函数,实现多态性
shape1->draw(); // 绘制圆形
shape2->draw(); // 绘制矩形
// 释放内存
delete shape1;
delete shape2;
return 0;
}
下面给出具体代码分析应用过程:
这段代码演示了动态多态的概念。首先定义了一个基类 Animal
和一个派生类 Cat
。在基类中声明了一个虚函数 speak()
,并在派生类中重写了这个虚函数。
在 main()
函数中,通过传入 Cat
对象到函数 doSpeak()
中,利用基类的引用参数,实现了动态多态性。因为 speak()
函数是虚函数,并且通过基类引用调用,程序在运行时根据实际对象类型确定调用的版本,从而实现了多态性。
另外,通过 sizeof(Animal)
的输出可以看到,即使基类中只有一个虚函数,但其大小仍然为 1 字节,因为 C++ 编译器要确保每个对象有一个唯一的地址,即使是空类也需要占用一个字节的空间。
这段代码通过动态多态性展示了面向对象编程中的核心概念之一,即不同对象对同一消息作出不同的响应。
#include<iostream>
using namespace std;
// 多态
// 动物类
class Animal
{
public:
// 这里加了virtual之后,就变成了虚函数 ,就变成了多态的形式
// 此时就是一个非静态成员函数,不属于类对象上,因此相当于一个空类,空的类占用1个字节
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
// 猫类
class Cat : public Animal
{
public:
void speak()
{
cout << "小猫在说话" << endl;
}
};
//执行说话的函数
// 地址已经早绑定,在编译阶段确定函数地址
// 如果执行想让猫说话,那么这个函数地址就不能提前绑定,需要在运行阶段进行绑定,地址晚绑定
void doSpeak(Animal &animal) // 这里是使用引用的方式传递,引用做参数传递
{
animal.speak(); // 写这个函数只是为了验证多态而用,当父类中不加virtual时,不是虚函数,并不成为多态,
// 因此在子类中调用函数时,即使传入的是子类的对象,也无法调用子类函数
}
// 动态多态满足条件
// 1、存在继承关系
// 2、子类中要重写父类的虚函数 重写就是 子类中的函数返回值类型,函数名 参数列表都要与父类相同
// 重写和重载是有区别的,重载必须在同一个作用域下
// 动态多态的使用
// 父类的指针或者引用,指向子类对象
void test01()
{
Cat cat;
doSpeak(cat);
}
void test02()
{
cout << "Animal size of = " << sizeof(Animal) << endl;
}
int main()
{
test01();
// test02();
system("pause");
return 0;
}
实例运行结果如下图所示:
02-多态案例--计算机类的实现
下面给出一个建立计算机类的代码分析应用过程:
这段代码演示了利用多态实现计算器的方式。首先定义了一个抽象基类 AbstractCalculator
,其中包含一个纯虚函数 getResult()
,并且声明了两个成员变量 m_A
和 m_B
用于进行计算。然后分别创建了加法计算器类 AddCalculator
、减法计算器类 SubCalculator
和乘法计算器类 MulCalculator
,它们都继承自抽象基类,并实现了 getResult()
函数来完成具体的计算。
在 test02()
函数中,通过创建父类指针 AbstractCalculator* abc
,并使用 new
运算符动态分配内存来创建具体的子类对象,并通过父类指针调用虚函数 getResult()
实现了多态行为。这样的设计使得计算器的组织结构更清晰,可读性更强,同时也提高了代码的可扩展性和维护性。
值得注意的是,在使用动态内存分配时,需要在合适的时机使用 delete
运算符释放内存,以避免内存泄漏。
#include<iostream>
using namespace std;
// 分别利用普通写法和多态实现计算器
// 普通写法
class Calculator
{
public:
int getResult(string oper)
{
if (oper == "+")
{
return m_A + m_B;
}
else if(oper== "-")
{
return m_A - m_B;
}
else if (oper == "*")
{
return m_A * m_B;
}
// 如果想扩展新的功能,需要改变源码
// 在真实的开发中,提倡 开闭原则
// 开闭原则 : 对扩展进行开放,对修改进行关闭 也就是说尽量别修改源码
}
int m_A;
int m_B;
};
// 测试普通计算器写法
void test01()
{
Calculator c;
c.m_A = 10;
c.m_B = 10;
cout << c.m_A << "+" << c.m_B << "=" << c.getResult("+") << endl;
cout << c.m_A << "-" << c.m_B << "=" << c.getResult("-") << endl;
cout << c.m_A << "*" << c.m_B << "=" << c.getResult("*") << endl;
}
// 利用多态实现计算器
/* 好处 1、组织结构清晰
2、可读性强
3、对于前期和后期扩展以及维护性高
*/
// 实现抽象计算器类
class AbstractCalculator
{
public:
virtual int getResult() = 0;
int m_A;
int m_B;
};
// 加法计算器类
class AddCalculator :public AbstractCalculator
{
public:
virtual int getResult()
{
return m_A + m_B;
}
};
// 减法计算器类
class SubCalculator :public AbstractCalculator
{
public:
virtual int getResult()
{
return m_A - m_B;
}
};
// 乘法计算器类
class MulCalculator :public AbstractCalculator
{
public:
virtual int getResult()
{
return m_A * m_B;
}
};
// 多态使用
void test02()
{
// 多态使用条件
// 父类指针或者引用指向子对象 new 加数据类型,可以等于指针,开辟于堆区的数据
AbstractCalculator *abc = new AddCalculator;
abc->m_A = 10;
abc->m_B = 10;
cout << abc->m_A << "+" << abc->m_B << "=" << abc->getResult() << endl;
// 释放堆区数据
delete abc;
// 这里只是将指针数据释放了,指针没有任何改变。因此可以继续使用
abc = new SubCalculator;
abc->m_A = 10;
abc->m_B = 10;
cout << abc->m_A << "-" << abc->m_B << "=" << abc->getResult() << endl;
abc = new MulCalculator;
abc->m_A = 10;
abc->m_B = 10;
cout << abc->m_A << "*" << abc->m_B << "=" << abc->getResult() << endl;
}
int main()
{
// test01();
test02();
system("pause");
return 0;
}
实例运行结果如下图所示:
03-纯虚函数和抽象类
纯虚函数和抽象类是面向对象编程中的重要概念,用于实现接口和多态性。解释如下:
纯虚函数(Pure Virtual Function):
纯虚函数是在基类中声明但没有定义的虚函数。它用 virtual
关键字声明,然后在声明结尾处加上 = 0
。
纯虚函数没有函数体,因此不能在基类中实现它,而是在派生类中重写该函数来实现特定的行为。
基类中含有纯虚函数的类被称为抽象类。
抽象类(Abstract Class):
抽象类是包含纯虚函数的类,不能被实例化为对象。它主要用于提供接口以及作为其他类的基类。
抽象类中可以包含普通的成员函数和数据成员,但其中至少有一个纯虚函数是未实现的。
派生类必须实现所有基类中的纯虚函数,否则它们也将成为抽象类。
纯虚函数和抽象类的主要作用在于:
a、定义接口:通过纯虚函数定义接口,派生类必须实现这些接口。
b、实现多态性:通过基类指针或引用指向派生类对象,调用虚函数实现多态行为。
示例代码如下:
#include <iostream>
// 抽象基类 Animal
class Animal {
public:
// 纯虚函数,未实现
virtual void speak() const = 0;
};
// 派生类 Cat
class Cat : public Animal {
public:
// 实现纯虚函数
void speak() const override {
std::cout << "Meow!" << std::endl;
}
};
// 派生类 Dog
class Dog : public Animal {
public:
// 实现纯虚函数
void speak() const override {
std::cout << "Woof!" << std::endl;
}
};
int main() {
// 创建基类指针,指向派生类对象
Animal* animal1 = new Cat();
Animal* animal2 = new Dog();
// 调用虚函数,实现多态性
animal1->speak(); // 输出:Meow!
animal2->speak(); // 输出:Woof!
// 释放内存
delete animal1;
delete animal2;
return 0;
}
下面给出具体代码分析应用过程:这段代码展示了抽象类的概念和纯虚函数的用法。解释如下:
抽象类(Abstract Class):Base
类中声明了一个纯虚函数 func()
,使得 Base
成为抽象类。抽象类无法被实例化对象,主要用于提供接口和规范派生类的行为。抽象类中可以包含其他成员函数和数据成员,但至少有一个纯虚函数是未实现的。
纯虚函数(Pure Virtual Function):func()
被声明为纯虚函数,语法是在函数声明结尾处加上 = 0
。派生类必须重写(实现)基类中的纯虚函数,否则它们也将成为抽象类。
在 Son
类中,重写了 func()
函数,因此 Son
类不再是抽象类,并且可以实例化对象。test01()
函数演示了如何使用 Son
类的对象,首先创建了一个 Son
类的对象 son
,然后调用了它的 func()
函数。最后通过 delete
关键字释放了动态分配的内存,以避免内存泄漏。
总之,抽象类和纯虚函数提供了一种接口机制,可以定义一组方法,并要求派生类提供实现。这种设计有助于实现多态性和代码的灵活性。
#include<iostream>
using namespace std;
class Base
{
public:
virtual void func() = 0 ; // 加上这句代码就变成了抽象类,就无法使用该Base类创建对象
// 在多态中,通常父类中的虚函数的实现是毫无意义的,主要都是调用子类中重写的内容
// 因此可以将虚函数改为纯虚函数
// 纯虚函数语法 virtual 返回值类型 函数名 (参数列表) = 0;
// 当类中有了纯虚函数,这个类也称为抽象类
// 1、抽象类无法实例化对象
// 2、子类必须重写抽象类中的虚函数,否则也属于抽象类
};
class Son : public Base
{
public:
virtual void func() // virtual写不写都可以
{
cout << "func函数的调用" << endl;
}
};
void test01()
{
// Base b; 这两句代码都是错误的 无法实例化对象
// new Base;
Son *son= new Son; // Base *base = new Son; 输出一样的结果
son->func();
delete son;
}
int main()
{
test01();
system("pause");
return 0;
}
实例运行结果如下图所示:
04-多态案例--制作饮品
下面给出一个建立饮品类的代码分析应用过程:这段代码展示了利用抽象类和多态实现制作饮品的例子。解释如下:
AbstractDrinking 抽象类:AbstractDrinking
是一个抽象类,其中定义了四个纯虚函数,分别是煮水 (Boil
)、冲泡 (Brew
)、倒入杯中 (PourInCup
)、加入辅料 (PutSomething
)。makeDrink()
函数是一个普通的成员函数,其中调用了上述四个纯虚函数,实现了制作饮品的流程。
Coffee 和 Tea 类:Coffee
和 Tea
都是通过继承 AbstractDrinking
类来实现的具体饮品类。在这两个类中,重写了基类中定义的四个纯虚函数,实现了具体的制作流程。
doWork 函数:doWork
函数接收一个指向 AbstractDrinking
对象的指针,调用其 makeDrink
方法来制作饮品。通过传入不同的饮品对象,可以制作不同种类的饮品。
test01 函数:在 test01
函数中,通过创建 Coffee
和 Tea
对象,分别调用 doWork
函数来制作咖啡和茶。通过多态性,doWork
函数可以接受不同类型的饮品对象(Coffee
或 Tea
),并根据实际对象类型调用对应的制作流程。
代码作用:
通过抽象类和多态的设计,实现了制作饮品的流程规范化,将公共部分提取到抽象类中,具体实现留给子类。
通过示例中的 Coffee
和 Tea
类,展示了如何根据不同的饮品类型来制作不同的饮品,实现了灵活的制作流程。
这种设计模式有助于代码的灵活性和可扩展性,可以轻松添加新的饮品类别而无需修改现有的代码。
#include<iostream>
using namespace std;
// 多态案例二 制作饮品
class AbstractDrinking
{
public:
// 煮水
virtual void Boil() = 0;
// 冲泡
virtual void Brew() = 0;
// 倒入杯中
virtual void PourInCup() = 0;
// 加入辅料
virtual void PutSomething() = 0;
// 制作饮品
void makeDrink()
{
Boil();
Brew();
PourInCup();
PutSomething();
}
};
// 制作咖啡
class Coffee :public AbstractDrinking
{
public:
// 煮水
virtual void Boil()
{
cout << "A" << endl;
}
// 冲泡
virtual void Brew()
{
cout << "B" << endl;
}
// 倒入杯中
virtual void PourInCup()
{
cout << "C" << endl;
}
// 加入辅料
virtual void PutSomething()
{
cout << "D" << endl;
}
};
// 制作茶
class Tea :public AbstractDrinking
{
public:
// 煮水
virtual void Boil()
{
cout << "A" << endl;
}
// 冲泡
virtual void Brew()
{
cout << "B" << endl;
}
// 倒入杯中
virtual void PourInCup()
{
cout << "C" << endl;
}
// 加入辅料
virtual void PutSomething()
{
cout << "D" << endl;
}
};
// 开始工作
// 使用引用的话,下面就不需要使用new创建对象,void doWork(AbstractDrinking &abs)
void doWork(AbstractDrinking *abs)
{
// 这里传入的是一个父类指针,让这个传入的参数调用制作流程,而且,这个父类指针可以指向多个子类,也就说可以制作多种饮品
abs->makeDrink();
delete abs;
}
void test01()
{
// Coffee*coffee = new Coffee;
// doWork(coffee);
// 也可以直接这样写
// Coffee coffee;
// doWork(coffee);
doWork(new Coffee);
cout << "----------------" << endl;
// Tea tea;
// doWork(tea);
doWork(new Tea);
}
int main()
{
test01();
system("pause");
return 0;
}
实例运行结果如下图所示:这里的ABCD中就可以书写制作过程
05-虚析构和纯虚析构
虚析构函数和纯虚析构函数是用于解决在基类指针指向派生类对象时,释放对象时可能导致的资源泄漏的问题。让我详细解释一下:
虚析构函数(Virtual Destructor):虚析构函数是在基类中声明虚拟析构函数,即在析构函数前面加上 virtual
关键字。当基类指针指向派生类对象时,通过基类指针删除对象,可以确保调用派生类的析构函数,从而正确释放对象的资源,防止资源泄漏。
纯虚析构函数(Pure Virtual Destructor):纯虚析构函数是虚析构函数的一种特殊形式,即虚析构函数声明为纯虚函数。纯虚析构函数的声明方法是在虚析构函数后面加上 = 0
。当基类中有纯虚析构函数时,基类就成为抽象类,无法实例化对象,派生类必须实现自己的析构函数才能实例化对象。
示例代码如下:在这个示例中,Base
是一个抽象基类,其中声明了纯虚析构函数 ~Base()
。Derived
类继承自 Base
类,并实现了自己的析构函数 ~Derived()
。在 main()
函数中,通过基类指针 basePtr
创建了 Derived
类的对象,并使用 delete
关键字删除对象,确保了调用了正确的析构函数。
#include <iostream>
// 抽象基类 Base
class Base {
public:
// 纯虚析构函数
virtual ~Base() = 0;
};
// 纯虚析构函数的定义(即使是纯虚析构函数也需要提供实现)
Base::~Base() {
std::cout << "Base destructor" << std::endl;
}
// 派生类 Derived
class Derived : public Base {
public:
// 析构函数的实现
~Derived() {
std::cout << "Derived destructor" << std::endl;
}
};
int main() {
// 创建派生类对象
Base* basePtr = new Derived();
// 通过基类指针删除对象
delete basePtr;
return 0;
}
下面给出具体代码分析应用过程:这段代码展示了虚析构函数和纯虚析构函数的用法。解释如下:
Animal 抽象基类:Animal
类是一个抽象基类,其中声明了一个纯虚函数 speak()
,表示动物的说话行为。
Cat 派生类:Cat
类继承自 Animal
类,实现了 speak()
函数。Cat
类的构造函数接收一个 string
类型的引用,用来初始化猫的名字 m_Name
。
test01 函数:在 test01
函数中,创建了一个 Cat
对象,并通过 Animal
类的指针来调用 speak()
函数。这样做是因为 Animal
类中的 speak()
函数是纯虚函数,不能直接实例化 Animal
对象,但可以通过指针指向派生类对象来调用。
代码的作用是创建了一个猫的对象,并通过指向基类的指针来调用其说话的行为。这种设计方式通过抽象基类和纯虚函数,实现了多态性,可以方便地扩展新的动物类别,并统一调用它们的行为。
#include<iostream>
using namespace std;
#include <string>
// 虚析构和纯虚析构
class Animal
{
public:
// 纯虚函数
virtual void speak() = 0;
};
class Cat :public Animal
{
public:
Cat(string &name)
{
// 这里不这样定义也是可以的
m_Name = name;
}
void speak()
{
cout <<m_Name<< "小猫在说话" << endl;
}
string m_Name;
};
void test01()
{
string str = "Tom"; // 这样传入参数也是可以的,使用引用方式传入,但是牢记不能直接传入数值,需要传入变量
Animal *animal = new Cat(str);
animal->speak();
}
int main()
{
test01();
system("pause");
return 0;
}
实例运行结果如下图所示:
06-多态案例--电脑组装
下面给出一个电脑组装的案例进行分析应用过程:这段代码展示了一个简单的计算机组装程序,通过抽象基类和纯虚函数实现了组装不同厂商的电脑。解释如下:
抽象基类:CPU
、VideoCard
、Memory
分别是抽象基类,其中声明了纯虚函数 calculate()
、display()
、storage()
,分别表示计算、显示和存储功能。
Computer 类:Computer
类拥有三个成员指针,分别指向 CPU、显卡和内存。构造函数接收三个指针,初始化电脑的零件。work()
函数调用各个零件的功能。析构函数用于释放零件所占用的内存。
具体厂商类:IntelCPU
、IntelVideoCard
、IntelMemory
是具体厂商的类,继承了对应的抽象基类,并实现了各自的功能函数。
test01 函数:在 test01
函数中,创建了一个 Intel 品牌的电脑,分别实例化了 Intel 的 CPU、显卡和内存对象,然后通过 Computer
类组装成一台完整的电脑。最后通过调用 work()
函数来运行电脑,然后释放电脑对象的内存。
这段代码展示了面向对象编程中的多态性和抽象类的使用。通过使用抽象基类和纯虚函数,实现了对不同品牌电脑的组装,同时保证了系统的灵活性和扩展性。
#include<iostream>
using namespace std;
// 抽象cpu类
class CPU
{
public:
virtual void calculate() = 0;
};
// 抽象显卡类
class VideoCard
{
public:
virtual void display() = 0;
};
// 抽象内存条类
class Memory
{
public:
virtual void storage() = 0;
};
class Computer
{
public:
Computer(CPU *cpu, VideoCard *vc, Memory*mem)
{
m_cpu = cpu;
m_vc = vc;
m_mem = mem;
}
void work()
{
m_cpu->calculate();
m_vc->display();
m_mem->storage();
}
~Computer()
{
if (m_cpu != NULL)
{
delete m_cpu; // 电脑零件也是在队去开辟的,可以在这里使用析构函数进行释放
m_cpu = NULL;
}
if (m_vc != NULL)
{
delete m_vc;
m_vc = NULL;
}
if (m_mem != NULL)
{
delete m_mem;
m_mem = NULL;
}
}
private:
CPU *m_cpu;
VideoCard *m_vc;
Memory *m_mem;
};
// 具体厂商
// Intel厂商
class IntelCPU :public CPU
{
public:
// 在子类中必须重写纯虚函数
virtual void calculate()
{
cout << "A" << endl;
}
};
class IntelVideoCard :public VideoCard
{
public:
// 在子类中必须重写纯虚函数
virtual void display()
{
cout << "B" << endl;
}
};
class IntelMemory :public Memory
{
public:
// 在子类中必须重写纯虚函数
virtual void storage()
{
cout << "C" << endl;
}
};
void test01()
{
// 电脑零件
CPU *intelCpu = new IntelCPU;
VideoCard *intelCard = new IntelVideoCard;
Memory *intelMem = new IntelMemory;
// 创建第一台电脑
// 两种方法都可以,但是为了节约内存,选择new开辟堆区内存,再用delete释放
// Computer c1 = Computer(intelCpu, intelCard, intelMem);
Computer *c1 = new Computer(intelCpu, intelCard, intelMem);
// Computer(intelCpu, intelCard, intelMem);
c1->work();
delete c1;
}
int main()
{
test01();
system("pause");
return 0;
}
实例运行结果如下图所示:ABC代表步骤
总结
多态是面向对象编程中的一个重要概念,它允许使用基类的指针或引用来调用派生类的方法,从而实现了代码的灵活性和可扩展性。以下是多态的总结:
定义:多态是指相同的消息被不同的对象接收时,可以产生不同的行为。
实现方式:多态可以通过虚函数和继承来实现。通过在基类中声明虚函数,并在派生类中重写这些函数,可以实现运行时多态。
虚函数:虚函数是在基类中声明为虚拟的函数。通过使用 virtual
关键字来声明虚函数,可以使得在运行时根据对象的实际类型来调用对应的函数。
纯虚函数:纯虚函数是在基类中声明为纯虚函数的函数。通过在函数声明末尾加上 = 0
,可以使得基类成为抽象类,无法实例化对象,派生类必须实现纯虚函数才能实例化对象。
多态的优点:
提高了代码的可读性和可维护性,使得代码更加灵活和易于扩展。
降低了耦合度,使得对象之间的关联更加松散。‘
使得代码的逻辑更加清晰,减少了条件判断和分支语句的使用。
总的来说,多态是面向对象编程中的一个重要概念,通过虚函数和继承实现了代码的灵活性和可扩展性,是面向对象设计的核心之一。