简体   繁体   中英

virtual function pointer confusion

As we know that we can use a pointer to a base class to access the overridden virtual functions of the base class in the derived classes.

The following is an example of such.

#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;
}

The pshape[i] pointer can easily access the virtual function draw() . Now for the confusing part. "square" class is the base class of "rect" class. Hence if there's a square* pointer it can access the draw() function of "rect" class( square* psquare = new rect(2, 3); ) , and the output is:

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

Now if I remove the 'virtual' keyword from the square::draw() definition, it code still compiles and the output is the same:

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

Finally if I remove 'virtual' from the base function, the output of psquare->draw() is:

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

This is what confuses me. What exactly is happening here?

  1. Since square is parent of rect class, square should have its draw() function to be virtual in order to let rect override it. But it still is compiling and giving the same output as when it is virtualized.
  2. Since shape is the base class of all, removing the virtual keyword of draw() in shape should result in error. But that's not happening, it is compiling and giving another output calling square::draw() when psquare->draw() is called.

I may be wrong on so many things. Please correct what's wrong and tell me what exactly is going on here.

If a function is declared virtual in a base class, it is automatically virtual in all derived classes, just as if you put a virtual keyword in there. See C++ "virtual" keyword for functions in derived classes. Is it necessary? .

If the function is not virtual, then which version gets called will depend on the type of the pointer on which you call it. It is absolutely fine to call a member function in a parent class, since each instance of a derived class is an instance of each of its parent classes. So no error there.

Think first about what the compiler sees. For a given pointer, it observes upward the hierarchy, and if it see that same method qualified as virtual then a dynamic runtime call is emitted. If he cannot see virtual then the emitted call is the one that correspond to the "lowest" definition of the function up to the current type (or the first definition found going upward from the type). Thus if you have (provided that you have Square *p = new Rectangle() ) :

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

everything is clear, always virtual.

If you have:

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

then compiler sees that Shape's draw is virtual, then the call will be dynamic and the Rectangle::draw will be called.

If you have:

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

then compiler sees that Shape's draw is non virtual, then the call will be static and the Shape::draw will be called (or Base::draw() is not defined).

Worst things could happens in the case of mixing virtual non-virtual for the same function in a hierarchy... You should normally avoid mixing.

Summary

When you create a static object and call one of its functions, you get that class' version of the function. The compiler knows what class the object is at compile time.

When you create a pointer or reference to an object and call a non-virtual function, the compiler still assumes it knows the type at compile time. If the reference is actually to a subclass, you might get the superclass' version of the function.

When you create a pointer or reference to an object and call a virtual function, the compiler will look up the actual type of the object at runtime , and call the version of the function specific to it. One very important example is that if you want to destroy an object through a base class pointer, the destructor must be virtual in the base class, or else you will not call the correct version of the destructor. Frequently, that would mean any dynamic memory the subclass allocates would not be freed.

Example

Maybe it would help to think about the practical purpose of virtual classes. The keyword wasn't added just for its own sake! Let's change the example slightly, to deal with circles, ellipses and shapes.

There's a lot of boilerplate in here, but just remember: an ellipse is the set of points an average distance from both its focuses, and a circle is an ellipse whose focuses are the same.

#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;
}

The output of this is:

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.

You might want to consider for a moment how you would extend this to three dimensions, change from Cartesian to polar coordinates, add some other kind of shape such as triangles, or change the internal representation of ellipses to center and axes.

A side note:

Designers frequently think that, because a circle can be defined by a point and a distance, and an ellipse by two points and a distance, the ellipse class should be a daughter class of circle that adds a member for the second focus. This is a mistake, and not just out of ivory-tower mathematical pedantry.

If you take any algorithm for an ellipse (such as quickly computing its intersection with a line by using the equation for conic sections) and run it on a circle, it will still work. Here, we used the example of looking up whether a point is inside the circle twice as fast by using the knowledge that a circle has only one focus.

But this does not work in reverse! If ellipse inherits from circle and you tried to draw_fancy_circle(ellipse(f1, f2, d)); you would get a circle bigger than the ellipse you wanted to draw. Because ellipses violate the contract of the circle class, any code that assumes circles are really circles will silently bug out until you re-write them. That defeats the point of being able to write a bunch of code that will work on any type of object, and re-use it.

The implications of what relationship a square class should have to a rectangle is left as an exercise to the reader.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM