Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Прикладное программирование на CC++ с нуля до мультимедийных и сетевых приложений
.pdf
Классы 71
нии класса создаются и соответствующие объекты и их дополнительного описания в других функциях программы или в виде глобальных переменных уже
не требуется.
Для задания переменных-членов в классе дело обстоит точно так же, как
и в структурах. Необходимо только четко представлять себе, какие из свойств
в вашей программе следует сделать открытыми, а какие — закрытыми. Что же
касается декларирования методов класса, то здесь следует объявлять только
прототипы функций, то есть указывать тип функции (тип возвращаемого значения), имя функции и в круглых скобках типы аргументов. Например, для
метода, вычисляющего некое целочисленное значение по двум целочисленным аргументам, объявление прототипа может выглядеть так:
int MyFunc(int, int);
где функция имеет имя MyFunc. Для функции, не возвращающей никакого
значения и не имеющей аргументов, объявление прототипа может иметь следующий вид:
void NewFunc(void);
Описание методов должно быть выполнено где-то в программе. Здесь следует сказать несколько слов о рекомендациях по хорошему стилю программирования в системах ООП. Современные средства программирования на С++
(Borland C++ Builder, MS Visual Studio) настоятельно рекомендуют придерживаться модульного принципа построения программы. Применительно к программам с самостоятельно разрабатываемыми классами эти рекомендации заключаются в следующем. Функции, вызывающие методы класса или оперирующие со свойствами класса, помещаются в модуль с расширением .cpp.
 Builder'е это, скорее всего, будет файл Unit1.cpp, если не переименовывать
то, что предлагается по умолчанию. Заметим, что этот модуль вовсе не обязательно содержит главную функцию программы. Более того, в Builder'е это как
раз и не так. Объявления классов обычно помещаются в заголовочные файлы.
Допустим, это будет файл Unit2.h. Тогда описания реализаций функций-членов разумно поместить в соответствующий cpp-файл, который будет называться файлом реализации. В нашем случае файл реализации будет иметь имя
Unit2.cpp.
Теперь приступим к разработке простейшей программы с «самодельным»
классом. Пусть наш класс будет называться PROB. Единственным его свойством (переменной-членом) будет целое с именем iii закрытого типа. Методами
класса будут две открытые функции FuncIn è FuncOut. Назначение первой заключается в присвоении свойству iii значения, равного аргументу функции.
Второй метод должен отображать значение переменной iii. В программе должна быть выполнена инициализация свойства и вывод его значения на экран.
Теперь, подготовив папку для проекта, выполняем следующие шаги.
1. Запускаем Borland C++ Builder и сразу же сохраняем проект в папке,
оставив все, что предлагается по умолчанию. Компилируем пока пустой проект и убеждаемся в том, что все в порядке.

72 Глава 2. Объектно-ориентированное программирование
2. Добавляем в проект новую пару файлов Unit2.cpp è Unit2.h. Для этого
из пункта главного меню File открываем подпункт New и следующий подпункт Unit. Скомпилируйте проект снова и сохраните все с помощью комбинации клавиш Ctrl+Shift+S. Можете убедиться в том, что соответствующие
модули добавлены к проекту.
3. В текст файла Unit2.h после нескольких строк, созданных системой, добавьте код объявления класса. Теперь содержимое файла представляет собой
примерно следующее:
#ifndef Unit2H
#define Unit2H
#endif
class PROB
{
int iii;
public:
void FuncIn(int);
void FuncOut();
};
Итак, класс назван PROB, объявлена закрытая переменная-член iii и две
открытые функции-члены. В текст файла реализации Unit2.cpp также после
созданных системой строк добавьте код реализации функций. В результате будем иметь следующее:
#pragma hdrstop
#include <VCL.h>
#include "Unit2.h"
#pragma package(smart_init)
void PROB::FuncIn(int i)
{iii = i;}
void PROB::FuncOut()
{ShowMessage(iii);}
Синтаксис задания функций-членов понятен из текста. Сначала указывается тип возвращаемого значения — в нашем случае это void. Далее пишется
имя класса. Затем, после двух двоеточий, обычное описание функции.
С функцией FuncIn все достаточно ясно. Поскольку это метод класса, он
имеет прямой доступ к свойству iii. В теле функции свойство получает значение, равное аргументу функции. Второй метод должен вывести значение переменной iii в стандартный компонент Builder'à ShowMessag, являющийся
компонентом библиотеки VCL. В этой связи в модуль нужно добавить директиву подключения заголовочного файла VCL.h.
Переходим к оформлению модуля, вызывающего методы класса. Прежде
всего, открыв проект, поместите на форму стандартную кнопку из VCL и сде
лайте двойной щелчок мышью по значку кнопки. В тело функции-обработчи
-
-

Классы 73
ка события нажатия этой кнопки внесите текст работы с классом. Теперь
файл Unitr1.cpp будет иметь вид:
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
#include "Unit2.h"
//—————————————————————————————————————-
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//—————————————————————————————————————-
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//—————————————————————————————————————-
void __fastcall TForm1::Button1Click(TObject *Sender)
{
PROB cl1;
cl1.FuncIn(3);
cl1.FuncOut();
}
Что же происходит при нажатии на кнопку? В первой строке тела функции-обработчика создается объект класса. Назван он cl1. Во второй строке
вызывается метод FuncIn с аргументом, равным 3. Как можно видеть, при вызове метода сначала указывается имя объекта и затем, после разделителя, имя
функции. В результате свойство iii становится равным 3. Наконец функция
FuncOut произведет вывод значения этого самого свойства в панель
ShowMessage.
В рассмотренном примере мы создали функцию-член, выполняющую
инициализацию свойства. Часто является целесообразным присвоение начальных значений переменным-членам непосредственно при создании объекта. Для этих целей служит специальный метод класса, называемый конструктором. Конструктор должен быть объявлен как открытый метод с именем,
полностью совпадающим с именем класса. Конструктор не возвращает значения и не имеет типа (даже типа void). В теле конструктора пишутся выражения, предписывающие выполнение некоторого набора действий в момент образования объекта класса. В частности, этими действиями могут быть как раз
присваивания значений членам.
Применительно к классу, описанному выше, может понадобиться, например, начальное обнуление переменной iii, которую теперь будем задавать вещественной. В заголовочном файле Unit2.h в описании класса теперь поместим строку:
PROB();

74 Глава 2. Объектно-ориентированное программирование
В файле реализации Unit2.cpp опишем конструктор:
PROB::PROB()
{iii = 0;}
Теперь в вызывающем модуле Unit1.cpp при создании объекта в строке:
PROB cl1;
сразу произойдет присвоение значения 0 свойству iii.
Только что рассмотренный конструктор является методом без параметров.
Большие возможности предоставляются при использовании конструкторов с
параметрами. В частности, можно сделать конструктор, который инициализирует свойство объекта заданным значением — аргументом функции-конструктора. В этом случае выражения в файлах будут представлены, например, следующим образом:
PROB(int);
PROB::PROB(int i)
{iii = i;}
PROB cl2(5);
Хорошим стилем программирования в С++ предписывается использование при создании объектов классов оператора new. Этот оператор выделяет
память для объявляемого объекта (не только класса, но и любого другого, например, массива). Оператор заменяет функцию mallow языка С. Попутно отметим, что для освобождения памяти, распределенной оператором new, используется оператор delete, заменяющий функцию free â Ñ.
Из представленных ниже текстов файлов Unit2.h, Unit2.cpp è Unit1.cpp будет понятен синтаксис использования рассматриваемых конструкторов и операторов.
//———————————Unit2.h
#ifndef Unit2H
#define Unit2H
//—————————————————————————————————————#endif
class PROB
{
int iii;
public:
PROB();
PROB(int);
void FuncOut();
};
//——————————————Unit2.cpp
#include <vcl.h>
#pragma hdrstop

Классы 75
#include "Unit2.h"
//—————————————————————————————————————-
#pragma package(smart_init)
PROB::PROB()
{iii = 0;}
PROB::PROB(int i)
{iii = i;}
void PROB::FuncOut()
{ShowMessage(Intact(iii));}
//——————————— Unit1.cpp
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
#include "Unit2.h"
//—————————————————————————————————————#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//—————————————————————————————————————__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//—————————————————————————————————————-
void __fastcall TForm1::Button1Click(TObject *Sender)
{
PROB cl1;
PROB cl2(5);
PROB *cl3 = new PROB(7);
cl1.FuncOut();
cl2.FuncOut();
cl3->FuncOut();
delete cl3;
Close();
}
Итак, в первой строке функции-обработчика нажатия кнопки создается
объект cl1 класса PROB с использованием конструктора без параметров. При
этом обнуляется свойство iii объекта cl1. Во второй строке вызывается конструктор с аргументом, и переменная — член iii â cl2 получает значение 5. Наконец, в третьей строке объект создается по указателю cl3 с использованием
оператора new и снова конструктором с параметром. Далее в панель
ShowMessage последовательно выводятся значения iii для каждого из созданных объектов. Отметим, что, поскольку cl3 является адресом, при вызове
функции-члена соответствующего класса применяется не разделитель «.», а

76 Глава 2. Объектно-ориентированное программирование
разделитель «->». В предпоследней строке освобождается память, предоставленная объекту cl3, а в последней производится закрытие формы приложения,
то есть завершение работы программы.
Удаление объекта класса также сопровождается выполнением специального метода, называемого деструктором. Вызов деструктора в явном виде в
приложении не делается. Он производится автоматически либо при выходе из
блока (для локальных объектов), либо при завершении приложения (для глобальных объектов). Сразу отметим, что деструкторы, как и конструкторы, не
обязательно и явно декларировать — всегда, когда необходимо, будут срабатывать конструкторы и деструкторы по умолчанию. Однако если при удалении объекта необходимо выполнить какие-то действия, то «самодельный» деструктор может быть полезным. Имя функции-деструктора совпадает с именем класса, перед которым пишется символ ~ (тильда). Этот метод не имеет
типа и не возвращает никакого значения.
Для демонстрации работы деструктора добавим в файл Unit2.h строку:
~PROB();
В файле реализации дадим описание метода:
PROB::~PROB()
{ShowMessage("FINISH");}
Как нетрудно догадаться, выполнение деструктора заключается в выводе в
панель ShowMessage сообщения FINISH. Больше ничего в программе делать
не надо. По окончании работы приложения вы увидите на экране это самое
сообщение.
В заключение этого раздела необходимо сказать несколько слов о механизмах наследования в С++. Суть заключается в том, что на основе уже готового класса-родителя можно создать производный класс-наследник, который
наследует все свойства и методы родителя и может быть дополнен своими
собственными переменными-членами и функциями-членами. Использование
наследования продемонстрируем на примере нашей же программы. Ниже
представлен текст в заголовочном файле Unit1.h, которого мы пока не касались, поскольку там все создается автоматически и менять в нем что-либо
практически никогда не требуется.
#ifndef Unit1H
#define Unit1H
//—————————————————————————————————————#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
//—————————————————————————————————————class TForm1 : public TForm
{
__published: // IDE-managed Components
TButton *Button1;

Перегрузка функций и операторов 77
void __fastcall Button1Click(TObject *Sender);
private: // User declarations
public: // User declarations
__fastcall TForm1(TComponent* Owner);
};
//—————————————————————————————————————-
extern PACKAGE TForm1 *Form1;
//—————————————————————————————————————-
#endif
Итак, в первой строке после всех директив препроцессора написан заголовок определения класса TForm1. Следующая за именем нового класса конструкция public TForm как раз и означает, что TForm1 является наследником
класса TForm. Последний определен в системе C++ Builder и имеет множество разнообразных свойств и методов, которые передаются по наследству создаваемому классу. Собственными свойствами и методами нового класса будут
указатель *Button1 и функция Button1Click. Заниматься объяснением, что это
такое, здесь явно нецелесообразно. Важно подчеркнуть, что для обеспечения
доступа извне к элементам класса наследника используется спецификатор
__published:. Впрочем, еще раз заметим, что вручную здесь делать ничего не
следует. Обратим внимание на то, что в разделе public: задан и собственный
конструктор класса-наследника.
Перегрузка функций и операторов
За этим несколько загадочным заголовком кроются весьма простые вещи,
вовсе не связанные с возложением на функции или операторы лишних килограммов. Перегруженными называются две или более функции, имеющие
одинаковое имя и различающиеся типом или количеством аргументов или типом возвращаемого значения. Перегруженными операторами, соответственно,
будут операторы с одинаковыми названиями (обозначениями) или значками,
отличающиеся типами операндов и (или) результатами выполняемых операторами действий. Да... Наверное, понятней не стало, несмотря на то, что все
сформулировано верно.
Начнем с перегруженных функций. Классическим примером здесь служит
функция, вычисляющая модуль (абсолютную величину) числа. Стандартной
функцией в С/С++ для нахождения модуля целого числа является функция
int abs(int). Подчеркнем, что аргументом функции должно быть целое число, а
возвращаемое число также целого типа. Применительно к вещественным числам то же действие выполняет функция float fabs(float). Логично было бы попробовать «научить» функцию abs работать с любыми аргументами: целыми,
вещественными, комплексными и возвращать значения соответствующих типов. При этом имя функции хотелось бы оставить одним и тем же для всех
типов. Вот в этом и состоит смысл перегрузки функции. Как это делается
практически?

78 Глава 2. Объектно-ориентированное программирование
Если бы мы решили создать собственную функцию abs, возвращающую
модуль целого (типа int) числа, то это можно было бы сделать, например, так,
как показано в следующем модуле:
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
//—————————————————————————————————————-
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
int abs(int x)
{
int y;
if(x<0) y=-x; else y=x;
return y;
}
//—————————————————————————————————————-
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//—————————————————————————————————————-
void __fastcall TForm1::Button1Click(TObject *Sender)
{
ShowMessage(abs(-2));
}
Прежде всего, можно убедиться в том, что компиляция программы выдаст
ошибку, связанную с тем, что обнаруживается конфликт с уже существующей
стандартной функцией abs. Дело в том, что мы пытаемся просто переписать
функцию того же имени и типа, что и стандартная. Если заменить имя, например, на rabs, то все пройдет нормально, и программа выполнит необходимое действие. Однако мы хотим другого. Мы хотим, чтобы функция abs находила модуль как целого, так и вещественного числа. Для этого определение
функции представим в виде:
float abs(float x)
{
float y;
if(x<0) y=-x; else y=x;
return y;
}
В вызывающей функции можно, например, написать выражение:
ShowMessage(abs(-2.3));

Перегрузка функций и операторов 79
Таким образом, мы выполнили перегрузку функции abs íà òèï float. Аналогичную процедуру можно было бы проделать и для типов double è long.
Безотносительно темы данного раздела укажем, что тело функции abs
хотя и записано правильно, но несколько громоздко и не лучшим образом.
Более грамотно было бы использовать ранее нами не рассмотренную конструкцию языка С вида:
Условие ? Выражение1: Выражение2;
Эта конструкция работает следующим образом. Если Условие имеет значение ИСТИНА, то конструкция возвращает значение Выражения1, в противном случае — значение Выражения2. Тогда определение функции можно записать, в частности, так:
long abs(long x)
{
return x<0 ? -x: x;
}
Можно видеть, что запись тела функции выглядит гораздо компактнее.
Кроме того, в такой форме нам уже не требуется локальная переменная y.
Следует отметить, что с перегрузкой функций мы уже встречались при
изучении классов. Действительно, для рассматриваемого класса PROB мы организовывали два конструктора: без параметров и с одним параметром. Поскольку имя конструктора класса всегда одно и то же — совпадающее с именем класса, разница здесь именно в аргументах функции, то есть в нашем
случае перегрузка производится по количеству аргументов конструкторов.
Наряду с перегрузкой функций в С++ широко используется и перегрузка
операторов. Под термином оператор в языке понимается очень многое. Знак
операции сложения + является бинарным оператором, слева и справа от которого находятся операнды-слагаемые. Два знака ++ также являются оператором
для унарной операции инкремента. Не столь очевидным является, например,
тот факт, что пара прямых скобок [ ] также является оператором индексирования, то есть оператором присвоения номера объекту. Очевидно, перегрузка
операторов позволяет использовать один и тот же символ для сходных действий
над объектами различных типов. Большинство операторов в С++ могут быть
перегружены, однако, в силу большого разнообразия операторов, мы остановимся только на перегрузке простейших бинарных операций.
Перегрузка оператора всегда определяется относительно объектов конкретных классов. По этой причине наиболее удобно организовать перегрузку
с помощью специальной функции-члена класса, которую будем называть оператором-функцией. Тогда оператор-функция должна быть объявлена как открытая при определении класса. В общем виде реализация функции имеет
следующую структуру:
Тип Имя_класса::operator#(Аргументы)
{
//Исполняемые инструкции;
}

80 Глава 2. Объектно-ориентированное программирование
Здесь Òèï это тип возвращаемого функцией значения. В большинстве
случаев это будет тип класса, для которого делается перегрузка, но не обязательно. Далее указывается имя класса и после разделителя :: пишется служебное слово operator. Вместо символа # как раз и следует указать перегружаемый
оператор, как то +, —, * и так далее.
При перегрузке бинарных операторов у функции-оператора будет единственный аргумент. Этот аргумент будет принимать значение объекта класса,
стоящего справа от знака оператора. Тело функции должно обеспечить действие или действия, которые будут выполняться над этим аргументом и свойством класса, которое будет иметь значения операнда, стоящего слева. Результат действий должен быть возвращен инструкцией return.
Все сказанное, наверное, воспринимается пока слишком абстрактно. Что
же, попробуем продемонстрировать ситуацию примером. Создадим «самодельный» класс комплексных чисел и перегрузим для него некоторые арифметические операции. Обратимся к тексту заголовочного файла с описанием
класса.
class COMP
{
public:
double a,b;
COMP(double, double) ;
COMP() ;
COMP operator+(COMP);
COMP operator-(COMP);
COMP operator*(COMP);
};
Итак, организован класс с именем COMP, открытыми свойствами которого являются два вещественных числа a è b, которые соответствуют действительной и мнимой частям комплексного числа. Здесь же объявлены два конструктора — с параметрами и без параметров. Последние три объявления —
это как раз и есть декларации операторов-функций для перегрузки сложения,
вычитания и умножения.
Реализация представлена в следующем файле.
#include "Unit2.h"
#pragma package(smart_init)
COMP::COMP(double aa, double bb)
{a=aa; b=bb;}
COMP::COMP()
{a=0.0; b=0.0;}
COMP COMP::operator+(COMP r)
{
COMP tmp;
tmp.a = a+r.a; tmp.b = b+r.b;
return tmp;
}
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
