静态联编和动态联编

联编

1)函数名联编:将源代码中的函数调用解释为执行特定的函数代码块被称为函数名联编。
2)静态联编:在编译过程中进行联编称为静态联编。
3)动态联编:编译器必须生成能够在程序运行时选择正确的虚方法的代码。

指针和引用类型的兼容性

动态联编与通过指针和引用调用方法相关,这是由继承控制的。

C++不允许将一种类型的地址赋给另一种类型的指针,也不允许一种类型的引用指向另一种类型:

double x = 2.5;
int * pi = &x; //错误
long & r1 = x; //错误

指向基类的引用或指针可以引用派生类对象,而不必进行显式类型转换。

BrassPlus dilly("Annie Dill", 493222, 2000);
Brass * pb = &dilly; //ok
Brass * rb = dilly; //ok

1)向上强制转换:将派生类引用或指针转换为基类引用或指针。这使公有继承不需要进行显式转换。向上强制转换是可传递的。

公有继承不需要进行显式类型转换。BrassPlus对象都是Brass对象,因为它继承了Brass对象所有的数据成员和成员函数。所以对Brass对象执行任何操作,都适用于BrassPlus对象。因此,为处理Brass引用而设计的函数可以对BrassPlus对象执行同样的操作,而不必担心会导致任何问题。

向上强制转换是可传递的,如果从BrassPlus派生出BrassPlusPlus类,则Brass指针或引用可以引用Brass对象、BrassPlus对象或BrassPlusPlus对象。

2)向下强制转换:将基类指针或引用转换为派生类指针或引用。如果不使用显示类型转换,则向下强制转换是不允许的。

派生类可以新增数据成员,因此使用这些数据成员的类成员函数不能应用于基类。

假如从Employee类派生出Singer类,并添加了歌手音域的数据成员和用于报告音域的值的成员函数range(),则将range()方法应用于Employee对象是没有意义的。但如果允许隐式向下强制转换,则可能无意将指向Singer的指针设置为一个Employee对象的地址,并使用该指针来调用range()方法。

3)对于使用基类引用或指针作为参数的函数调用,将进行向上转换。假定每个函数都调用虚方法ViewAcct()。

void fr(Brass & rb);
void fp(Brass * pb);
void fv(Brass b);
int main()
{
    Brass b("Billy Bee", 123432, 10000.00);
    BrassPlus bp("Betty Beep", 232313, 12345.0);
    fr(b); //Brass::ViewAcct()
    fr(bp); //BrassPlus::ViewAcct()
    fp(b); //Brass::ViewAcct()
    fp(bp); //BrassPlus::ViewAcct()
    fv(b); //Brass::ViewAcct()
    fv(bp); //Brass::ViewAcct()
    ...
}

按值传递导致只将BrassPlus对象的Brass部分传递给函数fv()。但随引用和指针发生的隐式向上转换导致函数fr()和fp()分别为Brass对象和BrassPlus对象使用Brass::ViewAcct()和BrassPlus::ViewAcct()。

4)隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编,C++使用虚成员函数来满足这种需求。

虚成员函数和动态联编

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

BrassPlus ophelia; //派生类对象
Brass * bp; //基类指针
bp = &ophelia; //将Brass指针指向BrassPlus对象
bp->ViewAcct(); //用哪个版本

如果在基类中没有将ViewAcct()声明为虚的,则bp->ViewAcct()将根据指针类型(Brass*)调用Brass::ViewAcct()。指针类型在编译时已知,因此编译器在编译时,可以将ViewAcct()关联到Brass::ViewAcct()。

2)编译器对虚方法使用动态联编。
如果在基类中将ViewAcct()声明为虚的,则bp->ViewAcct()根据对象类型(BrassPlus)调用BrassPlus::ViewAcct()。对象类型为BrassPlus,但通常只有在运行程序时才能确定对象的类型。所以编译器生成的代码将在程序执行时,根据对象类型将ViewAcct()关联到Brass::ViewAcct()或BrassPlus::ViewAcct()。

为什么有两种类型的联编,为什么默认为静态联编
1)效率:
为了程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向对象的类型,这增加了额外的处理开销。

如果类不会用基类,则不需要动态联编,如果派生类不重新定义基类的任何方法,也不需要使用动态联编,这样使用静态联编更合理,效率更高。

由于静态联编的效率更高,因此被设置为C++的默认选择。
2)概念模型:
在设计类时,可能包含一些不在派生类重新定义的成员函数。Brass::Balance()函数返回账户结余,不应该重新定义。

不将该函数设置为虚函数:效率更高。指出不要重新定义该函数。这表明,仅将那些预期将被重新定义的方法声明为虚的。

注意:如果要在派生类中重新定义基类的方法,则将它设置为虚方法,否则,设置为非虚方法。

虚函数的工作原理

编译器处理虚函数的方法:给每个对象添加一个隐藏成员。

虚函数表:隐藏成员中保存了一个指向函数地址数组的指针。

虚函数表中存储了为类对象进行声明的虚函数的地址。
1)基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。
2)派生类对象将包含一个指向独立地址表的指针。
3)如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址。
4)如果派生类没有重新定义虚函数,该虚函数表将保存函数原始版本的地址。
5)如果派生类定义了新的虚函数,则该函数的地址也将被添加到虚函数表中。

注意:无论类中包含的虚函数是1个还是10个,都只需要在对象中添加1个地址成员,只是表的大小不同而已。

调用规则:
1)调用虚函数时,程序将查看存储在对象中的虚函数表地址。
2)然后转向相应的函数地址表。
3)如果使用类声明中定义的第一个虚函数,则程序将使用数组中的第一个函数地址,并执行具有该地址的函数。
4)如果使用类声明中的第三个虚函数,程序将使用地址为数组中第三个元素的函数。

使用虚函数时,内存和执行速度:
1)每个对象都将增大,增大量为存储地址的空间。
2)对于每个类,编译器都创建一个虚函数地址表。
3)对于每个函数调用,都需要执行一项额外的操作,即到表中查找地址。

虽然非虚函数的效率比虚函数稍高,但不具备动态联编功能。

有关虚函数注意事项

虚函数要点:
1)在基类方法的声明中使用关键字virtual可使该方法在基类以及所有的派生类中是虚的。
2)如果使用指向对象的引用或指针来调用虚方法,程序将使用为对象类型定义的方法,称为动态联编。这样基类指针或引用可以指向派生类对象。
3)如果定义的类将被用作基类,则应将那些要在派生类中重新定义的类方法声明为虚的。

构造函数
构造函数不能是虚函数,创建派生类对象时,将调用派生类的构造函数,而不是基类的构造函数,然后,派生类的构造函数将使用基类的一个构造函数,这种顺序不同于继承机制。因此,派生类不继承基类的构造函数,所以将类构造函数声明为虚的没有什么意义。

析构函数
析构函数可以是虚函数,除非类不用做基类。

假设Employee是基类,Singer是派生类,并添加一个char*成员,该成员指向由new分配的内存。当Singer对象过期时,必须调用~Singer()析构函数来释放内存。

Employee * pe = new Singer; //合法,Employee是Singer基类
...
delete pe; //~Employee()? ~Singer()?

如果使用默认的静态联编,delete语句将调用~Employee()析构函数。这将释放由Singer对象中Employee部分指向的内存,但不会释放新的类成员指向的内存。但如果析构函数是虚的,则将先调用~Singer析构函数释放由Singer组件指向的内存,然后,调用~Employee()析构函数来释放由Employee组件指向的内存。

即使基类不需要显式析构函数提供服务,也不应依赖于默认构造函数,而应提供虚析构函数,即使不执行任何操作:

virtual ~BaseClass() {}

给类定义一个析构函数并非错误,即使这个类不用做基类;这只是一个效率方面的问题。

通常给基类提供一个虚析构函数,即使这个类不用做基类,或者它并不需要析构函数。

友元
友元不能是虚函数,因为友元不是类成员,只有成员才能是虚函数。如果由于这个原因引起设计问题,可以通过让友元函数使用虚成员函数来解决。

没有重新定义
如果派生类没有重新定义函数,将使用该函数的基类版本。如果派生类位于派生链中,则将使用最新的虚函数版本,例外的情况是基类版本是隐藏的。

重新定义将隐藏方法

class Dwelling
{
public:
      virtual void showperks(int a) const;
...
};
class Hovel : public Dewlling
{
public:
      virtual void showperks() const;
...
};

这将导致出现编译器警告:

Warning: Hovel::showperks(void) hides Dwelling::showperks(int)

也可能不会出现警告。但不管结果怎样,代码将具有如下含义:

Hovel trump;
trump.showperks(); //有效
trump.showperks(5); //无效

新定义将showperks()定义为一个不接受任何参数的函数。重新定义不会生成函数的两个重载版本,而是隐藏了接受一个int参数的基类版本。总之,重新定义继承的方法并不是重载。如果重新定义派生类中的函数,将不只是使用相同的函数参数列表覆盖基类声明,无论参数列表是否相同,该操作将隐藏所有的同名基类方法。

经验规则:
1)如果重新定义继承的方法,应确保与原来的原型完全相同,但如果返回类型是基类引用或指针,则可以修改为指向派生类的引用或指针。这种特性被称为返回类型协变,因为允许返回类型随类类型的变化而变化。这种只适用于返回值,而不适用于参数。

class Dwelling
{
public:
//一个基类方法
      virtual Dwelling & build(int n);
      ...
};
class Hovel : public Dwelling
{
public:
//具有返回类型协变的派生方法
      virtual Hovel & build(int n); //相同函数名
      ...
}

2)如果基类声明被重载了,则应在派生类中重新定义所有的基类版本。

class Dwelling
{
public:
//三个重载showperks()
      virtual void showperks(int a) const;
      virtual void showperks(double x) const;
      virtual void showperks() const;
      ...
};
class Hovel : public Dwelling
{
public:
//三个重定义showperks()
      virtual void showperks(int a) const;
      virtual void showperks(double x) const;
      virtual void showperks() const;
      ...
};

如果只重新定义一个版本,则另外两个版本将被隐藏,派生类对象将无法使用他们。注意:如果不需要修改,则新定义可只调用基类版本:

void Hovel::showperks() const {Dwelling::showperks();}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阳光开朗男孩

你的鼓励是我最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值