Last month, I explained how C++ compilers typically implement virtual functions by illustrating how using virtual functions affects the storage layout for objects (Setting storage layout for polymorphic objects). This month, I'll continue by showing how to implement virtual functions in C in a way that generates machine code very similar to what you get from C++.
As before, my sample classes represent an assortment of two-dimensional geometric shapes such as circle, rectangle, and triangle, all derived from a common base class called shape. In C++, the definition for the base class shape looks like:
class shape {
public:
shape(); // constructor
virtual double area() const;
virtual double perimeter() const;
private:
coordinates position;
color outline, fill;
};
The area and perimeter member functions are virtual. A class, such as shape, with a least one virtual function is a polymorphic type. C++ compilers typically add a hidden pointer to each polymorphic type. That pointer is commonly called a vptr and it points to a table of function pointers called a vtbl.
You can implement a polymorphic shape type in C using the following declarations:
// shape.h—base class for shapes
#ifndef SHAPE_H_INCLUDED
#define SHAPE_H_INCLUDED
typedef struct shape shape;
typedef struct shape_vtbl shape_vtbl;
struct shape_vtbl {
double (*area)(shape const *s);
double (*perimeter)(shape const *s);
};
struct shape {
shape_vtbl *vptr;
coordinates position;
color outline, fill;
};
#endif
A class derived from a polymorphic base class will be polymorphic as well, and it inherits the base class's vptr. The vptr must have the same offset in the base class subobject (the base class portion) of a derived class object as it does in a base class object.
In C++, the definition for a circle class derived from shape looks like:
class circle: public shape {
public:
circle(double r); // constructor
virtual double area() const;
virtual double perimeter() const;
private:
double radius;
};
In C, the declarations for a polymorphic circle type look like:
// circle.h – circle derived from shape
#ifndef CIRCLE_H_INCLUDED
#define CIRCLE_H_INCLUDED
#include "shape.h"
typedef struct circle circle;
struct circle {
shape base; // the base class sub-object
double radius;
};
void circle_construct(circle *c, double r);
#endif
The base member of the circle structure above includes all the members inherited from the shape base class, including vptr. The circle_construct function initializes a circle, including its vptr. I'll cover the details of initializing the vptr in an upcoming column.
As I showed last month, the C++ definition for a rectangle class derived from shape looks a lot like the definition for circle:
class rectangle: public shape {
public:
rectangle(double h, double w);
virtual double area() const;
virtual double perimeter() const;
private:
double height, width;
};
Similarly, the C declarations for a polymorphic rectangle type look a lot like the declarations for circle:
// rectangle.h—rectangle interface
#ifndef RECTANGLE_H_INCLUDED
#define RECTANGLE_H_INCLUDED
#include "shape.h"
typedef struct rectangle rectangle;
struct rectangle {
shape base; // the base class sub-object
double height, width;
};
void rectangle_construct(rectangle *t, double h, double w);
#endif
In C++, a call to the virtual area function applied to a shape looks exactly like a non-virtual call, as in:
shape *s;
~~~
s->area();
If s points to a circle (the dynamic type of *s is circle), then the call above calls circle::area. If s points to a rectangle, then the call above calls rectangle::area.
C doesn't provide explicit support for classes with member functions. In C, you simply use ordinary functions to emulate member functions. For example:
circle_area(&c);
applies the circle_area function to circle c. You get the same runtime performance as a C++ member function call, but without any compile-time checking to enforce access control.
In C, virtual function calls look unlike any other kind of function call. For example, a call to the virtual area function applied to a shape looks like:
shape *s;
~~~
s->vptr->area(s);
In this case, if s points to a circle (the dynamic type of *s is circle), then the call above calls circle_area. If s points to a rectangle, then the call above calls rectangle_area.
As I hinted earlier, this works only if you initialise the vptr properly, something I'll cover in an upcoming column.
文章评论(0条评论)
登录后参与讨论