C++之继承与多态
在程序设计领域,一个广泛认可的定义是“一种将不同的特殊行为和单个泛化记号相关联的能力”。和纯粹的面向对象程序设计语言不同,C++中的多态有着更广泛的含义。除了常见的通过类继承和虚函数机制生效于运行期的动态多态(dynamic polymorphism)外,带变量的宏,模板,函数重载,运算符重载,拷贝构造等也允许将不同的特殊行为和单个泛化记号相关联,由于这种关联处理于编译期而非运行期,因此被称为静态多态(static polymorphism)。
静态多态性
1、 函数重载与缺省参数
(1)函数重载的实现原理
假设,我们现在想要写一个函数(如Exp01),它即可以计算整型数据又可以计算浮点数,那样我们就得写两个求和函数,对于更复杂的情况,我们可能需要写更多的函数,但是这个函数名该怎么起呢?它们本身实现的功能都差不多,只是针对不同的参数:
int sum_int(int nNum1, int nNum2)
{
return nNum1 + nNum2;
}
double sum_float(float nNum1, float nNum2)
{
return nNum1 + nNum2;
}
C++中为了简化,就引入了函数重载的概念,大致要求如下:
1、 重载的函数必须有相同的函数名
2、 重载的函数不可以拥有相同的参数
2、 运算符重载
运算符重载也是C++多态性的基本体现,在我们日常的编码过程中,我们经常进行+、—、*、/等操作。在C++中,要想让我们定义的类对象也支持这些操作,以简化我们的代码。这就用到了运算符重载。
比如,我们要让一个日期对象减去另一个日期对象以便得到他们之间的时间差。再如:我们要让一个字符串通过“+”来连接另一个字符串……
要想实现运算符重载,我们一般用到operator关键字,具体用法如下:
返回值 operator 运算符(参数列表)
{
// code
}
例如:
CMyString Operator +(CMyString & csStr)
{
int nTmpLen = strlen(msString.GetData());
if (m_nSpace <= m_nLen+nTmpLen)
{
char *tmpp = new char[m_nLen+nTmpLen+sizeof(char)*2];
strcpy(tmpp, m_szBuffer);
strcat(tmpp, msString.GetData());
delete[] m_szBuffer;
m_szBuffer = tmpp;
}
}
这样,我们的函数就可以写成:
int sum (int nNum1, int nNum2)
{
return nNum1 + nNum2;
}
double sum (float nNum1, float nNum2)
{
return nNum1 + nNum2;
}
到现在,我们可以考虑一下,它们既然拥有相同的函数名,那他们怎么区分各个函数的呢?
那就是通过C++名字改编(C++名字粉碎),,对于重载的多个函数来说,其函数名都是一样的,为了加以区分,在编译连接时,C++会按照自己的规则篡改函数名字,这一过程为"名字改编".有的书中也称为"名字粉碎".不同的C++编译器会采用不同的规则进行名字改编,例如以上的重载函数在VC6.0下可能会被重命sum_int@@YAHHH@Z和sum_float@@YAMMM@Z这样方便连接器在链接时正常的识别和找到正确的函数。
(2)缺省参数
无论是Win系统下的API,还是Linux下的很多系统库,它们的好多的函数存在许多参数,而且大部分都是NULL,倘若我们有个函数大部分的时候,某个参数都是固定值,仅有的时候需要改变一下,而我们每次调用它时都要很费劲的输入参数岂不是很痛苦?C++提供了一个给参数加默认参数的功能,例如:
double sum (float nNum1, float nNum2 = 10);
我们调用时,默认情况下,我们只需要给它第一个参数传递参数即可,但是使用这个功能时需要注意一些事项,以免出现莫名其妙的错误,下面我简单的列举一下大家了解就好。
A、 默认参数只要写在函数声明中即可。
B、 默认参数应尽量靠近函数参数列表的最右边,以防止二义性。比如
double sum (float nNum2 = 10,float nNum1);
这样的函数声明,我们调用时:sum(15);程序就有可能无法匹配正确的函数而出现编译错误。
3.宏多态
带变量的宏可以实现一种初级形式的静态多态:
// macro_poly.cpp
#include <iostream>
#include <string>
// 定义泛化记号:宏ADD
#define ADD(A, B) (A) + (B);
int main()
{
int i1(1), i2(2);
std::string s1("Hello, "), s2("world!");
int i = ADD(i1, i2); // 两个整数相加
std::string s = ADD(s1, s2); // 两个字符串“相加”
std::cout << "i = " << i << "\n";
std::cout << "s = " << s << "\n";
}
当程序被编译时,表达式ADD(i1, i2)和ADD(s1, s2)分别被替换为两个整数相加和两个字符串相加的具体表达式。整数相加体现为求和,而字符串相加则体现为连接(注:string.h库已经重载了“+”)。程序的输出结果符合直觉:
1 + 2 = 3
Hello, + world! = Hello, world!
4.类中的早期绑定
先看以下的代码:
#include<iostream>
using namespace std;
class animal
{
public:
void sleep(){
cout<<"animal sleep"<<endl; }
void breathe(){
cout<<"animal breathe"<<endl;
}
};
class fish:public animal
{
public:
void breathe(){
cout<<"fish bubble"<<endl;
}
}; int main()
{
fish fh;
animal *pAnimal=&fh;
pAnimal->breathe();
}
答案是输出:animal breathe
从编译的角度
C++编译器在编译的时候,要确定每个对象调用的函数的地址,这称为早期绑定(early binding),当我们将fish类的对象fh的地址赋给pAn时,C++编译器进行了类型转换,此时C++编译器认为变量pAn保存的就是animal对象的地址。当在main()函数中执行pAn->breathe()时,调用的当然就是animal对象的breathe函数。
内存模型的角度
对于简单的继承关系,其子类内存布局,是先有基类数据成员,然后再是子类的数据成员,当然后面讲的复杂情况,本规律不一定成立。
我们构造fish类的对象时,首先要调用animal类的构造函数去构造animal类的对象,然后才调用fish类的构造函数完成自身部分的构造,从而拼接出一个完整的fish对象。当我们将fish类的对象转换为animal类型时,该对象就被认为是原对象整个内存模型的上半部分,也就是图中的“animal的对象所占内存”。那么当我们利用类型转换后的对象指针去调用它的方法时,当然也就是调用它所在的内存中的方法。因此,输出animal breathe,也就顺理成章了。
前面输出的结果是因为编译器在编译的时候,就已经确定了对象调用的函数的地址,要解决这个问题就要使用迟绑定(late binding)技术。当编译器使用迟绑定时,就会在运行时再去确定对象的类型以及正确的调用函数。而要让编译器采用迟绑定,就要在基类中声明函数时使用virtual关键字(注意,这是必须的,很多学员就是因为没有使用虚函数而写出很多错误的例子),这样的函数我们称为虚函数。一旦某个函数在基类中声明为virtual,那么在所有的派生类中该函数都是virtual,而不需要再显式地声明为virtual。
动态多态性
下面我们将上面一段代码进行部分修改
#include<iostream>
using namespace std;
class animal
{
public:
void sleep(){
cout<<"animal sleep"<<endl; }
virtual void breathe(){
cout<<"animal breathe"<<endl;
}
};
class fish:public animal
{
public:
void breathe(){
cout<<"fish bubble"<<endl;
}
}; int main()
{
fish fh;
animal *pAnimal=&fh;
pAnimal->breathe();
}
运行结果:fish bubble
编译器为每个类的对象提供一个虚表指针,这个指针指向对象所属类的虚表。在程序运行时,根据对象的类型去初始化vptr,从而让vptr正确的指向所属类的虚表,从而在调用虚函数时,就能够找到正确的函数。由于pAn实际指向的对象类型是fish,因此vptr指向的fish类的vtable,当调用pAn->breathe()时,根据虚表中的函数地址找到的就是fish类的breathe()函数。正是由于每个对象调用的虚函数都是通过虚表指针来索引的,也就决定了虚表指针的正确初始化是非常重要的。换句话说,在虚表指针没有正确初始化之前,我们不能够去调用虚函数。那么虚表指针在什么时候,或者说在什么地方初始化呢?
答案是在构造函数中进行虚表的创建和虚表指针的初始化。还记得构造函数的调用顺序吗,在构造子类对象时,要先调用父类的构造函数,此时编译器只“看到了”父类,并不知道后面是否后还有继承者,它初始化父类对象的虚表指针,该虚表指针指向父类的虚表。当执行子类的构造函数时,子类对象的虚表指针被初始化,指向自身的虚表。
当fish类的fh对象构造完毕后,其内部的虚表指针也就被初始化为指向fish类的虚表。在类型转换后,调用pAn->breathe(),由于pAn实际指向的是fish类的对象,该对象内部的虚表指针指向的是fish类的虚表,因此最终调用的是fish类的breathe()函数。
下面详细的介绍内存的分布
基类的内存分布情况
对于无虚函数的类A:
class A
{
void g(){.....}
};
则sizeof(A)=1;
如果改为如下:
class A
{
public:
virtual void f()
{
......
}
void g(){.....}
}
则sizeof(A)=4! 这是因为在类A中存在virtual function,为了实现多态,每个含有virtual function的类中都隐式包含着一个静态虚指针vfptr指向该类的静态虚表vtable, vtable中的表项指向类中的每个virtual function的入口地址
例如 我们declare 一个A类型的object :
A c;
A d;
则编译后其内存分布如下:
从 vfptr所指向的vtable可以看出,每个virtual function都占有一个entry,例如本例中的f函数。而g函数因为不是virtual类型,故不在vtable的表项之内。说明:vtab属于类成员静态pointer,而vfptr属于对象pointer
继承类的内存分布状况
假设代码如下:
public B:public A
{
public :
int f() //override virtual function
{
return 3;
}
};
则
A c;
A d;
B e;
编译后,其内存分布如下:
从中我们可以看出,B类型的对象e有一个vfptr指向vtable address:0x00400030 ,而A类型的对象c和d共同指向类的vtable address:0x00400050a
动态绑定过程的实现
我们说多态是在程序进行动态绑定得以实现的,而不是编译时就确定对象的调用方法的静态绑定。
其过程如下:
程序运行到动态绑定时,通过基类的指针所指向的对象类型,通过vfptr找到其所指向的vtable,然后调用其相应的方法,即可实现多态。
例如:
A c;
B e;
A *pc=&e; //设置breakpoint,运行到此处
pc=&c;
此时内存中各指针状况如下:
可以看出,此时pc指向类B的虚表地址,从而调用对象e的方法。继续运行,当运行至pc=&c时候,此时pc的vptr值为0x00420050,即指向类A的vtable地址,从而调用c的方法。
对于虚函数调用来说,每一个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表。所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以呢,才能实现动态的对象函数调用,这就是C++多态性实现的原理。
需要注意的几点
总结(基类有虚函数):
1、每一个类都有虚表。
2、虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现。如果基类3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
3、派生类的虚表中虚函数地址的排列顺序和基类的虚表中虚函数地址排列顺序相同。
下面想将虚函数和纯虚函数做个比较
虚函数
引入原因:为了方便使用多态特性,我们常常需要在基类中定义虚函数。
纯虚函数
引入原因:为了实现多态性,纯虚函数有点像java中的接口,自己不去实现过程,让继承他的子类去实现。
在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。 这时我们就将动物类定义成抽象类,也就是包含纯虚函数的类
纯虚函数就是基类只定义了函数体,没有实现过程定义方法如下
virtual void Eat() = 0; 直接=0 不要 在cpp中定义就可以了
虚函数和纯虚函数的区别
1虚函数中的函数是实现的哪怕是空实现,它的作用是这个函数在子类里面可以被重载,运行时动态绑定实现动态
纯虚函数是个接口,是个函数声明,在基类中不实现,要等到子类中去实现
2 虚函数在子类里可以不重载,但是虚函数必须在子类里去实现。
类的多继承
一个类可以从多个基类中派生,也就是说:一个类可以同时拥有多个类的特性,是的,他有多个基类。这样的继承结构叫作“多继承”,最典型的例子就是沙发-床了:
SleepSofa类继承自Bed和Sofa两个类,因此,SleepSofa类拥有这两个类的特性,但在实际编码中会存在如下几个问题。
a) SleepSofa类该如何定义?
Class SleepSofa : public Bed, public Sofa
{
….
}
构造顺序为:Bed sofa sleepsofa (也就是书写的顺序)
b) Bed和Sofa类中都有Weight属性页都有GetWeight和SetWeight方法,在SleepSofa类中使用这些属性和方法时,如何确定调用的是哪个类的成员?
可以使用完全限定名的方式,比如:
Sleepsofa objsofa;
Objsofa.Bed::SetWeight(); // 给方法加上一个作用域,问题就解决了。
虚继承
倘若,我们定义一个SleepSofa对象,让我们分析一下它的构造过程:它会构造Bed类和Sofa类,但Bed类和Sofa类都有一个父类,因此Furniture类被构造了两次,这是不合理的,因此,我们引入了虚继承的概念。
class Furniture{……};
class Bed : virtual public Furniture{……}; // 这里我们使用虚继承
class Sofa : virtual public Furniture{……};// 这里我们使用虚继承
class sleepSofa : public Bed, public Sofa {……};
这样,Furniture类就之构造一次了……
总结下继承情况中子类对象的内存结构:
单继承情况下子类实例的内存结构
(1)一般继承(无虚函数覆盖)
假设有如下所示的一个继承关系:
(2)一般继承(有虚函数覆盖)
在这个类的设计中,假设只覆盖了父类的一个函数:f()。那么,对于派生类的实例,其虚函数表会是下面的一个样子
class A
{
public:
A(){m_A = 0;}
virtual fun1(){};
int m_A;
};
class B:public A
{
public:
B(){m_B = 1;}
virtual fun1(){};
virtual fun2(){};
int m_B;
};
int main(int argc, char* argv[])
{
B* pB = new B;
return 0;
}
则在VC6.0下的内存分配图:
在该图中,子类只有一个虚函数表,与以上的两种情况向符合。
多继承情况下子类实例的内存结构(非虚继承)
(1)多重继承(无虚函数覆盖)
假设有下面这样一个类的继承关系。注意:子类并没有覆盖父类的函数:
对于子类实例中的虚函数表,是下面这个样子:
(2)多重继承(有虚函数覆盖)
下图中,我们在子类中覆盖了父类的f()函数。
下面是对于子类实例中的虚函数表的图:
在多继承(非虚继承)情况下,对应于以下例程序:
#include <stdio.h>
class A
{
public:
A(){m_A = 1;};
~A(){};
virtual int funA(){printf("in funA\r\n"); return 0;};
int m_A;
};
class B
{
public:
B(){m_B = 2;};
~B(){};
virtual int funB(){printf("in funB\r\n"); return 0;};
int m_B;
};
class C
{
public:
C(){m_C = 3;};
~C(){};
virtual int funC(){printf("in funC\r\n"); return 0;};
int m_C;
};
class D:public A,public B,public C
{
public:
D(){m_D = 4;};
~D(){};
virtual int funD(){printf("in funD\r\n"); return 0;};
int m_D;
};
从该图中可以看出,此时子类中确实有三个来自于父类的虚表。
多继承情况下子类实例的内存结构(存在虚继承)
在虚继承下,Der通过共享虚基类SuperBase来避免二义性,在Base1,Base2中分别保存虚基类指针,Der继承Base1,Base2,包含Base1, Base2的虚基类指针,并指向同一块内存区,这样Der便可以间接存取虚基类的成员,如下图所示:
class SuperBase
{
public:
int m_nValue;
void Fun(){cout<<"SuperBase1"<<endl;}
virtual ~SuperBase(){}
};
class Base1: virtual public SuperBase
{
public:
virtual ~ Base1(){}
};
class Base2: virtual public SuperBase
{
public:
virtual ~ Base2(){}
};
class Der:public Base1, public Base2
{
public:
virtual ~ Der(){}
};
void main()
{
cout<<sizeof(SuperBase)<<sizeof(Base1)<<sizeof(Base2)<<sizeof(Der)<<endl;
}
1) GCC中结果为8, 12, 12, 16
解析:sizeof(SuperBase) = sizeof(int) + 虚函数表指针
sizeof(Base1) = sizeof(Base2) = sizeof(int) + 虚函数指针 + 虚基类指针
sizeof(Der) = sizeof(int) + Base1中虚基类指针 + Base2虚基类指针 + 虚函数指针
GCC共享虚函数表指针,也就是说父类如果已经有虚函数表指针,那么子类中共享父类的虚函数表指针空间,不在占用额外的空间,这一点与VC不同,VC在虚继承情况下,不共享父类虚函数表指针,详见如下。
2)VC中结果为:8, 16, 16, 24
解析:sizeof(SuperBase) = sizeof(int) + 虚函数表指针
sizeof(Base1) = sizeof(Base2) = sizeof(int) + SuperBase虚函数指针 + 虚基类指针 + 自身虚函数指针
sizeof(Der) = sizeof(int) + Base1中虚基类指针 + Base2中虚基类指针 + Base1虚函数指针 + Base2虚函数指针 + 自身虚函数指针
如果去掉虚继承,结果将和GCC结果一样,A,B,C都是8,D为16,原因就是VC的编译器对于非虚继承,父类和子类是共享虚函数表指针的。
(1) 部分虚继承的情况下子类实例的内存结构:
#include "stdafx.h"
class A
{
public:
A(){m_A = 0;};
virtual funA(){};
int m_A;
};
class B
{
public:
B(){m_B = 1;};
virtual funB(){};
int m_B;
};
class C
{
public:
C(){m_C = 2;};
virtual funC(){};
int m_C;
};
class D:virtual public A,public B,public C
{
public:
D(){m_D = 3;};
virtual funD(){};
int m_D;
};
int main(int argc, char* argv[])
{
D* pD = new D;
return 0;
}
(2)全部虚继承的情况下,子类实例的内存结构
class A
{
public:
A(){m_A = 0;}
virtual funA(){};
int m_A;
};
class B
{
public:
B(){m_B = 1;}
virtual funB(){};
int m_B;
};
class C:virtual public A,virtual public B
{
public:
C(){m_C = 2;}
virtual funC(){};
int m_C;
};
int main(int argc, char* argv[])
{
C* pC = new C;
return 0;
}
(3) 菱形结构继承关系下子类实例的内存结构
class A
{
public:
A(){m_A = 0;}
virtual funA(){};
int m_A;
};
class B :virtual public A
{
public:
B(){m_B = 1;}
virtual funB(){};
int m_B;
};
class C :virtual public A
{
public:
C(){m_C = 2;}
virtual funC(){};
int m_C;
};
class D: public B, public C
{
public:
D(){m_D = 3;}
virtual funD(){};
int m_D;
};
int main(int argc, char* argv[])
{
D* pD = new D;
return 0;
}
对于子类虚表的个数和设置,貌似虚继承与非虚继承的差别不是很大。
参考:
http://blog.csdn.net/chen_yi_long/article/details/8662822
http://blog.csdn.net/zyq0335/article/details/7657465
http://haoel.blog.51cto.com/313033/124595/
http://blog.csdn.net/xsh_123321/article/details/5956289
C++之继承与多态的更多相关文章
- Objective-C中的继承和多态
面向对象编程之所以成为主流的编程思想和他的继承和多态是分不开的,只要是面向对象语言都支持继承和多态,当然不同的OOP语言之间都有其特点.OC中和Java类似,不支持多重继承,但OOP语言C++就支持多 ...
- java中抽象、分装、继承和多态的理解
1.抽象.封装装.继承和多态是java面向对象编程的几大特点. 抽象:所谓抽象就是对某件事务,我们忽略我们不关心不需要的部分,提取我们想要的属性和行为,并且以代码的形式提现出来:例如我们需要对一个学生 ...
- [转] JS中简单的继承与多态
这里讲了一个最最最简单的JS中基于原型链的继承和多态. 先看一下以下这段代码的实现(A是“父类”,B是“子类”): var A = function(){ this.value = 'a'; this ...
- 网络电视精灵~分析~~~~~~简单工厂模式,继承和多态,解析XML文档,视频项目
小总结: 所用技术: 01.C/S架构,数据存储在XML文件中 02.简单工厂模式 03.继承和多态 04.解析XML文档技术 05.深入剖析内存中数据的走向 06.TreeView控件的使用 核心: ...
- OC的封装、继承与多态
面向对象有三大特征:封装.继承和多态. 一.封装 封装是将对象的状态信息隐藏在对象内部,不允许外部程序直接访问对象内部信息,而是通过该类所提供的方法来实现对内部信息的操作和访问.简而言之,信息隐藏,隐 ...
- 2、C#面向对象:封装、继承、多态、String、集合、文件(上)
面向对象封装 一.面向对象概念 面向过程:面向的是完成一件事情的过程,强调的是完成这件事情的动作. 面向对象:找个对象帮你完成这件事情. 二.面向对象封装 把方法进行封装,隐藏实现细节,外部直接调用. ...
- Java学习笔记 07 接口、继承与多态
一.类的继承 继承的好处 >>使整个程序架构具有一定的弹性,在程序中复用一些已经定义完善的类不仅可以减少软件开发周期,也可以提高软件的可维护性和可扩展性 继承的基本思想 >>基 ...
- JavaScript 面向对象程序设计(下)——继承与多态 【转】
JavaScript 面向对象程序设计(下)--继承与多态 前面我们讨论了如何在 JavaScript 语言中实现对私有实例成员.公有实例成员.私有静态成员.公有静态成员和静态类的封装.这次我们来讨论 ...
- Java继承和多态实例
我们知道面向对象的三大特性是封装.继承和多态.然而我们有时候总是搞不清楚这些概念.下面对这些概念进行整理, 为以后面向抽象的编程打下坚实的基础. 封装的概念还是很容易理解的.如果你会定义类,那么相信你 ...
- python基础——继承和多态
python基础——继承和多态 在OOP程序设计中,当我们定义一个class的时候,可以从某个现有的class继承,新的class称为子类(Subclass),而被继承的class称为基类.父类或超类 ...
随机推荐
- RAC 修改 spfile 参数
我们知道数据库的参数文件有spfile 和pfile. RAC 的参数文件比较特殊. 因为默认情况下,RAC的spfile 是放在共享设备上(RAW设备或者ASM磁盘组).而在各节点的pfile文件里 ...
- php实现cookie加密解密
1.加密解密类 class Mcrypt { /** * 解密 * * @param string $encryptedText 已加密字符串 * @param string $key 密钥 * @r ...
- ubuntu初次设置root密码
初次安装ubuntu创建的用户不是root用户,但是需要root权限的时候又需要密码,那么如何设置密码呢? 很简单.如下几步操作
- php 加密 解密 方法
base64 Base64编码可用于在HTTP环境下传递较长的标识信息 base64_encode base64_decodeserialize 可以将 ...
- 大数据工具篇之flume1.4-安装部署指南
一.引言 flume-ng是一个分布式.高可靠和高效的日志收集系统,flume-ng是flume的新版本的意思,其中“ng”意为new generate(新一代),目前来说,flume-ng 1.4是 ...
- 手动安装Android Support Library(23.0.1)
在搭建React-Native开发环境的时候,使用Android Sdk Manager无法找到Android Support Library这一项. 所以google了一下,找到了解决办法. 访问A ...
- [转]JSON.stringify 详解
来自:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify J ...
- [转]Serv-U 配置
- BOM及改变this指向
bom ( borwser object model 浏览器对象模型) 定义js操作浏览器的属性和方法 window.open(url way()) 中有两个参数 url代表打开的网页地址 wa ...
- 安卓手机安装虚拟定位的方法Xposed安装器+模拟位置(Xposed模块)
原文:https://www.52pojie.cn/thread-571328-1-1.html 未测试,据说只支持某些手机,小米和华为很难安装,建议买其他品牌. Xposed安装器步骤:·ROOT你 ...