Ниже представлен подробно откомментированный код объектно-ориентированной системы на C. Работающий исходный код можно скачать по адресу: https://gist.github.com/be9/03fc3444dd90d1c78f58
Система взята из книги Object-Oriented Programming With ANSI C (автор Axel-Tobias Schreiner).
Заголовочный файл, содержащий в себе базовые операции с объектами: создание, удаление, вызов виртуальных методов.
// Стандартная преамбула для заголовочного файла, гарантирующая его однократное
// включение
#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.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
// Стандартная преамбула для заголовочного файла, гарантирующая его однократное
// включение
#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
Реализация конструктора и виртуального метода 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
// Стандартная преамбула для заголовочного файла, гарантирующая его однократное
// включение
#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
Реализация конструктора и виртуальной функции 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;
Главная программа, которая создаёт и уничтожает объекты классов 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;
}