Что такое инкапсуляция с
БлогNot. Лекции по C/C++: Классы, часть 1 (инкапсуляция)
Лекции по C/C++: Классы, часть 1 (инкапсуляция)
С усложнением программ основной идеей обеспечения их управляемости стало моделирование. Реальный мир состоит из взаимодействующих между собой объектов. С точки зрения программирования объекты, характеризуемые разнородными данными, можно описать как раз структурами, а взаимодействие объектов обеспечить функциями, которые будут работать с данными этих структур. При классическом процедурном подходе плохо лишь то, что данные «сами по себе», а функции их обработки тоже «сами по себе», тогда как моделировать удобнее и естественнее объекты, обладающие некой целостностью.
В принципе, механизм структур позволяет решить задачу объединения данных и функций, которые их обрабатывают, в одном объекте – ведь в структуры можно включать такие объекты, как указатели на функции. Указатель на функцию — это переменная, которая содержит адрес функции определённого типа с определённым набором параметров. Соответственно, косвенное обращение по этому указателю представляет собой вызов функции. А если переставить указатель на другую функцию того же типа с соответствующим списком параметров, то и будет вызвана другая функция. Поведение программы становится гибким, а значит, возможности моделирования возрастают.
Приведём пример. Мы хотим написать программу, позволяющую табулировать различные функции одним и тем же кодом. Естественным решением будет описание структуры, содержащей нужную информацию о функции:
Какая конкретно функция табулируется, будет определять указатель f , включённый в структуру. Передавая структуру типа tab в функцию табулирования run , мы сможем решить поставленную задачу:
Допишем программу, построив таблицы значений нескольких функций:
- в функцию run приходится передавать структуру или указатель на неё;
- по-прежнему функция run никак не связана со структурой;
- нельзя разделить или ограничить доступ к переменным и функциям структуры.
Можно сделать функцию «полноценным» членом структуры, но тогда всё равно придётся использовать инструментарий классов, немного забегая вперёд (показаны только изменённые участки кода):
1. В этом коде this — «указатель объекта на самого себя», this->x1 означает, что надо взять переменную x1 , описанную именно внутри текущей структуры (в fun1 при вызове fun1.run(); ), а не в другой структуре или глобально.
2. Оператор :: имеет в C++ наивысший приоритет и означает указание области видимости объекта; в нашем случае то, что функция run относится к структурному типу tab .
В таком коде видно, что функция относится именно к структуре, мы вызываем её в виде fun1.run() , а не run(fun1) . Кроме того, мы сэкономили память стека, не передавая целые структуры внутрь функций.
Хотя у любого объекта класса есть собственная копия всех данных-членов, каждая функция-член существует в единственном экземпляре. Функция-член может обращаться к данным-членам разных объектов с помощью указателя this . Код с классами легче модифицируется и остаётся управляемым в больших проектах. Все современные языки и системы программирования объектно ориентированы, то есть, используют механизм классов.
Итак, объектно-ориентированное программирование (ООП) — парадигма программирования, основанная на понятиях класса и объекта.
Объект – это сущность, обладающая определённым состоянием, которое характеризуется свойствами, и поведением, которое характеризуется методами.
С точки зрения процедурного программирования, свойствам соответствуют переменные или массивы, описывающие состояние объекта, методам — функции, позволяющие управлять свойствами или обмениваться информацией с внешним миром.
Например, у класса «Студент» могут быть такие свойства как фамилия, группа, дата рождения, стипендия и т.д., методы «показать данные», «сменить номер группы», «начислить стипендию» и т.п.
Объекты всегда принадлежат одному или нескольким классам, которые определяют поведение объекта и являются его моделью. Термины «объект» и «экземпляр класса» взаимозаменяемы.
Класс — абстрактный тип данных, определяющий интерфейс и реализацию для всех своих экземпляров.
Например, объектом (экземпляром) класса «Студент» является конкретный «студент Иванов»:
Объекты обладают тремя базовыми свойствами (они же – три базовых принципа ООП):
1. Инкапсуляция — механизм языка, разделяющий и ограничивающий доступ к составляющим объект компонентам (методам и свойствам). Например, приватные свойства и методы класса доступны только для экземпляров этого же, но не других классов.
Так, для класса «Служащий» свойство «Зарплата» может быть приватно. А для начисления зарплаты или получения сведений о ней мы можем предусмотреть в классе публичные методы «ПолучитьВеличинуЗП», «НачислитьЗП» и т.п. Вообще, чаще всего свойства класса приватны, а основные методы публичны. Это связано ещё и с тем, что прямое изменение свойств класса в виде объект.свойство=значение не даёт возможности выполнения дополнительных действий и затрудняет модификацию программы.
Пояснение этой важной мысли:
Код
конечно, выглядит естественней, чем
Но что, если свойство, изменённое нами «напрямую», меняет кто-то или что-то ещё? А если понадобилось при каждом «прямом» присваивании значения свойству сделать нечто дополнительно (сообщить в налоговую, что на счету добавилось денег)? А если нужно при присваивании провести встроенный контроль на допустимость значения? А в программе уже 1000 записей вида object.property=a ? В случае же object.setproperty(a) мы изменим только один-единственный метод setproperty в классе объекта.
2. Наследование — механизм языка, позволяющий описать новый класс на основе уже существующего (родительского, базового) класса. Класс-потомок может добавить собственные методы и свойства, а также пользоваться родительскими методами и свойствами.
Например, класс «Студент» в нашей программе может быть потомком класса «Человек», и пользоваться рядом его свойств и методов («Фамилия», «Дата рождения»), а также иметь свои, отсутствующие в базовом классе («номер группы», «стипендия»).
3. Полиморфизм – это единообразная обработка разнотипных данных, возможность перегрузки классами своих методов и методов классов-предков.
Например, базовый класс «Геометрическая фигура» может иметь метод «Нарисовать», по-разному реализованный в классах-потомках «Прямоугольник» и «Окружность». Но для программиста все методы будут вызываться под одним именем draw , что удобно.
В языках программирования, построенных на парадигме ООП, обычно имеется иерархия встроенных классов, на вершине которой находится класс с именем вроде Object . А все остальные классы являются его потомками того или иного иерархического «поколения».
- повысить степень повторного использования кода;
- обеспечить удобный механизм для коллективной разработки приложений;
- писать сколь угодно большие проекты, основанные на иерархии классов, в то время как процедурная программа длиннее 10-20 тысяч строк неизбежно становится плохо управляемой;
- более гибко управлять созданием, удалением и доступностью объектов в процессе выполнения программы;
- за счет механизма инкапусляции обеспечивать безопасность приложений.
Общий вид оператора описания класса следующий:
По умолчанию члены класса приватны, так что private: в начале можно было не указывать.
Функции-члены связаны с именем класса оператором :: и обычно описаны сразу после описания класса, к которому принадлежат. В остальном они выглядят как обычные функции С++:
Доступ к членам класса осуществляет оператор . (точка), как и для структур, он позволяет связать свойство или метод с конкретным экземпляром класса. При использовании указателей, как и со структурами, конструкция (*указатель).поле заменяется на указатель->поле .
Для того, чтобы вызываемая функция точно «знала», с каким из объектов класса она работает, Си++ неявно передает в любую нестатическую функцию дополнительный скрытый параметр — указатель на текущий объект, называемый this . Параметр this видим в теле вызываемой функции, устанавливается при вызове в значение адреса начала объекта и может быть использован для доступа к членам объекта.
Указание this позволит не перепутать имеющие одинаковые имена name параметр функции и имя одного из свойств класса.
Конструктор
Класс на Си++ может иметь любое количество конструкторов, предназначенных для инициализации данных класса при создании нового экземпляра. Конструктор всегда имеет то же имя, что и класс, в котором он определён, с созданием экземпляра всегда связано явное или неявное выполнение конструктора. Если отсутствует явно описанный конструктор, создается конструктор по умолчанию. Конструктор вызывается компилятором явно при создании объекта и неявно при выполнении оператора new, применяемого к объекту, а также при копировании объекта данного класса. Конструктор не возвращает значений. Как правило, он описан в public-секции класса. В остальном он оформляется как обычная функция, например с явно описанным конструктором наш класс примет вид:
Заметим, что в этом несложном классе уже хватает «подводных камней», например, функция setname не проверяет, достаточно ли места там, куда она копирует строку:
С другой стороны, мы не можем «заставлять» функцию каждый раз перераспределять память, освобождая прежнее содержимое – ведь для объектов из стека, таких как Petrov , делать delete попросту нельзя. Одним из возможных решений было бы «не копировать лишнего», учитывая то, что имени может быть и не задано:
Однако такая функция может породить серьёзные проблемы при копировании в неинициализированный объект наподобие того, что делается в строке
У такого объекта name может быть заполнено «мусором» и len получит неправильное значение, что неизбежно вызовет ошибку при попытке освобождения памяти. Приемлемой была бы такая реализация:
Но и это будет работать при условии, что свойство this->name предварительно инициализировалось в конструкторе до вызова setname .
Конструктор копирования для класса X имеет один аргумент типа &X и выполняется при присваивании экземпляров класса:
Компилятор «опознаёт» конструктор копирования потому, что его параметром является ссылка на объект класса. Это тот самый объект, что находится справа от знака «присвоить», а объект слева от » = » доступен через this . Напишем реализацию конструктора копирования:
Деструктор
Деструктор выполняет действия, противоположные действиям конструктора, то есть, «разрушает» объект данного класса и вызывается явно или неявно. Неявный вызов деструктора связан с прекращением существования объекта из-за завершения области его определения. Явное уничтожение объекта выполняет оператор delete . Деструктор имеет то же имя, что класс, но предваренное символом
. Деструкторы не могут получать аргументы и быть перегружены. Класс может объявить только один общедоступный деструктор. Если класс не содержит объявления деструктора, компилятор автоматически создаст его.
Для нашего класса явный деструктор может иметь вид:
Здесь в области видимости внутри тела деструктора нет других переменных с именем name , так что this->name можно не писать.
При этом деструктор был описан в секции public класса:
Если мы, при наличии явного деструктора, в приведённом выше коде
не задали явного конструктора копирования, конструктор копирования по умолчанию выполнил Copeikin.name = Ivanov->name , то есть, не скопировал строку, а лишь установил указатель Copeikin.name на тот же адрес памяти, куда показывал Ivanov->name . После явного или неявного удаления объекта Ivanov следствием попытки показать значение Copeikin.name будет крах программы. Подобные «потери памяти» очень трудноуловимы, поэтому за работой явных деструкторов нужно следить особенно внимательно.
Существует «правило большой тройки» для любых классов: если в классе есть явный деструктор, то требуется явный конструктор копирования и оператор » паскалевских» массивов на C++. Для этих массивов элементы можно нумеровать с произвольного целого числа, а не только с нуля.
Узнав о переопределении операторов, мы сможем обращаться к объектам класса в виде my[5] вместо my.get(5) .
Оператор : (двоеточие) в конструкторе класса означает «список инициализации». Это список, в котором через запятую перечислены пары из имени члена класса и значения, которое необходимо ему присвоить, взятого в круглые скобки. Через список инициализация членов класса происходит перед тем, как выполняется вход в тело конструктора, и выполняется быстрее. К тому же, константные данные можно инициализировать только через список.
Пример 2. Определим альтернативный класс «Студент» и выполним над его объектами основные описанные в лекции действия.
Немного примеров-рассуждений о конструкторах и деструкторах есть также в этой заметке.
Что такое инкапсуляция с
В прошлой теме использовался следующий класс Person:
Спецификатор public является спецификатором доступа (access specifier), то есть определяет параметры доступа к членам класса — переменным и функциям. В частности, он делает их доступными из любой части программы или открытыми. По сути спецификатор public определяет общедоступный интерфейс класса.
То есть в данном случае поля name и age являются открытыми, и мы можем присвоить им во внешнем коде любые значения:
В том числе можно присвоить какие-то недопустимые значения, например, отрицательное значение для возраста пользователя. Естественно это не очень хорошая ситуация.
Однако с помощью другого спецификатора private мы можем скрыть реализацию членов класса, то есть сделать их закрытыми, инкапсулировать внутри класса.
Перепишем класс Person с исключением спецификатора private:
Теперь переменные name и age в классе Person являются закрытыми, поэтому мы не можем обратиться к ним напрямую. Мы можем к ним обращаться только внутри класса.
Чтобы все таки можно было получить извне значения переменных name и age, определены дополнительные функции getAge и getName. Установить значение переменной name напрямую можно только через конструктор, а значение переменной age — через конструктор или через функцию setAge. При этом функция setAge устанавливает значение для переменной age, если оно соответствует определенным условиям.
Таким образом, состояние класса скрыто извне, к нему можно получить доступ только посредством дополнительно определенных функций, который представляют интерфейс класса.
Также стоит отметить, что если в классе отсутствует спецификатор доступа или для некоторых членов класса он не определен, то по умолчанию используется спецификатор доступа private:
Для переменных name и age здесь не определен спецификатор доступа, поэтому для них по умолчанию используется спецификатор private.
Основы объектно-ориентированного программирования

Все основанные на объектах языки (C#, Java, С++, Smalltalk, Visual Basic и т.п.) должны отвечать трем основным принципам объектно-ориентированного программирования (ООП), которые перечислены ниже:
Инкапсуляция
Как данный язык скрывает детали внутренней реализации объектов и предохраняет целостность данных?
Наследование
Как данный язык стимулирует многократное использование кода?
Полиморфизм
Как данный язык позволяет трактовать связанные объекты сходным образом?
Прежде чем погрузиться в синтаксические детали реализации каждого принципа, важно понять базовую роль каждого из них.
Роль инкапсуляции
— это механизм программирования, объединяющий вместе код и данные, которыми он манипулирует, исключая как вмешательство извне, так и неправильное использование данных. В объектно-ориентированном языке данные и код могут быть объединены в совершенно автономный черный ящик. Внутри такого ящика находятся все необходимые данные и код. Когда код и данные связываются вместе подобным образом, создается объект. Иными словами, объект — это элемент, поддерживающий инкапсуляцию.
Т.е. инкапсуляция представляет собой способности языка скрывать излишние детали реализации от пользователя объекта. Например, предположим, что используется класс по имени DatabaseReader, который имеет два главных метода: Open() и Close().
Фиктивный класс DatabaseReader инкапсулирует внутренние детали нахождения, загрузки, манипуляций и закрытия файла данных. Программистам нравится инкапсуляция, поскольку этот принцип ООП упрощает кодирование. Нет необходимости беспокоиться о многочисленных строках кода, которые работают «за кулисами», чтобы реализовать функционирование класса DatabaseReader. Все, что потребуется — это создать экземпляр и отправлять ему соответствующие сообщения (например, «открыть файл по имени AutoLot.mdf, расположенный на диске С:»).
С идеей инкапсуляции программной логики тесно связана идея защиты данных. В идеале данные состояния объекта должны быть специфицированы с использованием ключевого слова private (или, возможно, protected). Таким образом, внешний мир должен вежливо попросить, если захочет изменить или получить лежащее в основе значение. Это хороший принцип, поскольку общедоступные элементы данных можно легко повредить (даже нечаянно, а не преднамеренно).
Основной единицей инкапсуляции в C# является класс, который определяет форму объекта. Он описывает данные, а также код, который будет ими оперировать. В C# описание класса служит для построения объектов, которые являются экземплярами класса. Следовательно, класс, по существу, представляет собой ряд схематических описаний способа построения объекта.
Код и данные, составляющие вместе класс, называют членами. Данные, определяемые классом, называют полями, или переменными экземпляра. А код, оперирующий данными, содержится в функциях-членах, самым типичным представителем которых является метод. В C# метод служит в качестве аналога подпрограммы. (К числу других функций-членов относятся свойства, события и конструкторы.) Таким образом, методы класса содержат код, воздействующий на поля, определяемые этим классом.
Роль наследования
Следующий принцип ООП — наследование — касается способности языка позволять строить новые определения классов на основе определений существующих классов. По сути, наследование позволяет расширять поведение базового (или родительского) класса, наследуя основную функциональность в производном подклассе (также именуемом дочерним классом):

Т.е. наследование представляет собой процесс, в ходе которого один объект приобретает свойства другого объекта. Это очень важный процесс, поскольку он обеспечивает принцип иерархической классификации. Если вдуматься, то большая часть знаний поддается систематизации благодаря иерархической классификации по нисходящей.
Если не пользоваться иерархиями, то для каждого объекта пришлось бы явно определять все его свойства. А если воспользоваться наследованием, то достаточно определить лишь те свойства, которые делают объект особенным в его классе. Он может также наследовать общие свойства своего родителя. Следовательно, благодаря механизму наследования один объект становится отдельным экземпляром более общего класса.
Роль полиморфизма
Последний принцип ООП — полиморфизм. Он обозначает способность языка трактовать связанные объекты в сходной манере. В частности, этот принцип ООП позволяет базовому классу определять набор членов (формально называемый полиморфным интерфейсом), которые доступны всем наследникам. Полиморфный интерфейс класса конструируется с использованием любого количества виртуальных или абстрактных членов.
По сути, — это член базового класса, определяющий реализацию по умолчанию, которая может быть изменена (или, говоря более формально, переопределена) в производном классе. В отличие от него, — это член базового класса, который не предусматривает реализации по умолчанию, а предлагает только сигнатуру. Когда класс наследуется от базового класса, определяющего абстрактный метод, этот метод обязательно должен быть переопределен в производном классе. В любом случае, когда производные классы переопределяют члены, определенные в базовом классе, они по существу переопределяют свою реакцию на один и тот же запрос.
Рассмотрим для примера стек, т.е. область памяти, функционирующую по принципу «последним пришел — первым обслужен». Допустим, что в программе требуются три разных типа стеков: один — для целых значений, другой — для значений с плавающей точкой, третий — для символьных значений. В данном примере алгоритм, реализующий все эти стеки, остается неизменным, несмотря на то, что в них сохраняются разнотипные данные. В языке, не являющемся объектно-ориентированным, для этой цели пришлось бы создать три разных набора стековых подпрограмм с разными именами. Но благодаря полиморфизму для реализации всех трех типов стеков в C# достаточно создать лишь один общий набор подпрограмм. Зная, как пользоваться одним стеком, вы сумеете воспользоваться и остальными.
В более общем смысле понятие полиморфизма нередко выражается следующим образом: «один интерфейс — множество методов«. Это означает, что для группы взаимосвязанных действий можно разработать общий интерфейс. Полиморфизм помогает упростить программу, позволяя использовать один и тот же интерфейс для описания общего класса действий. Выбрать конкретное действие (т.е. метод) в каждом отдельном случае — это задача компилятора. Программисту не нужно делать это самому. Ему достаточно запомнить и правильно использовать общий интерфейс.