初识C++之多态

多态性是将接口与实现进行分离;用形象的语言来解释就是实现以共同的方法,但因个体差异,而采用不同的策略。
1、什么是多态
  多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。它是面向对象程序设计(OOP)的一个重要特征。如果一个语言只支持类而不支持多态,只能说明它是基于对象的,而不是面向对象的。C++中的多态性具体体现在运行和编译两个方面。它可以简单地概括为“一个接口,多种方法”。
  那么多态的作用是什么呢,封装可以使得代码模块化,继承可以扩展已存在的代码,他们的目的都是为了代码重用。而多态的目的则是为了接口重用。也就是说,不论传递过来的究竟是那个类的对象,函数都能够通过同一个接口调用到适应各自对象的实现方法。

#define _CRT_SECURE_NO_WARNINGS 1 
#include <iostream>
using namespace std;
class Animal
{
public:virtual void Sleep() //这里的关键字virtual后面会讲到{cout << "Sleep()" << endl;}
public:char sex[5]; int age;
};class Person: public Animal
{ 
public:virtual void Sleep(){cout << "Person lying down to sleep!" << endl;}
public:char notionality[20]; //国籍char name[10];
};class Horse: public Animal
{
public:virtual void Sleep(){cout << "Horse sleep standing up!" << endl;}
public:char rase[20]; //种族
};int main()
{Person p;Horse h;Animal *pa1 = &p;Animal *pa2 = &h;pa1->Sleep();pa2->Sleep();return 0;
} 

输出:
这里写图片描述
可以看到,虽然pa1、pa1都是基类的指针,但是调用时却调用的是Person、Horse两个派生类的方法。这是因为pa1和pa2分别指向了Person、Horse两个派生类的对象。原来下来慢慢剖析。

2、多态的分类
①编译时多态:也叫静态多态,在编译时就可以确定对象使用的形式。 实现机制使通过函数重载(运算符重载实质也属于函数重载)。
②运行时多态:也叫动态多态,其具体引用的对象在运行时才能确定。 实现机制是通过虚函数。
  静态多态性与动态多态性的实质区别就是函数地址是早绑定还是晚绑定。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。
  

3、动态多态性存在的三个必要条件
①要有类的继承关系;
②要有虚函数(只有重写了虚函数的才能算作是体现了C++多态性);
注意函数重写、重载、重定义的区别,可以参考:
http://blog.csdn.net/ljx_5489464/article/details/51127081
③要有父类指针/引用指向子类对象(用于访问派生类中同名覆盖成员函数)。

4、多态的实现
  C++多态性是通过虚函数来实现的,通过指向派生类的基类指针或引用,访问派生类中同名覆盖成员函数。虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写(重写的话可以有两种,直接重写成员函数和重写虚函数,只有重写了虚函数的才能算作是体现了C++多态性)。
  虚函数:在某基类中声明为 virtual 并在一个或多个派生类中被重新定 义的成员函数,用法格式为:
  virtual 函数返回类型 函数名(参数表)
  {
   函数体;
  }
关于虚函数这里不做过剖析,可以参考:
http://blog.csdn.net/ljx_5489464/article/details/51138393
  前面说了,多态的动态性实现是靠虚函数,它的实质是在基类定义一个虚函数,在派生类中对其进行重写,然后定义基类的指针指向派生类对象,然后用该指针调用虚函数,此时调用的就是指针指向的对象对应的类里面的同名函数。那么这种机制在底层是怎样实现的呢?  
  想要弄清楚这个就必须要清楚一个东西–>虚表,全称为虚拟函数表。什么是虚表,虚表就是一张表,它里面存放虚函数的入口地址,在C++语言中,每个有虚函数的类或者基类有虚函数的派生类,编译器都会为它生成一个虚拟函数表(注意:虚表是从属于类的)。此外,编译器会为包含虚函数的类加上一个成员变量,是一个指向该虚函数表的指针(常被称为vfptr,注意:虚表指针是从属于对象的)。也就是说,如果一个类含有虚表,则该类的所有对象都会含有一个虚表指针,并且该虚表指针指向同一个虚表。
  虚表的内容是依据类中的虚函数声明次序–填入函数指针。派生类会继承基础类别的虚表(以及所有其他可以继承的成员),再在此虚表上添加上自己的虚函数指针,当我们在派生类中改写虚函数时,虚表就受了影响,改写后的的虚函数的地址会替换原虚函数的地址存放于虚表中。

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;class no_Virtual
{
public:void Fun(){cout << "no_Virtual::Fun()" << endl;}
};class Virtual
{
public:virtual void Fun()     //注意这儿加了关键字virtual{cout << "Virtual::Fun()" << endl;}
};int main()
{cout << sizeof(no_Virtual) << endl;cout << sizeof(Virtual) << endl;return 0;
}

这里写图片描述
①no_Virtual类没有任何数据成员,也没有虚函数,按说它的大小应该是0,但这儿它的大小是1。我理解的原因有以下两点:
  这个是因为no_Virtual既然是一个类型,那么就能用来定义变量,而定义变量肯定要分配空间,分配多大合适呢?0个?显然不可能,2个、4个?太浪费了,它什么都不用存储,分配那么大干什么,所以编译器就为我们选了个折中的办法,分配一个,既不会浪费空间,也能用来定义变量。
  既然no_Virtual是类型,用来定义变量的,那么要是它一个空间都不占的话,用它定义出来的变量就都从同一位置开始存储了,那我们访问那块空间时,怎么知道到底是访问哪一个变量呢?
②Virtual类没有任何数据成员,但是有虚函数,按我们上面的说法,它的大小是4,结果很给面子的正确了。
定义一个Virtual类的变量,看看是不是像上面说的那样,有一个从属于它的虚表指针,从监视窗口看,确实是的。
这里写图片描述

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;class Base
{
public:virtual void Fun1(){cout << "Base::Fun1()" << endl;}virtual void Fun2()     //注意这儿加了关键字virtual{cout << "Base::Fun2()" << endl;}
};class Derived: public Base
{
public:virtual void Fun3()     //注意这儿加了关键字virtual{cout << "Derived::Fun3()" << endl;}virtual void Fun4()     //注意这儿加了关键字virtual{cout << "Derived::Fun4()" << endl;}
};void PrintVirTable()
{Base b;Derived d;cout << "Base:" << endl;int *vfpTable = (int *)(*(int*)&b);while (*vfpTable != NULL){int *pFun = (int*)(*vfpTable);cout << pFun << endl;vfpTable++;}cout << "Derived:" << endl;vfpTable = (int *)(*(int*)&d);while (*vfpTable != NULL){int *pFun = (int*)(*vfpTable);cout << pFun << endl;vfpTable++;}
}int main()
{PrintVirTable();return 0;
}

输出结果:
这里写图片描述

因为VS的监视窗口好像有Bug:
这里写图片描述
像这儿,派生类的对象就只能看到继承与基类的虚表,而不能看到派生类自己的,所以上面我一个函数分别打印了基类与派生类的虚表,可以很直观的看到,首先,派生类的虚表是继承于基类的,然后在加上自己的虚函数的地址到虚表中去,就构成了自己的虚表。
现在来剖析一下打印虚表的函数是怎么实现的:
首先得清楚带有虚函数的类创建的对象,前四个字节里面存储的就是虚表指针的地址,如下图:
这里写图片描述

再回过头来看看虚表的打印函数:

void PrintVirTable()
{Base b;Derived d;cout << "Base:" << endl;int *vfpTable = (int *)(*(int*)&b);while (*vfpTable != NULL){int *pFun = (int*)(*vfpTable);cout << pFun << endl;vfpTable++;}cout << "Derived:" << endl;vfpTable = (int *)(*(int*)&d);while (*vfpTable != NULL){int *pFun = (int*)(*vfpTable);cout << pFun << endl;vfpTable++;}
}

它的原理其实是这样的:
这里写图片描述

再来看一个例子:

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
using namespace std;class Base
{
public:virtual void Fun1(){cout << "Base::Fun1()" << endl;}virtual void Fun2()     //注意这儿加了关键字virtual{cout << "Base::Fun2()" << endl;}
};class Derived: public Base
{
public:virtual void Fun3()     //注意这儿加了关键字virtual{cout << "Derived::Fun3()" << endl;}virtual void Fun4()     //注意这儿加了关键字virtual{cout << "Derived::Fun4()" << endl;}
public:int data;
};void PrintVirTable()
{Derived d1;Derived d2;cout << "d1::virTable" << endl;int *vfpTable = (int *)(*(int*)&d1);while (*vfpTable != NULL){int *pFun = (int*)(*vfpTable);cout << pFun << endl;vfpTable++;}cout << "d2::virTable" << endl;vfpTable = (int *)(*(int*)&d2);while (*vfpTable != NULL){int *pFun = (int*)(*vfpTable);cout << pFun << endl;vfpTable++;}
}int main()
{PrintVirTable();return 0;
}

输出结果:
这里写图片描述

这儿就证明了如果一个类有虚函数,那么该不同对象公用同一张虚表。因为函数的地址是在编译期间的确定的,一个函数的地址只会被存储一次,虽然累的对象不同,但它们调用该类的同名函数其实是调用的同一函数,因此它们的虚表会相同。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/526166.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

删除一个无头单链表的非尾节点(C语言)

void DelNotTailNode(PSListNode pos) {PSListNode pNode NULL;assert(pos);if (NULL pos->pNextNode){return;}else{DataType temp 0;//交换pos和pos->pNextNode的数据&#xff08;相当于交换了两个结点的位置&#xff09;&#xff0c;使问题转换为删除pos指向的结点…

浅析Linux开发工具之gcc/g++

在windows开发平台&#xff0c;我们用惯了vc、vs等IDE&#xff08;集成开发环境&#xff09;&#xff0c;在编译好源代码之后&#xff0c;按下相应按钮&#xff0c;IDE就会为我们完成编译&#xff0c;链接的过程。然而在Linux平台下&#xff0c;却没有这么方便的开发环境&#…

Linux权限的简单剖析

一、权限是什么 权限&#xff08;privilege&#xff09;是指某个特定的用户具有特定的系统资源使用权力。举个简单的例子&#xff0c;夏日炎炎&#xff0c;你看到路边有卖西瓜的&#xff0c;你想要吃西瓜&#xff0c;你就得买它&#xff0c;买它其实就是获取你对西瓜的使用权限…

Linux文件的三种时间属性

一、Linux文件时间属性的分类 我们在用windows系统时&#xff0c;在查看磁盘文件时&#xff0c;经常会看到文件或目录的后面有一个时间信息&#xff0c;这个是文件在磁盘上别创建的时间。其实&#xff0c;在windows系统中&#xff0c;文件还有文件的修改时间、访问时间两个时间…

浅析Linux开发工具之Makefile

一、什么是Makefile 在windows平台下&#xff0c;有很多的IDE供我们使用&#xff0c;我们不会去考虑怎么把一个很大的工程编译链接为一个可执行程序&#xff0c;因为这些事IDE都为我们做了&#xff0c;而在Linux平台下&#xff0c;我们并没有这么高端的IDE供我们使用&#xff…

嵌入式面试准备

题目都摘于网上 嵌入式系统中经常要用到无限循环&#xff0c;如何用C编写死循环 while(1){}或者for(;&#x1f609; 内存分区 代码区&#xff0c;全局区&#xff08;全局变量&#xff0c;静态变量&#xff0c;以及常量&#xff09;&#xff0c;栈区&#xff0c;堆区 const关键…

C语言extern与static修饰变量

extern和static在C语言里面的作用这里就不做过多的阐述了&#xff0c;下面直接通过一个小程序来看一看他们修饰的变量的特性。 #include <stdio.h>int count 3;int main() {int i 0, count 2, sum 0;for (i 0; i < count; i 2, count){static int count 4;cou…

函数值的交换

《函数值的交换》 交换函数的几种方式&#xff1a; (1) //error int Swap1(int a,int b) { int tmp; tmp a; a b; b tmp; return 0; } 在函数Swap1中&#xff0c;a和b的地址的值并没有交换。只是把10和20赋给了a和b&#xff0c;a和b原本的值并没有改变。 (2) #…

数组的下标越界

《数组下标越界》 数组定义的一般形式为&#xff1a;类型说明符 数组名[常量表达式] [常量表达式]这两个常量表达式分别指定了二维数组的行数和列数&#xff0c;程序编译时据此向内存申请空间。 引用二维数组的格式为&#xff1a;数组名[行下标] [列下标]&#xff0c;下标都是从…

指针的加减法计算

(1) 指针&#xff1a;&#xff08;p&#xff09;表示加一个单元格&#xff08;单元格的字节随类型而定&#xff09;#include<stdio.h>int main(){int arr[10]{1,2,3,4,5,6};int *p arr;p;printf("%d %d\n",arr[0],arr[1]);return 0;}(2) 指针加数字&#xff1…

字符串在指针和数组上赋值的区别

1 #include<stdio.h> 2 int main() 3 { 4 char *str1 "abcde";// 字符串常量 5 char str2[] "abcde";// 字符数组 6 str1[0] x;//error 7 str2[0] x; 8 return 0; 9 } 注&#xff1a;代码运行到第6行崩溃 *str1是一个指…

继承与多态(一)

目录 一、继承的概念&#xff1a; 二、公有继承 三、私有继承 四、保护继承 五、保护继承与保护成员的访问 一、继承的概念&#xff1a; 在C中可以用已有的类来定义新的类&#xff0c;新类将继承原有类的全部特性&#xff0c;原有类称为基类&#xff08;父类&#xff09;&…

对象的使用

目录 一、对象指针 二、对象引用 三、对象数组 四、动态对象 五、this指针 六、组合对象 一、对象指针 定义&#xff1a;占用一块连续的内存区域&#xff0c;由此可以使用一个指向对象的指针来访问对象。它指向存放该对象的地址。 优点&#xff1a; &#xff08;1&a…

友元

概念&#xff1a;在C中&#xff0c;类具有封装性和隐蔽性&#xff0c;只有类的函数成员才能访问类的私有成员&#xff0c;程序中的其他函数是无法访问类的私有成员&#xff0c;友元为类的封装隐藏开了一个小孔&#xff0c;外界可以访问类内部的一些属性。如果某个对象说明为某个…

常对象与常成员

一、常对象 概念&#xff1a;如果某个对象不允许修改&#xff0c;则该对象称为常对象。 PS&#xff1a; &#xff08;1&#xff09;不允许常对象调用任何类的成员函数&#xff0c;而且常对象一旦定义&#xff0c;在其生存期内不允许修改&#xff0c;否则导致编译错误。 &am…

指针在数组里的加法

在C语言中&#xff0c;指针与数组经常放在一起使用&#xff0c;很多情况下数组和指针可以互相转换。 //伪代码 int arr[10] {0,1,2,3,4,5,6,7,8,9};int * p arr;arr[i] * (pi);//i是大于0小于数组长度-1的任何正整数*(arr1) arr[i]; 数组和指针的不同&#xff1a; &…

单链表(带头节点)

带头结点单链表的内存分布情况 头文件 #pragma once //带头节点的单链表 //单链表尾节点的next为NULL //List为一条链表&#xff1b;Node* 一个节点的地址 typedef struct Node {int data;//数据Node *next;//下一个节点的地址 }Node ,*List ;//List Node *//初始化 void Ini…

双向链表(带头结点)

带头结点双向链表的内存分布情况 头文件 #pragma once //双向链表 typedef struct DNode {int data;DNode* next;DNode* prio; }DNode , *DList ;//初始化 void InitList(DList plist);//头插法 bool Insert_head(DList plist,int val);//尾插法 bool Insert_tail(DList plist…

给出一个数:(1)求解有几位数,(2)分别输出每一位数字(3)按逆序输出各位数字

目录 &#xff08;1&#xff09;求解有几位数 (2)分别输出每一位数字 (3)按逆序输出各位数字 &#xff08;1&#xff09;求解有几位数 #include<stdio.h>unsigned int GetFigures(int n) {unsigned int figures 0; while(n!0){n / 10;figures;}return figures; }int …

c++两个数组对比去掉重复的元素_LeetCode 题解 | 167.两数之和 II 输入有序数组...

点击上方蓝字设为星标下面开始今天的学习&#xff5e;力扣 167.两数之和 II - 输入有序数组(点击文末阅读原文查看题目)题目描述给定一个已按照 升序排列 的有序数组&#xff0c;找到两个数使得它们相加之和等于目标数。函数应该返回这两个下标值 index1 和 index2&#xff0c;…