本文引入作者Paul S. R. Chisholm关于Pure Virtual Function Called问题的深入见解,要说明的是翻译部分对原文进行了删减,很遗憾的去除了作者一些幽默风格的解说,直截了当地说明问题。

面向对象 C++: 程序员的观点
    在C++中,虚函数允许关联类实例在运行时拥有不同的行为(也称作运行时多态):
class Shape {
public:
virtual double area() const;
double value() const;
// Meyers 3rd Item 7:
virtual ~Shape();
protected:
Shape(double valuePerSquareUnit);
private:
double valuePerSquareUnit_;
};
class Rectangle : public Shape {
public:
Rectangle(double width, double height, double valuePerSquareUnit);
virtual double area() const;
// Meyers 3rd Item 7:
virtual ~Rectangle();
// ...
};
class Circle : public Shape {
public:
Circle(double radius, double valuePerSquareUnit);
virtual double area() const;
// Meyers 3rd Item 7:
virtual ~Circle();
// ...
};
double
Shape::value() const
{
// Area is computed differently, depending
// on what kind of shape the object is:
return valuePerSquareUnit_ * area();
}

在 C++中:

(1)一个函数接口通过函数声明来指定。

(2)成员函数声明在类定义中。

(3)一个函数的实现在函数的定义部分指定。衍生类可以根据自己的需要重新定义一个函数,并给出它的实现。

(4)当一个虚函数被调用时,其具体实现是根据运行时中对象的动态类型而不是该对象指针或引用的静态类型来选择。
print(shape->area()); // Might invoke Circle::area() or Rectangle::area().
在基类中一个 纯虚函数只有声明而没有定义。具有纯虚函数的类是抽象类,并且无法创建实例。它的衍生类必须重新定义所有继承而来的纯虚函数。
class AbstractShape {
public:
virtual double area() const = 0;
double value() const;
// Meyers 3rd Item 7:
virtual ~AbstractShape();
protected:
AbstractShape(double valuePerSquareUnit);
private:
double valuePerSquareUnit_;
protected:
AbstractShape(double valuePerSquareUnit);
private:
double valuePerSquareUnit_;
};
// Circle and Rectangle are derived from AbstractShape.
// This will not compile, even if there's a matching public constructor:
// AbstractShape* p = new AbstractShape(value);
// These are okay:
Rectangle* pr = new Rectangle(height, weight, value);
Circle* pc = new Circle(radius, value);
// These are okay, too:
AbstractShape* p = pr;
p = pc;

面向对象C++: 表面下隐藏的东西
How does all this run time magic happen? The usual implementation is, every class with any virtual functions has an array of function pointers, called a "vtbl". Every instance of such as class has a pointer to its class's vtbl, as depicted below. 
Figure 1. A class's vtbl points to the class's instance member functions. 
If an abstract class with a pure virtual function doesn't define the function, what goes in the corresponding place in the vtbl? Traditionally, C++ implementors have provided a special function, which prints "Pure virtual function called" (or words to that effect), and then crashes the program.


Figure 2. An abstract class's vtbl can have a pointer to a special function. 
Build 'em Up, Tear 'em Down
When you construct an instance of a derived class, what happens, exactly? If the class has a vtbl, the process goes something like the following.



Step 1: Construct the top-level base part:.
Make the instance point to the base class's vtbl.
Construct the base class instance member variables.
Execute the body of the base class constructor.
Step 2: Construct the derived part(s) (recursively): 
Make the instance point to the derived class's vtbl.
Construct the derived class instance member variables.
Execute the body of the derived class constructor.

Destruction happens in reverse order, something like this:

Step 1: Destruct the derived part:
(The instance already points to the derived class's vtbl.)
Execute the body of the derived class destructor.
Destruct the derived class instance member variables.
Step 2: Destruct the base part(s) (recursively): 
Make the instance point to the base class's vtbl.
Execute the body of the base class destructor.
Destruct the base class instance member variables.

Two of the Classic Blunders

What if you try to call a virtual function from a base class constructor?
// From sample program 1:
AbstractShape(double valuePerSquareUnit)
: valuePerSquareUnit_(valuePerSquareUnit)
{
// ERROR: Violation of Meyers 3rd Item 9!
std::cout << "creating shape, area = " << area() << std::endl;
}
(Meyers, 3rd edition, Item 9: "Never call virtual functions during construction or destruction.") 
This is obviously an attempt to call a pure virtual function. The compiler could alert us to this problem, and some compilers do. If a base class destructor calls a pure virtual function directly (sample program 2), you have essentially the same situation. 
If the situation is a little more complicated, the error will be less obvious (and the compiler is less likely to help us): 
// From sample program 3:
AbstractShape::AbstractShape(double valuePerSquareUnit)
: valuePerSquareUnit_(valuePerSquareUnit)
{
// ERROR: Indirect violation of Meyers 3rd Item 9!
std::cout << "creating shape, value = " << value() << std::endl;
}

The body of this base class constructor is in step 1(c) of the construction process described above, which calls a instance member function (value()), which in turn calls a pure virtual function (area()). The object is still an AbstractShape at this point. What happens when it tries to call the pure virtual function? Your program likely crashes with a message similar to, "Pure virtual function called."

Similarly, calling a virtual function indirectly from a base class destructor (sample program 4) results in the same kind of crash. The same goes for passing a partially-constructed (or partially-destructed) object to any function that invokes virtual functions.

These are the most commonly described root causes of the "Pure Virtual Function Called" message. They're straightforward to diagnose from postmortem debugging; the stack trace will point clearly to the problem.
Pointing Out Blame

There's at least one other problem that can lead to this message, which doesn't seem to be explicitly described anywhere in print or on the net. (There have been some discussions on the ACE mailing list that touch upon the problem but they don't go into detail.)

Consider the following (buggy) code:
// From sample program 5:
AbstractShape* p1 = new Rectangle(width, height, valuePerSquareUnit);
std::cout << "value = " << p1->value() << std::endl;
AbstractShape* p2 = p1; // Need another copy of the pointer.
delete p1;
std::cout << "now value = " << p2->value() << std::endl;

Let's consider these lines one at a time.
AbstractShape* p1 = new Rectangle(width, height, valuePerSquareUnit);

A new object is created. It's constructed in two stages: Step 1, where the object acts like a base class instance, and Step 2, where it acts like a derived class instance.
std::cout << "value = " << p1->value() << std::endl;

Everything's working fine.
AbstractShape* p2 = p1; // Need another copy of the pointer.

Something odd might happen to p1, so let's make a copy of it.
delete p1;

The object is destructed in two stages: Step 1, where the object acts like a derived class instance, and Step 2, where it acts like a base class instance.

Note that the value of p1 might change after the call to delete. Compilers are allowed to "zero out" (i.e., render unusable) pointers after destructing their pointed-to data. Lucky (?) for us, we have another copy of the pointer, p2, which didn't change.
std::cout << "now value = " << p2->value() << std::endl;

Uh oh.

This is another classic blunder: going indirect on a "dangling" pointer. That's a pointer to an object that's been deleted, or memory that's been freed, or both. C++ programmers never write such code ... unless they're clueless (unlikely) or rushed (all too likely).

So now p2 points to an ex-object. What does that thing look like? According to the C++ standard, it's "undefined". That's a technical term that means, in theory, anything can happen: the program can crash, or keep running but generate garbage results, or send Bjarne Stroustrup e-mail saying how ugly you are and how funny your mother dresses you. You can't depend on anything; the behavior might vary from compiler to compiler, or machine to machine, or run to run. In practice, there are several common possibilities (which may or may not happen consistently):
The memory might be marked as deallocated. Any attempt to access it would immediately be flagged as the use of a dangling pointer. That's what some tools (BoundsChecker, Purify, valgrind, and others) try to do. As we'll see, the Common Language Runtime (CLR) from Microsoft's .NET Framework, and Sun Studio 11's dbx debugger, work this way.
The memory might be deliberately scrambled. The memory management system might write garbage-like values into the memory after it's freed. (One such value is "dead beef": 0xDEADBEEF, unsigned decimal 3735928559, signed decimal -559038737.)
The memory might be reused. If other code was executed between the deletion of the object and the use of dangling pointer, the memory allocation system might have created a new object out of some or all of the memory used by the old object. If you're lucky, this will look enough like garbage that the program will crash immediately. Otherwise the program will likely crash sometime later, possibly after curdling other objects, often long after the root cause problem occurred. This is the kind of problem that drives C++ programmers crazy (and makes Java programmers overly smug).
The memory might have been left exactly the way it was.

The last is an interesting case. What was the object "exactly the way it was"? In this case, it was an instance of the abstract base class; certainly that's the way the vtbl was left. What happens if we try to call a pure virtual member function for such an object?

"Pure virtual function called".

(Exercise for the reader: Imagine a function that, unwisely and unfortunately, returned a pointer or reference to a local variable. This is a different kind of dangling pointer. How could this also generate this message?)
Meanwhile, Back in the Real World

Nice theory. What happens in practice?

Consider five test programs, each with its own distinctive defect:
Directly calling a virtual function from a base class constructor.
Directly calling a virtual function from a base class destructor.
Indirectly calling a virtual function from a base class constructor.
Indirectly calling a virtual function from a base class destructor.
Calling a virtual function via a dangling pointer.

Scott Meyers’ home page:
http://www.aristeia.com/
About the Author

 posted on 2010-06-18 14:36  chao_yu  阅读(1158)  评论(0编辑  收藏  举报