【问题标题】:virtual function pointer confusion虚函数指针混淆
【发布时间】:2017-07-25 03:23:59
【问题描述】:

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

下面是一个例子。

#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”类是“rect”类的基类。因此,如果有一个 square* 指针,它可以访问 "rect" 类的 draw() 函数(square* psquare = new rect(2, 3);) ,输出为:

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

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

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

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

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

这让我很困惑。这里到底发生了什么?

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

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

【问题讨论】:

  • 你的类层次结构是错误的。矩形是长和宽恰好相同的正方形。但是你的类层次结构说矩形是正方形,事实并非如此。
  • 最好发布一个minimal reproducible example,准确地显示您感到困惑的行为。
  • @DavidSchwartz 其实两者都不是,见square-rectangle-problem
  • "square" class is the base class of "rect" class 这是一个非常普遍的设计错误,但它仍然是一个错误。
  • '覆盖'。不是“覆盖”。

标签: c++ pointers inheritance polymorphism virtual


【解决方案1】:

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

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

【讨论】:

    【解决方案2】:

    首先考虑编译器看到的内容。对于给定的指针,它向上观察层次结构,如果它看到与virtual 相同的方法,则发出动态运行时调用。如果他看不到virtual,则发出的调用是对应于当前类型的函数“最低”定义(或从该类型向上找到的第一个定义)的调用。 因此,如果你有(前提是你有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的draw是虚拟的,那么调用就会是动态的,就会调用Rectangle::draw。

    如果你有:

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

    然后编译器看到 Shape 的 draw 是非虚拟的,那么调用将是静态的并且 Shape::draw 将被调用(或者 Base::draw() 没有定义)。

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

    【讨论】:

      【解决方案3】:

      总结

      当您创建一个静态对象并调用其函数之一时,您将获得该类的函数版本。编译器在编译时就知道对象是什么类。

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

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

      示例

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

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

      #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 类应该是 circle 的子类,它添加了一个成员第二个重点。这是一个错误,不仅仅是出于象牙塔的数学学究。

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

      但这不能反过来!如果ellipse 继承自circle 而你尝试draw_fancy_circle(ellipse(f1, f2, d)); 你会得到一个比你想画的椭圆更大的圆。因为椭圆违反了 circle 类的约定,所以任何假设圆是真正圆的代码都会默默地出错,直到你重新编写它们。这违背了编写一堆适用于任何类型对象的代码并重用它的意义。

      square 类与rectangle 之间的关系的含义留给读者作为练习。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-01-25
        • 2010-09-25
        • 2011-09-04
        • 2016-11-08
        相关资源
        最近更新 更多