C++ 语言的学习 day05 纯虚函数,析构函数建议定义成虚函数,解决菱形继承,文件操作,二进制文件,虚函数表

一、纯虚函数
     为什么用:在多态中,父类的虚函数往往用不到,也不会主动调用父类的虚函数,所以父类中的虚函数函数体是没用的。


但声明后,必须要实现,否则报错。为了解决这个问题,出现了纯虚函数,只有函数声明 没有函数实现
格式:返回值 函数名(函数参数) = 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++面向对象程序设计的基石。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值