- •Глава 12
- •Конструкторы и деструкторы. Список инициализации элементов
- •Конструкторы по умолчанию и конструкторы копирования
- •Указатель this
- •Встраиваемые (inline-) функции
- •Статические члены класса
- •Использование указателей на функции-члены класса
- •Массивы объектов класса
- •Дружественные функции и друзья класса
- •Глава 13
- •Множественное наследование
- •Виртуальные базовые классы
- •Глава 14
- •Перегрузка функций
- •Перегрузка конструкторов
- •Создание и использование конструкторов копирования
- •Определение адреса перегруженной функции
Глава 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;
}
В этом примере осуществляется побайтовый вывод переданного объекту беззнакового целочисленного значения в двоичном виде.
