Объектно ориентированное программирование на Си без плюсов. Часть 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. Начну с конструктора и деструктора.

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

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

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

image-loader.svg

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

image-loader.svg

2. События.

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

image-loader.svg

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

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

image-loader.svg

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

image-loader.svg

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

image-loader.svg

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

image-loader.svg

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

3. Методы.

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

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

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

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

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

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

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

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

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

image-loader.svg

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

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

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

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

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

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

image-loader.svg

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

image-loader.svg

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

image-loader.svg

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

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

image-loader.svg

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

image-loader.svg

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

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

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

image-loader.svg

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

image-loader.svg

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

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

image-loader.svg

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

image-loader.svg

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

image-loader.svg

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

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

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

image-loader.svg

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

image-loader.svg

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

© Habrahabr.ru