一、纯虚函数
为什么用:在多态中,父类的虚函数往往用不到,也不会主动调用父类的虚函数,所以父类中的虚函数函数体是没用的。
但声明后,必须要实现,否则报错。为了解决这个问题,出现了纯虚函数,只有函数声明 没有函数实现
格式:返回值 函数名(函数参数) = 0;
注意:如果函数中有纯虚函数,则当前类为抽象类。抽象类不可以定义对象。
如果父类有 纯虚函数,子类中必须要重写,否则子类也是抽象类。
抽象类中可以写非纯虚函数,也可以写成员变量。
二、析构函数建议定义成虚函数
在多态的时候,如果子类中有成员变量开辟了空间,那么父类的指针释放的时候,默认湖调用父类的析构函数,子类成员变量申请的内存空间会导致内存泄漏,为了防止出现则使用虚析构函数
父类可以定义纯虚析构,vritual ~类名() = 0;
如果子对象没有成员变量申请空间,则没必要用纯虚析构。
代码:
头文件:
#ifndef XX_H
#define XX_H
//纯虚函数 的重写
//虚函数的析构函数
#include<iostream>
#include<cstring>
#include<string>
using namespace std;
class xx
{
private:
int age=0;
public:
xx()
{
cout<<"xx已经构造!\n";
}
virtual void eat()=0;
virtual ~xx()
{
cout<<"xx 已经析构!\n";
}
};
class x:public xx //公共继承xx
{
private:
int age1;
public:
x()//构造函数
{
age1=10;
cout<<"x已经构造!"<<endl;
}
virtual void eat()//virtyal 写不写都是虚函数 重写需要返回值,函数名,参数列表,都要一样
{
cout<<"age1:"<<age1<<endl;
}
virtual ~x()
{
cout<<"x已经析构!\n";
}
};
#endif // XX_H
.cpp 文件
#include <iostream>
#include"xx.h"
#include"name.h"
using namespace std;
int main(int argc, char *argv[])
{
x b;//虚函数的析构函数的验证 与虚函数的父子继承的重写
return 0;
}
三、解决菱形继承
虚继承:解决菱形继承
格式:class 派生类名:virtual public 基类名{};
虚继承特点: 当使用虚继承后,如果孙在类中发现有相同的爷爷类,则只会拷贝一份爷爷类的成员变量和方法。
代码:
头文件:
#ifndef NAME_H
#define NAME_H
//解决菱形继承问题
#include<iostream>
#include<string>
#include<cstring>
using namespace std;
class name//爷爷
{
private:
int age;
public:
void getAge()
{
cout<<"age: "<<age<<endl;
}
name()
{
age=0;
cout<<"name "<<endl;
}
};
class name1:virtual public name //公共继承 儿子1
{
private:
int age1;
public:
virtual void eat()
{
cout<<"age1: "<<age1<<endl;
}
name1()
{
age1=1;
cout<<"name1 "<<endl;
}
};
class name2:virtual public name //公共继承 儿子2
{
private:
int age2;
public:
virtual void eat()
{
cout<<"age2: "<<age2<<endl;
}
name2()
{
age2=2;
cout<<"name2 "<<endl;
}
};
class name3:public name1 ,public name2 //孙子
{
private:
int age3;
public:
void getAge3()
{
cout<<"age3: "<<age3<<endl;
}
name3()
{
age3=3;
cout<<"name3 "<<endl;
}
virtual void eat()//这个函数在这里是 重写,因为之前都是虚函数//没有虚函数的化 就是隐藏了。
{
cout<<"asdas"<<endl;
}
};
#endif // NAME_H
.cpp文件
#include <iostream>
#include"xx.h"
#include"name.h"
using namespace std;
int main(int argc, char *argv[])
{
//解决菱形继承 的问题
//爷爷类 ,儿子类 ,孙子类
name3 a;
a.getAge3();
a.getAge();
a.name1::eat();
cout<<endl<<endl;
name2 b;
b.getAge();
return 0;
}
四、文件操作
文件: 文本文件:存放ascii内容的文件 字符文件
二进制文件:以二进制的形式存储的文件。一般用户不能直接读取。
1. 文件操作三大类:
写操作: ofstream
读操作: ifstream
读写操作:fstream
头文件: #include <fstream>
2.文本文件
写文件: 1.创建对象流 ofstream os
2.打开文件 os.open函数
2.1void open(const char_t* _Filename, ios_base::openmode _Mode = ios_base::out)
参数:_Filename 文件路径 文件名
_Mode 打开模式
ios::in 以读的方式打开文件
ios::out 以写的方式打开文件
ios::app 追加的方式打开文件 不可以 ios::trunc
ios::binary 二进制方式打开文件
ios::trunc 如果文件存在则清空 是否在只写的方式才清空
如果有多个打开模式,用安位或操作 |
3.数据写入 os << "数据" ;
4.关闭文件os.close();
3.读文件
1.创建对象流 ifstream is
2.打开文件 is.open函数
2.1void open(const char_t* _Filename, ios_base::openmode _Mode = ios_base::out)
参数:_Filename 文件路径 文件名
_Mode 打开模式
ios::in 以读的方式打开文件
ios::out 以写的方式打开文件
ios::app 追加的方式打开文件 不可以 ios::trunc
ios::binary 二进制方式打开文件
ios::trunc 如果文件存在则清空
如果有多个打开模式,用安位或操作 |
3.数据读取buf
方式1: is >> buf ; 把一行数据读取到buf中,如果buf内存空间不够,会出现异常。
方式2: while (is.getline(buf, sizeof(buf))) 更安全
参数: buf是需要存放数据的buf
sizeof(buf)缓存大小
方式3: string line; while (getline(is, line)) 读取的内容放入line中
第一个参数是流对象,第二个参数是string对象 读取的内容存入string对象中
方 式4 is.get() 读取一个字符 返回值是一个字符
4.关闭文件os.close();
二进制文件:
注意:文件的打开需要加上 ios::binary
写文件:write函数写文件 (ofstrem流的成员函数)
ofstream& write(const _Elem* _Str, streamsize _Count) ;
参数:str 需要写入到文件的buf count 需要写入到文件的大小
读文件:ifstream& read(_Elem* _Str, streamsize _Count)
参数:str 需要读入到内存的buff count 需要读取的到buf的大小
文件偏移位置:
对于iftream (get)和ofstream (put)都有输入和输出的偏移量,读取的位置和写入的位置
可以通过一些函数获取到或者可以设置偏移位置
seekg(偏移量,起始位置) --- 设置输入流的偏移位置
seekp(偏移量,起始位置) -----设置输出流的偏移位置
tellg() ----获取输入流的偏移位置
tellp () ----获取输出流的偏移位置
偏移起始位置如下: ios::beg 从流的开始位置计算
ios::cur 从流的当前位置开始计算
ios::end 从文件流的末尾开始计算
文件a中写入的是5个学生的信息(int age ,string name)把文件a拷贝到文件b中。
代码:
#define _CRT_SECURE_NO_WARNINGS //预防strcpy的报错
#include<iostream>
#include<cstring>
#include<fstream>
#include<cstring>
#include<string>
#include<ctime>
#include<cstdlib>
using namespace std;
struct student
{
int age;
string name;
};
int main()
{
/*
//1.创建一个输出流对象
ofstream os; //打开一个流 向文件写入数据
//2.打开文件
os.open("gw.txt", ios::out);
//3.写入文件
char w[1000] = "";
int i = 10;
while (i--)
{
time_t tm;
tm=time(NULL);
strcpy(w,ctime(&tm));
os << w<< endl;
}
//4.关闭文件
os.close();
//1.创建一个读取对象
ifstream is; //打开一个流 向文件读取数据
//2.打开文件
is.open("gw.txt", ios::in);
//3.读取文件
char w1[200];
char w2[200];
char w3[200];
char w4[200];
char w5[200];
int a = 0;
while (1)
{
is >> w1>>w2>>w3>>w4>>w5 ;
cout << w1 <<" "<<w2 << " " <<w3 << " " <<w4 << " " <<w5<< endl;
a = strlen(w1);
if (a == 0)
{
break;
}
}
//while(is>>buf)//一个buf 的读,遇到空格,回车就结束
//{
// cout<<buf<<endl;
//}
//
//
//一行一行的读
//while(is.getline(buf,sizeof(buf)))//buf 的空间大一点
//{
// cout<<buf<<endl;
//}
//第三种方式
char c;
//while(c=is.get()!=EOF)
//{
// cout<<c<<endl;
//}
//第四种方式
//string line;
//while(getline(is,line))
//{
// cout<<line<<endl;
//}
is.close();
*/
/*
/
//拷贝一个文件,到另外一个文件 (知道内容的格式)
//1.创建一个输出流对象
ofstream os;
//2.打开文件
os.open("gw1.txt", ios::out);//打开了返回 turn 失败返回 false
if (!os.is_open())
{
cerr << "out open" << endl;
return 0;
}
//3.创建一个读取对象
ifstream is;
//4.打开文件
is.open("gw.txt", ios::in);
if (!is.is_open())//打开了返回 turn 失败返回 false
{
cerr << "out open" << endl;
return 0;
}
//5.拷贝文件到另外的文件
int a = 0;
char w1[200];
char w2[200];
char w3[200];
char w4[200];
char w5[200];
while (1)
{
is >> w1 >> w2 >> w3 >> w4 >> w5;
os << w1 << " " << w2 << " " << w3 << " " << w4 << " " << w5 << endl;
cout << w1 << " " << w2 << " " << w3 << " " << w4 << " " << w5 << endl;
a = strlen(w1);
if (a == 0)
{
break;
}
}
is.close();
os.close();
*/
//
/*
//二进制版本的 c++ 版本
//1.创建一个输出流对象
ofstream os; //打开一个流 向文件写入数据
//第二种打开方式
//构造的方法打开
//ofstream os("dd.txt",ios::out | ios::binary);
//2.打开文件
os.open("gw2.txt", ios::out | ios::binary);
if (!os.is_open())
{
cerr << "bin out open " << endl;
}
//3.写入文件
char w[1000] = "我是长沙sadada的";
int i = 10;
os.write((const char*)w, sizeof(w));//写入二进制
os.close();
*/
/*
//1.创建一个读取对象
ifstream is; //打开一个流 向文件读取数据
//2.打开文件
is.open("gw2.txt", ios::in| ios::binary);
if (!is.is_open())
{
cerr << "bin in open error"<< endl;
}
char w[100];
is.read(w, sizeof(w));
cout << w << endl;
is.close();
*/
//作业:写入五个数据,并且读取
string name="dasd";
struct student * w = new struct student[5];
for (int i = 0; i < 5; i++)//给数组赋值
{
w[i].age = i;
w[i].name = name;
}
ofstream os;//打开一个流,向文件写入数据
os.open("gw3.txt", ios::out | ios::binary);
if (!os.is_open())//看看写入 的流是否打开失败
{
cerr << "bin out open error" << endl;
}
for (int j = 0; j < 5; j++)
{
os.write((const char*)&w[j], sizeof(struct student));//写入学生类的数据
}
os.close();
//1.创建一个读取对象
ifstream is; //打开一个流 向文件读取数据
//2.打开文件
is.open("gw3.txt", ios::in | ios::binary);
if (!is.is_open())//看看读取 的流是否打开失败
{
cerr << "bin in open error" << endl;
}
struct student kk;
cout << "读取的信息" << endl;
for (int i = 0; i < 5; i++)
{
is.read((char *)&kk, sizeof(struct student));
cout << kk.age << " " << kk.name << endl;
}
is.close();
return 0;
}
五。虚函数表
概述
为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是如何实现动态绑定的。
类的虚表
每个包含了虚函数的类都包含一个虚表。
我们知道,当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。
我们来看以下的代码。类A包含虚函数vfunc1,vfunc2,由于类A包含虚函数,故类A拥有一个虚表。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
图1:类A的虚表示意图
虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。
虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。
虚表指针
虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。
为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。
图二
上面指出,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。
四、动态绑定
说到这里,大家一定会好奇C++是如何利用虚表和虚表指针来实现动态绑定的。我们先看下面的代码。
class A {
public:
virtual void vfunc1();
virtual void vfunc2();
void func1();
void func2();
private:
int m_data1, m_data2;
};
class B : public A {
public:
virtual void vfunc1();
void func1();
private:
int m_data3;
};
class C: public B {
public:
virtual void vfunc2();
void func2();
private:
int m_data1, m_data4;
};
类A是基类,类B继承类A,类C又继承类B。类A,类B,类C,其对象模型如下图所示。
图三
由于这三个类都有虚函数,故编译器为每个类都创建了一个虚表,即类A的虚表(A vtbl),类B的虚表(B vtbl),类C的虚表(C vtbl)。类A,类B,类C的对象都拥有一个虚表指针,*__vptr,用来指向自己所属类的虚表。
类A包括两个虚函数,故A vtbl包含两个指针,分别指向A::vfunc1()和A::vfunc2()。
类B继承于类A,故类B可以调用类A的函数,但由于类B重写了B::vfunc1()函数,故B vtbl的两个指针分别指向B::vfunc1()和A::vfunc2()。
类C继承于类B,故类C可以调用类B的函数,但由于类C重写了C::vfunc2()函数,故C vtbl的两个指针分别指向B::vfunc1()(指向继承的最近的一个类的函数)和C::vfunc2()。
虽然图3看起来有点复杂,但是只要抓住“对象的虚表指针用来指向自己所属类的虚表,虚表中的指针会指向其继承的最近的一个类的虚函数”这个特点,便可以快速将这几个类的对象模型在自己的脑海中描绘出来。
非虚函数的调用不用经过虚表,故不需要虚表中的指针指向这些函数。
假设我们定义一个类B的对象。由于bObject是类B的一个对象,故bObject包含一个虚表指针,指向类B的虚表。
int main()
{
B bObject;
}
- 现在,我们声明一个类A的指针p来指向对象bObject。虽然p是基类的指针只能指向基类的部分,但是虚表指针亦属于基类部分,所以p可以访问到对象bObject的虚表指针。bObject的虚表指针指向类B的虚表,所以p可以访问到B vtbl。如图3所示。
int main()
{
B bObject;
A *p = & bObject;
}
int main()
{
B bObject;
A *p = & bObject;
p->vfunc1();
}
程序在执行p->vfunc1()时,会发现p是个指针,且调用的函数是虚函数,接下来便会进行以下的步骤。
首先,根据虚表指针p->__vptr来访问对象bObject对应的虚表。虽然指针p是基类A*类型,但是*__vptr也是基类的一部分,所以可以通过p->__vptr可以访问到对象对应的虚表。
然后,在虚表中查找所调用的函数对应的条目。由于虚表在编译阶段就可以构造出来了,所以可以根据所调用的函数定位到虚表中的对应条目。对于 p->vfunc1()的调用,B vtbl的第一项即是vfunc1对应的条目。
最后,根据虚表中找到的函数指针,调用函数。从图3可以看到,B vtbl的第一项指向B::vfunc1(),所以 p->vfunc1()实质会调用B::vfunc1()函数。
如果p指向类A的对象,情况又是怎么样?
int main()
{
A aObject;
A *p = &aObject;
p->vfunc1();
}
当aObject在创建时,它的虚表指针__vptr已设置为指向A vtbl,这样p->__vptr就指向A vtbl。vfunc1在A vtbl对应在条目指向了A::vfunc1()函数,所以 p->vfunc1()实质会调用A::vfunc1()函数。
可以把以上三个调用函数的步骤用以下表达式来表示:
(*(p->__vptr)[n])(p)
可以看到,通过使用这些虚函数表,即使使用的是基类的指针来调用函数,也可以达到正确调用运行中实际对象的虚函数。
我们把经过虚表调用虚函数的过程称为动态绑定,其表现出来的现象称为运行时多态。动态绑定区别于传统的函数调用,传统的函数调用我们称之为静态绑定,即函数的调用在编译阶段就可以确定下来了。
那么,什么时候会执行函数的动态绑定?这需要符合以下三个条件。
- 通过指针来调用函数
- 指针upcast向上转型(继承类向基类的转换称为upcast,关于什么是upcast,可以参考本文的参考资料)
- 调用的是虚函数
如果一个函数调用符合以上三个条件,编译器就会把该函数调用编译成动态绑定,其函数的调用过程走的是上述通过虚表的机制。
总结
封装,继承,多态是面向对象设计的三个特征,而多态可以说是面向对象设计的关键。C++通过虚函数表,实现了虚函数与对象的动态绑定,从而构建了C++面向对象程序设计的基石。