1. 多态的基本概念
多态是C++面向对象的三大特性之一
多态分为两类:
- 静态多态:函数重载 和 运算符重载 属于静态多态,即复用函数名
- 动态多态:派生类 和 虚函数 实现运行时多态
静态多态和动态多态的区别:
- 静态多态的函数地址早绑定 - 编译阶段确定函数地址
- 动态多态的函数地址晚绑定 - 运行阶段确定函数地址
案例:
#include<iostream>
using namespace std;
//多态
//动物类
class Animal
{
public:
//定义为虚函数,在程序执行时才绑定函数地址
virtual void speak()
{
cout << "动物在说话" << endl;
}
};
//猫类
class Cat : public Animal
{
public:
//重写父类的虚函数(speak 函数),函数返回值类型、函数名、参数列表要完全相同
void speak()
{
cout << "小猫在说话" << endl;
}
};
//狗类
class Dog : public Animal
{
public:
//重写父类的虚函数(speak 函数),函数返回值类型、函数名、参数列表要完全相同
void speak()
{
cout << "小狗在说话" << endl;
}
};
//地址早绑定,在编译阶段就确定了函数的地址
//如果想让猫说话,函数地址不能提前绑定,要在程序执行时绑定函数地址
void doSpeak(Animal &animal) //Animal &animal = cat;
{
animal.speak();
}
void test01()
{
Cat cat;
doSpeak(cat);
Dog dog;
doSpeak(dog);
}
int main()
{
test01();
return 0;
}
输出:
---------------------------------------------------------------------------------
小猫在说话
小狗在说话
总结
实现动态多态要满足的条件:
- 有继承关系
- 父类中的函数指定为虚函数
- 子类重写父类的虚函数
多态使用条件:
- 在类外函数中,形参指定为父类的指针或引用,调用函数时指向子类对象
区分重写和重载:
- 重写:函数返回值类型 函数名 参数列表 完全一致
- 重载:对已有的运算符重新进行定义,赋予其另一种功能,以适应不同的数据类型
2. 多态原理
定义父类内函数为虚函数时,会创建一个虚函数指针vfptr
,指向一个虚函数表vftable
,在表中保存虚函数的函数入口地址&Animal :: speak
,此时若子类继承该父类,子类中也会复制一份同样的虚函数指针和虚函数表,如下图所示:
当子类重写父类的虚函数时,子类虚函数表中的内容会被替换成子类的虚函数地址&Cat :: speak
,如下图所示:
示例:
#include<iostream>
using namespace std;
//多态
//动物类
class Animal1
{
public:
//定义为虚函数,在程序执行时才绑定函数地址
void speak()
{
cout << "动物1在说话" << endl;
}
};
class Animal2
{
public:
//定义为虚函数,在程序执行时才绑定函数地址
virtual void speak()
{
cout << "动物2在说话" << endl;
}
};
void test01()
{
cout << "size of Animal1: " << sizeof(Animal1) << endl;
cout << "size of Animal2: " << sizeof(Animal2) << endl;
}
int main()
{
test01();
return 0;
}
输出:
---------------------------------------------------------------------------------
size of Animal1: 1
size of Animal2: 8
3. 多态案例一:计算器类
案例描述:
分别用普通写法和多态技术,设计实现两个操作数进行运算的计算器类
多态优点:
- 代码组织结构清晰
- 可读性强
- 利用前期和后期的扩展以及维护
示例:
#include<iostream>
using namespace std;
//分别利用普通写法和多态技术实现一个计算器类
//普通写法
//如果想扩展新的功能,需要修改源码
//在真实场景中,提倡:开闭原则
class Calculator
{
public:
int getResult(string oper)
{
if (oper == "+")
{
return m_num1 + m_num2;
}
else if (oper == "-")
{
return m_num1 - m_num2;
}
else if (oper == "*")
{
return m_num1 * m_num2;
}
else if (oper == "/")
{
if (m_num2 == 0)
{
cout << "Error: Division by zero." << endl;
return 0; // 在实际应用中,这里可能需要更复杂的错误处理
}
return m_num1 / m_num2;
}
// 如果操作符不匹配,抛出异常
cout << "Error: Invalid operation." << endl;
return 0;
}
int m_num1; //操作数1
int m_num2; //操作数2
};
void test01()
{
//创建计算器对象
Calculator c;
c.m_num1 = 10;
c.m_num2 = 10;
cout << c.m_num1 << " + " << c.m_num2 << " = " << c.getResult("+") << endl;
cout << c.m_num1 << " - " << c.m_num2 << " = " << c.getResult("-") << endl;
cout << c.m_num1 << " * " << c.m_num2 << " = " << c.getResult("*") << endl;
cout << c.m_num1 << " / " << c.m_num2 << " = " << c.getResult("/") << endl;
}
//利用多态实现计算器
// 多态优点:
// ● 代码组织结构清晰
// ● 可读性强
// ● 利用前期和后期的扩展以及维护
//实现计算器类抽象
class AbstractCalculator
{
public:
virtual int getResult()
{
return 0;
}
int m_num1; //操作数1
int m_num2; //操作数2
};
//加法计算器类
class AddCalculator : public AbstractCalculator
{
public:
int getResult()
{
return m_num1 + m_num2;
}
};
//减法计算器类
class SubCalculator : public AbstractCalculator
{
public:
int getResult()
{
return m_num1 - m_num2;
}
};
//乘法计算器类
class MulCalculator : public AbstractCalculator
{
public:
int getResult()
{
return m_num1 * m_num2;
}
};
//除法计算器类
class DivCalculator : public AbstractCalculator
{
public:
int getResult()
{
if (m_num2 == 0)
{
cout << "Error: Division by zero." << endl;
return 0; // 在实际应用中,这里可能需要更复杂的错误处理
}
return m_num1 / m_num2;
}
};
void test02()
{
//多态使用要满足:父类指针或者引用指向子类对象
//加法运算
AbstractCalculator * abc = new AddCalculator;
abc->m_num1 = 100;
abc->m_num2 = 100;
cout << abc->m_num1 << " + " << abc->m_num2 << " = " << abc->getResult() << endl;
//用完后,手动销毁堆区数据
delete abc;
//减法运算
abc = new SubCalculator;
abc->m_num1 = 100;
abc->m_num2 = 100;
cout << abc->m_num1 << " - " << abc->m_num2 << " = " << abc->getResult() << endl;
//用完后,手动销毁堆区数据
delete abc;
//乘法运算
abc = new MulCalculator;
abc->m_num1 = 100;
abc->m_num2 = 100;
cout << abc->m_num1 << " * " << abc->m_num2 << " = " << abc->getResult() << endl;
//用完后,手动销毁堆区数据
delete abc;
//除法运算
abc = new DivCalculator;
abc->m_num1 = 100;
abc->m_num2 = 100;
cout << abc->m_num1 << " / " << abc->m_num2 << " = " << abc->getResult() << endl;
//用完后,手动销毁堆区数据
delete abc;
}
int main()
{
test01();
cout << "-------------------------" << endl;
test02();
return 0;
}
输出:
--------------------------------------------------------------------------------
10 + 10 = 20
10 - 10 = 0
10 * 10 = 100
10 / 10 = 1
-------------------------
100 + 100 = 200
100 - 100 = 0
100 * 100 = 10000
100 / 100 = 1
4. 纯虚函数和抽象类
在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容
因此可以将虚函数改为纯虚函数
纯虚函数语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,这个类也被称作抽象类
抽象类的特点:
- 无法实例化对象,只能找个儿子去继承
- 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
示例:
#include<iostream>
using namespace std;
//纯虚函数和抽象类
class Base
{
public:
//类中只要有一个纯虚函数,那么这个类就是抽象类
//抽象类的特点:
// ● 无法实例化对象,只能找个儿子去继承
// ● 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
virtual void func() = 0;
};
class Son : public Base
{
public:
void func()
{
cout << "Son类的func函数调用" << endl;
}
};
void test01()
{
// Base b; //抽象类在栈区不能实例化对象
// new Base; //抽象类在堆区不能实例化对象
// Son s; //子类必须重写父类中的纯虚函数,否则无法实例化对象
// s.func();
//使用new关键字创建了一个Son类型的对象,并将其地址赋给了一个指向Base类型的指针base。
Base * base = new Son;
base->func();
delete base;
// Base & base = s;
// base.func();
}
int main()
{
test01();
return 0;
}
输出:
---------------------------------------------------------------------------------
Son类的func函数调用
5. 多态案例二:制作饮品
案例描述:
制作饮品的大致流程为:煮水 - 冲泡 - 倒入杯中 - 加入辅料
利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶
示例:
#include<iostream>
using namespace std;
//多态案例二:制作饮品
class AbstractDrinking
{
public:
//煮水
virtual void Boil() = 0;
//冲泡
virtual void Brew() = 0;
//倒入杯中
virtual void PourInCup() = 0;
//加入辅料
virtual void AddCondiments() = 0;
//制作饮品
void MakeDrink()
{
Boil();
Brew();
PourInCup();
AddCondiments();
}
};
//制作咖啡
class Coffee : public AbstractDrinking
{
public:
//煮水
virtual void Boil()
{
cout << "煮农夫山泉" << endl;
}
//冲泡
virtual void Brew()
{
cout << "冲泡雀巢咖啡" << endl;
}
//倒入杯中
virtual void PourInCup()
{
cout << "倒入水晶杯中" << endl;
}
//加入辅料
virtual void AddCondiments()
{
cout << "加入糖和牛奶" << endl;
}
};
//制作茶叶
class Tea : public AbstractDrinking
{
public:
//煮水
virtual void Boil()
{
cout << "煮百岁山" << endl;
}
//冲泡
virtual void Brew()
{
cout << "冲泡茶叶" << endl;
}
//倒入杯中
virtual void PourInCup()
{
cout << "倒入陶瓷杯中" << endl;
}
//加入辅料
virtual void AddCondiments()
{
cout << "加入枸杞" << endl;
}
};
//制作函数
void doWork(AbstractDrinking *abs)
{
abs->MakeDrink();
delete abs;
}
void test01()
{
//制作咖啡
doWork(new Coffee);
cout << "--------------------------" << endl;
//制作茶叶
doWork(new Tea);
}
int main()
{
test01();
return 0;
}
输出:
--------------------------------------------------------------------------------
煮农夫山泉
冲泡雀巢咖啡
倒入水晶杯中
加入糖和牛奶
--------------------------
煮百岁山
冲泡茶叶
倒入陶瓷杯中
加入枸杞
6. 虚析构和纯虚析构
多态使用时,父类的指针或引用会指向子类对象,但无法访问子类的析构函数。如果子类的析构函数中有属性开辟到堆区,那么父类指针在释放堆内存时,无法释放子类析构函数中的堆内存。
解决方式:将父类中的析构函数改为虚析构或纯虚析构
虚析构和纯虚析构的共性:
- 可以解决父类指针释放子类对象
- 都需要有具体的函数实现
虚析构和纯虚析构的区别:
- 如果是纯虚析构,该类属于抽象类,无法实例化对象
虚析构语法:virtual ~类名()
纯虚析构语法:virtual ~类名() = 0;
示例:
#include<iostream>
using namespace std;
//虚析构和纯虚析构
class Animal
{
public:
Animal()
{
cout << "Animal 构造函数调用" << endl;
}
//利用虚析构,可以解决父类指针释放子类对象时,子类析构函数没有被调用的问题
// virtual ~Animal()
// {
// cout << "Animal 虚析构函数调用" << endl;
// }
//纯虚析构也可以解决这个问题,但需要有声明和实现
//纯虚析构的声明,有了纯虚析构后,这个类也属于抽象类,不能实例化对象
virtual ~Animal() = 0;
//纯虚函数
virtual void speak() = 0;
};
//纯虚析构的实现
Animal::~Animal()
{
cout << "Animal 纯虚析构函数调用" << endl;
}
class Cat : public Animal
{
public:
Cat(string name)
{
cout << "Cat 构造函数调用" << endl;
m_name = new string(name);
}
void speak()
{
cout << *m_name << " 猫在说话" << endl;
}
~Cat()
{
if (m_name != NULL)
{
cout << "Cat 析构函数调用" << endl;
delete m_name;
m_name = NULL;
}
}
string *m_name;
};
void test01()
{
Animal *animal = new Cat("Tom");
animal->speak();
//父类指针在析构时,不会调用子类中的析构函数,导致子类如果有堆区属性,会发生内存泄漏
delete animal;
}
int main()
{
test01();
return 0;
}
输出:
---------------------------------------------------------------------------------
Animal 构造函数调用
Cat 构造函数调用
Tom 猫在说话
Cat 析构函数调用
Animal 纯虚析构函数调用
总结:
- 虚析构和纯虚析构时父类中的概念,且不能同时存在
- 虚析构和纯虚析构的作用相同,就是用来解决通过父类指针释放子类对象的问题
- 如果子类中没有堆区数据,可以不写虚析构和纯虚析构
- 拥有纯虚析构函数的类也属于抽象类
7. new关键字进阶
7.1. new关键字的用法
在C++中,new关键字用于动态分配内存。它可以用来创建单个对象或对象数组,并返回指向新分配的内存的指针。new后面可以跟多种类型的说明符,用以指定需要分配的内存类型。这里是一些常见的用法:
- 基本数据类型:可以使用new来分配基本数据类型(如int、float、char等)的内存。
int* pInt = new int;
float* pFloat = new float;
- 类类型:可以分配自定义类或结构体的实例。
MyClass* pObj = new MyClass;
- 数组:可以分配一个指定大小的数组。数组元素可以是基本数据类型或对象。
int* pArray = new int[10]; // 分配有10个整数的数组
MyClass* pObjects = new MyClass[5]; // 分配包含5个MyClass对象的数组
- 带括号的构造函数参数:可以在使用new创建类实例时传递构造函数参数。
MyClass* pObj = new MyClass(arg1, arg2);
- 带初始值的基本数据类型:可以为基本数据类型指定初始值。
int* pInt = new int(42);
- 带初始化列表的数组:C++11开始,可以为数组元素提供一个初始化列表。
int* pArray = new int[4]{1, 2, 3, 4};
- 带圆括号或花括号的初始化:C++11允许使用圆括号或花括号初始化对象,包括基本数据类型和类类型。
int* pInt = new int{42};
MyClass* pObj = new MyClass{arg1, arg2};
- 放置new(placement new):这是一种高级用法,允许在已分配的内存(指定位置)上构造对象。这不分配内存,但调用构造函数来初始化内存。
void* pMemory = malloc(sizeof(MyClass));
MyClass* pObj = new(pMemory) MyClass;
new的这些用法提供了灵活的动态内存分配机制。使用new分配的内存通常需要使用delete(对于单个对象)或delete[](对于对象数组)来释放,以避免内存泄露。
7.2. 大厂面试:放置new(Placement new)
放置new是C++中的一个高级功能,它允许在已经分配的内存上构造对象,而不是由new表达式分配新内存。这种用法特别适用于需要在特定位置构造对象或需要细粒度控制内存分配的场合。我们来逐步解析这个过程:
7.2.1. 步骤 1: 分配内存
首先,你需要有一块已经分配好的内存。这块内存可以来自多种来源,比如使用malloc从堆上分配、来自一个大的内存池、或者是一个已经存在的、足够大的对象。
void* pMemory = malloc(sizeof(MyClass)); // 使用malloc分配足够存储MyClass对象的内存
这里,我们使用malloc为MyClass类型的对象分配了足够的内存。注意,malloc返回的是一个void*类型的指针,表示这块内存是未类型化的。
7.2.2. 步骤 2: 使用放置new构造对象
一旦有了这块内存,你就可以使用放置new在这块特定的内存上构造一个对象。语法上,放置new看起来像是传递一个额外的参数给new操作符:
MyClass* pObj = new(pMemory) MyClass;
在这个表达式中,pMemory是我们提供给放置new的内存地址。MyClass是我们想要构造的对象类型。这个操作不会分配新的内存,而是在pMemory指向的位置上调用MyClass的构造函数来初始化对象。
7.2.3. 细节解释
放置new的工作原理是重载new操作符。C++标准库提供了这样的重载版本:
void* operator new(size_t, void* p) noexcept;
这个版本的new接受一个size_t大小(通常是由编译器自动提供,对应于对象的大小)和一个指向已分配内存的指针。这个重载仅仅返回传入的指针,实际上不进行任何内存分配。
7.2.4. 使用场景
- 内存池:当你有一大块预分配的内存,并希望在其中高效地分配和回收小块内存时,放置new非常有用。
- 缓存对齐:某些硬件或特定的性能优化要求对象按照特定的边界对齐。通过放置new,你可以确保对象在满足这些要求的地址上构造。
- 异常安全:在已分配内存中构造对象可以避免在内存分配失败时处理复杂的异常逻辑。
7.2.5. 注意事项
- 使用放置new需要确保分配的内存足够大且正确对齐,以存放指定类型的对象。
- 由于malloc和new有不同的内存释放机制(free vs. delete),使用放置new在malloc分配的内存上构造对象时,需要手动调用对象的析构函数,然后使用free释放内存。
例如,销毁在上述例子中创建的对象:
pObj->~MyClass(); // 显式调用析构函数
free(pMemory); // 释放内存
放置new提供了一种灵活的方式来在特定的内存位置上构造对象,但它也要求开发者对内存管理有深入的理解,以避免错误使用。
8. 多态案例三:电脑组装
案例描述:
- 电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储)
- 将每个零件封装出抽象基类,并且提供不同的厂商生产不同的零件,例如:Inter厂商、Lenovo厂商
- 创建电脑类提供让电脑工作的函数,并且调用每个零件工作接口
- 测试时组装三台不同的电脑进行工作
示例:
#include<iostream>
using namespace std;
//多态案例三:电脑组装
//1、抽象出配件类
//1.1 CPU抽象类
class CPU
{
public:
virtual void calculate() = 0;
};
//1.2 显卡抽象类
class VideoCard
{
public:
virtual void display() = 0;
};
//1.3 内存条抽象类
class Memory
{
public:
virtual void storage() = 0;
};
//2、电脑类
class Computer
{
public:
Computer(CPU *cpu, VideoCard *vc, Memory *mem)
{
m_cpu = cpu;
m_videoCard = vc;
m_memory = mem;
}
void doWork()
{
m_cpu->calculate();
m_videoCard->display();
m_memory->storage();
}
//提供析构函数,释放电脑零件占用的内存
~Computer()
{
if (m_cpu != NULL)
{
delete m_cpu;
m_cpu = NULL;
}
if (m_videoCard != NULL)
{
delete m_videoCard;
m_videoCard = NULL;
}
if (m_memory != NULL)
{
delete m_memory;
m_memory = NULL;
}
}
private:
CPU *m_cpu; //CPU的零件指针
VideoCard *m_videoCard; //显卡的零件指针
Memory *m_memory; //内存条的零件指针
};
//3、具体零件厂商类
//3.1 Intel CPU
class InterCpu : public CPU
{
public:
void calculate()
{
cout << "Intel CPU 计算" << endl;
}
};
//3.2 Lenovo CPU
class LenovoCpu : public CPU
{
public:
void calculate()
{
cout << "Lenovo CPU 计算" << endl;
}
};
//3.3 Intel 显卡
class InterVideoCard : public VideoCard
{
public:
void display()
{
cout << "Intel 显卡开始显示" << endl;
}
};
//3.4 Lenovo 显卡
class LenovoVideoCard : public VideoCard
{
public:
void display()
{
cout << "Lenovo 显卡开始显示" << endl;
}
};
//3.5 Intel 内存条
class InterMemory : public Memory
{
public:
void storage()
{
cout << "Intel 内存条开始存储" << endl;
}
};
//3.6 Lenovo 内存条
class LenovoMemory : public Memory
{
public:
void storage()
{
cout << "Lenovo 内存条开始存储" << endl;
}
};
//4、测试(组装电脑)
void test()
{
//1、第一台电脑组装
//1.1 第一台电脑的零件
CPU *inter_cpu = new InterCpu;
VideoCard *inter_card = new InterVideoCard;
Memory *Inter_mem = new InterMemory;
//1.2 创建电脑
Computer *computer1 = new Computer(inter_cpu, inter_card, Inter_mem);
//1.3 电脑工作
cout << "第一台电脑开始工作:" << endl;
computer1->doWork();
//1.4 释放内存
delete computer1;
cout << "------------------------------------------------" << endl;
cout << "第二台电脑开始工作:" << endl;
//2、第二台电脑组装
Computer *computer2 = new Computer(new LenovoCpu, new LenovoVideoCard, new LenovoMemory);
computer2->doWork();
delete computer2;
cout << "------------------------------------------------" << endl;
cout << "第三台电脑开始工作:" << endl;
//2、第三台电脑组装
Computer *computer3 = new Computer(new InterCpu, new LenovoVideoCard, new InterMemory);
computer3->doWork();
delete computer3;
}
int main()
{
test();
return 0;
}
输出:
-------------------------------------------------------------------------------
第一台电脑开始工作:
Intel CPU 计算
Intel 显卡开始显示
Intel 内存条开始存储
------------------------------------------------
第二台电脑开始工作:
Lenovo CPU 计算
Lenovo 显卡开始显示
Lenovo 内存条开始存储
------------------------------------------------
第三台电脑开始工作:
Intel CPU 计算
Lenovo 显卡开始显示
Intel 内存条开始存储