C++语言学习06

发布时间 2023-09-01 21:06:04作者: 优秀还天籁

一、虚函数、虚函数表、虚表指针、覆盖
1、虚函数
只要是在成员函数前面加 virtual 后,该函数就成为虚函数,此时该类就会
像虚继承一样多了一个虚表指针(虚函数表指针、虚指针)
2、虚函数表
虚表指针指向的是属于该类的一张表格的首地址,该表格中记录了该类中所有虚函数的首地址

    通过 ((void(*)(void))(**(int**)b))();可以直接通过虚函数表以及虚表指针
    来访问虚函数表中的第一个虚函数void func(void)
3、覆盖(重写)(是构成多态的基础)
    当使用virtual修饰父类的成员函数时,此时父类中就会多一个虚表指针以及一张虚函数
    表,子类继承父类时,会把父类的虚表指针以及虚函数表一起继承过来,然后编译器会去比
    较父子类中同名的成员函数的格式,如果格式完全相同的虚函数,就会把子类中虚函数表中
    原来同名父类虚函数的地址改为子类同名函数的地址,这时就称为覆盖

    此时使用父类指针或引用指向子类对象时,调用虚函数则会去执行的是被覆盖后的虚函数表中
    所指向子类的同名且格式相同的成员函数,不再调用父类的同名虚函数

4、构成覆盖的条件
    a、子类必须以public继承父类
    b、父类中的被覆盖的函数必须是虚函数
    c、子类中必须有与父类虚函数同名的成员函数,且该函数的返回值、
        参数列表、常属性都必须相同
    d、返回值类型相同,或者子类的同名成员函数的返回值类型可以向父类
    虚函数的返回值类型做隐式转换,且有继承关系

    重载、覆盖、隐藏、重写的区别
    隐藏:
    1、父子类中,同名且格式不相同,无论是否有 virtual 修饰都构成隐藏
    2、父子类中,如果同名且格式相同,如果没有 virtual 修饰则构成隐藏
    3、隐藏可以隐藏同名成员变量、成员函数,但是覆盖只能覆盖,满足条件的成员虚函数
    4、在父子类中,同名成员函数要么构成隐藏、要么构成覆盖
    5、除去父子类外,其他的不同作用域下同名标识符也构成隐藏

二、多态
什么是多态:
指的是一条指令可以有多种形态,当调用一个指令时,它能够根据参数、环境的不同作出不同
的操作,这种情况称为多态

    C++中根据确定指令具体操作的时间划分多态:
        编译时多态、运行时多态

编译时多态:
    当调用函数重载版本时,编译器会根据参数的类型、个数等确定调用的是哪个版本的重载函数,这就是
    所谓的编译时多态,还有例如泛型中模版技术等(重载是编译时多态的一种方式)
    

运行时多态(动态多态):
    当子类覆盖了父类的同名函数,然后使用父类指针或引用去访问虚函数,该父类指针或引用即可能指向
    父类对象、也可能指向子类对象,所以调用的是父类还是子类的同名函数在编译期间无法确定,需要在运行
    期间才能确定,称为运行时多态(必须发生覆盖)
构成运行时多态的条件:
    1、父子类必须以public继承
    2、子类中有对父类成员函数构成覆盖
    3、运行时才能确定父类指针或引用指向的是父类还是子类的对象

对象切片(Object Slicing)是指在面向对象编程中,当一个派生类对象被赋值给一个基类对象时,派生类特有的成员变量和方法会被丢失,只保留基类部分。
这种现象称为对象切片。

尝试:构造函数和析构函数能否是虚函数并形成覆盖?为什么?
[构造函数、析构函数的虚函数中名字不考虑]

三、虚构造和虚析构
虚构造:
C++中不允许构造函数为虚函数
假设构造函数可以定义为虚函数,那么此时子类的构造函数就会自动覆盖父类
的构造函数,当创建子类对象时,会执行子类的构造函数,但是按照执行顺序
会先执行父类的构造函数,而此时父类的构造函数已经被覆盖为子类的构造函数,
又重新回来形成死循环,所以编译器禁止把构造函数定义为虚函数

    其次构造函数的使命就是去把类中的成员创建,包括虚函数表、虚表指针,因此
    如果把构造函数定义为虚函数,但是构造函数都没有执行成功没有所谓的虚函数表、
    虚表指针,因此想要把构造函数放入虚函数表中就是一个悖论

虚析构:
    析构函数可以定义为虚函数
    当使用类多态,通过父类指针或引用释放子类对象时,如果析构函数不是虚函数,
    那么只会执行父类的析构函数,但是由于在创建子类对象时,一定执行了子类的
    构造函数,如果在子类构造函数中申请了资源,此时对象销毁时没有调用子类的析构
    函数,就会导致内存泄露

    只有把父类的析构函数定义为虚函数,通过父类指针或引用释放子类对象时,会先调
    用子类的析构函数(覆盖),当子类的析构函数执行结束后,按照对象的释放顺序,
    会自动执行父类的析构函数,此时子类已经被释放完,所以调用父类的析构函数不会
    再继续因为覆盖而去执行子类的析构函数,因此不会造成内存泄露

    总结:
        当使用多态时且子类的析构函数中有需要释放的资源,此时父类中就必须设置
        为虚析构

四、纯虚函数、抽象类、纯抽象类
纯虚函数的格式:
virtual 返回值 函数名(参数列表) = 0;(这里面的=0,表示该函数没有默认实现)
1、纯虚函数可以不去实现,一般人也没必要去实现
2、父类中如果有纯虚函数,那么继承该父类的子类必须对其进行覆盖,
否则无法创建对象
3、有纯虚函数的类不能创建对象
4、纯虚函数就是为了强制子类去覆盖,为了强制子类实现某些功能
5、有纯虚函数的类都称为抽象类
6、析构函数可以设置为纯虚函数,但是必须要在类外定义

补充:
    纯虚函数的语法规则:声明在基类内部进行,而定义(如果提供)则在类外进行
纯抽象类:
    所有的成员函数都是纯虚函数的类,称为纯抽象类,这种类一般用
    于设置功能接口,所以也称为接口类

了解一下工厂模式,实现一个简单的工厂模式

五、C++ I/O流
头文件: fstream
C++把对文件的读写操作都封装在以下的类中:
ofstream类 对文件的写操作,继承了ostream类的功能
ifstream类 对文件的读操作,继承了istream类的功能
fstream类 对文件读写操作,继承了ofstream、ifstream类的功能

六、C++对文本文件的读写
1、创建流对象,通过流对象打开文件
a、ofstream ofs(const char* filename,openmode mode);
补充:下面的这些成员函数是可以通过流对象使用的,例如:ofs.open();
b、通过成员函数方式,打开文件
void open(const char* filename);
void open(const char* filename,openmode mode);
filename: 文件的路径
mode: 打开方式
ios::app 添加输出
ios::ate 当以打开时寻找到EOF
ios::binary 以二进制的形式打开
ios::in 为读取打开文件
ios::out 为写入打开文件
ios::trunc 覆盖存在的文件

    ifstream类。默认以只读的方式打开O_RDONLY 打开文件,文件不存在则失败"r"
    ofstream类。默认以只读的方式打开O_WRONLY|O_CREAT|O_TRUNC 打开文件,
    文件不存在则创建、存在则清空 "w"
    fstream类。默认以读写的方式打开O_RDWR打开文件,文件不存在则失败,文件存在
    不清空 "r+"

    注意:ios::里面打开模式单独使用和混合使用时候有些功能会有所删减变化,不是简单
    的功能相加,具体底层调用哪个打开模式可以通过 strace ./a.out 来追踪底层对系统
    函数的调用

2、如何判断文件是否打开成功
    a、通过 !流对象名 执行了该类的!运算符重载版本
        if(!fs)     //为真  失败
    b、通过good\fail成员函数判断是否成功
        bool good();
        功能:判断上一次流操作是否成功,成功返回真,一般用于判断文件是否打开成功

3、读写文件
    流对象 << 写操作
    流对象 >> 读操作

4、关闭文件
    成员函数:
    void close(void)
    注意:它只是关闭流对象当前的文件,但是流对象没有销毁,还可以继续通过open成员函数重新打开
    别的文件

练习1:设计一个学生类并实例化对象,然后把学生对象信息以文本格式写入stu.txt中
练习2:从stu.txt中读取数据,并实例化一个学生对象并显示

如何以文本方式读写类对象?
    读写类对象时绝大部分成员变量都是私有的,因此无法直接在类外进行读写
    由于ostream/istream分别是ofstream/ifstream的父类,因此如果重载了>> << 运算符,既可以
    用于平时输出、输入类对象,并且还可以直接用于类对象的文本方式流操作读写

        out << 类对象
        cin >> 类对象
        //重载过后
        ofs << 类对象
        fs >> 类对象//这是成立的

七、C++的随机读写
C++为文件IO流提供了两套设置位置指针的成员函数,为了兼容一些有两个位置指针的操作系统,但是UNIX、Linux
windows系统底层只有一个文件位置指针,所以使用哪套没区别

istream& seekg(off_type offset, ios::seekdir origin);
功能:通过 偏移量+基础位置 设置位置指针的位置
    offset: 偏移量
    origin:基础位置
        ios::beg    文件开头
        ios::cur    当前位置
        ios::end    文件末尾
istream& seekg(pos_type position);
    功能:通过绝对值的方式设置位置指针的位置
    seekp  功能类似

pos_type tellp();
功能:获取位置指针所在文件的绝对位置
tellg功能类似

八、C++对二进制文件的读写操作
1、创建流对象、打开文件
ios::binary 以二进制模式打开文件
2、读写操作
ostream& write(const char *buffer,streamsize num);
功能:以二进制方式写文件
buffer:待写入数据的内存首地址
num:待写入的字节数
注意:C++的write只会有两种结果,要么num个字节全部写入,要么一个都没写入,
可以通过good、fail判断上一次的写操作是否成功

    istream& read(char *buffer,streamsize num);
    功能:以二进制方式读文件
    buffer:存储读取到的数据的内存首地址
    num:要读取的字节数

    streamsize gcount();
    功能:获取上一次读操作中成功读取到的字节数//这个是配合写操作时用到的,读多少,写多少

    bool eof();
    功能:判断读操作是否读到了文件末尾,如果是返回真

二进制读写需要注意的问题:
1、对象的成员变量中不应该有指针类型(或string类型),因为此时写入时只会把指针变量存储的地址写入,而下次读取到该指针变量时,该指针地址已经没有意义了
2、一直读取二进制文件
while(true)
{
// 读操作
if(fs.eof()) break;
// 读成功,执行相应操作
}

    移动普通文件
        目标路径不是当前路径
        目标路径是当前路径  重命名
    移动目录文件
        通过 LinuxC中的目录流操作去完成

//工厂模式
工厂模式(Factory Pattern)是一种创建型设计模式,在 C++ 中被广泛使用。
它提供了一种将对象创建过程与使用这些对象的客户端代码分离的方法。
工厂模式通过定义一个工厂类来负责创建具体产品类的实例,客户端不需要直接调用具体产品类的构造函数,
而是通过工厂类的接口来创建所需的对象。这样,当需要添加或更改创建的产品类时,只需要修改工厂类,而不需要修改客户端代码。

    工厂模式主要分为三种:

    简单工厂模式(Simple Factory Pattern):简单工厂模式是一个工厂类根据传入的参数来创建相应的产品类实例。
    客户端只需要知道工厂类的接口,而不需要关心具体产品类的实现。

    工厂方法模式(Factory Method Pattern):工厂方法模式定义了一个抽象工厂类,该类包含一个抽象的工厂方法,
    用于创建具体产品类的实例。具体的工厂类继承自抽象工厂类,并实现抽象工厂方法。客户端通过具体工厂类创建所需的产品实例,
    而不需要知道具体产品类的实现。

    抽象工厂模式(Abstract Factory Pattern):抽象工厂模式是一种更高级的工厂模式,
    它允许创建一系列相关或相互依赖的产品对象。抽象工厂模式定义了一个抽象工厂类,该类包含一组抽象方法,
    用于创建一系列相关产品的实例。具体的工厂类继承自抽象工厂类,并实现这些抽象方法。
    客户端通过具体工厂类创建所需的产品实例,而不需要知道具体产品类的实现。

    工厂模式的主要优点是将对象创建和使用过程分离,降低了客户端与具体产品类之间的耦合度,提高了代码的可维护性和可扩展性。

//回调模式
回调模式(Callback Pattern)是一种常见的编程模式,它允许在某个函数或方法中传递一个函数作为参数,
然后在适当的时机执行这个函数。回调模式在 C++ 中通常使用函数指针、函数对象(仿函数)或者 C++11 引入的 Lambda 表达式来实现。

回调模式的主要优点是提高了代码的灵活性和可重用性,因为它可以在运行时动态地决定要调用哪个函数。
此外,回调模式可以用于实现事件驱动的编程,例如异步操作、观察者模式等。