• LeekinDeveloper@Gmail.com

多态公有继承


多态公有继承

多态的公有继承首先是建立在is-a的关系之上的,简单来说就是使用virtual关键来在基类和派生类当中,使函数有不一样的表现方式,例如:一个基类叫:brass,派生类叫:brassplus,brass类中有一个方法能够获取用户密码的方法getCumPasswd(),但是在派生类当中我想让他不获取用户的密码,而是获取当前用户的密码,这样基类当中的原函数将重新定义,但是为了不覆盖父类当中的原函数,我们就要在函数前加virtual关键字,来让函数在父类和基类当中有不同的表现形式。

如果要在派生类中重新定义基类的方法,通常应在基类中将方法声明为虚的。这样,程序将根据对象类型,而不是指针或引用类型来选择方法版本。为基类声明一个虚析构函数也是一种惯例。

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#ifndef __apple_H__
#define __apple_H__

class Friut
{
public:
Friut ();
virtual ~Friut ();

virtual void drawSelf(int x.int y)
{
printf("x point:" + x + "y point:"+y);
}
};

class apple : public Friut
{
private:
typedef Friut super;
public:
apple ();
virtual ~apple ();
void drawSelf(int x.int y)
{
Point point(double(x),double(y));//强制类型转换为double
draw(point);
}
virtual void show();
};
#endif // __apple_H__
//注意:这只是一个例子,它并不运行

首先在Friut当中定义的函数drawSelf,为了在派生类当中能够重新定义他的功能,我们使用在父类当中使用virtual关键字声明函数。

静态联编与动态联编

1. 概念

将源代码中的函数调用解释为执行特定的函数代码块被称为函数联编。

静态联编

在c++当中,引许我们使用函数重载,这是一个复杂的任务,,编译器必须查看函数参数以及函数名才能确定使用哪个函数,C/C++编译器可以在编译过程中完成这种联编,在编译过程中联编称为静态联编,也称早期联编。发生在编译函数重载过程。

动态联编

虚函数的这项工作将更为复杂,使用基类还是派生类当中的函数并不能在编译阶段完成,因为编译器不知道用户选择了哪种类型的对象,所以,编译器必须生成能够在程序运行时选择正确的虚方法的代码,这被称为动态联编,又称晚期联编。

编译器对非虚方法使用静态联编

2. 动态联编如何工作的

动态联编在编译的过程,编译器会给每个对象生成一个隐藏成员,隐藏成员中保存了一个指向函数地址数组的指针,这个数组就叫虚函数表(vtb1),这个表将记录下了基类函数原型的地址,如果在派生类当中定义了新的虚函数,则该函数的地址被添加到vtb1中。

书本例子

知道虚函数的工作原理能让我们更进一步理解虚函数,调用函数时,程序将查看储存在对象中的虚函数表中的地址,然后转向相应的函数地址表,如果使用类声明的第一个函数,则程序使用数组中第一个函数地址,并执行该地址的函数,

1
2
3
apple bigApple;
Friut *pFu = &bigApple;
pFu->drawSelf(12,45);

如代码所示,pFu调用drawSelf函数,将调用基类还是派生类当中的drawSelf函数呢?如果我们没有使用虚函数来定义这个函数,将调用父类Friut当中的drawSelf函数,如果使用了虚函数,在编译阶段生成了虚函数表,那么将在bigApple对象中生成一个虚函数表,并位于数组第一个位置,当调用drawSelf函数时,实质pFu指针地址是bigApple所在地址,也是首先从vtb1中取出的地址,所以将调用apple类当中的drawSelf函数。

3. 使用虚函数的性能消耗

  • 每个对象都将增大,增大量为储存地址的空间;
  • 对于每个类,编译器将生成一个虚函数地址表;
  • 对于每个函数调用,都要执行一个额外操作,即:查看虚函数表

4. 使用虚函数注意事项

  • 构造函数不能是虚的,我们在创建派生类对象的时候,首先要调用基类构造函数生产基类对象,所以这里构造函数不能为虚,而是各自拥有创建对象的构造函数

  • 析构函数应该是虚的。举例说明:

    1
    2
    3
    Friut *p_Fu = new apple();
    ......
    delete p_Fu;

delete p_Fu;这里是调用~Friut()还是~apple()呢?如果我们的~Friut()和~apple()是虚的,将先调用~apple()析构函数释放内存,在调用~Friut()释放基类内存。析构函数不是虚的,将只能释放apple对像的内存.

  • 友元不能是虚函数,因为友元不是类成员,而只有成员才能是虚函数。

  • 如果派生类没有重新定义函数,将使用该函数的基类版本。

  • 隐藏基类方法,note:如果要使用虚函数,那么请确保在派生类中函数名和参数类型、参数个数相同,否则,将视为重新定义,这个方法将覆盖基类中的方法。如果返回值的类型是基类的引用或指针,可以将其改为派生类的引用或指针。这叫返回类型协变,因为引许返回类型随类类型的变化而变化。