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

         

Функции посимвольного ввода-вывода



.
--------------------------------- Посимвольный ввод ----¬
¦ int fgetc(FILE *fd) - явно указанный файл ¦
¦ int getc(FILE *fd) ¦
¦ int fgetchar(void) - стандартный ввод ¦
¦ inc getchar(void) ¦
¦ L--- код символа или EOF ¦
¦ int ungetc(int ch, FILE *fd) - возвратить символ ¦
¦ в файл (повторно читается) ¦
L--------------------------------------------------------
-------------------------------- Посимвольный вывод ----¬
¦ int fputc(int ch,FILE *fd)- явно указанный файл ¦
¦ int putc(int ch, FILE *fd) ¦
¦ int fputchar(int ch) - стандартный вывод ¦
¦ inc putchar(int ch) ¦
¦ L--- код символа или EOF ¦
L--------------------------------------------------------

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



Функции построчного ввода-вывода



.
-------------------------------------- Построчный ввод -¬
¦ char *gets(char *str) - стандартный ввод ¦
¦ char *fgets(char *str, int n, FILE *fd) ¦
¦ L-- str или NULL(ошибка) ¦ - явно указанный файл ¦
¦ максимальная длина строки -- ¦
L--------------------------------------------------------
------------------------------------- Построчный вывод -¬
¦ char *puts(char *str) - стандартный ввод ¦
¦ char *fputs(char *str, FILE *fd) ¦
¦ L-- str или NULL(ошибка) - явно указанный файл ¦
L--------------------------------------------------------

Примечание: при вводе-выводе все строки функции используют в качестве стандартного ограничителя строки в памяти символ '\0'. Символ конца строки '\n' уничтожается при стандартном вводе-выводе (gets - не записывает в строку, а puts автоматически добавляет при выводе) и сохраняется в строке при вводе-выводе из явно указанного файла (fgets - записывает в строку, fputs - выводит имеющийся в строке (сам не добавляет)).



Функции позиционирования и блочного ввода-вывода



.
------------------------------------- Позиционирование ---¬
¦ void rewind(FILE *fd) - установить указатель ¦
¦ в файле на начало ¦
¦ long ftell(FILE *fd) - получить значение ¦
¦ ¦ указателя в файле ¦
¦ L-- номер байта (позиция) или -1L ¦
¦ int fseek(FILE *fd, long pos, int mode) - установить ¦
¦ ¦ ¦ ¦ указатель в файле ¦
¦ ¦ номер байта (позиция)-- ¦ на заданную позицию¦
¦ ¦ способ позиционирования --------- ¦
¦ ¦ SEEK_SET(0) - от начала ¦
¦ ¦ SEEK_CUR(1) - от текущей позиции ¦
¦ ¦ SEEK_END(2) - от конца файла ¦
¦ L---- 0 или EOF (ошибка) ¦
¦ int fgetpos(FILE *fd, long *ppos) - аналоги ftell,fseek ¦
¦ int fsetpos(FILE *fd, long *ppos) с указателем, раз- ¦
¦ мещенным в памяти ¦
L----------------------------------------------------------
------------------------------------ Блочный ввод-вывод --¬
¦ int fread(void *buff, int size, int nrec, FILE *fd) ¦
¦ int fwrite(void *buff, int size, int nrec, FILE *fd) ¦
¦ ¦ ¦ ¦ L-- число записей ¦
¦ ¦ ¦ L----------- размер записи ¦
¦ ¦ L------------------- адрес памяти ¦
¦ L----------- число введенных(выведенных) записей ¦
L----------------------------------------------------------



Функции работы с символами (Заголовочный файл - ctype.h)


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


int isalnum(int c);
// латинская буква или цифры (A-Z,a-z,0-9)


int isalpha(int c);
// латинская буква (A-Z,a-z)


int isascii(int c);
// символ ASCII - код в диапазоне 0..127


int iscntrl(int c);
// код
управляющего (неотображаемого) символа ASCII -


// 0x00..0x1F или 0x7F.




int isdigit(int c);
// десятичная цифра (0-9)


int isgraph(int c);
// видимый (отображаемый) символ ASCII (0x21-0x7E)


int ispunct(int c);
// символ - разделитель (iscntrl или isspace)


int isspace(int c);
// символ - эквивалент пробелa: пробел (0x20), горизонталь-


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


// перевод страницы, возврат каретки (0x09-0x0D)


int isupper(int c);
// символ верхнего регистра клавиатуры


int isxdigit(int c);
// символ шестнадцатеричной цифры (0-9, A-F, a-f)


int toascii(int c);
// преобразование целого в символ кода ASCII - очистка


// старших битов, начиная с 7-го


int tolower(int c);
// преобразование символа - латинской буквы верхнего


// регистра к нижнему (большой - в маленькую). Остальные


// символы не меняются


int toupper(int c);
// преобразование латинской буквы нижнего регистра к верхнему



Функция как тип данных


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



Функция main


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


void main(void) {...}



Глобальные переменныеИнициализация


Программа в целом представляет собой просто набор функций с обязательной функцией main, имеющих каждая собственный набор локальных переменных. Но кроме этого в ее состав включаются еще переменные, доступные сразу нескольким функциям. Такие переменные называются ГЛОБАЛЬНЫМИ ПЕРЕМЕННЫМИ (в Си ВНЕШНИМИ ПЕРЕМЕННЫМИ). Будучи определенными в любом месте программы вне тела функции, они становятся доступными любой функции, следующей за ней по тексту программы:


int B[10]; // B


int sum() // B


{ ...B[i]... } // B


int n; // B,n


void nf() // B,n


{...B[i]...n...} // B,n


char c[80]; // B,n,c


void main() // B,n,c


{...B[i]...n...c[k]...} // B,n,c

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

Инициализация включается в синтаксис определения переменной:


int a=5, B[10]= {1,5,4,2}, C[]={ 0,1,2,3,4,5 };

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



Границы памяти, адресуемой указателем


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



Иерархические структуры данных


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



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



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



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

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



-список, элемент которого содержит массив указателей


struct elem // Элемент односвязного списка


{
elem *next;
void *pp[20]; // Массив указателей на элементы данных


};


// Подсчет количества элементов в структуре данных


int count(elem *p)
{
elem *q; int cnt; // Цикл по списку


for (cnt=0, q=p; q!=NULL; q=q-&#62next)
{
int i; // Цикл по массиву указателей


for (i=0; q-&#62pp[i]!=NULL; i++)
cnt++;
}
return cnt; }



-массив, каждый элемент которого является заголовком списка



struct list
{
list *next;
void *data;
};
int count(list *p[])
{
int k,cnt; // Цикл по массиву заголовков списков

for (cnt=0, k=0; p[k]!=NULL; k++)
{
list *q; // Цикл по списку

for (q=p[k]; q!=NULL; q=q-&#62next)
cnt++;
}
return cnt; }


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

void **p[20]; // массив указателей

// на массивы указателей

int count(void **p[])
{
int k,cnt; // Цикл по массиву верхнего уровня

for (cnt=0, k=0; p[k]!=NULL; k++)
{
int i; // Цикл по массиву нижнего уровня

for (i=0; p[k][i]!=NULL; i++)
cnt++;
}
return cnt; }

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

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


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


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


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




//------------------------------------------------------bk57-01.cpp




// Двухуровневый массив указателей на целые .

&#35include &#60iostream.h&#62
&#35include &#60string.h&#62
&#35define N 4
int **p[20]={NULL}; // Двухуровневый массив указателей на целые

// Исходное состояние массивов нижнего уровня нет

int size(int *p[]) // Количество элементов в массиве указателей

{ for (int i=0; p[i]!=NULL; i++); return i; }

void show(int **p[]) // Обход структуры данных со сквозной нумерацией

{ int i,j,k;
for (i=0,k=0; p[i] != NULL; i++)
for (j =0; p[i][j] != NULL; j++,k++)
cout &#60&#60 k &#60&#60 " " &#60&#60 i &#60&#60 " " &#60&#60 j &#60&#60 " " &#60&#60 *p[i][j] &#60&#60 endl;
}

// Включение в массив указателей нижнего уровня по номеру.

// Проверка на переполнение

int F3(int *p[], int *q, int n)
{
int i,m=size(p);
for (i=m; i&#62=n; i--) p[i+1] = p[i];
p[n] = q;
return m+1==N;
}

// Включение по логическому номеру. Для каждого массива указателей

// нижнего уровня из логического номера вычитается количество

// указателей в текущем массиве, пока не будет найден тот,

// в который попадает новый указатель.

void F117(int **p[],int *q, int n)
{ int i,j,l,sz;
if (p[0]==NULL) // Отдельная ветка для пустой структуры данных

{
p[0]=new int*[N+1];
p[0][0]=q;
p[0][1]=NULL;
return;
} // Поиск места включения

for (i =0; p[i] != NULL; i++,n-=sz)
{
sz=size(p[i]); // Количество указателей в массиве

if (n&#60=sz) break; // Номер попадает в текущий массив

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

if (p[i]==NULL) { i--; n=size(p[i]); }
if (F3(p[i],q,n)) // Вызов функции включения для нижнего уровня

{ // Переполнение создание нового массива

// Раздвижка в массиве указателей верхнего уровня

// Создание нового массива нижнего уровня

// Перенос указателей

for (int ii=0; p[ii] != NULL; ii++);
for(int h=ii;h&#62i;h--) p[h+1]=p[h];
p[i+1]=new int*[N+1];
for(j=0;j&#60N/2;j++) p[i+1][j]=p[i][j+N/2];
p[i][N/2]=NULL;
p[i+1][N/2]=NULL;
}
}

void main()
{while(1)
{
int n, *s=new int;
cin &#62&#62 *s &#62&#62 n;
if (n&#60 0) break;
F117(p,s,n);
show(p);
}}


Иерархия и конструирование типов данных


В Си используется общепринятый принцип иерархического конструирования типов данных. Имеется набор базовых типов данных, операции над которыми включены в язык программирования. Производные типы данных конструируются в программе из уже известных, в том числе базовых типов данных. Понятно, что в языке программирования отсутствуют операции для работы с производным типом данных в целом. Но для каждого способа его определения существует операция ВЫДЕЛЕНИЯ СОСТАВЛЯЮЩЕГО ТИПА ДАННЫХ.

.


ПРОИЗВОДНЫЙ ТИП ДАННЫХ ОПЕРАЦИЯ ВЫДЕЛЕНИЯ СОСТАВЛЯЮЩЕГО ТИПА ДАННЫХ
Массив [] - элемент массива
Указатель * - косвенное обращение по указателю
Структура(объединение) . - элемент структуры
Указатель на структуру -&#62 - элемент структуры, адресуемой указателем
Функция () - вызов функции

Теперь рассмотрим известные нам операции с точки зрения взаимоотношения типов данных и переменных.


struct man A;
int n;
n = A.dd;

В программе определяется переменная производного типа данных -структурированная переменная A. Ее имя идентифицирует всю переменную в целом -область памяти с именем А, которой соответствует тип данных struct man . Операция "точка" в выражении A.dd выделяет в переменной A компоненту, которая соответствует составляющему типу данных int dd . Если рассматривать эту операция только с позиции преобразования типов данных, то в ней осуществляется переход от производного типа данных переменной А к типу данных составляющей ее компоненты -int .

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


.
БТД --- тип выражения -----op2(op1(X))
| |
определение ПТД выделение составляющего ТД
| |
ПТД1 -- тип выражения --------- op1(X)
| |
определение ПТД выделение составляющего ТД
| |
ПТД2 --------------------------- X
определение переменной

Прежде всего, в программе создается цепочка определений производных типов данных: базовый тип данных (БТД) используется для определения производного (ПТД1), который в свою очередь используется для определения другого производного типа данных (ПТД2) и т.д..
Затем определяется переменная (X), которая относится к одному из типов данных в этой цепочке (ПТД2). Под нее выделяется область памяти, которая получает общее имя (X). К этому имени могут быть применены операции выделения составляющих типов данных (op1 и op2), они осуществляют переход к внутренним компонентам составляющих переменную типов данных. Операции эти должны применяться в обратном порядке по отношению к последовательности определения типов данных. Типы полученных выражений также повторяют в обратном порядке эту последовательность.

Посмотрим, как укладывается в предложенную схему пример с пресловутой struct man:

.

struct man B[20];
char c;
c = B[i].name[j];

БТД cha r B[i].name[j]
символ | |
| | операция []
ПТД1 char[20]; B[i].name
массив символов | |
| | операция "."
ПТД2 struct ma n B[i]
структура { char name[20];...}; |
| |операция []
ПТД3 struct man B[10]; --- B
массив структур





Базовый тип char (БТД) используется для создания производного типа -
массив из 20 символов (ПТД1). Тип данных -структура (ПТД2) использует массив символов в качестве одного из составляющих ее элементов. Последний тип данных -массив из 10 структур (ПТД3) порождает переменную B соответствующего типа. Затем все происходит в обратном порядке. Операции [],"." и [] последовательно выделяют в переменной B i-ю структуру, элемент структуры name и j-й символ в этом элементе.





Если внимательно посмотреть на схему, то можно заметить, что в программе в явном виде упоминаются только два типа данных -базовый char и структура struct man . Остальные два типа -массив символов и массив структур -отсутствуют. Эти типы данных создаются "на ходу", в процессе определения переменной B и элемента структуры name . Следовательно, в Си существует некоторый способ, который позволяет наряду с определением переменной задать и ее тип. На самом деле он является основным и используется при
работе с массивами, указателями и функциями.


Индексная таблицаИндексный файл


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

Однако поддержание упорядоченности записей в файле является достаточно трудоемкой операцией. Более эффективно использовать таблицу ссылок на записи, которая упорядочивается требуемым образом. Такая таблица называется индексной или просто индексом. Сама таблица может размещаться в отдельном файле, который называется индексным файлом.

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

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

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



Индексное дерево


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

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

Второй способ основан на естественном хранении дерева в массиве в следующем виде: индексы вершин левого и правого поддерева для вершины с номером n имеют значения 2n и 2n+1 соотвественно. Однако если различные ветви дерева имеют разную длину, то такое представление неэффективно использует память. Кроме того, при выполнении сложных преобразований структуры дерева требуется выполнять копирование его вершин.

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

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


- -1 - левое поддерево на 1 длиннее правого;


- 0 - длины поддеревьев совпадают;


- 1 - правое поддерево на 1 длиннее левого.

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

В приведенном примере используется ссылка на указатель для передачи в функцию указателя на текущую вершину с возможностью его изменения:





//------------------------------------------------------bk510-01.cpp

struct tree
{
int value; // Значение элемента

tree *left,*right; // Ссылки на поддеревья

int balance; // Значение разницы длин

}; // поддеревьев

int insert( tree *&#38pp, int v) // Включение вершины со значением v

{
tree *pnew;
if (pp == NULL) // Включить на место пустой ссылки

{
pnew = new tree;
pnew-&#62right = NULL; // Правое и левое поддерево

pnew-&#62left = NULL; // пусты

pnew-&#62balance = 0; // Вершина сбалансирована

pp = pnew; // Включить новую вершину

return 1; // Длина увеличилась

}
if (pp-&#62value) &#62 v) // Включить в левое поддерево

{
if (!insert(pp-&#62left, v))
return 0 ; // Длина дерева не возросла

pp-&#62balance --; // Иначе - баланс влево

switch(pp-&#62balance)
{
case 0: return 0; // Длина уменьшилась

case -1: return 1; // Длина возросла

case -2: .... // Выполнить балансировку

} // левого поддерева

}
else // Включить в правое поддерево

{
if (!insert(pp-&#62right, v))
return 0; // Длина дерева не возросла

pp-&#62balance ++; // Иначе - баланс вправо

switch(pp-&#62balance)
{
case 0: return 0; // Длина уменьшилась

case 1: return 1; // Длина возросла




case 2: .... // Выполнить балансировку

} // правого поддерева

}
}

Варианты балансировки левого и правого поддерева абсолютно симметричны. Структура фрагмента дерева и фрагмент алгоритма балансировки приводятся ниже для правого поддерева:

C &#62 B B &#62 C
pp-&#62balance == 2 p1-&#62balance == 1 p1-&#62balance == -1
--¬ --¬ --¬
pp --&#62¦5¦ pp ---&#62¦7¦ pp-----&#62¦B¦
L-- L-- L--
-----+--¬ ----+---¬ ----+-----¬
--¬ --¬ --¬ --¬ --¬ --¬
¦A¦ ¦7¦&#60-- p1 ¦5¦ ¦C¦ ¦5¦ ¦7¦
L-- L-- L-- L-- L-- L--
-----+----¬ ---+--¬ -----+---¬ ---+---¬
--¬ --¬ --¬ --¬ --¬ --¬--¬ --¬
p2--&#62¦B¦ ¦C¦

¦A¦ ¦B¦ ¦A¦ ¦D¦¦E¦ ¦C¦

L-- L-- L-- L-- L-- L--L-- L--

D --+-- E





//------------------------------------------------------bk510-02.cpp

tree *p1,*p2;
p1 = pp-&#62 right;
if (p1-&#62balance == 1) // Вариант 1

{
pp-&#62balance = 0;
pp-&#62right = p1-&#62left; // Правый (5) = B

p1-&#62left = pp; // Левый (7) = 5

pp = p1; // Вершина поддерева = 7

}
else // Вариант 2

{
p2 = p1-&#62left; //

p1-&#62left = p2-&#62right; // Левый (7) = E

p2-&#62right = p1; // Правый (B) = 7

pp-&#62right = p2-&#62left; // Правый (5) = D

p2-&#62left = pp; // Левый (B) = 5

// Баланс (5) = 0 или -1

// Баланс (7) = 0 или 1

pp-&#62balance = -(p2-&#62balance == 1);
p1-&#62balance = (p2-&#62balance == -1);
pp = p2; // Вершина поддерева = B

}
pp-&#62balance = 0;
return 0;


Исключение парных фрагментов



//------------------------------------------------------prg1pr-04.cpp


void cut(char c[])
{
for (int i=0; c[i]!=0; i++)
{
if (c[i]==' ') continue;
for (int j=i+1; c[j]==c[i]; j++);
for (; c[j]!=0; j++)
{
for (int k=0; i+k&#60j &#38&#38 c[i+k]==c[j+k]; k++);
if (k&#62=4)
{
int m1,m2;
for (m1=j, m2=j+k; c[m2]!=0; m1++,m2++)
c[m1]=c[m2];
c[m1]=0;
j--; // j=i;


}
}
}
}


&#35include &#60iostream.h&#62
void main()
{
char c1[]="00000abcaa0000aabcabcaaa";
cut(c1);
cout &#60&#60 c1 &#60&#60 endl;
}



Итерационные процессы и циклы


Специфика итерационных циклов, приближенные вычисления, решение уравнений, степенные ряды.



Итераторы


Рассмотрим случай, когда структура данных (массив указателей, список, дерево) включают в себя переменные одного типа, но сам тип может меняться в каждом конкретном экземпляре структуры данных (например, массивы указателей на int, double, strust man ). Очевидно, что алгоритмы поиска, сортировки, включения, исключения и т.д. будут совпадать с точностью до операции над этими переменными. Например, для сортировки массивов указателей на целые переменные и строки могут использоваться идентичные алгоритмы, различающиеся только операцией сравнения двух переменных соответствующих типов (операция "&#62" и функция strcmp ). Если эту операцию реализовать отдельной функцией, а указатель на нее передавать в качестве параметра, то мы получим универсальную функцию сортировки массивов указателей на переменные любого типа данных, то есть итератор.

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

Типичными итераторами являются:



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



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



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

Ниже приводятся примеры итераторов для массива указателей и списка. Чтобы список мог содержать переменные произвольного типа, они представлены отдельными переменными, указатели на которые типа void* занесены в элементы списка.


//------------------------------------------------------bk56-02.cpp


//----- Элемент списка ------------------------------------


struct list
{
list *next; // Указатель на следующий


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


} *PH; // Пример заголовка списка


//----- Итератор: для каждого элемента списка ------------


void ForEach(list *pv, void (*pf)(void*) )
{
for (; pv !=NULL; pv = pv-&#62next)
(*pf)(pv-&#62pdata);
}
//----- Итератор: поиск первого в списке по условию ------



void *FirstThat(list *pv, int (*pf)(void*))
{
for (; pv !=NULL; pv = pv-&#62next)
if ((*pf)(pv-&#62pdata)) return pv -&#62pdata;
return NULL;
}
//----- Примеры использования итератора ------------------

//----- Функция вывода целой переменной

void print(void *p)
{ cout &#60&#60 *(int*)p; }
//----- Функция проверки целой переменной на значение &#62 0

int positiv(void *p)
{ return *(int*)p &#62 0; }

//----- Вызов итераторов для списка, содержащего указатели

// на целые переменные

void main()
{ int *pp;
ForEach(PH,print);
if ((pp = (int*)FirstThat(PH,positiv)) !=NULL)
cout &#60&#60 *pp;
}
//----- Итератор сортировки для массива указателей

void Sort(void **pp, int (*pf)(void*,void*))
{
int i,k; // сортировка по алгоритму "пузырька"

do
for (k=0,i=1; pp[i] !=NULL; i++)
if ( (*pf)(pp[i-1],pp[i]) ==0) // вызов функции сравнения

{
void *q;
k++; q = pp[i-1]; pp[i-1] = pp[i]; pp[i] = q;
}
while(k);
}
// Пример вызова итератора сортировки для массива

// указателей на целые переменные

// Функция сравнения дает "тройной" результат

// 0 - равны;

// -1 - первый меньше второго;

// 1 - первый больше второго.

int compare _int(void *p1, void *p2)
{
if (* (int*)p1 == * (int*)p2) return 0;
if (* (int*)p1 &#60 * (int*)p2) return -1;
return 1;
}
void main()
{ int a1=5, a2=6, a3=3, a4=2;
int *PP[] = {&#38a1, &#38a2, &#38a3, &#38a4, NULL};
Sort(PP,compare);
}

Из приведенных примеров просматривается общая схема итератора:


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


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


-итератор выполняет алгоритм обработки структуры данных в соответствии со своим назначением: foreach - обходит все переменные, firstthat - обходит и проверяет все переменные, итератор сортировки -сортирует переменные (или соответствующие элементы структуры данных, например, списка);

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


Итоговый экзамен (семестр)


1. Итоговый экзамен по информатике состоит в разработке программы на Си++.

2. Тематика задач, выносимых на экзамен, соответствует разделам курса Информатика:



- 4.4 - данные произвольного формата;



- 4.5 - функции с
переменным количеством параметров;



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



- 5.4 - рекурсия;



- 5.2, 5.3, 5.5, 5.7 - структуры данных;



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

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

При подготовке к экзамену можно использовать соответствующие варианты лабораторных работ 2,3,4 семестров.

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

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

5. Пример задачи. Класс разреженных матриц. Матрица представлена динамическим массивом ненулевых коэффициентов (строка, столбец - int, значение - double).
Переопределить операцию сложения матриц, результат - объект-значение, операнды не меняются.
Комментарии : объект разреженная матрица представлен динамическим массивом структурированных переменных elem. Elem содержит " координаты" коэффициента и его значение. Кроме того объект содержит текущую размерность массива количество ненулевых коэффициентов. Частная проблема при сложении матриц определение размерности выходного массива коэффициентов. Сложение происходит в два этапа сначала определяется размерность выходных данных, а затем происходит само формирование выходного массива коэффициентов. Алгоритм сложения : создается массив коэффициентов в промежуточном (выходном) объекте. Затем в него переносятся коэффициенты первого объекта. Для каждого из коэффициентов второго объекта проверяется, есть ли соответствующий ему в первом объекте. Если есть, то коэффициент суммируется с коэффициентом первого объекта, иначе добавляется в выходной объект. Поскольку операция возвращает копию (значение) объекта-результата и объект имеет динамические данные, обязателен конструктор копирования. Используется также служебный метод (приватный), производящий поиск заданного коэффициента и возвращающий его индекс в динамическом массиве. Сложение корректно выполняется для всех случаев, в том числе и для полностью нулевых матриц. Отсутствует реакция объектов на отсутствие динамической памяти в операторе new.


struct elem{
int x,y; // координаты коэффициента

double k; // значение коэффициента

};

class matrix{
elem *pd; // Динамический массив коэффициентов

int sz; // Размерность массива

int find(int,int); // Поиск позиции коэффициента

public:
matrix(matrix&#38); // Конструктор копирования

matrix(); // Конструктор нулевой матрицы

~matrix();
matrix operator+(matrix&#38); // Переопределенная операция сложения

};

matrix::matrix(){
sz=0; pd=new elem; // Для единообразия объектов выделяем память

}

matrix::~matrix(){ // Деструктор

delete pd;
}

matrix::matrix(matrix &#38two){ // Конструктор копирования



pd = new elem[two.sz];
sz=two.sz;
for (int i=0; i&#60sz; i++) pd[i]=two.pd[i];
}

// Личный метод поиска " места расположения" коэффициента

int matrix::find(int xx, int yy){
for (int i=0; i&#60sz; i++)
if (pd[i].x==xx &#38&#38 pd[i].y==yy) return i;
return 1;
}

matrix matrix::operator+(matrix &#38two){
int newsz=sz; // Размерность выходного массива

for (int i=0; i&#60two.sz; i++) // Если коэффициент из второго отсутствует

if (find(two.pd[i].x, two.pd[i].y)==-1)
newsz++; // в первом - увеличить размерность на 1

matrix out(); // Выходной объект

delete out.pd; // При создании не пустой

out.pd = new elem[newsz];
for (i=0; i&#60sz; i++) // Копировать первый в выходной

out.pd[i]=pd[i];
out.sz=sz;
for (i=0; i&#60two.sz; i++) // Если коэффициент из второго присутствует

{ // в выходном сложить, иначе добавить.

int nn=out.find(two.pd[i].x, two.pd[i].y);
if (nn!=-1)
out.pd[nn]+= two.pd[i];
else
out.pd[out.sz++] = two.pd[i];
}
return out;
}

// Вариант 2. Можно использовать в качестве временного

// объекта второй операнд, если передавать его по значению

matrix matrix::operator+(matrix two){
int newsz=sz; // Размерность выходного массива

for (int i=0; i&#60two.sz; i++) // Если коэффициент из второго отсутствует

if (find(two.pd[i].x, two.pd[i].y)==-1)
newsz++; // в первом - увеличить размерность на 1

// Перераспределить память во втором объекте

two.pd=realloc(two.pd, sizeof(elem)*newsz;
for (i=0; i&#60sz; i++) // Если коэффициент из второго присутствует

{ // в выходном сложить, иначе добавить.

int nn=two.find(pd[i].x, pd[i].y);
if (nn!=-1)
two.pd[nn]+= pd[i];
else
two.pd[two.sz++] = pd[i];
}
return two;
}

Избыточность в двоичных файлах и защита от ошибок


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



Явное преобразование типа


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


double x,d; // double x,d; int n;


d = x - (int)x; // n = x; d = x - d;



Явное преобразование типа указателя " на лету"


Рассмотрим три операции присваивания:


char *p, A[20];
int *q; p = A;
q = (int*)p; p[2] = 5; // 1


(q = ( int*)p) [2] = 5; // 2


((int *)p )[2] = 5; // 3


// p - тип указателя char* до преобразования


// (int*) - явное преобразование указателя


// на байт в указатель на целое


// (int*)p -тип указателя int* после преобразования

Все они дают одинаковый результат -записывают целое 5 во второе слово области памяти, определенной как массив байтов (символов) с именем A. Однако, если в первом и втором случае используется промежуточный указатель типа int*, то в последнем случае такой указатель создается как рабочая переменная, которая получает тип int* и значение переменной p . Такая операция называется явным преобразованием типа указателя. В скобках указывается абстрактный тип данных - указатель на требуемый тип (например, int* ).



Экзаменационные билеты ( семестр )


по курсу "Информатика: алгоритмические языки и программирование" (семестр 1).

Билет 1.

1. Двоичная система счисления. Формы представления целых чисел со знаком и без знака, плавающая точка. Представление текста.

2. Оставить в строке не более чем по одному пробелу между словами.

Билет 2.

1. Представление отрицательных чисел. Дополнительный код.

2. Упорядочить массив в порядке возрастания (сортировка выбором). Ищется минимум из 0..n элементов и переставляется на первое место, ищется минимум из 1..n элементов и переставляется на второе и т.д..

Билет 3.

1. Базовые типы данных в Си: char,int,long,float,double. Модификатор типа unsigned. Особенности типа данных char. Связь с формами представления данных в ЭВМ.

2. Заменить в строке маленькие латинские буквы на большие.

Билет 4.

1. Понятие массива. Особенности определения и использования массивов в Си. Понятие индекса.

2. Подсчитать в строке количество фрагментов, симметричных относительно центрального символа (например, "abcba"), и содержащих не менее 5 символов.

Билет 5.

1. Массивы символов, строки. Работа со строками в Си.

2. Упорядочить массив в порядке возрастания методом пузырька. Сравниваются элементы 0-1,1-2,2-3,...,n-2-n-1 и переставляются, если они не в порядке возрастания. Цикл повторяется, пока число перестановок при просмотре не станет равным 0.

Билет 6.

1. Понятие переменной. Глобальные (внешние) и локальные (автоматические) переменные программы. Определение типа переменных. Инициализация простых переменных и массивов.

2. Преобразовать строку символов, содержащую число в десятичной системе в двоичную систему (целое типа int).

Билет 7.

1. Программа на Си как совокупность переменных и функций. Общая схема синтаксиса функции. Заголовок, результат, формальные параметры, тело функции. Фактические параметры.

2. Найти подстроку в строке. Строка и подстрока заданы в массивах символов. Результат - индекс начала подстроки в строке или -1.

Билет 8.

1. Формальные и фактические параметры функции.
Способ передачи параметров функции в Си. Использование стека при вызове функции.

2. Подсчитать количество повторений наиболее часто встречающейся латинской буквы в заданной строке.



Билет 9.

1. Операции. Выражения. Классификация операций: арифметические, сравнения, логические, машинно-ориентированные, выделения составляющего типа данных. Особенности выполнения операций на Си: приоритеты, направление выполнения.

2. Двоичный поиск индекса (позиции) заданного значения в упорядоченном массиве.



Билет 10.

1. Явные и неявные преобразования типов данных в выражениях.

2. Подсчитать количество слов в строке. Слово - любая последовательность символов, кроме пробела.



Билет 11.

1. Операторы. Роль символа ";". Основные операторы в Си: if, while, do-while, for, switch, continue, break, return, goto. Вложенность операторов.

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



Билет 12.

1. Арифметические операции. "Смысл" операций "%" и "/". Работа с цифрами числа. Особенности выполнения операции присваивания. Ошибки преобразования типов данных в арифметических операциях.

2. Сортировка погружением: если имеется уже отсортированная часть массива, то очередной элемент справа от нее погружается (обменом) до тех пор, пока не натолкнется на меньший элемент.



Билет 13.

1. Структуры данных. Использование массива для представления структур данных: последовательность (строка), стек, очередь.

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



Билет 14.

1. Работа со строками в Си как с последовательностями. Примеры: поиск конца строки, просмотр строки, копирование строки, добавление символа к строке. Стандартные функции ввода-вывода символов и строк.

2. Определить в массиве максимальную длину последовательности расположенных подряд возрастающих значений.





Билет 15.

1. Трансляция и ее фазы: препроцессор, лексический, синтаксический и семантический анализ, генерация кода. Понятие компилятора и интерпретатора.

2. Сортировка подсчетом. Для заданного элемента массива его место в выходном массиве определяется количеством элементов, меньших его во входном (учесть наличие одинаковых).



Билет 16.

1. Модульное программирование.
Объектный модуль, библиотека, связывание, компоновщик (LINK).

2. Преобразовать дробную часть переменной типа double в строку символов (путем последовательного умножения дробной части на 10).



Билет 17.

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

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



Билет 18.

1. Понятие стека и очереди как способов организации данных. Представление стека и очереди в массиве.

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



Билет 19.

1. Циклические программы. Итерационный цикл. Программы вычисления суммы ряда, корня функции.

2. Найти в строке максимальное количество одинаковых подряд идущих символов (например, "aaaaaaa").



Билет 20.

1. Сортировка и поиск. Линейный и двоичный поиск. Сортировка. Классификация алгоритмов сортировки. Трудоемкость линейного поиска, двоичного поиска и сортировки.

2. Найти наименьшее общее кратное для всех элементов массива - минимальное число, которое делится на все элементы массива без остатка.



Билет 21.

1. Понятие слияния упорядоченных последовательностей. Сортировка слиянием. Сортировка путем однократного слияния.

2. Сортировка однократным слиянием. Массив из N элементов разделяется на M частей, каждая из которых сортируется независимо, затем данные сливаются обратно во входной массив.



Билет 22.

1. Понятие слияния упорядоченных последовательностей. Сортировка слиянием.


Сортировка путем циклического распределения-слияния.

2. Написать функцию проверки, является ли заданное число простым. С ее помощью написать программу поиска простых чисел в диапазоне 1000-2000, две любые части которого - также простые (например, 1997, 1-997,19-97,199-7)



Билет 23.

1. Сортировка разделением. Понятие медианы. Рекурсивный характер сортировки разделением. "Быстрая сортировка".

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



Билет 24.

1. Основы анализа программ. "Смысл" переменных в базовых фрагментах -признак, счетчик, накопитель, максимум (примеры). "Смысл" переменных при завершении циклов.

2. Написать программу поиска чисел в диапазоне 100-10000, для которых куб суммы цифр равен значению самого числа (например 512 - 5+1+2=8).



Билет 25.

1. Последовательность как структура данных. Основные операции над последовательностью (добавление, удаление, включение).

Удалить из строки все комментарии вида /*...*/.














Эпизодическое объектно-ориентированное программирование


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



Кэширование структур данных при работе с файлами


Поэлементная загрузка данных из файла хороша в том случае, когда речь идет об отдельной операции над файлом. В этом случае при минимальных затратах используемой памяти получается минимальная же скорость доступа к данным. Очевидно, что путем сохранения наиболее часто используемых данных в памяти программы можно значительно увеличить скорость доступа к файлу. Такой способ организации доступа к данным называется КЭШИРОВАНИЕМ.

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

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



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



- виртуальная память представляет собой виртуальное адресное пространство адресов, в которое отображаются данные из " файлов подкачки" и библиотек. В этом случае обычная оперативная память играет роль КЭШ-памяти по отношению к виртуальной ;



-PROXY- сервер Интернет играет роль КЭШ-памяти для наиболее часто используемых страниц и " прозрачен" по отношению к системе адресации, принятой в сети.

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



//------------------------------------------------------bk59-12.cpp

// Массив указателей - кэширование строк

char **pcash; // Массив указателей в памяти (кэш)

long *fcash; // и в файле

int size; // Размерность массива указателей

int nstr; // Количество загруженных строк

FILE *fd;
//---- Загрузка управляющих структур

void load(char *name)
{
int i,n;
if ((fd=fopen(name,"rb"))==NULL) return;
fread((void*)&#38 size ,sizeof(int),1,fd); // Прочитать размерность

fcash=new long[size]; // Создать динамический массив

pcash=new char*[ size]; // файловых указателей и указателей

fread((void*)pp,sizeof(long), size ,fd); // на строки.

for (i=0; i&#60n; i++) pcash[i]=NULL;
nstr=0;
}
//---- Загрузка строки с кэшированием

char *load_cash(int n)
{
if (pcash[n]!=NULL) return pcash[n]; // Строка уже в кэш-памяти

if (nstr==MAX)
{ /* Вытеснение лишней строки из памяти * /
nstr--;
}
nstr++;
int sz;
fseek(fd,fcash[i],SEEK_SET); // Установиться по i-му файловому

fread((void*)&#38sz,sizeof(int),1,fd); // указателю и прочитать запись

p cash[n]=new char[sz]; // переменной длины - строку

fread((void*)p cash[n],sz,1,fd);
return pcash[n];
}

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


Класс: тип данных, определенный программистом


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



Классификация операций


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



-арифметические ( +,-,*,/,% );



-логические ( &#38&#38, ||, ! );



-сравнения ( &#60,&#62,&#62=, =,==,!=); &#60/b



-машинно-ориентированные (операции над машинными словами, поразрядные ( &#38,|,^,~,&#60&#60,&#62&#62 );



-присваиваниe (=,++,--,+=,-=,*-,/= и т.д.);



-работa с указателями и памятью (*,&#38,sizeof);



-выделение составляющего типа данных ( (),*,[], . , -&#62 );



-явноe преобразованиe типа ( (тип) );



-условная ( ?: );



-последовательность выражений ( ","-запятая).



КЛАССЫ ДОПОЛНИТЕЛЬНЫХ ТИПОВ ДАННЫХ


Варианты классов:

1. Целые произвольной длины, представленные в двоичном виде.

2. Целые произвольной длины, представленные в виде строки символов (цифр).

3. Строки произвольной длины.

4. Матрицы произвольной размерности, представленные динамическим массивом коэффициентов.

5. Разреженные матрицы произвольной размерности, представленные динамическим массивом ненулевых коэффициентов (в виде "координат" и значения коэффициента).

6. Степенной многочлен произвольной степени.

Варианты переопределяемых операций:

1. Возврат содержимого объекта в динамическом массиве (байтов, символов, коэффициентов).

2. Ввод из строки символов.

3. Сложение с int.

4. Сложение с объектом того же класса.

5. Выделение компоненты объекта (символа, коэффициента) (операция []).

6. Формирование ссылки на компоненту объекта. (операция ()).

7. Получение размерности объекта (операция int).

8. Конструктор копирования объекта.

9. Конструктор объекта из символьной строки, целого, массива коэффициентов.

10.Операция присваивания.

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

12.Ввод из двоичного файла произвольного доступа.

Варианты сохранения результата операции:

1. Результат возвращается в новом объекте, операнды не меняются.

2. Результат возвращается в виде ссылки на один из измененных операндов.



Классы памяти и области действия переменных


Переменные в программе обладают различными свойствами в зависимости от места и способа их определения:



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



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

Областью действия переменной могут быть:



-тело функции или блока, ограниченное скобками;



-текущий модуль от места определения или объявле ния переменной до конца модуля;



-все модули программы.

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



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



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



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

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

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

Синтаксис определения: любая переменная, определенная в начале тела функции или блока (после "{"), по умолчанию является автоматической.


Автоматическая переменная, в определении которой присутствует служебное слово register, становится РЕГИСТРОВОЙ: транслятор по возможности стремится хранить ее в регистрах процессора, что увеличивает быстродействие выполняемых над ней операций:



{ register char *p; // С памятью нужно работать ...

*p++; // эффективно



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

Синтаксис определения: любая переменная, определенная вне тела функции, по умолчанию является внешней.

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

.

Файл a.c Файл b.c

.

определение переменной объявление переменной
int a,B[20]={1,5,4,7}; extern int a,B[];
... область действия ... ... область действия ...



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

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

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

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


Классы потокового ввода-вывода



ios - базовый потоковый класс
streambuf - буферизация потоков
istream - потоки ввода
ostream - потоки вывода
iostream - двунаправленные потоки
iostream_withassign - поток с переопределенной операцией присваивания
istrstream - строковые потоки ввода
ostrstream - строковые потоки вывода
strstream - двунаправленнные строковые потоки
ifstream - файловые потоки ввода
ofstream - файловые потоки вывода
fstream - двунаправленные файловые потоки



Стандартные потоки (istream,ostream,iostream) - для работы с терминалом.



Строковые потоки (istrstream, ostrstream, strstream) - для ввода-вывода из строковых буферов, размещенных в памяти.



Файловые потоки (ifstream, ofstream, fstream) - для работы с файлами.



Классы структур данных


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



Конструктор копирования объектов


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


dat date2 = date1;

Однако имеются случаи, в которых создание объекта без вызова конструктора осуществляется неявно:



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



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

Во всех трех случаях транслятор не вызывает конструктора для вновь создаваемого объекта:



-dat2 в приведенном определении;



-создаваемого в стеке формального параметра;



-временного объекта, сохраняющего значение, возвращаемое функцией.

Вместо этого в них копируется содержимое объекта-источника:



-dat1 в приведенном примере;



-фактического параметра;



-объекта -результата в операторе return.

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


//------------------------------------------------------bk73-06.cpp


//------Конструктор копирования для класса строк


class string
{
char *Str;
int size;
public:
string(string&#38); // Конструктор копирования


};
string::string(string&#38 right) // Создает копии динамических


{ // переменных и ресурсов


s = new char[right-&#62size];
strcpy(Str,right-&#62Str);
}

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



Контекстная замена


Контекстная замена -поиск и замена в строке фрагментов, заданных одной строкой (контекста) на фрагмент, заданный другой, например:


Исходная строка Заменяемая Заменяющая Результат
строка строка
"abc234pe4234e" "234" "_00_" "abc_00_pe4_00_e"

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

Шаг 1: Исходные данные и результат. Строки заданы массивами символов s,s1,s2. Результирующая строка размещается в том же массиве, что и исходная. Контроль размерности не производится:


void Context(char s[], char s1[], char s2[]) {...}

Шаг 2: Основной цикл программы состоит в посимвольной проверке строки на вхождение в нее подстроки, начиная с текущего символа. В случае вхождения производится замена:


void Context(char s[], char s1[], char s2[])
{
int n;
for (n=0; s[n] !='\0'; n++)
{
// Если начиная с n-го символа расположена


// подстрока s1, заменить ее на s2 в строке


}
}

Шаг 3: Проверка утверждения, что начиная с n-го символа в строке s расположена подстрока s2:


int i;
for (i=0; s[n+i] !='\0' &#38&#38 s1[i] !='\0'; i++)
if (s[n+i] != s1[i]) break;
if (s1[n+i]=='\0')
{
// заменить s1 на s2 в строке, начиная с s[n]


}

Шаг 4: Замена подстроки s1 на s2, начиная с n-го символа строки s, заключается перемещении "хвоста" строки s вправо или влево в зависимости от знака разности длин строк и в переписывании строки s2 на место строки s1. Для получения длины строки воспользуемся стандартной функцией strlen:


int l2,dd,k;
l2 = strlen(s2)
dd = l2 - strlen(s1);
if (dd != 0)
{
// сдвинуть "хвост" строки s на dd символов


}
for (k = 0; k &#60 l2; k++)
s[n+k] = s2[k];

Шаг 5: Для сдвига всего "хвоста" (для простоты, начиная с n-го символа) влево (dd &#60 0) необходимо все символы, начиная с n+dd перенести на dd позиций влево. Для перемещения вправо нужно сначала найти конец строки, чтобы от него производить перемещения символов с убывающим индексом


if (dd &#60 0)
{
for (k=n; s[k+dd]!='\0'; k++)
s[k] = s[k+dd];
s[k]='\0';
}
else
{
for (k=n; s[k]!='\0'; k++);
for (; k != n; k--)
s[k+dd] = s[k];
}

Примечание: в случае удачной замены проверка возможности последующей замены производится, начиная со следующего символа. Тогда при наличии замен вида "nnn" на "nnnn..." программа будет расширять строку до бесконечности.



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


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


A.name[k]

мы несомненно имеем дело со структурированной переменной (операция "." ), элементом которой является массив name (операция [] ). Такой способ задания типа переменной в окружении (контексте) операций выделения составляющего типа будем называть контекстным определением типа. В нем могут использоваться только следующие операции:



- [] -элемент массива;



- * -косвенное обращение по указателю;



-() -вызов функции;



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

Заметим, что операции ("." и -&#62), соответствующие структуре, здесь не упоминаются. Это значит, что структурированный тип данных может быть определен только явно. Это связано с тем, что структура конструируется из разнородных элементов, а остальные типы данных строятся на основе однородных.

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


int *p;



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


char *p[];



Переменная, которая является массивом, при косвенном обращении к элементу которого получаем указатель на символ (строку) - массив указателей на символы (строки).


char (*p)[][80];



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



int (*p)();




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

int (*p[10])();




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

char *(*(*p)()));


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



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


-определение и описание переменных;


-формальные параметры функций;


-результат функции;


-определение элементов структуры (
struct);


-определение абстрактного типа данных;


-
определение типа данных (спецификатор typedef).




Контроль преобразования типов указателей


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


extern void* malloc(int n);
dat *p;
p = (dat*) malloc (10*sizeof(dat)); // преобразование void* в dat*

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



Лаборато р ные занятия, их содержание, объем в часа х ( часа)


1. . Оболочка Borland C , ввод, трансляция и выполнение готовой программы (4 часа).

2. . Анализ, ввод и отладка фрагментов программ с использованием переменных - счетчиков, накоп и телей и максимумов (bk23.doc ) (4 часа).

3. . Анализ, ввод и отладка фрагментов программ, работающих с цифрами числа (bk23.doc ) (4 часа).

4. . Анализ, ввод и отладка фрагментов программ с проверкой свойств делимости (bk23.doc ) (4 часа).

5. . Проектирование программ решения арифметических задач (bk31.doc ) (4 часа).

6. . Работа со строками (bk34.doc ) (4 часа).

7. . Итерационные циклы. Программа вычисления суммы ряда (bk33.doc) (4 часа).

8. . Сортировка и поиск. Разработка алгоритма сортировки (bk37.doc ) (4 часа).


Все лабораторные работы - 4-часовые.

1. . Указатели ( bk41.doc ) .

2. . Структуры, массивы структур и указатели ( bk42.doc ) .

3. . Указатели и данные переменного формата ( bc44.doc ) .

4. . Функции с переменным количеством параметров ( bk45.doc ) .

5. . Машинно-ориентированные операции и работа с битами (bk35.doc).

6. . Массивы указателей ( bk52.doc ) .

7. . Списки ( bk53.doc ) .

8. . Заключительное занятие.




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

· Целые произвольной длины, представленные строкой цифр.

· Целые произвольной длины во внутреннем двоичном представлении (ди намический массив байтов).

· Матрица переменной размерности.

· Разреженная матрица переменной размерности, ненулевые элементы представлены динамическим массивом с элементами

· Смешанные числа.

· Дата, время.

· Группа строк, последовательно расположенная в памяти и ограниченная двойным 0.

· Разреженная матрица переменной размерности, ненулевые элементы представлены односвязным списком.

2. Переопределение операций (в разработанном классе).

3. Разработка класса структур данных и набора стандартных методов :

· Двоичное дерево, содержащее массив указателей.

· Дерево, содержащее указатель на элемент данных и массив указателей на потомков.

· Двухуровневый массив указателей.

· Массив указателей на односвязные списки.

· Список, каждый элемент которого содержит массив указателей.

· Двоичное дерево, содержащее односвязный список.

· Массив указателей на двоичные деревья.

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

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

6. . Разработка абстрактного базового класса объектов, хранимых в структуре данных.

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

8. . Разработка демонстрационной программы.




Все лабораторные работы - 4-часовые.

1. . Рекурсия ( bk54.doc ) .

2. . Деревья ( bk55.doc ) .

3. . Указатели на функции. Итераторы ( bk56.doc ) .

4. . Иерархические структуры данных ( bk58.doc ) .

5. . Текстовые файлы с элементами прямого доступа (bk57.doc).

6. . Двоичные файлы произвольного доступа ( bk57.doc ) .

7. . Обработка прерываний и резидентные программы ( bk83.doc ) .

8. . Заключительное занятие.



Линейная рекурсия


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


// Рекурсивный алгоритм


int fact(int n)
{
if (n==1) return 1;
return n * fact(n-1);
}
// Циклический алгоритм


int fact(int n)
{
for (int s=1; n!=0; n--) s *=n;
return s;
}



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


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



Линейный кроссворд


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

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



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



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



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

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

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


char *w[]={"РАСИСТ","МАТРАС","МАСТЕР","СИСТЕМА","СТЕРВА",
NULL}; // Множество слов - глобальные данные


int step(char *lw) // параметр - текущее слово цепочки



{ int n; // результат - можно присоединить

// оставшиеся

// проверка условия завершения рекурсии

// - все слов из w[] присоединены

for (n=0; w[n]!=NULL;n++)
{ // проверка на присоединение

char *pw; // очередного слова

if (*w[n]==0) continue;
pw=w[n]; // пустое слово - пропустить

w[n]=""; // исключить проверяемое слово из

// множества

{ // попытка присоединить слово

if (step(pw)) // - рекурсивный вызов

{ ...
return 1; // удача - завершить успешно

}
}
w[n]=pw; // возвратить исключенное слово

} // неудача - нельзя присоединить

return 0; // ни одного слова

}

Данный " набросок" не содержит некоторых частностей, которые не меняют общей картины :


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


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


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





//------------------------------------------------------bk54-03.cpp

&#35include &#60iostream.h&#62
&#35include &#60string.h&#62
char *w[]={"РАСИСТ","МАТРАС","МАСТЕР","СИСТЕМА","СТЕРВА",
NULL};

int step(char *lw) // параметр - текущее слово цепочки

{ int n;
for (n=0; w[n]!=NULL;n++)
if (*w[n]!=0) break;
if (w[n]==NULL) // цепочка выстроена, все слова

return 1; // из w[] присоединены

for (n=0; w[n]!=NULL;n++)
{ // проверка на присоединение

char *pw; // очередного слова

if (*w[n]==0) continue;
pw=w[n]; // пустое слово - пропустить

w[n]=""; // исключить проверяемое слово из

if (TEST(lw,pw)) // множества

{ // попытка присоединить слово

if (step(pw)) // присоединено - попытка вывести




{ // цепочку из нового слова,

cout &#60&#60 pw &#60&#60 "\n";
return 1; // удача - вывести слово и выйти

}
}
w[n]=pw; // возвратить исключенное слово

}
return 0;
}

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



int TEST(char *s, char *r)
{ int n,k;
n=strlen(s);
if (n==0) return 1;
for (;*s!=0 &#38&#38 n&#62 1; s++,n--)
if (strncmp(s,r,n)==0) return 1;
return 0; }





Другая техническая проблема - удобство первоначального запуска рекурсии. Функция TEST при первом параметре - пустой строке возвращает
ИСТИНУ при любом виде второй строки. Этого достаточно, чтобы запустить первый шаг рекурсии. При наличии пустой строки в качестве параметра функции step на первом шаге рекурсии будет производиться безусловная проверка каждого слова на предмет создания цепочки.

void main() { step(""); }


Макроопределения и функции проверки флагов состояния



.
--------------------------------------------------------¬
¦ &#35define ferror(pf) ((pf)-&#62flags &#38 _F_ERR) ¦
¦ &#35define feof(pf) ((pf)-&#62flags &#38 _F_EOF) ¦
¦ &#35define fileno(pf) ((pf)-&#62fd) ¦
¦ void clearerr(FILE *pf) - сбросить _F_EER и _F_EOF ¦
L--------------------------------------------------------



Манипуляторы


Манипуляторы - функции потока, которые можно включать в операции помещения и извлечения в потоки ( &#60, &#62).


endl // Помещение в выходной поток символа


// конца строки '\n' и вызов функции flush


ends // Помещение в выходной поток символа '\0'


flush // Вызов функции вывода буферизованных данных


// в выходной поток


dec // Установка основания 10 системы счисления


hex // Установка основания 16 системы счисления


oct // Установка основания 8 системы счисления


ws // Установка игнорирования при вводе пробелов


setbase(int) // Установка основания системы счисления


// (0 - 10 - по умолчанию, также 8,10,16)


resetiosflasg(long)
// Сброс флагов форматирования по маске


setiosflags(long)
// Установка флагов форматирования по маске


setfill(int) // Установка заполняющего символа


setprecision(int)
// Установка точности вывода вещественных


// чисел


setw(int) // Установка ширины поля ввода-вывода

Пример вызова манипулятора:


cout &#60&#60 15 &#60&#60 hex &#60&#60 15 &#60&#60 setbase(8) &#60&#60 15;



Машинная арифметика - целые произвольной точности


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

Операция сложения выполняется побайтно. Возникающий при сложении двух байтов перенос (8-й бит с маской 0x0100) используется в операции сложения следующих двух байтов:


//------------------------------------------------------bk48-02.cpp


//------Сложение целых произвольной разрядности


void add(unsigned char out[], unsigned char in1[],
unsigned char in2[], int n)
{int i;
int carry; // Бит переноса


unsigned w; // Рабочая переменная для сложения двух байтов


for (i=0, carry=0; i&#60n; i++)
{
out [i] = w = in1[i]+in2[i]+carry;
carry = (w &#38 0x0100) &#62&#62 8;
}}

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


//------------------------------------------------------bk48-03.cpp


//------Сдвиг целых произвольной разрядности


void lshift(unsigned char in[], int n)
{ int carry; // Бит переноса


int i,z;
for (carry=0, i=0; i&#60n; i++)
{
z=(in[i] &#38 0x80)&#62&#62 7; // Выделить старший бит (перенос)


in[i] &#60&#60= 1; // Сдвинуть влево и установить


in[i] |=carry; // старый перенос в младший бит


carry = z; // Запомнить новый перенос


}}
void rshift(unsigned char in[], int n)
{
int carry; // Бит переноса


int i,z;
for (carry=0, i=n-1; i&#62=0; i--)
{
z = in[i] &#38 1; // Выделить младший бит (перенос)



in[i] &#62&#62= 1; // Сдвинуть вправо и установить

in[i] |= carry &#60&#60 7; // старый перенос в старший бит

carry = z; // Запомнить новый перенос

}}

В переменной carry запоминается значение старшего (младшего) бита, который переносится в следующий байт на место младшего (старшего).

В операции умножения реализован самый простой алгоритм сложения и сдвига. В множителе bb подряд просматриваются все разряды, начиная с младшего (путем одноразрядного сдвига его вправо). Множитель при этом каждый раз сдвигается на 1 разряд влево (умножается на 2). Если очередной разряд множителя равен 1, то текущее сдвинутое значение множимого добавляется к произведению. Чтобы не усложнять программу, значения множимого и множителя не сохраняются:



//------------------------------------------------------bk48-04.cpp

//------Умножение целых произвольной разрядности

void mul(unsigned char out[], unsigned char aa[],
unsigned char bb[], int n)
{
int i;
for (i=0; i&#60n; i++) out[i]=0;
for (i=0; i&#60 n* 8; i++)
{ // Цикл по количеству битов

if (bb[0] &#38 1 ) // Разряд множителя равен 1

add(out,out,aa,n); // Добавить множимое к произведению

lshift(aa,n); // Множимое - влево

rshift(bb,n); // Множитель - вправо

}}




Машинная зависимость в языках высокого уровня


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



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



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



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

Рассмотрим в качестве примера простую строку программы и перечислим причины ее машинной зависимости


char far *p = (char far*)0xB8000000;

Здесь дано определение переменной - указателя на область байтов, который инициализирован шестнадцатеричной константой :



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



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



Массив как производный тип данных


МАССИВ - - упорядоченная последовательность переменных одного и того же типа, имеющая общее имя.

Номер элемента в последовательности называется ИНДЕКСОМ . В языке Си количество элементов в массиве должно быть определено во время трансляции (задано константой) и не может быть изменено в процессе выполнения программы. Элементы массива размещаются в памяти последовательно и нумеруются от 0 до n-1, где n -их количество в массиве. Пример определения массива :

.


int а[20] = {1,5,4,7}; где
int - тип данных элементов массива
a - имя массива
20 - количество элементов массива
1,5,4,7 - инициализация (начальные значения первых 4 элементов, остальные 0)

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



Массив указателей


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


class MU
{
int sz; // Текущая размерность МУ


void **p; // Динамический МУ на элементы


// данных произвольного вида


int extend(); // Увеличить размерность МУ


public:
MU(int); // Конструктор - создание МУ


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


int size(); // Количество элементов данных


void *operator[](int); // Извлечение по логическому номеру


int operator()( void*,int);
// Включение по логическому номеру


void *remove(int); // Удалить по логическому номеру


void *min( int(*)(void*.void*));
// Итератор поиска минимального


};

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


MU::MU(int n=20)
{ sz=n; p=new void*[n]; p[0]=0; }


MU::~MU()
{ delete p; }


int MU::extend()
{
void **q=new void*[sz*2];
if (q==NULL) return 0;
for (int i=0; i&#60sz; i++) q[i]=p[i];
delete p;
sz*=2;
return 1;
}

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


int MU::size()
{ int i; for (i=0; p[i]!=NULL; i++); return i; }


void *MU::operator[](int n=-1)
{ // Извлечение по логическому номеру


int k=size(); // По умолчанию - последний


if (n==-1) n=k-1;
if (n&#60 0 || n&#62=k) return NULL; // Недопустимый номер


return p[n];


}

int MU::operator()(void *q, int n=-1)
{ // Включение по логическому номеру

int k=size();
if (n==-1) n=k; // По умолчанию - последним

if (n&#60 0 || n&#62k) return 0; // Недопустимый номер

if (k==sz-1) extend(); // При переполнении - увеличить размерность

for (int i=k; i&#62=n; i--) p[i+1]=p[i];
p[n]=q;
return 1;
}

void *MU::remove(int n=-1) // Удалить по логическому номеру

{
int k=size();
if (n==-1) n=k-1; // По умолчанию - удалить последний

if (n&#60 0 || n&#62=k) return NULL;
void *q=p[n];
for (;p[n]!=NULL; n++) // "Уплотнить" массив указателей

p[n]=p[n+1];
return q;
}

void *MU::min( int (*cmp)(void*,void*))
{ // Итератор поиска минимального

void *pmin;
int i;
for (i=0, pmin=p[0]; p[i]!=NULL; i++)
if ( (*cmp)(pmin,p[i]) &#62 0) pmin=p[i];
return pmin;
}


Массив указателей в файле


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

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


//------------------------------------------------------bk59-08.cpp


void save(char *p[], char *name)
{
FILE *fd;
int i,n;
long *pp;
if ((fd=fopen(name,"wb"))==NULL) return; // Создать двоичный файл


for (n=0; p[n]!=NULL; n++); // Определить размерность МУ


pp=new long[n]; // Создать динамический массив


fwrite((void*)&#38n,sizeof(int),1,fd); // файловых указателей в памяти


fwrite((void*)pp,sizeof(long),n,fd); // Записать в файл размерность


for (i=0; i&#60n; i++) // массива файловых указателей


{ // и сам массив (занять место)


pp[i]=ftell(fd); // Записать строку в файл в виде


int sz=strlen(p[i])+1; // записи переменной длины и


fwrite((void*)&#38sz,sizeof(int),1,fd); // сохранить адрес в массиве


fwrite((void*)p[i],sz,1,fd); // файловых указателей


}
fseek(fd,sizeof(int),SEEK_SET); // Обновить в файле массив


fwrite((void*)pp,sizeof(long),n,fd); // файловых указателей


fclose(fd);
}

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





//------------------------------------------------------bk59-09.cpp

char **load(char *name) // Функция возвращает динамический

{ // массив указателей на строки

FILE *fd;
int i,n;
long *pp;
char **p;
if ((fd=fopen(name,"rb"))==NULL) return;
fread((void*)&#38n,sizeof(int),1,fd); // Прочитать размерность

pp=new long[n]; // Создать динамический массив

p=new char*[n+1]; // файловых указателей и указателей

fread((void*)pp,sizeof(long),n,fd); // на строки.

// Первый - прочитать из файла

for (i=0; i&#60n; i++)
{
int sz;
fseek(fd,pp[i],SEEK_SET); // Установиться по i-му файловому

fread((void*)&#38sz,sizeof(int),1,fd); // указателю и прочитать запись

p[i]=new char[sz]; // переменной длины - строку

fread((void*)p[i],sz,1,fd);
}
p[n]=NULL;
fclose(fd);
return p;
}

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





//------------------------------------------------------bk59-10.cpp

char *load(char *name, int num) // Возвращается строка =

{ // динамический массив

FILE *fd;
int i,n,sz;
long pp;
char *p;
if ((fd=fopen(name,"rb"))==NULL) return; // Режим чтения двоичного файла

fread((void*)&#38n,sizeof(int),1,fd); // Считать размерность МУ

if (num&#62=n) return NULL; // Нет записи с таким номером

fseek(fd,sizeof(int)+sizeof(long)*num,SEEK_SET); // Установить на указатель

fread((void*)&#38pp,sizeof(long),1,fd); // с номером n и прочитать его

fseek(fd,pp,SEEK_SET); // Установиться на запись

fread((void*)&#38sz,sizeof(int),1,fd); // Прочитать длину записи

p=new char[sz]; // Создать динамический массив

fread((void*)p,sz,1,fd); // Прочитать запись - строку

fclose(fd);
return p; // Возвратить указатель на строку

}


Массивы указателей


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



Массивы указателей на строки


Другой широко распространенной интерпретацией массива указателей является трактовка каждого из них как указателя на массив (статический или динамический) указуемых переменных. Для указуемых переменных типа char (символы) эта структура данных обычно понимается как массив указателей на строки и имеет определение:


char *pc[20];

Существуют различные варианты формирования массива указателей на строки.

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


char *pc[] = { "aaa", "bbb", "ccc", NULL};

Вариант 2. Массив указателей создается статически, для размещения строк используется двумерный массив символов (массив строк), указатели назначаются программно:


char *pc[20], cc[19][80];
for (i=0; i&#60 19; i++) pc[i] = cc[i];
pc[i] = NULL;

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

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


char *pc[20], *p, c[80];
for (i=0; i&#60 19; i++)
{
gets(c); // ввод строки


if (strlen(c)==0) break; // пустая строка - конец


p = new char [strlen(c)+1]; // динамический массив


strcpy(p,c); // под строку


pc[i] = p;
}
pc[i] = NULL;

Вариант 4. Массив указателей также создается в динамической памяти:


char **pp, *p, c[80];
pp = new char* [20]; // динамический массив


for (i=0; i&#60 19; i++) // из 20 указателей char*


{
gets(c); // ввод строки


if (strlen(c)==0) break; // пустая строка - конец


p = new char [strlen(c)+1]; // динамический массив


strcpy(p,c); // под строку


pp[i] = p;
}
pp[i] = NULL;



Математические функции (math.h)



int abs(int n); // Абсолютное значение n


double acos(double x); // Функция арккосинуса


double asin(double x); // Функция арксинуса


double atan(double x); // Функция арктагенса


double atan2(double y,double x); // Функция арккосинуса y/x


double cos(double x); // Функция косинуса (x - в радианах)


double cosh(double x); // Функция гиперболического косинуса


double exp(double x); // Экспоненциальная функция


double fabs(double x); // Абсолютное значение x


double floor(double x); // Целая часть x


double fmod(double y,double x); // Остаток от деления y на x


double hypot(double y,double x); // Гипотенуза катетов y,x


long labs(dong n); // Абсолютное значение n


double ldexp(double x, int n); // Значение x * 2 ^ n


double log(double x); // Функция натурального логарифма


double log10(double x); // Функция десятичного логарифма


double poly(double x, int n, double c[]);


// Степенной полином


// y = c[n]*x ^n + c[n-1]*x ^(n-1) + ... + c[1]*x + c[0]


double pow(double y,double x); // Функция y в степени x


double pow10(int n); // Функция 10 в степени n


double sin(double x); // Функция синуса


double sinh(double x); // Функция гиперболического синуса


double sqrt(double x); // Функция квадратного корня


double tan(double x); // Функция тангенса


double tanh(double x); // Функция гиперболического тангенса



Механизм прерываний в архитектуре I


Прерывание в IBM PC представляет собой вызов подпрограммы по длинному (far) адресу с предварительным сохранением в стеке состояния процессора (регистр флагов) и полного текущего адреса (CS:IP). Вектор прерывания - область памяти из 4-х байтов, содержащая начальный адрес подпрограммы обработки прерывания. В терминологии Си вектор прерывания - это указатель на функцию обработки прерывания . Первые 1024 байта оперативной памяти содержат 256 векторов прерывания, имеющих номера от 0 до 255. В терминах Си такой массив векторов прерываний определяется как


void interrupt (*IVEC[256])();

Значение индекса в этом массиве имеет также называние - номер вектора прерывания. Каждому источнику аппаратного прерывания соответствует свой номер вектора прерывания. Команда "INT nn" при выполнении процессором вызывает программное прерывание с номером nn.

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



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



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



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

Процесс вхождения в прерывание по вектору N можно условно проиллюстрировать средствами Си :



void interrupt (*IVEC[256])();

*(--SP) = FLAGS;
*(--SP) = _CS;
*(--SP) = _IP;
_CS = MK_SEG(IVEC[N]);
_IP = MK_OFFS(IVEC[N]);

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


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


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




- поскольку при входе в прерывание процессор устанавливается в состояние запрещения прерывания, то реально обработка следующего прерывания может произойти только после выхода из текущего. Этим самым исключается возможность появления
ВЛОЖЕННЫХ ПРЕРЫВАНИЙ (см.ниже).

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

_IP = *SP++;
_CS = *SP++;
FLAGS = *SP++;


Многоуровневые индексы


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

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

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



Многоуровневые массивы указателей на строки


Для массива указателей на строки типа char*[] существует аналог -двойной указатель типа char**, который можно интерпретировать как указатель на массив указателей на строки. При этом типы char*[] и char** соотносятся как указатель-константа и указатель-переменная(см п.4.1). Двойной указатель может использоваться для работы с динамическими массивами строк. В последнем случае как сами строки, так и массив указателей на них представлены динамическими переменными. В качестве примера рассмотрим создание массива указателей на строки при чтении последних из файла.


//------------------------------------------------------bk52-04.cpp


&#35define SIZE0 100
char **loadfile(FILE *fd)
{
char **pp,*q,str[80];
int i;
pp = new char* [SIZE0]; // Создать динамический


if (pp ==NULL) return(NULL); // массив указателей


for (i=0; fgets(str,80,fd) !=NULL; i++)
{ // Читать строку из файла


q = new char [strlen(str)+1];
if (q==NULL) return(NULL); // Создать динамический


strcpy(q,str); // массив символов и


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


pp[i] = q; // Сохранить указатель


if ((i+1) % SIZE0 ==0) // Расширить массив указа-


{ // телей при переполнении


pp = realloc(pp,sizeof(char *) *(i+1+SIZE0));
if (pp ==NULL) return(NULL);
}
}
pp[i] = NULL; // Ограничитель массива


return pp; // указателей


}

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


char **pp[20]; char ***ppp;

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



Многоуровневые указатели


Рассмотрим определение переменной:


double **pp;

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



-указатель на одиночный указатель на переменную типа double ;



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



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



-указатель на массив, содержащий указатели на массивы переменных типа double .

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


double **pp, *pd[20];
pp = pd; // или pp = &#38pd[0];


*(pp+i) // или pp[i]


**(pp+i) // или *pp[i]

Здесь повторяется та же самая система эквивалентности обычных массивов и указателей -типов double* и double[], но применительно не к массивам обычных переменных, а к массивам указателей. Короче говоря, типы double** и double *[] различаются так же, как указатель-переменная и указатель-константа.

Массив указателей типа double *[] является статической структурой данных, размерность которой определяется при трансляции. Двойной указатель типа double** может ссылаться и на динамический массив указателей, который создается во время работы программы под заданную размерность:


//------------------------------------------------------bk52-02.cpp


double **create(int sz)
{ double **pp,*q; // создать динамический массив


int i; // указателей размерностью sz+1


pp = new double *[sz+1];
for (i=0; i&#60sz; i++) // создать динамическую переменную


{
q = new double;
*q = i; // указатель занести в динамический


pp[i] = q; // массив


}
pp[sz] = NULL;
return(pp); // возвратить указатель


} // на динамический массив указателей



Модель системы процессов, работающих в разделении времени


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

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


// Дескриптор процесса -------------------------------------


struct PROCESS
{
unsigned SS,SP; // Указатель стека процесса


char *PS; // Область стека процесса


unsigned STATUS; // Слово состояния процесса


unsigned DELAY; // Тайм - аут


}
TASK[NT], // Массив дескрипторов процессов


*PT; // Указатель на дескриптор текущего процесса

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



-АКТИВЕН - в данный момент выполняется (или прерван),



-ГОТОВ - может быть выполнен ;



-БЛОКИРОВАН - не может быть выполнен по каким-либо внешним причинам.

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


// Биты блокировки процесса ---------------------------


&#35define TWAIT 1 // Тайм - аут процесса


&#35define IO 2 // Ввод - вывод


&#35define MESS 4 // Ожидание сообщения


&#35define SEM 8 //
Семафор


&#35define OFF 16 // Не активен

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



/ / Обработчик прерывания по таймеру ------------------------------
void interrupt TIMER(bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,flgs)
{
static int n;
//----- Запоминание состояния задачи ----------------------

(*OLDTIM)(); // Старый обработчик прерывания

if (DISP) return; // Диспетчер занят - переключение невозможно

DISP++; // Занять диспетчер - защита от повторного входа

PT-&#62SP = _SP; // Сохранить текущий указатель стека

PT-&#62SS = _SS;
//----- Поиск готового процесса --------------------------

if (RUN) {DISP--; return; }
for (CT++,n=0; n &#60 NT; n++,CT++)
{ // Поиск " по кругу" от текущего процесса

CT %= NT; // Выбирается готовый - без битов блокировок

if (TASK[CT].STATUS ==0) break;
}
//----- Отсутствие готовых процессов ---------------------------

if (n == NT) printf("a-a-a-a-a-a-a-a");
//----- Восстановление состояния нового процесса------------

PT = TASK + CT; // Указатель на новый активный процесс

_SS = PT-&#62SS; // Установить стек процесса

_SP = PT-&#62SP;
DISP--;
} // Выйти из прерывания через стек нового

// процесса - переключиться на новый





Обработчик прерывания от таймера выполняет функции ДИСПЕТЧЕРА параллельных процессов. Поскольку в работе системы могут возникнуть ситуации, когда переключение процессов запрещено, и, наоборот, при работе диспетчера следует защититься от выполнения некоторых действий, которые могут быть связаны с изменением данных в дескрипторах процессов, то в программу вводятся переменные RUN , запрещающая переключение задач, и
DISP , индицирующая, что в данный момент работает диспетчер.











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


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









Функция FORK ищет "выключенный" процесс в массиве дескрипторов, (блокированный в состоянии OFF ), создает динамический массив для размещения его стека, после чего устанавливает на него указатели стека и заполняет его текущее состояние. Для этого используется определение структурированной переменной STACK , последовательность элементов в которой соответствует последовательности запоминания переменных в стеке функцией обработки прерывания типа
void interrupt.



// Стек функции обработки прерывания ------------------

struct STACK
{ unsigned bp,di,si,ds,es,dx,cx,bx,ax,ip,cs,flgs; };
//--------------------------------------------------------

// Запуск процесса в режиме разделения времени

// Возвращает номер процесса

int FORK(void (*pf)())
{
int nt,n;
struct STACK *sp;
RUN++; // Запретить переключение процессов

for ( nt=-1,n=0; n &#60 NT; n++) // Искать свободный дескриптор

if (TASK[n].STATUS &#38 OFF) break;
if (n != NT)
{
nt = n; // Резервировать память под стек

TASK[nt].PS = (char *)malloc(SIZE);
// Текущий указатель стека с конца

// выделенной памяти (стек " вверх дном" )

sp = (struct STACK *)(TASK[nt].PS + SIZE - sizeof(struct STACK));
sp-&#62ax = sp-&#62bx = sp-&#62cx = sp-&#62dx = 0;
sp-&#62si = sp-&#62di = 0; // Сформировать начальное состояние стека

sp-&#62ds = sp-&#62es = _DS;
sp-&#62cs = _CS;
sp-&#62ip = (unsigned)pf; // Начало процесса - адрес функции

TASK[nt].SS = _DS; // Указатель на стек процесса в дескрипторе

TASK[nt].SP = (unsigned)sp;
TASK[nt].STATUS = 0; //Процесс - ГОТОВ

}
RUN--;
return(nt);
}



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


Заметим, что при порождении процесса переключения процессов запрещаются (устанавливается признак RUN ).



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



// Блокирование текущего процесса --------------------------

void BLOCK(int mask)
{
RUN++;
PT-&#62STATUS |= mask;
RUN--;
geninterrupt(TIMVEC);
}

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



// Уничтожение текущего процесса -------------------------

void KILL()
{
RUN++;
asm cli
PT-&#62STATUS |= OFF;
free(PT-&#62PS);
RUN--;
asm sti
geninterrupt(TIMVEC);
}
// Тайм - аут процесса -------------------------------------

void WAIT( unsigned tt)
{
PT-&#62DELAY = tt;
BLOCK(TWAIT);
}

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



// Вычисление тайм - аутов в диспетчере -------------------

if (!NOCLOCK)
{
for (n=0; n&#60NT; n++)
if (TASK[n].STATUS &#38 TWAIT)
if (--TASK[n].DELAY ==0) TASK[n].STATUS &#38= ~TWAIT;
} NOCLOCK=0;



Основная функция main имеет в себе отдельные нюансы, которые могут быть поняты только в терминах процессов.


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



void main()
{ int n;
RUN = DISP = 0;
TASK[0].STATUS=0;
for (n=1; n &#60 NT; n++) TASK[n].STATUS = OFF;
RUN++;
CT = 0;
PT = TASK;
OLDTIM = getvect(TIMVEC);
setvect(TIMVEC,TIMER);
// В момент первого прерывания от таймера main становится

// нулевым процессом (это можно сделать и принудительно)

geninterrupt(TIMVEC);
textbackground(BLACK);
clrscr();
FORK(consume);
FORK(produce);
RUN--;
// В данном примере " нулевой" процесс ничего не делает, хотя

// он может выполнять все функции обычного процесса

// (geninterrupt используется для принудительного переключения

// процессов, а NOCLOCK вводится в диспетчер, чтобы не учитывать

// такие принудительные прерывания для ожидающих (блокированных)

// процессов)

for(STOP=0;!STOP;)
{ NOCLOCK++; geninterrupt(TIMVEC); }
setvect(TIMVEC,OLDTIM);
clrscr();
}