Препроцессорные средства в C и С++
Препроцессорные средства в C и С++
Препроцессорные средства в C и С++3.1 Основные понятия препроцессорной обработкиПрепроцессорная обработка (макрообработка) -- это преобразование текста путем замены препроцессорных переменных их значениями и выполнения препроцессорных операторов (директив препроцессора). В общем случае препроцессорные средства включают: - определение препроцессорных переменных и присвоенных им значений; - средства управления просмотром преобразуемого текста; - правила подстановки значений макропеременных. Определение препроцессорной переменной часто называют макроопределением или макросом, а подстановку ее значения в обрабатываемый текст -- макрорасширением. Макрообработка состоит в последовательном просмотре исходного текста и выделения в нем лексем -- сканировании текста. Если выделенная лексема является препроцессорной переменной, она заменяется на свое значение, т.е. строится макрорасширение. Если встречается препроцессорная директива, то она выполняется. Лексемы, не являющиеся препроцессорными переменными или директивами, переносятся в выходной текст без изменения. Результатом такой обработки является текст, не содержащий препроцессорных директив и препроцессорных переменных. Если исходный текст был программой на C или C++, то после макрообработки должен быть получен синтаксически правильный текст на C или C++. Как правило, строковые литералы (строки в кавычках) рассматриваются препроцессором как отдельные лексемы и переносятся в выходной текст без изменения. Препроцессор обычно обеспечивает возможность включения в программу исходных текстов из других файлов, в Си/Си++ это выполняется по директиве # include имя файла Если включаемый файл находится в одном из оглавлений, указываемых в установках интегрированной среды в пункте options-directory-include, где можно указать несколько путей, разделяя их точкой с запятой, имя файла заключается в уголковые кавычки, например # include <iostream.h> в остальных случаях указывается полный путь к включаемому файлу в кавычках:# include “c:\myinclud\includ1.h” При включении файла на место директивы #include вставляется текст из этого файла, а последующие строки исходного файла сдвигаются вниз, после чего вставленный текст сканируется препроцессором. Отметим, что директивы препроцессора всегда записывается с новой строки и первым символом директивы должен быть знак #, которому могут предшествовать только пробелы и знаки табуляции. Концом текста директивы служит конец строки. Если директива не помещается в одной строке, в конце строки ставится знак \ и директива продолжается на следующей строке. Количество строк продолжения не ограничивается. 3.2. Препроцессорные переменныеПрепроцессорная переменная (макроимя) объявляется директивой # define идентификатор значение Например: # define L_NAME 6 # define END_FORMULA `;' # define DEBUG Если объявленное таким способом макроимя встретится в последующем тексте программы, оно будет заменено на соответствующее значение: char namevar [L_NAME]; // эквивалентно char namevar[6]; if ( c != END_FORMULA ) ...... // if ( c != `;')..... Переменная DEBUG объявлена. но не имеет значения. В последующем тексте можно проверять. объявлено или нет это имя, и в зависимости от результата проверки включать или не включать в программу некоторые операторы. Объявленное в define макроимя известно препроцессору от точки его объявления до конца файла или пока не встретится директива # undef имя Например, #undef DEBUG Если в последующем тексте встретится имя DEBUG, оно будет рассматриваться как обычное, а не препроцессорное имя. Имеется ряд предопределенных макроимен, предусмотренных стандартами на языки C и C++, в том числе: _ _LINE_ _ - номер строки в исходном файле, _ _FILE_ _ - имя обрабатываемого файла, _ _DATE_ _ - дата начала обработки препроцессором, _ _TIME_ _ - время начала обработки, _ _STDC_ _ - программа должна соответствовать стандарту ANSI. _ _cplusplus - компилировать программу в соответствии с синтаксисом Си++, _ _PASCAL_ _ - последующие имена по умолчанию имеют тип “имя языка Pascal” Предопределенные имена нельзя объявлять в #define или отменять в #undef. Макросы FILE, DATE и TIME могут использоваться в сообщениях, выдаваемых в начале программы для указания, какая версия программы используется, например, cout << “\n Файл “ << __FILE__ << “ От “ << __DATE__ << “\n”; Макрос PASCAL применяется при описании функций, предназначенных для использования в программах, написанных на языке Pascal, а также функций, вызываемых операционной системой Windows. При программировании на Си директивы типа #define MAX_LEN 80 обычно применяются для задания именованных констант. Введение описателя const в последние версии Си и в Си++ позволяет определять именованные константы так же, как и обычные переменные.3.3. Макроопределения (макросы)Рассмотренный выше вариант директивы #define -- частный случай. Полный синтаксис этой директивы имеет вид: # define идентификатор(параметры) список_замены Параметры задаются их именами, список замены - это текст на C, C++, содержащий имена параметров, например: # define MAX(a,b) ( (a) > (b) )? (a) : (b) # define PRINT(a) cout<< #a<<“= “<<(a)<<endl; Если в области действия этих макроопределений встретится текст x = MAX( y + p, s); то он будет заменен на x = ( (y + p) > (s)) ? (y + p) : (s); оператор PRINT(x) будет заменен на cout<<“x”<<“= “<<(x)<<endl; Знак # перед именем параметра означает, что значение аргумента рассматривается как строковый литерал. Если между двумя параметрами в макрорасширении стоят знаки ##, то значения аргументов сцепляются по правилу сцепления строк, например, # define VAR(a,b) ( a##b ) x = d [ VAR(i,j)]; // x = d [ ( ij )]; Использование макросов в ряде случаев позволяет сократить исходный текст программы и сделать его более наглядным. Например, если поместить в файл-заголовок макросы #if defined(__cplusplus) # define _PTRDEF(name) typedef name * P##name; # define _REFDEF(name) typedef name & R##name; # define _STRUCTDEF(name) struct name; \ _PTRDEF(name) \ _REFDEF(name) \ #endif то мы получим возможность одной строкой программы _STRUCTDEF(MyStruct) объявить имя структурного типа MyStruct, указатель на этот тип PMyStruct и тип ссылки на него RMyStruct, т.е. получить в выходном тексте строчкиstruct MyStruct; typedef MyStruct *PMyStruct; typedef MyStruct &RMystruct; 3.4. Условная компиляцияДирективы препроцессора # if, # else , # endif и # elif позволяют, в зависимости от результатов проверки некоторых условий, включать в программу один из нескольких вариантов текста: # if препроцессорное_условие текст 1 # else текст 2 # endif дальнейший текст. Условие -- это константное выражение, которое строится из макроимен, констант и знаков операций, включая логические связки && и | | . Допускается также выражение sizeof (имя_типа) и препроцессорная функция defined( макроимя ), возвращающая 1, если это макроимя определено, и 0, если оно не определено. Вместо директивы # if defined( DEBUG ) можно написать# ifdef DEBUG а вместо# if !defined( DEBUG ) написать# ifndef DEBUG Комбинации #if - #else могут быть вложенными, причем последовательность #else - #if заменяется одной директивой #elif с условием: # if препроцессорное_условие_1 текст 1 # elif препроцессорное_условие_2 текст_2 # else текст_3 # endif В файлах заголовков для предотвращения многократного включения одного и того же заголовка в программу обычно присутствует текст вида: #if !defined(_ _DEFS_H) #define _ _DEFS_H /* Текст объявляемых заголовков */ ....................................... #endif /* Конец _ _DEFS_H */ 4. Объектно-ориентированные средства С++4.1 Объектные типы данныхОбъектные типы данных - это агрегатные типы, полностью определяемые программистом, описание объектного типа должно содержать компоненты-данные, определяющие область возможных значений переменных этого типа, и описание операций, допустимых над переменными этого типа и компонентами-данными, составляющими переменную. Для сохранения совместимости с программами на Си синтаксис описания объектного типа в Си++ выбран подобным описанию структурного типа или типа объединения в Си. В сущности структуры и объединения в Си++ рассматриваются как варианты объектных типов. Имеются три варианта объектных типов: структура (struct), объединение (union) и класс (class), различающиеся возможностями доступа к компонентам типа. В дальнейшем для краткости все варианты объектных типов будем называть классами. Описание объектного типа строится по схеме: вариант_типа имя_типа : список_базовых_классов { компоненты (члены ) класса } Компонентами класса могут быть компоненты-данные и компоненты-функции. Компоненты-функции предназначены для выполнения операций над объектным данным, их часто называют методами класса. Для каждого компонента класса устанавливается уровень доступа либо явно, указанием уровня доступа одним из ключевых слов public, protected или private с двоеточием, либо неявно, по умолчанию. Указание уровня доступа относится ко всем последующим компонентам класса, пока не встретится указание другого уровня доступа. Уровень доступа public разрешает доступ к компонентам класса из любого места программы, в котором известна переменная этого класса. Уровень доступа private разрешает доступ к компонентам класса только из методов этого класса. Уровень доступа protected имеет смысл только в иерархической системе классов и разрешает доступ к компонентам этого уровня из методов производного класса. По умолчанию для всех компонент класса типа struct принимается уровень доступа public, но можно явно задавать и другие уровни доступа, уровень доступа к компонентам класса типа class по умолчанию private, явно можно определять и другие уровни, для класса типа union уровень доступа public и не может быть изменен. Например, пусть программист решил в классе TPoint (точка) запретить внешний доступ к координатам точки и разрешить внешний доступ к методам перемещения точки на плоскости. Описание класса TPoint можно построить так: class TPoint { private: int x,y; public: void movePoint ( int newx, int newy); // в новую точку void relmove ( int dx, int dy ); // смещение на dx,dy int getx ( void ) ( return x ; }; int gety ( void ) { return y ; }; }; Описание тела компоненты-функции может быть включено в описание класса, как это сделано в примере для функций 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 ( ) 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; // Чистая виртуальная функция }; Тогда в производных классах должна быть объявлена замещающая ее функция print, выполняющая реальные действия: class TIntArray : public TBase { /* Другие методы */ virtual void print ( ); } class TRealArray : public TBase { /* Другие методы */ virtual void print ( ); } В программе, использующей объекты классов TIntArray и TRealArray могут создаваться экземпляры этих классов с возможностью обращения к ним через указатель на базовый класс: TBase *pb; TIntArray aint(5,3); TRealArray areal(4,2); Тогда для печати массивов могут применяться операторы pb = &aint; pb->print(); //Печать массива aint pb = &areal; pb->print(); // Печать массива areal Приведем еще один пример использования виртуальных функций. Пусть некоторый любитель домашних животных решил завести каталог своих любимцев и для каждого вида животных определил свой класс с общим базовым классом Pet. Для краткости ограничимся в описании каждого животного его кличкой и типовым излаваемым животным звуком с возможностью вывода на экран списка кличек и представления издаваемых ими звуков. Программа: #include <iostream.h> struct Pet // Базовый класс { char *name; virtual void speak() = 0; Pet( char *nm){name=nm;} } struct Dog : public Pet { virtual void speak( ) { cout<<name<<“ говорит “”<<“ Ав - ав”<<endl; }; Dog(char *nm): Pet(nm) { }; }; struct Cat : public Pet { virtual void speak( ) { cout<<name<<“ говорит “ <<“ Мяу-Мяу”<<endl; Cat(char *nm): Pet(nm) { }; } int main () { Pet *mypets[ ] = { new Dog(“Шарик”), new Cat(“Мурка”), new Dog(“Рыжий “)}; // Список животных const int sz = sizeof( mypets)/ sizeof( mypets [ 0 ]); for ( int k = 0: k < sz; k++) mypets [ k ]->speak(); return 0; } 4.6. “Дружественные” (friend) функцииФункция, объявленная в производном классе, может иметь доступ только к защищенным (protected) или общим (public) компонентам базового класса. Функция, объявленная вне класса, может иметь доступ только к общим (public) компонентам класса и обращаться к ним по имени, уточненному именем объекта или указателя на объект. Чтобы получить доступ к личным компонентам объектов некоторого класса Х в функции, не имеющей к ним доступа, эта функция должна быть объявлена дружественной в классе X: class X { friend void Y:: fprv( int, char*); /* Другие компоненты класса X */ } Можно объявить все функции класса Y дружественными в классе X; class Y; class X { friend Y; /* Другие компоненты класса X */ } class Y { void fy1(int, int); int fy2( char*, int); /* Другие компоненты класса Y */ } Дружественной может быть и функция, не являющаяся компонентой какого-либо класса, например, class XX { friend int printXX ( ); /* Другие компоненты класса ХХ */ } Здесь функция printXX имеет доступ ко всем компонентам класса XX, независимо от закрепленного за ними уровня доступа. В теории объектно-ориентированного программирования считается, что при хорошо спроектированной системе классов не должно быть необходимости в дружественных функциях, однако в ряде случаев их использование упрощает понимание и последующие модификации программы. 4.7. Статические компоненты классаОписатель static в С++ имеет различное назначение в зависимости от контекста, в котором он применен. Переменные и функции, объявленные вне класса и вне тела функции с описателем static, имеют область действия, ограниченную файлом, в котором они объявлены. Переменные, объявленные как static внутри функции, видимы только внутри этой функции, но сохраняют свои значения после выхода из функции и инициализируются только при первом обращении к функции. Компоненты класса также могут объявляться с описателем static, такие компоненты - данные являются общими для всех экземпляров объектов этого класса и размещаются в памяти отдельно от данных объектов класса. Доступ к static - компонентам класса возможен по имени, уточненному именем класса (именем типа) или именем объекта этого класса, причем к static - компонентам класса можно обращаться до создания экземпляров объектов этого класса. Статическое данное - член класса должно быть обязательно инициализировано вне описания класса: class TBase //базовый класс для массивов всех типов { static int nw; int size, //размер элемента
Страницы: 1, 2
|