问题描述
最近写程序时,做了一个基类供其他模块调用,调试时出现了下面的错误:
pure virtual method called
如图所示:
纯虚函数被调用。纯虚函数是需要子类具体实现的函数,怎么就被调用了呢?
首先明确观点:永远不要在构造和析构中直接/间接调用virtual函数。
下面是根据查阅的资料和自己的理解作出的解释,供参考。
抽象类和纯虚函数
来复习一下c++中的纯虚函数和抽象类。
c++使用虚函数来实现运行时多态,如:
class Shape {
public:
virtual double area() const = 0;
double value() const;
// Effective c++ 第三版之条款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;
virtual ~Rectangle();
// ...
};
class Circle : public Shape {
public:
Circle(double radius, double valuePerSquareUnit);
virtual double area() const;
virtual ~Circle();
// ...
};
double
Shape::value() const
{
// Area is computed differently, depending
// on what kind of shape the object is:
return valuePerSquareUnit_ * area();
}
Effective c++ 第三版之条款7: 为多态基类声明virtual 析构函数。
基类Shape中声明了纯虚的area()成员函数,即所谓的抽象基类,子类 Rectangle 和 Circle 分别作出了各自的实现。当虚函数被调用时,执行的具体版本取决于指针或者引用的类型。
如下代码:
print(shape->area());
可能调用 Circle::area()
,也可能调用Rectangle::area()
,取决于运行时 shape的实际类型。
虚函数表vtbl
运行时多态的实现的通常解释是使用虚函数表(virtual table),它是一个保存函数指针的数组。
具有虚函数的类都会维护一个函数指针的数组,该类的每个实例都有一个指向vtbl的指针,如下图所示:
在拥有纯虚函数的抽象类中,由于没有具体的函数实现,c++通常会把它的vtbl存放一个特殊函数,该函数会输出“Pure virtual function called”,并终止程序。
构造步骤
在构建派生类的实例时,如果它具有vtbl,过程大致为:
- 构建顶层基类:
a. 使实例指向基类的vtbl
b. 构造基类实例成员变量
c. 执行基类构造函数的主体
- 构建派生类(递归方式)
a. 使实例指向派生类的vtbl
b. 构造派生类实例成员变量
c. 执行派生类构造函数的主体
析构过程与构造过程相反,大致为:
- 析构派生类
a. (实例已经指向派生类的vtbl)
b. 执行派生类析构函数主体
c. 析构派生类实例成员变量
- 析构基类(递归)
a. 使实例指向基类的vtbl
b. 执行基类析构函数主体
c. 析构基类实例成员变量
两个常见错误
- 直接在构造函数中调用虚函数
Shape(double valuePerSquareUnit)
: valuePerSquareUnit_(valuePerSquareUnit)
{
// ERROR: 违背了Effective C++ 条款9!
std::cout << "creating shape, area = " << area() << std::endl;
}
Effective C++ 条款9:绝不在构造和析构过程中调用virtual函数。
在构造或析构中直接调用虚函数时,编译器可能会发出警告。
- 间接调用
Shape::Shape(double valuePerSquareUnit)
: valuePerSquareUnit_(valuePerSquareUnit)
{
// ERROR
std::cout << "creating shape, value = " << value() << std::endl;
}
在上述构造过程的1-c中,调用了成员函数value,value又调用了纯虚函数area,这时对象的实例还未构造,就可能会看到“Pure virtual function called”。
还有一个
考虑如下代码:
Shape* p1 = new Rectangle(width, height, valuePerSquareUnit); // 创建对象实例
std::cout << "value = " << p1->value() << std::endl; // ok
Shape* p2 = p1; // Need another copy of the pointer. 注意,为避免问题,请使用智能指针
delete p1; // 对象进行2步析构:先析构派生类,再析构基类,p1可能发生变化
std::cout << "now value = " << p2->value() << std::endl; // error, 间接指向“悬空”的指针。 这是指向已删除对象或已释放内存的指针,或两者都有
当p2指向过时对象时,结果是未定义的,它可能崩溃或继续运行,产生错误结果或者在未来某一时刻崩溃。 在实践中,有几种常见的可能性(可能一致或不一致):
- 内存可能被标记为已释放。任何访问它的尝试都将立即标记为使用了悬空指针。某些工具(BoundsChecker,Purify,valgrind和其他工具)可能会检测到这些情况。
- 内存可能被故意加密。释放后,内存管理系统可能会将类似垃圾的值写入内存。 (其中一个值是“死牛肉”:0xDEADBEEF,无符号十进制3735928559,带符号十进制-559038737。)
- 内存可能会被重用。如果在删除对象和使用悬挂指针之间执行了其他代码,则内存分配系统可能已经从旧对象使用的部分或全部内存中创建了一个新对象。如果幸运的话,这看起来像垃圾,程序立即崩溃。否则,该程序可能会在某个时间之后崩溃,可能是在凝结其他对象之后,通常是在根本原因问题发生很长时间之后。这种问题使C++程序员发疯。
- 内存可能已经完全保留了。
最后一种情况下,内存完全保留意味它是抽象基类的实例,如果为该对象调用纯虚成员函数,会发生什么?
“Pure virtual function called”
总结
根据以上分析,以下情况会导致异常:
- 从基类构造函数直接调用虚函数。
- 从基类析构函数直接调用虚函数。
- 从基类构造函数间接调用虚函数。
- 从基类析构函数间接调用虚函数。
- 通过悬空指针调用虚拟函数。
实际中编译器和程序的行为可能会因编译器类型和程序执行环境而异。
参考资料
“Pure Virtual Function Called”: An Explanation
[Effective c++ 第三版]
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/24636.html