Всем привет, это 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)

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
stalker320 Автор
13.12.2025 02:35Спасибо, ознакомлюсь. А так пока в планах с чат-гпт напополам созданный алгоритм по определению принадлежности треугольнику(Я узнал 3 способа сделать это, но всё оказалось проще, когда узнал про смысл cross-product. А там звёзды и сошлись с принципами работы OpenGL) развивать и публиковать по-тихоньку.

Serpentine
13.12.2025 02:35struct EmployeeClass { const char* name; char[4]; // Пустые байты, чтоб соответствовать размеру. (См. P. S. для большей информации) // WARNING: Довольно платформо-специфично, требует дополнительных проверок, // которые я ещё не проводил };А что за «специфичная платформа»™ подобное за С17 примет и не подавится? Для друга интересуюсь.
О различных ошибках при выполнении примеров кода прошу сообщать в комментарии, если они не указаны заранее.
Попробуйте сами их хотя бы собрать, если не сделали это до публикации.
Как-то мне однажды написали, что массивы существуют физически, а не как ссылка на первый элемент и размер, охватывающий все последующие.
А еще можно сишный букварь открыть (абсолютно любой) и не путать имя массива и его представление в памяти, ну и указатели со ссылками.

stalker320 Автор
13.12.2025 02:35А что за «специфичная платформа»™ подобное за С17 примет и не подавится? Для друга интересуюсь.
Ладно, у меня компилятор подавился. Но gcc на linux как-то съел
struct {int;};и не подавился. Спасибо за помощь, сейчас подредачу статью.
websitedev
Вообще, думаю, что некоторые из этих принципов можно применить в процедурном стиле не сильно привязываясь к стандартному синтаксису ООП.