Visual C++中的虚函数和纯虚函数的原理

微软技术分享 微软最有价值专家
全栈领域优质创作者
博客专家认证
2024-02-12 09:59:18

我是荔园微风,作为一名在IT界整整25年的老兵,今天针对 Visual C++中的虚函数和纯虚函数的原理来聊聊。本文程序全部在Microsoft Visual Studio 2022上调试通过。

首先,先满足一下急性子的同学,因为有的同学是因为急于了解虚函数和纯虚函数才来看这篇帖子的,那你可以先这样理解:

C++中的虚函数就是JAVA中的普通函数, C++ 中的纯虚函数就是JAVA中的抽象函数, C++ 中的抽象类就是JAVA中的抽象类, C++ 中的虚基类就是JAVA中的接口。

这就是本人的学习方法和别人不一样的地方,我年轻时学C++始终不得要领,于是我把JAVA学会后再去学C++,就全都搞明白了。所以我的帖子都是用这种方法去学C++的。希望那些学C++很吃力的同学可以来借鉴我的这个学习方法。

好了,我们先来看一个程序,一个父亲和一个儿子,希望父亲的学习本领能被儿子继承下去。我们在儿子类中重新定义学习方法。我们希望如果对象是儿子,就调用 儿子类的学习方法,如果对象是父亲,那么就调用父亲类的学习方法。  

#include <iostream>
using namespace std;

class Father
{
public:
  void eat()
  {
    cout<<"eat"<<endl;
  }
  void run()
  {
    cout<<"run"<<endl;
   }
  void study()
  {
    cout<<"study"<<endl;
  }
};

class Son : public Father
{
public:
  void study()
   {
    cout<<"new study"<<endl;
  }
};

void fn(Father *p)
{
    p->study();
}

int main()
{
  Father *p;
  Son boy;
  p=&boy;
  fn(p);
  return 0;
}
 

我们在儿子类中重新定义了study()方法,上一代人可能喜欢用做笔记的方法来学习,而新一代人总是有新的学习方法的吧,比如在设计好的教学游戏中学习,哈哈。接着定义了一个全局函数fn(),指向父亲类的指针作为 fn()函数的参数。在main()函数中,定义了一个儿子类的对象,将它的地址赋给了指向父亲类的指针变量 p,然后调用 fn()函数。当我们将儿子类的对象boy的地址直接赋给指向父亲类的指针变量,C+编译器居然不报错。这是因为儿子对象也是一个父亲对象,将儿子类型转换为父亲类型不用强制类型转换,C++编译器会自动进行这种转换。反过来,则不能把父亲对象看成是儿子对象,如果一个父亲对象确实是儿子对象,那么在程序中需要进行强制类型转换,这样编译才不会报错。

大家可以猜想一下上面程序运行的结果,输出的结果是“study”,为什么输出的结果不是“new study”呢?这是因为在我们将儿子类的对象fh的地址赋给p时,C++编译器进行了类型转换,此时C++编译器认为变量p保存的就是父亲对象的地址。当在 fn函数中执行p->study()时,调用的当然就是父亲对象的study函数。为了帮助大家更好地理解对象类型的转换,我们给出了儿子对象内存模型,如图所示:

https://img-blog.csdnimg.cn/f64211521b0f48ec9a7899aa7a595319.jpeg

当构造儿子类的对象时,首先要调用父亲类的构造函数去构造父亲类的对象,然后才调用儿子类的构造函数构完成自身部分的构造,从而拼接出一个完整的儿子对象。

当我们将儿子类的对象转换为父亲类型时,该对象就被认为是原对象整个内存模型的上半部分,也就是上图中的“父亲的对象所占内存”。当我们利用型转换后的对象指针去调用它的方法时,自然也就是调用它所在的内存中的方法。

现在我们在父亲类的study()方法前面加上一个virtual关键字,

#include <iostream>
using namespace std;

class Father
{
public:
  void eat()
  {
    cout<<"eat"<<endl;
  }
  void run()
  {
    cout<<"run"<<endl;
   }
  virtual void study()
  {
    cout<<"study"<<endl;
  }
};

class Son : public Father
{
public:
  void study()
   {
    cout<<"new study"<<endl;
  }
};

void fn(Father *p)
{
    p->study();
}

int main()
{
  Father *p;
  Son boy;
  p=&boy;
  fn(p);
  return 0;
}
 

用virtual关键字申明的函数叫作虚函数。运行这个程序,结果调用的是儿子类的学习方法。程序输出结果是“new study”。

这就是C++中的多态性。当C++编译器在编译的时候,发现父亲类的study()函数是虚函数,这个时候C++就会采用迟绑定(latebinding)技术。也就是编译时并不确定具体调用的函数,而是在运行时,依据对象的类型(在程序中,我们传递的儿子类对象的地址)来确认调用的是哪一个函数,这种能力就叫作C++的多态性。我们没有在study()函数前加virtual关键字时,C++编译器在编译时就确定了哪个函数被调用,这叫作早期绑定(early binding)。

C++的多态性概括就是:在基类的函数前加上 virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。

下面我们将study()函数申明为纯虚函数,会了发生什么事呢?

#include <iostream>
using namespace std;

class Father
{
public:
  void eat()
  {
    cout<<"eat"<<endl;
  }
  void run()
  {
    cout<<"run"<<endl;
   }
  virtual void study()=0;
};

class Son : public Father
{
public:
  void study()
   {
    cout<<"new study"<<endl;
  }
};

void fn(Father *p)
{
    p->study();
}

int main()
{
  Father *p;
  Son boy;
  p=&boy;
  fn(p);
  return 0;
}
 

纯虚函数是指被标明为不具体实现的虚成员函数(注意:纯虚函数也可以有函数体,但这种提供函数体的用法很少见)。纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义。含有纯虚函数的类叫作抽象类,这种类不能声明对象,只是作为基类为派生类服务。在派生类中必须完全实现基类的纯虚函数,否则,派生类也变成了抽象类,不能实例化对象。

纯虚函数多用在一些方法行为的设计上。在设计基类时,不太好确定或将来的行为多种多样,而此行为又是必需的,我们就可以在基类的设计中,以纯虚函数来声明此种行为,而不具体实现它。

C++的多态性是由虚函数来实现的,而不是纯虚函数。在子类中如果有对基类虚函数的覆盖定义,那么无论该覆盖定义是否有virtual关键字,都是虚函数。


文章来源: https://blog.csdn.net/wang2015cn/article/details/131426986
版权声明: 本文为博主原创文章,遵循CC 4.0 BY-SA 知识共享协议,转载请附上原文出处链接和本声明。


...全文
48 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

460

社区成员

发帖
与我相关
我的任务
社区描述
微软技术社区为中国的开发者们提供一个技术干货传播平台,传递微软全球的技术和产品最新动态,分享各大技术方向的学习资源,同时也涵盖针对不同行业和场景的实践案例,希望可以全方位地帮助你获取更多知识和技能。
windowsmicrosoft 企业社区
社区管理员
  • 微软技术分享
  • 郑子铭
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

微软技术社区为中国的开发者们提供一个技术干货传播平台,传递微软全球的技术和产品最新动态,分享各大技术方向的学习资源,同时也涵盖针对不同行业和场景的实践案例,希望可以全方位地帮助你获取更多知识和技能。

予力众生,成就不凡!微软致力于用技术改变世界,助力企业实现数字化转型。

试试用AI创作助手写篇文章吧