C++的静态联编和动态联编

在C++中,虚函数(virtual function)是支持动态多态性的一种机制。通过在基类中将函数声明为虚函数,你允许派生类重写(override)这个函数,并且可以在运行时根据对象的实际类型来确定调用哪个类的函数实现。这是面向对象编程中的一个核心概念,允许你编写更加灵活和可复用的代码。

虚函数的声明

在基类中,你可以通过将函数前加上virtual关键字来声明一个虚函数。例如:

class Base {  
public:  
    virtual void func() {  
        // 基类的实现  
    }  
};

在派生类中,你可以重写这个虚函数:

class Derived : public Base {  
public:  
    void func() override {  
        // 派生类的实现  
    }  
};

注意,在C++11及之后的版本中,你可以使用override关键字来显式指明一个函数是重写了基类中的虚函数。这不是必须的,但它可以帮助编译器检查你的代码,确保你确实是在重写一个基类的虚函数,而不是声明一个新的函数。

动态多态性的实现

虚函数的工作原理依赖于虚函数表(vtable)。当你声明一个类为包含虚函数的类时,编译器会为这个类创建一个虚函数表。这个表包含了指向类中所有虚函数的指针。每个对象都会有一个指向其类的虚函数表的指针(vptr)。当你通过基类指针或引用调用一个虚函数时,程序会查找vptr指向的虚函数表,找到正确的函数实现来调用。

纯虚函数和抽象类

在基类中,你还可以声明纯虚函数,这样的函数没有实现,并且要求派生类必须提供实现。一个包含纯虚函数的类是抽象类,不能被实例化。纯虚函数的声明方式是在函数声明后面添加 = 0

class AbstractBase {  
public:  
    virtual void pureVirtualFunc() = 0; // 纯虚函数  
};  
  
class ConcreteDerived : public AbstractBase {  
public:  
    void pureVirtualFunc() override {  
        // 派生类中必须提供实现  
    }  
};

虚析构函数

如果基类中有动态分配的资源,并且你计划通过基类指针来删除派生类对象,你应该将基类的析构函数声明为虚函数。这样可以确保在删除派生类对象时调用正确的析构函数,从而正确释放资源。

class Base {  
public:  
    virtual ~Base() {  
        // 清理基类资源  
    }  
};  
  
class Derived : public Base {  
public:  
    ~Derived() {  
        // 清理派生类资源  
    }  
};

使用虚函数和动态多态性,你可以编写更加灵活和模块化的代码,其中不同类型的对象可以以统一的方式被处理,而具体的行为则在运行时根据对象的实际类型确定。

虚函数是实现动态联编的基础

联编是指一个计算机程序自身彼此关联的过程,在这个联编过程中,需要确定程序中的 操作调用(函数调用) 与 执行该操作(函 数) 的代码段之间的映射关系。

意思就是这个函数的实现有多种,联编就是把调用和对应的实现进行映射的操作。

按照联编进行的阶段不同,可分为静态联编和动态联编。

静态联编 静态联编工作是在程序编译连接阶段进行的,这种联编又称为早期联编,因为这种联编实在 程序开始运行之前 完成的。在程 序编译阶段进行的这种联编在编译时就解决了程序的操作调用与执行该操作代码间的关系。 动态联编 编译程序在编译阶段并不能确切地指导将要调用的函数,只有在程序执行时才能确定将要调用的函数,为此要确切地指导将要 调用的函数,要求联编工作在程序运行时进行,这种在 程序运行时进行的 联编工作被称为动态联编。 C++中,动态联编是在 虚函数的支持下实现的 。 静态联编和动态联编都是属于多态性的,他们在不同的阶段对不同的实现进行不同的选择。 动态联编需要虚函数的支持,这是因为虚函数的工作原理决定的,而正是因为使用了虚函数来实现动态联编,也让动态联编的 效率略低于静态联编。通常,编译器处理虚函数的方法是: 给每个对象添加一个隐藏成员,隐藏成员保存了一个指向函数地 址数组的指针 ,这个数组就是虚函数表(virtual function table, vtbl)。虚函数表中存储了为类对象进行声明的虚函数的地址,调 用虚函数时,程序将查看存储在对象中的vtbl地址,然后转向相应的函数地址表,如果使用类声明中定义的第一个虚函数,则 程序将使用数组中的第一个函数地址,并执行具有该地址的函数,如果使用类声明中的第三个虚函数,程序将使用地址位数组 中第三个元素的函数。 虚函数这个概念是C++的精华之一。遇到虚函数时要注意: 定义一个函数为虚函数,不代表函数为不被实现的函数(可以有自己的实现) 定义他位虚函数是为了允许用基类的指针来调用子类的这个函数(提供了基类调用子类函数的方式) 定义一个函数为纯虚函数,才代表函数没有被实现(声明后面接=0 virtual func() = 0 此时派生类必须要实现此虚函数) 具有纯虚函数的类是 抽象类 ,不能用于生成对象(即不能实例化),只能派生,他派生的类如果没有实现纯虚函数,那么他 的派生类还是抽象函数。 虚析构函数 虚析构函数顾名思义就是将析构函数定义为虚函数。如果我们在派生中分配了内存空间,但是基类的析构函数不是虚析构函 数,就会发生内存泄漏。先看一个例子

#include <iostream>
using namespace std;
class Base
{
 public:
 Base(){ data = new char[10];}
 ~Base(){ cout << "destroying Base data[]\n";delete []data;}
 private:
 char *data;
};
class Derive: public Base
{
 public:
 Derive(){ D_data = new char[10];}
 ~Derive(){ cout << "destroying Derive data[]\n";delete []D_data;}
 private:
 char *D_data;
 };int main()
{
Base *basePtr = new Derive();
delete basePtr;
return 0;
}
/*输出结果:
$ ./a.out
destroying Base data[]*/

在这个例子中,派生类的析构函数并没有被调用,这在大的项目中就是一个灾难。究其原因是我们在main函数中定义了一个 Base的指针,当我们delete一个动态分配的Base指针时,Base指针此时却指向了Derive类型的对象,但编译器还是按照Base 类型调用了析构函数,没有执行Derive类型的虚析构函数。修改Base类的析构函数为虚析构函数即可以确保执行正确的析构 函数版本。 最后总结一下关于虚函数的一些常见问题: 1. 虚函数是动态绑定的,也就是说,使用虚函数的指针和引用能够正确找到实际类的对应函数,而不是执行定义类的函 数,这就是虚函数的基本功能。 2. 构造函数不能是虚函数。而且,在构造函数中调用虚函数,实际执行的是父类的对应函数,因为自己还么有构造好,多 态此时是被disable的。 3. 析构函数可以是虚函数,而且,在一个复杂类结构中,这往往是必须的。 4. 将基类中的一个函数定义为纯虚函数,实际上是将这个类定义位抽象类,不能实例化对象。 5. 纯虚函数通常没有定义体,但也可以拥有。(如果Base的析构函数为纯虚函数,那么在类外定义Base::~Base(){…}的方 式来定义其定义体) 6. 析构函数可以是纯虚的,但纯虚析构函数必须有定义体,因为析构函数的调用是在子类中隐含的。 7. 非纯的虚函数必须有定义体,不然是一个错误。 8. 派生类的override虚函数定义必须和父类完全一致,除了一个特例,如果父类返回值是一个指针或引用,子类override时 可以返回这个指针(或引用)的派生。如在Base中定义了virtual Base clone();在Derive中可以定义virtual Derive clone()。

在C++中,联编(Binding)是确定程序中的标识符与所引用实体之间的对应关系的过程。根据这个对应关系的确定时机,联编可以分为静态联编(Static Binding)和动态联编(Dynamic Binding)。

静态联编(Static Binding)

静态联编也称为早期联编(Early Binding),是指在编译时期就确定了程序中的标识符与其所引用实体之间的对应关系。在C++中,这通常涉及到函数重载的解析和模板实例化等。

例如,当你调用一个重载函数时,编译器会根据你提供的参数类型和数量在编译时期就确定调用哪个函数。这就是静态联编的一个例子。

动态联编(Dynamic Binding)

动态联编也称为晚期联编(Late Binding)或运行时联编(Run-time Binding),是指在运行时确定程序中的标识符与其所引用实体之间的对应关系。在C++中,这主要涉及到虚函数的调用。

动态联编是通过虚函数表(Vtable)实现的。当一个类包含虚函数时,编译器会为这个类创建一个虚函数表,表中包含了指向虚函数地址的指针。当创建类的对象时,编译器会在对象的内存布局中加入一个指向虚函数表的指针(Vptr)。在运行时,通过这个虚函数表指针,可以找到并调用正确的虚函数实现。

例如,如果你有一个基类Shape和一个派生类CircleShape中有一个虚函数draw()Circle重写了这个虚函数。当你通过一个Shape指针或引用来调用draw()函数时,实际调用的函数(Shape::draw()还是Circle::draw())会在运行时根据指针或引用实际指向的对象类型来确定。这就是动态联编的一个例子。

总结

  • 静态联编:在编译时期确定标识符与实体的对应关系。
  • 动态联编:在运行时确定标识符与实体的对应关系,主要通过虚函数表实现。

在面向对象编程中,动态联编是实现多态性的关键机制之一。它允许你编写更加灵活和可扩展的代码,因为你可以在不修改已有代码的情况下添加新的类和方法。

  • 13
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值