C++学习笔记(十六)

一、多态

1. 多态的基本概念

多态是C++面向对象三大特性之一

多态分为两类

  1. 静态多态:函数重载 和 运算符重载属于静态多态,复用函数名

  2. 动态多态:派生类和虚函数实现运行时多态

静态多态和动态多态区别:

  1. 静态多态的函数地址早绑定 ------ 编译阶段确定函数地址

  2. 动态多态的函数地址晚绑定 ------ 运行阶段确定函数地址

cpp 复制代码
#include <iostream>

using namespace std;

// 动物类
class Animal
{
public:
	// 虚函数
	virtual void speak()
	{
		cout << "动物在叫......" << endl;
	}
};

// 猫类
class Cat :public Animal
{
public:
	void speak()
	{
		cout << "小猫在喵喵叫......" << endl;
	}
};

// 狗类
class Dog :public Animal
{
public:
	void speak()
	{
		cout << "小狗在汪汪叫......" << endl;
	}
};

// 执行叫的函数
// 地址早绑定 在编译阶段就确定了函数地址
// 如果想执行让猫叫或者让狗叫,那么这个函数的地址就不能提前绑定,需要在运行阶段进行绑定,地址晚绑定
void doSpeak(Animal& animal) // Animal& animal = cat;父类的引用指向子类的对象
{
	animal.speak();
}

void test()
{
	Cat cat;
	doSpeak(cat);

	Dog dog;
	doSpeak(dog);
}
int main(int argc, char* argv[])
{
	test();
	return 0;
}

动态多态满足的条件:

  1. 有继承关系

  2. 子类重写父类的虚函数

动态多态使用:父类的指针或者引用指向子类对象

重写:函数返回值类型、函数名、参数列表完全一致

重载:函数的参数列表不同

2. 多态的原理

在定义了虚函数的类中,都会有一个虚函数表指针,指向该类的虚函数表,表内记录虚函数的地址

当派生类继承基类时,会继承基类的虚函数表指针,指向基类的虚函数表

如果在派生类中实现了重写,先将基类虚表内容拷贝一份至派生类的虚表当中(派生类的虚表是自己生成的,这两个虚表的地址不一样),然后将重写的虚函数的地址放入虚表的对应位置。

3. 多态案例一:计算机类

案例描述:

分别应用普通写法和多态技术,设计实现两个操作数进行运算的计算器

多态的优点:

  1. 代码组织结构清晰

  2. 可读性强

  3. 利于前期和后期的扩展以及维护

普通实现:

cpp 复制代码
#include <iostream>
#include <string>

using namespace std;

class Calculator
{
public:
	double m_num1;
	double m_num2;

	double 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 == "/")
			return m_num1 / m_num2;
	}
};

void test()
{
	Calculator c;
	c.m_num1 = 20;
	c.m_num2 = 3.3;

	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;
}
int main(int argc, char* argv[])
{
	test();
	return 0;
}

如果想扩展新的功能,需要修改源码

在真实的开发中,提供开闭原则

开闭原则:对扩展进行开放,对修改进行关闭

cpp 复制代码
#include <iostream>
#include <string>

using namespace std;

// 实现计算器基类
class BaseCalculator
{
public:
	double m_num1;
	double m_num2;

	virtual double getResult()
	{
		return 0;
	}
};

// 加法计算器类
class AddCalculator : public BaseCalculator
{
public:
	double getResult()
	{
		return m_num1 + m_num2;
	}
};

// 减法计算器类
class SubCalculator : public BaseCalculator
{
public:
	double getResult()
	{
		return m_num1 - m_num2;
	}
};

// 乘法计算器类
class MulCalculator : public BaseCalculator
{
public:
	double getResult()
	{
		return m_num1 * m_num2;
	}
};

// 除法计算器类
class DivCalculator : public BaseCalculator
{
public:
	double getResult()
	{
		return m_num1 / m_num2;
	}
};

void test()
{
	// 多态使用条件
	// 父类指针或者引用指向子类对象

	// 加法运算
	BaseCalculator *calculator = new AddCalculator;
	calculator->m_num1 = 20;
	calculator->m_num2 = 3.3;
	cout << calculator->m_num1 << " + " << calculator->m_num2 << " = " << calculator->getResult() << endl;
	// 堆区数据手动开辟手动释放
	delete calculator;

	// 减法运算
	calculator = new SubCalculator;
	calculator->m_num1 = 20;
	calculator->m_num2 = 3.3;
	cout << calculator->m_num1 << " - " << calculator->m_num2 << " = " << calculator->getResult() << endl;
	delete calculator;

	// 乘法运算
	calculator = new MulCalculator;
	calculator->m_num1 = 20;
	calculator->m_num2 = 3.3;
	cout << calculator->m_num1 << " * " << calculator->m_num2 << " = " << calculator->getResult() << endl;
	delete calculator;

	// 除法运算
	calculator = new DivCalculator;
	calculator->m_num1 = 20;
	calculator->m_num2 = 3.3;
	cout << calculator->m_num1 << " / " << calculator->m_num2 << " = " << calculator->getResult() << endl;
	delete calculator;
}
int main(int argc, char* argv[])
{
	test();
	return 0;
}

4. 纯虚函数和抽象类

在多态中,通常父类中虚函数的实现是毫无意义的,主要都是调用子类重写的内容

因此可以将虚函数改为纯虚函数

纯虚函数语法:virtual 返回值类型 函数名 (参数列表)= 0;

当类中有了纯虚函数,这个类也称为抽象类

抽象类的特点:

1. 无法实例化对象

2. 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

cpp 复制代码
#include <iostream>

using namespace std;

class Base
{
public:
	virtual void func() = 0;
};

class Son1 : public Base
{
	void func()
	{
		cout << "已对父类的的纯虚函数进行重写..." << endl;
	}
};

class Son2 : public Base
{
public:

};

void test()
{
	// 无法对抽象类进行实例化
	// Base b;
	
	Base* ptr1 = new Son1;
	ptr1->func();
	
	// 子类必须重写抽象类中的纯虚函数,否则也属于抽象类
	// Base* ptr2 = new Son2;
}

int main(int argc, char* argv[])
{
	test();
	return 0;
}

5. 多态案例二:制作饮品

案例描述:制作饮品的大致流程为:煮水 -> 冲泡(咖啡、茶叶) -> 倒入杯中 -> 加入辅料(糖、柠檬)

利用多态技术实现本案例,提供抽象制作饮品基类,提供子类制作咖啡和茶叶

cpp 复制代码
#include <iostream>

using namespace std;

class AbstractDrinking
{
public:
	void Boil()
	{
		cout << "煮水中..." << endl;
	}
	virtual void Brew() = 0;
	void PourInCup()
	{
		cout << "正在倒入杯中..." << endl;
	}
	virtual void PutSomething() = 0;
	void Done()
	{
		cout << "制作完成..." << endl;
	}
	void makeDrink()
	{
		Boil();
		Brew();
		PourInCup();
		PutSomething();
		Done();
	}
};

class Coffee : public AbstractDrinking
{
public:
	void Brew()
	{
		cout << "正在冲泡咖啡..." << endl;
	}
	void PutSomething()
	{
		cout << "正在加入糖..." << endl;
	}
};

class Tea : public AbstractDrinking
{
public:
	void Brew()
	{
		cout << "正在冲泡茶叶..." << endl;
	}
	void PutSomething()
	{
		cout << "正在加入枸杞..." << endl;
	}
};

void doWork(AbstractDrinking *abs)
{
	abs->makeDrink();
	delete abs;
}

void test()
{
	// 制作咖啡
	doWork(new Coffee);
	
	cout << "-------------------" << endl;

	// 制作茶
	doWork(new Tea);
}
int main(int argc, char* argv[])
{
	test();
	return 0;
}

6. 虚析构和纯虚析构

多态使用时,如果子类中有属性开辟到了堆区,那么父类指针在释放时无法调用到子类的析构代码

解决方式:将父类中的析构函数改为虚析构 或者纯虚析构

虚析构和纯虚析构的共性:

  1. 可以解决父类指针释放子类对象

  2. 都需要有具体的函数实现

虚析构和纯虚析构的区别:

  1. 如果时纯虚析构,该类属于抽象类,无法实例化对象

虚析构语法:virtual ~类名( ){ }

纯虚析构语法:需要声明和实现

virtual ~类名() = 0;

类名::~类名( ){ }

cpp 复制代码
#include <iostream>

using namespace std;

class Base
{
public:
	virtual void func() = 0;
	Base()
	{
		cout << "基类的构造函数已调用..." << endl;
	}
	~Base()
	{
		cout << "基类的析构函数已调用..." << endl;
	}
};

class Son : public Base
{
public:
	void func()
	{
		cout << "派生类中重写的纯虚函数已调用..." << endl;
	}
	Son()
	{
		cout << "派生类的构造函数已调用..." << endl;
	}
	~Son()
	{
		cout << "派生类的析构函数已调用..." << endl;
	}
};

void test()
{
	Base* ptr = new Son;
	ptr->func();
	delete ptr;
}

int main(int argc, char* argv[])
{
	test();
	return 0;
}

从运行结果可以看出,父类指针在析构时,不会调用子类中的析构函数,如果子类中有堆区属性,就会出现内存泄漏的问题

利用虚析构可以解决父类指针释放子类对象时不干净的问题

cpp 复制代码
#include <iostream>

using namespace std;

class Base
{
public:
	virtual void func() = 0;
	Base()
	{
		cout << "基类的构造函数已调用..." << endl;
	}
	virtual ~Base()
	{
		cout << "基类的析构函数已调用..." << endl;
	}
};

class Son : public Base
{
public:
	void func()
	{
		cout << "派生类中重写的纯虚函数已调用..." << endl;
	}
	Son()
	{
		cout << "派生类的构造函数已调用..." << endl;
	}
	~Son()
	{
		cout << "派生类的析构函数已调用..." << endl;
	}
};

void test()
{
	Base* ptr = new Son;
	ptr->func();
	delete ptr;
}

int main(int argc, char* argv[])
{
	test();
	return 0;
}

纯虚析构也可以解决这个问题,但是要注意,纯虚析构不同于纯虚函数,纯虚析构需要类内声明类外实现

cpp 复制代码
#include <iostream>

using namespace std;

class Base
{
public:
	virtual void func() = 0;
	Base()
	{
		cout << "基类的构造函数已调用..." << endl;
	}
	virtual ~Base() = 0;
};
Base::~Base()
{
	cout << "基类的纯虚析构函数已调用..." << endl;
}

class Son : public Base
{
public:
	void func()
	{
		cout << "派生类中重写的纯虚函数已调用..." << endl;
	}
	Son()
	{
		cout << "派生类的构造函数已调用..." << endl;
	}
	~Son()
	{
		cout << "派生类的析构函数已调用..." << endl;
	}
};

void test()
{
	Base* ptr = new Son;
	ptr->func();
	delete ptr;
}

int main(int argc, char* argv[])
{
	test();
	return 0;
}

总结:

  1. 虚析构或纯虚析构就是用来解决通过父类指针释放子类对象

  2. 如果子类中没有堆区数据,可以不写为虚析构或纯虚析构

  3. 拥有纯虚析构函数的类也属于抽象类

7. 多态案例三:电脑组装

案例描述:

电脑主要组成部件为CPU(用于计算),显卡(用于显示),内存条(用于存储)

将每个零件封装出抽象类,并且提供不同的厂商生产不同的零件,例如Intel厂商和Lenovo厂商

创建电脑类提供让电脑工作的函数,并且调用每个零件工作的接口

测试时组装三台不同的电脑进行工作

cpp 复制代码
#include <iostream>

using namespace std;

class CPU
{
public:
	virtual void calculate() = 0;
};

class VideoCard
{
public:
	virtual void show() = 0;
};

class Memory
{
public:
	virtual void store() = 0;
};

class IntelCPU : public CPU
{
public:
	void calculate()
	{
		cout << "计算能力:10" << endl;
	}
};

class IntelVideoCard : public VideoCard
{
public:
	void show()
	{
		cout << "显示能力:7" << endl;
	}
};

class IntelMemory : public Memory
{
public:
	void store()
	{
		cout << "存储能力:8" << endl;
	}
};

class LenovoCPU : public CPU
{
public:
	void calculate()
	{
		cout << "计算能力:9" << endl;
	}
};

class LenovoVideoCard : public VideoCard
{
public:
	void show()
	{
		cout << "显示能力:6" << endl;
	}
};

class LenovoMemory : public Memory
{
public:
	void store()
	{
		cout << "存储能力:9" << endl;
	}
};

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->show();
		m_mem->store();
	}
	~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;
};

void test()
{
	// 第一台电脑
	cout << "--------第一台电脑--------" << endl;
	CPU* First_CPU = new IntelCPU;
	VideoCard* First_VideoCard = new IntelVideoCard;
	Memory* First_Memory = new IntelMemory;
	Computer* c1 = new Computer(First_CPU, First_VideoCard, First_Memory);
	c1->work();
	delete c1;
	
	// 第二台电脑
	cout << "--------第二台电脑--------" << endl;
	CPU* Second_CPU = new LenovoCPU;
	VideoCard* Second_VideoCard = new LenovoVideoCard;
	Memory* Second_Memory = new LenovoMemory;
	Computer* c2 = new Computer(Second_CPU, Second_VideoCard, Second_Memory);
	c2->work();
	delete c2;

	// 第三台电脑
	cout << "--------第三台电脑--------" << endl;
	CPU* Third_CPU = new IntelCPU;
	VideoCard* Third_VideoCard = new LenovoVideoCard;
	Memory* Third_Memory = new IntelMemory;
	Computer* c3 = new Computer(Third_CPU, Third_VideoCard, Third_Memory);
	c3->work();
	delete c3;

}
int main(int argc, char* argv[])
{
	test();
	return 0;
}

二、文件操作

程序运行时产生的数据都属于临时数据,程序一旦运行结束都会被释放

通过文件可以将数据持久化

C++中对文件操作需要包含头文件 <fstream>

文件类型分为两种:

  1. 文本文件 :文件以文本的ASCLL码形式存储在计算机中

  2. 二进制文件 :文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂他们

操作文件的三大类:

  1. ofstream:写操作

  2. ifstream:读操作

  3. fstream:读写操作

1. 文本文件

1.1 写文件

写文件步骤如下:

  1. 包括头文件

include <fstream>

  1. 创建流对象

ofstream ofs;

  1. 打开文件

ofs.open("文件路径",打开方式);

  1. 写数据

ofs << "写入的数据";

  1. 关闭文件

ofs.close();

文件打开方式:

|-------------|---------------|
| 打开方式 | 解释 |
| ios::in | 为读文件而打开文件 |
| ios::out | 为写文件而打开文件 |
| ios::ate | 初始位置:文件尾 |
| ios::app | 追加方式写文件 |
| ios::trunc | 如果文件存在先删除,再创建 |
| ios::binary | 二进制方式 |

注意:文件打开方式可以配合使用,利用 | 操作符

例如:用二进制方式写文件: ios::binary | ios::out

cpp 复制代码
#include <iostream>
#include <fstream>

using namespace std;


int main(int argc, char* argv[])
{
	// 1. 包含头文件 fstream

	// 2. 创建流对象
	ofstream ofs;

	// 3. 指定打开方式
	ofs.open("D:\\test.txt", ios::out);

	// 4. 写内容
	ofs << "姓名:张三" << endl;
	ofs << "年龄:23" << endl;
	ofs << "性别:男" << endl;
	ofs << "居住地:地球" << endl;

	// 5. 关闭文件
	ofs.close();
	return 0;
}

总结:

  1. 文件操作必须包含头文件 fstream

  2. 读文件可以利用ofstream或者fstream类

  3. 打开文件时需要指定操作文件的路径,以及打开方式

  4. 利用 << 可以向文件中写数据

  5. 操作完成后,需要关闭文件

1.2 读文件

读文件与写文件步骤相似,但是读取方式相对比较多

读文件步骤如下:

  1. 包含头文件

include <fstream>

  1. 创建流对象

ifstream ifs;

  1. 打开文件并判断文件是否打开成功

ifs.open("文件路径",打开方式)

  1. 读数据

四种方式读取

  1. 关闭文件

ifs.close()

cpp 复制代码
#include <iostream>
#include <fstream>
#include <string>

using namespace std;


void main(int argc, char* argv[])
{
	// 1. 包含头文件 fstream

	// 2. 创建流对象
	ifstream ifs;

	// 3. 指定打开方式
	ifs.open("D:\\test.txt", ios::in);

	// 判断文件是否打开成功
	if (!ifs.is_open())
	{
		cout << "文件打开失败..." << endl;
		return;
	}

	// 4. 读内容
	// (1).第一种
	char buffer[1024] = { 0 };
	while (ifs >> buffer)
	{
		cout << buffer << endl;
	}
	// (2).第二种
	char buffer[1024] = { 0 };
	while (ifs.getline(buffer,sizeof(buffer)))
	{
		cout << buffer << endl;
	}
	// (3).第三种
	string buffer;
	while (getline(ifs,buffer))
	{
		cout << buffer << endl;
	}
	// (4).第四种
	char c;
	while ((c = ifs.get()) != EOF)
	{
		cout << c;
	}

	// 5. 关闭文件
	ifs.close();
}

2. 二进制文件

以二进制的方式对文件进行读写操作

打开方式要指定为 ios::binary

2.1 写文件

二进制方式写文件主要利用流对象调用成员函数 write

函数原型:ostream& write(const char * buffer, int len);

参数解释:字符指针buffer指向内存中一段存储空间。len时读写的字节数

cpp 复制代码
#include <iostream>
#include <fstream>

using namespace std;

class Person
{
public:
	char m_Name[64];
	int m_Age;
};

void test()
{
	// 1. 包含头文件 fstream

	// 2. 创建流对象
	ofstream ofs;

	// 3. 指定打开方式
	ofs.open("D:\\test.txt", ios::out | ios::binary);

	// 4. 写内容
	Person p = { "张三",18 };
	ofs.write((const char*)&p, sizeof(Person));

	// 5. 关闭文件
	ofs.close();
}

int main(int argc, char* argv[])
{
	test();
	return 0;
}

总结:

文件输出流对象可以通过write函数,以二进制方式写数据

2.2 读文件

二进制方式读文件主要利用流对象调用成员函数read

函数原型:istream& read(char * buffer, int len);

参数解释:字符串指针buffer指向内存中一段存储空间。len时读写的字节数

cpp 复制代码
#include <iostream>
#include <fstream>

using namespace std;

class Person
{
public:
	char m_Name[64];
	int m_Age;
};

void test()
{
	// 1. 包含头文件 fstream

	// 2. 创建流对象
	ifstream ifs;

	// 3. 指定打开方式
	ifs.open("D:\\test.txt", ios::in | ios::binary);

	// 判断文件是否打开成功
	if (!ifs.is_open())
	{
		cout << "文件打开失败..." << endl;
		return;
	}
	// 4. 写内容
	Person p;
	ifs.read((char*)&p, sizeof(Person));
	cout << "姓名:" << p.m_Name << endl;
	cout << "年龄:" << p.m_Age << endl;

	// 5. 关闭文件
	ifs.close();
}

int main(int argc, char* argv[])
{
	test();
	return 0;
}
相关推荐
守护者17020 分钟前
JAVA学习-练习试用Java实现“使用Arrays.toString方法将数组转换为字符串并打印出来”
java·学习
源码哥_博纳软云22 分钟前
JAVA同城服务场馆门店预约系统支持H5小程序APP源码
java·开发语言·微信小程序·小程序·微信公众平台
学会沉淀。29 分钟前
Docker学习
java·开发语言·学习
Rinai_R44 分钟前
计算机组成原理的学习笔记(7)-- 存储器·其二 容量扩展/多模块存储系统/外存/Cache/虚拟存储器
笔记·物联网·学习
吃着火锅x唱着歌44 分钟前
PHP7内核剖析 学习笔记 第四章 内存管理(1)
android·笔记·学习
ragnwang1 小时前
C++ Eigen常见的高级用法 [学习笔记]
c++·笔记·学习
西猫雷婶1 小时前
python学opencv|读取图像(二十一)使用cv2.circle()绘制圆形进阶
开发语言·python·opencv
kiiila1 小时前
【Qt】对象树(生命周期管理)和字符集(cout打印乱码问题)
开发语言·qt
小_太_阳1 小时前
Scala_【2】变量和数据类型
开发语言·后端·scala·intellij-idea
胡西风_foxww1 小时前
【es6复习笔记】rest参数(7)
前端·笔记·es6·参数·rest