C++ 虚函数 、纯虚函数、接口的实用方法和意义

时间:2022-10-13 09:59:44
也许之前我很少写代码,更很少写面向对象的代码,即使有写多半也很容易写回到面向过程的老路上去。在写面向过程的代码的时候,根本不管什么函数重载和覆盖,想到要什么功能就变得法子的换个函数名字,心里想想:反正函数重载本质也就是入栈了两个不同的函数。
回过头来讲,让我了解标题这三个概念的实际用处,还是在于我这第四次重写毕业论文的代码,将它改写成面向对象的时候,才理解的。在面向对象设计的过程中, 类是从抽象逐渐具体起来的,父类可以是非常非常抽象的东西,而最终实例化的子类就非常具体了。在这个继承的过程中,不断的对父类进行填充丰富,最终得到的 子类就是有血有肉的 - 我的理解。
纯虚函数,就是虚函数了以后,末尾还要加=0的那一类函数。我一直没想通的是,既然这个函数完全没有实现方法,那么定义这个函数有个蛋用啊?我也曾经试着 在网上搜索过纯虚函数的意义和作用,回答大多千篇一律照本宣科。于是我渐渐的也就无视这个纯虚函数了。直到现在我开始写一个PSO算法的时候,才发现天哪 这居然是一个完全不可或缺的东西!如果说虚函数还可以用重命名作为另外一种解决方法,那么纯虚函数则是没有第二种可以替代的方法。我可以拿一个非常简单的 代码说明一下:
class test{ public: virtual void print(); virtual void order()=0; int array[20]; };

void test::print(){
order();
printf("打印结果: ");
for(int i=0; i<20; i++)
printf("%d ", array[i]);
}
至于接口,这是一个只有JAVA中才用到的概念,C++中不存在接口,与接口相似的是:抽象类。因为JAVA不允许多重继承类,但可以继承多个接口。关于
接口,在我编写JAVA
SERVLET的时候,碰到过一个httpservlet,用户需要为doget和dopost等函数编写实现方法。而这些函数就可以看成是纯虚函数,它
在HTTPservlet也类似于上述代码的order函数,有着在局部函数中的作用。

总结 :1、虚函数
通过虚函数,在调用不同的衍生类的时候,可以拥有不同的功能。同时,我们可以通过将这么做完全可以,只要你自己能熟记或者找到这个重命名函数是干嘛用的;但是在大一点的项目中,由于类中的函数成百上千,恐怕你就会为此疯狂。

本文较为深入的分析了C++中虚函数与纯虚函数的用法,对于学习和掌握面向对象程序设计来说是至关重要的。具体内容如下:

首先,面向对象程序设计(object-oriented programming)的核心思想是数据抽象、继承、动态绑定。通过数据抽象,可以使类的接口与实现分离,使用继承,可以更容易地定义与其他类相似但不完全相同的新类,使用动态绑定,可以在一定程度上忽略相似类的区别,而以统一的方式使用它们的对象。

虚函数的作用是实现多态性(Polymorphism),多态性是将接口与实现进行分离,采用共同的方法,但因个体差异而采用不同的策略。纯虚函数则是一种特殊的虚函数。虚函数联系到多态,多态联系到继承。所以本文中都是在继承层次上做文章。没了继承,什么都没得谈。

一、虚函数

1 . 定义

在C++中,基类必须将它的两种成员函数区分开来:一种是基类希望其派生类进行覆盖的函数;另一种是基类希望派生类直接继承而不要改变的函数。对于前者,基类通过在函数之前加上virtual关键字将其定义为虚函数(virtual)。

1
2
3
4
5
6
7
8
9
class Base{ // 基类
public:
  virtual int func(int n) const;
};
  
class Derive_Class : public Base{
public:
  int func(int n) const; // 默认也为虚函数
};

当我们在派生类中覆盖某个函数时,可以在函数前加virtual关键字。然而这不是必须的,因为一旦某个函数被声明成虚函数,则所有派生类中它都是 虚函数。任何构造函数之外的非静态函数都可以是虚函数。派生类经常(但不总是)覆盖它继承的虚函数,如果派生类没有覆盖其基类中某个虚函数,则该虚函数的 行为类似于其他的普通成员,派生类会直接继承其在基类中的版本。

2 . 动态绑定

当我们使用基类的引用(或指针)调用一个虚函数时将发生动态绑定(dynamic binding)。因为我们直到运行时才能知道到底调用了哪个版本的虚函数,可能是基类中的版本也可能是派生类中的版本,判断的依据是引用(或指针)所绑 定的对象的真实类型。与非虚函数在编译时绑定不同,虚函数是在运行时选择函数的版本,所以动态绑定也叫运行时绑定(run-time binding)。

3 . 静态类型与动态类型

静态类型指的是变量声明时的类型或表达式生成的类型,它在编译时总是已知的;动态类型指的是变量或表达式表示的内存中的对象的类型,它直到运行时才 可知。当且仅当通过基类的指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。如果表达式既不是 引用也不是指针,则它的动态类型永远与静态类型一致。

4 . final和override

派生类中如果定义了一个函数与基类中虚函数同名但形参列表不同,编译器会认为这是派生类新定义的函数。如果我们的意图本是覆盖虚函数,则这种错误很 难发现。通过在派生类中的虚函数最后加override关键字使得意图更加清晰。如果我们使用override标记了某个函数,但该函数并没有覆盖已存在 的虚函数,编译器将报错。

1
2
3
4
5
6
7
8
9
class Base{ // 基类
public:
  virtual int func(int a, int b) const;
};
  
class Derive_Class : public Base{
public:
  int func(int a) const override; // 报错,没有覆盖虚函数
};

如果我们定义一个类,并不希望它被继承。或者希望某个函数不被覆盖,则可以把类或者函数指定为final,则之后任何尝试继承该类或覆盖该函数的操作将引发错误。

1
2
3
4
class Base final { /*  */ };   // 基类不能被继承
class Derive_Class : public Base { /* */ };   // 报错
  
void func(int) const final;  // 不允许后续的其他类覆盖func(int)

5 . 回避虚函数的机制

在某些情况下,我们希望对虚函数的调用不要进行动态绑定,而是强迫其执行虚函数的某个特定版本。可以使用作用域运算符实现这一目的。

1
2
// 强行调用基类中定义的函数版本而不管baseP的动态类型是什么
int a = baseP->Base::func(42);

如果一个派生类虚函数需要调用它的基类版本,但是没有使用作用域运算符,则在运行时该调用将被解析为对派生类版本自身的调用,从而导致无限递归。

二、纯虚函数

1 . 定义

为了方便使用多态特性,我们常常需要在基类中定义虚函数。在许多情况下,在基类中不能对虚函数给出有意义的实现。为了让虚函数在基类什么也不做,引 进了“纯虚函数”的概念,使函数无须定义。我们通过在函数体的位置(即在声明语句的分号之前)书写=0就可以将一个虚函数说明为纯虚函数(pure virtual)。其中,=0只能出现在类内部的虚函数声明语句处:

1
2
3
4
class Base{ // 抽象基类
public:
  virtual int func(int n) const =0;
};

需要注意的是,我们也可以为纯虚函数提供定义,不过函数体必须定义在类的外部。

2 . 抽象基类

含有(或者未经覆盖直接继承)纯虚函数的类叫抽象基类(abstract base class)。抽象基类负责定义接口,而后续的其他类可以覆盖该接口。如果派生类中没有重新定义纯虚函数,而只是继承基类的纯虚函数,则这个派生类仍然还 是一个抽象基类。因为抽象基类含有纯虚函数(没有定义),所以我们不能创建一个抽象基类的对象,但可以声明指向抽象基类的指针或引用。

1
Base base;  // 错误,不能实例化抽象基类

总结:

①.虚函数必须实现,不实现编译器会报错。



②.父类和子类都有各自的虚函数版本。由多态方式在运行时动态绑定。



③.通过作用域运算符可以强行调用指定的虚函数版本。



④.纯虚函数声明如下:virtual void funtion()=0; 纯虚函数无需定义。包含纯虚函数的类是抽象基类,抽象基类不能创建对象,但可以声明指向抽象基类的指针或引用。



⑤.派生类实现了纯虚函数以后,该纯虚函数在派生类中就变成了虚函数,其子类可以再对该函数进行覆盖。



⑥.析构函数通常应该是虚函数,这样就能确保在析构时调用正确的析构函数版本。