Рефетека.ру / Информатика и програм-ие

Учебное пособие: Препроцессорные средства в 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 ( )

{ 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; // Чистая виртуальная функция

};

Тогда в производных классах должна быть объявлена замещающая ее функция 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, //размер элемента

count, //текущее число элементов

maxCount, //размер выделенной памяти

delta; //приращение памяти

/* Другие компоненты класса TBase */

}

int TBase::nw =1; /* Инициализация статической компоненты класса */

Статические компоненты - функции могут вызываться до создания экземпляров объектов этого класса и поэтому имеют доступ только к статическим данным класса:

class X

{ static int sx1,sx2;

static void fsx ( int k);

int x1,x2;

/* Другие компоненты класса X */

}

int X::sx1 = 1;

int X::sx2 = 2;

int main ()

{ ..........

X:: fsx( 3 );

..............

}

4.8. Переопределение (перегрузка) операций

В языках программирования определена семантика операций, выполняемых над базовыми (предопределенными) типами данных, например, если x, y и z - переменные типа float, то запись x = y + z; предполагает интуитивно очевидные действия, сложение x и y и присваивание переменной z полученной суммы.

Желательно было бы и для типов, определяемых в программе, в том числе для классов, определить семантику и алгоритмы операций сложения, вычитания, умножения и т.д., чтобы иметь возможность вместо вызова соответствующих функций записывать просто x + y и в случае, когда x и y являются объектами некоторых классов. В C++ это достигается переопределением имеющихся в языке операций для других типов данных.

Переопределенная операция объявляется так:

тип_результата operator знак_операции (формальные параметры)

{ описание_алгоритма_выполнения_операции }

Например:

class TPoint

{ int x,y;

public:

TPoint& operator+=( const TPoint& adder );

TPoint& operator-=( const TPoint& subber );

friend TPoint operator - ( const TPoint& one, const TPoint& two);

friend TPoint operator + ( const TPoint& one, const TPoint& two);

friend int operator == ( const TPoint& one, const TPoint& two);

friend int operator != ( const TPoint& one, const TPoint& two);

};

Полное определение этих операций для объектов класса TPoint имеет вид:

inline TPoint& TPoint::operator += ( const TPoint& adder )

{ x += adder.x; y += adder.y; return *this;}

inline TPoint& TPoint::operator -= ( const TPoint& subber )

{ x -= subber.x; y -= subber.y; return *this;}

Остальные операции определяются аналогичным образом.

Пусть в программе имеются объявления:

TPoint x(12,3), y(21,30), z(18,30);

Тогда можно записать:

x +=y; y-=z; TPoint r = x + z:

Общие правила переопределения операций сводятся к следующему:

- Двуместные операции должны иметь два параметра, одноместные - один параметр, причем, если операция объявлена как компонента класса, то неявным первым операндом является экземпляр объекта (следовательно при определении двуместной операции будет задаваться один параметр, одноместная операция объявляется с пустым списком параметров). Если операция переопределяется вне класса (с описателем friend ), то для двуместной операции должны быть заданы два параметра, для одноместной операции - один параметр.

- При переопределении сохраняется приоритет исходной операции т.е. операция + будет выполняться раньше операции = и т.д.

- При переопределении не наследуются свойства коммутативности и ассциативности, т.е. результат выражения х + y - z может отличаться от результата выражения y - z + x и зависит от того, как определены соответствующие операции.

- Не допускается переопределение операций . (точка), .* ( точка -звездочка, обращение к указателю на компоненту класса или структуры), :: (разрешение контекста), а также операции # и ##, используемые при препроцессорной обработке.

- Переопределяемые операции = (присваивание), () (функция), [ ] (индекс), -> (обращение к компоненте класса по указателю) всегда должны быть компонентами класса и не могут быть static.

- Переопределяемые операции new и delete должны быть static - компонентами класса.

В остальном к переопределяемым операциям предъявляются те же требования, что и к функциям.

5. Шаблоны функций и классов


5.1. Шаблоны функций


Часто встречаются функции, реализующие одни и те же действия для аргументов различных типов. Например, сортировка массива по возрастанию его элементов может выполняться одним и тем же методом и для данных типа int и для данных типа double. Различие состоит только в типах параметров и некоторых внутренних переменных.

В более поздние версии С++ включено специальное средство, позволяющее параметризовать определение функции, чтобы компилятор мог построить конкретную реализацию функции для указанного типа параметров функции. Параметризованное определение функции строится по схеме:

template < class имя_класса >

Заголовок функции

{ /* Тело функции */ }

Имя класса является параметром и задается идентификатором, локализованным в пределах определения функции. Хотя бы один из параметров функции должен иметь тип, соответствующий этому идентификатору.

Параметризованное определение функции сортировки массива методом перестановок может быть построено следующим образом:

template <class T >

void sort ( T a[ ], int n )

{ T temp;

int sign;

for ( int k = 0; k > n; k++)

{ sign = 0;

for ( i = 0; i <n - k; i++)

if ( a [ i ] > a [ i + 1])

{ temp = a [ i ];

a[ i ] = a[ i + 1 ];

a[ i + 1 ] = temp; sign++;

}

if ( sign == 0 ) break;

}

return;

}

Если в программе будут объявлены массивы

int aint [10];

double afl [20];

и установлены значения элементов этих массивов, то вызов функции

sort ( aint, 10 );

обеспечит вызов sort для упорядочения массива целых, а вызов функции

sort ( afl , 20 )

обеспечит вызов sort для упорядочения массива с элементами типа double.

Если элементами массива являются объекты какого-либо определенного программистом класса, для которого определена операция отношения >, то функция sort может быть вызвана и для такого массива. Разумеется, в объектном коде программы будут присутствовать все варианты реально вызывамой функции sort. Параметризация функции сокращает объем исходного текста программы и повышает его надежность.

В описателе template можно указывать несколько параметров вида class имя_типа, а также параметры базовых типов. Например, функция

template < class T1, class T2 >

void copy ( T1 a[ ], T2 b[ ], int n)

{ for ( int i = 0; i <n; i++)

a[ i ] = b [ i ] ;

}

копирует первые n элементов массива b типа T2 в первые n элементов массива a типа T1. Разумеется, программист несет ответственность за то, чтобы такое копирование было возможным.

5.2. Шаблоны классов

По аналогии с параметризованной функцией можно построить параметризованное описание класса, позволяющее создавать экземпляры классов для конкретных значений параметров. Параметризованный класс описывается следующим образом:

template <class T >

class описание класса

Как и для функций, в описателе template может быть задано несколько параметров. В самом описание класса имена параметров используются как имена типов данных, типов параметров функций и типов значений, возвращаемых функциями.

В качестве примера приведем описание класса stack, предназначенного для построения стеков фиксированного максимального размера с элементами произволного типа.

enum BOOLEAN ( FALSE, TRUE );

template <class Type >

class stack

{ private:

enum ( EMPTY = -1 );

Type* s; /* Указатель на массив стека */

int max_len; /* Максимальная длина стека */

int top; /* Индекс элемента в вершине стека */

public:

stack ( ) : max_len ( 100 ) /* конструктор без параметров */

{ s = new Type [ 100 ]; top = EMPTY; }

stack ( int size ) : max_len( size ) /* Второй конструктор */

{ s = new Type [ size ]; top = EMPTY; }

~stack ( ) { delete [ ] s; } /* Деструктор */

void reset ( ) { top = EMPTY; } /* Очистить стек */

void push ( Type c ) { s [ ++top ] = c; }

Type pop ( ) { return (s [top—] }

Type top_of ( ) { return ( s [top ] }

BOOLEAN empty ( ) { return BOOLEAN ( top == EMPTY ) }

BOOLEAN full ( ) { return BOOLEAN ( top == max_len ) }

};

Следует отметить, что в этом примере с целью сокращения исходного текста не предусмотрен контроль выхода за пределы стека в методах push и pop.

Чтобы создать экземпляр параметризованного объектного типа, нужно уточнить имя типа значением параметра в угловых скобках:

stack < int > stack_of_int (50); /* Стек на 50 элементов типа int */

stack < myClass > stmc (20); /* Стек на 20 элементов типа myClass */

В приведенном примере все компоненты-функции определены в описании класса. Когда полное определение функции-члена класса задается вне описания класса, оно должно уточняться описателем template. Например, если бы метод top_of был определен вне описания класса, определение имело бы вид:

template < class Type >

Type top_of ( ) { return s [ top ];}

Отметим некоторые специфические черты описаний параметризованных классов.

Если в параметризованном классе определены friend-функции, то когда такая функция не зависит от параметра, будет использоваться единственная friend-функция для всех значений параметра, а когда friend-функция зависит от параметра, будет использоваться своя friend-функция для каждого значения параметра.

Если в параметризованном классе имеются статические (static) компоненты, то для каждого значения параметра будет использоваться свой экземпляр статической компоненты.

6. Классы для ввода-вывода потоков


6.1. Система классов ввода-вывода


Система ввода-вывода С++ основывается на концепции потоков данных, поток представляет собой, с одной стороны, последовательность данных, с другой стороны поток рассматривается как переменная некоторого объектного типа. Это позволяет вынести общие свойства и операции процессов ввода-вывода в определения базовых классов. Ввод-вывод из файлов и консоли, как правило, выполняется с использованием буфера и получение данных программой или вывод данных сводится к пересылке данных из одной области памяти в другую. Реальное обращение к внешним устройствам происходит только при исчерпании данных в буфере (при вводе) или при заполнении буфера (при выводе).

Система классов ввода-вывода С++ использует два базовых класса: класс ios и класс streambuf.

В классе ios определены данные, характеризующие состояние потока, и функции, позволяющие получить доступ к информации о состоянии потока или изменить его состояние. Состояние потока определяется набором битовых флагов, для обращения к отдельным флагам в классе ios описаны перечислимые константы:

- биты состояния (статуса) потока

enum io_state { goodbit = 0x00, // никакие биты не установлены, все хорошо

eofbit = 0x01, // конец файла

failbit = 0x02, // ошибка в последней операции ввода/вывода

badbit = 0x04, // попытка выполнить неверную операцию

hardfail = 0x80 // неисправимая ошибка

};

- биты режима использования потока (режима ввода/вывода)

enum open_mode { in = 0x01, // поток открыт для чтения

out = 0x02, // поток открыт для записи

ate = 0x04, // перейти в конец файла при открытии

app = 0x08, // режим добавления в конец файла

trunc = 0x10, // усечение существующего файла

nocreate = 0x20, // ошибка открытия файла, если он не существует

noreplace = 0x40, // ошибка открытия, если файл существует

binary = 0x80 // двоичный (не текстовый) файл

};

- флаги направления позиционирования в потоке

enum seek_dir { beg=0, cur=1, end=2 };

- флаги - манипуляторы управления вводом/выводом

enum { skipws = 0x0001, // пропускать пробелы при вводе

left = 0x0002, // выравнивание влево при выводе

right = 0x0004, // выравнивание вправо при выводе

internal = 0x0008, // пробел после знака или основания системы счисления

dec = 0x0010, // преобразование в десятичную систему счисления

oct = 0x0020, // преобразование в восьмеричную систему счисления

hex = 0x0040, // шестнадцатеричное преобразование

showbase = 0x0080, // использовать индикатор системы счисления при выводе

showpoint = 0x0100, // указывать десятичную точку при выводе

//(в числах с плавающей точкой)

uppercase = 0x0200, // прописные буквы при шестнадцатеричном выводе

showpos = 0x0400, // добавлять '+' для положительных целых

scientific= 0x0800, // применять нотацию вида 1.2345E2

fixed = 0x1000, // применять нотацию вида 123.45

unitbuf = 0x2000, // очищать все потоки после вставки в поток

stdio = 0x4000, // очищать stdout, stderr после вставки в поток

boolalpha = 0x8000 // вставлять/извлекать булевы как текст или цифры

};

Поскольку эти перечислимые константы объявлены как компоненты класса ios, для доступа к ним требуется уточнение контекста, например, ios::in.

Класс streambuf обеспечивает создание и использование буфера ввода-вывода и содержит компоненты-данные для управления буфером и методы доступа к данным в буфере. Объект класса ios содержит указатель на связанный с ним объект streambuf.

Классы istream и ostream являются производными от класса ios, в них определены функции, выполняющие ввод (istream) и вывод (ostream) данных базовых типов и строк.

В классе istream определены операции бесформатного ввода (без преобразования вводимых данных)и операции форматного ввода с преобразованием из внешнего представления во внутреннее.

Функции get() читают из потока данные типа char в массив, определяемый первым параметром, второй параметр задает максимальное число вводимых символов (l), третий параметр устанавливает символ-ограничитель, ограничивающий ввод. За последним введенным символом в массив пишется символ ‘\0’.

istream _FAR & _RTLENTRY get( char _FAR *, int l, char = '\n');

istream _FAR & _RTLENTRY get( signed char _FAR *, int l, char = '\n');

istream _FAR & _RTLENTRY get( unsigned char _FAR *, int l, char ='\n');

Функции read() выполняют чтение из потока и занесение в массив указанного числа символов

istream _FAR & _RTLENTRY read( char _FAR *, int l);

istream _FAR & _RTLENTRY read( signed char _FAR *, int l);

istream _FAR & _RTLENTRY read(unsigned char _FAR *, int l);

Для ввода строки, заканчивающейся символом-ограничителем, служит функция getline() при этом символ-ограничитель также заносится в массив

istream _FAR & _RTLENTRY getline( char _FAR *, int, char = '\n');

istream _FAR & _RTLENTRY getline( signed char _FAR *, int, char= '\n');

istream _FAR & _RTLENTRY getline(unsigned char _FAR *, int, char= '\n');

Для извлечения из потока данных типа char вплоть до символа-ограничителя служит вариант функции get():

istream _FAR & _RTLENTRY get(streambuf _FAR &, char = '\n');

Другие варианты функции get() обеспечивают извлечение одного символа

istream _FAR & _RTLENTRY get( char _FAR &);

istream _FAR & _RTLENTRY get( signed char _FAR &);

istream _FAR & _RTLENTRY get(unsigned char _FAR &);

int _RTLENTRY get(); // возвращает значение символа

int _RTLENTRY peek(); // возвращает следующий символ без удаления его из потока

int _RTLENTRY gcount(); // число символов, извлеченных из потока

// возвращение указанного символа в поток ввода

istream _FAR & _RTLENTRY putback(char);

Пропуск символов с остановкой по ограничителю выполняет функция

istream _FAR & _RTLENTRY ignore(int = 1, int = EOF);

Форматный ввод релизуется на основе переопределения операций ввода

istream _FAR & _RTLENTRY operator>> (bool _FAR &);

istream _FAR & _RTLENTRY operator>>

(istream _FAR & (_RTLENTRY *_f)(istream _FAR &));

istream _FAR & _RTLENTRY operator>>

(ios _FAR & (_RTLENTRY *_f)(ios _FAR &) );

istream _FAR & _RTLENTRY operator>> ( char _FAR *);

istream _FAR & _RTLENTRY operator>> ( signed char _FAR *);

istream _FAR & _RTLENTRY operator>> (unsigned char _FAR *);

istream _FAR & _RTLENTRY operator>> ( char _FAR &);

istream _FAR & _RTLENTRY operator>> ( signed char _FAR &);

istream _FAR & _RTLENTRY operator>> (unsigned char _FAR &);

istream _FAR & _RTLENTRY operator>> (short _FAR &);

istream _FAR & _RTLENTRY operator>> (int _FAR &);

istream _FAR & _RTLENTRY operator>> (long _FAR &);

istream _FAR & _RTLENTRY operator>> (unsigned short _FAR &);

istream _FAR & _RTLENTRY operator>> (unsigned int _FAR &);

istream _FAR & _RTLENTRY operator>> (unsigned long _FAR &);

istream _FAR & _RTLENTRY operator>> (float _FAR &);

istream _FAR & _RTLENTRY operator>> (double _FAR &);

istream _FAR & _RTLENTRY operator>> (long double _FAR &);

Извлечение из istream и вставка в объект типа streambuf выполняет оператор-функция

istream _FAR & _RTLENTRY operator>> (streambuf _FAR *);

Макрос FAR управляет способом представления указателей, макрос RTLENTRY определяет применение стандартной билиотеки времени выполнения.

Аналогичные операции для вывода определены в классе ostream:

// вставить символ в поток

ostream _FAR & _RTLENTRY put( char);

ostream _FAR & _RTLENTRY put(signed char);

ostream _FAR & _RTLENTRY put(unsigned char);

// вставить в поток строку

ostream _FAR & _RTLENTRY write(const char _FAR *, int l);

ostream _FAR & _RTLENTRY write(const signed char _FAR *, int l);

ostream _FAR & _RTLENTRY write(const unsigned char _FAR *, int l);

Операции форматированного вывола

Вывод "true" или "false" для данных типа bool

ostream _FAR & _RTLENTRY ostream::operator<< (bool);

Вывод ланных типа char

ostream _FAR & _RTLENTRY operator<< ( char);

ostream _FAR & _RTLENTRY operator<< ( signed char);

ostream _FAR & _RTLENTRY operator<< (unsigned char);

Вывод числовых данных с преобразованием во внешнее представление

ostream _FAR & _RTLENTRY operator<< (short);

ostream _FAR & _RTLENTRY operator<< (unsigned short);

ostream _FAR & _RTLENTRY operator<< (int);

ostream _FAR & _RTLENTRY operator<< (unsigned int);

ostream _FAR & _RTLENTRY operator<< (long);

ostream _FAR & _RTLENTRY operator<< (unsigned long);

ostream _FAR & _RTLENTRY operator<< (float);

ostream _FAR & _RTLENTRY operator<< (double);

ostream _FAR & _RTLENTRY operator<< (long double);

Вывод строк, оканчивающихся нулевым байтом

ostream _FAR & _RTLENTRY operator<< (const char _FAR *);

ostream _FAR & _RTLENTRY operator<< (const signed char _FAR*);

ostream _FAR & _RTLENTRY operator<< (const unsigned char _FAR*);

Вывод значения указателя в символьном формате

ostream _FAR & _RTLENTRY operator<< (void _FAR *);

Извлечение данных их объекта streambuf и вставка в тот же ostream

ostream _FAR & _RTLENTRY operator<< (streambuf _FAR *);

Вывод значений манипуляторов

ostream _FAR & _RTLENTRY operator<<

(ostream _FAR & (_RTLENTRY *_f)(ostream _FAR &));

ostream _FAR & _RTLENTRY operator<<

(ios _FAR & (_RTLENTRY *_f)(ios _FAR &));

Имеется также класс iostream, производный от класса ios и объединяющий возможности классов istream и ostream.

Для рассмотренных выше классов отсутствуют конструкторы копирования и операция присваивания, точнее, они объявлены, но не определены. Для тех случаев, когда конструктор копирования и операция присваивания необходимы, предусмотрены классы istream_withassign, ostream_withassign и iostream_withassign. Как экземпляры объектов этих классов всегда объявляется объект cin (экземпляр istream_withassign), обычно предназначенный для ввода с клавиатуры, и объекты cout, cerr и clog (экземпляры ostream_withassign), обычно предназначенные для вывода на экран.

Ввод-вывод для дисковых файлов обепечивается классами, описания которых содержатся в файле fstream.h.

Класс filebuf, производный от streambuf, предназначен для добавления в streambuf дополнительных средств управления буфером ввода-вывода.

Класс fstreambase, производный от класса ios, служит базой для остальных классов, обеспечивающих файловый ввод-вывод, в нем определены методы:

void _RTLENTRY open(const char _FAR *, int, int = filebuf::openprot);

void _RTLENTRY attach(int);

void _RTLENTRY close();

void _RTLENTRY setbuf(char _FAR *, int);

Назначение этих методов очевидным образом следует из их названий.

Для непосредственной работы с файлами служат классы ifstream, ofstream и fstream, базой для них служат классы fstreambase и, соответственно, istream, ostream и iostream.

6.2. Вывод в файл. Ввод из файла


Для вывода данных в дисковый файл в программе должна присутствовать директива препроцессора

#include <fstream.h>

подключающая описания необходимых классов.

Прежде чем выводить данные необходимо создать объект типа ofstream, для которого имеется несколько конструкторов:

_RTLENTRY ofstream(); // Пустой объект, без привязки к файлу

// С привязкой к файлу, полное имя которого задается первым аргументом:

_RTLENTRY ofstream(const char _FAR *, int = ios::out,

int = filebuf::openprot);

// С привязкой к ранее открытому файлу, заданному своим дескриптором

_RTLENTRY ofstream(int);

// То же, что и предыдущий вариант, но задается новый буфер вывода

_RTLENTRY ofstream(int __f, char _FAR *, int);

Наиболее часто оказывается полезным второй вариант конструктора, в котором указывается только первый параметр - полное имя файла. Этот конструктор создает объект типа ofstream, открывает указанный файл и присоединяет его к потоку вывода.

Собственно операции вывода реализуются вызовом методов put, write или с использованием переопределенных операций <<.

Аналогичным способом обеспечивается и ввод из файла: создается объект типа ifstream и для ввода применяются методы get, read или переопределенные операции >>.

Для типа ifstream имеется набор аналогичных конструкторов:

_RTLENTRY ifstream();

_RTLENTRY ifstream(const char _FAR *,int = ios::in,

int = filebuf::openprot);

_RTLENTRY ifstream(int);

_RTLENTRY ifstream(int __f, char _FAR *, int);

В качестве примера рассмотрим программу, копирующую данные из одного файла в другой.

#include <fstream.h>

#include <process.h> // Для вызова exit

int main ( int argc, char* argv [ ] )

{ char ch;

if ( argc != 3 ) // Проверка числа аргументов

{ cerr << “ Вызов dcopy файл1 файл2 \n” ; exit ( 1 ) ; }

ifstream source( argv [ 1 ] ) ; // Входной поток

if ( ! source )

{ cerr << “ Нельзя открыть входной файл “ << argv [ 1 ] ;

exit ( 1 ); }

ofstream dest ( argv [2 ] ) ;

if ( ! dest )

{ cerr << “ Нельзя открыть выходной файл “ << argv [ 2 ] ;

exit ( 1 ); }

while ((ch = source.get ( ) ) != EOF )

dest.put( ch );

close ( source ); close ( dest );

return 0 ;

}


6.3. Ввод-вывод данных объектных типов


Организация вывода данных определенных программистом объектных типов, в общем случае зависит от предполагаемого дальнейшего использования этих данных. Могут сохраняться в файле все компоненты данные, или только часть из них, может применяться форматированный или бесформатный вывод. Часто возникает необходимость сохранить объектные данные в файле для последующего их восстановления в той же или другой программе.

Для вывода объекта в файл в определение класса может быть включена функция-компонента с параметром ссылкой на объект типа ostream. Часто такой функции назначают имя print или printon. Более изящным считается переопределение оператора << для вывода объектного данного. Если компоненты-данные объекта имеют уровень доступа protected или private, а оператор << не является компонентой класса, его следует объявить как friend-метод.

Пусть, например, в программе определен класс complex:

class complex

{ double re, im ;

public:

complex (double r =0, double i =0 ): re ( r ), im ( i )

{ } // конструктор

double real ( ) {return ( re ); }

double image ( ) { return ( im ); }

/* другие методы */

}

Тогда для форматированного вывода комплексного числа в формате ( вещественная часть, мнимая часть ) можно так переопределить операцию << :

ostream& operator << ( ostream& s, complex c )

{ int old_precision = s.precision ( 4 ); // установка числа дробных цифр

s << “(“ << c.real( ) <<

“, “ << c.image( ) << “)” ;

s.precision ( old_precision ) ; // восстановление числа дробных цифр

return ( s );

}

В данном случае в переопределении << нет обращения к личным переменным класса Complex. Если не использовать методы real и image, переопределение << нужно было бы включить в описание класса Complex с описателем friend.

Ввод объектного данного из файла или стандартного потока организуется аналогично: либо в определение класса включается функция-компонента для инициализации компонент данных их входного потока, либо переопределяется операция >> для ввода компонент-данных из потока. В некоторых случаях оказывается более удобным включить в описание класса дополнительный конструктор с параметром-ссылкой на объект типа istream, при этом все базовые классы должны иметь аналогичные конструкторы.

Пусть в описание класса Complex включена переопределенная операция >>:

class complex

{ double re, im ;

public:

complex (double r =0,double i=0 ): re( r ), im( i ) { } // конструктор

double real ( ) {return ( re ); }

double image ( ) { return ( im ); }

friend istream& operator >> ( istream& , Complex& );

/* другие методы */

}

Если предположить, что комплексные числа поступают из потока либо в виде вещественного числа, возможно заключенного

в скобки, либо в виде пары чисел, разделенных запятой и заключенной в скобки,

то переопределяемую операцию ввода можно описать следующим образом:

istream& operator >> ( istream& s, Complex& c )

{ double re_n = 0, im_n = 0;

char ch = 0;

s >> ch ;

if ( ch == ‘(‘ )

{ s >> re_n >> ch ;

if ( ch == ‘,’ ) s >> im_n >> ch;

if ( ch != ‘)’) s.clear ( ios::failbit ); //Установка признака ошибки

}

else { s.putback ( ch ); s >> re_n ; }

if ( s ) { c.re = re_n; c.im = im_n; } // Если не было ошибки

return ( s );

}

Рассмотренные выше примеры сохранения объектов

в потоке и восстановления их из потока иллюстрируют наиболее простые варианты

этих операций. Проблема сохранения объектов в потоке существенно усложняется,

когда требуется сохранять в одном потоке объекты разных типов, когда между

объектами разных типов имеются ссылки и некоторый объект содержит указатель

на объект другого типа, причем на один и тот же объект могут ссылаться

несколько других объектов. В этой ситуации требуется для каждого сохраняемого

в потоке объекта заносить в поток идентификатор типа объекта, гарантировать,

что каждый объект, на который имеются ссылки из других объектов, будет

помещен в поток только один раз. Средства для разрешения этих проблем имеются

в библиотеке классов-контейнеров classlib, содержащей

файл objstrm.h с определениями необходимых

классов и макросов.

Приложение 1. Задачи по программированию на Си


1. Составить функцию для подсчета числа серий положительных, отрицательных чисел и нулей длиной не менее К в одномерном массиве целых чисел. Серией называется последовательность элементов массива, принадлежащих одному классу:

int series ( int n, int *mas, int *kzero, int *kplus, int *kminus, int k);

2. Составить функцию для слияния двух упорядоченных по возрастанию массивов целых чисел:

int merge (int n, int m, int *mas1, int *mas2, int *res);

3. Составить функцию для построения списка индексов (номеров), строк упорядоченного по возрастанию элементов заданного (k-го) столбца матрицы. Элементы матрицы - целые числа:

void sort (int n, int *mas, int k, int* index);

4. Составить функцию для определения элемента матрицы, являющегося седловой точкой. Седловой точкой называется элемент, удовлетворяющий условиям:

a[k,l] = max min{ a[i,j] } = min max { a[i,j] }

i<=n,j<=m j<=m,i<=n

Если седловой точки нет, установить k=l=-1

5. Составить функцию для подсчета количества различных чисел в массиве, содержащем n целых чисел.

int count(int *a, int n);

6. Составить функцию для разделения текста, заданного строкой литер, на отдельные слова и подсчета числа слов. Под словом понимается последовательность литер, отличных от пробела, ограниченная слева началом строки или пробелом и справа - пробелом, знаком препинания или концом строки.

int kwords(char* ss, char * sm, int kmax);

ss - исходная строка,

sm - массив строк длиной до 30 литер каждая (для размещения выделенных слов),

kmax - максимальное количество выделенных слов.

Предусмотреть сигнализацию о случаях, когда функция неприменима ( слишком много слов или слишком длинное слово ).

7. Составить функцию для определения

а) наибольшего простого числа, не превосходящего заданное целое n,

б) наименьшего простого числа, превосходящего заданное n.

int simple (int n);

8. Составить функцию для разложения заданного целого числа на простые множители. Результатом функции должен быть массив, содержащий простые множители, и целое число - количество множителей.

int simplefactor(int n, int *masfactor);

9. Составить функцию для вычисления числа сочетаний из n злементов по m (n и m - целые):

Cnm = n! / ((n-m)! * m!)

Результатом функции должно быть целое число, если Cnm < 32767 и булевское значение true, или вещественное число, если Cnm >=32767 и булевское значение false.

Boolean binom (int n, int m, int *cnm, double *dcnm);

10. Многочлены представляются в памяти ЭВМ целым числом n - степенью многочлена и массивом коэффициентов a[0],a[1],...,a[n].

а) Составить функцию для вычисления значения y=P(x) многочлена для заданного аргумента x.

double valpoly( int n, double *a);

б) Составить функцию для вычисления коэффициентов многочлена-произведения двух других многочленов, заданных своими степенями и массивами коэффициентов. Функция возвращает степень многочлена - произведения.

int polyprod( int n, double *a, int m, double *b, double *res);

в) Составить функцию для вычисления коэффициентов многочлена-суммы двух других многочленов. Функция возвращает степень многочлена - суммы.

int polysum ( int n, double *a, int m, double *b, double *res);

11. В некоторой программе обработки таблиц сведения о динамически образуемых столбцах определяются глобальными описаниями :

const int L_cln = 20; /*длина столбца таблицы*/

const int L_tab=30; /*максимальное число столбцов*/

struct COLUM

{ char name [ 5 ]; /* имя столбца */

double *adr; /* указатель на столбец */

};

COLUM Tabl [ L_tab } /*массив сведений о столбцах */

Для обращения к столбцам используются их индексы в массиве Tabl. Если столбец не размещен в памяти, то значение поля adr равно 0.

а) Составить функцию для выполнения поэлементного сложения двух столбцов с образованием столбца сумм. Входные данные - номера (индексы в Tabl ) столбцов слагаемых и столбца результата. Если столбец-результат не размещен в памяти, то разместить его и отметить зто в Tabl.

int sumcol( int k, int l, int r);

Функция возвращает 0 при успешном выполнении операции или 1, если один или оба столбца-слагаемых не размещены в памяти.

б) Составить функцию для вычисления столбца частных от деления элементов одного столбца на элементы другого столбца. Предусмотреть сигнализацию о случае, когда элемент столбца-делителя равен нулю. Остальные условия - как в варианте (а).

int subcol ( int k, int l, int r);

в) Составить функцию для вычисления элементов столбца относительных приростов в процентах di=(ai-bi)/ai*100 для заданного исходного столбца. Остальные условия - как в вариантах (а) и (б).

int relincr ( int k, int r);

Функция возвращает 1, если встретится делитель, равный 0, в остальных случаях возвращает 0.

г) Составить функцию для печати таблицы, содержащей столбцы, перечень которых задан списком (массивом ) индексов (номеров) столбцов в массиве Tabl.

int tabprint ( int *list, int n);

12. В некоторой программе аналитических преобразований выражения, определяющие формулы, хранятся в виде динамических структур - деревьев с описанием :

enum tpn = { FUNC, OPER, VAR, CNST}; /*типы узлов*/

struct node

{ enum tpn tp_node; /* тип узла */

int cop; /* тип операции или номер функции, константы, переменной */

node *al, *ar; /* указатели на подчиненные узлы */

} ;

а) Составить функцию для обхода дерева, заданного указателем на его корень, в порядке : левое поддерево, корень, правое поддерево. Выводить на экран (печать) тип узла и значение поля cop в узле.

void lkp ( node *root);

б) В условиях пункта (а) выполнить обход дерева в порядке : корень, левое поддерево, правое поддерево.

void klp ( node *root );

в) В условиях пункта (а) выполнить обход дерева в порядке : левое поддерево, правое поддерево, корень.

void lpk ( node *root);

г) Составить функцию для освобождения памяти, занятой некоторым поддеревом. Входной параметр - указатель на корень удаляемого поддерева.

void deltree ( node *root);

13. В некоторой программе сообщения об ошибках хранятся в файле из записей, каждая запись состоит из двух строк по 60 литер. Позиция записи в дисковом файле определяется кодом ошибки - целым числом.

а)Составить функцию для вывода сообщения об ошибке на экран. Входной параметр код (номер) ошибки. Файл с сообщениями об ошибках открывается в главной программе.

б)Составить программу для создания файла сообщений об ошибках. При использовании этой программы пользователь указывает имя файла, максимальное число сообщений об ошибках, а затем задает коды ошибок и соответствующие им сообщения в произвольном порядке.

14. Составить функцию для решения системы линейных уравнений

Препроцессорные средства в C и С++ 1<=i<=n

методом исключения неизвестных.

int gauss (int n, double *a, double *b);

Функция возвращает 0 при успешном выполнении и 1, если метод не работает.

15. В памяти хранится массив из n<Nmax вещественных чисел - значений реализации некоторой случайной величины.

а)Cоставить функцию для вычисления оценок математического ожидания и дисперсии случайной величины

void randval( int n, double *a, double *m, double *d);

a - исходный массив, m - вычисляемая оценка математического ожидания, d - оценка дисперсии.

б)Cоставить функцию для построения гистограммы (распределения частот) для реализации случайной величины. Входные данные: n, x -массив значений случайной величины x[1],...,x[n], k - число интервалов, на которые разбивается диапазон значений случайной величины.Результат функции r - массив из k чисел, значения частот попадания в соответствующий интервал.

16. Выполнить варианты 15 (а),(б) для случая,когда значения реализации случайной величины хранятся в текстовом файле, а остальные данные сообщаются пользователем, результаты расчетов выводятся на экран.

17. Значения некоторой функции представлены таблицей с постоянным шагом и в памяти хранятся: число точек таблицы n, массив значений функции y[1],...,y[n], начальное значение аргумента x[1] и шаг по аргументу.

а)Составить функцию для выборки значения функции из таблицы с линейной интерполяцией.

б)Составить функцию для вычисления значения обратной функции для функции,заданной таблицей c постоянным шагом.

18. Составить функцию для выборки значений функции, заданной таблицей с переменным шагом. Таблица хранится в памяти в форме массива из n строк и двух столбцов(аргумент и значение функции).

19. Составить функцию для сортировки (упорядочения) массива mas из n элементов по возрастанию значений элементов:

void sort (int n, int *mas);

20. Составить функцию для сортировки (перестановки строк) матрицы из n строк и m столбцов по возрастанию элементов k-го столбца;

void sortmas ( int n, int m, int *matr, int k);

21. Составить функцию для умножения матрицы matr из n строк и m столбцов на вектор vect (из n элементов) с размещением результата в массиве res:

void matrvect( int n, int m, float *matr, float *vect, float *res);

22. Составить функцию для вычисления произведения матриц a из n строк и m с толбцов и b из m строк и k столбцов с помещением результата в матрицу c:

void matrprod ( int n, int m, int k, float *a, float *b, float *c);

Приложение 2. Задачи по разработке систем объектов


1. Построить систему классов для описания плоских геометрических фигур: круг, квадрат. прямоугольник. Предусмотреть методы для создания объектов, перемещения на плоскости, изменения размеров и вращения на заданный угол.

2. Построить описание класса, содержащего информацию о почтовом адресе организации. Предусмотреть возможность раздельного изменения составных частей адреса. создания и уничтожения объектов этого класса.

3. Составить описание класса для представления комплексных чисел с возможностью задания вещественной и мнимой частей как числами типов double, так и целыми числами. Обеспечить выполнение операций сложения, вычитания и умножения комплексных чисел.

4. Составить описание класса для работы с цепными списками строк (строки произвольной длины) с операциями включения в список. удаление из списка элемента с заданным значением данного. удаления всего списка или конца списка. начиная с заданного элемента.

5. Составить описание класса для объектов - векторов, задаваемых координатами концов в трехмерном пространстве. Обеспечить операции сложения и вычитания векторов с получением нового вектора (суммы или разности), вычисления скалярного произведения двух векторов, длины вектора, cos угла между векторами.

6. Составить описание класса прямоугольников со сторонами, параллельными осям координат. Предусмотреть возможность перемещения прямоугольников на плоскости, изменение размеров, построение наименьшего прямоугольника, содержащего два заданных прямоугольника, и прямоугольника, являющегося общей частью (пересечением) двух прямоугольников.

7. Составить описание класса для определения одномерных массивов целых чисел (векторов). Предусмотреть возможность обращения к отдельному элементу массива с контролем выхода за пределы индексов, возможность задания произвольных границ индексов при создании объекта и выполнения операций поэлементного сложения и вычитания массивов с одинаковыми границами индексов, умножения и деления всех элементов массива на скаляр, печати (вывода на экран) элементов массива по индексам и всего массива.

8. Составить описание класса для определения одномерных массивов строк фиксированной длины. Предусмотреть возможность обращения к отдельным строкам массива по индексам, контроль выхода за пределы индексов, выполнения операций поэелементного сцепления двух массивов с образованием нового массива, слияния двух массивов с исключением повторяющихся элементов, печать (вывод на экран) элементов массива и всего массива.

9. Составить описание класса многочленов от одной переменной, задаваемых степенью многочлена и массивом коэффициентов. Предусмотреть методы для вычисления значения многочлена для заданного аргумента, операции сложения, вычитания и умножения многочленов с получением нового объекта - многочлена, печать (вывод на экран) описания многочлена.

10. Составить описание класса одномерных массивов строк, каждая строка задается длиной и указателем на выделенную для нее память. Предусмотреть возможность обращения к отдельным строкам массива по индексам, контроль выхода за пределы индексов, выполнения операций поэелементного сцепления двух массивов с образованием нового массива, слияния двух массивов с исключением повторяющихся элементов, печать (вывод на экран) элементов массива и всего массива.

11. Составить описание объектного типа TMatr, обеспечивающего размещение матрицы произвольного размера с возможностью изменения числа строк и столбцов, вывода на экран подматрицы любого размера и всей матрицы.

Литература


1. Керниган Б., Ритчи Д. Язык программирования Си. - М., “Радио и связь”, 1989.

2. Пол Ирэ. Объектно-ориентированное программирование с использованием С++: Пер. с англ. - Киев: НИПФ “ДиаСофт Лтд, 1995.

3. Цимбал А.А. и др. Turbo C++, язык и его применение. - М.: Джен Ай Лтд, 1993, - 512с.

4. Bjarne Stroustrup The C++ Programming language, Addison Weasley, 1986.

Рефетека ру refoteka@gmail.com