<< Предыдущая страница

 3. Объектно-ориентированное программирование и С++

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

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

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

Глава носит обзорный характер, она призвана познакомить читателя со специальной терминологией ООП, к которой автор вынужден прибегать на протяжении всей книги. Это вызвано тем, что C++Builder является типичной системой ООП и претендует на кульминационную роль в истории его развития.

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

Инкапсуляция есть объединение в едином объекте данных и кодов, оперирующих с этими данными. В терминологии ООП данные называются членами данных (data members) объекта, а коды - объектными методами или функциями-членами (methods, member functions).

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

Другим немаловажным следствием инкапсуляции является легкость обмена объектами, переноса их из одной программы в другую. Простота и доступность принципа инкапсуляции ООП стимулирует программистов к расширению Библиотеки Визуальных Компонент, входящей в состав C++Builder.

3.2 Классы, компоненты и объекты

Класс не имеет физической сущности, его ближайшей аналогией является объявление структуры. Память выделяется только тогда, когда класс используется для создания объекта. Этот процесс также называется созданием экземпляра класса (class instance).

Любой объект языка C++ имеет одинаковые атрибуты и функциональность с другими объектами того же класса. За создание своих классов и поведение объектов этих классов полную ответственность несет сам программист. Работая в некоторой среде, программист получает доступ к обширным библиотекам стандартных классов (например, к Библиотеке Визуальных Компонент C++Builder).

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

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

C++Builder вводит понятие компонент (components) - специальных классов, свойства которых представляют атрибуты объектов, а их методы реализуют операции над соответствующими экземплярами компонентных классов. Понятие метод обычно используется в контексте компонентных классов и внешне не отличается от термина функция-член обычного класса. C++Builder позволяет манипулировать видом и функциональным поведением компонент не только с помощью методов (как это делают функции-члены обычных классов), но и посредством свойств и событий, присущих только классам компонент. Работая в среде C++Builder, вы наверняка заметите, что манипулировать с компонентным объектом можно как на стадии проектирования приложения, так и во время его выполнения.

Свойства (properties) компонент представляют собой расширение понятия членов данных и хотя не хранят данные как таковые, однако обеспечивают доступ к членам данных объекта. C++Builder использует ключевое слово _property для объявления свойств. При помощи событий (events) компонента сообщает пользователю о том, что на нее оказано некоторое предопределенное воздействие. Основная сфера применения методов в программах, разрабатываемых в среде C++Builder -это обработчики событий (event handlers), которые реализуют реакцию программы на возникновение определенных событий. Легко заметить некоторое сходство событий и сообщений операционной системы Windows. Типичные простые события —нажатие кнопки или клавиши на клавиатуре. Компоненты инкапсулируют свои свойства, методы и события.

На первый взгляд компоненты ничем не отличаются от других объектных классов языка C++, за исключением ряда особенностей, среди которых пока отметим следующие:

• Большинство компонент представляют собой элементы управления интерфейсом с пользователем, причем некоторые обладают весьма сложным поведением.

• Все компоненты являются прямыми или косвенными потомками одного общего класса-прародителя (TComponent).

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

• Компоненты размещаются только в динамической памяти кучи (heap) с помощью оператора new, а не на стеке, как объекты обычных классов.

• Свойства компонент заключают в себе RTTI - идентификацию динамических типов.

• Компоненты можно добавлять к Палитре компонент и далее манипулировать с ними посредством Редактора форм интегрированной среды визуальной разработки C++Builder.

ООП интерпретирует взаимодействие с объектами как посылку запросов некоторому объекту или между объектами. Объект, принявший запрос, реагирует вызовом соответствующего метода. В отличие от других языков ООП, таких как SmallTalk, C++ не поощряет использование понятия "запрос". Запрос - это то, что делается с объектом, а метод - это то, как объект реагирует на поступивший запрос.

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

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

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

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

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

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

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

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

3.4 Разработка классов

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

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

3.4.1 Объявление базового класса

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

Каждое объявление внутри класса определяет привилегию доступа к именам класса в зависимости от того, в какой секции имя появляется. Каждая секция начинается с одного из ключевых слов: private, protected и public. Листинг 3.1 иллюстрирует обобщенный синтаксис объявления базового класса.

class className 

private:
<приватные члены данных> <приватные конструкторы> <приватные методы>

protected:
<защищенные члены данных> <защищенные конструкторы> <защищенные методы>

public:
<общедоступные свойства> <общедоступные члены данных>
<общедоступные конструкторы> <общедоступный деструктор> <общедоступные методы>

Листинг 3.1. Объявление базового класса.

Таким образом, объявление базового класса на C++ предоставляет следующие права доступа и соответствующие области видимости:

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

Защищенные protected имена имеют доступ, разрешенный методам данного и производных от него классов.

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

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

1. Секции могут появляться в любом порядке, а их названия могут встречаться повторно.

2. Если секция не названа, компилятор считает последующие объявления имен класса приватными. Здесь проявляется отличие объявлений класса и структуры - последняя рассматривается по умолчанию как общедоступная.

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

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

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

6. Методы (так же как конструкторы и деструкторы), которые содержат более одной инструкции C++, рекомендуется объявлять вне класса.

Листинг 3.2 представляет попытку наполнить объявление базового класса некоторым конкретным содержанием. Отметим характерное для компонентных классов C++Builder объявление свойства Count в защищенной секции, а метода SetCount, реализующего запись в член данных FCount - в приватной секции.

class TPoint {
private:
   
int FCount; // Приватный член данных
    void _fastcall
SetCount(int Value);

protected: 
   
_property int Count = // Защищенное свойство
   { read= FCount, write=SetCount };

    double x; // Защищенный член данных
    double у; // Защищенный член данных

public:
    TPoint(double xVal, double yVal); // Конструктор
    double getX();
    double getY() ;

};

Листинг 3.2. Объявление базовой компоненты TPoint.

Объявления и определения методов хранятся в разных файлах (с расширениями .h и .срр, соответственно). Листинг 3.3 показывает, что когда методы определяются вне класса, их имена следует квалифицировать. Синтаксис такой квалификации метода, определяющей его область видимости, имеет следующий вид:

<имя класса>::<имя метода>

TPoint::TPoint(double xVal, double yVal)
{ // Тело конструктора
}

void _fastcall TPoint::SetCount( int Value )
{
    if ( Value = FCount ) // Новое значение члена данных?
    {
       FCount = Value; // Запись нового значения
       Update(); // Вызов метода Update
    }
}

double TPoint::getX()
{ // Тело метода getX, квалифицированного в классе^TPoint

}

Листинг 3.3. Определения конструктора и методов вне класса.

После того, как вы объявили класс, его имя можно использовать как идентификатор типа при объявлении объекта этого класса (например, TPoint* MyPoint;).

3.4.2 Конструкторы и деструкторы

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

• Имеют имя, идентичное имени своего класса.

• Не имеют возвращаемого значения.

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

• Автоматически генерируются компилятором как public, если не были вами объявлены иначе.

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

• Могут содержать неявные обращения к операторам new и delete, если объект требует выделения и уничтожения динамической памяти.

Листинг 3.4 демонстрирует обобщенный синтаксис объявлений конструкторов и деструктора.

class className
{
public:
// Другие члены данных
    className(); // Конструктор по умолчанию
    className(<список параметров>);// Конструктор с аргументами
    className(const className&); // Конструктор копирования

// Другие конструкторы
    ~className(); // Деструктор

// Другие методы

}; 

Листинг 3.4. Объявления конструкторов и деструктора.

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

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

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

Конструктор копирования предназначен для создания объектов данного класса путем копирования данных из другого, уже существующего объекта этого класса. Такие конструкторы особенно целесообразны для создания копий объектов, которые моделируют динамические структуры данных. Однако, по умолчанию компилятор создает так называемые конструкторы поверхностного копирования (shallow copy constructors), которые копируют только члены данных. Поэтому если какие-то члены данных содержат указатели, сами данные не будут копироваться. Для реализации "глубокого" копирования в код конструктора надо включить соответствующие инструкции.

Класс может объявить только один общедоступный деструктор, имени которого, идентичному имени своего класса, должен предшествовать знак ~ (тильда). Деструктор не имеет параметров и может быть объявлен виртуальным. Если класс не содержит объявления деструктора, компилятор автоматически создаст его.

Обычно деструкторы выполняют операции, обратные тем, что выполняли соответствующие конструкторы. Если вы создали объект класса файл, то в деструкторе этот файл, вероятно, будет закрываться. Если конструктор класса выделяет динамическую память для массива данных (с помощью оператора new), то деструктор, вероятно, освободит выделенную память (с помощью оператора delete) и т.п.

<< Предыдущая страница