CPP虚函数详解与实例
CPP虚函数详解与实例
在 CMU_15445 的Project3 中大量使用了虚函数,抽象类的方法 主要在 Expression(表达式) 以及 Executor(Plan_Node的执行) 中, 在完成 Part1 的时候仅关注了功能的实现, 还没有完全搞清楚为什么要使用虚函数以及抽象类, 以及虚函数背后的原理, 本次补充一下.
虚函数的定义#
大多人看到虚函数的第一眼往往是,
程序运行时可以通过父类的指针或引用调用子类的函数.
但是, 这只是虚函数的工作方式, 也不是虚函数的目的, 也不是工作原理, 所以看到这句话大部分人还是一头雾水, 也没有继续看下去的意愿.
所以我们从头开始讲, 并且我将会引入最近在做的 CMU_15445 中的表达式的例子, 用于更详细的解释.
虚函数的目的#
我们知道 OOP 的三大特性是, 封装, 继承与多态, 其中虚函数是实现多态的方式, 虚函数的目的也就是实现多态.
多态 (polymorphism)#
我引用一段英文描述, 我觉得很好的解释了什么是多态:
Polymorphism is a popular concept in object-oriented programming (OOP), referring to the idea that an entity in code such as a variable, function or object can have more than one form. The word polymorphism is derived from Greek and means "having multiple forms." Apart from computer programming, the idea of polymorphism occurs in other real-world areas, including biology, chemistry and drug development.
多态的概念就不再赘述了, 在 CMU_15445 的 Project3 中, 我们用表达式来解释多态就是, BUSTUB 中支持 SQL 语法的多种表达式, 我们将这些表达式的属性以及功能进行抽象, 得到一个基类, 也就是 abstract_expression
, 这个类是一个抽象类, 在这个类中只有虚函数. 并且抽象出一些方法为 Evaluate
, EvaluateJoin
等方法. 每一个具体的表达式都将继承这个抽象表达式, 在抽象类中定义的这些方式是使用虚函数的方式定义的. 然后在子类中重写这些表达式中的方法, 来实现自己的特定功能, 这就是多态. 简单粗暴的理解就是, 表达式表现出多种不同的状态, 这是一种设计思想.
用虚函数来实现多态#
在抽象类中定义的函数都是虚函数, 在abstract_expression
基类中定义的方法都是如此, 使用虚函数来实现多态有几个特征和条件:
- 父类的指针可以指向派生类的对象, 这一点 CPP 是满足的.
- 派生类中实现了基类中定义的虚函数, 根据自身的性质来实现这些函数.
- 虚函数的覆盖规则保证, 所有子类中实现的虚函数与抽象类中的虚函数在参数与返回值上保持一致, 仅函数实现不同.
简单点说, 就是在代码实现中, 子类需要重写基类(抽象类)中定义的虚函数方法, 在写代码的时候可以通过父类的指针来调用这个函数, 但是程序实际执行的过程中会根据指针指向的具体实例(派生类的对象), 调用派生类中实现的虚函数.
在 BUSTUB 中的实例就是:
在 PlanNode 中的 Expression 有多种类型, 但是如果具体实现的时候, 每次执行表达式的操作的时候都需要判断表达式的类型以及操作的类型, 那么实际代码实现中会存在大量的条件判断以及重复代码, 这时就可以使用多态的方式优雅的调用 Executor 的执行函数.
例如在 SeqScanExecutor
的 Next() 函数中, 当然这个 Next() 函数本身也是实现了一个虚函数. 哈哈哈哈.
SeqScanPlanNode 中, filter_predicate_
的类型有很多种, 可以是ComparisonExpression
, LogicalExpression, 或者
ConstantExpression等类型, 实际执行的时候, 实现的方式是使用下面的代码, 只需要调用指针
filter_predicate_指向的对象的
Evaluate()` 函数即可, 并不需要写太多的条件判断.
auto SeqScanExecutor::Next(Tuple *tuple, RID *rid) -> bool {
// scan the iterator of this table
while (!table_iterator_->IsEnd()) {
// get the current tuple in this iterate
auto [tuple_meta, current_tuple] = table_iterator_->GetTuple();
// skip the deleted tuple
if (!tuple_meta.is_deleted_) {
auto schema = this->GetOutputSchema();
/** filter the */
if(plan_->filter_predicate_ == nullptr || plan_->filter_predicate_->Evaluate(¤t_tuple, schema).GetAs<bool>()) {
*tuple = current_tuple;
*rid = table_iterator_->GetRID();
++(*table_iterator_);
return true;
}
}
// in the TableIterator, it reload the Prefix Increment
++(*table_iterator_);
}
return false;
}
在上面的代码中, 我们知道 filter_predicate_
在定义的时候如下:
/** The table whose tuples should be scanned */
table_oid_t table_oid_;
/** The table name */
std::string table_name_;
/** The predicate to filter in seqscan.
* For Fall 2023, We'll enable the MergeFilterScan rule, so we can further support index point lookup
*/
AbstractExpressionRef filter_predicate_;
filter_predicate_
是一个抽象类的指针, CPP 满足, 父类的指针可以指向子类的对象. 在程序执行的时候, 在调用抽象类中的虚函数方法的时候, plan_->filter_predicate_->Evaluate(¤t_tuple, schema).GetAs<bool>()
会根据指针指向的具体的实例来判断调用的函数.
因此, 如果 filter_predicate_
指向一个 ComparisonExpression
实例, 那么就调用 ComparisonExpression
派生类中的 Evaluate()
函数, 如果是 LogicalExpression
实例, 就调用对应的 Evaluate()
方法.
虚函数的实现机制#
虚函数实现多态的一大特点就是, 在代码中, 使用的是虚函数的指针调用函数, 但是实际程序运行的时候, 调用的是该指针指向的具体对象实例化类的函数.
CPP实现这一步骤的机制是使用虚函数表, 虚函数表是指将一个类中虚函数的地址写入一个数组表中, 并且每个类的一个实例化的对象会使用一个指针隐式的保存该类的虚函数表的地址. 我们使用下面的例子来解释这一原理, 还是使用 BUSTUB 中的 Expression:
我们在图的右边将抽象类 AbstractExpression
简化为类 A
, ComparisonExpression
是类 B
, LogicExpression
是类 C
.
程序编译运行时使用虚函数表的原理可以总结为:
- 每一个类都有一个对应的虚函数表, 并且会记录这个虚函数表的首地址.
- 实例化一个类的对象的时候, 会使用一个指针记录这个类的虚函数表的首地址.
- 如果基类中的虚方法没有在派生类中重写, 那么派生类将继承基类中的虚方法, 而且派生类中虚函数表将保存基类中未被重写的虚函数的地址, 例如派生类B中的
Function1
, 派生类C中的Function_1
,Function_2
,Function_3
. - 如果派生类重写了基类的虚方法, 该派生类虚函数表将保存重写的虚函数的地址, 而不是基类的虚函数地址, 例如派生类B中的
Function_2
和Function_3
. - 如果派生类中定义了新的虚方法, 则该虚函数的地址也将被添加到派生类虚函数表中, 例如派生类 B 中的
Function_4
,Function_5
, 和派生类 C 中的Function_6
,Function_7
- 程序在运行期间会根据基类的指针, 例如我们的例子中是抽象类指针
AbstractExpressionRef filter_predicate_
, 找到实例化的对象的虚函数表的指针, 然后在这张虚函数表中找到对应的虚函数地址, 最后调用该函数.
因此在程序实现运行的过程中, 基类的指针实际调用的是派生类的地址. 完成了多态的机制.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· 阿里巴巴 QwQ-32B真的超越了 DeepSeek R-1吗?
· 【译】Visual Studio 中新的强大生产力特性
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 【设计模式】告别冗长if-else语句:使用策略模式优化代码结构