一、虚函数、虚函数表、虚表指针、覆盖
1、虚函数
-
在成员函数前面加 virtual 后,该函数就称为虚函数,此时该类就会像虚进程一样多了一个虚表指针(虚函数表指针,虚指针)
class Base { public: void func(void) { cout << "Base func" << endl; } } cout << sizeof(Base) << endl; // 输出1用于this指针返回的地址 ,函数前virtual 输出4
2、虚函数表
- 虚表指针指向的是一张表格的首地址,该表格中记录了该类中所有虚函数的首地址
class Base
{
public:
virtual void func(void)
{
cout << "void func" << endl;
}
}
Base* b = new Base;
((void(*)(void))(*(int*)*(int*)b))();// 和下一行等效
((void(*)())(**(int**)b))(); // 和下一行等效
b->func();
- 如果类中没有其他成员变量,则可以直接通过虚函数表以及虚表指针来访问虚函数表中第一个虚函数void func(void)
3、覆盖(重写) 是构成多态的基础
-
当使用 virtual 修饰父类的成员函数时,此时父类中就会多一个虚表指针以及一张虚函数表,子类继承父类时会把父类的虚表指针以及虚函数表一起继承过来,然后编译器会去比较父子类中同名的成员函数的格式,如果格式完全相同的虚函数,就会把子类中虚函数表中原来同名父类虚函数的地址改为子类同名函数的地址。称为覆盖
-
此时使用父类指针或引用指向子类对象时,调用虚函数则会去执行的是被覆盖后的虚函数表中所指向子类的同名且格式相同的成员函数,不再调用父类的同名虚函数
-
构成覆盖的条件
-
子类以public继承父类
-
父类中被覆盖的函数必须是虚函数
-
子类中必须有与父类虚函数同名的成员函数,且该函数的返回值、参数列表、常属性都必须相同
-
返回值类型相同,或者子类同名成员函数的返回值类型可以向父类虚函数的返回值类型做隐式转换,且有继承关系
-
4、重载、覆盖、隐藏、重写的区别
-
隐藏:
-
父子类中,同名且格式不同,无论是否有virtual修饰,都构成隐藏
-
父子类中,同名且格式相同,如果没有virtual修饰,则构成隐藏
-
隐藏可以隐藏同名成员变量、成员函数,但是覆盖只能覆盖满足条件的成员虚函数
-
在父子类中,同名成员函数,要么构成隐藏,要么构成覆盖
-
除去父子类外,其他的不同作用域下同名标识符也构成隐藏
-
二、多态
1、什么是多态
-
指的是一条指令可以有多种形态,当调用一个指令时,它能够根据参数,环境的不同作出不同的操作,这种情况称为多态
-
C++中根据确定指令具体操作的时间划分多态:
- 编译时多态、运行时多态
2、编译时多态
-
当调用函数重载版本时,编译器会根据参数的类型、个数等确定调用的是哪个版本的重载函数,这就是所谓的编译时多态
-
还有例如泛型编程中模板技术等
3、运行时多态
- 当子类覆盖了父类的同名函数,然后使用父类指针或引用去访问虚函数,该父类指针或引用既可以指向父类对象,也可以指向子类对象,所以调用的是父类还是子类的同名函数在编译期间无法确定,需要在运行期间才能确定,称为运行时多态(必须发生覆盖)
4、构成运行时多态的条件
-
父子类以public继承
-
子类中有对父类成员函数构成覆盖
-
运行时才能确定父类指针或引用指向的是父类还是子类的对象
三、虚构造和虚析构
1、虚构造
-
C++中不允许构造函数为虚函数
-
假设可以定义为虚函数,那么此时子类的构造函数就会自动覆盖父类的构造函数,当创建子类对象时,会执行子类的构造函数,但是按照执行顺序会先执行父类的构造函数,而此时父类的构造函数已经被覆盖成子类的构造函数,又重新回来形成死循环,所以编译器禁止把构造函数定义为虚函数
-
构造函数的使命就是去把类中的成员创建,包括虚函数表、虚表指针。如果把构造函数定义为虚函数,但是构造函数都没有执行成功就没有所谓的虚函数表、虚表指针,因此想要把构造函数放入虚函数表中就是一个悖论
2、虚析构
-
析构函数可以定义为虚函数
-
当使用类多态,通过父类指针去引用或释放子类对象时,如果析构函数不是虚函数,那么只会执行父类的析构函数,但是由于在创建子类对象时,一定执行了子类的构造函数,如果在子类构造函数中申请了资源,此时对象销毁时没有调用子类的析构函数,就会导致内存泄漏
-
只有把父类的析构函数定义为虚函数,通过父类指针或引用释放子类对象时,会先调用子类的析构函数(覆盖),当子类的析构函数执行结束后(子类已经完成释放),按照对象的释放顺序,会自动执行父类的析构函数,此时子类已经被释放完,所以调用父类的析构函数不会再继续因为覆盖而去执行子类的析构函数,因此不会造成内存泄漏
3、总结
- 当使用多态时且子类的析构函数中需要有释放的资源,此时父类中就必须设置为虚析构
四、纯虚函数、抽象类、纯抽象类
纯虚函数的格式
-
virtual 返回值 函数名(参数列表) = 0;
-
纯虚函数可以不去实现,一般人也没必要去实现
-
父类中如果有纯虚函数,那么继承该父类的子类必须对其进行覆盖,否则无法创建对象
-
有纯虚函数的类不能创建对象
-
纯虚函数就是为了强制子类去覆盖,为了强制子类实现某些功能
-
有纯虚函数的类都成为抽象类
-
析构函数可以设置为纯虚函数,但是必须在类外定义
纯抽象类
- 所有的成员函数都是纯虚函数的类,称为纯抽象类,这种类一般用于设置功能接口,所以也称为接口类
了解工厂模式
五、IO流
头文件:fstream
C++把对文件的读写操作都封装在一下的类中:
-
ofstream类 对文件的写操作,继承了ostream类的功能
-
ifstream类 对文件的读操作,继承了istream类的功能
-
fstream类 对文件的读写操作,继承了ofstream、ifstream类的功能
六、C++对文本文件的读写
1、创建流对象,通过流对象打开文件
-
通过构造函数创建
ofstream ofs(const char *filename,openmode mode);
-
通过成员函数的方式,打开文件
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 覆盖存在的文件
-
-
#include <fstream> int main() { ifstream ifs("test.txt"); if(!ifs)// 运算符重载 cout << "文件打开失败" << endl; else cout << "文件打开成功" << endl; }
-
ifstream类,默认以只读方式O_RDONLY打开文件,文件不存在则失败 “r”
-
ofstream类,默认以只写方式O_WRONLY|O_CREAT|O_TRUNC打开文件,文件不存在则创建、存在则清空 “w”
-
fstream类,默认以读写方式O_RDWR打开文件,文件不存在则失败,文件存在不清空 “r+”
-
注意:ios::里面的打开模式单独使用和混合使用时候有些功能会有所删减变化,不是简单的功能相加,具体底层调用可以通过strace ./a.out来追踪底层对系统函数的调用
2、如何判断文件是否打开成功
-
通过 !流对象名 执行了该类的 !运算符重载版本
if(!ifs) // 为真 失败
-
通过good\fail成员函数判断是否成功
bool good(); // 功能:判断上一次流操作是否成功,成功返回真,一般用于判断文件是否打开成功 bool fail(); // 功能:判断上一次流操作是否失败
3、读写文件
-
流对象 << 写操作
-
流对象 >> 读操作
-
关闭文件
-
成员函数
-
void close(void)
-
注意:只是关闭流对象当前的文件,但是流对象没有销毁,还可以继续通过open成员函数重新打开别的文件
-
-
如何以文本方式读写类对象
-
读写类对象时绝大部分成员变量都是私有的,因此无法直接在类外进行读写
-
由于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();
// 功能:判断读操作是否读到了文件末尾,如果是返回真
二进制读写需要注意的问题
-
对象的成员变量中不应该有指针类型(或string类型),因为此时写入只会把指针变量存储的地址写入,而下次读取到该指针变量时,该指针地址已经没有意义了
-
一直读取二进制文件
while(true) { // 读操作 if(fs.eof())break; // 读成功,执行相应操作 }