Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Прикладное программирование на CC++ с нуля до мультимедийных и сетевых приложений

.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
Классы 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;
}