C++ Virtual/Pure Virtual Explained
如果一个函数被定义为虚函数,并且与纯虚函数相同,那么它究竟意味着什么?
从维基百科的虚拟功能…
In object-oriented programming, in languages such as C++, and Object Pascal, a virtual function or virtual method is an inheritable and overridable function or method for which dynamic dispatch is facilitated. This concept is an important part of the (runtime) polymorphism portion of object-oriented programming (OOP). In short, a virtual function defines a target function to be executed, but the target might not be known at compile time.
与非虚拟函数不同,当重写虚拟函数时,最派生的版本将用于类层次结构的所有级别,而不仅仅是创建它的级别。因此,如果基类的一个方法调用虚拟方法,则将使用派生类中定义的版本,而不是基类中定义的版本。
这与非虚拟函数不同,后者仍然可以在派生类中被重写,但"新"版本将仅由派生类及其以下的类使用,但根本不会更改基类的功能。
然而…
A pure virtual function or pure virtual method is a virtual function that is required to be implemented by a derived class if the derived class is not abstract.
当存在纯虚拟方法时,类是"抽象的",不能单独实例化。相反,必须使用实现纯虚拟方法的派生类。纯虚拟根本没有在基类中定义,因此派生类必须定义它,或者派生类也是抽象的,不能实例化。只能实例化没有抽象方法的类。
虚拟提供了一种重写基类功能的方法,纯虚拟需要它。
我想评论一下维基百科对虚拟的定义,这里有几个人重复了这个定义。[在写这个答案的时候,]维基百科定义了一个虚拟方法,它可以在子类中被重写。[幸运的是,维基百科从那时起就被编辑过,现在它正确地解释了这一点。]这是不正确的:任何方法,不仅仅是虚拟方法,都可以在子类中被重写。虚拟所做的是为您提供多态性,即在运行时选择方法的最派生重写的能力。
考虑以下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | #include <iostream> using namespace std; class Base { public: void NonVirtual() { cout <<"Base NonVirtual called. "; } virtual void Virtual() { cout <<"Base Virtual called. "; } }; class Derived : public Base { public: void NonVirtual() { cout <<"Derived NonVirtual called. "; } void Virtual() { cout <<"Derived Virtual called. "; } }; int main() { Base* bBase = new Base(); Base* bDerived = new Derived(); bBase->NonVirtual(); bBase->Virtual(); bDerived->NonVirtual(); bDerived->Virtual(); } |
这个程序的输出是什么?
1 2 3 4 | Base NonVirtual called. Base Virtual called. Base NonVirtual called. Derived Virtual called. |
派生重写基的每个方法:不仅是虚拟方法,而且是非虚拟方法。
我们看到,当您有一个指向派生(bDrived)的基指针时,调用非虚调用基类实现。这是在编译时解决的:编译器发现bDrived是一个基*,而非虚拟的不是虚拟的,所以它在类基上进行解析。
但是,调用virtual会调用派生类实现。由于关键字virtual,方法的选择发生在运行时,而不是编译时。在编译时,这里发生的是编译器发现这是一个基*,并且它正在调用一个虚拟方法,因此它插入对vtable的调用,而不是类基的调用。这个vtable在运行时被实例化,因此运行时解析为最派生的覆盖。
我希望这不会太令人困惑。简而言之,任何方法都可以被重写,但只有虚拟方法才能提供多态性,也就是说,最派生的重写的运行时选择。然而,在实践中,重写一个非虚拟方法被认为是不好的实践,而且很少使用,所以很多人(包括写维基百科文章的人)认为只有虚拟方法可以被重写。
虚拟关键字赋予C++支持多态性的能力。当您有指向某个类的对象的指针时,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | class Animal { public: virtual int GetNumberOfLegs() = 0; }; class Duck : public Animal { public: int GetNumberOfLegs() { return 2; } }; class Horse : public Animal { public: int GetNumberOfLegs() { return 4; } }; void SomeFunction(Animal * pAnimal) { cout << pAnimal->GetNumberOfLegs(); } |
在这个(愚蠢的)示例中,getNumberOfLegs()函数根据调用对象的类返回适当的数字。
现在,考虑函数"somefunction"。它不关心传递给它的是什么类型的动物对象,只要它是从动物衍生出来的。编译器将自动将任何动物派生类强制转换为动物,因为它是一个基类。
如果我们这样做:
1 2 | Duck d; SomeFunction(&d); |
它会输出"2"。如果我们这样做:
1 2 | Horse h; SomeFunction(&h); |
它会输出"4"。我们不能这样做:
1 2 | Animal a; SomeFunction(&a); |
因为它不会编译,因为getNumberOfLegs()虚拟函数是纯的,这意味着它必须通过派生类(子类)来实现。
纯虚拟函数主要用于定义:
a)抽象类
这些是必须从中派生并实现纯虚拟函数的基类。
b)界面
这些是"空"类,其中所有函数都是纯虚拟的,因此您必须派生并实现所有函数。
在C++类中,Virtual是指定它的关键字,一个方法可以重写(即由一个子类实现)。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | class Shape { public: Shape(); virtual ~Shape(); std::string getName() // not overridable { return m_name; } void setName( const std::string& name ) // not overridable { m_name = name; } protected: virtual void initShape() // overridable { setName("Generic Shape"); } private: std::string m_name; }; |
在这种情况下,子类可以覆盖initshape函数来执行一些专门的工作:
1 2 3 4 5 6 7 8 9 10 11 12 | class Square : public Shape { public: Square(); virtual ~Square(); protected: virtual void initShape() // override the Shape::initShape function { setName("Square"); } } |
术语pure virtual是指需要由子类实现但尚未由基类实现的虚拟函数。通过使用virtual关键字并在方法声明末尾添加a=0,可以将方法指定为纯virtual。
因此,如果要使shape::initshape纯虚拟化,请执行以下操作:
1 2 3 4 5 6 | class Shape { ... virtual void initShape() = 0; // pure virtual method ... }; |
通过向类中添加纯虚方法,可以使类成为抽象基类。这对于分离接口和实现非常方便。
"virtual"表示该方法可以在子类中被重写,但在基类中有一个可直接调用的实现。纯虚拟"意味着它是一个没有直接可调用实现的虚拟方法。在继承层次结构中,此类方法必须至少重写一次——如果某个类有任何未实现的虚拟方法,则无法构造该类的对象,编译将失败。
@Quark指出纯虚拟方法可以有一个实现,但由于纯虚拟方法必须被重写,因此不能直接调用默认实现。下面是一个带有默认值的纯虚拟方法的示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | #include <cstdio> class A { public: virtual void Hello() = 0; }; void A::Hello() { printf("A::Hello "); } class B : public A { public: void Hello() { printf("B::Hello "); A::Hello(); } }; int main() { /* Prints: B::Hello A::Hello */ B b; b.Hello(); return 0; } |
根据注释,编译是否会失败是特定于编译器的。至少在GCC4.3.3中,它不会编译:
1 2 3 4 5 6 7 8 9 10 | class A { public: virtual void Hello() = 0; }; int main() { A a; return 0; } |
输出:
1 2 3 4 5 | $ g++ -c virt.cpp virt.cpp: In function ‘int main()’: virt.cpp:8: error: cannot declare variable ‘a’ to be of abstract type ‘A’ virt.cpp:1: note: because the following virtual functions are pure within ‘A’: virt.cpp:3: note: virtual void A::Hello() |
虚拟关键字是如何工作的?
假设人是一个基础阶级,印第安人是从人而来的。
1 2 3 4 5 6 7 8 9 10 11 12 13 | Class Man { public: virtual void do_work() {} } Class Indian : public Man { public: void do_work() {} } |
将do_work()声明为virtual简单意味着:只在运行时确定要调用哪个do_work()。
假设我这样做了,
1 2 3 | Man *man; man = new Indian(); man->do_work(); // Indian's do work is only called. |
如果不使用virtual,编译器将静态地确定或静态地绑定它,具体取决于调用的对象。因此,如果一个对象调用do_work(),那么即使它指向一个印度对象,也会调用man的do_work()。
我认为最热门的答案是误导性的——任何方法,无论虚方法是否可以在派生类中具有重写的实现。具体参考C++,正确的区别是运行时(当使用虚拟时)绑定和编译时间(当未使用虚拟但方法被重写时,基指针指向派生对象)绑定相关函数。
似乎还有另一个误导性的评论说,
"Justin, 'pure virtual' is just a term (not a keyword, see my answer
below) used to mean"this function cannot be implemented by the base
class."
这是错的!纯虚拟函数也可以有一个主体,并且可以实现!事实是抽象类的纯虚函数可以静态调用!两位非常好的作者是bjarne stroustrup和stan lippman….因为他们写的语言。
Simula,C++,和C.*,默认使用静态方法绑定,程序员可以指定特定的方法应该使用动态绑定,将它们标记为虚拟的。动态方法绑定是面向对象编程的核心。
面向对象编程需要三个基本概念:封装、继承和动态方法绑定。
Encapsulation allows the implementation details of an
abstraction to be hidden behind a
simple interface.Inheritance allows a new abstraction to be defined as an
extension or refinement of some
existing abstraction, obtaining some
or all of its characteristics
automatically.Dynamic method binding allows the new abstraction to display its new
behavior even when used in a context
that expects the old abstraction.
虚拟方法可以由派生类重写,但需要在基类中实现(将被重写的实现)
纯虚拟方法没有基类的实现。它们需要由派生类定义。(所以从技术上讲,被覆盖不是正确的术语,因为没有什么可以覆盖)。
当派生类重写基类的方法时,虚拟对应于默认的Java行为。
纯虚方法对应于抽象类中抽象方法的行为。只包含纯虚拟方法和常量的类将是接口的CPP悬挂式按钮盒。
虚函数是在基类中声明并由派生类重新定义的成员函数。虚函数是按继承顺序分层的。当派生类不重写虚函数时,将使用其基类中定义的函数。
纯虚函数是一个不包含相对于基类的定义的函数。它在基类中没有实现。任何派生类都必须重写此函数。
"虚函数或虚方法是一个函数或方法,其行为可以在继承类中被具有相同签名的函数重写"-维基百科
对于虚拟函数来说,这不是一个很好的解释。因为,即使成员不是虚拟的,继承类也可以覆盖它。你可以自己试试看。
当一个函数以一个基类作为参数时,差异就显现出来了。当您将继承类作为输入时,该函数使用重写函数的基类实现。但是,如果该函数是虚拟的,则使用在派生类中实现的函数。
纯虚函数
试试这个代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #include <iostream> using namespace std; class aClassWithPureVirtualFunction { public: virtual void sayHellow()=0; }; class anotherClass:aClassWithPureVirtualFunction { public: void sayHellow() { cout<<"hellow World"; } }; int main() { //aClassWithPureVirtualFunction virtualObject; /* This not possible to create object of a class that contain pure virtual function */ anotherClass object; object.sayHellow(); } |
在类AnotherClass中,移除sayhellow函数并运行代码。你会出错的!因为当一个类包含一个纯虚函数时,不能从该类创建任何对象,并且它是继承的,那么它的派生类必须实现该函数。
虚函数
尝试其他代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #include <iostream> using namespace std; class aClassWithPureVirtualFunction { public: virtual void sayHellow() { cout<<"from base "; } }; class anotherClass:public aClassWithPureVirtualFunction { public: void sayHellow() { cout<<"from derived "; } }; int main() { aClassWithPureVirtualFunction *baseObject=new aClassWithPureVirtualFunction; baseObject->sayHellow();///call base one baseObject=new anotherClass; baseObject->sayHellow();////call the derived one! } |
这里,sayhellow函数在基类中被标记为虚函数。它表示试图在派生类中搜索函数并实现该函数的编译器。如果找不到,则执行基函数。谢谢。
虚拟函数必须在基类和派生类中都有一个定义,但不是必需的,例如toString()或toString()函数是虚拟的,因此您可以通过在用户定义的类中重写它来提供自己的实现。
虚函数在普通类中声明和定义。
纯虚拟函数必须声明为以"=0"结尾,并且只能在抽象类中声明。
具有纯虚拟函数的抽象类不能具有该纯虚拟函数的定义,因此它意味着必须在派生自该抽象类的类中提供实现。