哈Ha 如果您知道标题中问题的答案,那么恭喜,您不需要本文。 它是针对像我这样的编程初学者的,他们不能总是独立地理解C ++和其他类型语言的所有复杂性,并且如果可以的话,最好还是从别人的错误中学习。
在本文中,我将不仅仅回答“
为什么我们需要C ++中的虚函数 ”这个问题,而是将通过实践给出一个示例。 作为一个简短的答案,您可以转向产生如下内容的搜索引擎:“
需要虚拟函数来提供多态性-三只OOP鲸鱼之一。由于有了它们,机器本身可以通过指针确定对象的类型,而无需向程序员加载此任务。 ” 好的,但是“为什么”问题仍然存在,尽管现在含义有所不同:“
为什么要依靠机器,花更多的时间和内存,如果您可以自己播客指针,因为它所指向的对象类型几乎总是已知的? ”的确,乍一看,强制转换会使虚函数失效,而这正是导致误解和错误代码的原因。 在小型项目中,损失是看不见的,但是,正如您将很快看到的那样,随着程序的增长,种姓的增加几乎以几何级数递增。
首先,让我们回顾一下可能根本需要种姓和虚函数的地方。 当为使用类型A声明的对象分配新操作以为与类型A兼容的类型B的对象(通常从A继承)分配内存时,类型就会丢失。通常,该对象不是一个对象,而是整个数组。 相同类型的指针数组,每个指针都在等待分配完全不同类型的对象的存储区。 这是我们将考虑的示例。
我不会拖很长时间了,任务是这样的:基于一个用Markedit超文本标记语言标记的文档(您可以
在此处阅读有关内容),构建一个解析树,并创建一个包含相同HTML标记文档的文件。 我的解决方案由三个顺序例程组成:将源文本解析为标记,从标记构建语法树,并基于该语法构建HTML文档。 我们对第二部分感兴趣。
事实是目标树的节点具有不同的类型(节,段落,文本节点,链接,脚注等),但是对于父节点,指向子节点的指针存储在数组中,因此具有一种类型-节点。
解析器本身的简化形式是这样的:它使用
Root类型创建树语法
树的“根”,声明常规类型
Node的
open_node指针(立即为其分配
树地址)和枚举类型
Node_type的
类型变量,然后循环开始,
从头开始
遍历令牌到最后。 每次迭代时,首先将
open_node打开节点的类型输入到type变量中(枚举形式的类型存储在节点的结构中),然后是
switch语句 ,该
语句检查下一个标记的类型(词法分析器已经仔细提供了标记的类型)。 在交换机的每个分支中,都提供了另一个分支来检查
类型变量,正如我们记得的那样,其中包含打开节点的类型。 根据其值,将执行不同的操作,例如:将某个类型的节点列表添加到一个打开的节点,在一个打开的节点中打开某个类型的另一个节点,并将其地址传递给
open_node ,关闭该打开的节点,引发异常。 适用于本文的主题,我们对第二个示例感兴趣。 每个打开的节点(通常是每个可以打开的节点)已经包含一个指向
Node类型的
节点的指针数组。 因此,当我们在一个打开的节点中打开一个新节点(将另一个类型的对象的内存区域分配给下一个数组指针)时,对于C ++语义分析器,它仍然是
Node类型的实例,而无需获取新的字段和方法。 现在将指向它的指针分配给变量
open_node ,而不会丢失
Node的类型。 但是,当您需要调用方法(例如段落)时,如何使用常规
Node类型的指针呢? 例如,
open_bold() ,它将在其中打开一个粗体字体节点? 毕竟,
open_bold()被声明并定义为
Paragraph类的方法,而
Node完全不知道它。 另外,
open_node也被声明为指向
Node的指针,并且它必须接受所有打开节点类型的方法。
这里有两种解决方案:显而易见的解决方案和正确的解决方案。 对于初学者来说显而易见的是
static_cast ,并且虚函数是正确的。 我们首先来看一下使用第一种方法编写的开关解析器的一个分支:
case Lexer::BOLD_START: { if (type == Node::ROOT) { open_node = tree->open_section(); open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::SECTION) { open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->open_bold(); else if (type == Node::TITLE) open_node = static_cast<Title*>(open_node)->open_bold(); else if (type == Node::QUOTE) open_node = static_cast<Quote*>(open_node)->open_bold(); else if (type == Node::UNORDERED_LIST) { open_node = static_cast<Unordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::ORDERED_LIST) { open_node = static_cast<Ordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } open_node = static_cast<Section*>(open_node)->open_paragraph(); open_node = static_cast<Paragraph*>(open_node)->open_bold(); } else if (type == Node::LINK) open_node = static_cast<Link*>(open_node)->open_bold(); else
还不错 现在,我将不再使用它很长时间,我将展示使用虚函数编写的同一部分代码:
case Lexer::BOLD_START: { if (type == Node::ROOT) { open_node = tree->open_section(); open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else if (type == Node::SECTION) { open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else if (type == Node::UNORDERED_LIST) { open_node = open_node->close(); while (open_node->get_type() != Node::SECTION) open_node = open_node->close(); open_node = open_node->open_paragraph(); open_node = open_node->open_bold(); } else
收益是显而易见的,但是我们真的需要吗? 毕竟,您必须在
Node类中将所有派生类的所有方法声明为虚方法,并以某种方式在每个派生类中实现它们。 答案是肯定的。 该程序中没有专门的方法(29),并且在与它们不相关的派生类中的实现仅由一行组成:
throw string(“ error!”); 。 您可以启用广告素材模式,并为每次异常抛出提供唯一的一行。 但最重要的是-由于减少了代码,减少了错误数量。 强制转换是导致代码错误的最重要原因之一。 因为在应用
static_cast之后,如果调用的类包含在给定的类中
,则编译器将停止宣誓。 同时,不同的类可能包含具有相同名称的不同方法。 就我而言,代码中隐藏了6个! 错误,而其中之一在多个开关分支中重复。 这是:
else if (type == Node:: open_node = static_cast<Title*>(open_node)->open_italic();
接下来,在扰流器下,我提供了解析器的第一版和第二版的完整列表。
解析器与转换 Root * Parser::parse (const Lexer &lexer) { Node * open_node(tree); Node::Node_type type; for (unsigned long i(0), len(lexer.count()); i < len; i++) { type = open_node->get_type(); if (type == Node::CITE || type == Node::TEXT || type == Node::NEWLINE || type == Node::NOTIFICATION || type == Node::IMAGE) throw string("error!"); switch (lexer[i].type) { case Lexer::NEWLINE: { if (type == Node::ROOT || type == Node::SECTION) ; else if (type == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->add_text("\n"); else if (type == Node::TITLE) open_node = static_cast<Title*>(open_node)->add_text("\n"); else if (type == Node::QUOTE) open_node = static_cast<Quote*>(open_node)->add_text("\n"); else if (type == Node::UNORDERED_LIST) { open_node = static_cast<Unordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } } else if (type == Node::ORDERED_LIST) { open_node = static_cast<Ordered_list*>(open_node)->close(); while (open_node->get_type() != Node::SECTION) { if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::UNORDERED_LIST) open_node = static_cast<Unordered_list*>(open_node)->close(); else if (open_node->get_type() == Node::PARAGRAPH) open_node = static_cast<Paragraph*>(open_node)->close(); } } else if (type == Node::LINK) { open_node = static_cast<Link*>(open_node)->add_text(lexer[i].lexeme); } else
可以访问虚拟方法的解析器 Root * Parser::parse (const Lexer &lexer) { Node * open_node(tree); Node::Node_type type; for (unsigned long i(0), len(lexer.count()); i < len; i++) { type = open_node->get_type(); if (type == Node::CITE || type == Node::TEXT || type == Node::NEWLINE || type == Node::NOTIFICATION || type == Node::IMAGE) throw string("error!"); switch (lexer[i].type) { case Lexer::NEWLINE: { if (type == Node::ROOT || type == Node::SECTION) ; else if (type == Node::PARAGRAPH || type == Node::TITLE || type == Node::QUOTE || type == Node::TITLE || type == Node::QUOTE) open_node = open_node->add_text("\n"); else if (type == Node::UNORDERED_LIST || type == Node::ORDERED_LIST) { open_node = open_node->close(); while (open_node->get_type() != Node::SECTION) open_node = open_node->close(); } else
从1357行开始,代码减少到487,几乎是三倍,这还不包括行的长度!还有一个问题:交货时间如何?为了确定开放节点的类型,我们必须为计算机本身支付多少毫秒?我进行了一个实验-在家用计算机上,同一文档的第一种和第二种情况下,解析器的工作时间以毫秒为单位固定。结果是:投放-538毫秒。虚拟功能-1174毫秒。总计636毫秒-代码紧凑和没有错误的费用。这很多吗?可能吧 但是,如果我们需要一个尽可能快地运行并且需要尽可能少的内存的程序,我们就不会去OOP并用汇编语言来编写它,这要花一个星期的时间,并且冒犯大量错误的风险。所以我的选择是在程序中static_cast和dynamic_cast相遇的地方,用虚拟函数替换它们。您对此有何看法?