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

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


  1. degs
    26.07.2015 03:14
    +28

    Вообще-то, был такой человек по фамилии Страуструп, он давным давно схожие идеи продвигал. Не помню толком чем у него там кончилось.


    1. symbix
      26.07.2015 04:56
      +20

      Еще был такой Алан Кей, который говорил, что когда придумал термин «ООП», он точно не имел ввиду то, что там у Страуструпа получилось :-)


    1. StrangerInRed
      26.07.2015 08:50
      +1

      Ага, был такой парень Брэд Кокс, который тоже решил запилить свой язык. Так вот, то что выше, намного более похоже на его детище.


    1. Bombus
      26.07.2015 09:53
      +1

      Не только был, но и есть!
      Сейчас ему 64.


  1. eterevsky
    26.07.2015 09:42
    +3

    Какой смысл писать на C и хранить ссылки на методы в полях структуры? Так даже в языках высокого уровня не всегда делается, чтобы не копировать в сто миллионов объектов с двумя полями ссылки на 10 методов.


    1. roman_kashitsyn
      26.07.2015 10:49
      +7

      А не надо хранить в объекте указатели на функции. Надо положить все указатели в отдельную структуру (размещённую в статической памяти) и хранить в объектах указатель на эту структуру. Примерно так это и сделано в C++, и называется это vtable.


      1. mapron
        26.07.2015 11:13

        и по-моему именно таким образом в ядре linux реализованы «виртуальные методы» в С.


        1. roman_kashitsyn
          26.07.2015 16:09

          Не только в ядре Linux. Подобная реализация подсистемы VFS традиционна для ядер UNIX.


          1. Ilya_Smelykh
            26.07.2015 22:27

            И не только VFS с file_operations, но все подсистемы в ядре сделано таким макаром и я не вижу чего-то плохого в этом, это стандарт уже де-факто.


  1. roman_kashitsyn
    26.07.2015 10:57
    +10

    Всё это уже давно написано, причём существенно лучше: Object-Oriented Programming With ANSI-C

    Пример того, как подобные идеи выглядят на практике — GObject из GLib.


    1. mapron
      26.07.2015 11:18
      +3

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

      upd. ну может понадобиться для тех проектов, в которые нельзя тащить плюсы ну никак. Например, linux kernel.


      1. grafmishurov
        26.07.2015 20:27

        В Linux kernel есть kobject для этого. У них в коде ядра на C много разных парадигм и абстракций, не только объектно-ориентированная.


      1. klirichek
        26.07.2015 21:16
        +1

        А вот не скажите! Как только влезаешь в reverse-engineering, так сразу всё «всплывает». И безымянные структуры внутри основной, и передача предка по указателю…
        И вообще, MS-ная технология COM рулит, когда есть только отладочные символы и дизассемблер! С становится настоящим высокоуровневым дизассемблером. А С++ — синтаксическим сахаром над ним. Ну и косяки разработчиков заодно тоже выявляются (MS в OE/WinMail заюзали тот же GUID для похожего интерфейса, но с уже другими параметрами (64 vs 32 бита шириной)


    1. Ilya_Smelykh
      26.07.2015 22:30

      Учитывая то, что GLib это основа современного Linux freedesktop мы получаем, что весь gui и окологуевый софт включая фрэймворки типа GStreamer написаны используя те же идеи, вот это поворот.


  1. 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;
    }
    


  1. kmu1990
    26.07.2015 13:46

    Компилятор выдает ошибку
    Ошибку выдает компоновщик, т. е. вы сначала 2 часа будете ждать пока все скомпилируется, а потом, при компоновке, вам сообщат, что вы оказывается «обратились к приватной функции» (причем шифрованным сообщением) и зря ждали 2 часа пока все скомпилируется.

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

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


  1. Elfet
    26.07.2015 17:00
    +1

    Инкапсуляция
    подразумевает скрытие данных от разработчика. В ООП языках мы обычно скрываем поля класса, а для доступа к ним пишем сеттеры и геттеры.

    Это неверно. Инкапсуляция есть скрыть деталей реализации от пользователя.


  1. Door
    26.07.2015 21:58

    Кхм, я не в тему статьи, но мне интересно, почему вдруг ввели новые фичи со странным стилем именования: _Generic, _Bool, _Thread_local?


    1. JIghtuse
      27.07.2015 06:20

      В Си нет нэймспейсов, соответственно для фич языка зарезервированы имена, начинающиеся с подчерка. Самому так называть переменные/структуры можно, но не рекомендуется.
      Системные библиотеки поверх этих _Bool, _Generic обычно делают обёртки с более удобным именованием.


  1. semenyakinVS
    27.07.2015 02:35
    +1

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

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

    П.С.: Код в пример чем-то андроидовское NDK и эпловый Core Foundation напомнил… Что логично, впрочем — это и есть ООП API, которое имитируется средствами С.


    1. semenyakinVS
      27.07.2015 18:30
      +1

      Минусанули. Выходит, что-то не так понимаю в бинарной совместимости библиотек С++. Был бы благодарен, если бы кто-нибудь объяснил в чём ошибка.


  1. alecv
    27.07.2015 12:39

    Взгляните, кстати, как написан FreeRADIUS. Там почти ООП на голом C.


  1. Tiendil
    27.07.2015 12:43
    +1

    >Инкапсуляция подразумевает скрытие данных от разработчика.
    Не от, а для.

    Инкапсуляция позволяет скрывать несущественные для данного уровня абстракции детали реализации, но полный запрет доступа к ним к инкапсуляции относится слабо. Просто некоторые ЯП, не будем показывать пальцем, поставили знак равенства между такой классной штукой как инкапсуляция и не такой классной (хотя, иногда, и полезной) как сокрытие.


  1. Morozov_5F
    27.07.2015 14:50

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


    1. roman_kashitsyn
      27.07.2015 17:04
      +1

      Можно подумать, что виртуальные функции и наследование — это самая важная фича C++.
      Не знаю, как обстоят дела у остальных, но лично мне нужно заводить классы с виртуальными методами довольно редко. Да и коллеги подобное делают весьма нечасто.

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

      Когда я пишу на C, больше всего я тоскую по деструкторам.


  1. AxisPod
    28.07.2015 08:49
    +5

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