Министерство образования и науки Украины
Национальная металлургическая академия Украины
Кафедра автоматизации производственных процессов
КОНСПЕКТ ЛЕКЦИЙ
по дисциплине «Объектно-ориентированное программирование»
ЧАСТЬ 2
«Основы ООП: классы, объекты, перегрузка операций,
наследование, виртуальные функции, полиморфизм»
для студентов направления 6.0925 – “Автоматизация и компьютерно-интегрированные технологии”
Днепропетровск 2008
ОГЛАВЛЕНИЕ
1. КЛАССЫ И АБСТРАГИРОВАНИЕ ДАННЫХ 3
1.1. Принципы объектной ориентации 3
1.2. Определения структур 4
1.3. Доступ к элементам структуры 4
1.4. Использование определенного пользователем типа Time с помощью Struct 5
1.5. Использование абстрактного типа данных Time с помощью класса 7
1.6. Область действия класс и доступ к элементам класса 12
1.7. Отделение интерфейса от реализации 14
1.8. Управление доступом к элементам 17
1.9. Функции доступа и обслуживающие функции-утилиты 19
1.10. Инициализация объектов класса: конструкторы 21
1.11. Использование конструкторов с аргументами по умолчанию 22
1.12. Использование деструкторов 25
Вопросы для самопроверки 26
2. КЛАССЫ: КОМПОЗИЦИЯ И ДИНАМИЧЕСКОЕ УПРАВЛЕНИЕ ОБЪЕКТАМИ 27
2.1. Константные объекты и функции-элементы 27
2.2. Композиция: классы как элементы других классов 32
2.3. Дружественные функции и дружественные классы 36
2.4. Использование указателя this 37
2.5. Динамическое распределение памяти с помощью операций new и delete 42
2.6. Статические элементы класса 43
2.7. Абстракция данных и скрытие информации 47
2.8. Классы контейнеры и итераторы 49
Вопросы для самопроверки 49
3. ПЕРЕГРУЗКА ОПЕРАЦИЙ 51
3.1. Основы перегрузки операций 51
3.2. Ограничения на перегрузку операций 52
3.3. Функции-операции как элементы класса и как дружественные функции 53
3.4. Перегрузка операций поместить в поток и взять из потока 54
3.5. Перегрузка унарных операций 57
3.6. Перегрузка бинарных операций 57
3.7. Пример: класс массив 58
3.8. Преобразования типов 69
1. КЛАССЫ И АБСТРАГИРОВАНИЕ ДАННЫХ
1.1. Принципы объектной ориентации
Начнем знакомство с объектной ориентацией в С++. Техника объектно-ориентированного проектирования предполагает следующие основные этапы:
выполнение постановки задачи для построения модели или системы;
определение объектов, необходимых для реализации системы;
определение атрибутов для каждого объекта;
определение вариантов поведения каждого объекта;
указание способов взаимодействия объектов друг с другом для достижения глобальной цели системы.
Ключевым принципом объектной ориентации является инкапсуляция данных (атрибутов) и функций (вариантов поведения), в совокупности называемых объектами. Объекты обладают свойством скрытия информации. Это значит, что хотя объекты могут знать, как связываться друг с другом посредством хорошо определенного интерфейса, им обычно не позволено знать, как реализуются другие объекты, – детали реализации спрятаны внутри самих объектов. Так, можно ездить на автомобиле, не зная технических деталей его внутреннего функционирования – трансмиссии, выхлопной трубы и др.
В С и других процедурно-ориентированных языках программирование стремится быть ориентированным на действия, тогда как в идеале программирование на С++ объектно-ориентированное. В С единицей программирования является функция. В С++ единицей программирования является класс, на основе которого, в конечном счете, создаются объекты.
Программисты на С основное внимание уделяют написанию функций. Группы действий, выполняющие некоторую задачу, объединяются в функции, а функции группируются, чтобы образовать программу. Данные несомненно важны в С, но современная точка зрения состоит в том, что данные существуют в первую очередь для поддержки действий, выполняемых функциями. Глаголы в описании проектируемой системы помогают программисту на С определить множество функций, которые будут совместно работать для реализации системы.
Программисты на С++ основное внимание уделяют созданию своих собственных определяемых пользователем типов, называемых классами. Классы –это типы, определяемые программистом. Каждый класс содержит данные и набор функций, манипулирующих с этими данными. Компоненты-данные класса называются данными-элементами. Компоненты-функции класса называются функциями-элементами. Подобно тому, как сущность встроенного типа, такого, как int, называется переменной, сущность определяемого пользователем типа (т.е. класса) называется объектом. Центром внимания в С++ являются не функции, а объекты. Имена существительные в описании проектируемой системы помогают программисту на С++ определить множество классов. Эти классы используются для создания объектов, которые будут совместно работать для реализации системы.
Классы в С++ являются естественным продолжением структуры struct в С. Прежде чем рассматривать специфику разработки классов на С++, мы обсудим структуры и построим определенный пользователем тип, основанный на структуре. Слабости, которые мы выявим в этом подходе, помогут объяснить запись класса.
1.2. Определения структур
Структуры – это составные типы данных, построенные с использованием других типов. Рассмотрим следующее определение структуры:
Ключевое слово struct начинает определение структуры. Идентификатор Time – тег (обозначение, имя-этикетка) структуры. Тэг структуры используется при объявлении переменных структур данного типа. В этом примере имя нового типа — Time. Имена, объявленные в фигурных скобках описания структуры – это элементы структуры. Элементы одной и той же структуры должны иметь уникальные имена, но две разные структуры могут содержать не конфликтующие элементы с одинаковыми именами. Каждое определение структуры должно заканчиваться точкой с запятой. Приведенное объяснение верно и для классов.
Определение Time содержит три элемента типа int – hour, minute и second (часы, минуты и секунды). Элементы структуры могут быть любого типа и одна структура может содержать элементы многих разных типов. Структура не может, однако, содержать экземпляры самой себя. Например, элемент типа Time не может быть объявлен в определении структуры Time. Однако, может быть включен указатель на другую структуру Time. Структура, содержащая элемент, который является указателем на такой же структурный тип, называется структурой с самоадресацией. Структуры с самоадресацией полезны для формирования связных структур данных.
Предыдущее определение структуры данных не резервирует никакого пространства в памяти; определение только создает новый тип данных, который используется для объявления переменных. Переменные структуры объявляются так же, как переменные других типов. Объявление
объявляет timeObject переменной типа Time, timeArray – массивом с 10 элементами типа Time, а timePtr – указателем на объект типа Time.
1.3. Доступ к элементам структуры
Для доступа к элементам структуры (или класса) используются операции доступа к элементам — операция точка (.) и операция стрелка (->). Операция точка обращается к элементу структуры (или класса) по имени переменной объекта или по ссылке на объект. Например, чтобы напечатать элемент hour структуры timeObiect используется оператор
cout << timeObject.hour;
Операция стрелка, состоящая из знака минус (-) и знака больше (>), записанных без пробела, обеспечивает доступ к элементу структуры (или класса) через указатель на объект. Допустим, что указатель timePtr был уже объявлен как указывающий на объект типа Time и что адрес структуры timeObject был уже присвоен timePtr. Тогда, чтобы напечатать элемент hour структуры timeObject с указателем timePtr, можно использовать оператор
cout << timePtr->hour;
Выражение timePtr->hour; эквивалентно (*timePtr).hour, которое разыменовывает указатель и делает доступным элемент hour через операцию точка. Скобки нужны здесь потому, что операция точка имеет более высокий приоритет, чем операция разыменования указателя (*). Операции стрелка и точка наряду с круглыми и квадратными скобками имеют второй наивысший приоритет (после операции разрешения области действия) и ассоциативность слева направо.
1.4. Использование определенного пользователем типа Time с помощью Struct
Программа на рис. 1.1 создает определенный пользователем тип структуры Time с тремя целыми элементами: hour, minute и second. Программа определяет единственную структуру типа Time, названную dinnerTime, и использует операцию точка для присвоения элементам структуры начальных значений 18 для hour, 30 для minute и 0 для second. Затем программа печатает время в военном (24-часовом) и стандартном (12-часовом) форматах. Заметим, что функции печати принимают ссылки на постоянные структуры типа Time. Это является причиной того, что структуры Time передаются печатающим функциям по ссылке – этим исключаются накладные расходы на копирование, связанные с передачей структур функциям по значению, а использование const предотвращает изменение структуры типа Time функциями печати.
Существуют препятствия созданию новых типов данных указанным способом с помощью структур. Поскольку инициализация структур специально не требуется, можно иметь данные без начальных значений и вытекающие отсюда проблемы. Даже если данные получили начальные значения, возможно, это было сделано неверно. Неправильные значения могут быть присвоены элементам структуры (как мы сделали на рис.1.1), потому что программа имеет прямой доступ к данным. Программа присвоила плохие значения всем трем элементам объекта dinnerTime типа Time. Если реализация struct изменится (например, время теперь будет представляется как число секунд после полуночи), то все программы, которые используют struct, нужно будет изменить. Не существует никакого «интерфейса», гарантирующего, что программист правильно использует тип данных, и что данные являются непротиворечивыми.
Результат выполнения программы:
Рисунок 1.1. Создание структуры, задание и печать ее элементов
Используем далее уже известную структуру Time, но уже как класс, и продемонстрируем некоторые преимущества создания таких так называемых абстрактных типов данных, как классы. Классы и структуры в С++ можно использовать почти одинаково. Различие между ними состоит в доступности по умолчанию элементов каждого из этих типов.
1.5. Использование абстрактного типа данных Time с помощью класса
Классы предоставляют программисту возможность моделировать объекты, которые имеют атрибуты (представленные как данные-элементы) и варианты поведения или операции (представленные как функции-элементы). Типы, содержащие данные-элементы и функции-элементы, обычно определяются в C++ с помощью ключевого слова class.
Функции-элементы иногда в других объектно-ориентированных языках называют методами, они вызываются в ответ на сообщения, посылаемые объекту. Сообщение соответствует вызову функции-элемента.
Когда класс определен, имя класса может быть использовано для объявления объекта этого класса. Рис.1.2 содержит простое определение класса Time.
Определение нашего класса Time начинается с ключевого слова class. Тело определения класса заключается в фигурные скобки ({ }). Определение класса заканчивается точкой с запятой. Определение нашего класса Time, как и нашей структуры Time, содержит три целых элемента hour, minute и second.
Рисунок 1.2. Простое определение класса Time
Остальные части определения класса – новые. Метки public: (открытая) и private: (закрытая) называются спецификаторами доступа к элементам. Любые данные-элементы и функции-элементы, объявленные после спецификатора доступа к элементам public: (и до следующего спецификатора доступа к элементам), доступны при любом обращении программы к объекту класса Time. Любые данные-элементы и функции-элементы, объявленные после спецификатора доступа к элементам private: (и до следующего спецификатора доступа к элементам), доступны только функциям-элементам этого класса. Спецификаторы доступа к элементам всегда заканчиваются двоеточием (:) и могут появляться в определении класса много раз и в любом порядке.
Определение класса в нашей программе содержит после спецификатора доступа к элементам public прототипы следующих четырех функций-элементов: Time, setTime, printMilitary и printStandard. Это – открытые функции-элементы или открытый интерфейс услуг класса. Эти функции будут использоваться клиентами класса (т.е. частями программы, играющими роль пользователей) для манипуляций с данными этого класса.
Функция-элемент с тем же именем, что и класс называется конструктором этого класса. Конструктор – это специальная функция-элемент, которая инициализирует данные-элементы объекта этого класса. Конструктор класса вызывается автоматически при создании объекта этого класса. Обычно класс имеет несколько конструкторов; это достигается посредством перегрузки функции.
После спецификатора доступа к элементам private следуют три целых элемента. Это говорит о том, что эти данные-элементы класса являются доступными только функциям-элементам класса и «друзьям» класса. Таким образом, данные-элементы могут быть доступны только четырем функциям, прототипы которых включены в определение этого класса (или друзей этого класса). Обычно данные-элементы перечисляются в части private, а функции-элементы – в части public. Как мы увидим далее, можно иметь функции-элементы private и данные public; последнее не типично и считается в программировании дурным вкусом.
Когда класс определен, его можно использовать в качестве типа в объявлениях, например, следующим образом:
Имя класса становится новым спецификатором типа. Может существовать множество объектов класса как и множество переменных типа, например, такого, как int. Программист по мере необходимости может создавать новые тины классов. Это одна из многих причин, по которым C++ является расширяемым языком.
Программа на рис.1.3 использует класс Time. Эта программа создает единственный объект класса Time, названный t. Когда объект создается, автоматически вызывается конструктор Time, который явно присваивает нулевые начальные значения всем данным-элементам закрытой части private. Затем печатается время в военном и стандартном форматах, чтобы подтвердить, что элементы получили правильные начальные значения. После этого с помощью функции-элемента setTime устанавливается время, и оно снова печатается в обоих форматах. Затем функция-элемент setTime пытается дать данным-элементам неправильные значения, и время снова печатается в обоих форматах.
Данные-элементы hour, minute и second предваряются спецификатором доступа к элементам private. Эти закрытые данные-элементы класса обычно недоступны вне класса (друзья класса могут иметь доступ к закрытым элементам класса.) Глубокий смысл такого подхода заключается в том, что истинное представление данных внутри класса не касается клиентов класса. Например, было бы вполне возможно изменить внутреннее представление и представлять, например, время внутри класса как число секунд после полуночи. Клиенты могли бы использовать те же самые открытые функции-элементы и получать те же самые результаты, даже не осознавая произведенных изменений. В этом смысле, говорят, что реализация класса скрыта от клиентов. Такое скрытие информации способствует модифицируемости многих программ и упрощает восприятие класса клиентами.
Результат выполнения программы:
Рисунок 1.3. Использование АТД Timeкак класса
В этой программе (рис.1.3) конструктор Time просто присваивает начальные значения, равные 0, данным-элементам, (т.е. задает военное время, эквивалентное 12 AM). Это гарантирует, что объект при его создании находится в известном состоянии. Неправильные значения не могут храниться в данных-элементах объекта типа Time, поскольку конструктор автоматически вызывается при создании объекта типа Time, а все последующие попытки изменить данные-элементы тщательно рассматриваются функцией setTime.
Данные-элементы класса не могут получать начальные значения в теле класса, где они объявляются. Эти данные-элементы должны получать начальные значения с помощью конструктора класса или им можно присваивать значения через функции.
Функция с тем же именем, что и класс, но со стоящим перед ней символом тильда (~) называется деструктором этого класса (пример на рис.1.3 не включает деструктор). Деструктор производит «завершающие служебные действия» над каждым объектом класса перед тем, как память, отведенная под этот объект, будет повторно использована системой.
Функции, которыми класс снабжает внешний мир, предваряются меткой public. Открытые функции реализуют все возможности класса, необходимые для его клиентов. Открытые функции класса называют интерфейсом класса или открытым интерфейсом.
Отметим использование бинарной операции разрешения области действия (::) в каждом определении функции-элемента, следующем за определением класса на рис.1.3. После того, как класс определен, и его функции-элементы объявлены, эти функции-элементы должны быть описаны. Каждая функция-элемент может быть описана прямо в теле класса (вместо включения прототипа функции класса) или после тела класса. Когда функция-элемент описывается после соответствующего определения класса, имя функции предваряется именем класса и бинарной операцией разрешения области действия (::) Поскольку разные классы могут иметь элементы с одинаковыми именами, операция разрешения области действия «привязывает» имя элемента к имени класса, чтобы однозначно идентифицировать функции-элементы данного класса.
Несмотря на то, что функция-элемент, объявленная в определении класса, может быть описана вне этого определения, эта функция-элемент все равно имеет областью действия класс, т.е. ее имя известно только другим элементам класса пока к ней обращаются посредством объекта класса, ссылки на объект класса или указателя на объект класса.
Если функция-элемент описана в определении класса, она автоматически встраивается inline. Функция-элемент, описанная вне определения класса, может быть сделана встраиваемой посредством явного использования ключевого слова inline. Напомним, что компилятор резервирует за собой право не встраивать никаких функций.
Функции-элементы printMilitary и printStandard не получают никаких аргументов. Это происходит потому, что функции-элементы неявно знают, что они печатают данные-элементы определенного объекта типа Time, для которого они активизированы. Это делает вызовы функций-элементов более краткими, чем соответствующие вызовы функций в процедурном программировании. Это уменьшает также вероятность передачи неверных аргументов, неверных типов аргументов или неверного количества аргументов.
Классы упрощают программирование, потому что клиент (или пользователь объекта класса) имеет дело только с операциями, инкапсулированными или встроенными в объект. Такие операции обычно проектируются ориентированными именно на клиента, а не на удобную реализацию. Клиентам нет необходимости касаться реализации класса. Интерфейсы меняются, но не так часто, как реализации. При изменении реализации соответственно должны изменяться ориентированные на реализацию коды. А путем скрытия реализации мы исключаем возможность для других частей программы оказаться зависимыми от особенностей реализации класса.
Часто классы не создаются «на пустом месте». Обычно они являются производными от других классов, обеспечивающих новые классы необходимыми им операциями. Или классы могут включать объекты других классов как элементы. Такое повторное использование программного обеспечения значительно увеличивает производительность программиста. Создание новых классов на основе уже существующих классов называется наследованием. Включение классов как элементов других классов называется композицией.
1.6. Область действия класс и доступ к элементам класса
Данные-элементы класса (переменные, объявленные в определении класса) и функции-элементы (функции, объявленные в определении класса) имеют областью действия класс. Функции, не являющиеся элементом класса, имеют областью действия файл.
При области действия класс элементы класса непосредственно доступны всем функциям-элементам этого класса и на них можно ссылаться просто по имени. Вне области действия класс к элементам класса можно обращаться либо через имя объекта, либо ссылкой на объект, либо с помощью указателя на объект.
Функции-элементы класса можно перегружать, но только с помощью других функций-элементов класса. Для перегрузки функции-элемента просто обеспечьте в определении класса прототип для каждой версии перегруженной функции и снабдите каждую версию функции отдельным описанием.
Функции-элементы имеют внутри класса область действия функцию: переменные, определенные в функции-элементе, известны только этой функции. Если функция-элемент определяет переменную с тем же именем, что и переменная в области действия класс, последняя делается невидимой в области действия функция. Такая скрытая переменная может быть доступна посредством операции разрешения области действия с предшествующим этой операции именем класса. Невидимые глобальные переменные могут быть доступны с помощью унарной операции разрешения области действия.
Операции, использованные для доступа к элементам класса, аналогичны операциям, используемым для доступа к элементам структуры. Операция выбора элемента точка (.) комбинируется для доступа к элементам объекта с именем объекта или со ссылкой на объект. Операция выбора элемента стрелка (->) комбинируется для доступа к элементам объекта с указателем на объект.
Программа на рис.1.4 использует простой класс, названный Count, с открытым элементом данных х типа int и открытой функцией-элементом print, чтобы проиллюстрировать доступ к элементам класса с помощью операций выбора элемента. Программа создает три экземпляра переменных типа Count — counter, counterRef (ссылка на объект типа Count) и counterPtr (указатель на объект типа Count). Переменная counterRef объявлена, чтобы ссылаться на counter, переменная counterPtr объявлена, чтобы указывать на counter. Важно отметить, что здесь элемент данных х сделан открытым просто для того, чтобы продемонстрировать способы доступа к открытым элементам.
Результат выполнения программы:
Рисунок 1.4. Доступ к данным-элементам объекта и функциям-элементам посредством имени объекта, ссылки и указателя на объект
1.7. Отделение интерфейса от реализации
Один из наиболее фундаментальных принципов разработки хорошего программного обеспечения состоит в отделении интерфейса от реализации. Это облегчает модификацию программ. Что касается клиентов класса, то изменения в реализации класса не влияют на клиента до тех пор, пока интерфейс класса, изначально предназначенный для клиента, остается неизменным (функции класса можно расширять за пределы исходного интерфейса).
Для отделения интерфейса от реализации помещайте объявление класса в заголовочный файл, чтобы оно было доступно любому клиенту, который захочет использовать класс. Это формирует открытый интерфейс класса. Помещайте определения функций-элементов класса в исходный файл. Это формирует реализацию класса.
Это стимулирует независимых продавцов программного обеспечения поставлять библиотеки классов для продажи или лицензирования. Продавцы поставляют в своих продуктах только заголовочные файлы и объектные модули. Не выдается никакой оригинальной, патентоспособной информации, как это было бы в случае поставки исходных кодов. Сообщество пользователей C++ извлекает выгоду, имея в своем распоряжении большинство библиотек классов, поставляемых независимыми продавцами.
На самом деле все выглядит не в таком розовом свете. Заголовочные файлы содержат некоторую часть реализации и краткие сведения о других частях реализации. Встраиваемые функции-элементы, например, должны находиться в заголовочном файле, так что когда компилятор компилирует клиента, клиент может включить определение встраиваемой функции inline. Закрытые элементы перечисляются в определении класса в заголовочном файле, так что эти элементы видимы для клиентов, несмотря на то, что клиенты не могут иметь к ним доступа.
Рисунок 5 разбивает программу на рис.1.3 на ряд файлов. При построении программы на C++ каждое определение класса обычно помещается в заголовочный файл, а определения функций-элементов этого класса помещаются в файлы исходных кодов с теми же базовыми именами. Заголовочные файлы включаются (посредством #include) в каждый файл, в котором используется класс, а файлы с исходными кодами компилируются и компонуются с файлом, содержащим главную программу. Документация на используемый компилятор определяет как компилировать и компоновать программы, содержащие множество исходных файлов.
Программа на рис.1.5 состоит из заголовочного файла timel.h, в котором объявляется класс Time, файла timel.cpp, в котором описываются функции-элементы класса Time, и файла fig6_5.cpp, в котором описывается функция main. Выходные данные этой программы идентичны выходным данным программы на рис.1.3.
//Заголовочный файл класса Time
//Исходный файл определений функций-элементов класса Time
//Программа драйвера класса Time
Результат выполнения программы:
Рисунок 1.5. Отделение интерфейса от реализации
Объявление класса заключено в следующие директивы препроцессора:
// Предотвращение многократного включения заголовочного файла
#ifndef TIME1_H
#define TIME1_H
…
#endif
При построении больших программ в заголовочные файлы будут помещаться также и другие определения и объявления. Приведенные выше директивы препроцессора предотвращают включение кода между #ifndef и #endif, если определено имя ТIМЕ1_Н. Если заголовок еще не включался в файл, то имя ТIМЕ1_Н определяется директивой #define и операторы заголовочного файла включаются в результирующий файл. Если же заголовок уже был включен ранее, ТIМЕ1_Н уже определен и операторы заголовочного файла повторно не включается. Попытки многократного включения заголовочного файла обычно случаются в больших программах с множеством заголовочных файлов, которые могут сами включать другие заголовочные файлы. Замечание: по негласному соглашению в приведенных выше директивах используется имя символической константы, представляющее собой просто имя заголовочного файла с символом подчеркивания вместо точки.
1.8. Управление доступом к элементам
Спецификаторы доступа к элементу public и private (а также, protected – защищенные) используются для управления доступом к данным-элементам класса и функциям-элементам. По умолчанию режим доступа для классов — private (закрытый), так что все элементы после заголовка класса и до первого спецификатора доступа являются закрытыми. После каждого спецификатора режим доступа, определенный им, действует до следующего спецификатора или до завершающей правой скобки (}) определения класса. Спецификаторы private, public и protected могут быть повторены, но такое употребление редко и может привести к беспорядку.
Закрытые элементы класса могут быть доступны только для функций-элементов (и дружественных функций) этого класса. Открытые элементы класса могут быть доступны для любых функций в программе.
Основная задача открытых элементов состоит в том, чтобы дать клиентам класса представление о возможностях (услугах), которые обеспечивает класс. Этот набор услуг составляет открытый интерфейс класса. Клиентов класса не должно касаться, каким образом класс выполняет их задачи. Закрытые элементы класса и описания открытых функций-элементов недоступны для клиентов класса. Эти компоненты составляют реализацию (implementation) класса.
Клиент класса может быть функцией-элементом другого класса или глобальной функцией. По умолчанию доступ к элементам класса — private. Доступ к элементам класса можно явно установить как public, protected или private. В отличие от этого доступ к элементам структуры struct по умолчанию — public. Доступ к элементам структуры struct также может быть установлен явно как public, protected или private.
Поскольку элементы класса по умолчанию являются закрытыми, никогда не возникает необходимости явно использовать спецификатор доступа к элементам private. Однако, многие программисты предпочитают сначала описывать интерфейс класса (т.е. открытые элементы класса), а затем перечислять закрытые элементы, откуда вытекает необходимость явного использования в определении класса спецификатора доступа к элементам private.
Из того, что данные класса закрытые, не следует, что клиенты не могут изменять эти данные. Данные могут быть изменены функциями-элементами или друзьями этого класса. Эти функции должны быть спроектированы так, чтобы гарантировать целостность данных.
Доступ к закрытым данным класса должен тщательно контролироваться использованием функций-элементов, называемых функциями доступа. Например, чтобы разрешить клиентам прочитать закрытое значение данных, класс может иметь функцию «получить» («get»). Чтобы дать клиентам возможность изменять закрытые данные, класс может иметь функцию «установить» («set»). Казалось бы, подобные изменения противоречат смыслу закрытых данных. Но функция-элемент set (установить) может обеспечить проверку правильности данных (например, проверку диапазона) и дать уверенность в том, что данные установлены верно. Функция set может также быть переводчиком между формой данных, используемой в интерфейсе, и формой, используемой в реализации. Функция get (получить) не требует представления данных в «сыром», необработанном виде; функция get может редактировать данные и ограничивать область данных, видимых клиенту.
1.9. Функции доступа и обслуживающие функции-утилиты
He все функции-элементы необходимо делать открытыми как часть интерфейса класса. Некоторые функции-элементы оставляются закрытыми и служат обслуживающими функциями-утилитами для других функций класса.
Функции доступа могут читать или отображать данные. Другим типичным применением функций доступа является проверка истинности или ложности условий — такие функции часто называют предикатными функциями. Примером предикатной функции могла бы быть функция isEmpty для любого класса контейнера – класса, способного содержать внутри себя много объектов, например, связного списка, стека или очереди. Программа проверяла бы функцию isEmpty прежде, чем пытаться прочесть очередной элемент из объекта контейнера. Предикатная функция isFull могла бы проверять объект класса контейнер, чтобы выяснить, имеется ли в нем дополнительное пространство.
Рис.1.6 демонстрирует запись функции-утилиты. Функция-утилита не является частью интерфейса класса. Она является закрытой функцией-элементом, которая поддерживает работу открытых функций-элементов класса. Функции-утилиты не предназначены для использования клиентами класса.
Класс Salesperson имеет массив, содержащий 12 сведений о месячных продажах, которым с помощью конструктора присвоены нулевые начальные значения и которым значения, задаваемые пользователем, устанавливаются с помощью функции setSales. Открытая функция-элемент printAnnualSales печатает сумму продаж за последние 12 месяцев. Функция-утилита totalAnnualSales суммирует сведения о продажах за 12 месяцев, обеспечивая работу printAnnualSales. Функция-элемент printAnnualSales редактирует сведения о продажах и переводит их в формат суммы долларов.
Результат выполнения программы:
Рисунок 1.6. Использование функции-утилиты
1.10. Инициализация объектов класса: конструкторы
После создания объекта его элементы могут быть инициализированы с помощью функции конструктор. Конструктор — это функция-элемент класса с тем же именем, что и класс. Программист предусматривает конструктор, который затем автоматически вызывается при создании объекта (создании экземпляра класса). Данные-элементы класса не могут получать начальные значения в определении класса. Они либо должны получить эти значения в конструкторе класса, либо их значения можно установить позже, после создания объекта. Конструкторы не могут указывать типы возвращаемых значений или возвращать какие-то значения. Конструкторы можно перегружать, чтобы обеспечить множество начальных значений объектов класса.
Когда объявляется объект класса, между именем объекта и точкой с запятой можно в скобках указать список инициализации элементов. Эти начальные значения передаются в конструктор класса.
1.11. Использование конструкторов с аргументами по умолчанию
Конструктор из timel.cpp (рис.1.5) присваивает нулевые (т.е. соответствующие 12 часам по полуночи в военном формате времени) начальные значения переменным hour, minute и second. Конструктор может содержать значения аргументов по умолчанию. Программа на рис.1.7 переопределяет функцию конструктор Time так, чтобы она включала нулевые значения аргументов по умолчанию для каждой переменной. Задание в конструкторе аргументов по умолчанию позволяет гарантировать, что объект будет находиться в непротиворечивом состоянии, даже если в вызове конструктора не указаны никакие значения. Созданный программистом конструктор, у которого все аргументы — аргументы по умолчанию (или который не требует никаких аргументов), называется конструктором с умолчанием, т.е. конструктором, который можно вызывать без указания каких-либо аргументов.
Рисунок 1.7. Использование конструктора с аргументами по умолчанию
Для каждого класса может существовать только один конструктор с умолчанием. В этой программе конструктор вызывает функцию-элемент setTime со значениями, передаваемыми конструктору (или значениями по умолчанию), чтобы гарантировать, что значение, предназначенное для hour, находится в диапазоне от 0 до 23, а значения для minute и second — в диапазоне от 0 до 59. Если значение выходит за пределы диапазона, оно устанавливается равным нулю с помощью setTime (это пример гарантии того, что данные-элементы будут в непротиворечивом состоянии).
Результат выполнения программы:
Программа на рис.1.7 создает 5 экземпляров объектов Time и задает им начальные значения; одному — со всеми тремя аргументами по умолчанию в вызове конструктора, второму — с одним указанным аргументам, третьему — с двумя указанными аргументами, четвертому — с тремя указанными аргументами и пятому — с тремя неверно указанными аргументами. Отображается содержание данных каждого объекта после его создания и задания начальных значений.
Если для класса не определено никакого конструктора, компилятор создает конструктор с умолчанием. Такой конструктор не задает никаких начальных значений, так что после создания объекта нет никакой гарантии, что он находится в непротиворечивом состоянии.
1.12. Использование деструкторов
Деструктор — это специальная функция-элемент класса. Имя деструктора совпадает с именем класса, но перед ним ставится символ тильда (~). Это соглашение о наименовании появилось интуитивно, потому что операция тильда является поразрядной операцией дополнения, а по смыслу деструктор является дополнением конструктора.
Деструктор класса вызывается при уничтожении объекта — например, когда выполняемая программа покидает область действия, в которой был создан объект этого класса. На самом деле деструктор сам не уничтожает объект — он выполняет подготовку завершения перед тем, как система освобождает область памяти, в которой хранился объект, чтобы использовать ее для размещения новых объектов.
Деструктор не принимает никаких параметров и не возвращает никаких значений. Класс может иметь только один деструктор — перегрузка деструктора не разрешается.
Деструкторы редко используются с простыми классами. Они имеют смысл в классах, использующих динамическое распределение памяти под объекты (например, для массивов и строк).
Вопросы для самопроверки
1. Заполните пробелы в следующих утверждениях:
a) Ключевое слово ______ начинает определение структуры.
b) Элементы класса доступны посредством операции ______ в сочетании с объектом класса или посредством операции ____ в сочетании с указателем на объект класса.
c) Элементы класса, указанные как ___ , доступны только функциям-элементам класса и друзьям класса.
d) ____ является специальной функцией-элементом, используемой для задания начальных значений элементам данных класса-
e) По умолчанию доступ к элементам класса — _____.
f) Функция _____ используется для присваивания значений закрытым данным-элементам класса.
g) _____ можно использовать для присваивания объекта класса другому объекту того же класса.
h) Функции-элементы класса обычно делаются ______ типа, а
данные-элементы — _______ типа.
i) Функция ______ используется для получения значений закрытых данных класса.
j) Набор открытых функций-элементов класса рассматривается как ________ класса.
k) Говорят, что реализация класса скрыта от его клиентов или ________________.
l) Для введения определения класса можно использовать ключевые слова ____ и ___.
m) Элементы класса, указанные как ______ , доступны везде в области действия объекта класса.
2. Найдите ошибку (или ошибки) в каждом из следующих пунктов и объясните, как их исправить.
a) Допустим, что в классе Time объявлен следующий прототип,
void ~Time (int);
b) Следующий фрагмент является частью определения класса Time.
class Time {
public;
// прототипы функций
private:
int hour = 0;
int. minute = 0;
int second = 0;
};
c) Допустим, что в классе Employee объявлен следующий прототип.
Int Employee(const char *, const *);
2. КЛАССЫ: КОМПОЗИЦИЯ И ДИНАМИЧЕСКОЕ УПРАВЛЕНИЕ ОБЪЕКТАМИ
2.1. Константные объекты и функции-элементы
Одним из наиболее фундаментальных принципов создания хорошего программного обеспечения является принцип наименьших привилегий. Рассмотрим один из способов применения этого принципа к объектам.
Некоторые объекты должны допускать изменения, другие – нет. Программист может использовать ключевое слово const для указания на то, что объект неизменяем – является константным и что любая попытка изменить объект является ошибкой. Например, в строке
const Time noon(12, 0, 0);
объявляется константный объект noon класса Time и присваивается ему начальное значение 12 часов пополудни.
Объявление константного объекта помогает провести в жизнь принцип наименьших привилегий. Случайные попытки изменить объект отлавливаются во время компиляции и не вызывают ошибок во время выполнения.
Компиляторы C++ воспринимают объявления const настолько неукоснительно, что в итоге не допускают никаких вызовов функций-элементов константных объектов (некоторые компиляторы дают в этих случаях только предупреждения). Это жестоко, поскольку клиенты объектов, возможно, захотят использовать различные функции-элементы чтения «get», а они, конечно, не изменяют объект. Чтобы обойти это, программист может объявить константные функции-элементы; только они могут оперировать константными объектами. Конечно, константные функции-элементы не могут изменять объект – это не позволит компилятор.
Константная функция указывается как const и в объявлении, и в описании с помощью ключевого слова const после списка параметров функции, но перед левой фигурной скобкой, которая начинает тело функции. Например, в приведенном ниже предложении объявлена как константная функция-элемент некоторого класса А