Pull to refresh

Псевдо ООП в C

Reading time 3 min
Views 70K


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

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

Articles