Всем привет, это 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). Они встраиваются там как поле, поэтому структура берёт их размер как совокупность размещённых подряд переменных.
