Приветствую!
Каждый раз, когда начинаешь решать какую-либо большую задачу, то на пути появляется множество маленьких. И найденные или не найденные решения маленьких подзадач превращаются в то, что мы в дальнейшем называем опытом. Но к сожалению, если не пользуешься чем-то постоянно, то когда-то найденные оригинальные решения со временем начинаешь забывать, а когда необходимо, начинаешь вспоминать и с удивлением, а иногда и с не пониманием долго смотришь на свои же шедевры.
Статья рассчитана на тех кто уже знаком с Си, а все примеры ориентированы на ОС Linux. Мои познания Windows закончились на «WinXP», после которой в Windows стало уже очень много политики ("безопасности") и коммерческой составляющей, но я сейчас не об этом и надеюсь, что здесь вы найдёте для себя полезные моменты, а если я в чём-то не прав или заблуждаюсь, то поправите.
И так, я решил попробовать писать в стиле объектно ориентированного программирования (далее ООП) на Си без плюсов. Многие скажут, что писать в стиле объектно ориентированного программирования (далее ООП) не для Си, и разные приёмы написания это - «псевдо-ООП». Но лично я считаю ООП всего лишь абстрактной парадигмой, определяющей стиль написания ПО и не более чем. А Си очень мощный и самодостаточный язык программирования.
Так сложилось, что изучать традиции ООП я начал с Delphi и Java, являющихся, как считается, на 100% объектно ориентированными языками программирования, а потому аналогия решений у меня ассоциируется именно с ними. И далее в тексте я иногда буду на них ссылаться, что надеюсь не испортит суть полного понимания.
В соответствии с определениями ООП все сущности должны быть объектами обладающими некоторыми свойствами и принадлежать к определённому классу.
У классов должны быть:
Конструктор и деструктор для рождения и уничтожения объектов соответственно;
Методы информирующие о изменении состояния (события);
Методы определяющие поведение объектов.
Для написания классов я предлагаю постепенно в тексте вводить простые правила нотации:
Новый файл — новый класс, как в Java. Вернее заголовочный файл mynewclass.h + основной файл mynewclass.c;
Перед именем функции пишется имя класса, например: void myclass_namefunction(…);
Перед новым определяемым типом пишется « t_ », например: t_mynewtype;
В макросах все вновь вводимые переменные начинаются с двойного подчёркивания, например: __i;
1. Начну с конструктора и деструктора.
В Си нет понятия классов и объектов, но есть структуры, есть макросы, чего вполне, как оказалось достаточно. В качестве объекта класса можно рассматривать переменную типа структуры, а основная задача конструктора это выделение необходимой памяти, инициализация переменных, иных необходимых объектов и возврат указателя на память, где создаётся «объект». В структуре не может быть функций, но никто не запрещает иметь ссылку на другую функцию или структуру.
У деструктора обратная задача — навести порядок и высвободить задействованные вычислительные ресурсы.
В соответствии с принятой нотацией типовой конструктор это функция, которая может выглядеть, как-то так:
![](https://habrastorage.org/getpro/habr/upload_files/5fa/fe1/6f2/5fafe16f22b810a4779dce6996ce39ea.png)
Ну, а деструктор соответственно:
![](https://habrastorage.org/getpro/habr/upload_files/827/2e8/2c9/8272e82c9f697ebb7faaff29ed228d84.png)
2. События.
Реализацию событий можно организовать при помощи указателей на функции. Я посчитал, что включать указатели на функции в общую структуру объекта нет смысла, да и расточительно выделять дополнительную память каждый раз при создании объекта. И, указатели на функции обратных вызовов (событий) решил объединять в отдельную структуру, имя которой содержит имя класса, а имена функций начинаются с «on_» например:
![](https://habrastorage.org/getpro/habr/upload_files/44b/1cf/856/44b1cf856795c17886313680e870283f.png)
Таким образом, помещая объявленную структуру в заголовочный файл получаем аналогию интерфейса, как в Java, который можно использовать например так:
В структуру нашего нового класса добавляем указатель на тип t_mynewclass_events, т.е.:
![](https://habrastorage.org/getpro/habr/upload_files/bfb/ff4/cac/bfbff4cacd5051609407657c454c66ae.png)
В файлах классах реализуем функцию «сеттер»:
![](https://habrastorage.org/getpro/habr/upload_files/cdd/f5f/ec3/cddf5fec3be0d5797ff25e05f64222c5.png)
В основном файле программы, используем всё это как-то так:
![](https://habrastorage.org/getpro/habr/upload_files/828/14b/9ab/82814b9ab5b281789a862d9a917577fe.png)
Ну, а в функциях класса вызываем событие так:
![](https://habrastorage.org/getpro/habr/upload_files/364/817/098/364817098da722be759e253da1de2537.png)
Собственно вот и вся реализация так называемого callback-а.
3. Методы.
В части методов, определяющих поведение объекта и доступных из вне (т.е. публичных), я ещё раз повторюсь и обобщу принятое мной правило, это не включать в структуру объекта ссылки на функции (методы класса), а, просто, название функций начинать с имени класса, например: void myclass_namefunction(…);. Считаю, такое решение вполне рациональным. Принадлежность к классу всегда можно определить по имени функции, а единственное неудобство "много букв" простить.
Двигаемся далее. В основе ООП есть три основополагающих понятия: инкапсуляция, полиморфизм и наследование.
1. Инкапсуляция.
Смысл её в том, что бы разделить частное (protected, private … ) и общедоступное ( public, published … ). Частное это внутренняя «кухня» определённого класса доступ до которой ограничен.
Решение на Си простое:
В заголовочном файле mynewclass.h пишем:
Саму структуру определяем в файле mynewclass.с:
Для доступа к полям структуры в заголовочном файле прописываем прототипы публичных функций:
Реализация функций в файле mynewclass.с буде выглядеть как-то так:
![](https://habrastorage.org/getpro/habr/upload_files/ed3/dba/479/ed3dba479449bfcc87e507d6b5a69527.png)
Теперь доступ к переменным структуры определяется «сетерами» и «гетерами», как в Java, а в структуре struct mynewclass могут быть приватные поля и методы объекта. Здесь стоит наверное отметить следующее, в одном процессе все методы (функции) для одного нашего «Класса» являются общими. А чтобы понимать с каким объектом должна отработать функция, то первым параметром отправляем ссылку на объект её вызывающего.
С инкапсуляцией надеюсь разобрались.
2. Полиморфизм.
В моём понимании это способность функции обрабатывать входные параметры различных типов, и в Си для этого есть несколько интересных решений.
Передача параметра в функцию через указатель void*, например так:
В начале для красоты введём собственные наименования типов при помощи перечисления:
![](https://habrastorage.org/getpro/habr/upload_files/6d9/704/6ba/6d97046ba96a46e8643181e4cc1c2514.png)
Тогда функцию оформляем следующим образом, например:
![](https://habrastorage.org/getpro/habr/upload_files/836/322/734/8363227347930633112680ee12a0b2aa.png)
Вызов функции будет соответственно:
![](https://habrastorage.org/getpro/habr/upload_files/818/42c/bcd/81842cbcd5c4c5326a9e9f707f982e18.png)
Надеюсь идея ясна и понятна.
2. Можно задачу полиморфизма решить через определение макроса. В отличие от функции в макросах в качестве параметра можно прописать тип передаваемого параметра. Например ниже представлен макрос, который удаляет элементы из массива любого типа:
![](https://habrastorage.org/getpro/habr/upload_files/210/e09/bd1/210e09bd1c8696b1c26af6230634a7a5.png)
3. А можно в функцию передать любую другую функцию, например так:
![](https://habrastorage.org/getpro/habr/upload_files/901/653/e77/901653e77d0e92f69e98fbea054f6982.png)
Это вроде аналогичной функции Synchronize(@function) из Delphi, но сейчас не об этом.
3. Наследование.
С наследованием в Си на самом деле не всё так, как хотелось бы. И вариант здесь похоже один - в структуру объекта включить указатель на структуру другого объекта, как-то так:
![](https://habrastorage.org/getpro/habr/upload_files/a81/9d9/09d/a819d909d96813a46f647ddcd07564e8.png)
А потом даже можно написать:
![](https://habrastorage.org/getpro/habr/upload_files/37c/db9/33f/37cdb933f3fdb094e556b274c16868b1.png)
Компилятор такую запись должен понять, но это всё равно не похоже на наследование свойств и методов от какого-то родительского класса, а скорее наоборот — порождение потомков с определёнными свойствами и методами принадлежащими новому «родителю семейства».
Но вот, что бы потомки знали, в случае обработки события, к какому экземпляру родителя оно относится, я предлагаю у потомков прописывать «фамилию родителя», то есть добавить в структуру каждого класса переменную указатель: void* parent и пару функций для работы с ней:
![](https://habrastorage.org/getpro/habr/upload_files/7e8/d20/4a0/7e8d204a06bd6690dc79978b6ab7f467.png)
Получаем следующее:
![](https://habrastorage.org/getpro/habr/upload_files/24f/c13/1c4/24fc131c4c3f3f67bb6a119af40d8bd8.png)
И, когда потомок вызовет указанную ему функцию-обработчик, мы сможем однозначно знать к какому экземпляру родителя это событие относится.
![](https://habrastorage.org/getpro/habr/upload_files/61d/dd2/414/61ddd2414247fce15acdc7d17855b62c.png)
Такая реализация наследования может быть и не выглядит классической, но если посмотреть с другой стороны, то чего-то большего возможно и не нужно.
Хотя почему реализация не классическая? В Delphi при объявлении нового класса это обычная практика включать в класс поля переменных других классов.
В общем, разобрав реализацию основных понятий ООП для создания класса получился некий шаблон, где заголовочный файл myclass.h должен выглядеть как-то так:
![](https://habrastorage.org/getpro/habr/upload_files/51f/be6/ef1/51fbe6ef1a169cb573a3a1e2c112df30.png)
Соответственно файл myclass.с должен выглядеть как-то так:
![](https://habrastorage.org/getpro/habr/upload_files/e39/603/03e/e3960303edfe5507e74bf17775ccaa31.png)
Теоретически, представленного выше, для создания объектов должно быть достаточно, а вот для практического применения и написания подавляющего большинства программ необходимо решение в виде таймера, но об этом я хочу рассказать в следующей части.