Псевдо ООП в C



    Язык Си не является объектно-ориентированным языком. И значит все что будет описано ниже это костыли и велосипеды.
    ООП включает в себя три столпа: инкапсуляция, наследование, полиморфизм. Ниже я покажу как этих вещей можно добиться в С.

    Инкапсуляция
    подразумевает скрытие данных от разработчика. В ООП языках мы обычно скрываем поля класса, а для доступа к ним пишем сеттеры и геттеры. Для скрытия данных в Си, существует ключевое слово 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
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 26
    • +28
      Вообще-то, был такой человек по фамилии Страуструп, он давным давно схожие идеи продвигал. Не помню толком чем у него там кончилось.
      • +20
        Еще был такой Алан Кей, который говорил, что когда придумал термин «ООП», он точно не имел ввиду то, что там у Страуструпа получилось :-)
        • НЛО прилетело и опубликовало эту надпись здесь
          • +1
            Не только был, но и есть!
            Сейчас ему 64.
          • +3
            Какой смысл писать на C и хранить ссылки на методы в полях структуры? Так даже в языках высокого уровня не всегда делается, чтобы не копировать в сто миллионов объектов с двумя полями ссылки на 10 методов.
            • +7
              А не надо хранить в объекте указатели на функции. Надо положить все указатели в отдельную структуру (размещённую в статической памяти) и хранить в объектах указатель на эту структуру. Примерно так это и сделано в C++, и называется это vtable.
              • 0
                и по-моему именно таким образом в ядре linux реализованы «виртуальные методы» в С.
                • 0
                  Не только в ядре Linux. Подобная реализация подсистемы VFS традиционна для ядер UNIX.
                  • 0
                    И не только VFS с file_operations, но все подсистемы в ядре сделано таким макаром и я не вижу чего-то плохого в этом, это стандарт уже де-факто.
            • +10
              Всё это уже давно написано, причём существенно лучше: Object-Oriented Programming With ANSI-C

              Пример того, как подобные идеи выглядят на практике — GObject из GLib.
              • +3
                Дочитал до середины, стало плохо. «Что только не выдумают, лишь бы не писать на C++». Нет, ну техники хорошие, но от осознания что все это может делать компилятор (с ровно такой же эффективностью!), становится не по себе.

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

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

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

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

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

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

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

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

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

                                    Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.