
Язык Си не является объектно-ориентированным языком. И значит все что будет описано ниже это костыли и велосипеды.
ООП включает в себя три столпа: инкапсуляция, наследование, полиморфизм. Ниже я покажу как этих вещей можно добиться в С.
Инкапсуляция
подразумевает скрытие данных от разработчика. В ООП языках мы обычно скрываем поля класса, а для доступа к ним пишем сеттеры и геттеры. Для скрытия данных в Си, существует ключевое слово static, которое, помимо других своих назначений, ограничивает видимость переменной (функции, структуры) одним файлом.
Пример:
//foo.c
static void foo1 () {
puts("foo1");
}
void foo2 () {
puts("foo2");
}
//main.c
#include <stdio.h>
int main() {
foo1();
foo2();
return 0;
}
Компилятор выдает ошибку
[main.c:(.text+0x1b): undefined reference to `foo1'
collect2.exe: error: ld returned 1 exit status]
Имея такую возможность, можно разделить public и private данные по разным файлам, а в структуре хранить только указатель на приватные данные. Понадобится две структуры: одна приватная, а вторая с методами для работы и указателем на приватную. Чтобы вызывать функции на объекте, договоримся первым параметром передавать указатель на структуру, которая ее вызывает.
Объявим структуру с сеттерами, геттерами и указателем на приватное поле, а также функции, которые будут создавать структуру и удалять.
//point2d.h
typedef struct point2D {
void *prvtPoint2D;
int (*getX) (struct point2D*);
void (*setX)(struct point2D*, int);
//...
} point2D;
point2D* newPoint2D();
void deletePoint2D(point2D*);
Здесь будет инициализироваться приватное поле и указатели на функции, чтобы с этой структурой можно было работать.
//point2d.c
#include <stdlib.h>
#include "point2d.h"
typedef struct private {
int x;
int y;
} private;
static int getx(struct point2D*p) {
return ((struct private*)(p->prvtPoint2D))->x;
}
static void setx(struct point2D *p, int val) {
((struct private*)(p->prvtPoint2D))->x = val;
}
point2D* newPoint2D() {
point2D* ptr;
ptr = (point2D*) malloc(sizeof(point2D));
ptr -> prvtPoint2D = malloc(sizeof(private));
ptr -> getX = &getx;
ptr -> setX = &setx;
// ....
return ptr;
}
Теперь, работа с этой структурой, может осуществляться с помощью сеттеров и геттеров.
// main.c
#include <stdio.h>
#include "point2d.h"
int main() {
point2D *point = newPoint2D();
int p = point->getX(point);
point->setX(point, 42);
p = point->getX(point);
printf("p = %d\n", p);
deletePoint2D(point);
return 0;
}
Как было показано выше, в «конструкторе» создаются две структуры, и работа с private полями ведется через функции. Конечно этот вариант не идеальный хотя бы потому, что никто не застрахован от присвоения приватной структуре null-указателя. Тем не менее, оставить один указатель лучше, чем хранить все данные в паблик структуре.
Наследование
как механизм языка не предусмотрено, поэтому тут без костылей никак не обойтись. Решение, которое приходит в голову — это просто объявить структуру внутри структуры. Но для того чтобы иметь возможность обращаться к ее полям напрямую, в C11 есть возможность объявлять анонимные структуры. Их поддерживает как gcc, так и компилятор от microsoft. Выглядит это вот так.
typedef struct point2D {
int x,y;
}
typedef struct point3D {
struct point2D;
int z;
} point3D;
#include <stdio.h>
#include "point3d.h"
int main() {
point3D *point = newPoint3D();
int p = point->x;
printf("p = %d\n", p);
return 0;
}
Компилировать надо с флагом -fms-extensions. Таким образом, становится возможным доступ к полям структуры в обход ее имени.
Но надо понимать, что анонимными могут быть только структуры и перечисления, но мы не можем объявлять анонимными примитивные типы данных.
Полиморфизм
В языках программирования и теории типов полиморфизмом называется способность функции обрабатывать данные разных типов. И такую возможность предоставляет ключевое слово _Generic, которое было введено в С11. Но стоит оговориться, что не все версии gcc его поддерживают. В _Generic передаются пары тип-значение, а при компиляции они транслируются в нужное значение. В общем, лучше один раз увидеть.
Создадим «функцию», которая будет определять тип структуры, переданной в нее, и возвращать ее имя в виде строки.
//points.h
#define typename(x) _Generic((x), point3D : "point3D", point2D : "point2D", point3D * : "pointer to point3D", point2D * : "pointer to point2D" )
//main.c
int main() {
point3D *point = newPoint3D();
puts(typename(point));
return 0;
}
Здесь видно, что в зависимости от типа данных будет возвращаться разное значение. А раз _Generic возвращает какое-то значение, так почему бы ему не вернуть указатель на функцию, тогда можно заставить одну и ту же «функцию» работать с разными типами данных.
//points.h
double do2D(point2D *p);
double do3D(point3D *p);
#define doSomething(X) _Generic((X), point3D* : do3D, point2D* : do2D ) (X)
//main.c
int main() {
point3D *point = newPoint3D();
printf("d = %f\n", doSomething(point));
return 0;
}
Теперь одну и туже функцию можно использовать с разными структурами.
Статьи по теме:
habrahabr.ru/post/205570
habrahabr.ru/post/154811
Комментарии (26)
eterevsky
26.07.2015 09:42+3Какой смысл писать на C и хранить ссылки на методы в полях структуры? Так даже в языках высокого уровня не всегда делается, чтобы не копировать в сто миллионов объектов с двумя полями ссылки на 10 методов.
roman_kashitsyn
26.07.2015 10:49+7А не надо хранить в объекте указатели на функции. Надо положить все указатели в отдельную структуру (размещённую в статической памяти) и хранить в объектах указатель на эту структуру. Примерно так это и сделано в C++, и называется это vtable.
mapron
26.07.2015 11:13и по-моему именно таким образом в ядре linux реализованы «виртуальные методы» в С.
roman_kashitsyn
26.07.2015 16:09Не только в ядре Linux. Подобная реализация подсистемы VFS традиционна для ядер UNIX.
Ilya_Smelykh
26.07.2015 22:27И не только VFS с file_operations, но все подсистемы в ядре сделано таким макаром и я не вижу чего-то плохого в этом, это стандарт уже де-факто.
roman_kashitsyn
26.07.2015 10:57+10Всё это уже давно написано, причём существенно лучше: Object-Oriented Programming With ANSI-C
Пример того, как подобные идеи выглядят на практике — GObject из GLib.mapron
26.07.2015 11:18+3Дочитал до середины, стало плохо. «Что только не выдумают, лишь бы не писать на C++». Нет, ну техники хорошие, но от осознания что все это может делать компилятор (с ровно такой же эффективностью!), становится не по себе.
upd. ну может понадобиться для тех проектов, в которые нельзя тащить плюсы ну никак. Например, linux kernel.grafmishurov
26.07.2015 20:27В Linux kernel есть kobject для этого. У них в коде ядра на C много разных парадигм и абстракций, не только объектно-ориентированная.
klirichek
26.07.2015 21:16+1А вот не скажите! Как только влезаешь в reverse-engineering, так сразу всё «всплывает». И безымянные структуры внутри основной, и передача предка по указателю…
И вообще, MS-ная технология COM рулит, когда есть только отладочные символы и дизассемблер! С становится настоящим высокоуровневым дизассемблером. А С++ — синтаксическим сахаром над ним. Ну и косяки разработчиков заодно тоже выявляются (MS в OE/WinMail заюзали тот же GUID для похожего интерфейса, но с уже другими параметрами (64 vs 32 бита шириной)
Ilya_Smelykh
26.07.2015 22:30Учитывая то, что GLib это основа современного Linux freedesktop мы получаем, что весь gui и окологуевый софт включая фрэймворки типа GStreamer написаны используя те же идеи, вот это поворот.
kafeman
26.07.2015 11:58+7Структуры при наследовании лучше все-таки именовать, будет удобнее работать с ними в дальнейшем. Про указатели на методы вам уже сказали выше. Конструктор и деструктор объекта лучше вынести в отдельные функции — опять же, сильно поможет при наследовании.
Вот переделанный пример из вашей статьи:
#include <stdio.h> #include <stdlib.h> typedef struct _Point2DPrivate { int x; int y; } Point2DPrivate; typedef struct _Point2D { Point2DPrivate *private; } Point2D; void Point2D_Constructor(Point2D *point) { point->private = malloc(sizeof(Point2DPrivate)); point->private->x = 0; point->private->y = 0; } void Point2D_Destructor(Point2D *point) { free(point->private); } void Point2D_SetX(Point2D *point, int x) { point->private->x = x; } int Point2D_GetX(Point2D *point) { return point->private->x; } void Point2D_SetY(Point2D *point, int y) { point->private->y = y; } int Point2D_GetY(Point2D *point) { return point->private->y; } Point2D *Point2D_New(void) { Point2D *point = malloc(sizeof(Point2D)); Point2D_Constructor(point); /* <-- Вызываем конструктор */ return point; } void Point2D_Delete(Point2D *point) { Point2D_Destructor(point); /* <-- Вызываем деструктор */ free(point); } typedef struct _Point3DPrivate { int z; } Point3DPrivate; typedef struct _Point3D { Point2D parent; Point3DPrivate *private; } Point3D; void Point3D_Constructor(Point3D *point) { Point2D_Constructor(&point->parent); /* <-- Вызываем родительский конструктор! */ point->private = malloc(sizeof(Point3DPrivate)); point->private->z = 0; } void Point3D_Destructor(Point3D *point) { Point2D_Destructor(&point->parent); /* <-- Вызываем родительский деструктор! */ free(point->private); } void Point3D_SetZ(Point3D *point, int z) { point->private->z = z; } int Point3D_GetZ(Point3D *point) { return point->private->z; } Point3D *Point3D_New(void) { Point3D *point = malloc(sizeof(Point3D)); Point3D_Constructor(point); /* <-- Вызываем конструктор */ return point; } void Point3D_Delete(Point3D *point) { Point3D_Destructor(point); /* <-- Вызываем деструктор */ free(point); } int main(int argc, char **argv) { /* Создаем экземпляр класса Point3D */ Point3D *point = Point3D_New(); /* Устанавливаем x и y координаты */ Point2D_SetX((Point2D*)point, 10); Point2D_SetY((Point2D*)point, 15); /* Теперь z координата */ Point3D_SetZ(point, 20); /* Должно вывести: x = 10, y = 15, z = 20 */ printf("x = %d, y = %d, z = %d\n", Point2D_GetX((Point2D*)point), Point2D_GetY((Point2D*)point), Point3D_GetZ(point)); /* Удаляем объект */ Point3D_Delete(point); return 0; }
kmu1990
26.07.2015 13:46Компилятор выдает ошибку
Ошибку выдает компоновщик, т. е. вы сначала 2 часа будете ждать пока все скомпилируется, а потом, при компоновке, вам сообщат, что вы оказывается «обратились к приватной функции» (причем шифрованным сообщением) и зря ждали 2 часа пока все скомпилируется.
Тем не менее, оставить один указатель лучше, чем хранить все данные в паблик структуре.
Не очевидное утверждение, особенно, на таком примере, в котором вы тут же в дополнение к закрытым данным делаете set и get с тривиальной функциональностью, это явно не лучше, чем просто оставить x и y доступными снаружи.
Кроме того, инкапсуляция может осуществляется как соглашение между программистами без всякой поддержки со стороны компилятора. Например, все поля, имена которых начинаются с нижнего подчеркивания лучше не трогать снаружи какого-то модуля, а если уж трогаете, то на свой страх и риск.
Elfet
26.07.2015 17:00+1Инкапсуляция
подразумевает скрытие данных от разработчика. В ООП языках мы обычно скрываем поля класса, а для доступа к ним пишем сеттеры и геттеры.
Это неверно. Инкапсуляция есть скрыть деталей реализации от пользователя.
Door
26.07.2015 21:58Кхм, я не в тему статьи, но мне интересно, почему вдруг ввели новые фичи со странным стилем именования:
_Generic
,_Bool
,_Thread_local
?JIghtuse
27.07.2015 06:20В Си нет нэймспейсов, соответственно для фич языка зарезервированы имена, начинающиеся с подчерка. Самому так называть переменные/структуры можно, но не рекомендуется.
Системные библиотеки поверх этих _Bool, _Generic обычно делают обёртки с более удобным именованием.
semenyakinVS
27.07.2015 02:35+1По поводу плашек к статье — насколько я знаю, не такое уж это ненормальное программирование. Описанный подход имитации работы с объектами на С (только без извращений с имитацией наследования) используется в случаи если нужно описать интерфейс бинарно совместимой библиотеки, с чем у С++, увы, врождённые проблемы. Использование API на языке С позволяет избежать издержек при вызове функционала классов, который в противном случаи пришлось бы вызывать через чисто виртуальные функции (описано, например, вот тут; работает, вроде, за счёт вхождения стандарта описания таблиц виртуальных функций в ABI).
Надеюсь, знающие люди исправят меня, если я что-то не так понимаю — не совсем уверен в том, что понимаю всё это правильно до конца…
П.С.: Код в пример чем-то андроидовское NDK и эпловый Core Foundation напомнил… Что логично, впрочем — это и есть ООП API, которое имитируется средствами С.semenyakinVS
27.07.2015 18:30+1Минусанули. Выходит, что-то не так понимаю в бинарной совместимости библиотек С++. Был бы благодарен, если бы кто-нибудь объяснил в чём ошибка.
Tiendil
27.07.2015 12:43+1>Инкапсуляция подразумевает скрытие данных от разработчика.
Не от, а для.
Инкапсуляция позволяет скрывать несущественные для данного уровня абстракции детали реализации, но полный запрет доступа к ним к инкапсуляции относится слабо. Просто некоторые ЯП, не будем показывать пальцем, поставили знак равенства между такой классной штукой как инкапсуляция и не такой классной (хотя, иногда, и полезной) как сокрытие.
Morozov_5F
27.07.2015 14:50Кстати говоря, в нашей компании основной продукт писался на С. Исторически так сложилось. Только вот наши разработчики умудрялись все это делать с помощью компилятора ANSI C, без всяких новомодных штучек. На вопрос «зачем все это?» ответ примерно такой — «Ну блин, мы же так долго парились, да и на С++ переписывать неохота»
roman_kashitsyn
27.07.2015 17:04+1Можно подумать, что виртуальные функции и наследование — это самая важная фича C++.
Не знаю, как обстоят дела у остальных, но лично мне нужно заводить классы с виртуальными методами довольно редко. Да и коллеги подобное делают весьма нечасто.
Удобное детерминированное управление ресурсами, обобщённое программирование, более строгая (чем в C) система типизации, большая свобода при построении zero-cost абстракций — вот особенности C++, которые меня радуют больше всего.
Когда я пишу на C, больше всего я тоскую по деструкторам.
AxisPod
28.07.2015 08:49+5ООП, это не поддержка тем или иным языком, это идиология разработки. И даже если язык явно не поддерживает это красивым синтаксисом это можно сделать без проблем. И это в первую очередь именно методология разработки, а не синтаксис на конкретном языке программирования. Если взять и посмотреть на код Кармака, он ООП, но на C и без подобных костылей описанных в статье. Да и мне приходилось работать с большой кучей библиотек с идиологией ООП, на C. Но всё же хранить методы в объекте, это только если замена виртуальности и ничего более.
degs
Вообще-то, был такой человек по фамилии Страуструп, он давным давно схожие идеи продвигал. Не помню толком чем у него там кончилось.
symbix
Еще был такой Алан Кей, который говорил, что когда придумал термин «ООП», он точно не имел ввиду то, что там у Страуструпа получилось :-)
StrangerInRed
Ага, был такой парень Брэд Кокс, который тоже решил запилить свой язык. Так вот, то что выше, намного более похоже на его детище.
Bombus
Не только был, но и есть!
Сейчас ему 64.