c ++虚拟关键字与覆盖函数
c++ virtual keyword vs overriding function
我正在学习 C++,并且正在学习 virtual 关键字。我已经搜索了互联网试图理解它无济于事。我进入我的编辑器并做了以下实验,期望它打印出基本消息两次(因为我的印象是需要 virtual 关键字来覆盖函数)。但是,它打印出两条不同的消息。如果我们可以简单地重写函数并且仍然看似获得多态行为,有人可以向我解释为什么我们需要 virtual 关键字吗?也许将来有人可以帮助我和其他人理解虚拟与覆盖。 (我得到的输出是 "I am the base" 后跟 "I am the derived")。
#include <iostream>
using namespace std;
class Base{
public:
void printMe(){
cout << "I am the base" << endl;
}
};
class Derived: public Base{
public:
void printMe(){
cout << "I am the derived" << endl;
}
};
int main() {
Base a;
Derived b;
a.printMe();
b.printMe();
return 0;
}
您在这里看不到行为,因为您已将 b
声明为 Derived
类型,因此编译器知道要使用哪些函数。为了揭示为什么 virtual
是必要的,你需要把事情搞混:
int main() {
Base a;
Base *b = new Derived();
a.printMe();
b->printMe();
delete b;
return 0;
}
现在 b
是 Base*
类型,这意味着它将使用 Base
上的函数加上虚函数 table 中的任何内容。这会破坏您的实施。您可以通过正确声明 virtual
.
来修复它
使用您拥有的代码,如果您这样做
Derived derived;
Base* base_ptr = &derived;
base_ptr->printMe();
你认为会发生什么?它不会打印出 I am the derived
,因为该方法不是虚拟的,并且分派是根据调用对象的静态类型(即 Base
)完成的。如果将其更改为虚拟,则调用的方法将取决于对象的动态类型而不是静态类型。
考虑以下示例。说明需要 virtual
和 override
的重要行是 c->printMe();
。请注意,c
的类型是 Base*
,但是由于多态性,它能够正确地从派生的 class 调用重写的方法。 override
关键字允许编译器强制派生 class 方法与标记为 virtual
的基 class 方法的签名相匹配。如果将 override
关键字添加到派生的 class 函数,则该函数在派生的 class 中也不需要 virtual
关键字,因为隐含了虚拟。
#include <iostream>
class Base{
public:
virtual void printMe(){
std::cout << "I am the base" << std::endl;
}
};
class Derived: public Base{
public:
void printMe() override {
std::cout << "I am the derived" << std::endl;
}
};
int main() {
Base a;
Derived b;
a.printMe();
b.printMe();
Base* c = &b;
c->printMe();
return 0;
}
输出为
I am the base
I am the derived
I am the derived
override
是C++11新增的关键字。
您应该使用它,因为:
编译器将检查基 class 是否包含匹配的 virtual
方法。这一点很重要,因为方法名称或其参数列表中的某些拼写错误(允许重载)可能会让人觉得有些东西被重写了,但实际上并没有。
如果对一个方法使用override
,如果不使用override
关键字就覆盖了另一个方法,编译器会报错。这有助于在发生符号冲突时检测不需要的覆盖。
virtual
并不意味着 "override"。在 class 中不要使用 "override" 关键字而不是覆盖方法,您可以简单地编写此方法省略 "virtual" 关键字,覆盖将隐式发生。开发人员在 C++11 之前编写 virtual
以表明他们的覆盖意图。简单的说virtual
的意思是:这个方法可以在一个subclasses.
中重写
我想你的问题是为什么有人会在程序中使用基 class 指针来调用派生 class。
一个这样的例子是当你想在你的程序中为所有派生的 class 提供一个公共函数。您不想使用不同的派生 class 类型参数创建相同的函数。
见下文
#include<iostream>
using namespace std;
class Base{
public:
virtual void printfunc() { cout<<"this is base class";};
};
class Derived:public Base{
public:
void printfunc(){cout<<"this is derived class";};
};
void printthis(Base *ptr)
{
ptr->printfunc();
}
int main()
{
Derived func;
printthis(&func);
return 0;
}
virtual
表示,“这不是真正的 C 函数,即将一系列参数压入堆栈,然后跳转到函数体的单个不变地址。”
相反,是这头野兽在 运行 时间在 table 中查找要执行的函数体的地址。层次结构中的每个 class 在 table 中都有一个条目。函数指针的table被称为vtable。这是 多态性 的运行时机制,它注入额外的代码来执行此查找,然后 dispatch 到函数体的适当专用版本。
此外,当使用此 vtable 调度机制时,您总是通过指向对象的指针访问您的对象,而不是直接访问(变量或引用)它,即。 Foo* foo{makeFoo()}; foo->someMethod()
对比 Loo loo{}; loo.someMethod()
。因此,从一开始就需要另一个取消引用才能使用此技术。
这是巧妙的部分:这些指针也可以指向派生的 classes 的任何对象,因此如果您有一个继承自 [=16= 的 class FooChild
],您可以使用 FoodParent *
指向 FooParent
或 FooChild
.
当调用该方法时,它不是仅仅执行普通的 C 操作,即在堆栈上准备参数,然后跳转到 barMethod()
的主体,而是执行一堆 运行时间首先查找根据 class 个性化的 barMethod 的几个不同实现之一。那个table叫做vtable。 class 层次结构中的每个 class 在此 table 中都有一个条目,说明函数体真正用于特定 class 的位置,因为它们可以有不同的,即使我们正在使用 FooParent *
指向其中任何一个的实例。
但这就是我们首先要这样做的原因:假设 virtual
不存在。而你,程序员,想要处理一堆来自 class 层次结构的对象。好吧,您最终编写的代码几乎与编译器手动为您注入的代码相同!为了将这些不同 classes 的实例传递到您编写的一些函数中来处理它们,您需要一个单一大小的类型来使函数调用代码起作用。因此,请使用指针,因为指针在您的机器上始终具有相同的大小(如今),无论它们指向的对象大小有多么不同。好的。所以它是指针。这是使用 virtual
.
所需的一种 类型擦除
然后你需要一个 switch
语句或其他东西来在它指向的特定 class 上分支。但如果您为编写的每个变体手动编码,那就是这样。太傻了。很快您就会意识到,使用 table 指向您要调用的 barMethod()
各种版本的指针会更好。然后你总是可以从每个变体中查找相同的 table,而不是重写手写的 switch 语句等。所以你会那样做。您将实现一个 table,其中对于从 FooParent
派生的层次结构中的每个 class 都有指向不同 barMethod()
的指针。他们都有相同的签名(参数列表、return 值等),但每个 class.
都有不同的主体
您将为每个 class 分配一个整数 i.d。或类似的东西,并将其用作 table 的偏移量。例如,FooChildA
和 FooChildB
可能是两个不同的 class,它们都派生自 FooParent
,因此您可以将 A 分配给 0,将 B 分配给 1,或类似的东西.然后使用这些作为偏移量跳转到 table 并获取指针。这就是查找 table 的一般工作方式。获得指针后,您会将所有参数压入堆栈,然后跳转到该指针。所以 virtual
只是一个关键字,它指示编译器为您将所有这些疯狂的高级代码注入到您的代码中,因此您不必手动执行。
问题是,它是 RUNTIME 多态性,通常可以通过模板等使用 COMPILE 时间多态性。它给虚拟层次结构中的每个函数调用增加了很多 运行时间膨胀。这对于非热循环实际上很好。但是对于 运行 一直在您的系统中(例如每隔几毫秒或更长时间)的事情,这确实是一个无法接受的 table 膨胀量。对于绝大多数情况,您可以在编译时执行与所有 table 查找相同的操作,而不是使用元编程,这样 运行 时间可以非常快。
至于 override
,那一团混乱应该从一开始就在语言中,并且应该与 virtual
关键字处于相同的文本位置。可悲的是,这两个“应该”都没有完成。所以在过去,你会在 class 层次结构的最父级中声明 barMethod()
为 virtual
,然后还在派生的 class 中声明 barMethod()
] 为 virtual
。在某些时候,由于奇怪的错误,这变得非常烦人。老实说,该功能并不直观,并且在了解它多年后很难教授甚至记住。
所以我们添加了 override
以及对编译器的提示,以便我们可以捕获错误。它只是意味着“不仅这个函数是虚拟的,所有那些疯狂的 vtable 调度东西也是如此,而且,这是 barMethod()
的 DERIVED 重新定义,因此编译器可以检查使确保您将参数等与派生它的父 class 完美匹配,因为如果没有此检查,如果您不小心未能将派生版本的参数列表与父版本完全匹配,而不是覆盖父版本,编译器只会说,“哦,另一个全新的虚拟成员函数层次结构正在开始,具有不同的参数,这是根。必须是新的重载集。"
我意识到这是一个非常令人困惑的陈述。但基本上,如果你有 barMethod()
和 barMethod(int)
和 barMethod(int, char*)
等等,这些都是不同的功能,彼此之间没有真正的关系。好像每个人都有不同的名字。你可以在脑海中这样想。它本质上是编译器本身如何看待它,name mangling。因此,如果您随后将它们设为 virtual
,您可能会认为在层次结构中的各种 classes 中声明它们也会将它们放入单个成员函数虚拟层次结构中。但事实并非如此。如果您改为使用 override
关键字将它们设为虚拟,编译器会注意到 barMethod(int) override
和 barMethod(int, char*) override
与 FooParent
中的任何内容都没有关系,后者只有 barMethod()
没有参数。但他们应该压倒某些东西。 ¡编译器错误!那很好。您想要那个编译器错误,否则您的代码会发送给客户并且看起来可以正常工作但绝对不是。
virtual
的要点是允许您使用单个指针类型来表示 classes 的整个层次结构的任何实例,但可能对它们中的每一个做不同的事情。如果程序员不确保所有派生的重新定义也是虚拟的,那将不会发生。并且覆盖确保他们不会意外创建新的 class 层次结构根。
在现代 C++ 中,我们认为同时要求 virtual
和 override
太烦人了,而且它总是让视觉上更难 grep 哪些 barMethod()
是根版本,以及哪些是派生的。因此他们说,“您可以删除派生重新定义的 virtual
关键字,而只使用 override
。”这被认为是现在唯一正确的说话方式。
struct FooParent
{
// The root has virtual
virtual void barMethod(){ /* body */ } // or `=0` for "pure virtual"
}
// Original way of doing it. Just use virtual again, but this isn't the root now. This is a derived class.
struct FooChild_OldSchool : FooParent
{
virtual void barMethod(); // Total trashmouth. Bug prone.
}
struct FooChild_OverrideDays : FooParent
{
virtual void barMethod() override; // Naughty mouth. Using both.
}
struct FooChild_NonTrashyWay2020 : FooParent
{
void barMethod() override; // Prim and proper mouth. Using only override in the derived class.
}
奇怪的是,override
在句法上位于不同的位置,在参数列表之后,而不是之前。据我所知,这确实不合逻辑。我真的希望我们能解决这个问题,让 override
和 virtual
去同一个地方,在声明的开头,或者更好的是,让 virtual
去 [=32] 的地方=] 确实如此,在参数列表之后。就像现在一样,它令人讨厌地不一致和混乱,imo。我之所以这么说,是因为我相信如果我们不承认它们是缺点,这些东西就会变得不可教。因为当你学习一门新语言时,你真的需要一个更流利的人说,“嘿,这很奇怪而且有疣。别担心。这不是因为你笨。这只是因为我们的语言是进化的不稳定。
我希望它是这样的...
struct FooChild_HowIWishItWas : FooParent
{
override void barMethod();
}
// OR EVEN BETTER! Allow us to change the location of virtual!
struct FooParent_HowIWishItWasEvenMore
{
void barMethod() virtual;
}
但事实并非如此。不过,这也许就是您在内部思考它的方式,然后只记得在实际键入代码时在语法上添加这种奇怪的古怪行为。想知道关于这个的论文是否能存活 5 分钟。嗯。
我正在学习 C++,并且正在学习 virtual 关键字。我已经搜索了互联网试图理解它无济于事。我进入我的编辑器并做了以下实验,期望它打印出基本消息两次(因为我的印象是需要 virtual 关键字来覆盖函数)。但是,它打印出两条不同的消息。如果我们可以简单地重写函数并且仍然看似获得多态行为,有人可以向我解释为什么我们需要 virtual 关键字吗?也许将来有人可以帮助我和其他人理解虚拟与覆盖。 (我得到的输出是 "I am the base" 后跟 "I am the derived")。
#include <iostream>
using namespace std;
class Base{
public:
void printMe(){
cout << "I am the base" << endl;
}
};
class Derived: public Base{
public:
void printMe(){
cout << "I am the derived" << endl;
}
};
int main() {
Base a;
Derived b;
a.printMe();
b.printMe();
return 0;
}
您在这里看不到行为,因为您已将 b
声明为 Derived
类型,因此编译器知道要使用哪些函数。为了揭示为什么 virtual
是必要的,你需要把事情搞混:
int main() {
Base a;
Base *b = new Derived();
a.printMe();
b->printMe();
delete b;
return 0;
}
现在 b
是 Base*
类型,这意味着它将使用 Base
上的函数加上虚函数 table 中的任何内容。这会破坏您的实施。您可以通过正确声明 virtual
.
使用您拥有的代码,如果您这样做
Derived derived;
Base* base_ptr = &derived;
base_ptr->printMe();
你认为会发生什么?它不会打印出 I am the derived
,因为该方法不是虚拟的,并且分派是根据调用对象的静态类型(即 Base
)完成的。如果将其更改为虚拟,则调用的方法将取决于对象的动态类型而不是静态类型。
考虑以下示例。说明需要 virtual
和 override
的重要行是 c->printMe();
。请注意,c
的类型是 Base*
,但是由于多态性,它能够正确地从派生的 class 调用重写的方法。 override
关键字允许编译器强制派生 class 方法与标记为 virtual
的基 class 方法的签名相匹配。如果将 override
关键字添加到派生的 class 函数,则该函数在派生的 class 中也不需要 virtual
关键字,因为隐含了虚拟。
#include <iostream>
class Base{
public:
virtual void printMe(){
std::cout << "I am the base" << std::endl;
}
};
class Derived: public Base{
public:
void printMe() override {
std::cout << "I am the derived" << std::endl;
}
};
int main() {
Base a;
Derived b;
a.printMe();
b.printMe();
Base* c = &b;
c->printMe();
return 0;
}
输出为
I am the base
I am the derived
I am the derived
override
是C++11新增的关键字。
您应该使用它,因为:
编译器将检查基 class 是否包含匹配的
virtual
方法。这一点很重要,因为方法名称或其参数列表中的某些拼写错误(允许重载)可能会让人觉得有些东西被重写了,但实际上并没有。如果对一个方法使用
override
,如果不使用override
关键字就覆盖了另一个方法,编译器会报错。这有助于在发生符号冲突时检测不需要的覆盖。virtual
并不意味着 "override"。在 class 中不要使用 "override" 关键字而不是覆盖方法,您可以简单地编写此方法省略 "virtual" 关键字,覆盖将隐式发生。开发人员在 C++11 之前编写virtual
以表明他们的覆盖意图。简单的说virtual
的意思是:这个方法可以在一个subclasses. 中重写
我想你的问题是为什么有人会在程序中使用基 class 指针来调用派生 class。
一个这样的例子是当你想在你的程序中为所有派生的 class 提供一个公共函数。您不想使用不同的派生 class 类型参数创建相同的函数。 见下文
#include<iostream>
using namespace std;
class Base{
public:
virtual void printfunc() { cout<<"this is base class";};
};
class Derived:public Base{
public:
void printfunc(){cout<<"this is derived class";};
};
void printthis(Base *ptr)
{
ptr->printfunc();
}
int main()
{
Derived func;
printthis(&func);
return 0;
}
virtual
表示,“这不是真正的 C 函数,即将一系列参数压入堆栈,然后跳转到函数体的单个不变地址。”
相反,是这头野兽在 运行 时间在 table 中查找要执行的函数体的地址。层次结构中的每个 class 在 table 中都有一个条目。函数指针的table被称为vtable。这是 多态性 的运行时机制,它注入额外的代码来执行此查找,然后 dispatch 到函数体的适当专用版本。
此外,当使用此 vtable 调度机制时,您总是通过指向对象的指针访问您的对象,而不是直接访问(变量或引用)它,即。 Foo* foo{makeFoo()}; foo->someMethod()
对比 Loo loo{}; loo.someMethod()
。因此,从一开始就需要另一个取消引用才能使用此技术。
这是巧妙的部分:这些指针也可以指向派生的 classes 的任何对象,因此如果您有一个继承自 [=16= 的 class FooChild
],您可以使用 FoodParent *
指向 FooParent
或 FooChild
.
当调用该方法时,它不是仅仅执行普通的 C 操作,即在堆栈上准备参数,然后跳转到 barMethod()
的主体,而是执行一堆 运行时间首先查找根据 class 个性化的 barMethod 的几个不同实现之一。那个table叫做vtable。 class 层次结构中的每个 class 在此 table 中都有一个条目,说明函数体真正用于特定 class 的位置,因为它们可以有不同的,即使我们正在使用 FooParent *
指向其中任何一个的实例。
但这就是我们首先要这样做的原因:假设 virtual
不存在。而你,程序员,想要处理一堆来自 class 层次结构的对象。好吧,您最终编写的代码几乎与编译器手动为您注入的代码相同!为了将这些不同 classes 的实例传递到您编写的一些函数中来处理它们,您需要一个单一大小的类型来使函数调用代码起作用。因此,请使用指针,因为指针在您的机器上始终具有相同的大小(如今),无论它们指向的对象大小有多么不同。好的。所以它是指针。这是使用 virtual
.
然后你需要一个 switch
语句或其他东西来在它指向的特定 class 上分支。但如果您为编写的每个变体手动编码,那就是这样。太傻了。很快您就会意识到,使用 table 指向您要调用的 barMethod()
各种版本的指针会更好。然后你总是可以从每个变体中查找相同的 table,而不是重写手写的 switch 语句等。所以你会那样做。您将实现一个 table,其中对于从 FooParent
派生的层次结构中的每个 class 都有指向不同 barMethod()
的指针。他们都有相同的签名(参数列表、return 值等),但每个 class.
您将为每个 class 分配一个整数 i.d。或类似的东西,并将其用作 table 的偏移量。例如,FooChildA
和 FooChildB
可能是两个不同的 class,它们都派生自 FooParent
,因此您可以将 A 分配给 0,将 B 分配给 1,或类似的东西.然后使用这些作为偏移量跳转到 table 并获取指针。这就是查找 table 的一般工作方式。获得指针后,您会将所有参数压入堆栈,然后跳转到该指针。所以 virtual
只是一个关键字,它指示编译器为您将所有这些疯狂的高级代码注入到您的代码中,因此您不必手动执行。
问题是,它是 RUNTIME 多态性,通常可以通过模板等使用 COMPILE 时间多态性。它给虚拟层次结构中的每个函数调用增加了很多 运行时间膨胀。这对于非热循环实际上很好。但是对于 运行 一直在您的系统中(例如每隔几毫秒或更长时间)的事情,这确实是一个无法接受的 table 膨胀量。对于绝大多数情况,您可以在编译时执行与所有 table 查找相同的操作,而不是使用元编程,这样 运行 时间可以非常快。
至于 override
,那一团混乱应该从一开始就在语言中,并且应该与 virtual
关键字处于相同的文本位置。可悲的是,这两个“应该”都没有完成。所以在过去,你会在 class 层次结构的最父级中声明 barMethod()
为 virtual
,然后还在派生的 class 中声明 barMethod()
] 为 virtual
。在某些时候,由于奇怪的错误,这变得非常烦人。老实说,该功能并不直观,并且在了解它多年后很难教授甚至记住。
所以我们添加了 override
以及对编译器的提示,以便我们可以捕获错误。它只是意味着“不仅这个函数是虚拟的,所有那些疯狂的 vtable 调度东西也是如此,而且,这是 barMethod()
的 DERIVED 重新定义,因此编译器可以检查使确保您将参数等与派生它的父 class 完美匹配,因为如果没有此检查,如果您不小心未能将派生版本的参数列表与父版本完全匹配,而不是覆盖父版本,编译器只会说,“哦,另一个全新的虚拟成员函数层次结构正在开始,具有不同的参数,这是根。必须是新的重载集。"
我意识到这是一个非常令人困惑的陈述。但基本上,如果你有 barMethod()
和 barMethod(int)
和 barMethod(int, char*)
等等,这些都是不同的功能,彼此之间没有真正的关系。好像每个人都有不同的名字。你可以在脑海中这样想。它本质上是编译器本身如何看待它,name mangling。因此,如果您随后将它们设为 virtual
,您可能会认为在层次结构中的各种 classes 中声明它们也会将它们放入单个成员函数虚拟层次结构中。但事实并非如此。如果您改为使用 override
关键字将它们设为虚拟,编译器会注意到 barMethod(int) override
和 barMethod(int, char*) override
与 FooParent
中的任何内容都没有关系,后者只有 barMethod()
没有参数。但他们应该压倒某些东西。 ¡编译器错误!那很好。您想要那个编译器错误,否则您的代码会发送给客户并且看起来可以正常工作但绝对不是。
virtual
的要点是允许您使用单个指针类型来表示 classes 的整个层次结构的任何实例,但可能对它们中的每一个做不同的事情。如果程序员不确保所有派生的重新定义也是虚拟的,那将不会发生。并且覆盖确保他们不会意外创建新的 class 层次结构根。
在现代 C++ 中,我们认为同时要求 virtual
和 override
太烦人了,而且它总是让视觉上更难 grep 哪些 barMethod()
是根版本,以及哪些是派生的。因此他们说,“您可以删除派生重新定义的 virtual
关键字,而只使用 override
。”这被认为是现在唯一正确的说话方式。
struct FooParent
{
// The root has virtual
virtual void barMethod(){ /* body */ } // or `=0` for "pure virtual"
}
// Original way of doing it. Just use virtual again, but this isn't the root now. This is a derived class.
struct FooChild_OldSchool : FooParent
{
virtual void barMethod(); // Total trashmouth. Bug prone.
}
struct FooChild_OverrideDays : FooParent
{
virtual void barMethod() override; // Naughty mouth. Using both.
}
struct FooChild_NonTrashyWay2020 : FooParent
{
void barMethod() override; // Prim and proper mouth. Using only override in the derived class.
}
奇怪的是,override
在句法上位于不同的位置,在参数列表之后,而不是之前。据我所知,这确实不合逻辑。我真的希望我们能解决这个问题,让 override
和 virtual
去同一个地方,在声明的开头,或者更好的是,让 virtual
去 [=32] 的地方=] 确实如此,在参数列表之后。就像现在一样,它令人讨厌地不一致和混乱,imo。我之所以这么说,是因为我相信如果我们不承认它们是缺点,这些东西就会变得不可教。因为当你学习一门新语言时,你真的需要一个更流利的人说,“嘿,这很奇怪而且有疣。别担心。这不是因为你笨。这只是因为我们的语言是进化的不稳定。
我希望它是这样的...
struct FooChild_HowIWishItWas : FooParent
{
override void barMethod();
}
// OR EVEN BETTER! Allow us to change the location of virtual!
struct FooParent_HowIWishItWasEvenMore
{
void barMethod() virtual;
}
但事实并非如此。不过,这也许就是您在内部思考它的方式,然后只记得在实际键入代码时在语法上添加这种奇怪的古怪行为。想知道关于这个的论文是否能存活 5 分钟。嗯。