Объектно ориентированное программирование на Си без плюсов. Часть 1. Введение

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Приветствую! 

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

Статья рассчитана на тех кто уже знаком с Си, а все примеры ориентированы на ОС Linux. Мои познания Windows закончились на «WinXP», после которой в Windows стало уже очень много политики ("безопасности") и коммерческой составляющей, но я сейчас не об этом и надеюсь, что здесь вы найдёте для себя полезные моменты, а если я в чём-то не прав или заблуждаюсь, то поправите.

И так, я решил попробовать писать в стиле объектно ориентированного программирования (далее ООП) на Си без плюсов. Многие скажут, что писать в стиле объектно ориентированного программирования (далее ООП) не для Си, и разные приёмы написания это - «псевдо-ООП». Но лично я считаю ООП всего лишь абстрактной парадигмой, определяющей стиль написания ПО и не более чем. А Си очень мощный и самодостаточный язык программирования.

Так сложилось, что изучать традиции ООП я начал с Delphi и Java, являющихся, как считается, на 100% объектно ориентированными языками программирования, а потому аналогия решений у меня ассоциируется именно с ними. И далее в тексте я иногда буду на них ссылаться, что надеюсь не испортит суть полного понимания.

В соответствии с определениями ООП все сущности должны быть объектами обладающими некоторыми свойствами и принадлежать к определённому классу.

У классов должны быть:

  1. Конструктор и деструктор для рождения и уничтожения объектов соответственно;

  2. Методы информирующие о изменении состояния (события);

  3. Методы определяющие поведение объектов.

Для написания классов я предлагаю постепенно в тексте вводить простые правила нотации:

  1. Новый файл — новый класс, как в Java. Вернее заголовочный файл mynewclass.h + основной файл mynewclass.c;

  2. Перед именем функции пишется имя класса, например: void myclass_namefunction(…);

  3. Перед новым определяемым типом пишется « t_ », например: t_mynewtype;

  4. В макросах все вновь вводимые переменные начинаются с двойного подчёркивания, например: __i;

 1. Начну с конструктора и деструктора.

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

У деструктора обратная задача — навести порядок и высвободить задействованные вычислительные ресурсы.

В соответствии с принятой нотацией типовой конструктор это функция, которая может выглядеть, как-то так:

Ну, а деструктор соответственно:

2. События.

Реализацию событий можно организовать при помощи указателей на функции. Я посчитал, что включать указатели на функции в общую структуру объекта нет смысла, да и расточительно выделять дополнительную память каждый раз при создании объекта. И, указатели на функции обратных вызовов (событий) решил объединять в отдельную структуру, имя которой содержит имя класса, а имена функций начинаются с «on_» например:

Таким образом, помещая объявленную структуру в заголовочный файл получаем аналогию интерфейса, как в Java, который можно использовать например так:

В структуру нашего нового класса добавляем указатель на тип t_mynewclass_events, т.е.:

В файлах классах реализуем функцию «сеттер»:

В основном файле программы, используем всё это как-то так:

Ну, а в функциях класса вызываем событие так:

Собственно вот и вся реализация так называемого callback-а.

3. Методы. 

В части методов, определяющих поведение объекта и доступных из вне (т.е. публичных), я ещё раз повторюсь и обобщу принятое мной правило, это не включать в структуру объекта ссылки на функции (методы класса), а, просто, название функций начинать с имени класса, например: void myclass_namefunction(…);. Считаю, такое решение вполне рациональным. Принадлежность к классу всегда можно определить по имени функции, а единственное неудобство "много букв" простить.

Двигаемся далее. В основе ООП есть три основополагающих понятия: инкапсуляция, полиморфизм и наследование.

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

Смысл её в том, что бы разделить частное (protected, private … ) и общедоступное ( public, published … ). Частное это внутренняя «кухня» определённого класса доступ до которой ограничен.

Решение на Си простое:

  1. В заголовочном файле mynewclass.h пишем: 

  2. Саму структуру определяем в файле mynewclass.с:

  3. Для доступа к полям структуры в заголовочном файле прописываем прототипы публичных функций:

  4. Реализация функций в файле mynewclass.с буде выглядеть как-то так:

Теперь доступ к переменным структуры определяется «сетерами» и «гетерами», как в Java, а в структуре struct mynewclass могут быть приватные поля и методы объекта. Здесь стоит наверное отметить следующее, в одном процессе все методы (функции) для одного нашего «Класса» являются общими. А чтобы понимать с каким объектом должна отработать функция, то первым параметром отправляем ссылку на объект её вызывающего.

С инкапсуляцией надеюсь разобрались.

2. Полиморфизм.

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

  1. Передача параметра в функцию через указатель void*, например так:

    В начале для красоты введём собственные наименования типов при помощи перечисления:

Тогда функцию оформляем следующим образом, например:

Вызов функции будет соответственно:

Надеюсь идея ясна и понятна.

2. Можно задачу полиморфизма решить через определение макроса. В отличие от функции в макросах в качестве параметра можно прописать тип передаваемого параметра. Например ниже представлен макрос, который удаляет элементы из массива любого типа:

3. А можно в функцию передать любую другую функцию, например так:

Это вроде аналогичной функции Synchronize(@function) из Delphi, но сейчас не об этом.

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

    С наследованием в Си на самом деле не всё так, как хотелось бы. И вариант здесь похоже один - в структуру объекта включить указатель на структуру другого объекта, как-то так:

А потом даже можно написать:

Компилятор такую запись должен понять, но это всё равно не похоже на наследование свойств и методов от какого-то родительского класса, а скорее наоборот — порождение потомков с определёнными свойствами и методами принадлежащими новому «родителю семейства».

Но вот, что бы потомки знали, в случае обработки события, к какому экземпляру родителя оно относится, я предлагаю у потомков прописывать «фамилию родителя», то есть добавить в структуру каждого класса переменную указатель: void* parent и пару функций для работы с ней:

Получаем следующее:

И, когда потомок вызовет указанную ему функцию-обработчик, мы сможем однозначно знать к какому экземпляру родителя это событие относится.

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

Хотя почему реализация не классическая? В Delphi при объявлении нового класса это обычная практика включать в класс поля переменных других классов. 

В общем, разобрав реализацию основных понятий ООП для создания класса получился некий шаблон, где заголовочный файл myclass.h должен выглядеть как-то так:

Соответственно файл myclass.с должен выглядеть как-то так:

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

Источник: https://habr.com/ru/post/568588/


Интересные статьи

Интересные статьи

В этом руководстве мы рассмотрим создание и монтирование LVM томов на рутованном устройстве Android. Это вторая часть моего проекта «Резервный сервер на Android», но она будет на 80% со...
Предлагаю ознакомиться с ранее размещенными материалами по проекту Starlink (SL): Часть 1. Рождение проекта ‣ Часть 2. Сеть SL ‣ Часть 3. Наземный комплекс ‣ Часть 4. Абонентский те...
Часть #1 (scanning)Часть #2 (connecting/disconnecting)Часть #3 (read/write), вы здесьВ предыдущей статье мы подробно поговорили о подключении/отключении BLE уст...
В прошлой статье мы познакомились с Вами с исторически первым способом организации построения multicast VPN с помощью технологий PIM и mGRE (Часть 1, Profile 0). На сегодняшний день суще...
Главным событием сентября в мире PostgreSQL безусловно является выход 13 версии. Однако жизненный цикл PostgreSQL 14 идет своим чередом и в сентябре прошел второй коммитфест изменений...