В данной главе будут рассмотрены основные термины и понятия объектно-ориентированного программирования (ООП). В чем, вообще говоря, особенность объектно-ориентированных языков, таких как 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 <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;
В этой программе структура содержит прототипы семи функций, но сами они описаны отдельно. Тригонометрические функции вычисляют синус, косинус, тангенс, котангенс, секанс и косеканс угла, значение которого в градусах передается в функцию 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