day06

**重难点**
一、虚函数、虚函数表、虚表指针、覆盖
    1、虚函数
        在成员函数前面加 virtual 后,该函数就称为虚函数,此时该类就会像虚继承一样多了一个虚表指针(虚函数表指针、虚指针)
    2、虚函数表
        虚表指针指向的是一张属于该类的一张表格的首地址,该表格中记录了该类中所有虚函数的首地址
        如果类中没有其他成员变量
        通过((void(*)(void))(**(int**)b))(); 可以直接通过虚函数表以及虚表指针来访问虚函数表中的第一个虚函数 void func(void)
    3、覆盖(****) (重写)  (是构成多态的基础)
        当使用virtual修饰父类的成员函数时,此时父类中救会多一个虚表指针以及一张虚函数表,子类继承父类时会把父类的虚表指针以及虚函数表一起继承过来,然后编译器会去比较父子类中同名的虚成员函数的格式,如果格式完全相同的虚函数,就会把子类中虚函数表中原来同名父类虚函数的地址改为子类同名函数的地址,这时就称为覆盖
        此时使用父类指针或引用指向子类对象时,调用虚函数则会根据被覆盖后的虚函数表中所指向子类的同名且格式相同的成员函数,不再调用父类的同名虚函数
    4、构成覆盖的条件
        1、子类以public继承父类
        2、父类中被覆盖的函数必须时 virtual 修饰的虚函数
        3、子类中必须有与父类虚函数同名的成员函数,且该函数的返回值、参数列表、常属性都必须相同
        4、返回值类型相同,或者子类同名成员函数的返回值类型可以向父类虚函数的返回值类型做隐式转换(自动转),且有继承关系
   
    【非常常考面试题】  (*****************)
        重载、覆盖、隐藏、重写的区别?
            隐藏:
                1、父子类中,如果同名且格式不相同,无论是否有virtual修饰都构成隐藏
                2、父子类中,如果同名且格式相同,如果没有virtual修饰则构成隐藏
                3、隐藏可以隐藏成员变量(成员变量、成员函数),覆盖只能覆盖满足条件的成员虚函数
                4、在父子类中,同名成员函数要么构成隐藏,要么构成覆盖
                5、除去父子类外,其他的不同作用域下同名标识符之间构成隐藏关系

二、多态
    什么是多态:
        指的是一条指令可以有多种形态,当调用一个指令时,它能够根据参数、环境的不同作出不同的操作,这种情况称为多态
    C++中根据确定指令具体操作的时间划分多态:
        编译时多态、运行时多态
    编译时多态:
        当调用函数重载版本时,编译器会根据参数的类型、个数等确定调用的是哪个版本的重载技术,这就是所谓的编译时多态,还有例如泛型编程中模板技术等
    运行时多态:
        当子类覆盖了父类的同名函数,然后使用父类指针或引用去访问虚函数,该父类指针或引用既可能指向父类对象、也可能指向子类对象,所有调用的是父类还是子类的同名函数在编译期间无法确定,需要在运行期间才能确定,称为运行时多态(必须发生覆盖)
    构成运行时多态的条件:
        1、父子类以public继承
        2、子类中有对父类成员函数构成覆盖
        3、运行时才能确定父类指针或引用指向的时父类还是子类的对象

    尝试:构造函数和析构函数能否是虚函数?为什么?(构造函数、析构函数的虚函数中名字不考虑)

三、虚构造和虚析构
    虚构造:
        C++中不允许构造函数为虚函数
        1、假设构造函数可以定义为虚函数,那么此时子类的构造函数就会自动覆盖父类的构造函数,当创建子类对象时,会执行子类的构造函数,但是按照执行顺序会先执行父类的构造函数,而此时父类的构造函数已经被覆盖成子类的覆盖函数,又重新回来形成死循环,所以编译器禁止把构造函数定义为虚函数
        2、其次构造函数的使命就是去把类中的成员创建,包括虚函数表、虚表指针,因此如果把构造函数定义为虚函数,但是构造函数都没有执行成功就没有所谓的虚函数表、虚表指针,因此想要把构造函数放入虚函数表中就是一个悖论
    虚析构:
        析构函数可以定义为虚函数
        1、当使用类多态,通过父类指针或引用释放子类对象时,如果析构函数不是虚函数,那么只会执行父类的析构函数,但是由于在创建子类对象时一定执行力子类的构造函数,如果在子类构造函数中申请了资源,此时对象销毁时没有调用子类的析构函数,就会导致内存泄漏
        2、只有把父类的虚构函数定义为虚函数,通过父类指针或引用释放子类对象时,会先调用子类的析构函数(被覆盖),当子类的析构函数执行结束后,按照对象的释放顺序,会自动执行父类的析构函数,此时已经被释放完,所以调用父类的析构函数不会再继续因为覆盖而去执行子类的析构函数,因此不会造成内存泄漏
        总结:
            当使用多态时且子类的析构函数中有需要释放的资源,此时父类中就必须设置为虚析构

四、纯虚函数、抽象类、纯抽象类
    纯虚函数的格式
        virtual 返回值 函数名(参数列表) = 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);
        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 为写入打开文件

                ifstream类,默认以只读方式打开 O_RDONLY 打开文件,文件不存在则失败 "r"
                ofstream类,默认以只写方式打开 O_WRONLY|O_CREAT|O_TRUNC 打开文件,文件不存在则创建,存在则清空 "w"
                fstream类, 默认以读写方式打开 O_RDWR 打开文件,文件不存在则失败,文件存在不清空 "r+"(打开文件后文件指针在开头)
                注意:ios::里面的打开模式单独使用和混合使用的时候有些功能会有所删减变化,不是简单的功能相加,具体底层调用哪个打开模式可以通过strace ./a.out 来追踪底层对系统函数的调用
       
    2、如何判断文件是否打开成功
        a、 通过 !流对象名 执行了该类的!运算符重载版本
            if(!ifs)    //  为真 失败
        b、 通过good\fail成员函数来判断是否成功
            bool good();
            功能:判断上一次流操作是否成功,成功返回真,一般用于判断文件是否打开成功
       
    3、读写文件
        流对象 << 写操作
        流对象 >> 读操作

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

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

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

七、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;
                // 读成功,执行相应操作
            }
posted @ 2023-08-31 19:55  歪爱慕外  阅读(11)  评论(0)    收藏  举报