Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
С++.doc
Скачиваний:
18
Добавлен:
16.02.2016
Размер:
455 Кб
Скачать

Глава 12

объектно-ориентированное программирование

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

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

Для написания более сложных задач понадобился новый под­ход к программированию, который и был реализован в модели ООП. Модель ООП основана на нескольких основополагающих концепциях.

Абстракция данных — это возможность определять новые ти­пы данных, с которыми можно работать почти так же, как и с основными типами данных. Такие типы часто называются абст­рактными типами данных, хотя термин "типы данных, опреде­ляемые пользователем" является более точным.

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

Для реализации этих двух основополагающих концепций в языке C++ используются классы. Термином класс определяется тип объек­тов. При этом каждый представитель (или экземпляр) класса назы­вается объектом. Каждый объект всегда имеет свое, уникальное состояние, определяемое текущими значениями его данных-членов (элементов-данных). Функциональное назначение класса определя­ется возможными действиями над объектами класса, которые зада­ются его функциями-членами (функциями-элементами или метода­ми). Причем термин "метод" чаще используется в литературе, по­священной языку Object Pascal. В каждом классе распределяется память для хранения данных, и устанавливаются допустимые опера­ции для каждого объекта данных данного типа. Создание объектов данного класса производится специальной функцией-членом, кото­рая называется конструктором, а уничтожение — другой специаль­ной функцией-членом, которая называется деструктором. Класс позволяет делать недоступными внутренние данные, представляя их как открытые (public), закрытые (private) и защищенные (protected). Класс устанавливает четко определенный интерфейс для взаимодействия объектов этого типа с остальным миром. Закры­тые коды или данные доступны только внутри этого объекта. С дру­гой стороны, открытые коды и данные, несмотря на то, что они за­даны внутри объекта, доступны для всех частей программы. Откры­тая часть объекта как раз и используется для создания интерфейса объекта с остальным миром. Полученными объектами можно управлять при помощи сообщений (чаш запросов), которые пред­ставляют собой просто вызовы функций-членов. Мы будем пользо­ваться термином запрос, чтобы не путать это понятие с сообщения­ми операционной системы Windows.

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

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

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

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

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

Классы

Понятие класса является, пожалуй, наиболее важным в языке C++. Синтаксис описания класса похож на синтаксис описания структуры. Вот его основная форма:

class <имя_класса>

{

//закрытые функции-члены и данные-члены класса

public:

//открытые функции-члены и данные-члены класса

} <список_объектов>;

В описании класса <список_объектов> не является обя­зательным. Вы можете объявить объекты класса позже, по мере необходимости. Так обычно и поступают. Хотя <имя_класса> также необязательно, его обычно указывают. Дело в том, что <имя_класса> становится новым именем типа данных, кото­рое используется для объявления объектов этого класса.

Функции и переменные, объявленные внутри объявления класса, становятся членами этого класса. Переменные, объявлен­ные внутри объявления класса, называются данными-членами этого класса; функции, объявленные внутри объявления класса, называются функциями-членами класса. По умолчанию, все функции и переменные, объявленные в классе, становятся закры­тыми для этого класса. Это означает, что они доступны только для других членов этого класса. Для объявления открытых чле­нов класса используется ключевое слово public, за которым следует двоеточие. Все функции и переменные, объявленные по­сле слова public, доступны и для других членов класса, и для любой другой части программы, в которой содержится класс.

Вот пример объявления класса:

class AnyClass

{

//закрытый элемент класса

int a;

public:

int get_a();

void set_a(int num);

};

Хотя функции get_a() и set_a() и объявлены в классе AnyClass, они еще не определены. Для определения функции-члена нужно связать имя класса, частью которого является функ­ция-член, с именем функции. Это достигается путем написания имени функции вслед за именем класса с двумя двоеточиями. Операцию, записываемую в виде двух двоеточий, называют опе­рацией расширения области видимости. Для задания функции-члена используется следующая общая форма:

<Тип> <имя_класса>: : <имя_функции>

(<список_параметров>)

{

//тело функции

}

Ниже приведены примеры определения функций-членов get_a() и set_a():

void AnyClass::get_a()

{

return a;

}

int AnyClass::set_a(int num)

{

a = num;

}

Объявление класса AnyClass не приводит к созданию объ­ектов типа AnyClass. Чтобы создать объект соответствующего класса, нужно просто использовать имя класса как спецификатор типа данных. Например,

AnyClass ob1, оb2;

После того, как объект класса создан, можно обращаться к от­крытым членам класса, используя операцию "точка", так же как к полям структуры.

Например,

obl.set_a(10) ;

ob2.set_a(37) ;

Эти операторы устанавливают значения переменной а в объ­ектах оb1 и оb2. Каждый объект содержит собственную копию всех данных, объявленных в классе. Это значит, что значение переменной а в оb1 отлично от значения этой переменной в оb2.

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

class Coord

{

public:

int x,y;

void SetCoord(int _x, int _y);

};

void

Coord::SetCoord(int _x, int _y)

{

x = _x;

у = _y;

}

int main()

{

Coord pt;

Coord *ptPtr = &pt; //указатель на объект

//...

pt.x =0; // объект. член_класса

ptPtr->y = 0; //укааатель->член_класса

ptPtr->SetCoord(10,20);

//...

return 0;

}

Среди программистов часто возникает спор о том, какой ме­тод доступа к членам класса - через оператор "." или через опе­ратор "->" - работает быстрее. Внесем ясность в этот вопрос. Оператор выбора члена "." работает в точности так же, как опе­ратор "->", за исключением того, что имени объекта предшеству­ет неявно сгенерированный компилятором оператор адреса "&". Таким образом, инструкцию

ObjName.FuncName();

компилятор трактует, как

(&ObjName)->FuncName() ;

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

<имя_класса>::<имя_члена>

Подробнее эту форму вызова элементов класса мы рассмот­рим позднее.

Язык C++ накладывает определенные ограничения на данные-члены класса:

• данные-члены не могут определяться с модификаторами auto, extern или register;

• данным-членом класса не может быть объект этого же класса (однако данным-членом может быть указатель или ссылка на объект этого класса, или сам объект друго­го класса).

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

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

class <имя_класса>;

Рассмотрим пример.

//Неполное объявление класса

class PrevDecl;

class AnyClass

{

int x;

PrevDecl* obPtr;

public:

AnyClass(int _x){x = _x;}

};

int main()

{

//...

return 0;

}

//Полное объявление класса

class PrevDecl

{

int a;

public:

PrevDecl();

};

Нельзя создать объект не полностью определенного класса. Попытка создания такого класса приводит к ошибке компиляции.

Заметим, что определение класса напоминает определение структуры, за исключением того, что определение класса:

• обычно содержит один или несколько спецификаторов доступа, задаваемых с помощью ключевых слов public, protected или private;

• вместо ключевого слова struct могут применяться class или union;

• обычно включает в себя наряду с элементами данных функции-члены;

• обычно содержит некоторые специальные функции-члены, такие, как конструктор и деструктор.

Ниже приведены примеры определения классов с использова­нием ключевых слов struct и union.

struct Point

{

private:

int x;

int у;

public:

int GetX();

int GetY();

void SetX(int _x);

void SetY(int _y);

};

union Bits

{

Bits(unsigned int n);

void ShowBits();

unsigned int num;

unsigned char c[sizeof(unsigned int)];

};

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

Таблица 12.1.

Спецификаторы доступа к классу

Спецификатор

Описание

private :

Данные-члены и функции-члены доступны только для функций-членов этого класса.

Protected:

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

public:

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

В C++ класс, структура и объединение рассматриваются как типы классов. Структура и класс подобны друг другу, за исклю­чением доступа по умолчанию: в структуре элементы имеют по умолчанию доступ public, в то время как в классе - private. Объединение, как и структура, по умолчанию предоставляет дос­туп public.

В приведенной ниже таблице 12.2 перечислены эти отличия.

Таблица 12.2.

Различие между классом, структурой и объединением

Различие

Классы

Структуры

Объединения

Ключевое слово:

class

struct

union

Доступ по умолчанию:

private

public

public

Перекрытие данных:

Нет

Нет

Да

Структуры и объединения могут иметь конструкторы и дест­рукторы. Вместе с тем имеется несколько ограничений на ис­пользование объединений в качестве классов. Во-первых, они не могут наследовать какой бы то ни было класс, и они сами не мо­гут использоваться в качестве базового класса для любого друго­го типа. Короче говоря, объединения не поддерживают иерархию классов. Объединения не могут иметь членов с атрибутом static. Они также не должны содержать объектов с конструк­тором и деструктором. Рассмотрим пример использования объе­динения в качестве класса:

#include <iostream.h>

union Bits

{

Bits(unsigned int n) ;

void ShowBits();

unsigned int num;

unsigned char с[sizeof(unsigned int)];

};

Bits::Bits(unsigned int n)

{

num = n;

}

void

Bits::ShowBits()

{

int i, j;

for (j=sizeof(unsigned int)-l; j>=0; j--)

{

cout « "Двоичное представление байта "

« j « ":";

for (i=128; i; i»=1)

{

if (i & c[j]) cout « "1";

else cout « "0";

}

cout « "\n";

}

}

int main()

{

Bits ob(2000);

ob.ShowBits() ;

return 0;

}

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