C++类开发第七篇(详细说说多态和编译原理)

这篇具有很好参考价值的文章主要介绍了C++类开发第七篇(详细说说多态和编译原理)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

polymorphism

静态联编和动态联编

多态性(polymorphism)提供接口与具体实现之间的另一层隔离,从而将”what”和”how”分离开来。多态性改善了代码的可读性和组织性,同时也使创建的程序具有可扩展性,项目不仅在最初创建时期可以扩展,而且当项目在需要有新的功能时也能扩展。

c++支持编译时多态(静态多态)和运行时多态(动态多态),运算符重载和函数重载就是编译时多态,而派生类和虚函数实现运行时多态。

静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)。如果函数的调用,在编译阶段就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就是说地址是早绑定的。而如果函数的调用地址不能编译不能在编译期间确定,而需要在运行时才能决定,这这就属于晚绑定(动态多态,运行时多态)。

将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。编译器必须查看函数参数以及函数名才能确定使用哪个函数。

指针和引用类型的兼容性以及向上类型转换

在C++里面动态联编与通过指针和引用的调用方法有关。通常c++不允许将一种一类的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型。

double x = 2.5;
int * pi = &x; //类型不对不能这样定义
long & r1 = x; //问题同上

对象可以作为自己的类或者作为它的基类的对象来使用。还能通过基类的地址来操作它。取一个对象的地址(指针或引用),并将其作为基类的地址来处理,这种称为向上类型转换。

父类引用或指针可以指向子类对象,通过父类指针或引用来操作子类对象。

就是指将子类对象的引用赋给父类类型的引用变量的过程。在面向对象编程中,这种类型转换是安全的,因为子类对象可以被当做父类对象来对待。通过向上类型转换,可以实现多态性,即一个父类引用变量可以引用不同子类对象,并根据实际对象类型调用相应的方法。

class Base {
public :
	virtual void func() {
		cout << "class Base" << endl;
	}
};
class Derive_1 : public Base {
public:
	void func() {
		cout << "class Derive_1" << endl;
	}
};
class Derive_2 : public Base {
public:
	void func() {
		cout << "class Derive_2" << endl;
	}
};
void test() {
	Derive_1 d1;
	Derive_2 d2;
	//向上类型转换
	Base* b1 = &d1;
	Base* b2 = &d2;
//通过父类引用变量调用子类方法
	b1->func();
	b2->func();

}

C++类开发第七篇(详细说说多态和编译原理)

虽然父类调用func函数,但是父类的指针全部指向了子类的引用,并且可以完成隐式类型转换。再如下面这个代码

class Base {
public :
	void func() {
		cout << "class Base" << endl;
	}
};
class Derive_1 : public Base {
public:
	void func() {
		cout << "class Derive_1" << endl;
	}
};

void GetQuestion(Base& b) {
	b.func();
}

void test() {
	Derive_1 d1;
	
	GetQuestion(d1);


}

C++类开发第七篇(详细说说多态和编译原理)

参数定的是基类的引用,但是传参传的是子类,最后调用的依然是基类的方法。这个地方就引出了一个叫做捆绑的概念。把函数体与函数调用相联系称为绑定(捆绑,binding)

当绑定在程序运行之前(由编译器和连接器)完成时,称为早绑定(early binding).C语言中只有一种函数调用方式,就是早绑定。上面的问题就是由于早绑定引起的,因为编译器在只有Base地址时并不知道要调用的正确函数。编译是根据指向对象的指针或引用的类型来选择函数调用。

在代码里,GetQuestion函数的参数类型是Base&,编译器确定了应该调用的func函数是Base::func(),并不是传入的d1.解决方法就是迟绑定(迟捆绑,动态绑定,运行时绑定,late binding),意味着绑定要根据对象的实际类型,发生在运行。C++语言要实现这种动态绑定,必须有某种机制来确定运行时对象的类型并调用合适的成员函数。对于一种编译语言,编译器并不知道实际的对象类型(编译器并不知道Animal类型的指针或引用指向的实际的对象类型)。

虚函数

其实在上面的代码里也能发现区别就是基类的函数是不是虚函数决定了这个绑定发生在什么时候。如果没有定义虚的,b.func();将根据引用类型调用函数,编译时已知类型之后,对于非虚方法就是用的是静态联编。

  1. 为创建一个需要动态绑定的虚成员函数,可以简单在这个函数声明前面加上virtual关键字,定义时候不需要.

  2. 如果一个函数在基类中被声明为virtual,那么在所有派生类中它都是virtual的.

  3. 在派生类中virtual函数的重定义称为重写(override).

  4. Virtual关键字只能修饰成员函数.

  5. 构造函数不能为虚函数

仅需要在基类中声明一个函数为virtual.调用所有匹配基类声明行为的派生类函数都将使用虚机制。虽然可以在派生类声明前使用关键字virtual(这也是无害的),但这个样会使得程序显得冗余和杂乱。

C++类开发第七篇(详细说说多态和编译原理)

用《C++Primers》里面的一个图解释一下虚函数的工作原理。通常编译器处理虚函数的方法是:给每个对象添加一个隐藏成员,这个隐藏成员中保存了一个指向函数地址数组的指针(图里的vptr),而这种地址数组就叫做虚函数表(vtbl)。虚函数表里存储了为类对象进行声明的虚函数的地址。比如基类对象Base包含一个指针,该指针指向基类中所有虚函数的地址表,派生类对象将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新的地址;相反,该虚函数表将保存原始版本的地址。

调用虚函数时,程序将查看存储在对象里的vtbl地址,然后转向相应的函数地址表,如果使用类声明中的第一个虚函数,则程序将使用数组中的第一个函数地址,并执行具有该地址的函数。总之使用虚函数时,在内存和执行速度方面有一定成本:

  1. 每个对象都会被增大其存储空间(和虚基类一样)
  2. 每个类编译器都会有一个虚函数地址表
  3. 每个函数的调用都需要执行一项额外的操作就是到表里查找。

实现动态绑定细节过程

当子类无重写基类虚函数时:

C++类开发第七篇(详细说说多态和编译原理)

C++类开发第七篇(详细说说多态和编译原理)

子类完全继承基类的函数,他们拥有各自的虚函数表C++类开发第七篇(详细说说多态和编译原理)

当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,此时由于子类并没有重写也就是覆盖基类的func1函数,所以调用func1时,仍然调用的是基类的func1.

当子类重写基类虚函数时

C++类开发第七篇(详细说说多态和编译原理)

子类重写了基类的func1,但是没有写func2,所以对应的地址表应该是C++类开发第七篇(详细说说多态和编译原理)

当程序执行到这里,会去animal指向的空间中寻找vptr指针,通过vptr指针找到func1函数,由于子类重写基类的func1函数,所以调用func1时,调用的是子类的func1.

抽象基类和纯虚函数

在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际的创建一个基类的对象。同时创建一个纯虚函数允许接口中放置成员原函数,而不一定要提供一段可能对这个函数毫无意义的代码。

做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),使得基类称为抽象类(abstract class).

  1. 纯虚函数使用关键字virtual,并在其后面加上=0。如果试图去实例化一个抽象类,编译器则会阻止这种操作。

  2. 当继承一个抽象类的时候,必须实现所有的纯虚函数,否则由抽象类派生的类也是一个抽象类。

  3. Virtual void fun() = 0;告诉编译器在vtable中为函数保留一个位置,但在这个特定位置不放地址。

建立公共接口目的是为了将子类公共的操作抽象出来,可以通过一个公共接口来操纵一组类,且这个公共接口不需要事先(或者不需要完全实现)。可以创建一个公共类.

class AbstractClass {
public:
	virtual void sleep() = 0;
	virtual void dolove() = 0;
	virtual void cook() = 0;
	void func() {
		cook();
		dolove();
		sleep();
	}

};
class Regina : public AbstractClass {
public:
	virtual void sleep() {
		cout << "Regina::sleep()" << endl;
	}
	virtual void dolove() {
		cout << "Regina::dolove()" << endl;
	}
	virtual void cook() {
		cout << "Regina::cook()" << endl;
	}
	
};
class Ivanlee : public AbstractClass {
public:
	virtual void sleep() {
		cout << "Ivanlee::sleep()" << endl;
	}
	virtual void dolove() {
		cout << "Ivanlee::dolove()" << endl;
	}
	virtual void cook() {
		cout << "Ivanlee::cook()" << endl;
	}
};
void home(AbstractClass* a) {
	a->func();
	delete a;
}
void home(AbstractClass& a) {
	a.func();
}
void test() {
	home(new Regina);
	Ivanlee ivan;
	home(ivan);
}

C++类开发第七篇(详细说说多态和编译原理)

纯虚函数和虚函数是 C++ 中的重要概念,它们都与多态性(polymorphism)和继承相关。它们之间的主要区别在于以下几点:

  1. 虚函数:
    • 虚函数是在基类中声明为虚函数的成员函数,它可以在派生类中被重写(覆盖)。
    • 虚函数可以有默认的实现,如果派生类没有重写虚函数,则会调用基类的实现。
    • 虚函数通过基类指针或引用调用时,可以根据指针或引用所指向的对象的实际类型来动态地决定调用哪个版本的函数(动态联编)。
  2. 纯虚函数:
    • 纯虚函数是在基类中声明并且没有给出实现的虚函数,它只是一个接口,要求任何派生类都必须提供实现。
    • 在 C++ 中,通过在虚函数声明后面加上 = 0 来将其声明为纯虚函数,例如:virtual void myFunction() = 0;
    • 含有纯虚函数的类称为抽象类,无法实例化对象,只能作为基类来派生出其他类。派生类必须提供纯虚函数的实现,否则它们也会变成抽象类。

虚析构函数

虚析构函数是为了解决基类的指针指向派生类对象,并用基类的指针删除派生类对象。当通过基类指针删除指向派生类对象的实例时,如果析构函数不是虚函数,那么只会调用基类的析构函数,而不会调用派生类的析构函数,这可能导致派生类资源得不到正确释放,从而产生内存泄漏或未定义的行为。

class Base {
public:
    virtual ~Base() {
        // 虚析构函数
    }
};

class Derived : public Base {
public:
    ~Derived() {
        // 派生类的析构函数
    }
};

int main() {
    Base* ptr = new Derived();
    delete ptr;  // 通过基类指针删除派生类对象
    return 0;
}

重写 重载 重定义

class Shape {
public:
    virtual double calculateArea() {
        return 0.0;
    }
};
  1. 重写(Override):

    • 重写是指派生类重新定义(覆盖)基类中已经存在的虚函数的行为。

    • 当派生类定义一个与基类中的虚函数具有相同名称和签名的函数时,它就会覆盖(重写)基类中的虚函数。

    • 通过使用重写,可以在派生类中改变虚函数的行为,实现多态性,即在运行时根据对象的实际类型来确定调用哪个版本的函数。

      class Rectangle : public Shape {
      public:
          double calculateArea() override {
              // 重写基类的虚函数
              return width * height;
          }
      private:
          double width, height;
      };
      
  2. 重载(Overload):

    • 重载是指在同一个作用域内允许存在多个同名函数,但它们的参数列表不同(参数类型、参数个数或参数顺序不同)。

    • 重载函数可以具有相同的名称,但是由于参数列表不同,编译器可以根据调用时提供的参数类型来确定应该调用哪个版本的函数。

      class Shape {
      public:
          virtual double calculateArea() {
              return 0.0;
          }
      
          double calculateArea(int a, int b) {
              // 重载的函数
              return a * b;
          }
      };
      
  3. 重新定义(Redefine):

    • 重新定义通常用于描述对于非虚函数的重新定义。在基类和派生类中,如果存在同名但参数列表不同的函数,这种情况称为函数的重新定义。

    • 在重新定义中,基类和派生类中的函数并不构成多态性,调用哪个版本的函数取决于编译器能够静态确定的最匹配的函数。文章来源地址https://www.toymoban.com/news/detail-837612.html

      class Circle : public Shape {
      public:
          void draw(int radius) {
              // 派生类中重新定义的函数
              cout << "Drawing a circle with radius " << radius << endl;
          }
      };
      

到了这里,关于C++类开发第七篇(详细说说多态和编译原理)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【C++】多态原理剖析,Visual Studio开发人员工具使用查看类结构cl /d1 reportSingleClassLayout

    author:Carlton tag:C++ topic:【C++】多态原理剖析,Visual Studio开发人员工具使用查看类结构cl /d1 reportSingleClassLayout website:黑马程序员C++ tool:Visual Studio 2019 date:2023年7月24日   目录 父类使用虚函数前后类内部结构变化 子类重写父类虚函数的作用及其机理         首先父类成员

    2024年02月15日
    浏览(74)
  • Vue基础第七篇

    在Vue中实现集中式状态(数据)管理的一个Vue插件,对vue应用中多个组件的共享状态进行集中式的管理(读/写),也是一种组件间通信的方式,且适用于任意组件间通信。 多个组件需要共享数据时  3.搭建vuex环境 创建文件: src/store/index.js 在 main.js 中创建vm时传入 store 配置

    2024年02月08日
    浏览(101)
  • OpenCV第七篇:车牌识别

    目录 1.调整图片大小,并获取灰度图  2.双边滤波去除噪音:cv2.bilateralFilter()。 3.边缘检测:cv2.Canny(image,threshold1,threshold2) 4.寻找轮廓:车牌(四边形) ​编辑 5.图像位运算进行遮罩 6.图像剪裁 7.字符识别:OCR 1.调整图片大小,并获取灰度图  2.双边滤波去除噪音:cv

    2024年02月06日
    浏览(51)
  • 网络基础(第七篇)静态路由配置

    三台PC机、三台路由器 配置静态路由: 第一步,Ensp上写好PC机的IP地址和子网掩码、每一个接口的网关和网段,如图(绿色):  第二步,配置三台PC机的IP地址、子网掩码、网关。      第三步,配置所有接口的网关: 第四步,完成所有接口配置后,开始配静态路由,这里

    2024年02月10日
    浏览(45)
  • Java学习手册——第七篇基础语法

    本篇为大家快速入门Java基础语法,了解一个语言的基础语法是必要的, 因为我们后期都是需要用这些基础语法汇聚成我们想要的功能和想法。 这些都是必知必会的,但是不需要十分掌握,需要用到时可知道哪里查询, 用多了就熟练了。 注释有:文档注释、多行注释、当行

    2024年02月20日
    浏览(44)
  • 容器(第七篇)docker-consul

    consul服务器: 1. 建立 Consul 服务 mkdir /opt/consul cp consul_0.9.2_linux_amd64.zip /opt/consul cd /opt/consul unzip consul_0.9.2_linux_amd64.zip mv consul /usr/local/bin/ //设置代理,在后台启动 consul 服务端 consul agent -server -bootstrap -ui -data-dir=/var/lib/consul-data -bind=192.168.80.15 -client=0.0.0.0 -node=consul

    2024年02月08日
    浏览(60)
  • 第七篇——Apache Kafka的设计与实现

    作者:禅与计算机程序设计艺术 Apache Kafka是Apache软件基金会推出的一个开源分布式流处理平台,它最初由LinkedIn开发并于2011年9月正式发布,目前已成为 Apache 项目之一,是一个基于发布-订阅模式的分布式、高吞吐量、可容错、高可靠的消息系统,能够提供实时的消费和发送

    2024年02月08日
    浏览(36)
  • Qt文件系统源码分析—第七篇QFileSelector

    本文主要分析Windows平台,Mac、Linux暂不涉及 本文只分析到Win32 API/Windows Com组件/STL库函数层次,再下层代码不做探究 本文QT版本5.15.2 QTemporaryFile继承QFile QFile、QSaveFile继承QFileDevice QFileDevice继承QIODevice QIODevice、QFileSystemWatcher继承QObject QLockFile、QFileInfo、QDir、QFileSelector无任何继

    2024年02月07日
    浏览(45)
  • [C++] 多态(下) -- 多态原理 -- 动静态绑定

    上一篇文章我们了解了虚函数表,虚函数表指针,本篇文章我们来了解多态的底层原理,更好的理解多态的机制。 [C++] 多态(上) – 抽象类、虚函数、虚函数表 下面这段代码中,Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket,这就是多态调用,但是这里我

    2024年02月04日
    浏览(49)
  • 【FPGA入门】第七篇、FPGA实现VGA接口驱动

    目录 第一部分、实验结果  1、横的三色彩条效果 2、竖的三色彩条效果 第二部分、VGA驱动基本知识 1、VGA分辨率问题         2、VGA驱动波形 2.1、工业标准的时序波形图 2.2、比上面那张图更容易理解的图 2.3、每个区域对应的时间 2.4、不同分辨率的表格 3、VGA扫描范围问题

    2024年02月07日
    浏览(47)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包