Объектно-ориентированная система на C

Ниже представлен подробно откомментированный код объектно-ориентированной системы на C. Работающий исходный код можно скачать по адресу: https://gist.github.com/be9/03fc3444dd90d1c78f58

Система взята из книги Object-Oriented Programming With ANSI C (автор Axel-Tobias Schreiner).

 

Файл new.h

Заголовочный файл, содержащий в себе базовые операции с объектами: создание, удаление, вызов виртуальных методов.

// Стандартная преамбула для заголовочного файла, гарантирующая его однократное
// включение
#ifndef __new_h
#define __new_h

// Нужно для size_t (беззнаковое целое с размером, соответствующим разрядности
// архитектуры)
#include <stddef.h>

// Нужно для va_list (тип, на котором основывается работа с переменным
// количеством аргументов у функций)
#include <stdarg.h>

/*
 * Базовая структура, в которой хранится информация о классе. Не путать со
 * структурой объекта! Для каждого класса существует одна-единственная глобальная
 * переменная типа Class.
 */
struct Class {
    // Размер объекта описываемого класса в байтах
    size_t size;

    /*
     * Указатель на функцию-конструктор, вызываемую в процессе создания
     * объекта. В качестве первого аргумента (self) функция принимает указатель
     * на выделенную под объект память (размером size).  Второй аргумент —
     * указатель на переменную типа va_list, которая «настроена» на первый аргумент
     * конструктора. Если конструктор принимает какие-либо аргументы, он должен
     * получить их с помощью нужного количества * вызовов va_arg.
     *
     * Функция-конструктор возвращает указатель, который будет
     * являться результатом вызова new. Как правило, она просто вернёт self.
     *
     * Указатель ctor может быть нулевым, в этом случае никакого
     * дополнительного конструирования объекта не производится, а просто
     * зануляется выделенная память.
     */
    void *(*ctor)(void *self, va_list *app);

    /*
     * Указатель на функцию-деструктор. В качестве аргумента функция принимает
     * указатель на объект и осуществляет необходимые операции по очистке:
     * закрытие файлов, освобождение дополнительно выделенной памяти и т.д.
     *
     * Указатель dtor может быть нулевым, в этом случае очистка не
     * производится, а просто освобождается выделенная память.
     *
     * Деструктор должен вернуть тот указатель, который передавался в 
     * качестве аргумента конструктору (обычно это просто self).
     */
    void *(*dtor)(void *self);

    /*
     * Указатель на реализацию виртуальной функции, которая будет вызываться в
     * зависимости от реального типа объекта. В данном примере есть всего одна
     * виртуальная функция, draw. Если задача требует большего количества
     * виртуальных функций, все они должны быть добавлены сюда с соответствующими
     * прототипами.
     *
     * Первый аргумент функции (self) – указатель на объект.
     */
    void (*draw)(const void *self);
};

// Функция создания объекта. Первый аргумент — указатель на структуру-описание
// класса (типа Class). Прочие аргументы будут переданы конструктору (ctor).
// Функция возвращает указатель на созданный и проинициализированный объект.
void *new(const void *class, ...);

// Функция уничтожения объекта. Аргумент item — указатель на объект
// (который был ранее возвращен функцией new). Перед освобождением памяти
// вызывает деструктор dtor (при его наличии).
void delete(void *item);

// Виртуальная функция draw. Аргумент self — указатель на объект. В зависимости от
// класса объекта будет вызвана соответствующая реализация draw.
void draw(const void *self);

#endif

 

Файл new.c

Реализация функций из new.h

// Для calloc и free
#include <stdlib.h>

// Для assert.h
#include <assert.h>
#include "new.h"

// Реализация функции new
void *new(const void *_class, ...)
{
    // Пребразуем тип переданного указателя. Хотя он и объявлен как void *
    // (в целях сокрытия данных о классе), мы-то знаем, что это struct Class *.
    const struct Class *class = _class;

    // Выделяем память. Стандартная функция calloc обычно используется для
    // создания массивов. Её первый аргумент — размер массива, второй — размер
    // ячейки массива. После выделения соответствующего количества памяти
    // она зануляет эту память и возвращает указатель.
    void *p = calloc(1, class->size);

    // Проверка на то, что память выделилась
    assert(p);

    // Прописывание класса объекта. Все объекты в системе устроены так, что в самых
    // первых байтах памяти, выделенной под объект, содержится указатель на
    // глобальную структуру типа Class, описывающую класс этого объекта. Здесь
    // этот указатель лежит в переменной class. p как раз указывает на то место,
    // куда должен лечь class.
    *(const struct Class **)p = class;

    // Если задан конструктор...
    if (class->ctor) {
        va_list ap;

        // Инициализируем работу с переменными аргументами. После выполнения
        // va_start ap будет указывать на следующий аргумент после _class,
        // то есть первый аргумент, который должен достаться функции-конструктору.
        va_start(ap, _class);

        // Вызываем конструктор
        p = class->ctor(p, &ap);

        // Очищаем ap
        va_end(ap);
    }

    // Возвращаем указатель на объект
    return p;
}

// Реализация функции delete
void delete(void *self)
{
    // Поскольку нам потребуется доступ к описанию класса объекта,
    // сделаем удобное преобразование типов
    const struct Class **cp = self;

    // Проверка деструктора. *cp получает указатель на struct Class,
    // из которой достаём поле dtor
    if (self && *cp && (*cp)->dtor)
        // Если деструктор задан, вызываем его. Он должен вернуть указатель
        // на исходный блок памяти
        self = (*cp)->dtor(self);

    // Освобождаем выделенный блок памяти
    free(self);
}

// Реализация функции draw
void draw(const void *self)
{
    // Поскольку нам потребуется доступ к описанию класса объекта,
    // сделаем удобное преобразование типов
    const struct Class * const *cp = self;

    // Проверяем, что объект, описание класса и указатель на реализацию виртуального
    // метода являются ненулевыми
    assert(self && *cp && (*cp)->draw);

    // Вызываем виртуальный метод по указателю
    (*cp)->draw(self);
}

 

Файл point.h

Объявление структуры данных и функций для класса Point

// Стандартная преамбула для заголовочного файла, гарантирующая его однократное
// включение
#ifndef __point_h
#define __point_h

// Структура объекта класса Point
struct Point {
    // Указатель на описание структуры класса (см. комментарии внутри функции
    // new)
    const void *class;

    // Координаты точки
    int x, y;
};

// Невиртуальный метод класса. Смещает точку в 2D-пространстве.
void move(void *_self, int dx, int dy);

// Ссылка на указатель на описание класса Point, объявленный в point.c.
extern const void *Point;

#endif

 

Файл point.c

Реализация конструктора и виртуального метода draw, невиртуального метода move, структура-описание класса.

// Для va_arg
#include <stdarg.h>
#include <stdlib.h>
// Для printf
#include <stdio.h>
#include "point.h"
// Для struct Class
#include "new.h"

// Реализация функции move
void move(void * _self, int dx, int dy)
{
    // Функция будет работать для объекта класса Point и объектов производных
    // классов, поскольку у всех них есть поля x и y, но дополнительного
    // «интеллекта», зависящего от действительного класса объекта, у неё нет.
    struct Point *self = _self;

    // Выполняем смещение
    self->x += dx;
    self->y += dy;
}

// Реализация конструктора для класса Point. Поскольку конструктор является
// деталью реализации и не будет вызываться извне напрямую (только по
// указателю), он объявлен как static.
static void *Point_ctor(void *_self, va_list *app)
{
    // В выделенную память ляжет структура Point
    struct Point *self = _self;

    // Достаём 2-й аргумент new и сохраняем в поле x
    self->x = va_arg(*app, int);
    // Достаём 3-й аргумент new и сохраняем в поле y
    self->y = va_arg(*app, int);

    // Возвращаем указатель на объект
    return self;
}

// Реализация виртуального метода draw для класса Point. Виртуальный метод
// также не предназначен для прямого вызова, поэтому объявляется с ключевым
// словом static.
static void Point_draw(const void *_self)
{
    // Мы знаем, что то, что нам передали, это указатель на Point
    const struct Point *self = _self;

    // Печать координат
    printf("\".\" at %d,%d\n", self->x, self->y);
}

// Описание класса Point. Эта переменная будет находиться в глобальной памяти,
// но будет недоступна извне (static)
static const struct Class _Point = {
    // Размер структуры данных объекта
    sizeof(struct Point),
    // Указатель на функцию-конструктор
    Point_ctor,
    // Деструктор отсутствует
    0,
    // Указатель на виртуальную функцию draw
    Point_draw
};

// Объявление глобального указателя на _Point, который и будет представлять
// собой класс Point для пользователей (служить аргументом для функции new).
const void *Point = &_Point;

 

Файл circle.h

Объявления для класса Circle

// Стандартная преамбула для заголовочного файла, гарантирующая его однократное
// включение
#ifndef __circle_h
#define __circle_h

// Для struct Point
#include "point.h"

// Структура объекта класса Circle
struct Circle {
    // Поскольку Circle наследуется от Point, он содержит все те поля, которые
    // есть в Point
    const struct Point _;

    // Поле, специфичное для Circle – радиус круга
    int rad;
};

// Ссылка на указатель на описание класса Circle, объявленный в circle.c.
extern const void *Circle;

#endif

 

Файл circle.c

Реализация конструктора и виртуальной функции draw для класса Circle. Описание класса Circle.

// Для printf
#include <stdio.h>
#include "circle.h"
// Для struct Class
#include "new.h"

// Объявление конструктора для Circle
static void *Circle_ctor(void *_self, va_list *app) {
    // Поскольку Circle унаследован от Point, первым делом мы
    // вызываем конструктор класса Point, передавая ему наши аргументы.
    // То, что он вернёт, можно преобразовать к указателю на Circle,
    // потому что выделенная память имеет размер sizeof (struct Circle).
    struct Circle *self = ((const struct Class *)Point)->ctor(_self, app);

    // Конструктор Point считал два аргумента и сохранил их в поля x и y.
    // Мы же считаем третий аргумент и сохраним его в поле rad
    self->rad = va_arg(*app, int);

    // Возвращаем указатель на объект
    return self;
}

// Эти два макроопределения удобны для доступа к полям базового класса Point
// Преобразуем p в указатель на struct Point и достаём соответствующее поле
#define x(p) (((const struct Point *)(p)) -> x)
#define y(p) (((const struct Point *)(p)) -> y)

// Реализация виртуальной функции draw для класса Circle
static void Circle_draw(const void * _self)
{
    // Мы знаем, что то, что нам передали, это указатель на Circle
    const struct Circle *self = _self;

    // Выводим поля. Для доступа к полям x и y используются макроопределения,
    // поскольку мы не можем напрямую написать self->x и self->y
    // (ведь они объявлены в struct Point, а не в struct Circle)
    printf("circle at %d,%d rad %d\n", x(self), y(self), self->rad);
}

// Описание класса Circle
static const struct Class _Circle = {
    // Размер структуры данных объекта
    sizeof(struct Circle),
    // Указатель на функцию-конструктор
    Circle_ctor,
    // Деструктор отсутствует
    0,
    // Указатель на виртуальную функцию draw
    Circle_draw
};

// Объявление глобального указателя на _Circle, который и будет представлять
// собой класс Circle для пользователей (служить аргументом для функции new).
const void *Circle = &_Circle;

Файл main.c

Главная программа, которая создаёт и уничтожает объекты классов Point и Circle.

Программа вызывается с аргументами, соответствующими классам (p и c).

// Для Point
#include "point.h"
// Для Circle
#include "circle.h"
// Для new.h
#include "new.h"

int main(int argc, char **argv)
{
    // Достаём аргументы командной строки по одному
    while (*++argv) {
        // Указатель на текущий объект
        void *p;

        // Анализируем первый символ
        switch (**argv) {
        case 'p':
            // Создаем объект класса Point
            p = new(Point, 1, 2);
            break;
        case 'c':
            // Создаем объект класса Circle
            p = new(Circle, 1, 2, 3);
            break;
        default:
            // Неизвестный аргумент, пропускаем
            continue;
        }

        // Отрисовка начального местоположения объекта
        draw(p);
        // Сдвиг объекта
        move(p, 10, 20);
        // Отрисовка нового местоположения объекта
        draw(p);
        // Уничтожение объекта
        delete(p);
    }

    return 0;
}