Всем привет, это Stalker320. Я вернулся в сеть и осознав несколько концепций, вернулся с интересным подходом к разработке с использованием Си.


Наверное, говорить про ООП в Си - это довольно громко, но если отбросить синтаксический сахар, который предоставляет, к примеру, C++, то концепция реализуема. Давайте рассмотрим по пунктам и адаптируем под реалии языка Си:

Инкапсуляция

Процесс отделения друг от друга элементов объекта, определяющих его устройство и поведение; инкапсуляция служит для того, чтобы изолировать контрактные обязательства абстракции от их реализации.

Для примера приведу код (Использую C17)

// inc/employee.h
#ifndef EMPLOYEE_H
#define EMPLOYEE_H

// Для удобства и комфорта
typedef unsigned char ret_t;
#define OK     ((ret_t) 0)
#define FAILED ((ret_t) 1)

typedef struct EmployeeClass *Employee /* Ссылка может считаться Объектом */;
#ifndef EMPLOYEE_NO_PUBLIC_IMPL // Любой файл, что не объявил EMPLOYEE_NO_PUBLIC_IMPL получит публичную структуру
struct EmployeeClass {
  const char* name;
  char data[4];
  // Пустые байты, чтоб соответствовать размеру. (См. P. S. для большей информации)
};
#endif

// Пояснение к моему стилю наименования:
// pemployee от p<имя-объекта>,
// где p - от Pointer,
// а <имя-объекта> - здесь(vvv) подставлено employee.
ret_t empCreate(const char* name, const int salary, Employee* pemployee);
// Здесь не указатель, здесь просто объект
void empDestroy(Employee employee);

const char* empGetName(Employee employee);
int empGetSalary(Employee employee);

#endif
// src/employee.c, реализация
#define EMPLOYEE_NO_PUBLIC_IMPL // мы описываем содержание сами.
#include <employee.h>
#include <string.h>
#include <stdlib.h>

struct EmployeeClass {
  const char* name;
  int salary;
};

ret_t empCreate(const char* name, const int salary, Employee* pemployee) {
  struct EmployeeClass* employee_obj = malloc(sizeof(struct EmployeeClass));
  if (employee_obj == NULL) {
    return FAILED;
  }
  employee_obj->name = name;
  employee_obj->salary = salary;
  *pemployee = employee_obj;
  return OK;
}
void empDestroy(struct Employee employee /* Если желаеете, можете в исходнике вставить this, или self, здесь нет ключевого слова */) {
  free(employee);
}

const char* empGetName(struct EmployeeClass* employee) {
  return employee->name;
}

int empGetSalary(struct EmployeeClass* employee) {
  return employee->salary;
}
// src/main.c
#include <employee.h>
#include <stdio.h>

int main() {
  Employee ivan;
  if (empCreate("Ivan", 30000, &ivan) != OK) {
    perror("Failed to create employee\n");
    return 1;
  }
  printf("Ivan' name: %s\n", empGetName(ivan)); 
  // Имя спокойно получается и выводится
  
  printf("Ivan' name: %s\n", ivan->name);
  // Имя получается,
  // так как main.c мы получаем имеем свою версию struct EmployeeClass.
  // Если убрать общую реализацию, то мы получим ту же ошибку,
  // что при получении ivan->salary

  printf("Ivan' salary: %d\n", empGetSalary(ivan));
  // Зарплата получается без происшествий методом.

  printf("Ivan' salary: %d\n", ivan->salary);
  // Всё ломается. Мы получаем ошибку синтаксиса,
  // так как файл main.c не имеет понятия о том,
  // что в struct EmployeeClass есть поле salary
  
  empDestroy(ivan); // Анигиллировали Ивана.
  
  return 0;
}

Наследование

Создание такого отношения между классами (отношение родитель/потомок), когда один класс заимствует, а также расширяет и/или специализирует (уточняет) структуру и функциональный контракт одного или нескольких родительских классов.

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

// inc/programmer.h
#ifndef PROGRAMMER_H
#define PROGRAMMER_H

#include <employee.h> // Без него Programmer так и не будет работать полностью.

/// Programmer - это объект класса ProgrammerClass.
/// Наследуется от EmployeeClass.
typedef struct ProgrammerClass *Programmer;
#ifndef PROGRAMMER_NO_PUBLIC_IMPL
struct ProgrammerClass {
  Employee employee;
  char[4]; // Имитируем оригинал, объявляя необходимые байты
};
// [0000 0000],[1,1,1,1]  
// 0 - это байты указателя на EmployeeClass,
// 1 - это байты char.
#endif // EMPLOYEE_NO_PUBLIC_IMPL

ret_t progCreate(const char* name, int salary, int skill, Programmer* pprogrammer);
void progDestroy();

Employee progGetEmployee(Programmer programmer);
// Для получения данных из класса сотрудника.
// Добавьте другие прародительские классы для удобства.
// Нечто вроде
// Human progGetHuman(Programmer programmer) {
//   return empGetHuman(programmer->employee);
// }
int progGetSkill(Programmer programmer);

#endif // PROGRAMMER_H

// src/programmer.c
#define PROGRAMMER_NO_PUBLIC_IMPL
#include <programmer.h>

struct ProgrammerClass {
  // Здесь мы, кстати, имеем доступ к employee->name
  Employee employee;
  // Если структура Employee была бы публична, то можно было бы сделать:
  // struct EmployeeClass;
  // В данном случае ко всем полям можно обратиться через структуру ProgrammerClass
  // Кстати, битовые поля (BitField) отличный экономии памяти:
  // struct {
  //   bool english:1;
  //   bool python:1;
  //   bool c:1;
  //   char:5; // Незадействованные поля.
  // }; 
  // Вне структуры и без указания размера(:1), каждое поле занимало бы по байту,
  // а так структура занимает 1 байт. Удобный способ сжимать данные.
  int skill;
};

ret_t progCreate(const char* name, int salary, int skill, Programmer* pprogrammer) {
  struct ProgrammerClass* programmer = malloc(sizeof(struct ProgrammerClass));
  programmer->skill = skill;
  empCreate(name, salary, &(programmer->employee));
  // Просто передадим старшему конструктору нашу структуру
  // А если структура встроена, то просто преобразуем
  // empCreate(name, salary, (struct EmployeeClass*) programmer);
  *pprogrammer = programmer;
  return OK;
}
void progDestroy(Programmer programmer) {
  empDestroy(programmer->employee);
  free(programmer);
}

Employee progGetEmployee(Programmer programmer) { // Для получения данных сотрудника
  return programmer->employee;
}

int progGetSkill(Programmer programmer) {
  return programmer->skill;
}
// src/main.c
#include <programmer.h>
#include <stdio.h>

int main() {
  Programmer ivan;
  if (progCreate("Ivan", 30000, 4, &ivan) != OK) {
    perror("Failed to create employee\n");
    return 1;
  }
  printf("Ivan' name: %s\n", empGetName(progGetEmployee(ivan))); 
  // Имя спокойно получается и выводится, через метод
  // Имя спокойно получается и выводится
  
  printf("Ivan' name: %s\n", empGetName(ivan->employee));
  // Имя получается,
  // так как main.c мы получаем имеем свою версию struct ProgrammerClass,
  // который делится родительскими данными.
  // Если убрать общую реализацию, то мы получим ту же ошибку,
  // что при получении ivan->salary

  printf("Ivan' salary: %d\n", empGetSalary(ivan));
  // Зарплата получается без происшествий методом.

  printf("Ivan' salary: %d\n", ivan->salary);
  // Всё ломается. Мы получаем ошибку синтаксиса,
  // так как файл main.c не имеет понятия о том,
  // что в struct EmployeeClass есть поле salary

  printf("Ivan' skill: %d\n", progGetSkill(ivan));
  // Спокойно и безпрепятственно работает
  
  printf("Ivan' skill: %d\n", ivan->skill);
  // Также ожидаемо ломается.
  
  progDestroy(ivan); // Обнулили Ивана.
  // empDestroy не требуется
  
  return 0;
}

Полиморфизм

свойство системы, позволяющее использовать объекты с одинаковым интерфейсом без информации о типе и внутренней структуре объекта.

Си не имеет возможности реализовать данный принцип из-за отсутствия множества инструментов, вроде шаблонов. Однако благодаря этому предварительная проектировка программы требует понимания необходимых реализаций, что позволяет не тратить время и силы на перепрограммирование (наиболее подходящее слово, несущее смысл - overcoding).

Абстракция

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

Как пример интерфейса я могу привести лишь такой код:

// inc/caster.h // macros
#ifndef CASTER_H
#define CASTER_H

#include <human_interface.h>

#define HUMAN_I_CAST (object) ((HumanI*) object)

#endif

// inc/human_interface.h
#ifndef HUMAN_INTERFACE_H
#define HUMAN_INTERFACE_H

typedef struct HumanInterface {
  int age;
} HumanI;

int humIGetAge(HumanI* interface);

#endif // HUMAN_INTERFACE
// src/human_interface.c
#include <human_interface.h>

int humIGetAge(HumanI* interface) {
  return interface->age;
}

// inc/employee.h
#ifndef EMPLOYEE_H
#define EMPLOYEE_H

typedef struct EmployeeClass *Employee;
#ifndef EMPLOYEE_NO_PUBLIC_IMPL
struct EmployeeClass {
  HumanI; // напрямую объединяем класс и интерфейс
  const char* name;
  char[4];
};
#endif // EMPLOYEE_NO_PUBLIC_IMPL

#endif // EMPLOYEE_H
// main.c
#include <caster.h>
#include <employee.h>
#include <stdio.h>

int main() {
  Employee ivan;
  if (empCreate("Ivan", 30000, &ivan) != OK) {
    perror("Failed to create employee\n");
    return 1;
  }
  humIGetAge(HUMAN_I_CAST(ivan));
  
  empDestroy(ivan); // Анигиллировали Ивана.
  
  return 0;
}

Синглтон

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

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

// inc/window.h
// И нет, это не интерфейс для доступа к какому-то окну, это консольный рендер.
#ifndef WINDOW_H
#define WINDOW_H

typedef struct {int x, y;} Vector2i;
typedef struct {float x, y, z;} Vector3f;

Vector2i v2iCreate(int x, int y);
Vector2i v3fCreate(float x, float y, float z);

// BufferBit
typedef unsigned char BufferBit;
#define WIN_CHAR_BUFFER_BIT ((BufferBit)  0b0001)
#define WIN_DEPTH_BUFFER_BIT ((BufferBit) 0b0010)
// В более серьёзной реализации
// я хранил ещё и цвет буквы (BG и FG в RGBA по 0xRRGGBBAA в размере int)

int winInit();
void winTerminate();

void winClear(BufferBit bits);
void winViewport(Vector2i size);

// Реализацию треугольников оставляю силам читателей.
// Подсказка: чтоб определить, что точка в треугольнике, то определите, что:
// * вершины лежат по часовой стрелке (Перекрёстное умножение и геометрический смысл)
// * вершины не лежат против часовой стрелки
// * всё это относительно отрисовываемой точки.
void winDrawChar(Vector3f point, char c);
int winDepthFn(float a, float b); // Будет только more

void winSwapBuffers();
#endif // WINDOW_H
// src/window.c
#include <window.h>
#include <winsize.h>
// библиотека, которая получает размер окна консоли. 
// Мне не очень хочется показывать пример платформо-зависимого кода на макросах
// для получения размера окна, думаю таких статей на хабре с десяток уже.

#include <stdio.h>

Vector2i v2iCreate(int x, int y) {
  return (Vector2i) {x, y};
}
Vector3f v3fCreate(float x, float y, float z) {
  return (Vector3f) {x, y, z};
}

static struct Data {
  Vector2i size;
  struct {
    char* char_buffer;
    float* depth_buffer;
  };
} *data = NULL;

int winInit() {
  int sx, sy;
  if(!wsGet(&sx, &sy)) {
    perror("Failed to get terminal size\n");
    return 1;
  }
  data = malloc(sizeof(struct Data));
  // Данные класса в единственном экземпляре

  data->size = v2iCreate(sx, sy); // Пакуем данные

  data->char_buffer = calloc(sx * sy, sizeof(char));
  data->depth_buffer = calloc(sx * sy, sizeof(float));

  for (int i = 0; i < sx * sy; i++) {
    data->char_buffer[i] = ' ';
    data->depth_buffer[i] = -1.0f; // Самая дальняя стенка от камеры.
  }
  
  return 0;
}
void winTerminate() {
  free(data->char_buffer);
  free(data->depth_buffer);
  free(data);
}

void winSetChar(Vector2i point, char c) {
  data->char_buffer[point.x + data->size.y * point.y] = c;
}
void winSetDepth(Vector2i point, float c) {
  data->depth_buffer[point.x + data->size.y * point.y] = c;
}
char winGetChar(Vector2i point) {
  return data->char_buffer[point.x + data->size.y * point.y];
}
float winGetDepth(Vector2i point) {
  return data->depth_buffer[point.x + data->size.y * point.y]
}
// Да, можно упростить через макрос.
// Нет, Ctrl+C, Ctrl+V мне в статье проще

// Очищаем буффера
void winClear(BufferBit bits) {
  for (int i = 0; i < sx * sy; i++) {
    if (bits & WIN_CHAR_BUFFER_BIT != 0)
      data->char_buffer[i] = ' ';
    if (bits & WIN_DEPTH_BUFFER_BIT != 0)
      data->depth_buffer[i] = -1.0f;
  }
}
void winViewport(Vector2i size) {
  free(data->char_buffer);
  free(data->depth_buffer);

  data->char_buffer = calloc(size.x * size.y, sizeof(char));
  data->depth_buffer = calloc(size.x * size.y, sizeof(float));
  data->size = size;
  
  for (int i = 0; i < sx * sy; i++) {
    data->char_buffer[i] = ' ';
    data->depth_buffer[i] = -1.0f; // Самая дальняя стенка от камеры.
  }
}

void winDrawChar(Vector3f point, char c) {
  if (point.x > +1.0f | point.y > +1.0f | point.z > +1.0f |
      point.x < -1.0f | point.y < -1.0f | point.z < -1.0f & 1 == 1)
    return;
  // Clipping, обрезка по краю проекции. Любое true занимает первый бит.
  for (int y = 0; y < data->size.y; y++) { 
    for (int x = 0; x < data->size.x; x++) {
      int cx = (int) ((point.x + 1.0f /* из [-1;1] в [0;2] */)
                      / 2.0f /*в [0;1] */
                      * (float) data->size.x), // В [0; size.x] ()int
          cy = (int) ((point.y + 1.0f /* из [-1;1] в [0;2] */)
                      / 2.0f /*в [0;1] */
                      * (float) data->size.y); // В [0; size.y] (int)
      // cx, cy от (c)urrent (X), (c)urrent (Y)
      if (winDepthFn(point.z, winGetDepth(cx, cy))))
        data->char_buffer[cx + cy * data->size.y] = c;
    }
  }
}
int winDepthFn(float a, float b) {
  // Упрощаю. Но на деле тут надо data->depthfn через switch проверять.
  return a > b;
}

void winSwapBuffers() {
  printf("\x1b[2J"); // Отправляем ANSI-символ очистки экрана.
  for (int y = 0; y < data->size.y; y++) { 
    char* chain = calloc(data->size.x + 1, sizeof(char)), *ptr = chain;
    for (int x = 0; x < data->size.x; x++) {
      *ptr++ = data->char_buffer[x + y * data->size.x];
    }
    *ptr = 0x00; // Терминал
    printf("%s\n", chain);
    free(chain); // Убираемся за собой.
}
// src/main.c
#include <window.h>
int main() {
  if (!winInit()) {
    return 1;
  }
  while (1) {
    winClear(WIN_CHAR_BUFFER_BIT | WIN_DEPTH_BUFFER_BIT);
    winDrawChar(v3fCreate(0.0f, 0.0f, 0.0f), 'C');
    winDrawChar(v3fCreate(0.0f, 0.0f, 0.1f), 'D');
    // В центре экрана должно висеть D,
    // Либо C и D рядом, округление - оно такое.
    winSwapBuffers(); // Выбрасываем на экран
  }
  winTerminate();
  return 0;
}
// ну не знакомо ли выглядит?
// Пример для сравнения
#include <GLFW/glfw3.h>
#include <stdlib.h>

int main() {
  if (!glfwInit()) return 1;
  glfwMakeContextCurrent(glfwCreateWindow(600, 450, "Window", NULL, NULL));
  if (glfwGetCurrentContext() == NULL) {
    glfwTerminate();
    return 1;
  }
  // Да, можно использовать текущий контекст как хранилище для окна.
  // Нет, я не уверен, что вы так делали сами.
  while (!glfwWindowShouldClose(glfwGetCurrentContext())) {
    glfwPollEvents(glfwGetCurrentContext());
    glfwSwapBuffers();
  }
  glfwDestroyWindow(glfwGetCurrentContext());
  glfwTerminate();
  return 0;
}

Итоги

В данной статье я привёл довольно много примеров, которые написал во время написания статьи. О различных ошибках при выполнении примеров кода прошу сообщать в комментарии, если они не указаны заранее.

P. S. Как-то мне однажды написали, что массивы существуют физически, а не как ссылка на первый элемент и размер, охватывающий все последующие. Но я нашёл место, где они существуют: в структурах или объединениях(union). Они встраиваются там как поле, поэтому структура берёт их размер как совокупность размещённых подряд переменных.

Комментарии (5)


  1. websitedev
    13.12.2025 02:35

    Вообще, думаю, что некоторые из этих принципов можно применить в процедурном стиле не сильно привязываясь к стандартному синтаксису ООП.


  1. ATmegAdriVeR
    13.12.2025 02:35

    По теме могу порекондовать ознакомиться с этим видеокурсом, с упором на встраиваемые системы, но суть та же. Автор очень подробно показывает, что "под капотом" у механизмов ООП, скрытых компилятором С++.

    https://www.state-machine.com/video-course

    Конкретно лекции:

    #29 OOP Part-1: Encapsulation (classes) in C and C++
    #30 OOP Part-2: Inheritance in C and C++
    #31 OOP Part-3: Polymorphism in C++
    #32 OOP Part-4: Polymorphism in C


    1. stalker320 Автор
      13.12.2025 02:35

      Спасибо, ознакомлюсь. А так пока в планах с чат-гпт напополам созданный алгоритм по определению принадлежности треугольнику(Я узнал 3 способа сделать это, но всё оказалось проще, когда узнал про смысл cross-product. А там звёзды и сошлись с принципами работы OpenGL) развивать и публиковать по-тихоньку.


  1. Serpentine
    13.12.2025 02:35

    struct EmployeeClass {
      const char* name;
      char[4];
      // Пустые байты, чтоб соответствовать размеру. (См. P. S. для большей информации)
      // WARNING: Довольно платформо-специфично, требует дополнительных проверок,
      // которые я ещё не проводил
    };

    А что за «специфичная платформа»™ подобное за С17 примет и не подавится? Для друга интересуюсь.

    О различных ошибках при выполнении примеров кода прошу сообщать в комментарии, если они не указаны заранее.

    Попробуйте сами их хотя бы собрать, если не сделали это до публикации.

    Как-то мне однажды написали, что массивы существуют физически, а не как ссылка на первый элемент и размер, охватывающий все последующие.

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


    1. stalker320 Автор
      13.12.2025 02:35

      А что за «специфичная платформа»™ подобное за С17 примет и не подавится? Для друга интересуюсь.

      Ладно, у меня компилятор подавился. Но gcc на linux как-то съел struct {int;}; и не подавился. Спасибо за помощь, сейчас подредачу статью.