虚函数指针混淆

virtual function pointer confusion

正如我们所知,我们可以使用指向基 class 的指针来访问派生 class 中基 class 的重写虚函数。

下面是这样的例子。

#include <iostream>

class shape {
public:
    virtual void draw() {
    std::cout << "calling shape::draw()\n";
    }
};

class square : public shape {
public:
    virtual void draw() {
        std::cout << "calling square::draw()\n";
    }
    int area() {
        return width*width;
    }
    square(int w) {
        width = w;
    }
    square() {
        width = 0;
    }

protected:
    int width;
};

class rect : public square {
public:
    virtual void draw() {
        std::cout << "calling rect::draw()\n";
    }

    int area() {
        return width*height;
    }
    rect(int h, int w) {
        height = h;
        width = w;
    }
protected:
    int height;
};

int main() {
    /*
    shape* pshape[3] = {
        new shape,
        new square(2),
        new rect(2, 3)
        };

    for (int i = 0; i<3; i++){
        pshape[i]->draw();
    }
    */
    square* psquare = new rect(2, 3);
    psquare->draw();
    system("pause");
    return 0;
}

pshape[i]指针可以轻松访问虚函数draw()。 现在是令人困惑的部分。 "square" class 是 "rect" class 的基础 class。因此,如果有一个 square* 指针,它可以访问 "rect" class(square* psquare = new rect(2, 3);draw() 函数]) ,输出为:

calling rect::draw()
Press any key to continue . . .

现在,如果我从 square::draw() 定义中删除 'virtual' 关键字,它的代码仍然可以编译并且输出是相同的:

calling rect::draw()
Press any key to continue . . .

最后,如果我从基本函数中删除 'virtual',psquare->draw() 的输出是:

calling square::draw()
Press any key to continue . . .

这就是让我困惑的地方。这里到底发生了什么?

  1. 由于 squarerect class 的父级,square 应该将其 draw() 函数设为虚函数,以便让 rect 覆盖它。但它仍在编译并提供与虚拟化时相同的输出。
  2. 因为shape是所有的基础class,删除shape中draw()的virtual关键字应该会导致错误。但这并没有发生,它正在编译并在调用 psquare->draw() 时给出另一个输出 calling square::draw()

我可能在很多事情上都错了。请更正错误并告诉我这里到底发生了什么。

如果函数在基 class 中声明为虚函数,则它在所有派生的 class 中自动为虚函数,就像您在其中放置 virtual 关键字一样。参见 C++ "virtual" keyword for functions in derived classes. Is it necessary?

如果函数不是虚拟的,那么调用哪个版本将取决于调用它的指针的类型。在父 class 中调用成员函数绝对没问题,因为派生 class 的每个实例都是其每个父 class 的实例。所以那里没有错误。

首先考虑编译器看到的是什么。对于给定的指针,它向上观察层次结构,如果它看到相同的方法符合 virtual 则发出动态运行时调用。如果他看不到 virtual,则发出的调用对应于函数的 "lowest" 定义,直至当前类型(或从该类型向上找到的第一个定义)。 因此如果你有(前提是你有Square *p = new Rectangle()):

Shape { virtual draw() }
Square : Shape { virtual draw() }
Rectangle : Square { virtual draw() }

一切都清晰,始终是虚拟的。

如果你有:

Shape { virtual draw() }
Square : Shape { draw() }
Rectangle : Square { virtual draw() }

然后编译器发现 Shape 的绘图是虚拟的,然后调用将是动态的,Rectangle::draw 将被调用。

如果你有:

Shape { draw() }
Square : Shape { draw() }
Rectangle : Square { virtual draw() }

然后编译器发现 Shape 的绘制是非虚拟的,然后调用将是静态的并且将调用 Shape::draw(或者 Base::draw() 未定义)。

在层次结构中为同一功能混合虚拟和非虚拟的情况下,可能会发生最糟糕的事情......您通常应该避免混合。

总结

当您创建一个静态对象并调用其中一个函数时,您会得到该函数的 class' 版本。编译器在编译时知道class对象是什么。

当您创建对象的指针或引用并调用非虚函数时,编译器仍然假定它在编译时知道类型。如果引用实际上是一个 subclass,你可能会得到函数的 superclass' 版本。

当您创建对象的指针或引用并调用虚函数时,编译器将在运行时间查找对象的实际类型,并调用特定于它的函数版本。一个非常重要的例子是,如果你想通过基class指针销毁一个对象,析构函数必须在基class中是虚的,否则你不会调用正确版本的析构函数。通常,这意味着 subclass 分配的任何动态内存都不会被释放。

例子

也许考虑虚拟 classes 的实际用途会有所帮助。添加关键字并不是为了它自己!让我们稍微改变一下示例,以处理圆形、椭圆形和形状。

这里有很多样板文件,但请记住:椭圆是与两个焦点的平均距离的点集,而圆是焦点相同的椭圆。

#include <cmath>
#include <cstdlib>
#include <iostream>

using std::cout;

struct point {
  double x;
  double y;

  point(void) : x(0.0), y(0.0) {}
  point( const point& p ) : x(p.x), y(p.y) {}
  point( double a, double b ) : x(a), y(b) {}
};

double dist( const point& p, const point& q )
// Cartesian distance.
{
  const double dx = p.x - q.x;
  const double dy = p.y - q.y;

  return sqrt( dx*dx + dy*dy );
}

std::ostream& operator<< ( std::ostream& os, const point& p )
// Prints a point in the form "(1.4,2)".
{
  return os << '(' << p.x << ',' << p.y << ')';
}

class shape
{
  public:
    virtual bool is_inside( const point& p ) const = 0; // Pure virtual.
    
  protected:
    // Derived classes need to be able to call the default constructor, but no
    // actual objects of this class may be created.
    shape() { cout << "Created some kind of shape.\n"; }
    // Destructors of any superclass that might get extended should be virtual
    // in any real-world case, or any future subclass with a nontrivial
    // destructor will break:
    virtual ~shape() {}
};

// We can provide a default implementation for a pure virtual function!
bool shape::is_inside( const point& _ ) const
{
  cout << "By default, we'll say not inside.\n";
  return false;
}

class ellipse : public shape {
  public:
    ellipse( const point& p1, const point& p2, double avg_dist )
    : f1(p1), f2(p2), d(avg_dist)
    {
      cout << "Ellipse created with focuses " << f1 << " and " << f2
           << " and average distance " << d << " from them.\n";
    }

    bool is_inside( const point& p ) const
    {
      const double d1 = dist( p, f1 ), d2 = dist( p, f2 );
      const bool inside = d1+d2 <= d*2;
      
      cout << p << " has distance " << d1 << " from " << f1 << " and "
           << d2 << " from " << f2 << ", whose average is "
           << (inside ? "less than " : "not less than ") << d << ".\n";
      return inside;
    }

  protected: // Not part of the public interface, but circle needs these.
    point f1;  // The first focus.  For a circle, this is the center.
    point f2;  // The other focus.  For a circle, this is the center, as well.
    double d;  // The average distance to both focuses.  The radius of a circle.
};

class circle : public ellipse {
  public:
    circle( const point& center, double r )
    : ellipse( center, center, r )
    {
      cout << "Created a circle with center " << center << " and radius " << r
           << ".\n";
    }

    // Override the generic ellipse boundary test with a more efficient version:
    bool is_inside( const point& p ) const
    {
      const double d1 = dist(p, f1);
      const bool inside = d1 <= d;
  
      cout << p << " has distance " << d1 << " from " << f1 << ", which is "
           << (inside ? "less than " : "not less than ") << d << ".\n";
      return inside;
    }
};

int main(void)
{
  const circle c = circle( point(1,1), 1 );
  const shape& s = c;
  
  // These call circle::is_inside():
  c.is_inside(point(0,0));
  s.is_inside(point(0.5, 1.5));
  dynamic_cast<const ellipse&>(s).is_inside(point(0.5,0.5));

  // Call with static binding:
  static_cast<const ellipse>(c).is_inside(point(0,0));
  // Explicitly call the base class function statically:
  c.ellipse::is_inside(point(0.5,0.5));
  // Explicitly call the ellipse version dynamically:
  dynamic_cast<const ellipse&>(s).ellipse::is_inside(point(0.5,0.5));

  return EXIT_SUCCESS;
}

这个输出是:

Created some kind of shape.
Ellipse created with focuses (1,1) and (1,1) and average distance 1 from them.
Created a circle with center (1,1) and radius 1.
(0,0) has distance 1.41421 from (1,1), which is not less than 1.
(0.5,1.5) has distance 0.707107 from (1,1), which is less than 1.
(0.5,0.5) has distance 0.707107 from (1,1), which is less than 1.
(0,0) has distance 1.41421 from (1,1) and 1.41421 from (1,1), whose average is not less than 1.
(0.5,0.5) has distance 0.707107 from (1,1) and 0.707107 from (1,1), whose average is less than 1.
(0.5,0.5) has distance 0.707107 from (1,1) and 0.707107 from (1,1), whose average is less than 1.

您可能想考虑一下如何将其扩展到三个维度,将笛卡尔坐标更改为极坐标,添加一些其他类型的形状(例如三角形),或者将椭圆的内部表示更改为中心和轴.

旁注:

设计者经常认为,因为一个圆可以由一个点和一个距离定义,一个椭圆可以由两个点和一个距离定义,所以 ellipse class 应该是一个女儿 [= circle 的 51=] 为第二个焦点添加了一个成员。这是一个错误,不仅仅是出于象牙塔的数学迂腐。

如果您对椭圆采用任何算法(例如使用圆锥曲线的方程快速计算其与直线的交点)并且 运行 它在圆上,它仍然有效。这里我们以利用圆只有一个焦点的知识,查找一个点是否在圆内的速度提高一倍的例子

但是反过来不行!如果 ellipse 继承自 circle 而你试图 draw_fancy_circle(ellipse(f1, f2, d)); 你会得到一个比你想画的椭圆大的圆。因为省略号违反了圆的约定 class,任何假定圆确实是圆的代码都会悄无声息地出错,直到您重新编写它们。这违背了能够编写一堆代码来处理任何类型的对象并重新使用它的意义。

square class 与 rectangle 之间的关系留作 reader 的练习。