博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
c++抽象类、纯虚函数以及巧用纯虚析构函数实现接口类【转】
阅读量:2193 次
发布时间:2019-05-02

本文共 5564 字,大约阅读时间需要 18 分钟。

在Java、C#中有关键词abstract指明抽象函数、抽象类,但是在C++中没有这个关键词,很显然,

在C++也会需要只需要在基类声明某函数的情况,而不需要写具体的实现,那C++中是如何实现

这一功能的,答案是纯虚函数 含有纯虚函数的类是抽象类,不能生成对象,只能派生。他派生

的类的纯虚函数没有被改写,那么它的派生类还是个抽象类。定义纯虚函数就是为了让基类不可

实例化化意义。

一.  纯虚函数

在许多情况下,在基类中不能给出有意义的虚函数定义,这时可以把它说明成纯虚函数,把它的定义留给派生类来做。定义纯虚函数的一般形式为:

class 类名{

 virtual 返回值类型 函数名(参数表)= 0;  // 后面的"= 0"是必须的,否则,就成虚函数了

};

纯虚函数是一个在基类中说明的虚函数,它在基类中没有定义,要求任何派生类都定义自己的版本。纯虚函数为各派生类提供一个公共界面。

从基类继承来的纯虚函数,在派生类中仍是虚函数。

二. 抽象类

1. 如果一个类中至少有一个纯虚函数,那么这个类被称为抽象类(abstract class)。

抽象类中不仅包括纯虚函数,也可包括虚函数。抽象类中的纯虚函数可能是在抽象类中定义的,也可能是从它的抽象基类中继承下来且重定义的。

2. 抽象类特点,即抽象类必须用作派生其他类的基类,而不能用于直接创建对象实例。

一个抽象类不可以用来创建对象,只能用来为派生类提供一个接口规范,派生类中必须重载基类中的纯虚函数,否则它仍将被看作一个抽象类。

3. 在effective c++上中提到,纯虚函数可以被实现(定义)(既然是纯虚函数,为什么还可以被实现呢?这样做有什么好处呢?下文中“巧用纯虚析构函数实现接口类”中将说明这一功能的目的。),但是,不能创建对象实例,这也体现了抽象类的概念。

三. 虚析构函数

虚析构函数: 在析构函数前面加上关键字virtual进行说明,称该析构函数为虚析构函数。虽然构造函数不能被声明为虚函数,但析构函数可以被声明为虚函数

一般来说,如果一个类中定义了虚函数, 析构函数也应该定义为虚析构函数。

例如:

class B

{

 virtual ~B();  //虚析构函数

  …

};

下面介绍一些实例:

[cpp]   
 
  1. #include <stdio.h>  
  2.   
  3. class Animal  
  4. {  
  5. public:  
  6.      Animal()   //构造函数不能被声明为虚函数  
  7.      {  
  8.     printf(" Animal construct! \n");  
  9.      }  
  10.      virtual void shout() = 0;  
  11.      virtual void impl() = 0;  
  12.      virtual ~Animal() {printf(" Animal destory! \n");};   // 虚析构函数  
  13. };  
  14.   
  15.   
  16. void Animal::impl()        // 纯虚函数也可以被实现。  
  17. {  
  18.      printf(" Animal: I can be implement! \n");  
  19. }  
  20.   
  21.   
  22. class Dog: public Animal  
  23. {  
  24. public:  
  25.      Dog()  
  26.      {  
  27.     printf(" Dog construct! \n");  
  28.      }  
  29.      virtual void shout() // 必须要被实现,即使函数体是空的  
  30.      {  
  31.          printf(" Dog: wang! wang! wang! \n");  
  32.      }  
  33.      virtual void impl()  
  34.      {  
  35.          printf(" Dog: implement of Dog!  \n");  
  36.      }  
  37.      virtual ~Dog() {printf(" Dog destory! \n");};   // 虚析构函数  
  38. };  
  39.   
  40.   
  41. class Cat: public Animal  
  42. {  
  43. public:  
  44.      Cat()  
  45.      {  
  46.     printf(" Cat construct! \n");  
  47.      }  
  48.      virtual void shout() // 必须要被实现,即使函数体是空的  
  49.      {  
  50.          printf(" Cat: miao! miao! miao! \n");  
  51.      }  
  52.       
  53.      virtual void impl()  
  54.      {  
  55.          printf(" Cat: implement of Cat!  \n");  
  56.      }  
  57.      virtual ~Cat() {printf(" Cat destory! \n");};   // 虚析构函数  
  58. };  
  59.   
  60.   
  61. /* 
  62. Animal f()  // error, 抽象类不能作为返回类型 
  63. {
     
  64.        
  65. } 
  66.  
  67. void display( Animal a) //error, 抽象类不能作为参数类型 
  68. {
     
  69.      
  70. } 
  71. */  
  72.   
  73.   
  74. //ok,可以声明抽象类的引用  
  75. Animal &display(Animal &a)  
  76. {  
  77.        Dog d;  
  78.        Animal &p = d;  
  79.        return p;  
  80.         
  81. }  
  82.   
  83. void test_func()  
  84. {  
  85.      //Animal a;  // error: 抽象类不能建立对象  
  86.       
  87.     Dog dog;   //ok,可以声明抽象类的指针  
  88.     Cat cat;   //ok,可以声明抽象类的指针  
  89.      
  90.     printf("\n");  
  91.   
  92.     Animal *animal = &dog;  
  93.     animal->shout();  
  94.     animal->impl();  
  95.      
  96.     printf("\n");  
  97.      
  98.     animal = &cat;  
  99.     animal->shout();  
  100.     animal->impl();  
  101.      
  102.     printf("\n");  
  103. }  
  104.   
  105. int main()  
  106. {  
  107.     test_func();  
  108.   
  109.     while(1);    
  110. }  
  111.   
  112.   
  113. //result:  
  114. /* 
  115. Animal construct! 
  116. Dog construct! 
  117. Animal construct! 
  118. Cat construct! 
  119.  
  120. Dog: wang! wang! wang! 
  121. Dog: implement of Dog! 
  122.  
  123. Cat: miao! miao! miao! 
  124. Cat: implement of Cat! 
  125.  
  126. Cat destory! 
  127. Animal destory! 
  128. Dog destory! 
  129. Animal destory! 
  130. */  
  131. (YC:代码已调试无误)  

四. 巧用纯虚析构函数实现接口类
c++不像java一样有纯接口类的语法,但我们可以通过一些手段实现相同的功能。
(1)能不能用“protected”实现接口类?

看如下代码:

[cpp]   
 
  1. #include <stdio.h>  
  2.   
  3.   
  4. class A  
  5. {  
  6. protected:  
  7.     virtual ~A()  
  8.     {  
  9.         printf(" A: 析构函数  \n");  
  10.     }  
  11. };  
  12. class B : public A  
  13. {  
  14. public:  
  15.     virtual ~B()  
  16.     {  
  17.         printf(" B: 析构函数  \n");  
  18.     }  
  19. };  
  20. int _tmain(int argc, _TCHAR* argv[])  
  21. {  
  22.     //A* p1 = new A;              //error:[1]有问题  
  23.     //delete p1;  
  24.   
  25.   
  26.     B* p2 = new B;           //ok:[2]没问题,输出结果为:  
  27.     delete p2;               /* B: 析构函数 
  28.                                         A: 析构函数*/(注意此处还是会调用A的析构函数的,不过编译没问题)  
  29.                    
  30.     //A* p3 = new B;  
  31.     //delete p3;                 //error:[3] 有问题  
  32.   
  33.   
  34.     return 0;  
  35. }  

通过在类中,将类的构造函数或者析构函数申明成protected ,可以有效防止类被实例话,要说实用的话,构造函数是protected更有用,肯定能保证类不会被实例化,而如果析构函数是protected的话,构造函数不是protected的话,还可能存在编译通过的漏洞,如下:

Case1:

[cpp]   
 
  1. class A  
  2. {  
  3. protected:  
  4.     A()  
  5.     {  
  6.         printf(" A: A()  \n");  
  7.     }  
  8. };  
  9.   
  10. int _tmain(int argc, _TCHAR* argv[])  
  11. {  
  12.     A* p1 = new A;                //编译不通过,无法访问protected构造函数  
  13.     delete p1;  
  14.   
  15.     return 0;  
  16. }  

Case2:

[cpp]   
 
  1. class A  
  2. {  
  3. protected:  
  4.     ~A()  
  5.     {  
  6.         printf(" A: ~A()  \n");  
  7.     }  
  8. };  
  9.   
  10. int _tmain(int argc, _TCHAR* argv[])  
  11. {  
  12.     A* p1 = new A;                //编译通过,此时因为仅仅是用到了A的构造函数,还不需要它的析构函数  
  13.     return 0;  
  14. }  
  15.   
  16. (附:如果将main中改为:  
  17. int _tmain(int argc, _TCHAR* argv[])  
  18. {  
  19.     A a;  
  20.     return 0;  
  21. }  
  22. 则编译出错,提示无法访问protected成员A::~A().两种情况出现差异的原因是什么?  
  23.   
  24. )  

Case3:

[cpp]   
 
  1. class A  
  2. {  
  3. protected:  
  4.     ~A()  
  5.     {  
  6.         printf(" A: ~A()  \n");  
  7.     }  
  8. };  
  9. int _tmain(int argc, _TCHAR* argv[])  
  10. {  
  11.     A* p1 = new A;                  
  12.     delete p1;                //编译失败,因为编译器发现A的析构函数是protected  
  13.     return 0;  
  14. }  

所以,一种可行的办法貌似是这样的:

[cpp]   
 
  1. class A  
  2. {  
  3. protected:  
  4.     virtual ~A()  
  5.     {  
  6.         printf(" A: ~A()  \n");  
  7.     }  
  8. };  
  9.   
  10. class B : public A  
  11. {  
  12. };  
  13.   
  14. int _tmain(int argc, _TCHAR* argv[])  
  15. {  
  16.     B* p =new B;       //ok:这种情况下确实是可行的(YC:仔细看会发现这种情况同“(1)看如下代码”下面的代码中ok的情况相同)  
  17.     delete  p;  
  18.     return 0;  
  19. }  

由于B public继承自A,所以其可以完全访问A的构造或析构函数,但是:

[cpp]   
 
  1. int _tmain(int argc, _TCHAR* argv[])  
  2. {  
  3.     A* p =new B;  
  4.     delete  p;                //error:由于p变成指向A的指针,字面上编译器需要知道A的析构函数,然后A的析构函数又是protected  
  5.     return 0;  
  6. }  

即便像这样B显示重载了A的析构函数:

[cpp]   
 
  1. class A  
  2. {  
  3. protected:  
  4.     virtual ~A()  
  5.     {  
  6.         printf(" A: ~A()  \n");  
  7.     }  
  8. };  
  9. class B : public A  
  10. {  
  11. public:  
  12.     virtual ~B()  
  13.     {  
  14.         printf(" B: ~B()  \n");  
  15.     }  
  16. };  
  17. int _tmain(int argc, _TCHAR* argv[])  
  18. {  
  19.     A* p =new B;  
  20.     delete  p;        //error:也还是不行,因为重载是运行时的事情,在编译时编译器就认定了A的析构函数,结果无法访问  
  21.     return 0  
  22. }  

小结:

貌似用protected这样的方法并不是很恰当,虽然在遵守一定规则的情况下确实有他的实用价值,但并不是很通用
(2)应该怎样实现接口类?

其实上面protected的思路是对的,无非是让父类无法实例化,那么为了让父类无法实例化,其实还有一个方法,使用纯虚函数

[cpp]   
 
  1. class A  
  2. {  
  3. public:            //这里就不用protected了  
  4.     virtual ~A() = 0;  
  5. };  
  6. class B : public A  
  7. {  
  8. };  
  9. int _tmain(int argc, _TCHAR* argv[])  
  10. {  
  11.     B* p =new B;  
  12.     delete  p;      //编译ok,链接error  
  13.     return 0;  
  14. }  

这样写貌似不错,以往大家都把类中的一般成员函数写成纯虚的,这次将析构函数写成纯虚的,更加增加通用性,编译也通过了,但就是在链接的时候出问题,报错说找不到A的析构函数的实现,很显然嘛,因为A的析构是纯虚的嘛。

那么如何修改上述代码可以达到既可以去除上述error,又可以让基类不能被实例化呢?如下所示:

[cpp]   
 
  1. class A  
  2. {  
  3. public:                 //这里就不用protected了  
  4.     virtual ~A() = 0                //它虽然是个纯虚函数,但是也可以被实现  
  5.     {                               //这个语法很好很强大(完全是为了实现其接口类而弄的语法吧)  
  6.         printf(" A: ~A()  \n");  
  7.     }  
  8. };  
  9. class B : public A  
  10. {  
  11. };  
  12. int _tmain(int argc, _TCHAR* argv[])  
  13. {  
  14.     B* p =new B;  
  15.     delete  p;  
  16.     A* p2 =new B;  
  17.     delete  p2;            //不用担心编译器报错了,因为此时A的析构函数是public  
  18.     return 0;  
  19. }  
  20. //result:  
  21. /* 
  22.  A: ~A() 
  23.  A: ~A() 
  24. */   

如此终于大功告成了,注意,不能将构造函数替代上面的析构函数的用法,因为构造函数是不允许作为虚函数的

补充:以上那个语法就真的只是为了这种情况而存在的,因为一般我们在虚类中申明的接口:

virtual foo()= 0;

virtual foo()= 0 {}

这两种写法是完全没有区别的纯虚函数的默认实现,仅仅在它是析构函数中才有意义!!!

所以可以说,老外是完全为了这一个目的而发明了这种语法...

最终的接口类

[cpp]   
 
  1. classInterface  
  2. {  
  3. public:         
  4.     virtual ~Interface() = 0 {}  
  5. };  

应该挺完美的了吧

转载地址:http://cinub.baihongyu.com/

你可能感兴趣的文章
Play on Words UVA - 10129 (欧拉路径)
查看>>
mininet+floodlight搭建sdn环境并创建简答topo
查看>>
【linux】nohup和&的作用
查看>>
Set、WeakSet、Map以及WeakMap结构基本知识点
查看>>
【NLP学习笔记】(一)Gensim基本使用方法
查看>>
【NLP学习笔记】(二)gensim使用之Topics and Transformations
查看>>
【深度学习】LSTM的架构及公式
查看>>
【python】re模块常用方法
查看>>
剑指offer 19.二叉树的镜像
查看>>
剑指offer 20.顺时针打印矩阵
查看>>
剑指offer 21.包含min函数的栈
查看>>
剑指offer 23.从上往下打印二叉树
查看>>
剑指offer 25.二叉树中和为某一值的路径
查看>>
剑指offer 60. 不用加减乘除做加法
查看>>
Leetcode C++《热题 Hot 100-14》283.移动零
查看>>
Leetcode C++《热题 Hot 100-15》437.路径总和III
查看>>
Leetcode C++《热题 Hot 100-17》461.汉明距离
查看>>
Leetcode C++《热题 Hot 100-18》538.把二叉搜索树转换为累加树
查看>>
Leetcode C++《热题 Hot 100-21》581.最短无序连续子数组
查看>>
Leetcode C++《热题 Hot 100-22》2.两数相加
查看>>