Информатика и технология программирования

         

Синтаксис указателя на функцию


Определение указателя на функцию имеет вид:


int (*pf)(); // без контроля параметров вызова


int (*pf)(int, char*); // с контролем по прототипу

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

В соответствии с принципом контекстного определения типа данных эту конструкцию следует понимать так: pf -переменная, при косвенном обращении к которой получается вызов функции, возвращающей int в качестве результата, то есть pf -адрес функции или указатель на функцию. Следует обратить внимание на то, что в определении указателя присутствует тип результата - указатель ссылается не на произвольную функцию, а только на одну из функций, дающих результат этого типа. Перед началом работы с указателем его необходимо назначить на соответствующий объект, в данном случае - на функцию. В синтаксисе Си выражение вида &#38имя_функции имеет смысл -начальный адрес функции или указатель на функцию. Кроме того, по аналогии с именем массива использование имени функции без скобок интерпретируется как указатель на эту функцию. Указатель может быть инициализирован и при определении. Таким образом, возможны следующие способы назначения указателей:


int INC( int а) { return a+1; }
extern int DEC(int);
int (*pf)(int);
pf = &#38INC;
pf = INC; // присваивание указателя


int (*pp)(int) = &#38DEC; // инициализация указателя

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


n = (*pf)(1) + (*pp)(n); // эквивалентно


n = INC(1) + DEC(n);

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




extern double sin(double);
extern double cos(double);
extern double tan(double);
char *names[] = { "sin","cos","tan",NULL};
double (*pf[])(double) = { sin, cos, tan};

Переменная pf представляет собой массив указателей на функции, инициализированный адресами библиотечных функций
sin, cos и tan .


double call(char *pn,int val)
// pn - имя вычисляемой функции

// val - значение аргумента



{
int i;
for (i=0; names[i]!=NULL; i++)
if (strcmp(names[i],pn) == 0)
{ // Имя найдено

return ((*pf[i])(val));
} // вызов функции по i-му

return(0.); // указателю в массиве pf

}

Система меню


Меню верхней части экрана. Вход по F10 или по Alt+"Z", где Z - первая (прописная) буква в соответствующем слове меню (например, Alt+F --&#62 строка меню File) .

.


---------------------------------------------------------------¬
¦File Edit Search Run Compile Debug Project Options Window Help¦
L---------------------------------------------------------------

Некоторые команды меню выполняются при нажатии функциональных клавиш (F1-F10) в сочетании с Shift, Alt или Ctrl, что указано в соответствующих строках меню.



Система объектов, управляемых сообщениями


Здесь мы рассмотрим очень приближенную модель того, что реально происходит при программировании в объектно-ориентированных библиотеках, основанных на обработке сообщений (или событий) объектами программы. (Например, OWL в Windows или Turbo Vision в DOS) . Система объектов, управляемых сообщениями, должна включать в себя несколько классов, взаимодействие между которыми скрыто от внешнего наблюдателя :



-класс сообщений ;



-базовый класс объектов, управляемых сообщениями ;



-класс прикладной программы, единственный объект которого является аналогом main и реализует в своем методе run диспетчер сообщений и объектов.

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


// Класс сообщений -----------------------------------


&#35define ms_NULL 0 // Пустое сообщение


&#35define ms_KEYB 1 // Символ клавиатуры


&#35define ms_TICK 2 // Тик таймера


&#35define ms_MS_MOVE 3 // Перемещение мыши


&#35define ms_MS_CLICK 4 // Кнопка мыши


&#35define ms_ECHO 5 // Ответ объекта с this


&#35define ms_EXIT 6 // Завершение программы


&#35define ms_KILL 7 // Уничтожение объекта


&#35define ms_BORN 8 // Порождение объекта


class MS
{
public:
char code; // Код сообщения


int x,y; // Параметры сообщения


void *p; // Указатель на данные


MS(char,int,int,void *); // Констуктор - создать сообщение


~MS();
void clear()
{ code=ms_NULL; }; // " Очистить" сообщение


};


MS::MS(char c,int xx,int yy,void *q)
{ code = c; x = xx; y = yy; p = q; }


MS::~MS() {}

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

zlist messages; // Сообщения программы

public:
PRG();
~PRG();
void SendEvent(char,int,int,void *);
void run();
};

PRG *OBJ::programm = NULL;





Конструктор связывает объект-программу с объектами класса OBJ путем установки в них статического указателя на самого себя. После этого все объекты могут передавать сообщения методом SendEvent , который просто ретранслируется в аналогичный метод SendEvent в классе PRG. Последний создает объект-сообщение и помещает указатель на него в
конец списка сообщений messages .

// Конструктор: связаться с классом OBJ

PRG::PRG()
{ OBJ::programm = this; }

//---- Прием и запись нового сообщения -------------------

void PRG::SendEvent(char code0,int x0,int y0, void *p0)
{ MS *pm;
pm = new MS(code0,x0,y0,p0);
messages((void *)pm); // Переопределенная операция x(void*) -

} // включить последним

Метод run представляет собой диспетчер сообщений, обеспечивая посредством их связь типа " каждый с каждым" . Это значит, что каждое сообщение пропускается через всю цепочку объектов, которые либо игнорируют его, либо обрабатывают. Обработка может закончится очисткой сообщения, тогда оно будет принято всего одним (первым) объектом, В противном случае сообщение будет широковещательным, то есть на него будут реагировать все объекты, которые настроены на его обработку.



// Диспетчер сообщений и объектов -------------------

void PRG::run()
{ MS *pm;
clock_t t;
t = clock();
while(1)
{
for (n=0; (pm = (MS *)messages.Remove(0) !=NULL; n++)
{ // Пока есть сообщения в очереди -

switch (pm-&#62code) // исключить первое

{ // и переключиться по коду

case ms_BORN: // Служебное сообщение от конструктора

objects(pm-&#62p); // объекта - включить в список объектов

break;
case ms_NULL:
break; // Пустое сообщение

case ms_EXIT:
return; // Сообщение о завершении работы

case ms_KILL: // Сообщение об уничтожении процесса -

void *q = pm-&#62p; // Посылается объектом, который хочет

// завершить работу

pp = (OBJ *)objects.Remove(q);
if (pp == NULL) break; // Исключить его из списка




delete pp; // Разрушить динамический объект -

break; // вызвать виртуальный деструктор

default: // Остальные сообщения передаются

int n=objects.size(); // всем объектам в списке

for (m=0; m&#60n; m++)
{ // Извлечь указатель на объект по номеру

OBJ *pp = (OBJ*)objects[m];
// Вызвать виртуальную функцию обработки

// сообщение объектом

pp-&#62HandleEvent(pm);
// Сообщение обработано данным объектом

if (pm-&#62code == ms_NULL) break;
}
}
delete pm; // Уничтожить сообщение

}
}
//----- Проверка источников сообщений ---------------------

if (kbhit()) // Сообщение от клавиатуры

{
pm = new MS(ms_KEYB,(int)getch(),0,NULL);
messages((void *)pm);
}
if ( clock()-t &#62 1) // Сообщение от таймера (часы)

{ t = clock();
pm = new MS(ms_TICK,0,0,NULL);
messages((void *)pm);
}
}}

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

Среди сообщений следует выделить служебные, касающиеся создания и уничтожения объектов. Когда в программе создается новый объект (а создается он в процессе обработки сообщения другим объектом), то его конструктор посылает сообщение о своем " рождении" , по которому указатель на этот объект включается в общий список. Уничтожение объекта происходит сложнее. Дело в том, что в процессе обработки сообщения объект не может уничтожить сам себя (то есть выполнить что-то вроде
delete this ). Хотя бы потому, что диспетчер продолжает процесс обработки сообщений. Вместо этого объект посылает служебное сообщение с просьбой " убить его" , которое отрабатывается диспетчером.


Диспетчер исключает динамический объект из списка и уничтожает его (как динамический объект).

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

Деструктор " вычищает" очереди сообщений и объектов, разрушая сами сообщения и объекты.

// Деструктор: уничтожение сообщений и объектов ----- --------

PRG::~PRG()
{
while ((MS *p=messages.Remove(0))!=NULL) delete p;
while ((OBJ *q=objects.Remove(0)) !=NULL) delete q;
}

Для создания прикладной программы, нужно определить производный класс от класса PRG , конструктор которого порождает те объекты, которые изначально присутствуют в программе. Естественно, что они должны быть из классов, управляемых сообщениями.



// Производный класс прикладной программы --------------------

class myPRG : public PRG
{
public:
myPRG() { MOUSE *pms = new MOUSE; }
};



Основная программа main состоит из двух строчек - создать единственный объект класса
myprog и вызвать для него метод run , поскольку все, что можно было, уже реализовано в предыдущих классах.

void main()
{
myPRG myprog;
myprog.run();
}

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

Класс " видимая точка" представляет собой символ, отображаемый на экране в заданной позиции. Данные этого объекта включают в себя координаты точки и код символа, а методы - перемещение объекта на одну позицию и отображение.


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



// Класс "Видимая точка" ---------------------------

class PT : public OBJ
{ char sym; // Отображаемый символ

int x,y; // Координаты точки

public:
PT(char); // Конструктор

~PT(); // Деструктор

void on(); // Высветить

void off(); // Погасить

void up(); // Перемещения точки

void down();
void left();
void right();
void OnPlace(int,int); // Позиционирование точки

void HandleEvent(MS *); // Обработка сообщений

};
//--------------------------------------------------------------

PT::PT(char c) { sym = c; x = 0; y = 0; }

void PT::off() { gotoxy(x+1,y+1); putch(' '); }

void PT::on() { gotoxy(x+1,y+1); putch(sym); }

void PT::OnPlace(int xx, int yy) {off(); x = xx; y = yy; on(); }

void PT::left() { off(); if (x &#62 0) x--; on(); }

void PT::right() { off(); if (x &#60 79) x++; on(); }

void PT::up() { off(); if (y &#62 0) y--; on(); }

void PT::down() { off(); if (y &#60 24) y++; on(); }

PT::~PT() {off(); }

void PT::HandleEvent(MS *pm) {}



Курсор - это " видимая точка" , которая управляется с клавиатуры, поэтому этот объект выделяет сообщения от клавиатуры с соответствующими кодами и вызывает для них методы перемещения из класса " видимой точки" .



// Класс " Курсор - управляемый с клавиатуры"

class MOUSE : public PT
{ public:
void HandleEvent(MS *);
MOUSE();
~MOUSE();
};
//--------------------------------------------------------------

MOUSE::MOUSE() : PT('&#35') // Конструктор - сконструировать

{ OnPlace(40,12); } // точку " &#35" и установить ее на 40,12

void MOUSE::HandleEvent(MS *pm)
{
switch (pm-&#62code)
{ // Реакция курсора на сообщения от клавиатуры

case ms_KEYB:
switch(pm-&#62x)
{
case '8': up(); pm-&#62clear(); break;
case '2': down(); pm-&#62clear(); break;
case '4': left(); pm-&#62clear(); break;
case '6': down(); pm-&#62clear(); break;
case '0': pm-&#62clear(); SendEvent(ms_KILL,0,0,(void *)(OBJ *)this); break;


case '5': pm-&#62clear(); SendEvent(ms_EXIT,0,0,NULL); break;
}
break;
}
}
MOUSE::~MOUSE() {}

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

// Класс "Движущиеся точки" -------------------------

class MOVING : public PT
{ int dx,dy;
public:
void oneStep();
void HandleEvent(MS *);
MOVING(int,int,int,int);
~MOVING();
};

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




Система помощи


.


---------------------------------------------------------------¬
¦File Edit Search Run Compile Debug Project Options Window Help¦
L----------------------------------------------------------¦----
-------------------+---¬
Тематическое содержание Help ----------- Contents F1 ¦
Перечень ключевых слов (индекс) -------- Index Shift+F1 ¦
Контекстный поиск по текущему слову ---- Topic Search Ctrl+F1 ¦
Возвращение к предыдущей теме ---------- Previous Topic Alt+F1 ¦
Информация о Help ---------------------- Help on help ¦
L-----------------------



Смысл индексных переменных при работе с массивами


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


while() {... c=A[i]; i++; ...} // Извлечь очередной и продвинуть индекс


while() {... c=A[i++]; ...} // " Нежесткая" связь с циклом


for (...;...;i++) { ... c=A[i]; ... } // " Жесткая" связь с циклом


while() {... A[i]=c; i++; ...} // Записать очередной и продвинуть индекс


while() {... A[i++]=c; ...} // " Нежесткая" связь с циклом


for (...;...;i++)
{ ... c=A[i]; ... } // " Жесткая" связь с циклом

Заметим, что процесс последовательного движения по массиву может быть " жестко" связан с выполнением цикла, тогда за каждый шаг цикла происходит строго одно перемещение и i++ может находиться в заголовке. Если же такой связи нет, тогда " движение" по массиву может быть косвенно связанным с самим циклом, через какие-либо условия. При этом оно может протекать со своей " скоростью" .

В качестве примера рассмотрим процесс разделения массива на две части относительно значения некоторого элемента - медианы. Пусть требуется переписать элементы массива A в массив B так, чтобы в выходном массиве слева относительно выбранного A[k] , называемого медианой, находились все элементы, меньшие данного, а справа - большие. Это можно сделать, если заполнять массив B с двух концов, перенося поочередно элементы из исходного массива. Для реализации такой программы потребуется три индекса и один цикл, поскольку имеет место три независимых " движения" по массивам, причем одно из них связано с циклом просмотра :



-" движение" по массиву A слева направо, связанное с циклом просмотра массива ;



-" движение" по массиву B в процессе заполнения его с левого конца ;



-" движение" по массиву B в процессе заполнения его с правого конца.


int A[20],B[20];
int i; // Движение по массиву A


int k1; // Движение по массиву B слева направо


int k2; // Движение по массиву B справа налево


int m; // Индекс элемента - медианы в А


for (i=0,k1=0,k2=19; i&#60 20; i++) // Просмотр A жестко связан с циклом


{
if (i==m) continue; // Медиану - пропустить


if (A[i]&#60A[m])
B[k1++]=A[i]; // Добавить слева


else
B[k2--]=A[i]; // Добавить справа


} // Движение по B не связано жестко с циклом


B[k1]=A[m]; // Перенести медиану в точку " встречи" k1-k2



Смысл операции присваивания


ПРИСВАИВАНИЕ -- это не только запоминание вычисленного значения. Переменная может использоваться также и как элемент логики алгоритма (к которой, как известно, относятся операторы) :



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



-два фрагмента из разных частей алгоритма могут быть связаны непосредственно - для этого используется оператор перехода goto. Другой способ - один из фрагментов формирует значение переменной, которой в дальнейшем пользуется второй фрагмент. Это - связь через переменную-результат.

.


Непосредственная связь Связь через переменную-результат
Фрагмент 1 Фрагмент 1
if (...) goto mmm; if (...) k=1; else k=0;
... ...
Фрагмент 2 Фрагмент 2
mmm: ... if (k==1) ...



Смысл переменных


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



- сначала формулируется результат, чего, собственно, мы хотим достигнуть ;



-затем определяется, какие для этого потребуются переменные, и какой " смысл" они будут иметь :



-в конце выбирается стандартный фрагмент алгоритма, который придает переменной этот смысл.

Перечислим наиболее простые варианты смысла переменных в программе:

l ПЕРЕМЕННАЯ - СЧЕТЧИК. Переменная считает количество появлений в программе того или иного события. В следующем примере переменная m подсчитывает количество положительных переменных в массиве. Заметим, что ключевой "фразой", определяющей смысл переменной-счетчика, является следующая :



for (...) { if (...удовлетворяет условию...) m++; }


for (i=0, m=0; i&#60n; i++) if(A[i]&#62 0) m++;

l ПЕРЕМЕННАЯ - ПРИЗНАК. Переменная фиксирует факт наступления какого-либо события в программе. Такая переменная принимает только логические значения 0 или 1 (событие наступило или не наступило). В одной точке программы проверяется это условие и устанавливается признак, в другой - наличие или отсутствие признака оказывает влияние на логику работы программы, в третьей - признак сбрасывается. Простой пример - суммирование элементов массива до первого отрицательного включительно:


for (s=0, k=0, i=0; i&#60n &#38&#38 k==0; i++)
{
s+=A[i];
if (A[i]&#60 0) k=1;
}

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


for (s=0, i=0; i&#60n; i++)
{
s+=A[i];
if (A[i]&#60 0) break;


}

Более сложный пример иллюстрирует тот факт, что переменная-признак " запоминает" факт наступления события и сохраняет его в течение некоторого времени.



for (i=0,s=0,k=0; i&#60 10; i++)
if (A[i]&#60 0) k=1;
else
{ if (k==1) s++; k=0; }

Несложно догадаться, что " смысл" переменной k - текущий элемент массива является отрицательным, а " смысл" s - счетчик. Счетчик увеличивается, если выполняется ветка
else - текущий элемент массива положителен, и в то же самое время k==1 - соответствует отрицательному значению элемента массива. Преодолеть это противоречие можно, если учесть, что признак проверяется в этой ветке до его изменения, то есть l проверяется его значение, оставшееся от предыдущего шага. Следовательно. фрагмент подсчитывает количество пар элементов вида " отрицательный - положительный" .



Еще один пример - обнаружение комментариев в строке. Признак com в процессе переписывания строки устанавливается в 1, если программа находится " внутри комментария" .

void copy(char dst[], char src[])
{ int i,com=0,j=0;
for (com=0,i=0; src[i]!=0; i++)
if (com)
{ // внутри комментария

if (src[i]== * &#38&#38 src[i+1]== / )
{ com=0; i++; } // не в комментарии, пропустить символ

}
else
{ // вне комментария

if (src[i]== / &#38&#38 src[i+1]== * )
{ com=1; i++; } // в комментарии, пропустить символ

else
dst[j++] = src[i]; // переписать символ в выходную строку

}
dst[j]=0; }

for (s=0,...;...;...) { получить k; s=s+k; }



Он дает переменной s единственный " смысл" - переменная накапливает сумму значений k , полученных на каждом из шагов выполнения цикла. Для доказательства этого факта можно
привлечь метод математической индукции : действительно, если на очередном шаге s содержит сумму, накопленную на предыдущих шагах, то после выполнения s=s+k она будет содержать сумму уже с учетом текущего шага. Типичный пример - сумма элементов массива.

for (s=0,i=0; i&#60 10; i++) s=s+A[i];

То же самое можно сказать и о произведении, которое накапливается следующим фрагментом :




for (s=1,...;...;...;) { Получить k; s=s*k; }

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

for (s=0,i=0; i&#60n; i++) // Сумма элементов массива

s+=A[i];

for (s=0,i=0; i&#60n &#38&#38 A[i]&#62=0; i++) // Сумма элементов массива до первого

s+=A[i]; // отрицательного

for (s=0,i=0; i&#60n; i++) // Сумма положительный элементов

if (A[i]&#62 0) s+=A[i]; // массива

for (s=0,x=0; x&#60=1; x+=0.1) // Сумма значений функции sin

s+=sin(x); // в диапазоне 0..1 с шагом 0.1

for (s=0,...;...;...) { получить k; if (k&#62s) s=k; }





Для доказательства этого факта можно привлечь метод математической индукции : действительно, если на очередном шаге s содержит максимальное значение, полученное на предыдущих шагах, то после выполнения if (k&#62s) s=k; она будет содержать такой же максимум, но уже с учетом текущего шага. Типичный пример - нахождение максимального элемента массива.

for (s=0,i=0; i&#60 10; i++) if (A[i]&#62s) s=A[i];

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

for (i=1,k=0; i&#60 10; i++) if (A[i]&#62A[k]) k=i;

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

for (i=0,k=-1; i&#60 10; i++) // k=-1 - нет элемента, принятого за минимальный

{ if (A[i]&#60 0) continue;
if (k==-1 || A[i]&#60A[k]) k=i;
}


Смысл переменных при завершении циклов


Наиболее простой и легкий способ достижения результата при написании программы, как можно чаще делить ее на части, каждую из которых можно программировать независимо. При том программа может оказаться не столь уж компактной, зато легко управляемой и понятной. Однако возникает вопрос - как части программы будут взаимодействовать между собой. Единственный ответ - через общие переменные. Тогда при разделении программы на части нужно сразу определить - какие переменные будут составлять интерфейс тех частей, то есть опять нагрузить их смыслом.

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


for (i=0; i&#60n; i++) if (A[i]&#60 0) break;
if (i==n) ...

В данном примере мы имеем два варианта завершения цикла. В первом случае, который зафиксирован в заголовке цикла, цикл завершается по достижении переменной i значения n . Альтернативный выход из цикла по break происходит при обнаружении отрицательного значения элемента массива. Естественно, если он происходит, то это будет раньше, чем естественное завершение. Тогда проверка (i==n) на самом деле имеет следующий "смысл": был или не был обнаружен в массиве отрицательный элемент. Если условие соблюдается, то не был. Если не соблюдается, то значение i содержит индекс первого встреченного отрицательного элемента. Таким образом, фрагмент цикла, содержащий break , проверяет выполнения свойства , обратного указанному в операторе if СВОЙСТВО " ВСЕОБЩНОСТИ" для всех элементов массива. Или, наоборот, проверяет СВОЙСТВО " СУЩЕСТВОВАНИЯ" элемента массива, для которого справедливо данное условие.



Смысл переменных в структурах данных


Как известно, программа использует переменные в процессе выполнения алгоритма для хранения результатов. Но это чисто внешняя характеристика переменных. В процессе работы с группой переменных программа связывает их единым смыслом - так появляется структура данных. Заметим, что понятие структуры данных тесно связано как со способом хранения данных так и со способами работы с ними. В качестве примера рассмотрим такую широко используемую структуру данных как l последовательность.

l ПОСЛЕДОВАТЕЛЬНОСТЬЮ называется упорядоченное множество переменных, количество которых может меняться. В идеальном случае последовательность может быть неограниченной, реально же в программе имеются те или иные ограничения на ее длину. Рассмотрим самый простой способ представления последовательности. Если переменные принимают целые положительные значения, то их последовательность может быть размещена в обычном массиве. Для того, чтобы определить текущее количество элементов последовательности можно поступить двумя способами :



-использовать дополнительную переменную - счетчик числа элементов ;



-добавлять каждый раз в качестве обозначения конца последовательности дополнительный элемент с особым значением - признак конца последовательности, например нулевой (ОГРАНИЧИТЕЛЬ ПОСЛЕДОВАТЕЛЬНОСТИ).

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


// создать пустую последовательность


A[0]=0;


// найти
конец последовательности


for(n=0; A[n]!=0; n++);


// добавить в конец последовательности


for(n=0; A[n]!=0; n++);
A[n]=c; A[n+1]=0;


// включить в последовательность под заданным номером n


for (i=0; A[i]!=0; i++);
for (; i&#62=n; i--) A[i+1]=A[i];
A[n]=c;


// удалить из последовательности под заданным номером n



for (i=0; A[i]!=0; i++);
if (n&#60=i) { for(; A[i]!=0; i++) A[i]=A[i+1]; }

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

int A[20]; int i=0;
while (i&#60 19; i++)
{ ... получить " c" ...
if (...нет очередного " с" ...) break;
A[i++]=c; // добавить " с" к последовательности

}
A[i]=0; // добавить ограничитель последовательности



В приведенном фрагменте ключевым является
выражение A[i++]=c; (или A[i]=c; i++ ) . В нем переменная i имеет смысл - текущий элемент последовательности, а массив A[] - сама последовательность. Само выражение понимается как - добавить " с" в последовательность. В данном примере добавление очередного элемента связано с шагом цикла, в котором это добавление производится. Однако такое же действие - добавление в последовательность, может быть никоим образом не связано с текущей логикой программы, оно может производиться при любых условиях, когда в этом возникает необходимость

for (k=0; ...for(j=0 ...if...
if (...есть очередное " с" ...)
A[i++]=c; // добавить " с" к последовательности

A[i]=0; // добавить ограничитель последовательности


Смысл выражений в арифметических задачах


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


n % 10... // младшая цифра числа n


n=n/10; // отбросить младшую цифру n


for (i=0; n!=0; i++, n/=10)
{ ... n%10... } // получить цифры числа в обратном порядке


n%i==0... // n делится нацело на i


s = s*10 + k; // добавить цифру k к значению числа s



Сортировка и поиск


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



Сортировка подсчетом


l Идея алгоритма : если для элемента массива A[i] подсчитано количество элементов, меньших его, например, cnt то это значение будет являться местом размещения в выходном массиве. Действительно, cnt элементов будут лежать слева.

l Фактыl :l

1. Потребуется два массива, входной A[ ] и выходной B[ ] , соответственно - формальные параметры функции, которая выполняет сортировку, а также их размерность - n .


void sort(int A[], int B[], int n)
{...}

2. Действие, связанное с подсчетом и перенесением элемента массива будет производиться для каждого из элементов массива A[ ], можно в любом порядке :


for (int i=0; i&#60n; i++)
{ подсчитать cnt для A[i] и перенести его в B[] }

3. Если для переменной cnt известен смысл - счетчик элементов, меньших текущего, то процесс перенесения значения из A[ ] в B[ ] можно записать, даже не зная способа его получения:


B[cnt]=A[i];

4. Для переменной-счетчика используется известный контекст :


for(cnt=0;...) { if(...) cnt++; }

5. Для подсчета количества значений, меньших A[i] нужно просмотреть в цикле все значения массива. Отдельный цикл требует нового индекса :


for (cnt=0,j=0; j&#60n; j++) if (A[j] &#60 A[i]) cnt++;

6. Уточнение. Если в массиве есть два одинаковых значения, то они попадут в одну и ту же ячейку выходного, поскольку счетчики их будут одинаковы. Следующая же ячейка окажется пуста. Это можно исправить, если, например, подсчитывать также и равные значения, но расположенные справа от текущего. Смысл фразы " справа от i- го" выражается соотношением j&#62i. Тогда фрагмент подсчета примет вид :


for (cnt=0,j=0; j&#60n; j++)
if (A[j] &#60 A[i] || A[j]==A[i] &#38&#38 j&#62i) cnt++;

Собранная из фрагментов программа будет иметь вид (не забывайте определения переменных) .


void sort(int A[], int B[], int n) // 1


{
for (int i=0; i&#60n; i++) // 2


{
for (cnt=0,j=0; j&#60n; j++) // 4,5,6


if (A[j] &#60 A[i] || A[j]==A[i] &#38&#38 j&#62i) cnt++;
B[cnt]=A[i]; // 3


}
}


Путем сравнения всех пар элементов массива для каждого из них подсчитывается количество элементов, меньших его. Это дает новое местоположение этого элемента в выходном массиве. Трудоемкость алгоритма -n*n/2. Текст ищите в " Вопросах без ответов " .



Сортировка слиянием


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

ОДНОКРАТНОЕ СЛИЯНИЕ. Очевидно, что если из упорядоченных последовательностей брать по одному очередному элементу из каждой, затем выбирать из них минимальный и переносить его, то выходная последовательность будет упорядоченной. Отсюда возможен самый простой способ однократного слияния : массив разбивается на n частей, каждая из них сортируется независимо, а затем отсортированные части объединяются слиянием. Текст ищите в " Вопросах без ответов " .

ЦИКЛИЧЕСКОЕ СЛИЯНИЕ. Первоначально массив разделяется на n последовательностей. Затем в каждой из них выбирается по одному элементу (первому) в таком порядке, чтобы получилась упорядоченная группа из n элементов, которая запоминается в выходной последовательности (слияние). Выходная последовательность будет состоять из групп по n элементов, каждая из которых упорядочена. Далее файл опять распределяется, но уже группами по n элементов по тем же самым n входным последовательностям. В результате слияния образуются упорядоченные группы из n*n элементов. Затем процесс повторяется группами по n*n*n и т.д.. В качестве примера рассмотрим случай с n=2 (двухпутевое слияние). Для простоты будем считать размерность сортируемого массива кратной 2.


void sort(int in[],int n, int v1[], int v2[])


{
int step,m,k0;
for (m=n; m!=1; m/=2) // Проверка на степень 2

if (m%2 !=0) return;
for (step=1 step &#60n; step *=2) // Слияние группами по

{ // step = 1,2,4...n/2

for (k0=0; k0 &#60 n/2; k0 += step)
// Распределить in[], начиная с 2*k0, группy по step

// элементов в v1 и в v2, начиная с k0

for (k0=0; k0 &#60 n; k0 += step*2)
// "Слить" по step элементов из v1 и v2, начиная с k0,

// в in[], начиная с 2*k0

}
}

Часть алгоритма, которая распределяет, начиная с 2*k0 из массива in группу из step элементов в массив v1 и такую же группу в v2, в комментариях не нуждается:

int i;
for (i=0, i&#60step; i++) v1[k0 + i] = in[2*k0 + i];
for (i=0, i&#60step; i++) v2[k0 + i] = in[2*k0 + step + i];

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

int i1,i2;
for (i1=i2=0; i1+i2 &#60 2*step; )
{
if (i1==step)
{ in[2*k0+i1+i2] = v2[k0+i2]; i2++; }
else
if (i2==step)
{ in[2*k0+i1+i2] = v1[k0+i1]; i1++; }
else
if (v1[k0+i1] &#60 v2[k0+i2])
{ in[2*k0+i1+i2] = v1[k0+i1]; i1++; }
else
{ in[2*k0+i1+i2] = v2[k0+i2]; i2++; }
}



Некоторая сложность алгоритма простого слияния состоит в слежении за группами, в результате чего получаются такие " дикие " индексы массивов. ПОРАЗРЯДНАЯ РАСПРЕДЕЛЯЮЩАЯ СОРТИРОВКА работает с двумя последовательностями целиком, не выделяя в нем групп. Слияние осуществляется как обычно, путем выбора минимального из двух текущих элементов последовательностей. Зато распределение происходит на первом шаге по значению старшего значащего бита (0/1), и по значению каждого следующего бита на очередном шаге. Алгоритм при этом упрощается настолько, что приведен в "Вопросах без ответов...".Распределение можно вести одновременно для нескольких соседних битов, при этом количество выходных массивов будет равно соответствующей степени 2.


Сортировка выбором


В массиве выбирается меньший элемент из 0..n и обменивается с нулевым. Затем то же самое повторяется с элементами от 1 до n-1 и т.д.. Трудоемкость алгоритма - n*n/2. Сортировка выбором естественно выглядит при работе со списками. Текст ищите в " Вопросах без ответов " .



Сортировки разделением


Сортировки, основанные на принципе разделения массива разделяют его на две части относительно некоторого значения, называемого МЕДИАНОЙ. Сами части не упорядочены, но обладают таким свойством, что элементы в левой части меньше медианы, а элементы правой - больше. Благодаря такому свойству эти части можно сортировать независимо. Для этого нужно вызвать ту же самую функцию сортировки, но уже не по отношению к массиву, а к его частям. Функции, вызывающие сами себя, называются рекурсивными и рассмотрены в 5.4. Рекурсивный вызов продолжается до тех пор, пока очередная часть массива не станет содержать единственный элемент:


void sort(int in[], int a, int b)
{
int i;
if (a==b) return;
// Разделить массив в интервале a..b


// на две части a..i-1 и i+1..b


// A[i] - медиана интервала


sort(in,a,i-1); sort(in,i+1,b);
}

В так называемой БЫСТРОЙ СОРТИРОВКЕ разделение производится с использованием оригинального алгоритма. Специфика алгоритма " быстрой сортировки" состоит в выполнении разделения на основе обмена. Сравнение элементов производится с концов массива (i=a, j=b) к середине (i++ или j--), причем "укорочение" происходит только с одной из сторон. После каждой перестановки меняется тот конец, с которого выполняется "укорочение". В результате этого массив разделяется на две части относительно значения первого элемента in[a] , который и становится медианой.


//------------------------------------------------------bk35-04.cpp


//-------"Быстрая" сортировка


void sort(int in[], int a, int b)
{
int i,j,mode;
if (a&#62=b) return;
for (i=a, j=b, mode=1; i &#60 j; mode &#62 0 ? j-- : i++)
if (in[i] &#62 in[j])
{
int c;
c = in[i]; in[i] = in[j]; in[j]=c;
mode = -mode;
}
sort(in,a,i-1); sort(in,i+1,b);
}

Очевидно, что медиана делит массив на две неравные части. Вышеуказанный алгоритм разделения можно выполнить итерационно, применяя его к той части массива, которая содержит его середину (по аналогии с двоичным поиском). Тогда в каждом шаге итерации медиана будет сдвигаться к середине массива. Другой пример обменной сортировки на основе разделения -ПОРАЗРЯДНАЯ СОРТИРОВКА - будет рассмотрен в 4.8.



Сортировки вставками


ПРОСТАЯ ВСТАВКА: определяется место в отсортированной части массива, куда должен помещаться элемент. Элементы от данной позиции сдвигаются вверх и на освободившееся место помещается элемент. Трудоемкость алгоритма при линейном поиске -n*n/4. Если используется двоичный поиск для определения места элемента, то трудоемкость алгоритма составляет n*log(n). Однако эта трудоемкость не учитывает затрат на перемещение элементов при сдвиге. Естественным образом простые вставки выполняются в линейных списках. Текст ищите в " Вопросах без ответов " .

ВСТАВКА ПОГРУЖЕНИЕМ: очередной элемент путем ряда обменов " погружается " до требуемой позиции в уже упорядоченную часть массива. Текст ищите в " Вопросах без ответов " .

Существенными в сортировках вставками являются затраты на обмены или сдвиги элементов. Для их уменьшения желательно сначала производить погружение с большим шагом, сразу определяя элемент "по месту". Этим отличается СОРТИРОВКА ШЕЛЛА: исходный массив разбивается на n частей, в каждую из которых попадают элементы с шагом m, начиная от 0,1,...,m-1 соответственно, то есть


.
0 , m , 2m , 3m ,...
1 , m+1, 2m+1, 3m+1,...
2 , m+2, 2m+2, 3m+2,...

Каждая часть сортируется отдельно с использованием алгоритма вставок. Затем выбирается меньший шаг, и алгоритм повторяется. Шаг может быть выбран равным степеням 2, например 64,32,16,8,4,2,1. Последняя сортировка выполняется с шагом 1.



Составные части языка программирования


Любой язык программирования содержит средства для представления перечисленных выше компонент. Рассмотрим вкратце каждую из них.

l Типы данных и переменные . Типы данных - это те формы представления данных, которые могут существовать и обрабатываться в языке программирования. Естественно, прежде всего следует упомянуть типы данных, которые совпадают с формами представления информации в любом процессоре (то есть представлены в архитектуре компьютера ). Это целые, вещественные числа, символы (текст). Кроме того, в любом языке программирования есть формы представления данных, ориентированные на применение языка, например строки, базы данных, графические объекты. Все формы представления данных, заложенные в язык " от рождения" , называются базовыми типами данных. Кроме того, программист может конструировать свои формы представления, состоящие из уже известных элементов, - производные типы данных. Самым известным производным типом данных является массив. На основе типов данных в программе определяются переменные, каждой из которых отводится память и ставится в соответствие форма представления данных.

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



-непосредственно операции преобразования данных ;



-операции ввода и вывода данных для внешнего представления ;



-вызов (выполнение) модулей (процедур, функций) ;



-присваивание, то есть запоминание значения переменной ;



-выделение более простых типов данных из переменных производного типа.







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


-последовательность действий (блок) ;


-условная конструкция (ветсвление) ;


-повторяющаяся конструкция (цикл) ;


-переход.

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





l Модульная структура программы (
процедуры, функции). Принцип модульности предполагает, что любая достаточно сложная система может быть представлена в виде группы автономных частей, взаимодействующих друг с другом по определенным правилам (интерфейсам). Тот же самый принцип, примененный к иерархической организации, предполагает, что любая система. Которая на верхнем уровне рассматривается как единое целое, при переходе на нижний уровень рассмотрения становится сколь угодно сложной и может включать в себя системы нижних уровней. То же самое касается больших программ. Модуль - это логически завершенная независимая часть алгоритма с собственным программным интерфейсом входных и выходных данных. Заметим, что принцип модульности затрагивает также и данные, хотя и в меньшей мере (Это касается базовых и производных типов данных, а также глобальных и локальных переменных).


Состояние потока


Состояние потока характеризуется элементом данных state, для которого определены флаги состояния и функции управления состоянием.


enum ios::io_state
{
goodbit = 0x00, // Ошибок нет


eofbit = 0x01, // Обнаружен признак конца файла


failbit = 0x02, // Ошибка форматирования или


// преобразования данных


badbit = 0x04, // Серьезная ошибка (буферизация,


// чтение после конца файла и т.д.)


hardfail = 0x08 // Аппаратная ошибка


};

Функции управления состоянием в класса ios :


int rdstate(); // Чтение текущего состояния


int eof(); // Проверка флага eof


int fail(); // Проверка badbit | failbit | hardfail


int bad(); // Проверка badbit | hardfail


int good(); // Проверка на отсутствие ошибок


int clear(int=0); // Установка флагов ошибки, по


// умолчанию - очистка всех


operator void*(); // Преобразование к типу void*,


// возвращает NULL, если fail()==1


int operator!(); // Возвращает 1, если fail()==1

Последние переопределения позволяют проверять наличие ошибок в потоках в виде :


if (cout) ... или if (!cout) ...



Состояние процесса


Если считать, что процесс может выполняться с собственной скоростью, независимо (асинхронно) от аналогичных процессов, то в каждый момент его выполнения может потребоваться " приостановить" или вообще " затормозить" его протекание. Для этих целей вводится понятие - СОСТОЯНИЕ ПРОЦЕССА.

Термин " состояние процесса" имеет непосредственное отношение к понятию ПРОЗРАЧНОСТЬ. Так, если некоторые действия включаются в выполнение процесса и не оказывают влияние на результаты его работы, то они называются ПРОЗРАЧНЫМИ. Очевидно, что любое действие может быть прозрачным, если перед его выполнением состояние процесса сохраняется, а после выполнения - восстанавливается.



Создание алфавитного и тематического каталогов


После выполнения 1-го и 2-го проходов форматирования база данных содержит информацию о всех терминах. Отдельная операция ЭКСПОРТ БД создает HTML-страницы алфавитного указателя по одной странице на каждую букву и общий файл алфавитного указателя _termins.htm. Процедуры создания тематического каталога находятся в стадии разработки.



Создание и уничтожение объектовКонструкторы и деструкторы


Процесс создания и уничтожения объектов класса обычно сопровождается некоторыми действиями (инициализация данных, резервирование памяти, ресурсов и т.д.), которые производятся функциями-элементами специального вида. Элементы-функции, неявно вызываемые при создании и уничтожении объектов класса называются КОНСТРУКТОРАМИ и ДЕСТРУКТОРАМИ . Они определяются как элементы-функции с именами, совпадающими с именем класса. Конструкторов для данного класса может быть сколь угодно много, если они отличаются формальными параметрами, деструктор же всегда один и имеет имя, предваренное символом "~".

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

Момент вызова конструктора и деструктора определяется временем создания и уничтожения объектов:



-для статических и внешних объектов -конструктор вызывается перед входом в main , деструктор -после выхода из main() . Конструкторы вызываются в порядке определения объектов, деструкторы -в обратном порядке;



-для автоматических объектов -конструктор вызывается при входе в функцию (блок), деструктор -при выходе из него;



-для динамических объектов -конструктор вызывается при выполнении оператора new , деструктор -при выполнении оператора delete .

В Си++ возможно определение массива объектов класса. При этом конструктор и деструктор вызываются для каждого элемента массива и не должны иметь параметров. При выполнении оператора delete кроме указателя на массив объектов необходимо также указывать его размерность.


//------------------------------------------------------bk73-01.cpp


&#35include &#60io stream.h&#62
&#35include &#60alloc.h&#62
&#35include &#60string.h&#62


&#35include &#60dos.h&#62
static int days[] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };

class dat
{
int day,month,year;
public:
dat(int=0,int=0,int=0); // Конструктор с параметрами

// (возможно умолчание)

dat(char *); // Конструктор с одним параметром

~dat(); // Деструктор

};
//------- Конструктор с параметром - текстовая строка -----

dat::dat(char *s)
{
int i;
char ss[80];
strcpy(ss,s);
for (i=0; ss[i] !=0; i++)
if (ss[i]=='-') ss[i]=','; // Замена '-' на ','

sscanf(ss,"%d%d%d",&#38day,&#38month,&#38year);
}
//------ Конструктор с тремя параметрами ------------------

// (по умолчанию 0 - текущая дата)

dat::dat(int d=0, int m=0, int y=0)
{
struct date x;
getdate(&#38x); // Стандартная функция получения текущей даты

// Проверка на значение по умолчанию

year = (y == 0) ? x.da_year : y;
month= (m == 0) ? x.da_mon : m;
day = (d == 0) ? x.da_day : d;
}
//------ Деструктор --------------------------------------

dat::~dat()
{
cout &#62&#62 "Дата ==&#62" &#62&#62 day &#62&#62 " -" &#62&#62 month &#62&#62 " -" &#62&#62 year;
}
//----------------------------------------------------------

dat a("12-12-1990"); // Внешний объект - конструктор

// вызывается перед main()

dat b[10]; // Массив объектов - конструктор без

// параметров вызывается перед main()

void xxx(dat &#38p)
{
dat c(12,12); // Вызывается конструктор dat(int,int,int)

// для автоматического объекта

dat d = p; // Конструктор для автоматического объекта не

// вызывается, т.к. объект инициализируется

} // копированием

// При выходе из функции вызываются

void main() // деструкторы для объектов c и d

{ int i,n;
cin &#60&#60 n;
dat *p = new dat[n]; // Создание массива динамических объектов -

// конструктор без параметров явно вызывается

// n раз

delete [n] p; // Уничтожение массива динамических объектов -

// деструктор явно вызывается n раз

} // Деструкторы для a и b[10] вызываются после

// выхода из main()


Списки как динамические структуры данных


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



-односвязные -каждый элемент списка имеет указатель на следующий;



-двусвязные -каждая элемент списка имеет указатель на следующий и на предыдущий элементы;



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







Сразу же необходимо отметить основное свойство списков как структур данных. Последовательность обхода списка, естественно, зависит не от физического размещения элементов списка в памяти, а от последовательности из связывания указателями. Точно так же определяется нумерация элементов списка - ЛОГИЧЕСКИЙ НОМЕР элемента в списке - это номер, получаемывй им в процессе движения по списку.

Односвязные списки являются наиболее простыми. Основным их недостатком является возможность просмотра только в одном направлении -от начала к концу. Без дополнительных "ухищрений" нельзя получить указатель на предыдущий элемент списка, который необходим при удалении текущего. Для односвязного списка наиболее простыми являются операции включения и исключения элементов в начале и конце списка, соответственно они используются для моделирования таких структур данных, как стеки и очереди.

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

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

Рассмотрим преимущества и недостатки списков в сравнении с обычными массивами и массивами указателей. Все эти структуры данных предполагают различные способы организации множества переменных. При этом обычный массив и список имеют противоположные свойства: если массив допускает произвольный доступ к своим элементам и ускоренный (двоичный) поиск при их упорядоченности, то список допускает только последовательный просмотр. Следовательно, массив обладает преимуществами с точки зрения операций поиска. Но, с другой стороны, изменение порядка следования переменных в массиве сопровождается их физическим перемещением, а в списке приводит только к "переброске" соответствующих указателей. Таким образом, списки предпочтительнее для структур данных, в которых операции поиска встречаются значительно реже, чем операции по изменению порядка следования элементов, и наоборот. В отношении массивов указателей и списков можно заметить, что массивы указателей критичны к количеству элементов в структуре данных -при увеличении этого количества их необходимо расширять, при уменьшении -желательно сокращать. Для списков такой проблемы не существует. Таким образом, списки предпочтительнее в структурах, где количество элементов меняется в широких пределах, массивы указателей -в противном случае.


Способ передачи параметров


В Си принят единый способ передачи параметров, который называется ПЕРЕДАЧЕЙ ПО ЗНАЧЕНИЮ. Выглядит он так:



-формальные параметры являются собственными переменными функции;



-при вызове функции происходит присваивание значений фактических параметров формальным (копирование первых во вторые);



-при изменении формальных параметров значения соответствующих им фактических параметров не меняются.

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

.


int f(int n1,int n2) // Эквивалент


int n1,n2 ,rez_f;
void f()
{ {
int i,j; int i,j;
... ...
n1 ++; n1 ++;
n2 = n1; n2 = n1;
return n2; rez_f = n2;
... ...
} }
int m=2; int m=2;
void main() void main()
{ {
int z; int z;
z = f( m,3); n1 = m; n2 = 3; f(); z = rez_f;
} }

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


int sum(int s[], int n) // Сумма элементов массива


int z; // Размерность передается


{ int i; // отдельным параметром


for (i=0,z=0; i&#60n; i++) z += s[i];
return(z); }
int c[10] = {1,6,4,7,3,56,43,7,55,33};
void main()
{ int nn; nn = sum(c,10); }

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



Способы анализа программы


Назначение любой программы - давать определенный результат для любых входных значений. Результат же - это набор значений, удовлетворяющих некоторым условиям, или обладающий некоторыми свойствами. Если посмотреть на программу с этой точки зрения, то она имеет много общего с математической теоремой. Действительно, теорема утверждает, что некоторое свойство имеет место на множестве элементов (например, теорема Пифагора устанавливает соотношение для гипотенузы и катетов всех прямоугольных треугольников). Программа обладает тем же самым свойством : для различных вариантов входных данных она дает результат, удовлетворяющий определенным условиям. Поэтому анализ программы - это не что иное, как формулировка теоремы о том, какой результат она дает.

переменных.

Убедиться, что теорема верна, можно различными способами. (Обратите внимание - убедиться, но не доказать ). Точно так же можно убедиться, что программа дает тот или иной результат :



- выполнить программу в компьютере или проследить ее выполнение на конкретных входных данных " на бумаге" (анализ методом единичных проб ) ;



-разбить программу на фрагменты с известным " смыслом" и попробовать соединить результаты их выполнения в единое целое (анализ на уровне неформальной логики и " здравого смысла " ) ;



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

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

Смысл фрагментов алгоритмов может быть разным :



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


-если циклический процесс завершается в зависимости от выполнения или невыполнения условия на множестве проверяемых элементов, то такой цикл проверяет свойства всеобщности или существования на этом множестве - в этом заключается его смысл ;


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


-каждая структура данных имеет стандартный набор выражений, имеющий смысл только применительно к ней ;


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


Способы формирования списков


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

Вариант 1. Элементы -обычные переменные, связи инициализируются транслятором, вся структура данных "зашита" в программный код:


struct list
{
list *next;
int val;
} a={0,NULL}, b={1,&#38a}, c={2,&#38b}, *ph = &#38c;

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


struct list A[100],*ph; // Создать список из


for (i=0; i&#60 99; i++) // элементов, размещенных


{ // в статическом массиве


A[i].next = A+i+1;
A[i].val = i;
}
A[99].next = NULL;
ph = A;

Вариант 3. Элементы списка являются динамическими переменными, связи между ними устанавливаются программно:


list *InsertFirst(list *ph, int n)
// Включить в начало списка


// ph - заголовок списка


// n - Значение элемента списка


//


{ list *pnew;
pnew = new list; // Создать элемент списка -


pnew-&#62val = n; // динамическую переменную,


pnew-&#62next = *ph; // заполнить его и


ph = pnew; // поместить в начало списка


return ph; // вернуть новый заголовок


}



Средства обработки прерываний в Си


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



- обработчик прерывания должен представлять собой функцию ;



- вызов этой функции должен осуществляться асинхронно, то есть ее имя (или указатель на нее) должно быть связано с вектором прерывания процессора ;



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



- функция должна иметь доступ как к глобальным (внешним), так и к локальным (автоматическим) переменным .

Для обработки прерываний в Си предусмотрен специальный вид функций с ключевым словом interrupt следующего вида (здесь и далее синтаксис приводится для Си++ - расширений файлов исходного текста вида . cpp ):


void interrupt INTR(bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,flgs) { } // или


void interrupt INTR( ...) { }

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



- перед началом программного кода функции генерируются команды сохранения в стеке всех регистров процессора - ax, bx, cx, dx, es, ds, si, di, bp, кроме сохраняемых аппаратно - регистра флагов, cs,ip и регистров указателя стека - ss,sp, которые остаются без изменения (переключения стека не происходит);



- ds устанавливается на сегмент данных Си-программы. Этим обеспечивается возможность работы с внешними переменными;



- в bp копируется текущее значение sp - этим обеспечивается возможность работы с автоматическими переменными;



- перед выходом из функции генерируются команды восстановления регистров из стека;



- выход из функции производится командой выхода из прерывания -
RETI.

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

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

Кроме того, можно использовать указатель на функцию обработки прерывания вида
void interrupt(*FUN)( ... ).

Поскольку прерывание предполагает вызов функции по длинному адресу (со сменой сегмента), такой указатель является длинным ( far ). Можно сказать, что подобный указатель является вектором прерывания.

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

Принцип обслуживания прерываний в DOS предполагает, что прикладная программа предварительно запоминает старый вектор прерывания и устанавливает собственный вектор прерывания - адрес собственной функции обработки. В ходе обслуживания прерывания программа должна вызвать обработчик прерывания по старому вектору - командой безусловного перехода или эмуляцией прерывания. Это приводит к выполнению цепочки обработчиков прерывания, перехватывающих вектор прерывания друг у друга. Для сохранения и установки вектора прерывания служат функции
getvect(), setvect(). В следующем примере рассматривается стандартный способ подключения собственного обработчика прерывания к вектору прерывания от таймера в начало цепочки.

&#35include &#60dos.h&#62
&#35define TIMVEC 0x8 // Номер вектора аппаратного

// прерывания от таймера

void interrupt (*TIMOLD)(...); // Старый вектор прерывания

void interrupt TIMINT(...) // Функция обработки прерывания

{
(*TIMOLD)(); // Эмуляция прерывания по

// старому вектору

// Собственная обработка




} // прерывания

void main()
{
TIMOLD = getvect(TIMVEC); // Сохранение старого вектора

setvect(TIMVEC,TIMINT); // Установка нового вектора

// на функцию TIMINT

setvect(TIMVEC, TIMOLD); // Восстановление старого вектора

}

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

(*TIMOLD)(); // Эмуляция прерывания по старому вектору

// Сохранение флагов в стеке и вызов функции

// по указателю


Ссылка (неявный указатель)


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



-синтаксис определения ссылки имеет вид синтаксиса определения указателя, в котором операция "*" заменена на "&#38";



-в процессе работы к ссылке обращаются как к обычной, с точки зрения синтаксиса, переменной, без использования операции косвенного обращения по указателю;



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



-при выполнении действий по чтению или записи значения ссылки транслятором выполняется косвенное обращение по соответствующему неявному указателю;



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



-размерностью ссылки является размерность указуемого типа данных, а не указателя.

.


struct dat { int day,month, year };
//--------------------------------------------------------


// Си++Эквивалент в "классическом" Си


//--------------------------------------------------------


//--------------- Инициализация константой --------------


int &#38a = 5; int aа = 5, *pa = &#38аa;
//--------------- Инициализация переменной --------------


int x; int x,*pa;
int &#38a = x; pa = &#38x;
a = 5; *pa = 5;
//-------------- Ссылка на структуру ---------------------


dat x; dat x;
dat &#38b = x; dat *pb = &#38x;
dat &#38c = {12,12,1990}; dat cc = {12,12,1990};
dat *pc = &#38cc;
b.year = 1990; pb-&#62year= 1990;
c.day=b.day+3; pc-&#62day = pb-&#62day+3;
// копирование структуры


c = b; *pc = *pb;
...sizeof(b)... ...sizeof(x)...
...sizeof(*pb)... // но не sizeof(pb)

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



ССЫЛКА -- псевдо-переменная, которая отображается на переменную или константу, которой она была инициализирована

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

Для формального параметра -ссылки:


-формальному параметру соответствует неявный указатель;


-формальный параметр в теле функции и фактический параметр в вызове имеют синтаксис обычной переменной;


-формальному параметру в теле функции соответствует косвенное обращение по неявному указателю;


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

.

void INC(int &#38a) void INC(int *pa)
//----------------------------------------------------------

// Си++ Эквивалент в "классическом Си"

//----------------------------------------------------------

{ {
a++; (*pa)++;
} }
... ...
int b = 5; int b = 5;
INC(b); INC(&#38b);

При изменении значения формального параметра - ссылки происходит аналогичное изменение фактического параметра. Такой
способ передачи параметров в языках программирования так и называется -ПЕРЕДАЧА ПАРАМЕТРА ПО ССЫЛКЕ.

Несколько сложнее обстоит дело со ссылкой -результатом функции:


-результат функции в операторе
return и результат функции в выражении имеют синтаксис (тип) переменной;


-в качестве результата возвращается неявный указатель, назначенный на переменную (адресное выражение), стоящее в операторе return;


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




- если же используется сам результат (например, сохраняется путем присваивания), то производится косвенное обращение по неявному указателю.

.

int &#38COPY(int *pa) int *COPY(int *pa)
//---------------------------------------------------------

// Си++ Эквивалент в "классическом Си"

//---------------------------------------------------------

{ {
return(*pa); return(pa);
} }
... ...
int a,b = 5,*pc; int a,b = 5,*pc;
//

//-- Косвенное обращение по неявному указателю - результату

a = COPY(&#38b); a = *COPY(&#38b);
//

//-- Передача неявного указателя без изменения ------------

pc = &#38COPY(&#38b); pc = COPY(&#38b);
pc = &#38COPY(&#38COPY(&#38b)); pc = COPY(COPY(&#38b));











Обратите внимание, что тип результата в операторе return(*pa) -не указатель, а указуемый тип. То же самое и в выражении, где находится вызов функции
COPY(&#38b), то есть по синтаксису функция COPY возвращает не тип-указатель, а тип указуемой переменной.

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

.

int &#38INC(int &#38a ) int *INC(int *pa)
//---------------------------------------------------------

Си++ Эквивалент в "классическом Си"
//---------------------------------------------------------

{ {
a++; (*pa)++;
return(a); return(pa);
} }
... ...
int а,b = 5; int а,b = 5;
а = INC( INC( INC(b))); a = *INC( INC( INC(&#38b)));



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

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




-значением;


-обычным указателем;


-ссылкой.



//------------------------------------------------------bk72-01.cpp

//--------------------------------------------------------

// 1. Параметр и результат - значения

//--------------------------------------------------------

struct dat { int day,month,year; };
// Эквивалент

dat Inc(dat x) // void Inc( dat x, dat *pout)

{ // {

x.day++; // x.day++;

return x; // *pout = x;}

} //

void main() //

{ //

dat a,b,*p; //

a = Inc(Inc(b)); // dat tmp1; Inc(x=b,&#38tmp1);

p = &#38Inc(b); // dat tmp2; Inc(x=tmp1; &#38tmp2);

a = *p; // dat tmp3; Inc(Inc(x=b,&#38tmp3);

} // p = &#38tmp3; a = *p;

//-------------------------------------------------------

// 2.Параметр и результат - указатели

//-------------------------------------------------------

struct dat { int day,month,year; };

dat* Inc(dat* x) //

{ //

x-&#62day++; //

return x; //

} //

void main() //

{ //

dat a,b,*p; //

a = *Inc(Inc(&#38b)); //

p = Inc(&#38b); //

a = *p; //

} //

//-------------------------------------------------------

// 3. Параметр и результат - ссылки

//-------------------------------------------------------

struct dat { int day,month,year; };

dat&#38 Inc(dat&#38 x) // Эквивалент

{ // dat *Inc(dat *px)

x.day++; // { (*px).day++;

return x; // return px;}

} //

void main() //

{ //

dat a,b,*p; //

a = Inc(Inc(b)); // dat *pp; pp=Inc(&#38b); a=*Inc(pp);

p = &#38Inc(b); // p = Inc( &#38b) ;

a = *p; // a = *p;

} //

Сравнение этих примеров показывает следующее:


-примеры со ссылками и указателями идентичны по реализации, но отличаются по синтаксису вызова функции;


-примеры со значениями и ссылками отличаются по реализации, но идентичны по синтаксису вызова функции.


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



Ссылка в качестве формального параметра используется всегда, когда требуется изменить значение соответствующего фактического параметра при вызове функции. Тип параметра может быть любым. В качестве примера рассмотрим ССЫЛКУ НА УКАЗАТЕЛЬ на текущий элемент списка и на текущую вершину
двоичного дерева в операциях включения. В соответствии с принципом контекстного определения типа данных последовательность операций "&#38,*" читается от переменной p справа налево как ссылка на указатель.



//------------------------------------------------------bk72-02.cpp

//------ Включение в список с использованием ссылки на указатель и рекурсии

struct list
{
int val;
list *next;
} *head=NULL;

void InsertList(list *&#38ph, int n)
{
list *pnew;
if (ph==NULL || n &#60 ph-&#62val)
{
pnew = new list;
pnew-&#62val=n;
pnew-&#62next=ph;
ph=pnew;
return;
}
InsertList(ph-&#62next, n);
}
//------ Включение в дерево с использованием ссылки на указатель и рекурсии

struct btree
{
int val;
btree *l,*r;
} *hd=NULL;

void Insert(btree *&#38p, int n)
{
if (p==NULL)
{
btree *pnew=new btree;
pnew-&#62val=n;
pnew-&#62l=pnew-&#62r=NULL;
p=pnew;
return;
}
if (p-&#62val &#60 n) Insert(p-&#62l,n);
else Insert(p-&#62r,n);
}


Стандартная библиотека ввода-вывода


Стандартная библиотека ввода-вывода (stdio) имеет функции, работающие как с символами, так и со строками. Если функция имеет в качестве параметра символ , то это функция посимвольного ввода, если массив символов, то строчного. Заметим, что символ конца строки в построчном вводе ( \0) формируется функцией ввода, обычно в последовательности символьном потоке для ограничения строки используется символ \n " перевод строки" . Кроме того, построчный ввод иногда бывает форматным, то есть вводит символы строки до первого ограничителя (пробела, запятой, точки и т.п..).


&#35 include &#60stdio.h&#62
void my_gets(char s[]) // Собственная функция gets(char s[])


{
int i;
char c;
for (i=0; i &#60 79 &#38&#38 (c=getchar()) !=EOF &#38&#38 c !='\n'; i++)
s[i] = c;
s[i]='\0';
}
&#35include &#60iostream.h&#62
char c,s[80];
void main() // Пример использования объектов потокового ввода


{
cin &#62&#62 c; // Посимвольный ввод


cin &#62&#62 s; // Построчный форматированный ввод


cin.getline(s,80); // Построчный неформатируемый ввод


}


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

В стандартной библиотеке каждый файл представлен структурированной переменной, в которой сосредоточена вся информация об открытом файле: тип файла, идентификатор файла в операционной системе (номер, handle ), буфер на 1 блок (сектор) файла, текущее состояние и способ работы с файлом. Назовем ее описателем или ДЕСКРИПТОРОМ ФАЙЛА. Спецификатор typedef для этой структуры, а также прототипы всех функций ввода-вывода содержатся в заголовочном файле stdio.h, который необходимо включить в текст программы макрокомандой &#35include.


typedef struct {.....} FILE;

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


&#35include &#60stdio.h&#62
FILE *fd; // Переменная fd - указатель на дескриптор файла


fd = fopen("aaa.txt","r");
// режим "чтение текстового файла"


// имя файла - строковая константа


// или указатель на строку


// fopen() возвращает указатель на дескриптор файла


// запомнить результат функции


if (fd == NULL)
printf("Файл не открыт\n");
else
{ // Указатель на дескриптор файла


fscanf(fd,....); // при обращении к файлу


fclose(fd); // Закрыть файл


}



Стандартные потоки (файлы) ввода-вывода


В библиотеке имеются внешние переменные-указатели на дескрипторы файлов - стандартных устройств ввода-вывода.


.
extern FILE *stdin, *stdout, *stderr, *stdaux, *stdprn;
стандартный ввод --- ¦ ¦ ¦ ¦
стандартный вывод ---------- ¦ ¦ ¦
регистрация ошибок ------------------ ¦ ¦
дополнительное устройство -------------------- ¦
устройство печати -------------------------------------

Эти файлы открываются библиотекой автоматически перед выполнением main и по умолчанию назначаются на терминал (stdin - клавиатура, stdout,stderr - экран), последовательный порт (stdaux) и принтер (stdprn). stdin и stdout могут быть переназначены в командой строке запуска программы на любые другие файлы


.
&#62test.exe &#60a.dat &#62c:\xxx\b.dat
¦ L------- файл stdout
L----------------- файл stdin

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



Статические элементы класса


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


. имя_класса::имя_элемента

Доступность ее определяется стандартным образом в зависимости от размещения в личной или общей части класса. Сама переменная должна быть явно определена в программе и инициализирована:


//------------------------------------------------------bk73-02.cpp


// Все объекты класса dat связаны в односвязный список


//----------------------------------------------------


&#35include &#60stdio.h&#62
class dat
{
int day,month,year;
static dat *fst; // Указатель на первый элемент


dat *next; // Указатель на следующий элемент


public:
void show(); // Просмотр всех объектов


dat(); // Конструктор


~dat(); // Деструктор


};
dat *dat::fst=NULL; // Определение статического элемента


//---------------------------------------------------------


void dat::show()
{
dat *p;
for (p=fst; p !=NULL; p=p-&#62next)
{ /* вывод информации об объекте */ }
}
//------ Конструктор - включение в начало списка --------


dat::dat()
{ /* ... */ next = fst; fst = this; }
//------ Деструктор - поиск и исключение из списка -------


dat::~dat()
{
dat *p; // указатели на текущий и предыдущий


dat *pred; // элементы списка


for ( p=fst,pred=NULL; p !=NULL; pred=p,p = p-&#62next)
if (p = this) break; // Найден - выйти


if (p!=NULL) // Найденный исключить из списка


{
if (pred==NULL) fst=fst-&#62next;
else pred-&#62next=p-&#62next;
}
}

Статическими могут быть объявлены также и функции-элементы. Их "статичность" определяется тем, что вызов их не связан с конкретным объектом и может быть выполнен по полному имени. Соответственно в них не используются неявный указатель на текущий объект this . Они вводятся, как правило, для выполнения действий, относящихся ко всем объектам класса. Для предыдущего примера функция просмотра всех объектов класса может быть статической:


class list
{ ...
static void show(); // Cтaтическая функция просмотра


} // списка объектов


//--------------------------------------------------------


static void list::show()
{
list *p;
for (p=fst; p !=NULL; p=p-&#62next)
{ ...вывод информации об объекте... }
}
//-------------------------------------------------------


void main()
{ ... list::show(); } // Вызов функции по полному имени



Стеки, очереди


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



Стратегии взаимодействия объектов программе


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



Строки, массивы символов и указатели char*


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



-указатель на отдельный байт;



-указатель на область памяти -массив байтов;



-указатель на отдельный символ;



-указатель на массив символов.

Какая из них в действительности имеет место -зависит от программы, транслятор здесь не накладывает ограничений. Тем не менее, по сложившейся традиции будем считать, что указатель на char ссылается на массив символов. А поскольку стандартным содержимым массива символов является строка - последовательность символов, ограниченная символом с кодом 0, то такой указатель логично назвать УКАЗАТЕЛЕМ НА СТРОКУ. Использование его в программе при работе со строками предпочтительнее в сравнении с массивом символов уже потому, что размерность массива символов фиксирована, а размерность памяти, адресуемой указателем, ограничивается внешними факторами. Таким образом, он является более естественным средством для представления строки.


int strlen(char *p) // Возвращает длину строки, заданной


{ // указателем на на строку char*


int n;
for (n=0; *p != '\0'; p++);
return n;
}
void strcat(char *dst, char *src)
{ // Объединяет строки,


while (*dst !='\0') dst++; // заданные указателями


for (; *src !='\0'; *dst++ = *src++);
*dst = '\0';
}

Несколько замечаний необходимо сделать о СТРОКОВЫХ КОНСТАНТАХ. Строковая константа представляет собой последовательность символов, заключенных в двойные кавычки (например "abcd"). Для строковой константы транслятор в любом контексте программы выполняет следующие действия:



-создает массив символов с размерностью, достаточной для размещения строки;



-инициализирует (заполняет) массив символами строки, дополняя символом '\0';



-в контекст программы, где присутствует строковая константа, включает указатель на созданный массив символов. Таким образом в программе ей соответствует тип char* -указатель на строку.


СТРОКОВАЯ КОНСТАНТА в любом контексте программы это указатель на создаваемый транслятором массив символов, инициализированный этой строкой.

char *q; // ПРОГРАММА

q = "ABCD";
char *q; // ЭКВИВАЛЕНТ

char A[5] = {'A','B','C','D','\0'};
q = A;

С точки зрения такого определения корректны и такие на первый взгляд " дикие"
выражения:

char c,*q;
c = "ABCD"[3];
q = "12345" + 2;
for (q = "12345"; *q !='\0'; q++);



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



extern int strcmp(char *, char*);
char *p,A[20];
strcmp(A,"1234");
strcmp(p,A+2);


Строковые потоки


Строковые потоки - потоки, источниками символов которых являются не файлы, а строки в памяти. Они наследуют все функции и операции istream, ostream и iostream. Конструкторы строковых потоков (p - буфер строки, n - длина, mode - режим открытия) .


istrstream(char *p);
ostrstream(char *p, int n, int mode);
// mode = ios::out - запись с начала строки


// mode = ios::ate - добавление к существующей строке


strstream (char *p, int n, int mode);
// ios::in - чтение строки


// ios::out - запись строки


// ios::ate - добавление к существующей строке


// функция seekg - позиционирование в строке



Структура данных - вовсе не массивы


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



Структура как базовый тип данных


Для базовых типов данных определены операции присваивания, они могут быть также формальными параметрами и результатом функции. Аналогичные действия могут выполняться в Си++ над структурами:



- операция ПРИСВАИВАНИЯ СТРУКТУРИРОВАННЫХ
ПЕРЕМЕННЫХ производит побайтное копирование одной структуры в другую. Присваивание возможно также при косвенном обращении по указателю на структуру;



- ФОРМАЛЬНЫЙ ПАРАМЕТР СТРУКТУРИРОВАННАЯ ПЕРЕМЕННАЯ : имеет место
способ передачи параметра по значению: в стеке резервируется место для размещения структуры -формального параметра и производится присваивание ей значения фактического параметра (копирование);



- РЕЗУЛЬТАТ ФУНКЦИИ СТРУКТУРИРОВАННАЯ ПЕРЕМЕННАЯ : при выполнении оператора return в такой функции значение операнда -структуры присваивается структурированной переменной, использующей результат функции. При отсутствии непосредственного присваивания результата транслятор создает неявную автоматическую структурированную переменную , в которой временно его сохраняет. Функция, возвращающая структуру в качестве результата, может иметь неявный параметр -адрес размещения результата (указатель);



-в обозначении типа данных -структуры служебное слово struct можно опускать.

В приведенном примере все механизмы передачи параметров и результата интерпретируются средствами " классического" Си:

.


struct dat { int day,month,year; };
//-------------------------------------------------------


// Си++ Эквивалент в "классическом" Си


//-------------------------------------------------------


dat COPY(dat x) void COPY(dat *ret, dat x)
{ {
return(x); *ret = x;
}; };
void main() void main()
{ {
dat a1,a2,a3,*p; dat a1,a2,a3,*p;
//----- Прямое присваивание структур -----------------


a1 = a2; a1 = a2;
//----- Присваивание структур косвенно по указателю --


p = &#38a3; p = &#38a3;
*p = a2; *p = a2;
//----- Прямое присваивание результата - структуры ---


a1 = COPY(a2); a2 = x;
ret = &#38a1;
*ret = x;
//----- Неявная структурированная переменная ---------


a1 = COPY(COPY(a2)); dat dummy;
x = a2;
ret = &#38dummy;
*ret = x;
x = dummy;
ret = &#38a1;
*ret = x;
} }



Структурное программирование


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

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

И, конечно же, нельзя не затронуть вопрос о взаимоотношении программирования и искусства, тем более, что "искусство программирования" - термин, особенно широко использующийся в названиях книг. Самое главное, в чем программирование противоположно искусству - способ мышления в процессе работы: если в искусстве оно образное, основанное на интуиции, ассоциациях, то в программировании - формально-логическое. Но с другой стороны, в процессе создания алгоритма программист сначала строит у себя в голове образную модель, которая соответствует уже работающей, но еще не написанной программе, а затем пытается ее формализовать. То есть он сначала должен мысленно увидеть, как в ней перемещаются элементы массивов, связываются между собой элементы списков и т.д.. В этом смысле, программисту необходимо образное мышление, интуиция, и, если хотите, вдохновение. С такой точки зрения программирование действительно является искусством.


Из сказанного следует, что если пишущий программу мыслит, то он уже придерживается какой-то технологии программирования, даже не подозревая об этом. Условно ее можно назвать - метод "северо-западного" угла ( имеется в виду лист бумаги или экран монитора). Метод заключается в написании программы сразу от начала до конца, без использования каких-либо общих принципов, то есть "как бог на душу положит".

Чтобы понять сущность технологии программирования как "образа мыслей", рассмотрим подробнее сам процесс. Основная последовательность действий, составляющих суть программирования, уже упоминалась: по формулировке задачи (описанию действий или желаемому результату) программист в голове строит ее образную модель. Затем в процессе ее "оживления" пытается перевести законы ее функционирования на формальный язык. Все в этом процессе может быть идеальным за исключением того, что человеческий разум не в состоянии одномоментно охватить более или менее сложный образ. Он разбивает его на более простые, с которыми в состоянии справиться. Любая технология программирования позволяет сделать этот процесс более регулярным и управляемым.

Составные части образной модели могут иметь и "внешнее" выражение - оно заключается в неформальной или полуформальной словесной формулировке действий, производимых этой частью. Например, "найти минимальный элемент в массиве А и запомнить его индекс в n". Технология программирования задает правила, при помощи которых можно переходить от одной, более абстрактной, к другой, более конкретной формулировке, переводя ее по частям на формальный язык программирования.

Из всего сказанного вырисовывается такая последовательность действий, которая может быть положена в основу l процесса проектирования программы с разбиением ее на достаточное число мелких шагов :


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


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


2. . выполняется сбор фактов, касающихся любых характеристик алгоритма, и попытка их представления средствами языка;


3. . создается образная модель происходящего процесса, используются графические и какие угодно способы представления, образные " картинки" , позволяющие лучше понять выполнение алгоритма в динамике;


4. . в образной модели выделяется наиболее существенная часть, для которой подбирается наиболее точная словесная формулировка;


5. . производится определение переменных, необходимых для формального представления данного шага алгоритма;


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


7. . для оставшихся неформализованных частей алгоритма в словесной формулировке) - перечисленная последовательность действий повторяется.

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





















Наиболее существенные моменты, выражающие сущность такого процесса проектирования, объединяются общим терминам "
структурное программирование" (СП). СП в настоящий момент является общепринятой традиционной технологией разработки программ, выразителем и специально созданным инструментом для которой является язык Паскаль.Заметим, что основное внимание в ней уделяется разработке алгоритма, а данным в связке ПРОГРАММА=АЛГОРИТМ+ДАННЫЕ отводится если не второстепенная, то подчиненная роль. СП предполагает именно последовательную, шаг за шагом разработку алгоритма. Основные принципы СП находятся на уровне " здравого смысла" , но тем не менее их методическое использование в практической работе получается не сразу и требует некоторого опыта. Перечислим их :


1. . модульное проектирование ;


2. . нисходящее проектирование ;


3. . пошаговое проектирование ;


4. . структурное проектирование (программирование без goto) ;


5. . одновременное проектирование алгоритма и данных ;


6. . модульное, нисходящее, пошаговое тестирование.

Одной фразой это можно сформулировать так :

СТРУКТУРНОЕ ПРОГРАММИРОВАНИЕ - - модульное нисходящее пошаговое проектирование и отладка алгоритма и структур данных


Структуры данных с элементами произвольных типов


Рассмотренные выше массивы указателей, деревья и списки представляют собой средства для хранения и упорядочения в памяти множеств различных объектов, представленных как самостоятельными переменными, так и частями структурированных переменных. При этом следует различать "системную" и "прикладную" части структуры данных. Первая обеспечивает связность и функционирование структуры данных, как таковой. Например, в списке "системной" частью является заголовок списка и указатели на соседние элементы. Хранимые в определенном порядке компоненты структуры данных, относящиеся к конкретной задаче, являются ее "прикладной" частью. Для обычного списка это будут все остальные элементы структуры ( struct ), представляющей элементы списка.

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



-в первом -"системная" и "прикладная" части структуры данных разделяются на отдельные переменные и связываются через указатель. Поскольку при работе с "системной" частью "прикладная" является неопределенной (произвольной), то такой указатель будет иметь тип void*. Но тогда при работе с "прикладной" частью потребуется преобразовать указатель void* к конкретному типу указателя на "прикладную" часть, и обратно. В "классическом" Си это может производиться по умолчанию, сопровождаясь предупреждением транслятора, в Си++ это должно быть сделано явно;



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

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

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





//------------------------------------------------------bk57-02.cpp

struct list
{
struct list *next; // системная часть элемента

void *pdata; // указатель на прикладную часть

};
//----- Включение элемента в конец списка

list *insert (list *p h, void *pd)
{
list *p =new list)); // создать "системную" часть

p-&#62next = NULL; // нового элемента списка

p-&#62pdata = pd; // привязать "прикладную"

if (ph == NULL) // часть к "системной"

return p;
for (list *q=ph; q-&#62next!=NULL; q=q-&#62next);
q-&#62next=p;
return ph;
}
//----- Исключение первого элемента ---------------------

void *exclude( list **ph)
{
list *p = *ph;
if (p==NULL) return NULL;
*ph = p-&#62next;
void *pd = p-&#62pdata;
delete p; // уничтожить системную часть

return pd;
}
//----- Работа с конкретным списком

void main()
{ int *pval, A[ 10] ={3,4,8,32,67,5,7,4,78,45};
list *PH=NULL; // пустой список

for (i=0; i&#60 10; i++)
{
pval=new int; *pval=A[i]; // создать переменную

PH=insert(PH, (void*) pval); // включение в список

} // с преобразованием типа

for (i=0; i&#60 10; i++)
{
pval = (int*)exclude( &#38PH); // исключение элемента

delete pval; // уничтожение элемента

}
}

Во втором случае используется подмена : функция, работающая с элементами списка типа list получает элементы типа list1 с совпадающей " системной" частью :





//------------------------------------------------------bk57-03.cpp

struct list
{
struct list *next; // системная часть элемента

};
// Включение элемента в конец списка ------------------

list *insert(list *ph, list *p)
{
if (ph == NULL) return p;
for (list *q=ph; q-&#62next!=NULL; q=q-&#62next);
q-&#62next=p;
return ph;
}
//----- Исключение первого элемента ----------------------

list *exclude(list * *ph)
{
list *p;
if ( *ph == NULL) return NULL;
p = *ph; *ph=p-&#62next;
return p;
}
//----- Работа с конкретным списком -----------------------

struct list1
{
struct list *next; // системная часть элемента

int value; // прикладная часть

} // элемента

*PH = NULL; // пустой список

void main()
{ list1 *pp;
for ( int i=0; i&#60 10; i++)
{
pp = new list1; // создание элемента

pp-&#62value = i; // заполнение

PH=(list1*)insert((list*)PH,(list*)pp);
// включение в список -

// преобразование типа list1* - list*

}
for (i=0; i&#60 10; i++) // исключение элемента -

{ // преобразование типа list* - list1*

pp = (list1*)exclude((list**)PH);
delete pp;
}
}

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


Структуры данных с произвольными связямиГрафы


Граф представляет собой структуру с произвольным характером связей между элементами. С точки зрения программирования наличие в элементе "A" указателя на элемент "B" соответствует наличию в графе дуги, направленной от "A" к "B". Тогда для неориентированного графа требуется наличие как прямого, так и обратного указателя. Процедуры работы с графом также могут предусматривать его рекурсивный обход. Однако при этом необходимо отмечать уже пройденные вершины для исключения зацикливания алгоритма. В приведенном ниже примере каждая вершина графа имеет счетчик проходов, который проверяется каждый раз при входе в вершину.


//------------------------------------------------------bk55-10.cpp


//------Рекурсивный обход графа


struct graph
{
int cnt; // Счетчик обходов вершин


graph **pl; // Динамический массив


}; // указателей


void ScanGraph(graph *p)
{ int i;
p-&#62cnt++; // Увеличить счетчик


for (i=0; p-&#62pl[i] !=NULL; i++)
{
if (p-&#62pl[i]-&#62cnt !=p-&#62cnt) // Вершина не просмотрена


ScanGraph(p-&#62pl[i]); // Рекурсивный обход


}
}



Структуры данных " в узком смысле"


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

Структуры данных этого типа строятся на основе трех основных базовых структур данных :



- массивов (массивы указателей) (см.5.2.) ;



- списков (см.5.3.) ;



- деревьев (см.5.5.).

Они могут быть как простыми, то есть состоящими из элементов одного типа, так и иерархическими, в которых элемент структуры данных верхнего уровня содержит в себе структуру данных нижнего уровня (см. 5.8).

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

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

Для работы со структурой данных имеет место набор стандартных операций :



- выбор (поиск) по логическому номеру ;



- добавление последним ;



- включение по логическому номеру ;



- удаление по логическому номеру ;



- " быстрый" (двоичный) поиск в упорядоченной структуре данных ;



- включение с сохранением упорядоченности ;



- сортировка неупорядоченной структуры данных .

И, наконец , последняя характеристика HYPERLINK "bk51.htm" \l "def1" 08d0c9ea79f9bace118c8200aa004ba90b02000000090000000303000000000000c000000000000046000009000000626b35312e68746d00ffffadde00000000000000000000000000000000000000001600000010000000030062006b00350031002e00680074006d000500000064006500660031000000 структуры данных. В зависимости от способа хранения элементов различают:

- структуры данных, хранящие сами элементы данных;



- структуры данных, хранящие указатели на элементы данных. В этом случае сами элементы данных полностью независимы от структуры. Такой способ организации данных часто называют КОЛЛЕКЦИЕЙ. В этом случае допускается, что структура данных может хранить как указатели на элементы данных одного типа, так и на элементы данных различных (произвольных) типов (указатели типа void* ).





Структуры данных: все, кроме массивов



5.1. Структура данных - переменные, связанные алгоритмом
5.2. Массивы указателей


5.3. Линейные списки


5.4. Рекурсия


5.5. Рекурсивные структуры данных. Деревья


5.6. Указатели на функции


5.7. В стремлении к совершенству


5.8. Последовательные текстовые файлы


5.9. Двоичные файлы произвольного доступа


5.10. Организация файлов баз данных



Структуры и объединения


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



Структуры переменной размерности


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


struct man
{
char name[30]; // Фиксированная часть


int dd,mm,yy;
char address[]; // Переменная часть


};
man *create()
{
man *p;
char xxx[200];
gets(xxx); // Ввод значения элемента address


p = malloc(sizeof(man) + strlen(xxx) + 1);
strcpy(p-&#62address, xxx);
// Ввод фиксированной части


return p;
}



Связанные записи в файлеФайловый указатель


При размещении в файле динамических структур данных возникает вопрос, каким образом в файле будут представлены указатели, которые используются в переменных динамических структур данных. Само собой разумеется, что значение указателя, представляющего собой адрес указуемой переменной в памяти программы, никакого смысла при размещении той же переменной в файле не имеет. Но тогда обычному указателю, связывающему две переменные в памяти, нужно сопоставить аналогичный указатель в файле -назовем его ФАЙЛОВЫМ УКАЗАТЕЛЕМ. Его значением является позиция (адрес) переменной при ее размещении в файле. Как известно, позиция в файле определяется значением типа long, поэтому файловый указатель не является типизированным: тип его одинаков для любой переменной. Теперь рассмотрим подробнее задачу размещения связанных переменных (или записей). Если структурированная переменная а1 имеет указатель ptr на переменную a2 , то при размещении в файле переменная a1 должна получить еще один дополнительный параметр -файловый указатель fptr на место размещения в файле переменной a2. Сформировать значение файлового указателя можно следующим образом:



-если указуемая переменная (a2) еще не размещена в файле, позиционироваться на конец файла, получить текущую позицию как значение ее адреса в файле и записать переменную в файл. Если указуемая переменная уже размещена в файле, то просто использовать адрес ее размещения;



-полученный адрес указуемой переменной (a2) в файле сохранить как значение файлового указателя (fptr) в переменной, содержащей обычный указатель (ptr в a1);



-сохранить переменную (a2) в файле.


&#35define FNULL -1L
struct x
{
int val;
x *ptr;
long fptr;
} a2 = {0,NULL,FNULL}, a1 = {1, &#38a2, FNULL};


fseek(fd, 0L, SEEK_END);
a1.fptr = ftell(fd);
fwrite((void*)a1-&#62ptr, sizeof(x), 1, fd);


fseek(fd, 0L, SEEK_END);
fwrite((void*)&#38a1, sizeof(x), 1, fd);

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


-разместить в файле все переменные структуры данных и запомнить их адреса в файле;


-сформировать значения файловых указателей в переменных структуры данных, расположенных в памяти;


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

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



//------------------------------------------------------bk59-03.cpp

&#35include &#60stdio.h&#62
&#35include &#60alloc.h&#62
struct ftree
{
int val;
struct ftree *left,*right; // Указатели в памяти

long fleft,fright; // Указатели в файле

};

&#35define FNULL -1L
&#35define TSZ sizeof(ftree)

//------ Функция записи возвращает адрес вершины в файле

//

long PutTree(ftree *p, FILE *fd)
{
long pos;
if (p == NULL) return(FNULL);
p -&#62fleft = PutTree(p-&#62left,fd);
p -&#62fright = PutTree(p-&#62right,fd);
pos = ftell(fd);
fwrite( (void*)p, TSZ, 1, fd);
return(pos);
}
//------ Функция формирует файл записей фиксированной длины

// В начало файла записывается указатель на головную

// вершину дерева

void SaveTree(ftree *p, char *name)
{
long pos0; // Указатель на головную запись

FILE *fd;
if ((fd=fopen(name,"wb")) ==NULL) return;
// Резервировать место под указатель

fwrite(&#38pos0, sizeof(long), 1, fd);
pos0 = PutTree(p,fd);
fseek(fd, 0L, SEEK_SET); // Обновить указатель

fwrite( (void*)&#38pos0, sizeof(long), 1, fd);
return;
}

Во втором случае рекурсивная функция записи сначала размещает текущую вершину и запоминает ее адрес в файле.


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





//------------------------------------------------------bk59-04.cpp

//------Вариант функции записи вершины дерева в файл

long PutTree_(ftree *p, FILE *fd)
{
long cpos; // Адрес в файле текущей

if (p==NULL) return(FNULL); // вершины дерева

fseek(fd, 0L, SEEK_END);
cpos = ftell(fd);
fwrite((void*)p, TSZ, 1, fd); // Записать в файл текущую вершину

p-&#62fright = PutTree(p-&#62right,fd);
p-&#62fleft = PutTree(p-&#62left,fd);
fseek(fd, cpos, SEEK_SET); // Обновить текущую вершину

fwrite((void*)p, TSZ, 1, fd);
return cpos;
}

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



//------------------------------------------------------bk59-05.cpp

//----- Функция возвращает указатель на динамическую

// переменную - вершину дерева, считывая ее и связанное

// с ней поддерево из файла

ftree *GetTree(long pos, FILE *fd)
{
struct ftree *p;
if (pos == FNULL) return NULL;
if ((p = new ftreeTSZ) ==NULL) return NULL;
fseek(fd,pos,SEEK_SET);
fread((void *)p, TSZ, 1, fd);
p -&#62left = GetTree(p -&#62fleft,fd);
p -&#62right = GetTree(p-&#62fright,fd);
return p;
}
//----- Функция открывает файл и читает файловый указатель

// на головную вершину дерева, по которой загружает дерево

// в память

ftree *LoadTree(char *name)
{
FILE *fd;
long phead;
if ((fd = fopen(name,"rb")) ==NULL) return(NULL);
fread((void*)&#38phead, sizeof(long), 1, fd);
return GetTree(phead, fd);
}


СвязываниеВнешние ссылки и точки входа


ВНЕШНЯЯ ССЫЛКА -- обращение к переменной или вызов функции во внутреннем представлении модуля, которые определены в другом модуле и отсутствуют в текущем ТОЧКА ВХОДА -- адрес переменной или функции во внутреннем представлении модуля, к которым возможно обращение из других модулей

ОБЪЕКТНЫЙ МОДУЛЬ -- файл данных, содержащий оттранслированные во внутреннее представление собственные функции и переменные, а также информацию о внешних ссылках и точках входа модуля в символьном виде

КОМПОНОВЩИК, линкер (LINK) -- программа, составляющая из объектных модулей и библиотек загрузочный модуль программы и выполняющая взаимное увязывание (разрешение) внешних ссылок и точек входа модулей

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



-автоматические и статические переменные преобразуются в двоичный код (адреса) без сохранения дополнительной символьной информации;



-операции над переменными, расположенными в данном модуле, аналогично преобразуются в двоичный код (команды);



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



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

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


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


-составление загрузочного модуля из двоичных данных объектных модулей с определением начального адреса каждого объектного модуля в файле (компоновка);


-связывание внешних ссылок и соответствующих точек входа (редактирование связей);


-подключение необходимых объектных модулей из библиотек с выполнением над ними вышеуказанных действий.

Рассмотрим подробнее алгоритм работы компоновщика:


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


-для каждой внешней ссылки ищется точка входа с таким же именем. Если она найдена, то ее адрес в загрузочном модуле подставляется на место внешней ссылки;


-если в загрузочном модуле остаются неразрешенные внешние ссылки, для которых не нашлось соответствующих точек входа, то компоновщик просматривает указанные библиотеки;


-библиотека представляет собой файл, в котором объединены несколько обычных объектных модулей (с общим каталогом в начале). Если в одном из объектных модулей компоновщик находит необходимую точку входа, то он читает весь объектный модуль и размещает его в загрузочном модуле программы. Затем повторяются действия по связыванию внешних ссылок и точек входа;


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


.

Исходные модули Объектные модули
a.c pp a.obj
extern double f(); 0000
double g() {} 1000 код функции g()
... внешняя ссылка
f(); 2000 CALL (0000) "f"
g(); 300 0 CALL 1000
4000
b.c pp b.obj
double f() { 1200 точка входа f=1200
код функции f()
... внешняя ссылка
printf("%d",n); 1400 CALL (0000) "printf"
} код функции f()
1600

.

Загрузочный модуль a.exe

.

0000 a.obj
1000 код функции g()
внешняя ссылка
2000 CALL (5200) - адрес точки входа f
3000 CALL 1000
4000 b.obj
5200 Точка входа f= 4000 + 1200 = 5200
код функции f()
внешняя ссылка
5400 CALL (6600) - адрес точки входа printf
код функции f()
5600 библиотечный модуль
Точка входа printf = 1000 + 5600 = 6600
6600 Код функции printf




Технология объектно-ориентированного программирования на Си++ (часов)


1. . Тотальное ООП как программирование " от класса к классу" (1 час).

2. . Пример разработки программы на основе системы классов - база данных. Абстрактный класс хр а нимого объекта данных, классы типов данных, класс строки, класс таблицы, классы файлов (3 часа)

3. . Модульное ООП. Заголовочные файлы, библиотеки модулей и проекты. О ОП с использованием стандартных библиотек классов. Работа c OWL - Object Windows Library (2 часа).

4. . Стратегии взаимодействия объектов в сложной программе. Принцип разработки программ под Windows - объекты, управляемые сообщениями ( bk66.doc ) (2 часа).



Технология программирования - это


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



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




Тип структуры данных и алгоритм, выполняемый над ней (или вид
итератора).



// 1-------------------------------------------------------

int F(double *p[])
{ int n;
for (n=0; p[n]!=NULL; n++);
return(n); }
// 2-------------------------------------------------------

void F(double *p[])
{ int i,k;
do {
k=0;
for (i=1; p[i]!=NULL; i++)
if (*p[i-1] &#62 *p[i])
{ double *dd;
dd=p[i]; p[i]=p[i-1]; p[i-1]=dd; k++;
}
} while(k);
}
// 3-------------------------------------------------------

void F(double *p[], double *q)
{ int i,n;
for (i=0; p[i]!=0; i++)
if (*p[i] &#62 *q) break;
for (n=i; p[n]!=NULL; n++);
for (; n &#62=i; n--)
p[n+1] = p[n];
p[i] = q;
}
// 4-------------------------------------------------------

int F(double **p[])
{ int k,i,j;
for (k=i=0; p[i]!=NULL; i++)
for (j=0; p[i][j] !=NULL; j++, k++);
return(k);
}
// 5-------------------------------------------------------

char **F(char a[][80], int n)
{ int i; double **p;
p = malloc((n+1)*sizeof(char*));
for (i=0; i&#60n; i++) p[i]=a[i];
p[n]=NULL;
return(p);
}
// 6-------------------------------------------------------

// strlen(char *) - длина строки

char *F(char *p[])
{ int i,sz,l,k;
for (i=sz=k=0; p[i]!=NULL; i++)
if ((l=strlen(p[i])) &#62sz) { sz=l; k=i; }
return(p[k]);
}
// 7-------------------------------------------------------

char **F(char c[])
{ char **p; int i,n, cnt;
p = malloc(20 * sizeof(char*));
for (i=n=cnt=0; c[n]!=0; n++)
{
if (c[n]==' ')
{ c[n]='\0'; cnt=0; }
else
{
cnt++;
if (cnt==1) p[i++]=&#38c[n];
if (i==19) break;
}
}
p[i]=NULL; return(p);
}
// 8-------------------------------------------------------

char *F(char *p[], int m)
{ int n; char *q;
for (n=0; p[n]!=NULL; n++);
if (m &#62=n) return (NULL);
q = p[m];
for (n=m; p[n]!=NULL; n++) p[n]=p[n+1];
return(q);
}
// 9-------------------------------------------------------

// strcmp(char*,char*) - сравнение строк

int F(char *p[], char *str)
{ int h,l,m;
for (h=0; p[h]!=NULL; h++);
for (h--,l=0; h &#62= l;)


{
m = (l+h) / 2;
switch(strcmp(p[m],str))
{
case 0: return(m);
case -1: l = m+1; break;
case 1: h = m-1; break;
}
}
return(-1);
}
// 10-------------------------------------------------------

// gets(char *) - ввод строки с клавиатуры

char **F()
{ int n; char **p, s[80];
p = malloc(100 * sizeof(char*));
for (n=0; n&#60 99 &#38 (gets(s),s[0]!='\0'); n++ )
{
p[n]=malloc(strlen(s)+1);
strcpy(p[n],s);
}
p[n]=NULL; return(p);
}
// 11-------------------------------------------------------

void F(char *p[], int m)
{ int n; char *q;
for (n=0; p[n]!=0; n++);
if (m &#62= n) return;
for (; n &#62 m; n--) p[n+1] = p[n];
p[m+1] = malloc(strlen(p[m]+1));
strcpy(p[m+1],p[m]);
}
// 12-------------------------------------------------------

char *F(char **p[], int n)
{ int k,i,j;
for (k=i=0; p[i]!=NULL; i++)
for (j=0; p[i][j] !=NULL; j++, k++)
if (k==n) return(p[i][j]);
return(NULL);
}
// 13--------------------------------------------------------

struct xxx { int v; xxx *next; };
int F(xxx *p)
{ int n;
for (n=0; p!=NULL; p=p-&#62next, n++);
return(n);
}
// 14---------------------------------------------------------

struct xxx { int v; xxx *next; };
void F(xxx **p, int v)
{ xxx *q;
q = malloc(sizeof(xxx));
q-&#62val = v; q-&#62next = *p; *p = q;
}
// 15---------------------------------------------------------

struct xxx { int v; xxx *next; };
xxx *F(xxx *p, int n)
{
for (; n!=0 &#38&#38 p!=NULL; n--, p=p-&#62next);
return(p);
}
// 16--------------------------------------------------------

struct xxx { int v; xxx *next; };
void F( xxx **p, int v)
{ xxx *q;
q = malloc(sizeof( xxx));
for ( ; *p !=NULL; p = &#38(*p)-&#62next);
q-&#62val = v; q-&#62next = NULL; *p = q;
}
// 17--------------------------------------------------------

void F( xxx **p, int n)
{ xxx *q;
for (; n!=0 &#38&#38 *p!=NULL; n--, p =&#38(*p)-&#62next);
q = *p;
if (*p != NULL) *p = (*p)-&#62next;
free (q);
}
// 18---------------------------------------------------------




int F( xxx *p)
{ int n; xxx *q;
if (p==NULL) return(0);
for (q = p, p = p-&#62next, n=1; p !=q; p=p-&#62next, n++);
return(n);
}
// 19---------------------------------------------------------

struct xxx { int v; xxx *next,*pred; };
void F( xxx **p, int v)
{ xxx *q;
q = malloc(sizeof( xxx));
q-&#62val = v; q-&#62next = q-&#62pred = q;
if (*p == NULL) *p = q;
else
{
q-&#62next = *p; q-&#62pred = (*p)-&#62pred;
(*p)-&#62pred-&#62next = q; (*p)-&#62pred = q; *p=q;
}
}
// 20--------------------------------------------------------

struct xxx { int v; xxx *next; };
void F( xxx **ph)
{ xxx *q, *tmp, **pp;
tmp = NULL;
while (*ph !=NULL)
{
q = *ph; *ph = (*ph)-&#62next;
for (pp = &#38tmp; *pp !=NULL &#38&#38 (*pp)-&#62val &#60 q-&#62val;
pp = &#38(*pp)-&#62next);
q-&#62next = *pp; *pp = q;
}
*ph = tmp;
}
// 21--------------------------------------------------------

struct xxx { int v; xxx *next,*pred; };
xxx *F( xxx **pp, int n)
{ xxx *q;
for (q = *pp; n!=0; q = q-&#62next, n--);
if (q-&#62next == q) { *pp = NULL; return(q); }
if (q == *pp) *pp = q-&#62next;
q-&#62pred-&#62next = q-&#62next;
q-&#62next-&#62pred = q-&#62pred;
return(q);
}
// 22--------------------------------------------------------

struct xxx { int v; xxx *p[4]; };
int F(xxx *q)
{
int i,n,m;
if (q==NULL) return 0;
for (n=F1(q-&#62p[0]),i=1; i&#60 4; i++)
if ((m=F(q-&#62p[i])) &#62n) n=m;
return n+1;
}
// 23--------------------------------------------------------

struct xxx { int v; xxx *l,*r; };
int F( xxx *p)
{
if (p==NULL) return(0);
return (1 + F(p-&#62r) + F(p-&#62l));
}
// 24--------------------------------------------------------

struct xxx
{
int val,cnt;
xxx **link;
};
int F(xxx *p)
{ int i,n;
p-&#62cnt++;
for (i=0, n=1; p-&#62link[i] !=NULL; i++)
if (p-&#62link[i]-&#62cnt != p-&#62cnt)
n += F(p-&#62link[i]);
return (n);
}
// 25--------------------------------------------------------




void F(int a[], int n, int v)
{
if (a[n] ==-1) { a[n]=v; return; }
if (a[n]==v) return;
if (a[n] &#62v)
F(a,2*n,v);
else
F(a,2*n+1,v);
}
void z3() {
int B[256],i;
for (i=0; i&#60 256; i++) B[i]=-1;
F(B,1,5); F(B,1,3); } // Пример вызова



// 26-------------------------------------------------------

struct xxx
{
int val,cnt;
xxx **link;
};
int F(xxx *p, xxx *dst, xxx *from)
{ int i,n,m;
if (from==NULL &#38&#38 p==dst) return (0);
for (i=0, n=-1; p-&#62link[i] !=NULL; i++)
if (p-&#62link[i] !=from)
{
m = F(p-&#62link[i],dst,p);
if (m &#62 n) n = m;
}
if (n !=-1) n++;
return (n);
}
void z4()
{
xxx *ph; int n;
n = F(ph,ph,NULL); } // Пример вызова

// 27--------------------------------------------------------

typedef int (*PCMP)(void*, void*);
void F(void **p, PCMP pf, void *q)
{ int n,i;
for (n=0; p[n]!=NULL; n++)
if ((*pf)(q,p[n]) ==1) break;
for (i=n; p[i] !=NULL; i++);
for (; i &#62 n; i--) p[i+1]=p[i];
p[n]=q;
}
// 28------------------------------------------------------

typedef int (*PCMP)(void*, void*);
void *F(void *p[], PCMP pf, void *q)
{ int h,l,m;
for (h=0, p[h]!=NULL; h++);
for (h--, l=0; l &#60=h;)
{
m = (h+l)/2;
switch ((*pf)(q, p[m]))
{
case 0: return(p[m]);
case -1: h = m-1; break;
case 1: l = m+1; break;
}
}
return(NULL);
}
// 29------------------------------------------------------

typedef int (*PTEST)(void*);
void *F(void *p, int sz, int n, PTEST pf)
{ char *q;
for (q=p; n!=0; q +=sz, n--)
if ((*pf)(q)) return(q);
return(NULL);
}
// 30---------------------------------------------------------

struct xxx { void *data; xxx *next; };
void F( xxx **p, void (*pf)(void*))
{
xxx *q;
for (; *p != NULL; p++)
for (q = *p; q != NULL; q = q-&#62next)
(*pf)(q-&#62data);
}
// 31--------------------------------------------------------

struct xxx { void *data; xxx *next; };
struct sxxx { xxx *ph; sxxx *next; };
void F( sxxx *p, void (*pf)(void*))
{
xxx *q;
for (; p != NULL; p = p-&#62next)
for ( q = p-&#62ph; q!=NULL; q=q-&#62next)


(*pf)(q-&#62data);
}
// 32--------------------------------------------------------

struct mxxx { void **data; mxxx *next; };
void F( mxxx *p, void (*pf)(void*))
{
void **q;
for (; p != NULL; p = p-&#62next)
for (q = p-&#62data; *q != NULL; q++)
(*pf)(*q);
}
// 33--------------------------------------------------------

void F(void ***p, void (*pf)(void*))
{
void **q;
for (; *p != NULL; p++)
for (q = *p; *q != NULL; q++)
(*pf)(*q);
}
// 34--------------------------------------------------------

void F(void *p, int sz, int n, void (*pf)(void*))
{
char *q;
for (q = p; n &#62 0; n--, q+=sz)
(*pf)(q);
}
// 35--------------------------------------------------------

struct xxx { void *data; xxx **link; };
void F( xxx *p, void (*pf)(void*))
{
xxx **q;
if (p==NULL) return;
(*pf)(p-&#62data);
for (q = p-&#62link; *q != NULL; q++)
F(*q,pf);
}
// 36--------------------------------------------------------

struct xxx { void **data; xxx *r, *l; };
void F( xxx *p, void (*pf)(void*))
{
void **q;
if (p==NULL) return;
F(p-&#62r, pf);
for (q = p-&#62data; *q != NULL; q++)
(*pf)(*q);
F(p-&#62l, pf);
}
// 37-------------------------------------------------------

struct xxx { void *data; xxx *next; };
struct zzz { xxx *ph; zzz *r, *l; };
void F( zzz *p, void (*pf)(void*))
{
xxx *q;
if (p==NULL) return;
F(p-&#62r, pf);
for (q = p-&#62ph; q != NULL; q = q-&#62next)
(*pf)(q-&#62data);
F(p-&#62l, pf);
}
// 38-------------------------------------------------------

struct xxx { void *data; xxx *next,*pred; };
void F( xxx *p, void (*pf)(void*))
{
xxx *q;
if (p==NULL) return;
q = p;
do {
(*pf)(q-&#62data);
q = q-&#62next;
} while (q != p);
}
// 39-------------------------------------------------------

void F(void ***p, int (*pf)(void*))
{
int i,j;
for (i =0; p[i] != NULL; i++)
for (j =0; p[i][j] != NULL; j++)
if ((*pf)(p[i][j])) return p[i][j];
return NULL;
}
// 40--------------------------------------------------------

struct xxx { int v; xxx **pp; };


int F(xxx *q)
{
int i,n,m;
if (q==NULL) return 0;
for (n=F(q-&#62p[0]),i=1; q-&#62p[i]!=NULL; i++)
if ((m=F(q-&#62p[i])) &#62n) n=m;
return n+1;
}
// 41---------------------------------------------------------

typedef int (*PCMP)(void*, void*);
struct xxx { void *data; xxx *next; };
void *F( xxx **p, PCMP pf)
{
xxx *q;
void *s;
for (s=p[0]-&#62data; *p != NULL; p++)
for (q = *p; q != NULL; q = q-&#62next)
if ((*pf)(s,q-&#62data)==-1) s=q-&#62data;
return s;
}
// 42--------------------------------------------------------

typedef int (*PCMP)(void*, void*);
struct xxx { void *data; xxx *next; };
struct sxxx { xxx *ph; sxxx *next; };
void *F( sxxx *p, PCMP pf)
{
xxx *q;
vois *s;
for (s=p-&#62ph-&#62data; p != NULL; p = p-&#62next)
for ( q = p-&#62ph; q!=NULL; q=q-&#62next)
if ((*pf)(s,q-&#62data))==-1) s=q-&#62data;
return s;
}
// 43--------------------------------------------------------

typedef int (*PCMP)(void*, void*);
struct xxx { void **data; xxx *next; };
void *F( mxxx *p, PCMP pf)
{
void **q;
void *s;
for (s=p-&#62data[0]; p != NULL; p = p-&#62next)
for (q = p-&#62data; *q != NULL; q++)
if ((*pf)(s,*q)==-1) s = *q;
return s;
}
// 44--------------------------------------------------------

typedef int (*PCMP)(void*, void*);
void *F(void ***p, PCMP pf)
{
void **q, *s;
for (s=p[0][0]; *p != NULL; p++)
for (q = *p; *q != NULL; q++)
if ((*pf)(s,*q)==-1) s = *q;
return s;
}
// 45--------------------------------------------------------

typedef int (*PCMP)(void*, void*);
void *F(void *p, int sz, int n, PCMP pf)
{
char *q;
void *s;
for (q = p, s = p; n &#62 0; n--, q+=sz)
if ((*pf)(s,q)==-1) s=q;
return s;
}
// 46--------------------------------------------------------

typedef int (*PCMP)(void*, void*);
struct xxx { void *data; xxx **link; };
void *F( xxx *p, void PCMP pf)
{
xxx **q;
void *s,*r;
if (p==NULL) return NULL;
s = p-&#62data;
for (q = p-&#62link; *q != NULL; q++)
{
r=F(*q,pf);


if (r!=NULL &#38&#38 (*pf)(s,r)==-1) s=r;
}
return r;
}
// 47--------------------------------------------------------

typedef int (*PCMP)(void*, void*);
struct xxx { void *data; xxx *next,*pred; };
void *F( xxx *p, PCMP pf)
{
xxx *q;
void *s;
if (p==NULL) return NULL;
q = p; s = p-&#62data;
do {
if ((*pf)(s,q-&#62data))==-1) s=q-&#62data;
q = q-&#62next;
} while (q != p);
return s;
}
// 48---------------------------------------------------------

typedef int (*PCMP)(void*, void*);
void F(void *p[], void PCMP pf)
{
int i,k;
do {
k=0;
for (i=1;p[i]!=NULL; i++)
if ((*pf)(p[i-1],p[i] &#62 0))
{ void *s = p[i-1];
p[i-1]=p[i];
p[i]=s; k++;
}
} while(k);
}
// 49---------------------------------------------------------

typedef int (*PCMP)(void*, void*);
void F(void *p[], void PCMP pf, void *q)
{ int i,j;
for (i=0; p[i]!=NULL &#38&#38 (*pf)(p[i],q) &#60 0; i++);
for (j=0; p[j]!=NULL; j++);
for (; j&#62=i; j--) p[j+1]=p[j];
p[i]=q;
}
// 50---------------------------------------------------------

typedef int (*PCMP)(void*, void*);
struct xxx { void *data; xxx *next; };
void F( xxx **p, PCMP pf, void *q)
{
xxx *s;
for (; *p!=NULL &#38&#38 (*pf)((*p)-&#62data,q) &#60 0; p = &#38(*p)-&#62next);
s = new xxx;
s-&#62data = q;
s-&#62next = (*p)-&#62next;
*p = s;
}
// ---------------------------------------------------------




Структура формальных параметров для функции




1-20. Вид файла (текстовый или двоичный). Структура данных в файле или формат данных.

21-30. Формат данных в памяти при работе с массивом
переменного формата.

31-40. Структура формальных параметров для функции с переменных количеством параметров.

41-50. Алгоритм, реализованный рекурсивной функцией.





// 1------------------------------------------------------

struct man { int dd,mm,yy; };
man *F(int n, FILE *fd)
{ man *p;
p = malloc(sizeof( man));
fseek (fd, (long)sizeof( man)*n, SEEK_SET);
fread (p, sizeof( man),1,fd);
return(p);
}
// 2------------------------------------------------------

void *F(FILE *fd)
{ int n;
void *p;
fread(&#38n,sizeof(int),1,fd);
if (n==0) return(NULL);
p = malloc(n);
fread(p,n,1,fd);
return(p);
}
// 3------------------------------------------------------

double *F(FILE *fd)
{ int n;
double *p;
fread(&#38n,sizeof(int),1,fd);
if (n==0) return(NULL);
p = malloc((n+1)*sizeof(double));
fread(p,sizeof(double),n,fd);
p[n]=0.0;
return(p);
}
// 4------------------------------------------------------

&#35define FNULL -1L
struct xxx { long fnext; };
xxx *F(int n,FILE *fd)
{ xxx *p;
long p0;
p = malloc(sizeof( xxx));
fseek(fd,0L,SEEK_SET);
fread(&#38p0,sizeof(long),1,fd);
for (; p0!=FNULL &#38&#38 n!=0; n--, p0 = p-&#62fnext)
{
fseek(fd,p0,SEEK_SET);
fread(p,sizeof( xxx),1,fd);
}
return(p);
}
// 5------------------------------------------------------

struct man { int dd,mm,yy; };
man *F(int n, FILE *fd)
{ man *p;
long fp;
p = malloc(sizeof( man));
fseek(fd, sizeof(long)*n,SEEK_SET);
fread(&#38fp,sizeof(long),1,fd);
fseek(fd,fp,SEEK_SET);
fread(p,sizeof( man),1,fd);
return(p);
}
// 6------------------------------------------------------

void *F(int n, FILE *fd)
{ int sz;
void *p;
fseek(fd,0L,SEEK_SET);
fread(&#38sz,sizeof(int),1,fd);
p = malloc(sz);
fseek (fd, (long)sz * n +sizeof(int), SEEK_SET);
fread (p, sz,1,fd);
return(p);
}
// 7------------------------------------------------------

void *F(int n, FILE *fd)


{ int sz;
void *p;
long p0;
fseek(fd,0L,SEEK_SET);
fread(&#38sz,sizeof(int),1,fd);
fread(&#38p0,sizeof(long),1,fd);
p = malloc(sz);
fseek (fd, p0 + sizeof(long)*n, SEEK_SET);
fread (&#38p0, sizeof(long),1,fd);
fseek(fd, p0, SEEK_SET);
fread(p, sz, 1, fd);
return(p);
}
// 8------------------------------------------------------

char *F( int n, FILE *fd)
{ char *p;
long fp;
int i;
fseek(fd, sizeof(long)*n,SEEK_SET);
fread(&#38fp,sizeof(long),1,fd);
fseek(fd,fp,SEEK_SET);
n = 80; p = malloc(n);
for (i=0;; i++)
{
if (i==n) p = realloc(p, n=n*2);
fread(p+i,1,1,fd);
if (p[i]=='\0') return(p);
}
return(p);
}
// 9------------------------------------------------------

&#35define FNULL -1L
char *F(int n, FILE *fd)
{ long p0;
int sz;
char *p;
fseek(fd,0L,SEEK_SET);
fread(&#38p0,sizeof(long),1,fd);
for (; p0!=FNULL &#38&#38 n!=0; n--)
{
fseek(fd,p0,SEEK_SET);
fread(&#38p0,sizeof(long),1,fd);
}
if (p0==FNULL) return(NULL);
fread(&#38sz,sizeof(int),1,fd);
p = malloc(sz+1);
fread(p,sz,1,fd);
p[sz]='\0';
return(p);
}
// 10--------------------------------------------------------

char *F(FILE *fd)
{ int n;
char *p;
fread(&#38n,sizeof(int),1,fd);
if (n==0) return(NULL);
p = malloc(n);
fread(p,n,1,fd);
return p;
}
// 11--------------------------------------------------------

void F(FILE *fd, char *s)
{ int n;
char *p;
fseek(fd,0L,SEEK_END);
n = strlen(s)+1;
fwrite(&#38n,sizeof(int),1,fd);
fwrite(p,n,1,fd);
}
// 12--------------------------------------------------------

double *F(FILE *fd)
{ int n, dn;
double *p;
fread(&#38n,sizeof(int),1,fd);
if (n==0) return(NULL);
dn = n / sizeof(double);
p = malloc((dn+1)*sizeof(double));
p[0]=dn;
fread(p+1,sizeof(double), dn, fd);
return p;
}
// 13--------------------------------------------------------

void F(FILE *fd, double *s, int dn)
{ int n;
n = dn * sizeof(double);
fseek(fd,0L,SEEK_END);
fwrite(&#38n,sizeof(int),1,fd);
fwrite(s,sizeof(double),dn,fd);
}
// 14------------------------------------------------------




void F(FILE *fd)
{ int n;
void *p;
fread(&#38n,sizeof(int),1,fd);
if (n==0) return;
p = malloc(n);
fread(p,n,1,fd);
switch (n)
{
case sizeof(int): printf("%d", *(int*)p); break;
case sizeof(long): printf("%ld", *(long*)p); break;
default: puts((char*)p); break;
}
free(p);
}
// 15------------------------------------------------------

char *F( int n, FILE *fd)
{ int m;
char *p;
long fp;
fseek(fd, sizeof(long)*n,SEEK_SET);
fread(&#38fp,sizeof(long),1,fd);
fseek(fd,fp,SEEK_SET);
fread(&#38m,sizeof(int),1,fd);
p=malloc(m);
fread(p,m,1,fd);
return p ;
}
// 16------------------------------------------------------

char *F(int n, FILE *fd1, FILE *fd2)
{
long pp;
char *q;
fseek(fd1,n*sizeof(long,SEEK_SET));
fread(&#38pp,sizeof(long),1,fd1);
q = malloc(80);
fseek(fd2,pp,SEEK_SET);
fgets(q,80,fd2);
return q;
}
// 17------------------------------------------------------

char **F(FILE *fd)
{ int n,m,i;
char **p;
long *fp;
fseek(fd, 0L,SEEK_SET);
fread(&#38n,sizeof(int),1,fd);
p = malloc(sizeof(char*)*(n+1));
fp = malloc(sizeof(long)*n);
fread(fp,sizeof(long,n,fd));
for (i=0; i&#60n; i++)
{
fseek(fd, fp[i],SEEK_SET);
fread(&#38m,sizeof(int),1,fd);
p[i]=malloc(m);
fread(p[i],m,1,fd);
}
p[n]=NULL:
return p ;
}
// 18------------------------------------------------------

&#35define FNULL -1L
struct ooo { ooo *p[20]; char *s; long fs; long fp[20]; }
ooo *F(FILE *fd, long pos)
{
int i,m;
ooo *q;
if (pos==FNULL) return NULL;
q = malloc(sizeof(ooo));
fseek(fd,pos,SEEK_SET);
fread(q, sizeof(ooo),1,fd);
fseek(fd,q-&#62fs,SEEK_SET);
fread(&#38m,sizeof(int),1,fd);
q-&#62s=malloc(m);
fread(q-&#62s,m,1,fd);
for (i=0; i&#60 20; i++) q-&#62p[i]=F(fd,q-&#62fp[i]);
return q;
}
... ooo *head = F(fd,0L); ...
// 19------------------------------------------------------

struct man { int dd,mm,yy; char *addr};
man *F(FILE *fd)
{ man *p;
int n;
fread(&#38n,sizeof(int),1,fd);
p = malloc(sizeof(man));
fread (p, sizeof(man),1,fd);


n = n - sizeof(man);
p-&#62addr = malloc(n);
fread(p-&#62addr,n,1,fd);
return(p);
}
// 20------------------------------------------------------

struct man { int dd,mm,yy; char *addr};
void F(FILE *fd, man *p)
{ int n;
n = sizeof(man)+strlen(p-&#62addr)+1;
fseek(fd,0L,SEEK_END);
fwrite(&#38n,sizeof(int),1,fd);
fwrite (p, sizeof(man),1,fd);
n = n - sizeof(man);
fwrite (p-&#62addr, n,1,fd);
}
// 21------------------------------------------------------

struct man { char name[20]; int dd,mm,yy; char *address; };
struct man *F(char *nm, char *addr)
{ struct man *p;
p = malloc(sizeof(struct man) + strlen(addr+1));
strcpy(p-&#62name,nm);
strcpy(p+1,addr);
p-&#62address = p+1;
return(p);
}
// 22------------------------------------------------------

int *F(char **p)
{ int *q,n,i,j;
char *s;
for (n=i=0; p[i]!=NULL; i++) n+= strlen(p[i]);
q = malloc(sizeof(int) + n + i);
*q = i;
for (s = q+1, i=0; p[i]!=NULL; i++)
{
for (j=0; p[i][j]!='\0'; j++) *s++ = p[i][j];
*s++ = '\0';
}
return(q);
}
// 23------------------------------------------------------

double F(int *p)
{ double *q,s; int m;
for (q=p+1, m=*p, s=0.; m&#62=0; m--) s+= *q++;
return(s);
}
// 24------------------------------------------------------

char *F(char **p)
{ int n,i,j;
char *s,*q;
for (n=i=0; p[i]!=NULL; i++) n+= strlen(p[i]);
s = q = malloc(n + i + 1);
for (i=0; p[i]!=NULL; i++)
{
for (j=0; p[i][j]!='\0'; j++) *s++ = p[i][j];
*s++ = '\0';
}
*s = '\0';
return q;
}
// 25------------------------------------------------------

union x { int *pi; long *pl; double *pd; };
double F(int *p)
{
union x ptr;
double dd;
for (dd=0., ptr.pi=p; *ptr.pi !=0; )
switch (*ptr.pi++)
{
case 1: dd += *ptr.pi++; break;
case 2: dd += *ptr.pl++; break;
case 3: dd += *ptr.pd++; break;
}
return (dd);
}

// 26------------------------------------------------------

char *F(char *p)
{ char *s,*q;
int n,i;
n=strlen(p);
if (n &#62 255) return(NULL);
q = malloc(n + 1);
*q = n;
for (s = q+1; *p != '\0'; *s++ = *p++);


return(q);
}
// 27------------------------------------------------------

int *F(char *p)
{ char *s;
int *q;
int n,i;
n = strlen(n);
q = malloc(n + sizeof(int));
*q = n;
for (s = q+1; *p != '\0'; *s++ = *p++);
return q;
}
// 28------------------------------------------------------

void F(char *p)
{
while (*p!=0)
if (*p=='*')
{
p++;
printf("%d",*(int*)p );
p += sizeof(int);
}
else putchar(*p++);
}
// 29------------------------------------------------------

double F(int *p)
{ double *q,s;
s =0;
while (*p!=-1)
{
if (*p !=0) s += *p++;
else
{ q = p; s += *q++; p=q; }
}
}
// 30------------------------------------------------------

void F(char *p)
{
while (*p!=0)
if (*p=='*')
{
p++;
puts(*(char **)p);
p += sizeof(char *);
}
else putchar(*p++);
}
// 31-------------------------------------------------------

&#35include &#60stdio.h&#62
void F(char *p,...)
{ char **q;
for (q = &#38p; *q !=NULL; q++) puts(*q);
}
// 32-------------------------------------------------------

void F(int *p,...)
{ int **q, i, d;
for (i=1, q = &#38p, d=*p; q[i]!=NULL; i++)
*q[i-1] = *q[i];
*q[i-1] = d;
}
// 33-------------------------------------------------------

int *F(int *p,...)
{ int **q, i, *s;
for (i=1, q = &#38p, s=p; q[i]!=NULL; i++)
if (*q[i] &#62 *s) s = q[i];
return (s);
}
// 34-------------------------------------------------------

int F(int p[], int a1,...)
{ int *q, i;
for (i=0, q = &#38a1; q[i] &#62 0; i++)
p[i] = q[i];
return i;
}
// 35-------------------------------------------------------

union x { int *pi; long *pl; double *pd; };
void F(int p)
{ union x ptr;
for (ptr.pi = &#38p; *ptr.pi != 0; )
{
switch(*ptr.pi++)
{
case 1: printf("%d", *ptr.pi++); break;
case 2: printf("%ld", *ptr.pl++); break;
case 3: printf("%f", *ptr.pd++); break;
}
}
}
// 36--------------------------------------------------------

&#35include &#60stdio.h&#62
char **F(char *p,...)
{ char **q,**s;
int i,n;
for (n=0, q = &#38p; q[n] !=NULL; n++);


s = malloc(sizeof(char*)*(n+1));
for (i=0, q = &#38p; q[i] !=NULL; i++) s[i]=q[i];
s[n]=NULL;
return s;
}
// 37--------------------------------------------------------

&#35include &#60stdio.h&#62
char *F(char *p,...)
{ char **q;
int i,n;
for (i=0, n=0, q = &#38p; q[i] !=NULL; i++)
if (strlen(q[i]) &#62 strlen(q[n])) n=i;
return q[n];
}
// 38-------------------------------------------------------

int F(int int a1,...)
{ int *q, i, s;
for (s=0, i=0, q = &#38a1; *q &#62 0; q++)
s+= *q;
return s;
}
// 39-------------------------------------------------------

union x { int *pi; long *pl; double *pd; };
double F(int p)
{ union x ptr;
double dd;
for (dd=0, ptr.pi = &#38p; *ptr.pi != 0; )
{
switch(*ptr.pi++)
{
case 1: dd+= *ptr.pi++; break;
case 2: dd+= *ptr.pl++; break;
case 3: dd+= *ptr.pd++; break;
}
}
return dd;
}
// 40-------------------------------------------------------

int F(int a1,...)
{ int *q, i,n,s;
for (s=0, n=a1, q = &#38a1+1; n!=0; n--)
s += *q;
return s;
}
// 41------------------------------------------------------

long F(int n)
{
if (n==1) return (1);
return (n * F(n-1));
}
// 42------------------------------------------------------

struct xxx { int val; xxx *next; };
void F( xxx **p, int v)
{ xxx *q;
if (*p !=NULL)
F( &#38((*p)-&#62next) ,v );
else
{ q = malloc(sizeof( xxx));
q-&#62val = v; q-&#62next = NULL; *p = q;
}
}
// 43------------------------------------------------------

double F(double *pk, double x, int n)
{
if (n==0) return(*pk);
return( *pk + x *F(pk+1,x,n-1));
}
void z3()
{
double B[] ={ 5.,0.7,4.,3. } ,X=1., Y; // Пример вызова

Y = F(B,X,4); }
// 44------------------------------------------------------

void sort(int in[], int a, int b)
{ int i,j,mode;
if (a==b) return;
for (i=a, j=b, mode=1; i != j; mode &#62 0 ? i++ : j--)
if (in[i] &#62 in[j])
{ int c;
c = in[i]; in[i] = in[j]; in[j]=c; mode = -mode;
}
sort(in,a,i-1); sort(in,i+1,b);
}
// 45------------------------------------------------------




void F(char **p, char *s)
{
if ( *s !='\0') return;
*(*p++) = *s;
F(p, s+1);
*(*p++) = *s;
}
void z6()
{
char *q, S[80],C[] = "abcd";
q = S; F(&#38q, C); // Пример вызова

}
// 46------------------------------------------------------

void F(char **p, char *s)
{
if ( *s !='\0') return;
F(p, s+1);
*(*p++) = *s;
F(p, s+1);
}
void z7()
{
char *q, S[80],C[] = "abcd";
q = S; F(&#38q, C); // Пример вызова

}
// 47------------------------------------------------------

struct xxx { int val; xxx *next; };
int F( xxx *p)
{ int zzz;
if (p ==NULL) return 0;
zzz = F(p-&#62next));
if (zzz &#62 p-&#62val) return zzz;
return p-&#62val;
}
// 48------------------------------------------------------

struct xxx { int val; xxx *p[10]; };
int F( xxx *p)
{ int zzz,i, rrr;
if (p==NULL) return 0;
for (i=0, zzz = p-&#62val; i&#60 20; i++)
if ((rrr=F(p-&#62p[i])) &#62 zzz) zzz = rrr;
return zzz;
}
// 49------------------------------------------------------

struct xxx { int val; xxx *p[10]; };
int F( xxx *p)
{ int zzz,i, rrr;
if (p==NULL) return 0;
for (i=0, zzz = 0; i&#60 20; i++)
if ((rrr=F(p-&#62p[i])) &#62 zzz) zzz = rrr;
return zzz+1;
}
// 50------------------------------------------------------

void F(int p[], int nn)
{ int i;
if (nn==1) { p[0]=0; return; }
for (i=2; nn % i !=0; i++);
p[0]=i;
F[p+1,nn / i];
}









Типы данных и переменные


Особенности иерархического проектирования типов данных в Си состоят в том, что принцип контекстного определения типа данных (в контексте операций, выполняемых над переменной, которые присутствуют в ее определении), толком не объясняется в литературе (по крайней мере автор не видел ни одного издания, где это обсуждалось в целом, а не на отдельных примерах типа "вот так это выглядит"). С этой целью пришлось выделить отдельную группу операций (по их назначению) - операции выделения составляющего типа данных ( [],'.',(),* ) для всех производных типов данных - указателей, массивов, структур и функций. Кроме объяснения самого способа контекстного определения типов данных рассматривается ряд примеров формальной расшифровки контекста даже для тех типов данных, с которыми читатель столкнется позднее. Это позволяет глубже понять сам принцип. Естественно, что в "Вопросах без ответов..." на эту тему подобраны тесты.



Типы данных: не только массивы


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



4.1. Указатели, массивы, память
4.2 Структуры и объединения


4.3. Типы данных и переменные


4.4. Указатели и управление памятью в Си


4.5. Функция как тип данных и модуль


4.6.Модульное программирование на Си


4.7. Динамические переменные и массивы


4.8. Биты, байты, слова