C++虚函数简析

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u012423865/article/details/79838107

  C++的虚函数是其实现多态的基础,今天在这里分享一下我对C++虚函数相关知识的系统总结,技术有限,如有不当,欢迎指正。
  在将内容前,将大致涉及到的内容图解如下:
 这里写图片描述
 
1. 有无虚函数在继承中的区别

//-- Zuo add on 2018-04-07
class A
{
public:
    virtual void fun(){ std::cout << "A::fun"; }
    void fun1(){ std::cout << "A::fun1"; }
};

class B : public A
{
public:
    virtual void fun(){ std::cout << "B::fun"; }
    void fun1(){ std::cout << "B::fun1"; }
};

void main(char argc, char** argv)
{
  A *a = new A;
  A *b = new B;
  a->fun();
  b->fun();
  a->fun1();
  b->fun1();
}

输出结果:

A::fun
B::fun
A::fun1
A::fun1

  可以看到,如果是虚函数,在继承的时候,如果子类有重写,就会被覆盖(由子类的实现代替父类的实现),如果是普通成员函数,就不会被子类的实现覆盖,这也就是多态的原型。
  例子很简单,想必大家很容易看懂,但是这里我提出两个问题大家思想下,后面会有答案:
  1.1. 如果将上文的A *a = new B;修改成 B *a = new B;结果会怎样变化?
  1.2. 如果虚函数有缺省值,缺省值会有什么特点?
2. 虚函数的本质
  虚函数的本质是因为C++给有虚函数的类增加了一张虚函数表,用来存储所有虚函数的入口地址。在子类继承父类的过程中,首先继承的是这张虚函数表Virtual-Table(以下简称VT),如果子类有对父类虚函数的重写,那么就会在VT中覆盖对应的函数地址。这样就可以实现同样的调用,在不同的子类里,有不同的实现,这也就是多态
  注意一个问题,VT是跟着类走的,也就是说,如果是上文中1.1提到的,那么VT的覆盖就不会发生,因为A *a = new B;等价于A *a = (A*)new B;正因为VT是跟着类走的,如果是B *a = new B;,那就无任何特别,普通的创建对象,如果是A *a = new B;,那在将B类转换成A类的时候,也就是VT合并的时候。所以上文的1.1,输出值就和class A毫无关系了。
  除了上面讲到的,还有两点特性:
   2.1. 只有虚函数的入口地址才会被存储在VT中,如果是普通成员函数,当然不会存储在里面。
   2.2. 为了提高虚函数的调用效率,VT的地址被存放在类的最前面。
  我从网上找了一张图比较明了:
  这里写图片描述
  在继承的过程在VT被子类重写的虚函数地址覆盖父类的虚函数地址,也就是同一个指针在不同的对象中可以指向不同的函数实现,这也就是虚函数的动态绑定实现多态的过程了。这个可以和普通函数的静态绑定相对比,普通函数是在编译期就静态绑定了,而虚函数是在运行期通过VT存储的函数地址实现动态绑定。
3. 纯虚函数
  纯虚函数是一种比虚函数更加极端的函数。它的形式如下:
  virtual void fun() = 0;
  它存在的目的是为了规范接口,使得子类必须要实现对应的接口,如果子类没有实现接口,则会编译报错。
  使用纯虚函数要注意一点,包含纯虚函数的类被称为抽象类,一般被设计为基类,且抽象类不能被实例化(因为有未实现的纯虚函数)。
4. 安全性-访问non-public虚函数
  VT的存在固然为实现C++的多态立下汗马功劳,但是凡事都有双面性,它的到来,也引入了C++的一些安全上的不足。上文我们说到了,为了提高多态调用的性能,C++将VT地址存放在类空间的段首位置,所以我们通过获取类的地址可以找到VT的地址,也就是可以得到一个类所有虚函数的地址,那如果,这里面的虚函数有是non-public的,那就破坏了C++的封装属性了。Show Code:

//-- Zuo add on 2018-04-07
class A
{
private:
    virtual void fun(){ qDebug() << "A fun"; }
};

void main(char argc, char** argv)
{
    A *a = new A;
    typedef void(*fun)(void);
    std::cout << "虚函数表地址 = " << (int*)(a) << std::endl;
    std::cout << "第一个虚函数地址 = " << (int*)(*(int*)a << std::endl;
    //-- 将虚函数地址转换为void fun(void)函数指针
    fun f = (fun)*((int*)(*(int*)a));
    f();
}

可以看到,这里可以无报错的访问到原本为private的虚函数。
5. 虚函数的缺省值不能被覆盖
  虚函数虽然可以被子类所覆盖以形成多态,但是有一个细节还是要注意,虚函数的缺省值是不能被覆盖的,还是上面的代码:

//-- Zuo add on 2018-04-07
class A
{
public:
    virtual void fun(int a = 1){ std::cout << "A::fun && a = " << a; }
};

class B : public A
{
public:
    virtual void fun(int a = 2){ std::cout << "B::fun && a = " << a; }
};

void main(char argc, char** argv)
{
  A *a = new A;
  A *b = new B;
  a->fun();
  b->fun();
}

输出结果:

A::fun && a = 1
B::fun && a = 1

可以看到这里的a没有被改变。

猜你喜欢

转载自blog.csdn.net/u012423865/article/details/79838107
今日推荐