Учебное пособие: Препроцессорные средства в C и С++
Описание тела компоненты-функции может быть включено в описание класса, как это сделано в примере для функций getx и gety, или помещено вне описания класса. Компоненты-функции при их вызове неявно получают дополнительный аргумент - указатель на переменную объектного типа, для которой вызвана функция и в теле функции можно обращаться ко всем компонентам класса. В связи с этим при описании тела компоненты-функции вне описания класса нужно использовать операцию разрешения контекста, чтобы информировать компилятор о принаждлежности функции к классу. Методы класса TPoint можно описать так:
void TPoint : : movePoint ( int newx, int newy ) { x = newx; y = newy ; }void TPoint : : relmove ( int dx, int dy ) { x += dx; y += dy ; }Чтобы выполнить начальную инициализацию компонент-данных при создании переменных объектного типа в описание типа включаются специальные методы-конструкторы. Имя конструктора совпадает с именем типа, конструктор не возвращает никакого значения и для него не указывается тип возвращаемого значения. Для рассмотренного выше класса TPoint можно было обойтись без конструктора и использовать для инициализации метод movePoint. Рассмотрим в качестве примера класс TRect, описывающий прямоугольник со сторонами, параллельными осям координат:
enum Boolean {FALSE, TRUE }; class TRect { public:TPoint a,b; // a - ????? ??????? ????, b - ?????? ?????? ???? void move( int dx, int dy) // ??????????? ??????????????{ a.relmove ( dx, dy ); b.relmove ( dx, dy );} void grow( int dx, int dy) // ????????????????? { a.x +=dx; a.y += dy; b.x +=dx; b.y += dy; }void intersect (const TRect& r); // ????? ????? ???? ??????????????? void Union ( const TRect& r); /* ?????????????, ???????????? ??? ?????????????? */Boolean contains ( const TPoint& p);/* TRUE, ???? ????? p ??????????? ?????????????? */ Boolean isEmpty( ); /* TRUE, ???? ?????? ??? ?????? ?????????????? ????? ???? */ TRect (int ax, int ay, int bx, int by ) // ??????????? { a,x - ax; a,y = ay; b.x = bx; b.y = by; }; TRect ( TPoint p1, TPoint p2) // ??????????? { a = p1; b = p2; }; TRect () // ??????????? { a.x = a.y = b.x = b.y = 0; };}; /* ?????? ?????? TRect */void TRect : : intersect (const TRect& r) { a.x = max (a.x, r.a.x ); b.x = min ( b.x, r.b.x ); a.y = max (a.y, r.a.y ); b.y = min ( b.y, r.b.y ); }; void TRect : : Union ( const TRect & r ) { a.x = ( a.x <= r.a.x ) ? a.x : r.a.x ; a.y = ( a.y <= r.a.y ) ? a.y : r.a.y ; b.x = ( b.x >= r.b.x ) ? b.x : r.b.x ; b.y = ( b.y >= r.b.y ) ? b.y : r.b.y ; }; Boolean TRect : : contains ( const TPoint & p ) { return Boolean (p.x >= p.x && p.x < b.x && p.y >= a.y && p.y < b.y); }; Boolean TRect : : isEmpty ( ) { return Boolean ( a.x >= b.x | | a.y >= b.y ); };Более полная информация о конструкторах объектных типов приведена в следующем разделе.
Объявление переменной объектного типа строится по общим правилам, но за идентификатором переменной можно указать в скобках аргументы определенного в классе конструктора, например:
TRect r1(2,4,20,50); // ????????????? ? ?????????????? ??????? ???????????? TRect *pr = &r1; // ???????? ?? TRect TRect r2, *ptr; // ??? r2 ???????????? ??????????? ??? ??????????В операции new для размещения в динамической памяти объектной переменной за именем типа также указываются аргументы конструктора этого типа:
ptr = new TRect( 7,3,18,40);Для обращения к компонентам объектного типа имя компоненты должно уточняться именем объектной переменной или указателем на нее:
r1.grow( 2, -3); pr->move( 1, 1); Boolean bb= r1.isEmpty( );4.2. Конструкторы и деструкторы
Описание класса обычно содержит специальные методы, вызываемые при создании переменной этого класса и удалении переменной из динамической памяти - конструкторы и деструкторы. Конструктор вызывается после выделения памяти для переменной и обеспечивает инициализацию компонент-данных, деструктор вызывается перед освобождением памяти, занимаемой объектной переменной, и предназначен для выполнения дополнительных действий, связанных с уничтожением объектной переменной, например, для освобождения памяти, выделенной для объекта вне участка, отведенного для компонент-данных.
Как уже отмечалось, конструктор всегда имеет имя, совпадающее с именем класса, для него не указывается тип возвращаемого значения и он не возвращает никакого значения. Конструктор должен обеспечивать инициализацию всех компонент-данных. Для класса может быть объявлено несколько конструкторов, различающихся числом и типами параметров. В общем случае различают следующие виды конструкторов: конструктор с параметрами, конструктор без параметров и конструктор копирования с одним параметром - ссылкой на переменную того же объектного типа. Если для объектного типа не определено ни одного конструктора, компилятор создает для него конструктор по умолчанию, не использующий параметров. Конструктор копирования необходим, если переменная объектного типа передается в какую-нибудь функцию как аргумент, поскольку все аргументы передаются в функцию по значению.
Деструктор необходим, если объектный тип содержит компоненту-данное, являющуюся указателем на динамическое данное, которое должно уничтожаться при уничтожении объектной переменной. Деструктор всегда имеет то же имя, что и имя класса, но перед именем записывается знак ~ (тильда). Деструктор не имеет параметров и подобно конструктору не возвращает никакого значения.
В качестве примера рассмотрим объектный тип TString для представления строковых данных с более высокой степенью защиты от ошибок, чем это обеспечено стандартными функциями обработки строк из файла-заголовка string.h.
#include <iostream.h>#include <string.h> class TString { public:TString(); // ??????????? ??? ?????????? TString(int n, char* s=0); // ???????????, ????????? ?????? ?????? /* ???????????, ????????????? ?????? ?? char ? ??????????? ????? ???? TString */ TString(char* s);TString(TString& st); // ??????????? ??????????? ~TString(); // ?????????? void print(); // ????? ?????? ?? ????? int sz; // ????? ?????? char* ps; // ????????? ?? ?????? ??? ???????? ?????? }; /* ?????? ?????? TString */TString::TString( ){sz=0; ps=0;} TString::TString(int n, char* s) { sz=n; ps=new char[n+1]; strncpy(ps,s,n); ps[sz]='\0'; } TString::TString(char* s) { sz=strlen(s)+1; ps=new char[sz]; strcpy(ps,s); } TString::TString(TString& str) { sz = str.sz; ps=new char[sz+1]; strcpy(ps,str.ps); } TString::~TString( ) { if (ps != 0) delete [] ps; } void TString::print( ) { if (sz == 0 ) { cout << " ???????????? "<< endl; return;} cout<<" ?????? = "<< ps <<endl;}Ниже приведен пример программы, иллюстрирующей использование данных типа TString.
int main() { char rabstr [60] = "yes"; while (*rabstr !='n') { cin >> rabstr; if (*rabstr == 'n')break; TString s1(); TString s2(6); TString s3(6, rabstr);TString* ps1=new TString(" ??? ?????? ?? ?????????");cout <<" s1="; ps1->print(); cout <<" s2="; s2.print(); cout <<" s3="; s3.print();} return 0; }Описание конструктора можно упростить, если компоненты-данные принадлежат к базовым типам или являются объектными переменными, имеющими конструктор. При описании конструктора после заголовка функции можно поставить двоеточие и за ним список инициализаторов вида идентификатор (аргументы ). Например, для класса TPoint из предыдущего параграфа можно было определить конструктор так:
class TPoint ( ..... public: TPoint ( int x0, int y0 ) : x (x0), y (y0){ };}В этом конструкторе все компоненты получают значения из списка инициализации, а тело конструктора представлено пустым составным оператором.
4.3. Производные классы
Классы образуют иерархическую структуру, когда выделяется некоторый базовый класс, содержащий общие данные и методы группы сходных классов, и строится несколько производных классов, в которых к данным и методам базового класса добавляются данные и методы, необходимые для реализации производного класса. Описание системы классов в этом случае выглядит так:
class TA // ??????? ????? { ?????????? ? ?????? TA } class TAA : public TA // ?????, ??????????? ?? ?????? TA { ?????????? ? ?????? TAA } class TAAB : public TAA // ?????, ??????????? ?? ?????? TAAB { ?????????? ? ?????? TAAB }Доступом к компонентам базового класса управляют ключевые слова public и private. Если базовый класс public, то в производном классе public-компоненты базового класса останутся public, protected-компоненты базового класса останутся protected, private-компоненты базового класса для функций производного класса будут недоступны.
Если базовый класс private, то в производном классе public и protected компоненты базового класса доступны для функций производного класса, но для следующего производного класса они будут считаться private, т.е. будут недоступны, private-компоненты базового класса недоступны в производных классах.
Конструктор производного класса должен вызывать конструктор своего базового класса:
class TBase { public: TBase( int s, int m, int d);/* ?????? ?????????? ?????? TBase */}class TVect : public TBase { public: TVect ( int k, int s, int m int d): TBase(s, m, d){ /* ????????????? ????????? ????????? TVect */}; }4.4. Пример построения системы классов
Известно, что при объявлении массивов в Си/Си++ количество элементов массива задается константой и в дальнейшем не может быть изменено. При обращении к элементам массив отсутствует контроль выхода за пределы индексов массива, что приводит к трудно обнаруживамым ошибкам в программах. Построим систему классов для обработки динамических массивов, в которые можно добавлять новые элементы и исключить возможность выхода за пределы текущего размера массива. Общие свойства массивов с такими сойствами, не зависящие от типа элементов массива, объединим в классе TBase, а для массивов с различными типами элеменов образуем свои классы. Описания классов объединим в файле заголовков TBASEARR.H, а определения методов приведем в файле TBASEARR.CPP.
// ???? TBASEARR.H#include <string.h>#include <iostream.h>class TBase //??????? ????? ??? ???????? ???? ????? {int size, //?????? ???????? count, //??????? ????? ????????? maxCount, //?????? ?????????? ?????? ? ?????? delta; //?????????? ?????? ? ?????? char *pmem; //????????? ?? ?????????? ?????? int changeSize(); //????????????????? ??????protected: void* getAddr( ){return (void*) pmem;};void addNewItem(void*); //?????????? ? ????? ???????void error(const char* msg){cout <<msg<<endl;}; public: int getCount() {return count;}; TBase(int s,int m,int d); TBase(); TBase(TBase&);~TBase(); }; /* ?????? ? ?????????? ???? int */class TIntArray: public TBase { public: int getElem(int index); // ????????????????????????? void putElem(int index,int &pe); // ???????????????????????????????void addElem(int& el); // ?????????? ???????? ? ????? ??????? TIntArray& add(TIntArray&); // ???????? ???? ???????? ???????????TIntArray& subtract(TIntArray&); // ?????????????????void printElem(int index); // ????? ???????? ???????? ?? ????? void print(); // ????? ?? ????? ????? ???????TIntArray(int m,int d):TBase((int)sizeof(int),m,d){ }; /*??????????? */ TIntArray(TBase& a):TBase( a ){}; /*??????????? */~TIntArray(); };Определения методов приведены в файле TBASEARR.CPP:
#include <iostream.h>#include <stdlib.h>#include <constrea.h>#include <tbasearr.h>/* ?????? ?????? TBase */TBase::TBase(int s,int m,int d):size(s),maxCount(m),delta(d) {char* p; int k; count = 0; p = pmem = new char [size * maxCount]; for (k=0; k < maxCount; k++) { *p = '\0'; p++;} } TBase::TBase():size(1),maxCount(10),delta(1) {char* p; int k; count = 0; p = pmem = new char [size *maxCount]; for (k=0; k < maxCount; k++) { *p = '\0'; p++;} } TBase::TBase(TBase& b):size(b.size),maxCount(b.maxCount),delta(b.delta) { int k; count = b.count; pmem = new char [size * maxCount]; for (k=0; k < maxCount * size; k++) { pmem[k] = b.pmem[k];} } TBase::~TBase () { delete [ ] pmem; }4.5 Виртуальные функции
4.5.1. Понятие о “позднем” связывании
При описании объектных типов функции, имеющие сходное назначение в разных классах, могут иметь одинаковые имена, типы параметров и возвращаемого значения. При обращении к такой функции с указанием имени объекта компилятору известно, какая из одноименных функций требуется. В то же время к объектам производного типа можно обращаться по указателю на базовый тип и тогда на этапе компиляции нельзя установить, функция какого из производных типов должна быть вызвана. В ходе выполнения программы требуется проверять, на объект какого типа ссылается указатель и после такой проверки вызывать требуемую функцию. Эти действия называют “поздним” связыванием, в отличие от “раннего” связывания, при котором уже на этапах компиляции или редактирования связей можно установить адрес точки входа вызываемой функции. В объектно-ориентированных языках программирования для решения этой проблемы применяются виртуальные методы.
4.5.2. Описание виртуальных функций
Функция-компонента класса объявляется как виртуальная указанием ключевого слова virtual. Функции-компоненты в производных классах, заменяющие виртуальную функцию базового класса должны объявляться с тем же именем, тем же списком параметров и типом возвращаемого значения, что и соответствующая функция базового класса. Если из производного класса не образуется новых производных классов, ключевое слово virtual в описании функции можно опустить.
Если в производном классе нет объявления функции с тем же именем, что и виртуальная функция базового класса, будет вызываться функция базового класса.
Виртуальная функция может быть объявлена в форме:
virtual void print ( ) = 0;Такая функция называется “чистой” (pure) виртуальной функцией, а объектный тип, содержащий ее объявление, называется абстрактным объектным типом. В программе не могут создаваться экземпляры абстрактных типов, такой тип может использоваться только для образования производных типов, причем в производном типе следует либо снова определить эту виртуальную функцию как чистую, либо обявить ее как обычную виртуальную функцию, выполняющую конкретные действия.
Виртуальные функции особенно полезны, когда к методом класса требуется обращаться через указатель на экземпляр класса, а сам этот указатель имеет тип указателя на базовый класс. Пусть, например, в классе TBase объявлена чистая виртуальная функция print:
class TBase //??????? ????? ??? ???????? ???? ????? {int size, //?????? ???????? count, //??????? ????? ????????? maxCount, //?????? ?????????? ?????? ? ?????? delta; //?????????? ?????? ? ?????? char *pmem; //????????? ?? ?????????? ?????? int changeSize(); //????????????????? ??????protected: void* getAddr( ){return (void*) pmem;};void addNewItem(void*); //?????????? ? ????? ???????void error(const char* msg){cout <<msg<<endl;}; public: int getCount() {return count;}; TBase(int s,int m,int d); TBase(); TBase(TBase&); ~TBase(); virtual void print ( ) = 0; // ???????????????????????? };