Глава 13. Основы ООП

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

Все новое — это хорошо забытое старое

Специалисты по маркетингу хорошо знают, что товар продается лучше, если где-нибудь на упаковке вставить слово "новинка". Поэтому не стоит полагать, что ООП является такой уж новой концепцией. Скотт Гудери (ScottGuthery) вообще утверждал в 1989, что ООП берет свое начало с появления подпрограмм в 40-х годах (см. статью "Является ли новое платье короля объектно-ориентированным?" ("Are the Emperor's New Clothes Object Oriented?"), Dr. Dobb's Journal, декабрь, 1989 г.).

В этой же статье утверждалось, что объекты, как основа ООП, появились еще в языке FORTRAN II.

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

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

В 60-х годах была реализована методика структурного программирования, подразумевавшая использование переменных с осмысленными именами, существование глобальных и локальных переменных и процедурное проектирование приложений по принципу "сверху вниз". Благодаря внедрению в жизнь этих концепций тексты программ стали понятнее и читабельнее, в результате чего упростился процесс отладки программ. Стало проще сопровождать программы, так как появилась возможность контролировать выполнение не отдельных команд, а целых процедур. Примерами языков, ориентированных на подобный процедурный подход, могут быть Ada, С и Pascal.

Бьярна Страуструпа (Bjarne Stroustrup) можно считать отцом ООП в том виде, в каком эта концепция представлена в языке C++, разработанном им в начале 80-х в компании BellLabs. С точки зрения Джеффа Дантеманна (JeffDuntemann), "ООП представляет собой то же структурное программированное, но с еще большей степенью структуризации. Это вторая производная от исходной теории построения программ." (См. статью "Лавирование среди айсбергов" ("DodgingSteamships"), Dr. Dobb'sJournal, июль, 1989 г.) Действительно, как вы убедитесь, ООП в C++ в значительной степени основано на концепциях и средствах структурного программирования языка С. И хотя язык C++ ориентирован на работу с объектами, при желании на нем можно писать как традиционные процедурные приложения, так и неструктурированные программы. Выбор за вами.

C++ и ООП

Все предыдущие главы книги были посвящены изучению методик традиционного структурного программирования. Структура процедурных программ в общем такова: имеется функция main(), а из нее вызываются другие функции программы. Тело функции main(), как правило, невелико. Ее роль обычно сводится к распределению задач между остальными функциями, которые описывают действия, выполняемые над данными.

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

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

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

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

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

Основная терминология

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

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

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

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

Под инкапсуляцией понимается хранение в одной структуре как данных (констант и переменных), так и функций их обработки (методов). Доступ к отдельным частям класса регулируется с помощью специальных ключевых слов public(открытая часть), private(закрытая часть) и protected(защищенная часть). Методы, расположенные в открытой части, формируют интерфейс класса и могут свободно вызываться всеми пользователями класса. Считается, что переменные-члены класса не должны находиться в секции public, но могут существовать интерфейсные методы, позволяющие читать и модифицировать значение каждой переменной. Доступ к закрытой секции класса возможен только из его собственных методов, а к защищенной — также из методов классов-потомков.

Иерархия классов

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

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

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

Полиморфизм и виртуальные функции

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

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

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

Первое знакомство с классом

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

Структура в роли примитивного класса

Структуры в C++ можно рассматривать как примитивные классы, поскольку они могут содержать не только данные, но и функции. Рассмотрим следующую программу:

//
//  sqroot.cpp
//  В этой программе на языке C++ создается структура,
//  содержащая, кроме данных, также функции.
//

#include <iostream.h>

# include <math.h>

struct math_operations { double data_value;

void  set_value (double value) { data_value = value; } 
double get_square (void)
{ return (data_value * data_value) ; }
double get_square_root (void) {
return (sqrt (data_value) }; } } math;

main ()       

// записываем в структуру число 35.63

math.set_value (35.63);

cout << "Квадрат числа равен " << math.get_square () << endl;

cout << "Корень числа равен  " << math.get_square_root () << endl;

return (0); }

В первую очередь обратите внимание на то, что помимо переменной структура содержит также несколько функций. Это первый случай, когда внутри структуры нам встретились описания функций. Такая возможность существует только в C++. Функции-члены структуры могут выполнять операции над переменными этой же структуры. Неявно подразумевается, что все члены структур являются открытыми, как если бы они были помещены в секцию public.

При выполнении программы на экране отобразится следующая информация:

Квадрат числа равен 1269.5 Корень числа равен  5.96909

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

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

//
//   trigon.cpp
//   В этой программе на языке C++ создается структура,
//   содержащая тригонометрические функции.
//

#include <iostream.h>

#include <math.h>

const double.DEG_TO_RAD = 0.0174532925;

struct degree {

double data value;

void  set_value (double angle);
double get_sine (void) ;
double get_cosine (void) ;
double get_tangent (void) ;
double get_cotangent (void) ;
double get_secant (void) ;
double get_cosecant (void) ; ) deg;

void degree: :set_value (double angle) {

data_value = angle;
}                                    

double degree: :get_sine (void) {

return (sin(DEG_TO_RAD * data_value) )

}

double degree::get_cosine(void) {

return(cos(DEG_TO_RAD * data_value)); }

double degree : : get_tangent (void) {

return ( tan (DEG_TO_RAD * data_value) ) ; }

double degree: :get_secant (void). {

return (1.0/ sin(DEG_TO_RAD * data_value) ) ; }

double degree: :get_cosecant (void) (

return (1.0/ cos(DEG_TO_RAD * data_value) ) ; )

double degree : : get_cotangent (void) <

return (1.0/ tan(DEG_TO_RAD * data_value) ) ; }

main() {

// устанавливаем значение угла равным 25 градусов deg . set_value ( 25.0 ) ;

cout << "Синус угла равен " << deg.get_sine () << endl;
cout << "Косинус угла равен " << deg.get_cosine () << endl;
cout << "Тангенс угла равен " << deg.get_tangent () << endl;
cout << "Секанс угла равен " << deg.get_secant () << endl;
cout << "Косеканс угла равен " << deg.get_cosecant () << endl;
cout << "Котангенс угла равен " << deg.get_cotangent () << endl;

return (0 ); }

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

void degree::set_value(double angle)

Имя функции состоит из имени структуры, за которым расположен оператор : :..

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

Синтаксис описания классов

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

class имя {

тип1 переменная1 тип2 леременная2 типЗ переменнаяЗ

public: метод1; метод2; методЗ;

};

По умолчанию все члены класса считаются закрытыми и доступ к ним могут получить только функции-члены этого же класса. Это именно то, что в C++ отличает классы от структур: все члены структур по умолчанию являются открытыми. Если необходимо изменить тип доступа к членам класса, перед ними следует указать один из спецификаторов доступа: public, protected или private.

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

class degree (

double data_value; public:

void  set_value(double);

double get_sine(void);

double get_cosine(void);
double get_tangent(void);
double get_secant(void);
double get_cosecant(void);

double get_cotangent(void);
} deg;

Здесь создается новый тип данных degree. Закрытая переменная-член data_value доступна только функциям-членам класса. Одновременно создается и объект данного класса — переменная deg. He показалось ли вам описание класса знакомым? Это, по сути, та же самая структура, которую мы рассматривали в предыдущем примере, лишь ключевое слово class превращает структуру в настоящий класс.

Простейший класс

Рассмотрим следующий пример программы:

//
//   class.срр
//   В этой программе на языке C++ создается простейший
//   класс, имеющий открытые и закрытые члены.
//

#include <iostream.h>
#include <math.h>

const double DEG_TO_RAD = 0.0174532925;

class degree (

double data_value;

public:

void  set_value(double angle);

double get_sine(void) ;

double get_cosine(void) ;

double get_tangent(void) ;

double get_secant(void) ;

double get_cosecant(void) ;

double get_cotangent(void); ) deg;

void degree::set_value(double angle) <

data_value = angle;                     
I

double degree::get_sine(void)                     
(

return(sin(DEG_TO_RAD * data_value));

}

double degree::get_cosine(void)

return(cos(DEG_TO_RAD * data_value));  

double degree::get_tangent(void)

return(tan(DEG_TO_RAD * data_value)); }

double degree::get_secant(void)     

return(1.0 / sin(DEG_TO_RAD * data_value)); )

double degree::get_cosecant(void)                                    

return (1.0/ cos(DEG_TO_RAD * data_value));

}

double degree::get_cotangent(void)

return(1.0/ tan(DEG_TO_RAD * data_value));

main()

// устанавливаем значение угла равным 25.0градусов
deg.set_value(25.0);

cout << "Синус угла равен, " << deg.get_sine() << endl;
cout << "Косинус угла равен " << deg.get_cosine() << endl;
cout << "Тангенс угла равен " << deg.get_tangent() << endl;
cout << "Секанс угла равен " << deg.get_secant() << endl;
cout << "Косеканс угла равен " << deg.get_cosecant() << endl;
cout << "Котангенс угла равен " << deg.get_cotangent() << endl; return(0); }

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

Программа выведет на экран следующую информацию:

Синус угла равен    0.422618
Косинус угла равен  0.906308
Тангенс угла равен  0.466308
Секанс угла равен   2.3662
Косеканс угла равен 1.10338
Котангенс угла равен 2.14451