多态是C++面向对象的三大特性之一。
多态分为静态多态和动态多态,静态多态包括函数重载和运算符重载等,动态多态是指派生类和虚函数实现运行时多态。
静态多态和动态多态的区别:静态多态的函数地址是早绑定的,其在编译阶段就已经确定了函数的地址;动态多态的函数地址是晚绑定的,其在运行阶段才确定函数地址。
函数重写:父类和子类中函数返回值类型、函数名、参数列表完全相同。
动态多态需要满足的条件:有继承关系;子类重写父类的虚函数。
动态多态使用时,父类的指针或引用指向子类的对象。
子类重写父类虚函数,虚函数表指针会将父类的函数表指针替换成本类的;如果不重写父类虚函数,子类将继承父类函数表指针,其指向的函数地址仍是父类的。
多态的简单代码示例如下。
#include
using namespace std;
class Animal
{
public:
virtual void speak() //通过虚函数实现地址晚绑定,vfptr是一个虚函数表指针
{
cout<<"Animal speak()"<<endl;
}
};
class Cat : public Animal
{
public:
void speak() //子类重写父类虚函数,虚函数表指针会将父类的替换成本类的
{
cout<<"Cat speak()"<<endl;
}
};
class Dog : public Animal
{
public:
void speak()
{
cout<<"Dog speak()"<<endl;
}
};
void dospeak(Animal &animal) //父类的引用可以指向子类对象
{
animal.speak();
}
void fun()
{
Cat cat;
cat.speak();
dospeak(cat); //传哪个对象就执行这个对象所在类的函数
Dog dog;
dospeak(dog);
}
int main()
{
fun();
system("pause");
return 0;
}
多态的优点:代码组织结构清晰;可读性强;利于前期和后期的扩展及维护。
利用普通方法实现计算器的代码如下。
#include
#include
using namespace std;
class Calculator
{
public:
int getResult(string str)
{
if(str == "+")
return num1+num2;
else if(str == "-")
return num1-num2;
else if(str == "*")
return num1*num2;
else if(str == "/")
{
if(num2 != 0)
return num1/num2;
else
cout<<"除数不能为0!"<<endl;
}
}
int num1;
int num2;
};
void fun()
{
Calculator c;
c.num1 = 10;
c.num2 = 20;
cout<<c.num1<<"+"<<c.num2<<"="<<c.getResult("+")<<endl;
cout<<c.num1<<"-"<<c.num2<<"="<<c.getResult("-")<<endl;
cout<<c.num1<<"*"<<c.num2<<"="<<c.getResult("*")<<endl;
cout<<c.num1<<"/"<<c.num2<<"="<<c.getResult("/")<<endl;
}
int main()
{
fun();
system("pause");
return 0;
}
利用多态方法实现计算器的代码如下。
#include
#include
using namespace std;
//计算器的抽象类
class AbstractCalculator
{
public:
virtual int getResult()
{
return 0;
}
int num1;
int num2;
};
//加法计算器类
class AddCalculator : public AbstractCalculator
{
public:
int getResult()
{
return num1+num2;
}
};
//减法计算器类
class SubCalculator : public AbstractCalculator
{
public:
int getResult()
{
return num1-num2;
}
};
//乘法计算器类
class MultiCalculator : public AbstractCalculator
{
public:
int getResult()
{
return num1*num2;
}
};
//除法计算器类
class DivCalculator : public AbstractCalculator
{
public:
int getResult()
{
return num1/num2;
}
};
void fun()
{
//指针方式
AbstractCalculator *c = new AddCalculator;
c->num1 = 10;
c->num2 = 20;
cout<<c->num1<<"+"<<c->num2<<"="<<c->getResult()<<endl;
delete c;
c = new SubCalculator;
c->num1 = 10;
c->num2 = 20;
cout<<c->num1<<"-"<<c->num2<<"="<<c->getResult()<<endl;
delete c;
c = new MultiCalculator;
c->num1 = 10;
c->num2 = 20;
cout<<c->num1<<"*"<<c->num2<<"="<<c->getResult()<<endl;
delete c;
c = new DivCalculator;
c->num1 = 10;
c->num2 = 20;
cout<<c->num1<<"/"<<c->num2<<"="<<c->getResult()<<endl;
delete c;
//引用方式
AddCalculator add;
AbstractCalculator &a = add;
add.num1 = 10;
add.num2 = 20;
cout<<add.num1<<"*"<<add.num2<<"="<<add.getResult()<<endl;
}
int main()
{
fun();
system("pause");
return 0;
}
上面代码的运行结果如下图所示。
利用多态方法的代码量相比于普通代码增多了,但是代码组织结构更加清晰,可读性也更强,利于扩展和维护。
在多态中,通常父类的虚函数实现是无意义的,调用的一般是子类重写后的函数。因此可以将父类的虚函数替换为纯虚函数。
纯虚函数的语法:virtual 返回值类型 函数名 (参数列表) = 0;
当类中有了纯虚函数,类就被称为抽象类。
抽象类的特点是:无法实例化对象,栈区和堆区都是不可以的;子类必须重写父类中的纯虚函数,否则其也属于抽象类。
纯虚函数和抽象类的例子如下。
#include
#include
using namespace std;
class Parent
{
public:
virtual void fun() = 0; //纯虚函数
//Parent类就是抽象类,无法实例化对象
};
class Son1 : public Parent
{
public:
void fun() //重写父类的纯虚函数
{
cout<<"Son1类的fun()调用!"<<endl;
}
};
class Son2 : public Parent
{
public:
void fun() //重写父类的纯虚函数
{
cout<<"Son2类的fun()调用!"<<endl;
}
};
int main()
{
Parent *p1 = new Son1; //创建对象
p1->fun(); //创建的对象不同,访问的函数也不同
delete p1;
Parent *p2 = new Son2;
p2->fun();
delete p2;
system("pause");
return 0;
}
程序的运行结果如下图所示。
多态的目的就是让函数的接口更加通用化。
制作饮品的流程:煮水、冲泡、倒入杯中、加入辅料。
利用多态技术实现制作饮品,提供抽象类制作饮品基类,提供子类制作咖啡和茶。
该案例的代码实现如下。
#include
#include
using namespace std;
class AbstractDrinking //抽象类
{
public:
virtual void Boil() = 0; //1煮水
virtual void Brew() = 0; //2冲泡
virtual void PourInCup() = 0; //3倒入杯中
virtual void PutOthers() = 0; //4加入辅料
void makeDrink()
{
Boil();
Brew();
PourInCup();
PutOthers();
}
};
class Tea : public AbstractDrinking
{
public:
Tea()
{
cout<<"制作茶的步骤:"<<endl;
}
//依次重写父类中的纯虚函数
void Boil()
{
cout<<"1.煮水"<<endl;
}
void Brew()
{
cout<<"2.冲泡茶叶"<<endl;
}
void PourInCup()
{
cout<<"3.倒入茶杯"<<endl;
}
void PutOthers()
{
cout<<"4.加入枸杞和桂圆"<<endl;
}
};
class Coffee : public AbstractDrinking
{
public:
Coffee()
{
cout<<"制作咖啡的步骤:"<<endl;
}
//依次重写父类中的纯虚函数
void Boil()
{
cout<<"1.煮水"<<endl;
}
void Brew()
{
cout<<"2.冲泡咖啡"<<endl;
}
void PourInCup()
{
cout<<"3.倒入咖啡杯中"<<endl;
}
void PutOthers()
{
cout<<"4.加入糖和牛奶"<<endl;
}
};
void fun(AbstractDrinking *p)
{
p->makeDrink();
delete p;
}
int main()
{
fun(new Tea);
cout<<"----------------------"<<endl;
fun(new Coffee);
system("pause");
return 0;
}
多态在使用时,如果子类中有属性开辟到堆区,那么父类指针在释放时无法调用到子类中的析构代码。因此,要将父类中的析构函数改为虚析构或者纯虚析构。
虚析构和纯虚析构的共性:可以解决父类中指针释放子类对象问题;都需要具体的函数实现。
如果子类中没有堆区数据,可以不写虚析构或者纯虚析构。
如果类中是纯虚析构,该类是抽象类,无法实例化对象。
虚析构的语法:virtual ~类名(){}
纯虚析构的语法:virtual ~类名()=0;类名:: ~类名(){}
虚析构代码的代码实现例子如下。
#include
#include
using namespace std;
class Animal
{
public:
Animal()
{
cout<<"Animal类构造函数"<<endl;
}
virtual ~Animal() //在父类中使用虚析构,解决子类中堆区内存释放问题
{
cout<<"Animal类析构函数"<<endl;
}
virtual void speak() = 0; //纯虚函数
};
class Cat : public Animal
{
public:
Cat(string catName)
{
cout<<"Cat类构造函数"<<endl;
name = new string(catName);
}
void speak()
{
cout<<*name<<" is speaking."<<endl;
}
~Cat() //析构函数中释放堆区内存
{
cout<<"Cat类析构函数"<<endl;
if(name != NULL)
{
delete name;
name = NULL;
}
}
string *name;
};
void fun()
{
Animal *p = new Cat("Tom");
p->speak();
delete p;
}
int main()
{
fun();
system("pause");
return 0;
}
上面程序中如果在父类中不使用虚析构,其运行结果如下图所示,可以看到,父类指针在析构的时候无法执行子类的析构函数,后果就是如果子类中有堆区数据属性,就会产生内存泄漏。
父类中使用虚析构,其运行结果如下图所示,子类中的析构函数也被执行了。
纯虚函数在父类中只做声明,不做实现。不同与纯虚函数,纯虚析构在父类中不仅需要有声明,还需要有具体的实现,因为父类中也可能有一些属性开辟在堆区。
在上面代码的基础上,需要修改为纯虚析构代码的部分如下。
class Animal
{
public:
Animal()
{
cout<<"Animal类构造函数"<<endl;
}
virtual ~Animal() = 0; //纯虚析构的声明
virtual void speak() = 0; //纯虚函数
};
Animal :: ~Animal() //纯虚析构的实现
{
cout<<"Animal类纯虚析构函数"<<endl;
}
电脑的主要组成部件有CPU、显卡、内存。将每个部件封装出抽象的基类,并提供不同的厂商生产不同的零件,创建电脑类提供让电脑工作的函数,并调用每个部件工作的接口。
该案例的代码实现如下。
#include
#include
using namespace std;
class CPU //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 *m)
{
this->cpu = cpu; //构造函数中接收各指针
this->vc = vc;
this->m = m;
}
void work()
{
cpu->calculate(); //调用各零件接口
vc->display();
m->storage();
}
~Computer() //析构函数释放指针
{
if(cpu != NULL)
{
delete cpu;
cpu = NULL;
}
if(vc != NULL)
{
delete vc;
vc = NULL;
}
if(m != NULL)
{
delete m;
m = NULL;
}
}
private:
CPU *cpu; //CPU零件指针
VideoCard *vc;
Memory *m;
};
//Intel厂商
class IntelCPU : public CPU
{
public:
void calculate()
{
cout<<"Intel_CPU_calculate()"<<endl;
}
};
class IntelVideoCard : public VideoCard
{
public:
void display()
{
cout<<"Intel_VideoCard_display()"<<endl;
}
};
class IntelMemory : public Memory
{
public:
void storage()
{
cout<<"Intel_Memory_storage()"<<endl;
}
};
//Lenovo厂商
class LenovoCPU : public CPU
{
public:
void calculate()
{
cout<<"Lenovo_CPU_calculate()"<<endl;
}
};
class LenovoVideoCard : public VideoCard
{
public:
void display()
{
cout<<"Lenovo_VideoCard_display()"<<endl;
}
};
class LenovoMemory : public Memory
{
public:
void storage()
{
cout<<"Lenovo_Memory_storage()"<<endl;
}
};
void test()
{
cout<<"第一台电脑组装:"<<endl;
CPU *icpu = new IntelCPU;
VideoCard *ivc = new IntelVideoCard;
Memory *m = new IntelMemory;
Computer *c1 = new Computer(icpu,ivc,m);
c1->work();
delete c1;
cout<<"----------------------"<<endl;
cout<<"第二台电脑组装:"<<endl;
Computer *c2 = new Computer(new LenovoCPU,new LenovoVideoCard,new LenovoMemory);
c2->work();
delete c2;
cout<<"----------------------"<<endl;
cout<<"第三台电脑组装:"<<endl;
Computer *c3 = new Computer(new LenovoCPU,new IntelVideoCard,new LenovoMemory);
c3->work();
delete c3;
}
int main()
{
test();
system("pause");
return 0;
}
程序运行时产生的数据都属于临时数据,程序运行结束后都会被释放,通过文件可以将数据持久化。
C++中对文件的操作需要包含头文件。
文件类型分为两种,文本文件和二进制文件。文本文件以文本的ASCII码形式存储。二进制文件以文本的二进制形式存储在计算机中,用户一般不能直接读懂它们。
操作文件的三大类:ofstream(写操作)、ifstream(读操作)、fstream(读写操作)。
文件打开方式 | 描述 |
---|---|
ios::in | 读文件 |
ios::out | 写文件 |
ios::ate | 初始位置是文件尾 |
ios::app | 追加方式写文件 |
ios::trunc | 如果文件存在先删除,再创建 |
ios::binary | 二进制方式 |
文件的打开方式可以配合使用,如果有多个中间使用 | 符连接。例如用二进制方式写文件 ios::binary | ios::out。
写文件的步骤:1.包含头文件 #include ;2.创建流对象 ofstream ofs;3.打开文件 ofs.open(“文件路径”,打开方式);4.写数据 ofs << “写入的数据”(cout是往屏幕上输出,ofs是往文件中输出);5.关闭文件 ofs.close()。
往文本文件中写内容的简单代码如下。
#include
#include //1.包含头文件
#include
using namespace std;
void test()
{
ofstream ofs; //2.创建流对象
ofs.open("test.txt",ios::out); //3.打开文件
ofs << "This is the first line." <<endl; //4.往文件写内容
ofs << "This is the second line." <<endl;
ofs << "This is the third line." <<endl;
ofs.close(); //5.关闭文件
}
int main()
{
test();
system("pause");
return 0;
}
程序运行结束后,右键Visual Studio中的cpp文件,选择打开所在的文件夹,在该文件夹下就生成了指定的文件,打开后其内容如下图所示。
可以看到,文件名就是我们在程序中指定的,当然程序中也可以将文件名指定为绝对路径,如果只是给出文件名,生成的文件默认就在项目所在目录下。文件中的内容也是通过代码写入的。
读文件的步骤:1.包含头文件 #include ;2.创建流对象 ifstream ifs;3.打开文件并判断文件是否打开成功 ifs.open(“文件路径”,打开方式),is_open()函数判断文件是否打开成功;4.读数据,共有四种方式;5.关闭文件 ifs.close()。
从文本文件中读取内容的简单代码如下。
#include
#include //1.包含头文件
#include
using namespace std;
void test()
{
ifstream ifs; //2.创建流对象
ifs.open("test.txt",ios::in); //3.打开文件并判断文件是否打开成功
if(!ifs.is_open())
{
cout<<"文件打开失败!"<<endl;
return;
}
//4.读文件内容
//char buf[1024] = {0}; //方式一读取 —— 读取到数组中
//while(ifs >> buf)
//{
// cout <
//}
//char buf[1024] = {0}; //方式二读取 —— 通过函数逐行读取到数组中
//while(ifs.getline(buf,sizeof(buf)))
//{
// cout <
//}
string buf; //方式三读取 —— 通过函数逐行读取到字符串中
while(getline(ifs,buf))
{
cout <<buf<<endl;
}
//char c; //方式四读取 —— 逐个字符读取
//while((c=ifs.get())!=EOF)
//{
// cout<
//}
ifs.close(); //5.关闭文件
}
int main()
{
test();
system("pause");
return 0;
}
以二进制方式往文件中写内容的简单代码如下。
#include
#include //1.包含头文件
#include
using namespace std;
class Person
{
public:
char name[64];
int age;
};
void test()
{
ofstream ofs; //2.创建流对象
ofs.open("person.txt",ios::out|ios::binary); //3.以二进制方式打开文件
//ofstream ofs("person.txt",ios::out|ios::binary); //2和3步骤也可以用本行代替
Person p = {"张三",22};
ofs.write((const char*)&p,sizeof(Person));
ofs.close(); //5.关闭文件
}
int main()
{
test();
system("pause");
return 0;
}
以二进制方式写的时候可以写入类这种类型的数据到文件,程序运行之后同样在项目文件夹下可以看到新出现的文件,打开后其内容如下图所示。
可以看到,以二进制写入文件后会出现乱码,有时候甚至都看不懂,不过没关系,只要确保写入的内容是对的,后面通过读取文件就可以验证。
以二进制方式读取文件内容的简单代码如下。
#include
#include //1.包含头文件
#include
using namespace std;
class Person
{
public:
char name[64];
int age;
};
void test()
{
ifstream ifs; //2.创建流对象
ifs.open("person.txt",ios::in|ios::binary); //3.以二进制方式打开文件
if(!ifs.is_open()) //判断文件是否打开成功
{
cout<<"文件打开失败!"<<endl;
return;
}
Person p;
ifs.read((char*)&p,sizeof(Person));
cout<<"姓名:"<<p.name<<" 年龄:"<<p.age<<endl;
ifs.close(); //5.关闭文件
}
int main()
{
test();
system("pause");
return 0;
}
程序运行后的结果如下图所示。
可以看到,虽然以二进制方式写入文件后会出现乱码,但是以二进制方式读取到的内容与写入的内容是一致的。
本文参考视频:
黑马程序员匠心之作|C++教程从0到1入门编程,学习编程不再难