Ооп в си кратко

Обновлено: 05.07.2024

Программист как строитель

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

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

Принципы ООП

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

Первым принципом в философии объектно-ориентированного программирования является инкапсуляция . Это скрытие данных от других объектов. Считается, что как работать с данными лучше всего знает тот объект, кому эти данные принадлежат. Действительно, сторонний объект может быть разработан другим программистом, который и знать не знает как работать с данными чужого объекта. В этом философия, и этого принципа стараются придерживаться. Еще одним философским принципом является наследование. Король и рыцарь хоть и похожи друг на друга своими свойствами, но одного факта достаточно, чтобы они не могли быть элементами одного массива. Тип данных у них разный.

При автоматизации действий с многочисленными объектам очень желательно обращаться к ним по индексу как к элементам массива. Вот в этот самый момент спасением является механизм наследования . Нужно найти много общего между юнитами и на этом основании создать общего предка от которого они произошли. Наследники станут чем-то похожими на предка. Для чего это делается?

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

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

На рисунке приведена так называемая диаграмма классов.

Для программистов это такой же документ, как чертежи для архитектора. Каждый класс или как в нашем случае структура описывается прямоугольником. В нем есть имя структуры, перечень свойств и методов работы с этими свойствами. Структура воин является интерфейсом для юнитов. Через два метода в интерфейсе можно менять содержимое свойств юнитов. У интерфейса нет способов напрямую обратиться к их данным. Только через вызов метода. Этим самым достигается инкапсуляция . Скрытие данных от внешних объектов. В языке Си не поддерживаются модификаторы доступа к данным на уровне синтаксиса, поэтому показать способ красивее довольно сложно.

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

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

Полиморфизм

При получении удара рыцарь теряет единицу здоровья, потому что носит много защиты. Король при реализации метода попадания определит его по своему. Так как он почти не носит защиту, то здоровья будет отниматься три единицы сразу. Принцип полиморфизма или множества форм говорит о том, что внутри объекта реализация методов может отличаться от того как это сделано в других объектах. Итак, главный слоган объектно-ориентированного подхода это инкапсуляция , наследование и полиморфизм . Он выражен в трех словах и показан при помощи трех объектов. Весьма символично. Естественно, что большинству мало понятно для чего такие сложности.

Использование паттерна Посредник

Пока это рыцарь и король можно придумать дюжину методов внутри объектов для взаимодействия между ними. Представим, что мы описали все тонкости их взаимодействия. Очевидно, что при развитии программы будут появляться все новые и новые объекты. Описание всех взаимодействий внутри юнитов будет трудозатратным мероприятием. Опыт решения многочисленных похожих задач закреплен в таком понятии как паттерны проектирования , они же шаблоны проектирования . Это не руководство к действию, это рекомендации как можно поступить при возникновении той или иной проблемы. Чтобы организовать взаимодействие между различными объектами хорошо подходит паттерн Посредник . Кто же такой посредник? Это такой объект, который берет на себя все проблемы взаимодействия между объектами. Юниты могут не знать о существовании друг друга. Они знают, что есть посредник и к нему можно обратиться за помощью. Посредник же знает сколько юнитов есть в наличии и у него есть методы, которыми можно рассудить все взаимоотношения объектов.После разрешения всех проблем посредник отдает все необходимые распоряжения через интерфейс взаимодействия с юнитами.

Если бы объектно-ориентированного подхода не было, то даже не знаю как можно было бы объяснить решение такого рода задач. С чего бы начать рассмотрение всего того, что изображено на этой диаграмме? Начнем с юнитов. Они повторяют друг за другом. Во-первых, у них есть указатель на объект посредник. Появился метод walkToNearest() , это передвигаться к ближайшему юниту. Поскольку у юнита нет информации кто вообще на карте есть, то этот метод переадресует вопрос посреднику.

Посредник направит юнита в нужном направлении, используя интерфейс. Следующий метод это изменить местоположение. Новое будет по координатам x и y . Этот метод есть в интерфейсе юнита и только через интерфейс вызывается.

Метод strike() наносит удар и решить было ли попадание по другому юниту может только посредник. Поэтому strike() перенаправит вопрос посреднику. Метод hit() есть в интерфейсе и каждый юнит реализует его по своему уменьшив здоровье на какое-то значение.

Методы getX() и getY() возвращают координаты юнита. Они есть в интерфейсе и нужны для того, чтобы посредник узнал о местонахождении юнита.
Теперь рассмотрим посредника. Посредник хранит интерфейсы всех юнитов в массиве. Имя массива — warriors . Чтобы знать точно сколько имеется юнитов есть свойство warriorsCount . Метод warriorsWalkRequest() обрабатывает запрос на перемещение юнита, рассчитывает в каком направлении двигаться и передают новые координаты через интерфейс. Метод warriorStrike() обрабатывает запрос на совершение удара юнитом. Если рядом не было соседа, то ничего не произойдет. Если рядом стоял юнит, то для него через интерфейс вызовется метод hit() . Что означает попадание. Как мы уже знаем, юнит вычтет у себя часть здоровья. Схема пока не идеальна, но в ней уже заложены большие возможности по расширению функционала юнитами и дальнейшему развитию игровой логики. Все дело в применение известного паттерна.

Исходный код примера на языке Си

В этом примере объект рыцаря , находясь в соседней клетке с объектом король , нанес удар. Нанесение удара сопровождается запросом к посреднику. Посредник проверил координаты юнитов и вызвал у объекта — соседа метод попадания hit() .

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

Недостаток ООП

Во первых, инкапсуляция. Сокрытие данных приводит к тому, что для их модификации приходится вызывать функции. Как мы уже знаем , вызов функции это работа со стеком, размещение параметров, локальных переменных и возврат результата. Это много машинных инструкций. При наследовании в некоторых языках технически происходит не все так просто, как было показано здесь. Решение о том, какая функция будет вызываться принимается во время работы программы. Любое решение это опять же машинные инструкции, идущие не на полезную работу.

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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



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

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



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


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


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

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

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

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

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

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

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

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

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

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

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

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

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


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


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

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


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


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

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


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




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


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

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

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


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


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

Основными механизмами в объектно-ориентированном программировании есть:

  • полиморфизм;
  • наследование;
  • инкапсуляция.

Инкапсуляция — это объединение переменных и функций (свойств и методов) в классе.

Полем класса может быть функция:

В этом случае её можно вызывать только для конкретного объекта этого классового типа:

Такую функцию можно описать прямо внутри класса, это означает то же, что inline для глобальных функций:

Но можно её описать (т.е. написать тело) и ниже, например, в .cpp при помещении объявления класса в .h:

Так как метод всегда зовется для какого-то объекта, ему всегда передается адрес этого объекта. Этот указатель доступен в теле метода как ключевое слово this. Типа псевдо-переменной this — C* const, если метод описан без слова const в конце, и const C* const — иначе:

Т.е. метод со словом const не может изменять свой объект.

Кроме того, в методах не обязательно писать this->f, достаточно просто написать "f".

Имена, примененные в методе, ищутся:

  • по <> блокам в самом методе, формальные параметры есть часть самого внешнего блока;
  • поля класса, если нашлось такое — то понимается как this->name;
  • глобалы.

Вот тут var — не поле класса, а просто такая хитрая глобальная переменная:

Её надо обязательно где-то описать, в классе она лишь объявлена:

Отличия от глобала:

  • для нее действуют метки private/protected/public;
  • имя объявлено в пространстве имен класса, а не в глобальном пространстве имен.

Точно так же statfunc — не настоящий метод, а просто такая функция:

Описывается она так же, как и метод, но в ней не бывает this и она не может прямо использовать имена нестатических (настоящих) полей и методов класса.

Вызвана она может быть:

  • из любого метода, и настоящего, и статического, как statfunc();
  • извне класса как C::statfunc, если позволяют метки доступа;
  • как obj.statfunc() или pobj->statfunc(), в этих случаях obj и pobj не более чем указывают на тип и не используются при вызове.

Внутри объявления класса могут встречаться метки доступа protected, public и private. Они задают уровень доступа ко всем объявлениям, следующим до следующей такой метки или конца класса. Меток доступа может быть сколько угодно.

Единственное отличие struct от class в том, что по умолчанию до первой такой метки объявления считаются private в class и public в struct.

Все, что объявлено private, может быть использовано только в контексте данного класса — внутри кода методов данного класса, в инициализаторах статических полей-данных и т.д. Эти сущности нельзя использовать даже в потомках, не говоря уже о внешнем коде.

Все, что объявлено public, может быть использовано где угодно.

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

То, что объявлено protected, может быть использовано как в контексте данного класса, так и в контексте его непосредственных наследников, более нигде.

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

Наследование — это помещение в производный класс (который наследует) свойств и методов базового класса (которого наследуют).

Это означает, что в объекте класса D есть все поля класса B (fb), плюс ещё и дополнительно объявленные, специфичные поля для D (fd). Концептуально это означает, что любой объект класса D — это, помимо всего прочего, ещё и полноценный объект класса B. Это и есть наследование.

Кроме того, указатель D* там, где это нужно, молча автоматически приводится к B*. Обратное приведение тоже возможно, но только с явным оператором приведения. Использовать можно только тогда, когда есть уверенность, что данный B* действительно указывает на B-часть в D.

Таким образом, любой объект D есть разновидность объекта B, такая, что к ней добавлено что-то ещё.

Класс B называется базовым, D — производным.

Тут в объекте D будет подобъекты B и B2, и три поля: fb, fb2 и fd.

Преобразование указателей от D* к B* работает и здесь. При этом, если преобразование к единственной (и вообще к первой) базе есть умозрительная операция внутри компилятора, не генерирующая кода, то преобразование ко второй и далее базе — D* к B2* — означает генерацию кода, который прибавит к указателю значение смещения B2 внутри D.

Возможно использовать и приватное наследование — class D : private B. В этом случае вся B-часть класса D доступна только из контекста класса D, т.е. аналогична тому, что объявлено private в классе B. Даже приведение указателя к B* возможно только в контексте класса D — приватный базовый класс считается деталью реализации, неизвестной внешнему миру.

В публичном же случае вся публичная часть B становится публичной частью D, а protected и private части В — приватными частями D.

Полиморфизм — это вызов виртуальной функции производного класса через указатель на базовый.

то в данном случае решение о том, какую функцию звать — B::f или D::f — принимается на основе известного компилятору типа указателя, т.е. B::f.

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

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

Эта возможность есть, она выглядит так:

В этом случае pb->f() будет исполнять совсем иной код. Обычно этот код находит указатель на таблицу виртуальных методов в начале объекта и выбирает функцию из этой таблицы. Указатель же на таблицу зависит от того, как создавался объект B — как подобъект в D или же непосредственно как B. Заполнение этого указателя обычно делается специальным кодом, автоматически сгенерированным внутри конструктора.

Достаточно объявить метод как virtual на вершине дерева наследования, где он встречается впервые. Писать virtual в потомках уже не обязательно, хотя и можно (ничего не делает, все равно virtual).

Допустимо ещё и такое:

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

Явная квалификация pb->D::f() пересиливает виртуальный механизм вызова, зовется D::f.

См. ассемблерный код, сгенеренный при использовании библиотеки Microsoft ATL — там такое на каждом шагу.

В Си++ имеется возможность гарантированной инициализации — т.е. дается гарантия того, что любой объект классового типа, как бы он не возник в программе, будет проинициализирован неким заданным в классе кодом.

Эта возможность реализуется конструктором.

Синтаксически конструктор есть метод, имя которого совпадает с именем класса:

Создание объекта такого класса обязательно требует указания параметра типа int, который помещается в поле i объекта.

Может быть несколько (совместно используемых) конструкторов, отличающихся параметрами, как и любые совместно используемые функции.

Конструктор является специальной функцией, у него может, например, быть сгенерирован не такой пролог/эпилог, как у обычных функций. Потому конструктор: а) не возвращает значения, даже void и б) нельзя брать адрес конструктора.

Объект классового типа может возникнуть в коде как:

Конструкторы глобалов зовутся до входа в main платформо-зависимым кодом.

Конструкторы локалов зовутся явно по ходу исполнения операторов в блоке. Синтаксис для глобалов и локалов:

В С++ есть ключевое слово explicit, позволяющее запретить неявное преобразование аргумента конструктора. Применяется только к конструкторам.

Вообще, если конструктор имеет 1 параметр, рекомендуется использовать explicit, т.к. неявное преобразование типов потенциально опасно.

Создание массива по new обязательно использует конструктор без параметров. Иначе же:

Скобки круглые (квадратные скобки - массив) и идут за именем класса (скобки за словом new передают параметры в operator new, а не в конструктор).

Для таких случаев в синтаксисе конструктора предусмотрен так называемый ctor-инициализатор (список инициализации):

В ctor-инициализаторе может быть проинициализировано любое поле и любая база. При этом для: а) полей, являющимися ссылками; б) для полей классовых типов, не имеющих конструктора без параметров; и в) для базовых классов, не имеющих такого конструктора - элемент в ctor-инициализаторе обязателен.

Явный синтаксис для временного объекта — MyClass(1, 2). Это выражение, его значение есть временный объект типа MyClass, проинициализированный вызовом MyClass::MyClass(1, 2).

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

Таким образом мы получаем, что (MyClass)1 и MyClass(1) — строго одно и то же.

Более того, Си++ не делает разницы в синтаксисе для классовых и неклассовых типов, т.е. синтаксис long(1) вполне возможен и опять же есть то же самое, что и (long)1. Точно так же можно:

Временный объект когда-то будет разрушен, точное время разрушения зависит от реализации, потому надо быть крайне осторожным с указателями на них (поместить такой указатель в контейнер — типичная ошибка начинающего).

Конструкторы не наследуются. Вместо этого конструктор производного класса обязательно неявно (через автосгенеренный компилятором код) зовет конструкторы базовых классов — так, как указано в ctor-инициализаторе, или же зовется конструктор базового класса без параметров (если такой есть и нет соответствующего элемента ctor-инициализатора).

Конструкторы не могут быть виртуальными, т.е. вызываемыми тем механизмом, что и virtual функции. Понятие "виртуальный конструктор" означает совсем иное (паттерн factory, реализованный на объекте типа type_info).

В конструкторах невозможны классические способы обработки ошибок (через код ошибки), возможно лишь: а) throw; б) никогда не писать способный к отказу код в конструкторе, заведя отдельный метод Init.

Если же у класса есть поля, обязательно требующие ctor-инициализатора (базы без конструктора по умолчанию/такие же поля/ссылки) — то такая генерация невозможна, и требуется обязательное написание конструкторов явно.

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

Следует отличать C::C(C&) и C::operator=(C&). Вторая функция вызывается для уже построенного объекта слева, конструктор же копирования — нет, он и строит этот объект (разница инициализации и присваивания).

В Си++ есть возможность гарантированного освобождения занятых объектом ресурсов при выходе объекта из области его жизни, т.е. гарантированное разрушение.

Для такого разрушения используется деструктор. Синтаксически это функция с именем ~MyClass:

Деструктор вызывается когда-то для любого объекта.

  • глобал — после main платформо-зависимым кодом
  • локал — по закрывающей > (включая переходы return/break/continue/goto, а также исключения)
  • по new — в delete или delete[]
  • поле или база класса — автосгенеренным кодом в прологе/эпилоге деструктора производного класса после исполнения явного тела этого деструктора
  • временный — когда-то по усмотрению компилятора.

Как и с конструктором, нельзя брать адрес деструктора и он не возвращает значений (даже void).

Деструкторы не наследуются. Если у класса не написан деструктор явно - он генерится компилятором, сгенеренный деструктор аналогичен пустому телу — ~C() <>.

Деструкторы могут быть виртуальными, и это рекомендуется практически всегда, кроме случаев, когда у класса вообще нет виртуальных методов.

Тело деструктора должно разрушить все поля объекта — позвать close(), CloseHandle, ->Release() и тому подобные функции.

Деструктор может быть объявлен private. Объекты такого класса могут создаваться только оператором new без квадратных скобок, любая другая попытка создания даст ошибку "разрушение невыполнимо-деструктор приватен". Такие объекты должны иметь метод разрушения, в котором будет исполняться delete this;

Объектно-ориентированное программирование (ООП) позволяет разложить проблему на составные части, каждая из которых становится самостоятельным объектом. Каждый из объектов содержит свой собственный код и данные, которые относятся к этому объекту.

Любая программа, написанная на языке ООП, отражает в своих данных состояние физических предметов либо абстрактных понятий – объектов программирования, для работы, с которыми она предназначена.

Все данные об объекте программирования и его связях с другими объектами можно объединить в одну структурированную переменную. В первом приближении ее можно назвать объектом.

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

Описание множества однотипных объектов называется классом .
Объект – это структурированная переменная, содержащая всю информацию о некотором физическом предмете или реализуемом в программе понятии.

Класс – это описание множества объектов программирования (объектов) и выполняемых над ними действий.

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

Основные понятия объектно-ориентированного программирования

Любая функция в программе представляет собой метод для объекта некоторого класса.

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

Вся программа в таком виде представляет собой объект некоторого класса с единственным методом run (выполнить).

  • инкапсуляция;
  • наследование;
  • полиморфизм.

Внутри объекта код и данные могут быть закрытыми или открытыми.

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

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

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

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

Иначе говоря, новый класс наследует как данные старого класса, так и методы их обработки.

Если объект наследует свои свойства от одного родителя, то говорят об одиночном наследовании . Если объект наследует данные и методы от нескольких базовых классов, то говорят о множественном наследовании .

Пример наследования – определение структуры, отдельный член которой является ранее определенной структурой.

Наследование структур


Полиморфизм – это свойство, которое позволяет один и тот же идентификатор (одно и то же имя) использовать для решения двух и более схожих, но технически разных задач.

Целью полиморфизма, применительно к ООП, является использование одного имени для задания действий, общих для ряда классов объектов. Такой полиморфизм основывается на возможности включения в данные объекта также и информации о методах их обработки (в виде указателей на функции).

Будучи доступным в некоторой точке программы, объект , даже при отсутствии полной информации о его типе, всегда может корректно вызвать свойственные ему методы.
Полиморфная функция – это семейство функций с одним и тем же именем, но выполняющие различные действия в зависимости от условий вызова.

Например, нахождение абсолютной величины в языке Си требует трех разных функций с разными именами:

В C++ можно описать полиморфную функцию, которая будет иметь одинаковое имя и разные типы и наборы аргументов.

Читайте также: