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


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


    但声明后,必须要实现,否则报错。为了解决这个问题,出现了纯虚函数,只有函数声明 没有函数实现
    格式:返回值 函数名(函数参数) = 0;

    注意:如果函数中有纯虚函数,则当前类为抽象类。抽象类不可以定义对象。
                   如果父类有 纯虚函数,子类中必须要重写,否则子类也是抽象类。
                抽象类中可以写非纯虚函数,也可以写成员变量。


    二、析构函数建议定义成虚函数
          在多态的时候,如果子类中有成员变量开辟了空间,那么父类的指针释放的时候,默认湖调用父类的析构函数,子类成员变量申请的内存空间会导致内存泄漏,为了防止出现则使用虚析构函数
           父类可以定义纯虚析构,vritual ~类名() = 0;
           如果子对象没有成员变量申请空间,则没必要用纯虚析构。


    代码:

    头文件:

    1. #ifndef XX_H
    2. #define XX_H
    3. //纯虚函数 的重写
    4. //虚函数的析构函数
    5. #include<iostream>
    6. #include<cstring>
    7. #include<string>
    8. using namespace std;
    9. class xx
    10. {
    11. private:
    12. int age=0;
    13. public:
    14. xx()
    15. {
    16. cout<<"xx已经构造!\n";
    17. }
    18. virtual void eat()=0;
    19. virtual ~xx()
    20. {
    21. cout<<"xx 已经析构!\n";
    22. }
    23. };
    24. class x:public xx //公共继承xx
    25. {
    26. private:
    27. int age1;
    28. public:
    29. x()//构造函数
    30. {
    31. age1=10;
    32. cout<<"x已经构造!"<<endl;
    33. }
    34. virtual void eat()//virtyal 写不写都是虚函数 重写需要返回值,函数名,参数列表,都要一样
    35. {
    36. cout<<"age1:"<<age1<<endl;
    37. }
    38. virtual ~x()
    39. {
    40. cout<<"x已经析构!\n";
    41. }
    42. };
    43. #endif // XX_H

    .cpp 文件

    1. #include
    2. #include"xx.h"
    3. #include"name.h"
    4. using namespace std;
    5. int main(int argc, char *argv[])
    6. {
    7. x b;//虚函数的析构函数的验证 与虚函数的父子继承的重写
    8. return 0;
    9. }

    三、解决菱形继承
         虚继承:解决菱形继承
         格式:class 派生类名:virtual public 基类名{};
         虚继承特点: 当使用虚继承后,如果孙在类中发现有相同的爷爷类,则只会拷贝一份爷爷类的成员变量和方法。


    代码:

    头文件:

    1. #ifndef NAME_H
    2. #define NAME_H
    3. //解决菱形继承问题
    4. #include<iostream>
    5. #include<string>
    6. #include<cstring>
    7. using namespace std;
    8. class name//爷爷
    9. {
    10. private:
    11. int age;
    12. public:
    13. void getAge()
    14. {
    15. cout<<"age: "<<age<<endl;
    16. }
    17. name()
    18. {
    19. age=0;
    20. cout<<"name "<<endl;
    21. }
    22. };
    23. class name1:virtual public name //公共继承 儿子1
    24. {
    25. private:
    26. int age1;
    27. public:
    28. virtual void eat()
    29. {
    30. cout<<"age1: "<<age1<<endl;
    31. }
    32. name1()
    33. {
    34. age1=1;
    35. cout<<"name1 "<<endl;
    36. }
    37. };
    38. class name2:virtual public name //公共继承 儿子2
    39. {
    40. private:
    41. int age2;
    42. public:
    43. virtual void eat()
    44. {
    45. cout<<"age2: "<<age2<<endl;
    46. }
    47. name2()
    48. {
    49. age2=2;
    50. cout<<"name2 "<<endl;
    51. }
    52. };
    53. class name3:public name1 ,public name2 //孙子
    54. {
    55. private:
    56. int age3;
    57. public:
    58. void getAge3()
    59. {
    60. cout<<"age3: "<<age3<<endl;
    61. }
    62. name3()
    63. {
    64. age3=3;
    65. cout<<"name3 "<<endl;
    66. }
    67. virtual void eat()//这个函数在这里是 重写,因为之前都是虚函数//没有虚函数的化 就是隐藏了。
    68. {
    69. cout<<"asdas"<<endl;
    70. }
    71. };
    72. #endif // NAME_H

    .cpp文件

    1. #include <iostream>
    2. #include"xx.h"
    3. #include"name.h"
    4. using namespace std;
    5. int main(int argc, char *argv[])
    6. {
    7. //解决菱形继承 的问题
    8. //爷爷类 ,儿子类 ,孙子类
    9. name3 a;
    10. a.getAge3();
    11. a.getAge();
    12. a.name1::eat();
    13. cout<<endl<<endl;
    14. name2 b;
    15. b.getAge();
    16. return 0;
    17. }

    四、文件操作
          文件: 文本文件:存放ascii内容的文件  字符文件
                     二进制文件:以二进制的形式存储的文件。一般用户不能直接读取。



          1. 文件操作三大类:
           写操作: ofstream
           读操作: ifstream    
           读写操作:fstream
           头文件: #include


    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中。 


    代码:

    1. #define _CRT_SECURE_NO_WARNINGS //预防strcpy的报错
    2. #include<iostream>
    3. #include<cstring>
    4. #include<fstream>
    5. #include<cstring>
    6. #include<string>
    7. #include<ctime>
    8. #include<cstdlib>
    9. using namespace std;
    10. struct student
    11. {
    12. int age;
    13. string name;
    14. };
    15. int main()
    16. {
    17. /*
    18. //1.创建一个输出流对象
    19. ofstream os; //打开一个流 向文件写入数据
    20. //2.打开文件
    21. os.open("gw.txt", ios::out);
    22. //3.写入文件
    23. char w[1000] = "";
    24. int i = 10;
    25. while (i--)
    26. {
    27. time_t tm;
    28. tm=time(NULL);
    29. strcpy(w,ctime(&tm));
    30. os << w<< endl;
    31. }
    32. //4.关闭文件
    33. os.close();
    34. //1.创建一个读取对象
    35. ifstream is; //打开一个流 向文件读取数据
    36. //2.打开文件
    37. is.open("gw.txt", ios::in);
    38. //3.读取文件
    39. char w1[200];
    40. char w2[200];
    41. char w3[200];
    42. char w4[200];
    43. char w5[200];
    44. int a = 0;
    45. while (1)
    46. {
    47. is >> w1>>w2>>w3>>w4>>w5 ;
    48. cout << w1 <<" "<<w2 << " " <<w3 << " " <<w4 << " " <<w5<< endl;
    49. a = strlen(w1);
    50. if (a == 0)
    51. {
    52. break;
    53. }
    54. }
    55. //while(is>>buf)//一个buf 的读,遇到空格,回车就结束
    56. //{
    57. // cout<<buf<<endl;
    58. //}
    59. //
    60. //
    61. //一行一行的读
    62. //while(is.getline(buf,sizeof(buf)))//buf 的空间大一点
    63. //{
    64. // cout<<buf<<endl;
    65. //}
    66. //第三种方式
    67. char c;
    68. //while(c=is.get()!=EOF)
    69. //{
    70. // cout<<c<<endl;
    71. //}
    72. //第四种方式
    73. //string line;
    74. //while(getline(is,line))
    75. //{
    76. // cout<<line<<endl;
    77. //}
    78. is.close();
    79. */
    80. /*
    81. /
    82. //拷贝一个文件,到另外一个文件 (知道内容的格式)
    83. //1.创建一个输出流对象
    84. ofstream os;
    85. //2.打开文件
    86. os.open("gw1.txt", ios::out);//打开了返回 turn 失败返回 false
    87. if (!os.is_open())
    88. {
    89. cerr << "out open" << endl;
    90. return 0;
    91. }
    92. //3.创建一个读取对象
    93. ifstream is;
    94. //4.打开文件
    95. is.open("gw.txt", ios::in);
    96. if (!is.is_open())//打开了返回 turn 失败返回 false
    97. {
    98. cerr << "out open" << endl;
    99. return 0;
    100. }
    101. //5.拷贝文件到另外的文件
    102. int a = 0;
    103. char w1[200];
    104. char w2[200];
    105. char w3[200];
    106. char w4[200];
    107. char w5[200];
    108. while (1)
    109. {
    110. is >> w1 >> w2 >> w3 >> w4 >> w5;
    111. os << w1 << " " << w2 << " " << w3 << " " << w4 << " " << w5 << endl;
    112. cout << w1 << " " << w2 << " " << w3 << " " << w4 << " " << w5 << endl;
    113. a = strlen(w1);
    114. if (a == 0)
    115. {
    116. break;
    117. }
    118. }
    119. is.close();
    120. os.close();
    121. */
    122. //
    123. /*
    124. //二进制版本的 c++ 版本
    125. //1.创建一个输出流对象
    126. ofstream os; //打开一个流 向文件写入数据
    127. //第二种打开方式
    128. //构造的方法打开
    129. //ofstream os("dd.txt",ios::out | ios::binary);
    130. //2.打开文件
    131. os.open("gw2.txt", ios::out | ios::binary);
    132. if (!os.is_open())
    133. {
    134. cerr << "bin out open " << endl;
    135. }
    136. //3.写入文件
    137. char w[1000] = "我是长沙sadada的";
    138. int i = 10;
    139. os.write((const char*)w, sizeof(w));//写入二进制
    140. os.close();
    141. */
    142. /*
    143. //1.创建一个读取对象
    144. ifstream is; //打开一个流 向文件读取数据
    145. //2.打开文件
    146. is.open("gw2.txt", ios::in| ios::binary);
    147. if (!is.is_open())
    148. {
    149. cerr << "bin in open error"<< endl;
    150. }
    151. char w[100];
    152. is.read(w, sizeof(w));
    153. cout << w << endl;
    154. is.close();
    155. */
    156. //作业:写入五个数据,并且读取
    157. string name="dasd";
    158. struct student * w = new struct student[5];
    159. for (int i = 0; i < 5; i++)//给数组赋值
    160. {
    161. w[i].age = i;
    162. w[i].name = name;
    163. }
    164. ofstream os;//打开一个流,向文件写入数据
    165. os.open("gw3.txt", ios::out | ios::binary);
    166. if (!os.is_open())//看看写入 的流是否打开失败
    167. {
    168. cerr << "bin out open error" << endl;
    169. }
    170. for (int j = 0; j < 5; j++)
    171. {
    172. os.write((const char*)&w[j], sizeof(struct student));//写入学生类的数据
    173. }
    174. os.close();
    175. //1.创建一个读取对象
    176. ifstream is; //打开一个流 向文件读取数据
    177. //2.打开文件
    178. is.open("gw3.txt", ios::in | ios::binary);
    179. if (!is.is_open())//看看读取 的流是否打开失败
    180. {
    181. cerr << "bin in open error" << endl;
    182. }
    183. struct student kk;
    184. cout << "读取的信息" << endl;
    185. for (int i = 0; i < 5; i++)
    186. {
    187. is.read((char *)&kk, sizeof(struct student));
    188. cout << kk.age << " " << kk.name << endl;
    189. }
    190. is.close();
    191. return 0;
    192. }

    五。虚函数表

    概述

    为了实现C++的多态,C++使用了一种动态绑定的技术。这个技术的核心是虚函数表(下文简称虚表)。本文介绍虚函数表是如何实现动态绑定的。

    类的虚表

    每个包含了虚函数的类都包含一个虚表。 
    我们知道,当一个类(A)继承另一个类(B)时,类A会继承类B的函数的调用权。所以如果一个基类包含了虚函数,那么其继承类也可调用这些虚函数,换句话说,一个类继承了包含虚函数的基类,那么这个类也拥有自己的虚表。

    我们来看以下的代码。类A包含虚函数vfunc1,vfunc2,由于类A包含虚函数,故类A拥有一个虚表。

    1. class A {
    2. public:
    3. virtual void vfunc1();
    4. virtual void vfunc2();
    5. void func1();
    6. void func2();
    7. private:
    8. int m_data1, m_data2;
    9. };

    图1:类A的虚表示意图


    虚表是一个指针数组,其元素是虚函数的指针,每个元素对应一个虚函数的函数指针。需要指出的是,普通的函数即非虚函数,其调用并不需要经过虚表,所以虚表的元素并不包括普通函数的函数指针。 
    虚表内的条目,即虚函数指针的赋值发生在编译器的编译阶段,也就是说在代码的编译阶段,虚表就可以构造出来了。

    虚表指针

    虚表是属于类的,而不是属于某个具体的对象,一个类只需要一个虚表即可。同一个类的所有对象都使用同一个虚表。 
    为了指定对象的虚表,对象内部包含一个虚表的指针,来指向自己所使用的虚表。为了让每个包含虚表的类的对象都拥有一个虚表指针,编译器在类中添加了一个指针,*__vptr,用来指向虚表。这样,当类的对象在创建时便拥有了这个指针,且这个指针的值会自动被设置为指向类的虚表。



     图二



    上面指出,一个继承类的基类如果包含虚函数,那个这个继承类也有拥有自己的虚表,故这个继承类的对象也包含一个虚表指针,用来指向它的虚表。

    四、动态绑定

    说到这里,大家一定会好奇C++是如何利用虚表和虚表指针来实现动态绑定的。我们先看下面的代码。

    1. class A {
    2. public:
    3. virtual void vfunc1();
    4. virtual void vfunc2();
    5. void func1();
    6. void func2();
    7. private:
    8. int m_data1, m_data2;
    9. };
    10. class B : public A {
    11. public:
    12. virtual void vfunc1();
    13. void func1();
    14. private:
    15. int m_data3;
    16. };
    17. class C: public B {
    18. public:
    19. virtual void vfunc2();
    20. void func2();
    21. private:
    22. int m_data1, m_data4;
    23. };


    类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的虚表。


    1. int main()
    2. {
    3. B bObject;
    4. }
    • 现在,我们声明一个类A的指针p来指向对象bObject。虽然p是基类的指针只能指向基类的部分,但是虚表指针亦属于基类部分,所以p可以访问到对象bObject的虚表指针。bObject的虚表指针指向类B的虚表,所以p可以访问到B vtbl。如图3所示。

    1. int main()
    2. {
    3. B bObject;
    4. A *p = & bObject;
    5. }

    1. int main()
    2. {
    3. B bObject;
    4. A *p = & bObject;
    5. p->vfunc1();
    6. }

    程序在执行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的对象,情况又是怎么样?

    1. int main()
    2. {
    3. A aObject;
    4. A *p = &aObject;
    5. p->vfunc1();
    6. }

    当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++面向对象程序设计的基石。

  • 相关阅读:
    前端工程化工具系列(七)—— PNPM(v9.2.0):高性能的 NPM 替代品
    android 各种偶现问题记录
    三、程序员指南:数据平面开发套件
    proxy实现链式编程
    SAP ABAP 动态结构实现发送企业微信应用消息
    数据压缩与管理:掌握Linux VDO和LVM的力量
    浏览器插件开发爬虫记录
    计算机毕业设计Java幼儿校园通系统的设计与实现(系统+程序+mysql数据库+Lw文档)
    Redis单线程为什么这么快
    c++中的类模板
  • 原文地址:https://blog.csdn.net/she666666/article/details/126549739