Pull to refresh

Comments 26

Вообще-то, был такой человек по фамилии Страуструп, он давным давно схожие идеи продвигал. Не помню толком чем у него там кончилось.
Еще был такой Алан Кей, который говорил, что когда придумал термин «ООП», он точно не имел ввиду то, что там у Страуструпа получилось :-)
UFO just landed and posted this here
Не только был, но и есть!
Сейчас ему 64.
Какой смысл писать на C и хранить ссылки на методы в полях структуры? Так даже в языках высокого уровня не всегда делается, чтобы не копировать в сто миллионов объектов с двумя полями ссылки на 10 методов.
А не надо хранить в объекте указатели на функции. Надо положить все указатели в отдельную структуру (размещённую в статической памяти) и хранить в объектах указатель на эту структуру. Примерно так это и сделано в C++, и называется это vtable.
и по-моему именно таким образом в ядре linux реализованы «виртуальные методы» в С.
Не только в ядре Linux. Подобная реализация подсистемы VFS традиционна для ядер UNIX.
И не только VFS с file_operations, но все подсистемы в ядре сделано таким макаром и я не вижу чего-то плохого в этом, это стандарт уже де-факто.
Дочитал до середины, стало плохо. «Что только не выдумают, лишь бы не писать на C++». Нет, ну техники хорошие, но от осознания что все это может делать компилятор (с ровно такой же эффективностью!), становится не по себе.

upd. ну может понадобиться для тех проектов, в которые нельзя тащить плюсы ну никак. Например, linux kernel.
В Linux kernel есть kobject для этого. У них в коде ядра на C много разных парадигм и абстракций, не только объектно-ориентированная.
А вот не скажите! Как только влезаешь в reverse-engineering, так сразу всё «всплывает». И безымянные структуры внутри основной, и передача предка по указателю…
И вообще, MS-ная технология COM рулит, когда есть только отладочные символы и дизассемблер! С становится настоящим высокоуровневым дизассемблером. А С++ — синтаксическим сахаром над ним. Ну и косяки разработчиков заодно тоже выявляются (MS в OE/WinMail заюзали тот же GUID для похожего интерфейса, но с уже другими параметрами (64 vs 32 бита шириной)
Учитывая то, что GLib это основа современного Linux freedesktop мы получаем, что весь gui и окологуевый софт включая фрэймворки типа GStreamer написаны используя те же идеи, вот это поворот.
Структуры при наследовании лучше все-таки именовать, будет удобнее работать с ними в дальнейшем. Про указатели на методы вам уже сказали выше. Конструктор и деструктор объекта лучше вынести в отдельные функции — опять же, сильно поможет при наследовании.

Вот переделанный пример из вашей статьи:

#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;
}
Компилятор выдает ошибку
Ошибку выдает компоновщик, т. е. вы сначала 2 часа будете ждать пока все скомпилируется, а потом, при компоновке, вам сообщат, что вы оказывается «обратились к приватной функции» (причем шифрованным сообщением) и зря ждали 2 часа пока все скомпилируется.

Тем не менее, оставить один указатель лучше, чем хранить все данные в паблик структуре.
Не очевидное утверждение, особенно, на таком примере, в котором вы тут же в дополнение к закрытым данным делаете set и get с тривиальной функциональностью, это явно не лучше, чем просто оставить x и y доступными снаружи.

Кроме того, инкапсуляция может осуществляется как соглашение между программистами без всякой поддержки со стороны компилятора. Например, все поля, имена которых начинаются с нижнего подчеркивания лучше не трогать снаружи какого-то модуля, а если уж трогаете, то на свой страх и риск.
Инкапсуляция
подразумевает скрытие данных от разработчика. В ООП языках мы обычно скрываем поля класса, а для доступа к ним пишем сеттеры и геттеры.

Это неверно. Инкапсуляция есть скрыть деталей реализации от пользователя.
Кхм, я не в тему статьи, но мне интересно, почему вдруг ввели новые фичи со странным стилем именования: _Generic, _Bool, _Thread_local?
В Си нет нэймспейсов, соответственно для фич языка зарезервированы имена, начинающиеся с подчерка. Самому так называть переменные/структуры можно, но не рекомендуется.
Системные библиотеки поверх этих _Bool, _Generic обычно делают обёртки с более удобным именованием.
По поводу плашек к статье — насколько я знаю, не такое уж это ненормальное программирование. Описанный подход имитации работы с объектами на С (только без извращений с имитацией наследования) используется в случаи если нужно описать интерфейс бинарно совместимой библиотеки, с чем у С++, увы, врождённые проблемы. Использование API на языке С позволяет избежать издержек при вызове функционала классов, который в противном случаи пришлось бы вызывать через чисто виртуальные функции (описано, например, вот тут; работает, вроде, за счёт вхождения стандарта описания таблиц виртуальных функций в ABI).

Надеюсь, знающие люди исправят меня, если я что-то не так понимаю — не совсем уверен в том, что понимаю всё это правильно до конца…

П.С.: Код в пример чем-то андроидовское NDK и эпловый Core Foundation напомнил… Что логично, впрочем — это и есть ООП API, которое имитируется средствами С.
Минусанули. Выходит, что-то не так понимаю в бинарной совместимости библиотек С++. Был бы благодарен, если бы кто-нибудь объяснил в чём ошибка.
Взгляните, кстати, как написан FreeRADIUS. Там почти ООП на голом C.
>Инкапсуляция подразумевает скрытие данных от разработчика.
Не от, а для.

Инкапсуляция позволяет скрывать несущественные для данного уровня абстракции детали реализации, но полный запрет доступа к ним к инкапсуляции относится слабо. Просто некоторые ЯП, не будем показывать пальцем, поставили знак равенства между такой классной штукой как инкапсуляция и не такой классной (хотя, иногда, и полезной) как сокрытие.
Кстати говоря, в нашей компании основной продукт писался на С. Исторически так сложилось. Только вот наши разработчики умудрялись все это делать с помощью компилятора ANSI C, без всяких новомодных штучек. На вопрос «зачем все это?» ответ примерно такой — «Ну блин, мы же так долго парились, да и на С++ переписывать неохота»
Можно подумать, что виртуальные функции и наследование — это самая важная фича C++.
Не знаю, как обстоят дела у остальных, но лично мне нужно заводить классы с виртуальными методами довольно редко. Да и коллеги подобное делают весьма нечасто.

Удобное детерминированное управление ресурсами, обобщённое программирование, более строгая (чем в C) система типизации, большая свобода при построении zero-cost абстракций — вот особенности C++, которые меня радуют больше всего.

Когда я пишу на C, больше всего я тоскую по деструкторам.
ООП, это не поддержка тем или иным языком, это идиология разработки. И даже если язык явно не поддерживает это красивым синтаксисом это можно сделать без проблем. И это в первую очередь именно методология разработки, а не синтаксис на конкретном языке программирования. Если взять и посмотреть на код Кармака, он ООП, но на C и без подобных костылей описанных в статье. Да и мне приходилось работать с большой кучей библиотек с идиологией ООП, на C. Но всё же хранить методы в объекте, это только если замена виртуальности и ничего более.
Sign up to leave a comment.

Articles