Абстрактные типы данных
Абстрактный тип данных используется в тех случаях, когда требуется обозначить некоторый тип данных как таковой без привязки к конкретной переменной:
-в операции sizeof;
-в операции явного преобразования типа данных;
-при объявлении формальных параметров внешней функции с использованием прототипа.
Во всех случаях используется тот же самый контекстный способ определения, в котором отсутствует имя переменной. Рассмотрим примеры:
p = malloc(sizeof(char *) * 20);
Резервируется память для размещения 20 указателей на строки.
long l;
((char *)&l) [2] = 5;
Указатель на переменную l типа long явно преобразуется в указатель на символ (байт) -значение 5 записывается во второй (начиная с нулевого) байт длинного целого l.
extern int strcmp(char *, char *);
прототип внешней функции сравнения строк с двумя формальными параметрами -указателями на строки.
Адресная арифметикаУказатель как адрес памяти
Рассмотренное понятие указателя не является полным. Более широкое толкование смысла указателя возникает в Си при введении особой операции указатель+целое , которая называется операцией АДРЕСНОЙ АРИФМЕТИКИ и интерпретируется следующим образом:
-любой указатель потенциально ссылается на неограниченную в обе стороны область памяти, заполненную переменными указуемого типа;
-переменные в области нумеруются от текущей указуемой переменной, которая получает относительный номер 0. Переменные в направлении возрастания адресов памяти нумеруются положительными значениями 1,2,3..., убывания - отрицательными --1,-2..;
-результатом операции " указатель + i" является адрес i-й переменной (значение указателя на i-ю переменную) в этой области относительно текущего положения указателя.
Таким образом, указатель ссылается на неограниченный массив с относительной нумерацией элементов массива от указуемой переменной. С учетом сказанного, ряд выражений, использующих операцию адресной арифметики, имеют свой особый смысл:
*p // Значение указуемой переменной;
p + i // Указатель на i-ю переменную после указуемой;
p -i // Указатель на i-ю переменную перед указуемой;
*(p+i) // Значение i-й переменной после указуемой;
p[i] // Значение i-й переменной после указуемой;
p++ // Установить указатель не переменную, следующую
// за указуемой;
p-- // Установить указатель на переменную,
// предшествующую указуемой;
p+=i // Переместить указатель на i переменных вперед
// относительно указуемой;
p-=i // Переместить указатель на i переменных назад
// относительно указуемой;
*p++ // Получить значение указуемой переменной и
// установить указатель на следующую;
*(--p) // Переместить указатель к переменной,
// предшествующей указуемой, и получить ее
// значение.
В операциях адресной арифметики транслятором автоматически учитывается размер указуемых переменных, то есть " +i" понимается не как смещение на i байтов, слов и пр., а как смещение на i указуемых переменных. Другая важная особенность: при перемещении указателя нумерация переменных в памяти остается относительной и всегда производится от текущей указуемой переменной.
Алгоритм
Обычно термин " язык программирования" ассоциируется с понятием алгоритм и программа. Самым общим является, пожалуй, понятие алгоритма, которое относится не только к компьютерам, но и вообще к любой формальной деятельности. Итак :
Прежде всего, для задания алгоритма необходимо определить объект воздействия (или управления), а также тот набор действий, который позволительно производить с ним. Эта троечка " алгоритм, набор действий и объект управления" определяют процесс, который происходит при выполнении алгоритма. Но алгоритм представляет всего-навсего описание этого процесса. Чтобы воспроизвести этот процесс, необходимо нечто, понимающее и воспроизводящее алгоритм. Назовем это ИНТЕРПРЕТАТОРОМ АЛГОРИТМА.
Как компьютер, так и любая система программирования ориентированы на обработку данных, то есть объектами управления являются данные, представленные обычно в виде переменных и форм их представления, называемых типами данных. Набор действий по обработке данных (переменных) в языке программирования обычно называется операциями, которые, соединяясь между собой, образуют выражения. Непосредственно алгоритм составляется на основе операторов языка программирования. Алгоритм обычно разбивается на логически завершенные части, которые называются модулями (процедурами, функциями). Все это в совокупности составляет программу :
-данные - переменные, создаваемые на основе типов данных ;
-выражения, включающие переменные и операции по их обработке ;
-логика алгоритма, составленная из операторов ;
-модули, соответствующие логически завершенным частям алгоритма.
Если отнести последние три компоненты к алгоритмической части программы, то можно дать такое короткое определение :
Программа -- алгоритм + данные .
Анализ " смысла" программы и ее частей
Концептуальный параграф для всего последующего. Дилетант смотрит на программу и видит отдельные синтаксические элементы, а программист "видит" процесс выполнения программы, в которой каждый фрагмент вносит свой "смысл". На 50% готовые программы состоят из таких "кирпичей", которые составляют практику и "здравый смысл" программирования. "Нагружаются смыслом" переменные - счетчики, признаки, накопители, минимумы. "Нагружаются" смыслом циклы, проверяющие свойства "всеобщности" и "существования". Наконец, существует "правило трех стаканов" для перестановки значений двух переменных с использованием третьей. Если кто считает, что это не надо объяснять - глубоко заблуждается. В конце - примерно 50 тестов на эти и другие темы.
Арифметические операции
Арифметические операции имеют в Си меньше всего специфики. Единственное, на что следует обращать внимание при их выполнении, -это размерность используемых целых переменных и переменных с плавающей точкой, неявные преобразования типов данных в выражениях и связанные со всем этим возможные потери значащих цифр (значимости) результата.
Операция "%" вычисляет остаток от деления первого операнда на второй. Она имеет также другой, содержательный смысл: второй операнд-константа выступает ограничителем возможных изменений первого операнда и называется модулем. Название такой операции звучит как "... по модулю ...":
a = (a + 1) % 16; // a присвоить a+1 по модулю 16
Атттестационный экзамен ()
СОДЕРЖАНИЕ ЭКЗАМЕНА
ФОРМА ПРОВЕДЕНИЯ ЭКЗАМЕНА
КРИТЕРИЙ ОЦЕНКИ ВЫПОЛНЕНИЯ ЗАДАНИЯ
КРИТЕРИИ СНИЖЕНИЯ ОЦЕНКИ
СОДЕРЖАНИЕ ЗАДАНИЯ ПО ДИСЦИПЛИНЕ "ИНФОРМАТИКА"
СОДЕРЖАНИЕ ЗАДАНИЕ ПО ДИСЦИПЛИНЕ "ТЕХНОЛОГИЯ ПРОГРАММИРОВАНИЯ"
Байт, машинное слово
Компьютеры обрабатывают данные, представленные в двоичной системе счисления. Как сказал бы Остап Бендер: "Это медицинский факт". Основа представления любых данных машинное слово. Машинное слово это упорядоченное множество двоичных разрядов, используемое для хранения команд программы и обрабатываемых данных. Каждый разряд, называемый БИТОМ -это двоичное число, принимающее значения только 0 или 1. Разряды в слове обычно нумеруются, справа налево, начиная с 0. Количество разрядов в слове называется размерностью машинного слова или его РАЗРЯДНОСТЬЮ машинного слова.
.
15 14 ... 8 7 5 ... 2 1 0
________________________________________________
1 0 0 0 1 1 0 1 0 1 0 1 1 0 1 1
________________________________________________
| | |
старший разряд (бит) 5-й разряд младший разряд
БАЙТ -- машинное слово минимальной размерности, адресуемое в процессе обработки данных.
Размерность байта - 8 бит -принята не только для представления данных в большинстве компьютеров, но и в качестве стандарта для хранения данных на внешних носителях, для передачи данных по каналам связи, для представления текстовой информации. Кроме того, байт является универсальным "измерительным инструментом" - размерность всех форм представления данных устанавливается кратной байту. При этом машинное слово считается разбитым на байты, которые нумеруются, начиная с младших разрядов.
Машинное слово двойной длины ( ДВОЙНОЕ СЛОВО ) используется для увеличения диапазона представления целых чисел. Двойные слова обрабатываются либо отдельными командами процессора, либо программно.
Самая распространенные формы представления данных, использующие одно машинное слово -целое число со знаком и без. Наиболее простая -целое положительное число без знака. В нем каждый разряд машинного слова имеет вес, в два раза больший, чем вес соседнего правого, то есть 1,2,4,8,16 и т.д. или последовательные степени 2. Тогда значение числа в машинном слове равно сумме произведений значений разрядов на их веса:
.
R0 * 1 + R1 * 2 + R2 * 4 + ... + R15 * 32768 или
.
0 1 16
R0 * 2 + R1 * 2 + ... + R15 * 2
Например, машинное слово 0000000001001001 имеет значение 1+8+128 = 137. На практике вместо двоичной системы используются восьмеричная и шестнадцатеричная системы счисления. Это объясняется тем, что одна восьмеричная цифра принимает значения от 0 до 7 и занимает 3 двоичных разряда. Аналогично шестнадцатеричная цифра принимает значения от 0 до 15, что соответствует 4-м двоичным разрядам ( ТЕТРАДА ).
При необходимости представить машинное слово в его "натуральном" виде как последовательность двоичных разрядов лучше всего пользоваться шестнадцатеричными константами. Поскольку обычных цифр для представления значений от 0 до 15 не хватает, то для недостающих используются прописные или строчные латинские буквы:
.
A - 10, D - 13,
B - 11, E - 14,
C - 12, F - 15.
Сама константа содержит ряд шестнадцатеричных цифр, предваренный последовательностью "0x", например:
.
0x1234, 0x1B8C, 0xB8000000, 0xFFFF
Перевести эти константы в двоичную систему очень просто. Достаточно представить каждую цифру в виде четырех двоичных разрядов, задающих ее значение в двоичной системе:
.
0x1B8C = 0001 1011 1000 1100
1 B 8 C
И наоборот, значение любого машинного слова из двоичного представления легко перевести в шестнадцатеричную константу, разбив на тетрады и заменив значение каждой из них соответствующей цифрой 0..9A..F.
Но на самом деле программиста обычно не интересует представление всего слова в виде последовательности битов. По условию поставленной задачи ему требуется иметь установленными в 0 или 1 отдельные биты. Это также очень просто сделать, если считать номера разрядов справа налево по 4 в каждой цифре, начиная с 0. Например, если в константе требуется установить в 1 девятым разряд, то он будет находиться в третьей справа цифре, содержащей разряды с номерами 8..11. Все остальные цифры будут нулевыми. Значение же этой цифры с установленным девятым разрядом будет равно 2.
В результате получим константу 0x0200. Наоборот, если в тетраде установлены в 1 значения всех битов, то ей соответствует цифра F. Тогда машинное слово со всеми единичными разрядами выглядит как 0xFFFF, а с единственным, установленным в 0 девятым разрядом -0xFDFF.
Аналогичным образом могут использоваться восьмеричные константы. В Си любая константа, содержащая цифры от 0 до 7 и начинающаяся с 0, считается восьмеричной, например 0177556.
.
7 Байт 0 Значение в 10-й системе счисления
____________________ 0 2 3 6
0 1 0 0 1 1 0 1 2 + 2 + 2 + 2 = 1+4+8+64 = 77
____________________
.
Восьмеричная константа 0 1 0 0 1 1 0 1
1 1 5 = 0115
.
Шестнадцатеричная константа 0 1 0 0 1 1 0 1
4 13 = 0x4D
.
Байт 1 Байт 0
_______________________________________
0 1 1 0 1 1 0 1 1 1 0 0 1 1 0 0
_______________________________________
0 6 6 7 1 4 = 066714
________________________________________
0 1 1 0 1 1 0 1 1 1 0 0 1 1 0 0
________________________________________
6 13 12 12 = 0x6DCC
Получить значение восьмеричной или шестнадцатеричной константы в десятичной системе можно также путем умножения цифр числа на веса разрядов -последовательные степени 8 или 16:
.
0 1 2 3
0x6DCC =12(C)*16 +12(C)*16 +13(D)*16 +6*16 = 12 + 12*16 + 13*256 + 6*4096
При обнаружении в тексте программы константы транслятор может самостоятельно определить ее размерность (стандартное или двойное машинное слово), исходя из количества значащих цифр. Кроме того, десятичные константы он считает всегда целыми со знаком. Программист может явно указать, что данная константа является "длинной" (символы L,l) или беззнаковой (символы U,u):
.
200 // Целое стандартной размерности
1000000 // Длинная константа
200l, 200L , 0xB8L // Длинные константы
123u, 60000U // Беззнаковые константы
077777777UL // Длинная беззнаковая константа
Базовые типы данных
В Си из-за его специфики не обойтись просто упоминанием форм представления данных. Приходится рассматривать структуру машинного слова, формы представления целого без знака и со знаком, преобразования типов данных в выражениях, их специфику. Все это, конечно, усложняет жизнь начинающему, но лучше это сделать именно здесь.
Базовые типы данных целых чисел
В Си имеется возможность использовать машинные слова различной размерности для определения целых переменных как в знаковой, так и в беззнаковой форме. Для этого используются следующие служебные слова:
-char -размерность переменной -байт;
-int -размерность переменной -стандартное машинное слово;
-short -укороченная размерность, меньшая либо равная int;
-long -размерность переменной -двойное слово;
-signed -форма представления целого со знаком;
-unsigned -беззнаковая форма представления целого.
Из этих служебных слов можно составить определение типа данных целой переменной. При этом signed считается заданным по умолчанию, а unsigned int сокращается до int :
int i; // целое со знаком, слово
char c; // целое со знаком, байт
unsigned char uc; // целое без знака, байт
unsigned u; // целое без знака, слово
long l; // целое со знаком, двойное слово
unsigned long ul; // целое без знака, двойное слово
short s; // целое со знаком, короткое слово
/ / (слово или байт)
Для определения размерности различных переменных и типов в Си имеется специальная операция, которая возвращает размерность типа данных, переменной и значения выражения выражения, подсчитанную в байтах:
sizeof (int) // размерность типа данных int - 2
sizeof l // размерность переменной типа long - 4
sizeof(i+2.0) // размерность значения выражения типа double - 8
В заключение еще раз вернемся к знаковой и беззнаковой форме представления и к взаимоотношениям между ними и транслятором. Для транслятора обе они -суть машинные слова одинаковой или разной длины. Поэтому когда речь идет о записи (присваивании) значения беззнаковой переменной в переменную со знаком (или наоборот), транслятор просто рассматривает это как копирование машинных слов без каких-либо преобразований. Например, знаковое -1 превращается в беззнаковое 0xFFFF или 65535. Поэтому когда в программе меняется форма представления целого со знаком в беззнаковую (и наоборот), то это не сопровождается абсолютно никакими действиями (машинными командами).
Одной из форм представления целого является также ПЕРЕЧИСЛИМЫЙ ТИП enum. Переменная такого типа принимает ряд значений, которые перечислены в отдельном определении типа enum в виде идентификаторов(имен). Каждому идентификатору соответствует явно заданное или неявно полученное при перечислении значение:
enum BOOL { FALSE, TRUE}; // 0,1
enum digit { four=4,one=1,two,three }; // 4,1,2,3.
BOOL x; ... if (x==FALSE) ...
digit D; ... D = ten; ...
В данном примере перечислимый тип BOOL имеет два значения - 0 и 1, которые обозначаются соответственно как FALSE и TRUE. Имя BOOL затем используется для определения переменной этого типа с именем x, которая может принимать указанные значения. То же самое производится в типом digit и переменной D. Как видим, перечислимый тип не выходит за рамки обычного целого. Более того, любая переменная такого типа в трансляторе реализуется как обычное целое и в процессе выполнения программы не проверяется на допустимость тех или иных значений. Например, можно написать:
x = 3; // Присвоить 3 переменной типа digit
Тогда зачем же они нужны? Для придания большей ясности программе: обозначения ограниченного количества вариантов, признаков и т.д..
Битовые поля
Поразрядные операции позволяют работать с отдельными битами и их группами (полями) внутри переменных (см.4.8). Аналогичные возможности дают элементы структуры, называемые битовыми полями
.
struct man
{ // 15 9 8 5 4 0
char name[20]; // ---------------------------------
unsigned dd:5; // | yy | mm | dd |
unsigned mm:4; // ---------------------------------
unsigned yy:7;
char *address; // 15 11 10 8 7 3 2 0
int xx:3; // ---------------------------------
int :5; // | | zz | ... | xx |
int zz:3; // ---------------------------------
};
Любая последовательность элементов структуры типа int или unsigned, в которой за каждым именем элемента следует двоеточие и целая константа, размещаются в отдельном машинном слове в виде групп битов (битовых полей). Константа определяет разрядность битового поля. Допустимая размерность машинного слова, в котором размещаются битовые поля, ограничивается транслятором. Допускается использование безымянных полей, которые в таком случае являются недоступными. Операции с битовыми полями ничем не отличаются по синтаксису от подобных операций с обычными элементами структуры. Естественно, что транслятор формирует команды, обеспечивающие маскирование, сдвиги и другие поразрядные операции, необходимые для превращения значения битового поля в значение обычной целой переменной без знака:
A.dd = 12; A.mm = 5; A.yy=96; A.dd++; A.zz = A.xx;
Биты, байты, слова
Работа с отдельными битами и полями машинного слова. Содержательная интерпретация всех логических поразрядных операций, в том числе с константами (маскирование). Примеры программ работы с растрами, упаковки, машинной арифметики.
Bk-t
N = 1 t =new Array()
function Set_quest() { elem = new Array() QST = new Array() for (i=1; i
Вопрос 1 Вопрос 2 Вопрос 3 Вопрос 4 Вопрос 5 Вопрос 6
Время
Всего
Правильно
Итого
Ответ
11. . Белецкий Я. Энциклопедия языка Си. М.: Мир, 1992.
12. . Болски М. Язык программирования Си (Справочник). ( Библиотека НГТУ - 51Б795 ).
13. . Джехани И. Программирование на языке Си. ( Библиотека НГТУ - 51Д409 ).
14. . Джонс Р. Программируем на Си. 1994. ( Библиотека НГТУ - 51Д424 ).
15. . Котлинская Г.П. Программирование на языке Си. ( Библиотека НГТУ - 51К734 ).
16. . Техника программирования на Turbo Си. ( Библиотека НГТУ - 51Т381 ).
17. . Тондо К. Язык Си. Книга ответов. ( Библиотека НГТУ - 51Т57 ).
18. . Уинер Р. Язык Turbo Си. ( Библиотека НГТУ - 51У931 ).
19. . Язык Си. Руководство для начинающих. ( Библиотека НГТУ - 51У977 ).
20. . Хэнок Л. Введение в программирование на языке Си. ( Библиотека НГТУ - 51Х993 )
21. . Эллис М., Строуструп Б. Справочное руководство по языку программирования Си++ с комментариями. - М.:Мир,1992.
22. . Стенли Б. Липпман. Си++ для начинающих (в 2 томах). М.: ГЕЛИОН, 1993.
23. . Дьюхарот С., Старк К. Программирование на Си++. Киев, НИПФ, Диасофт, 1993.
24. . Романов В.Ю. Программирование на языке Си++: Практический подход. -М.: Компьютер, 1993, -160 с., ил.
25. . Рассохин Д. От Си к Си++. М.:Издательство "ЭДЭЛЬ",1993.-128 с.
26. . Бабэ Б. Просто и ясно о Borland C++: Пер. с англ. -M.:Бином. -416 с.: илл.
Числа с плавающей точкой
Для использования чисел с дробной частью, а также для расширения диапазона используемых числовых данных вводится форма представления вещественных чисел или чисел с плавающей точкой:
.
p 2
X = m * 10, например 25.4 = 0.254 * 10
где 0.1 m < 1 </b -значащая часть числа, приведенная к интервалу 0.1 ... 1 , называемая мантиссой, a p -целое число, называемое порядком. Аналогично, если взять основание степени -2, то получим:
.
p 5
X = m * 2, например 25.4 = 0.79375 * 2
где 0.5 m < </b 1 -мантисса, a p -двоичный порядок. Тогда в двоичной системе число с плавающей точкой можно представить с использованием двух целых чисел со знаком ( m и p ), причем p -обычное, а m -представление дробной части, в которой десятичная точка считается расположенной после знакового разряда числа. Не вдаваясь в дальнейшие подробности, заметим, что любую арифметическую операцию над числами с плавающей точкой можно разложить в последовательность действий (алгоритм) над этими парами машинных слов и реализовать как программно, так и на аппаратном уровне.
В Си имеется три типа данных для чисел с плавающей точкой: обычной ( float ), двойной ( double ) и повышенной ( long double ) точности. Тип float используется, в основном, при вводе-выводе. Тип double обеспечивает стандартную точность вычислений в арифметических выражениях. Поэтому любая переменная типа float перед использованием в выражении автоматически преобразуется к double . Кроме того, если в операции присутствует одна переменная типа double , а вторая является целым числом, то последняя также преобразуется (приводится) к double .
Вещественные константы могут содержать как дробную часть с десятичной точкой, так и показатель степени. Символ перед показателем степени определяет точность константы (E,e -для double, F,f -для float, L,l -для long double ):
.
0.4 .665 3.1415926 1.17e2 -176E-3 1.1F5 3.33L
Что, как и зачем предлагается изучать
Раздел 1. "Программист дилетантствующий"
Раздел 2. "Программист продвинутый"
Раздел 3. "Программист системный"
Раздел 4. "Программист объектно-ориентированный"
Большинство изданий по языками и системам программирования путают две вещи : язык и практика программирования на нем. Общепринятая система погружения в язык - описание лексики, синтаксиса, напоминает изучение английского языка с помощью учебника грамматики и русско-английского словаря. Читающего же, в первую очередь интересует конечный результат - когда же он, наконец, начнет сам писать программы. Вопросы же технологии программирования, вообще "здравого смысла" при написании программ благополучно были отражены в книжках двадцатилетней давности и после этого забыты. Считается, что нынешние поколения программистов работают на столь высоком уровне, что неприлично напоминать о таких общеизвестных вещах, как списки, деревья, рекурсия.
Основная идея предлагаемого материала - не стоит, хотя бы при изложении столь открытого языка, как Си, начинать все валить в кучу - классы, объекты, структуры, функции, и прочая, и прочая, и прочая... Если задаться целью овладения навыками профессионального программирования на Си, то стоит делать это, как минимум, в несколько шагов, а именно :
- программирование на уровне Бейсик (l дилетантское) - простые переменные и массивы, основные алгоритмы решения арифметических задач, обработки текста, приближенных вычислений, законы информатики (простой и линейный поиск, сортировка), простые структуры данных - стек, очередь, модульное программирование (процедура, параметры), основы анализа программ путем разложения на стандартные фрагменты, основы технологии программирования ;
- программирование на уровне Паскаль (l продвинутое) - типы данных, иерархия типов данных и модулей (процедур) в сложный программах, производные типы данных - структуры, указатели, функции, динамическая память, общие вопросы организации памяти в программе ;
- организация данных - традиционный раздел, в котором Си предстает "во всей своей красе" (l системное).
Массивы указателей, списки, деревья, статические и динамические структуры данных. Указатели на функции и динамическое связывание, текстовые файлы и двоичные файлы произвольного доступа вплоть до физической организации баз данных (таблицы и индексы) ;
- технология объектно-ориентированного программирования и Си++ (l профессиональное). Классы и объекты. Иллюстрация возможностей Си++ средствами "классического" Си. "Эпизодическое" объектно-ориентированное программирование : определение классов, переопределение операций, объекты с динамическими данными. "Тотальное" объектно-ориентированное программирование - "от класса к классу". Наследование, полиморфизм. Технологии организации системы объектов в программе - объекты, управляемые сообщениями.
Отметим еще несколько принципиальных моментов, которые отслеживаются при изложении материала :
- если умение разрабатывать программы - это навыки " письма" , то не менее важными являются навыки " чтения" , то есть анализа уже готовых " хороших" программ. Поэтому в каждом параграфе должно быть 10-15 тестов - фрагментов программ и отдельных функций, которые составляют "Вопросы без ответов". Они также могут использоваться и как заготовки программ. Аналогично для самостоятельной работы должны предусматриваются 10-15 вариантов заданий.
- на каждом уровне погружения рассматриваются одни и те же алгоритмы, которые "вызывают чувство узнавания" - например, поиск минимума в массиве, массиве структур, дереве, и в том же дереве, представленном объектом класса.
- навык программирования в значительной степени приобретается по принципу "делай, как я", поэтому предпочтение отдается некоторому шаблону в решении поставленных задач. Импровизацию предлагается проявлять обучаемому.
Циклический список
Циклический список организован в соответствии с принятым принципом совмещения заголовка списка и его элементов в объектах одного класса. Первый элемент списка - текущий объект, доступный через this , является заголовком и не содержит данных. Остальные элементы - динамические, создаются при помещении в список новых данных и удаляются при их исключении.
class zlist
{
void *data;
zlist *next,*prev;
zlist *find(int); // Вспомогательный метод извлечения
public: // элемента списка по номеру
zlist(); // Конструктор пустого списка
~zlist(); //
int size(); // Количество элементов
void *operator[](int); // Извлечение
void operator()(void*,int); // Включение по номеру
void *remove(int); // Удаление по номеру
void *remove(void*); // Удаление по указателю на элемент данных
void *min( int(*)(void*,void*)); // Итератор поиска минимального
};
Конструктор списка определяет текущий объект как единственный элемент, который в соответствии с правилами построения циклического списка "замкнут сам на себя".
zlist::zlist()
{ prev=next=this; }
На вспомогательном методе извлечения элемента списка по его последовательному номеру можно увидеть все особенности объектно-ориентированной реализации. Первый элемент списка-заголовок является текущим объектом ( this ), при этом в процессе "счета" он не учитывается. Цикл просмотра начинается с первого информационного элемента ( this->next или next ) и завершается по возвращении на заголовок. В последнем случае логический номер не найден.
zlist *zlist::find(int n=-1)
{ zlist *p;
for (p=next; n!=0 && p!=this; n--, p=p->next);
return p; }
Метод подсчета количества элементов в структуре данных стандартным образом обходит циклический список .
int zlist::size()
{ int n; zlist *p;
for (n=0, p=next; p!=this; n++, p=p->next);
return n; }
Метод получения указателя на элемент данных по логическому номеру - переопределенная операция [ ] . Получает указатель на элемент списка при помощи внутреннего метода find и выделяет из него данные .
void *zlist::operator[](int n=-1)
{
if (n==-1) n=size()-1;
zlist *p=find(n);
return p==this ? NULL : p->data; }
Метод исключения элемента данных из списка находит прежде всего элемент списка, извлекает из него указатель на элемент данных. Сам элемент списка при этом удаляется, поскольку он является динамическим объектом. Указатель на элемент данных возвращается, поскольку структура данных не несет ответственность за размещение самого элемента данных в памяти и не распределяет память под него.
void *zlist::remove(int n=-1)
{
if (n==-1) n=size()-1; // По умолчанию - удалить последний
zlist *p=find(n); // Найти элемент списка по номеру
if (p==this) return NULL; // Номер не существует - удалить нельзя
void *s=p->data; // Сохранить указатель на данные
p->prev->next=p->next; // "Обойти" элемент двусвязного списка
p->next->prev=p->prev;
p->next=p->prev=p; // Перед удалением - сделать его
delete p; // "единственным"
return s;} // Возвратить указатель на данные
Метод исключения по указателю на элемент данных используется, когда необходимо удалить известный уже элемент данных. Метод ищет заданный указатель и удаляет содержащий его элемент списка.
void *zlist::remove( void *pd)
{
zlist *p= next;
for (; p!=ph; p=p->next)
{
if (p->data==pd)
{
p->prev->next=p->next; // "Обойти" элемент двусвязного списка
p->next->prev=p->prev; //
p->next=p->prev=p; // Перед удалением - сделать его
delete p; // "единственным"
return pd; // Возвратить указатель на данные
}
}
return NULL; }
Метод включения элемента данных по логическом номеру, наоборот, создает динамический объект - элемент списка, после чего включает его в список. Таким образом, элементами списка являются динамические объекты, создаваемые методами, работающими с его заголовком.
void zlist::operator()(void *s,int n=-1)
{ // По умолчанию - включить перед заголовком,
zlist *p=find(n); // в циклическом списке - последним
zlist *q=new zlist; // Создать новый элемент списка
q->data=s;
q->next=p; // Включить перед найденным ( p)
q->prev=p->prev;
p->prev->next=q;
p->prev=q;
}
Метод поиска минимального элемента является типичным итератором (см.5.6.), реализующим стандартный алгоритм поиска минимума в списке.
void *zlist::min( int (*cmp)(void*,void*))
{
if (next==this) return NULL; // Пустой список
zlist *pmin=next;
for (zlist *q=next; q!=this; q=q->next)
{
if ( cmp(pmin->data,q->data) > 0) pmin=q;
}
return pmin->data;
}
Отдельного обсуждения заслуживает деструктор. Дело в том, что деструктор может вызываться в двух совершенно различных ситуациях :
- когда удаляется элемент списка (при выполнении операции remove ). В этой ситуации он всегда является единственным удаляемым ;
- когда в программе удаляется сам объект-список. В этом случае деструктор вызывается для объекта-заголовка. Но если список не будет пустым, то деструктор должен предпринять меры к удалению включенных в него элементов списка.
Вся проблема заключается в том, что деструктор сам не в состоянии определить, в какой из приведенных ситуаций он находится. Ниже приводится один из способов решения проблемы. Метод remove перед удалением динамического объекта-элемента списка делает его "единственным". Деструктор же, наоборот, удаляет при помощи метода remove элементы списка, следующие за текущим объектом, пока он не станет единственным. Заметим, что при этом деструктор освобождает только элементы списка но ничего не делает с указателями на элементы данных (это отдельная проблема).
zlist::~zlist()
{ while (remove(0)!= NULL); }
В заключение рассмотрим пример использования объектов класса в программе . Хранимые в списке данные-строки являются статическими объектами, поэтому проблемы распределения памяти под них здесь не актуальны.
int CMP(void *s1,void *s2) {return strcmp((char*)s1,(char*)s2); }
zlist A;
A((void*)"aaaa");
A((void*)"bbbb");
A((void*)"cccc");
for (i=A.size()-1; i>=0; i--) cout << (char*)A[i] << " ";
cout << (char*)A.min(CMP) << " ";
cout << (char*)A.remove(1);
Деревья
Определение дерева имеет исключительно рекурсивную природу. Элемент этой структуры данных называется вершиной. Дерево представляет собой либо отдельную вершину, либо вершину, имеющую ограниченное число указателей другие деревья (ветвей). Нижележащие деревья для текущей вершины называются поддеревьями, а их вершины -потомками. По отношению к потомкам текущая вершина называется предком.
Вершину дерева можно определить таким образом:
struct tree
{
int val; // Значение элемента
tree *child[4]; // Указатели на потомков
};
Из определения элемента дерева следует только тот факт, что он имеет ограниченное число указателей на подобные элементы. Как и во всех динамических структурах данных характер связей между элементами определяется функциями, которые их устанавливают. В данном случае рекурсивное определение дерева требует и рекурсивного характера функций, работающих со связями. Простейшей функцией является функция обхода всех вершин дерева:
//------------------------------------------------------bk55-02.cpp
//------Рекурсивный обход дерева
void ScanTree(tree *p)
{
int i;
if (p == NULL) return; // следующей вершины нет
for (i=0; i< 4; i++) // рекурсивный обход
ScanTree(p->child[i]); // потомков с передачей
} // указателей на них
Само дерево обычно задается в программе указателем на его главную вершину. Часто обход дерева используется для получения информации, которая затем возвращается через результат рекурсивной функции. Так работает, например, функция определения максимальной глубины дерева:
//------------------------------------------------------bk55-03.cpp
//------Минимальная длина ветвей дерева
int M inDepth(tree *p)
{ int i, m in, nn;
if (p == NULL) return 0; // следующей вершины нет
for (min = MinDepth(p->child[0], i=1; i< 4; i++)
{ // обход потомков
nn = MinDepth(p->child[i]);
if (nn > max) max = nn;
} // возвращается глубина с
return min + 1; // учетом текущей вершины
}
Другой достаточно тривиальной процедурой является включение нового элемента в дерево.
Поскольку любой указатель в Си по определению адресует массив элементов указуемого типа неограниченной размерности, то функция malloc и оператор new могут использоваться для создания не только отдельных переменных, но и их массивов. Тот же самый указатель, который запоминал адрес отдельной динамической переменной, будет использоваться теперь для работы с массивом. Размерность его задается значением в квадратных скобках оператора new . В функции malloc объем требуемой памяти указывается как произведение размерности элемента на их количество ). Это происходит во время работы программы и, следовательно размерность массива может быть переменной от одного выполнения программы к другому: Здесь и далее используется эквивалентность синтаксиса *(p+i) и p[i] при использовании правил адресной арифметики для указателей. Массивы, создаваемые в динамической памяти, будем называть ДИНАМИЧЕСКИМИ. Заметим, что Си позволяется использовать одинаковый синтаксис при работе с обычными и динамическими массивами. Во многих языках интерпретирующего типа (например, Бейсик) подобный механизм скрыт в самом трансляторе, поэтому массивы там "по своей природе" могут быть переменной размерности, определяемой во время работы программы. Наиболее показательно применение динамических массивов символов при работе со строками. Напомним, что строкой называется последовательность символов произвольной длины, ограниченная символом конца строки '\0'.
Как известно, любого ресурса всегда не хватает. В компьютерах это прежде всего относится к памяти. Если на проблему ее распределения посмотреть с обычных житейских позиций, то можно извлечь много полезного для понимания принципов статического и динамического распределения памяти. Более изящно это перераспределение можно сделать с помощью функции низкого уровня realloc , которая резервирует память новой размерности и переписывает в нее содержимое старой области памяти (либо расширяет существующую) : Можно попытаться сделать динамический массив указателей " еще более динамическим " . Размерность массива указателей действительно определяется во время работы, но только однократно. Все равно по мере заполнения он может переполниться. В этом случае можно предусмотреть его " расширение " путем создания динамического массива указателей большей размерности и переписывания в него указателей из исходного массива. Такой массив указателей можно назвать ВИРТУАЛЬНЫМ МАССИВОМ. Подобная структура данных также называется коллекцией. Функции, включающие дополнительные указатели, должны проверять текущую размерность динамического массива указателей. При достижении значения, кратного N, к текущей размерности массива добавляется N элементов. Подобное решение, но для обычного динамического массива было приведено в 4.7. Динамические переменные и массивы - еще один шаг к профессиональному программированию. Здесь важно провести принципиальное различие между переменными, которые связаны с синтаксисом программы (статическими) и переменным, которые создаются в процессе ее работы (динамическими), а также обратить внимание на тот факт, что универсальная программа сама подстраивается под объем и размерность входных данных, и при этом эффективно использует память. Как известно, транслятор превращает вызов функции в команду процессора, в которой присутствует адрес этой функции. Если же функция внешняя, то это же самое делает компоновщик на этапе сборки программы (см.4.6). Это называется СТАТИЧЕСКИМ СВЯЗЫВАНИЕМ в том смысле, что в момент загрузки программы все связи между вызовами функций и самими функциями установлены. ДИНАМИЧЕСКИМ СВЯЗЫВАНИЕМ называется связывание вызова внешней функции с ее адресом во время работы программы. Это дает более компактный код программы, так как сама вызываемая функция может отсутствовать в памяти и загружаться только по мере необходимости. Само собой разумеется, что транслятор должен иметь встроенные механизмы поддержки таких функций и библиотек. Сам способ связывания носит название динамического связывания, а библиотеки -динамически связываемыми библиотеками (DLL - dynamic link library). Сам механизм основывается на использовании указателей на функции. Следующий пример раскрывает в первом приближении сущность процесса динамического связывания. Пример неправильного использования goto: Здесь имеется неявный внешний цикл, который не был замечен при проектировании программы. Исправленный вариант имеет вид: Рассмотрим возможные операции над массивом указателей на строки: Из последнего примера видно, что синтаксис операции по работе с символами строк с использованием массива указателей идентичен синтаксису такой же операции в двумерном массиве. Очевидно, что этим подчеркивается единство логической организации двух структур данных. Но при этом не следует забывать, что на самом деле физическая их реализация различна. Существует другая, весьма удобная, хотя и не столь эффективная форма представления данных, позволяющая выполнять арифметические операции произвольной точности. Она основана на сохранении десятичной системы счисления, при этом операции производятся над каждой цифрой числа отдельно с учетом взаимного влияния десятичных разрядов через переносы, заемы и т.п.. Возможны два варианта представления десятичных цифр : -отдельная цифра представлена 4 битами (тетрадой, шестнадцатеричной цифрой) ; -цифра задана символом во внешней форме представления, а само число - текстовой строкой. Например, число 17665 выглядит в этих формах представления следующим образом : В качестве иллюстрации технологии работы с отдельными цифрами числа в десятичной системе счисления рассмотрим пример функции, добавляющей 1 к числу во внешней форме представления, то есть в виде текстовой строки. Добавление 1 состоит в поиске первой цифры, отличной от 9, к которой добавляется 1. Все встречающиеся " на пути" цифры 9 превращаются в 0. Если процесс " превращения девяток" доходит до конца строки, то производится расширение строки следующей цифрой 1. Указанное свойство позволяет применить в двоичном дереве алгоритм двоичного поиска. Действительно, каждое сравнение искомого значения и значения в вершине двоичного дерева позволяют выбрать для следующего шага правое или левое поддерево. Алгоритмы включения и исключения вершин дерева не должны нарушать указанное свойство: при включении вершины дерева поиск места ее размещения производится путем аналогичных сравнений: Двоичное дерево имеет также естественное представление и в обычном массиве.
Поскольку функции работы с файлами относятся к библиотечным, то эта область программирования меньше всего связана со стандартами языка. Поэтому каждый дает ее описание "как бог на душу положит". Есть однако общий подход, который заключается в том, что с точки зрения программиста двоичный файл представляет собой полный аналог внутренней памяти программы, который, в отличие от обычных переменных, не распределяется и не управляется на транслятором, не исполнительной системой. Поэтому понимание того, что распределение памяти в двоичном файле ведется программой, а все остальное в принципе не отличается от работы с обычными структурами данных - поставлено во главу угла. Последовательно рассматриваются: работа с отдельными переменными в файле, массивами (файл записей фиксированной длины), массивами переменной размерности (файл записей переменной длины), таблицами переменной размерности. Затем отдельно вводится аналогия указателю - указатель в файле, на основе которого рассматриваются все структуры данных - массивы указателей, списки и деревья. Также рассматриваются различные варианты работы с файлом - полная загрузка структуры данных, поэлементная загрузка, кэширование. Существует и другое представление файла в программе, отличное от текстового. В нем файл выступает как аналог внутренней памяти компьютера и организован соответствующим образом. Подобно тому, как в памяти программы размещаются статические переменные в процессе трансляции и динамические - во время работы программы, так и программа может размещать в таком файле любые структуры данных. Файл -образ памяти носит название ДВОИЧНОГО ФАЙЛА ПРОИЗВОЛЬНОГО ДОСТУПА и имеет следующие свойства: При работе с файлом в режиме двоичного файла произвольного доступа используются функции стандартной библиотеки ввода-вывода. Прежде всего, при открытии или создании нового файла необходимо указать режим работы с файлом: С открытым файлом связано понятие ТЕКУЩЕЙ ПОЗИЦИИ (позиционера). Текущей позицией называется номер байта, начиная с которого производится очередная операция чтения - записи. При открытии файла текущая позиция устанавливается на начало файла, после чтения-записи порции данных перемещается вперед на размерность этих данных.
Список, в котором каждый элемент имеет указатели на последующий и предыдущий, называется двусвязным. Его основное отличие от односвязного -возможность просмотра его в обоих направлениях. Основное применение двусвязных списков - создание упорядоченных последовательностей элементов при достаточно частых операциях включения и исключения. В качестве примера рассмотрим включение в список нового элемента с сохранением упорядоченности значений. Наибольшие проблемы при работе со списками возникают при перемещении указателей в процессе перестановки элементов списков. Эти операции можно интерпретировать несколькими способами . 1. . Смысловая интерпретация присваивания указателя. При работе со списками каждый указатель имеет определенный смысл - первый, текущий, следующий, предыдущий и т.п. элементы списка. Поля pred,next также интерпретируются как указатели на следующий и предыдущий элементы списка в доступном через указатель. Тогда смыл присваивания указателей " один в один" переводится в словесное описание, как это было в приводимых выше комментария.
С учетом всего изложенного выше - "бросаем ребенка в воду". Предлагается простая программа, которая со всех сторон "обсасывается" примерно на 10 страницах, чтобы показать, как все соединяется между собой. Программа рассматривается с 6-7 сторон - переменные, операции, операторы, функции, ввод и вывод. Семестр 4 Нас прежде всего будет интересовать, какими свойствами обладают переменные и функции, определенные в различных точках модуля-файла, как они "узнают" о существовании друг друга и как они взаимодействуют между собой. Для начала сформулируем ряд необходимых терминов. ОПРЕДЕЛЕНИЕ ПЕРЕМЕННОЙ ИЛИ ФУНКЦИИ - - процесс создания программного эквивалента переменной или функции транслятором по их описанию в программе (трансляция во внутреннее представление) Определение функции включает в себя заголовок функции и тело, определение переменной -обычное контекстное определение и, возможно, ее инициализацию. В определении переменной: В определении функции: Например, определение размещается далее по тексту в текущем модуле или находится в другом модуле.
Структура файла записей фиксированной длины -обычный массив переменных одного типа. Соответственно записи в нем последовательно нумеруются, начиная с 0, номер записи является индексом массива, смещение (адрес) записи с номером n определяется как n * sizeof (тип данных записи). Количество записей в файле определяется путем деления его размера на размер записи. Структура файла может быть усложнена с целью обеспечения его большей универсальности и устойчивости к ошибкам выполнения операций ввода-вывода и программирования. Например, в начало файла можно поместить переменные, содержащие размер записи и количество их в файле. Файл записей переменной длины -это простая последовательность записей переменной длины, не содержащая какой-либо дополнительной информации о расположении записей. По своей природе такой файл является файлом последовательного доступа, поскольку определить адрес любой записи без знания размерности всех предыдущих не представляется возможным. Имеется два способа хранения записей переменной длины в файле: -в первом случае, используется специальное значение или код -ограничитель записи. Типичным примером является строка текста в памяти, имеющая в качестве ограничителя символ '\0' , который в файле превращается в последовательность символов ' \r' и '\n'. В этом смысле обычный текстовый файл при работе с ним построчно есть файл записей переменной длины; В качестве примера рассмотрим запись переменной длины, состоящую из фиксированной части - структуры и переменной части - строки, связанной с ней через указатель. Классы файловых потоков : Флаги режимов работы с файлом : Конструкторы объектов (для классов ifstream,ofstream,fstream) и функции открытия/закрытия файлов : Унаследованные переопределения операторов позволяют проверять наличие ошибок в потоках в виде : Функции ввода-вывода данных файловых потоков (унаследованы из istream,ostream и iostream) : Как же следует вести разработку рекурсивного алгоритма, " не вспоминая о прошлом" и " не заглядывая в будущее" . Опираясь на метод математической индукции можно сформулировать следующие правила : Проблема "Что первично -курица или яйцо?" применительно к программированию звучит как "Что первично: алгоритм (процедура, функция) или обрабатываемые им данные". В традиционной технологии программирования взаимоотношения процедуры -данные имеют более-менее свободный характер, причем процедуры (функции) являются ведущими в этой связке: как правило, функция вызывает функцию, передавая данные друг другу по цепочке. Соответственно, технология структурного проектирования программ прежде всего уделяет внимание разработке алгоритма. Как видно из рисунка, цепочка вызовов функций является основной в схеме взаимодействия элементов программы, взаимосвязь элементов данных определяется характером передачи параметров между функциями. Поэтому традиционную технологию программирования можно назвать программированием "от функции к функции". В технологии ООП взаимоотношения данных и алгоритма имеют более регулярный характер: во-первых, класс объединяет в себе данные (структурированная переменная) и методы (функции). Во-вторых, схема взаимодействия функций и данных принципиально иная. Метод (функция), вызываемый для одного объекта, как правило, не вызывает другую функцию непосредственно. Для начала он должен иметь доступ к другому объекту (создать, получить указатель, использовать внутренний объект в текущем и т.д.), после чего он уже может вызвать для него один из известных методов. Таким образом, структура программы определяется взаимодействием объектов различных классов между собой. Как правило, имеет место иерархия классов, а технология ООП иначе может быть названа как программирование "от класса к классу". Основная же особенность каждого указателя состоит в том, что он дает " степень свободы" или универсальности любому алгоритму обработки данных. Действительно, если некоторый фрагмент программы получает данные непосредственно в некоторой переменной, то он может обрабатывать ее и только ее. Если же данные он получает через указатель, то обработка данных (указуемых переменных) может производиться в любой области памяти компьютера (или программы). При этом сам фрагмент может и " не знать" , какие данные он обрабатывает, если значение самого указателя передано программе извне. Собирательный пример: база данных в файле - таблица произвольного формата с различными типами данных, средствами упорядочения и индексации. Реализовано в виде систем классов. До сих пор мы ограничивались краткими примерами и фрагментами программ. Теперь попробуем разработать программу, в которой найдут достойное отражение большинство разделов этой книги: алгоритмы сортировки и поиска, структуры данных, работа с двоичными файлами произвольного доступа, представление отдельных типов данных в виде классов, технология ООП: иерархия классов и виртуальные функции. Коль скоро формальные параметры функции являются псевдопеременными, которые при вызове содержат копии фактических параметров, на них распространяются все соглашения о типах данных и переменных. В частности, в заголовке функции используется стандартный контекстный способ определения типа переменной: абстрактными типами данных Исторически сложилось, что первоначальный синтаксис определения функции принципиально исключал возможность контроля транслятора за соответствием количества и типов формальных и фактических параметров. С одной стороны, это позволяло использовать механизм вызова функций и передачи параметров нестандартными способами, а, с другой стороны, являлось причиной многочисленных, трудно обнаруживаемых ошибок. В связи с этим был введен новый синтаксис определения, а также объявления функции, называемый прототипом. Если вызывается функция, определенная или объявленная по прототипу, то транслятор проверяет соответствие формальных и фактических параметров и, по возможности, выполняет неявные преобразования типов. Прототип отличается от традиционного синтаксиса следующим: -если функция не имеет параметров, то в объявлении по прототипу в скобках должен быть указан тип void (чтобы не спутать с традиционным синтаксисом объявления). Входные документы, представленные в RTF-формате, должны быть расположены в одном каталоге. При этом обеспечивается поддержка взаимных ссылок на термины. Допускается наличие таблиц, не содержащих объединенные ячейки (то есть обычных прямоугольных). База данных содержит таблицу СТИЛИ, в которой находится список форматируемых стилей и соответствующих им тегов HTML. Абзацы, выполненные другими стилями, игнорируются. Форматер объединяет последовательность абзацев, выполненных одним стилем, в один общий тэг. Первоначально в форматере установлены следующие стили :
Для примера рассмотрим функцию включения нового элемента в качестве потомка к вершине, максимально близкой к началу. Здесь есть проблема, общая для всех деревьев и рекурсивных алгоритмов. Войдя в поддерево, невозможно производит какие-либо действия для вершин, расположенных на том же уровне, но в других поддеревьях. Поэтому задача решается так : используется функция включения с просмотром дерева на заданную глубину, а сама глубина просмотра, в свою очередь, задается равной длине минимальной ветви дерева.
//------------------------------------------------------bk55-04.cpp
//------Включение вершины в дерево на заданную глубину
int Insert(tree *ph, int v, int d)
// результат логический - вершина включена
// ph - указателя на текущую вершину
// d - текущая глубина включения
{
if (d == 0) return 0; // Ниже не просматривать
for ( int i=0; i< 4; i++)
if (ph- >child[i] == NULL)
{
tree *pn=new tree;
ph->child[i]=pn;
pn->val=v;
for (i=0; i< 4; i++) p n->child[i]=NULL;
return 1;
}
else
if (Insert(ph->child[i], v , d-1)) return 1;
return 0;
}
void main()
{
tree PH={ 1,{NULL,NULL,NULL,NULL}}; // пример вызова функции
Insert( &PH, 5, MinDepth( &PH));
}
Здесь впервые всплывает на поверхность свойство дерева, ради которого оно, собственно говоря, и используется. Оно соответствует пословице " Дальше в лес - больше дров" . Точнее, количество просматриваемых вершин от уровня к уровню растет в геометрической прогрессии. Отсюда следует, как можно эффективно использовать деревья для поиска данных : если включать в вершины дерева данные таким образом, что наиболее часто используемые будут располагаться ближе к корню, и при этом анализ текущий вершин позволит сделать вывод о том, стоит ли " опускаться" в поддеревья. Рассмотрим пример. Допустим, дерево, каждая вершина которого содержит строку, организовано так, что самая короткая строка в поддереве находится в корневой вершине. Тогда при поиске слова в дереве будет соблюдаться принцип - чем оно короче, тем меньше вершин будет просмотрено.
//------------------------------------------------------bk55-05.cpp
struct tree1
{
char *key; // Ключевое слово
char *data; // Искомая информация
tree1 *child[4]; // Потомки
};
char *find(tree1 *ph, char *keystr)
{ char *s;
if (ph==NULL) return NULL;
if (strcmp(ph->key,keystr)==0) return ph->data;
// Вершина найдена
if (strlen(keystr)<strlen(ph->key)) return NULL;
// Короткие строки - ближе к корню
for (int i=0; i< 4; i++)
if ((s=find(ph->child[i],keystr))!=NULL) return s;
return NULL;
}
Функция включения в такое дерево ради сохранения свойств дерева при включении новой строки должна " вытеснять" более длинные строки из текущих вершин в поддеревья и заменять их на новые, более короткие.
Динамические массивы
//------------------------------------------------------bk47-01.cpp
// функция возвращает указатель на создаваемый
// динамический массив
int *GetArray()
{
int N,i; // Размерность массива
int *p; // Указатель на массив
cout >> "Элементов в массиве:"; // в динамической памяти
cin << N;
if ((p = new int[N + 1]) == NULL)
return(NULL); // или malloc((N+1)*sizeof(double))
for (i=0; i<N; i++)
{
cout << i << "-ый элемент :";
cin >> p[i];
}
p[i] = 0; // В конце последовательности
return(p); // данных - 0
} // Вернуть указатель
Естественно, что строка должна где-то храниться. В идеальном случае при любой операции над строкой создается динамический массив символов, размерность которого равна длине строки.
//------------------------------------------------------bk47-02.cpp
char *TwoToOne(char *p1, char *p2)
{ // Объединить две строки в одну
char *out; // Результат - динамический массив
int n1,n2;
for (n1=0; p1[n1]!='\0'; n1++);
for (n2=0; p2[n2]!='\0'; n2++);
out = new char [n1+n2+1];
if (out == NULL) return(NULL);
for (n1=0; p1[n1]!='\0'; out[n1++] = p1[n2++]);
for (n2=0; p2[n2]!='\0'; out[n1++] = p1[n2++]);
out[n1] = '\0';
return(out);
}
Динамические массивы и проблемы размерности данных
-самый неэффективный вариант : под каждый вид данных зарезервировать память заранее " по максимуму" . Применительно к массиву это означает, что мы заранее выбираем такую размерность, которая никогда не будет превышена. Но тем не менее, такое " никогда" рано или поздно может случиться , поэтому процесс заполнения массива лучше контролировать ;
-приемлемый вариант может быть реализован, если в какой-то момент времени выполнения программа " узнает" , какова в этот раз будет размерность обрабатываемых данных. Тогда она может создать динамический массив такой размерности и работать с ним. К сожалению, такое " знание" не всегда возможно ;
-идеальный вариант заключается в создании такой структуры данных, которая автоматически увеличивает свою размерность при ее заполнении. К сожалению, ни язык, ни библиотека здесь не помогут - такой массив можно реализовать только программно, по справедливости назвав ВИРТУАЛЬНЫМ МАССИВОМ.
//------------------------------------------------------bk47-03.cpp
// функция возвращает указатель на создаваемый
// динамический массив. Размерность массива увеличивается
// при заполнении с кратностью N - N, 2N, 3N ...
#define N 10
int *GetArray()
{
int i , *p; // Указатель на массив
p = new int[N]; // Массив начальной размерности
for (i=0; 1; i++)
{
cout << i << "-ый элемент";
cin >> p[i];
if ((i+1)%N==0) // Массив заполнен ???
{ // Создать новый и переписать
int *q=new int[i+1+N];
for (int j=0; j<=i; j++)
q[j]=p[j];
delete p; p=q; // Считать новый за старый ,
} // а старый уничтожить
if (p[i]==0) return p; // Ограничитель ввода - 0
}
}
p = (int*) realloc(p,sizeof(int)*(i+1+N));
Динамические массивы указателей переменной размерности
//------------------------------------------------------bk52-03.cpp
// Текущее количество указателей
int size(double **p)
{ for (int i=0; p[i]!=NULL; i++);
return i;
}
// Увеличение размерности виртуального массива
// при его возможном переполнении
#define N 20
double **extend(double **p)
{
if (p==NULL) // Создать динамический МУ при отсутствии
{p=new double*[N]; p[0]=NULL; return p;}
int k=size(p);
if ((k+1)%N==0) // количество указателей будет кратно N
{ // создать новый ДМУ
double **q=new double*[k+1+N];
for (int i=0; i<=k; i++)
q[i]=p[i]; // переписать указатели
delete p; // уничтожить старый ДМУ
return q; // вернуть новый ДМУ
}
return p;}
// Добавить указатель в конец ДМУ
double ** append(double **p, double *s)
{
double **q=extend(q); // при необходимости перераспределить
int k=size(q); // память
q[k]=s; q[k+1]=NULL; return q;
}
// Добавить указатель по номеру
double ** append(double **p, double *s , int n)
{
double **q=extend(q); // при необходимости перераспределить
int k=size(q); // память
if (n>=k) { q[k]=s; q[k+1]=NULL; } // включить последним
else
{
for (int j=k; j>=n; j--) q[j+1]=q[j];
q[n]=s;
}
return q;}
// Пример вызова
void main()
{ double a1,a2,a3,**vm =NULL;
vm=append( vm,&a1) ;
vm= append( vm,&a2) ;
vm= insert( vm,&a3,0) ;
}
Динамические переменные и массивы
Динамическое связывание
//------------------------------------------------------bk56-03.cpp
#include <math.h>
typedef void (*VPTR)();
typedef double (*DPTR)(double);
struct ENTRY // описание точки входа функции
{
char name[20];
VPTR pf;
};
struct ENTRY DLL[] = // массив точек входа
{
{"sin", (VPTR)sin}, {"cos", (VPTR)cos,},
{"tan", (VPTR)tan}, {"",NULL}};
// Поиск указателя на функцию по имени функции
// (функция возвращает указатель на функцию вида void f())
VPTR DLink(ENTRY *pDLL, char *ps)
{ int n;
for (n=0; pDLL[n].pf !=NULL; n++)
if (strcmp(pDLL[n].name,ps) == 0)
return pDLL[n].pf;
return NULL;
}
void main()
{
double (*pa1)(double), (*pa2)(double);
//----- Динамическое связывание и вызов функций
if ((pa1 = (DPTR)DLink(DLL,"a1")) == NULL) return;
if ((pa2 = (DPTR)DLink(DLL,"a2")) == NULL) return;
y = (*pa1)(2.0) + (*pa2)(x);
}
Допустимые случаи использования оператора goto
retry: for(...) { for (...)
{...
if () goto retry;...
if () goto fatal; }
}
fatal:
m1: for (i=0; i<n; i++)
{..if () goto m1;... }
while (1)
{ for (i=0; i<n; i++)
{ ...if () break;... }
if (i==n) break;
}
Дуализм двумерного массива и массива указателей
char *p[20];
p[i] // указатель на i-ю строку
p[i]++ // перейти в i-й строке с следующему символу
*(p[i] + j) // j-й символ в i-ой строке
p[i][j] // j-й символ в i-ой строке.
Двоично-десятичная арифметика
char s[]=" 17665" ;
long ss=0x00017655;
//------------------------------------------------------bk48-05.cpp
void inc(char s[])
{
for (int i=0; s[i]!=0; i++); // Поиск конца строки
for (int n=i-1; n>=0; n--) // Младшая цифра - в конце
{
if (s[n]== 9 ) // 9 превращается в 0
s[n]= 0 ;
else { s[n]++; return; } // добавить 1 к цифре и выйти
}
for (s[i+1]=0; i> 0; i--) // Записать в строку 1000...
s[i]= 0 ;
s[0]= 1 ;}
Двоичное дерево
struct btree
{
int val;
btree *left,*right;
};
//------------------------------------------------------bk55-06.cpp
//----- Рекурсивный поиск в двоичном дереве---------------
// Возвращается указатель на найденную вершину
btree *Search(btree *p, int v)
{
if (p==NULL) return(NULL); // Ветка пустая
if (p->val == v) return(p); // Вершина найдена
if (p->val > v) // Сравнение с текущим
return(Search(p->left,v)); // Левое поддерево
else
return(Search(p->right,v)); // Правое поддерево
}
//----- Включение значения в двоичное дерево--------------
// функция возвращает указатель на созданную вершину,
// либо на существующее поддерево
btree *Insert(btree *pp, int v)
{
if (pp == NULL) // Найдена свободная ветка
{ // Создать вершину дерева
btree *q = new btree; // и вернуть указатель
q->val = v;
q->left = q->right = NULL;
return q;
}
if (pp->val == v) return pp;
if (pp->val > v) // Перейти в левое или
pp->left=Insert(pp->left,v); // правое поддерево
else
pp->right=Insert(pp->right,v);
return pp;
}
void main()
{
btree *ss=Search(ph,5); // пример вызова
ph=Insert(ph,6);
}
Так, если вершина дерева имеет индекс n в массиве, то вершины левого и правого поддерева - 2*n и 2*n+1 соответственно. Головная вершина дерева имеет индекс, равный 1 . Кроме того, в таком массиве необходимо как-то обозначать пустые вершины (аналог указателя NULL).
Поиск в двоичном дереве требует количества сравнений, не превышающего максимальной длины ветви дерева, или максимальной длины цепочки его вершин. Следовательно, условием эффективности поиска в дереве является равенство длин его ветвей (сбалансированность). В самом крайнем случае дерево имеет одну ветвь и вырождается в односвязный список, в котором имеет место последовательный поиск. В идеальном случае, когда длины ветвей дерева отличаются не более, чем на 1 (сбалансированное дерево) и равны n или n-1 , при общем количестве вершин в дереве порядка " 2 в степени n " требуется не более n сравнений для нахождения требуемой вершины. Это соответствует характеристикам алгоритма двоичного поиска в упорядоченном массиве.
Таким образом, необходимым условием эффективного использования двоичного дерева для быстрого поиска является его сбалансированность. Поддержание сбалансированности при операциях включения и исключения является довольно трудной задачей.
Двоичное дерево может использоваться также для сортировки последовательности данных. Если производить включение в двоичное дерево последовательности элементов в порядке их поступления, то затем обычный рекурсивный обход этого дерева даст их упорядоченную последовательность:
//------------------------------------------------------bk55-07.cpp
// Рекурсивный обход двоичного дерева с выводом
// значений вершин в порядке возрастания
void Scan(btree *p)
{
if (p==NULL) return;
Scan(p->left);
cout << p->val << endl;
Scan(p->right);
}
void operator()(void* pnew,int (*cmp)(void*,void*))
{
if (data==NULL) { data=pnew; return; }
int n=(*cmp)(key,data);
if (n==0) return;
if (n < 0)
{
if (l==NULL) l=new btree;
(*l)(pnew,cmp);
}
else
{
if (r==NULL) r=new btree;
(*r)(pnew,cmp);
}
}
Простейшим случаем введения иерархии в систему классов является использование объектов ранее определенных классов в качестве элементов данных нового класса. Взаимодействие классов в этом случае ограничивается тем, что новый класс использует стандартный интерфейс объекта: функции-элементы и переопределенные операции, то есть работает с ним как с любым другим базовым типом данных. С абстрактной точки зрения элемент данных -объект класса представляет собой некоторое частное свойство более сложного класса, в котором он определен. Рассмотрим в качестве примера класс man -информация о человеке, включающая в себя даты рождения и поступления на работу:
class man
{
char name[20]; // Другие элементы класса
char *address;
dat dat1; // Дата рождения
dat dat2; // Дата поступления на работу
public: ...
man(char*); // Конструктор
};
Единственной проблемой здесь является конструирование объектов -элементов данных. Здравый смысл подсказывает, что если в конструкторе нового класса не содержится информации о конструировании объектов -элементов данных, то по умолчанию для них возможен только вызов их собственных конструкторов без параметров, причем перед вызовом конструктора нового класса. Действительно, последний может воспользоваться некоторыми свойствами уже инициализированных внутренних объектов.
Если все-таки требуется использовать конструкторы внутренних объектов с параметрами, то в заголовке конструктора нового класса их необходимо явно перечислить. Их параметры могут быть любыми выражениями, включающими формальные параметры конструктора нового класса:
class man
{
char name[20]; // Другие элементы класса
dat dat1; // Дата рождения
dat dat2; // Дата поступления на работу
public:
man(char *,char *,char *); // Конструкторы
man(char *);
};
//----- Конструктор класса man с неявным вызовом ----------
// конструкторов для dat1 и dat2 без параметров
man::man(char *p) { ... }
//----- Конструктор класса man с явным вызовом ------------
// конструкторов для dat1 и dat2 с параметрами
man::man(char *p,char *p1, char *p2) : dat1(p1), dat2(p2)
{ ... }
// Вызов конструктора для объекта dat1
// В качестве параметра передается строка -
// второй параметр вызова
// конструктора для класса man Вызов конструктора для объекта dat2
void main()
{
man JOHN("John","8-9-1958","15-1-1987");
// 1. Строка конструктора man
// 2. Строка передается конструктору объекта dat1 в объекте man
// 3. Строка передается конструктору объекта dat2 в объекте man
}
Другой способ создания иерархии классов заключается в том, что новый класс автоматически включает в себя все свойства старого класса, а затем развивает их. С абстрактной точки зрения старый класс определяет только общие свойства, а новый -конкретизирует более частные свойства.
Сохранение с новом классе свойств старого называется НАСЛЕДОВАНИЕМ . Принцип наследования состоит в том, что элементы данных старого класса автоматически становятся элементами данных нового класса, а все функции-элементы старого класса применимы к объекту нового класса, точнее к его старой составляющей.
Старый класс при этом называется БАЗОВЫМ КЛАССОМ (БК), новый - ПРОИЗВОДНЫМ КЛАССОМ (ПК).
Синтаксис определения производного класса имеет вид:
class производный : базовый_1, базовый_2,...базовый_n
{ определение личной и общей частей производного класса
}
Перечислим основные свойства базового и производного классов:
-объект базового класса определяется в производном классе как неименованный. Это значит, что он не может быть использован в явном виде как обычный элемент данных;
-элементы данных базового класса включаются в объект производного класса (как правило, транслятор размещает их в начале объекта производного класса). Однако личная часть базового класса закрыта для прямого использования в производном классе;
-функции-элементы базового класса наследуются в производном классе, то есть вызов функции, определенной в базовом классе возможен для объекта производного класса и понимается как вызов ее для входящего в него объекта базового класса;
-в производном классе можно переопределить (перегрузить) наследуемую функцию, которая будет вызываться вместо нее. При этом для выполнения соответствующих действий над объектом базового класса она может включать явный вызов переопределенной функции по полному имени.
Сказанное проиллюстрируем весьма условным примером определения производного класса:
class a
{
public:
void f() {}
void g() {}
};
// производный класс : базовый класс
class b : a
{
public:
void f() // "f" переопределяется
{ ...
a::f(); // явный вызов "f" для БК
} // "g" наследуется из БК
void h() {} // собственная функция в ПК
};
void main()
{
a A1;
b B1;
B1.f(); // вызов переопределенной b::f()
B1.g(); // вызов наследуемой a::f()
}
Предполагается, что при вызове в производном классе функций, наследуемых из базового, транслятор производит преобразование указателя this объекта производного класса в указатель на входящий в него объект базового класса, учитывая размещение второго в первом.
Взаимоотношение конструкторов и деструкторов базового и производного классов аналогичны описанным выше:
-если конструктор производного класса определен обычным образом, то сначала вызывается конструктор базового класса без параметров, а затем конструктор производного класса. Деструкторы вызываются в обратном порядке -сначала для производного, затем для базового;
-в заголовке конструктора производного класса может быть явно указан вызов конструктора базового класса с параметрами. Он может быть без имени, а может быть с именем базового класса. Если базовых классов несколько, то в вызовы конструкторов базовых классов должны быть перечислены через запятую и должны быть с поименованы.
Принцип наследования следует воспринимать прежде всего в рамках программирования " от класса к классу" .
При проектировании производного класса определяется потенциальное множество объектов с новыми свойствами, отличными от свойств объектов базового класса. Внешне наблюдаемые свойства объекта - это его методы. Поэтому перечисленные ниже варианты наследования методов базового класса в производном нужно воспринимать с более широкой точки зрения - как способы изменения свойств объекта.
1. l " Новое свойство" . Имя определяемого в производном классе метода не совпадает ни с одним из известных в базовом классе. В этом случае это - " новое свойство" объекта, которое объект приобретает в производном классе.
class a {
public: void f() {}
};
class b : public a
{
public: void newb() {} // newb() - новое свойство (метод)
};
2.l " Полное неявное наследование" . Если в производном классе метод не переопределяется, то по умолчанию он наследуется из базового класса. Это значит, что он может быть применен к объекту производного класса, при этом будет вызван метод для базового класса, причем именно для объекта базового класса, включенного в производный. Определенное в базовом классе свойство не меняется.
class a {
public: void f() {}
};
class b : public a
{
public: // f() - унаследованное свойство (метод)
}; // эквивалентно void f() { a::f(); }
3. l " Полное перекрытие" . Если в производном классе определяется метод, совпадающий с именем с методом базового класса, причем в теле метода отсутствует вызов одноименного метода в базовом классе, то мы имеем дело с полностью переопределенным свойством. В этом случае свойство объекта базового класса в производном классе отрицается, а метод производного класса " перекрывает" метод базового.
class a {
public: void f() {}
};
class b : public a
{
public:
void f() {...} // переопределенное свойство (метод)
};
4. l " Условное наследование" . Наиболее точно отражает сущность наследования последний вариант, в котором в производном классе переопределяется метод, перекрывающий одноименный метод базового класса.
Но в методе базового класса обязательно имеется вызов перекрытого метода базового класса - условный или безусловный. Этот прием наиболее полно соответствует принципу развития свойств объекта, поскольку свойство в производном классе является усложненным вариантом аналогичного свойства объекта базового класса.
class a {
public: void f() {}
};
class b : public a
{
public:
void f()
{... a::f(); .... }
// Переопределенное свойство развивает соответствующее свойство объекта
// базового класса. Переопределенный метод в явном виде вызывает метод
// в базовом классе по его полному имени.
};
Производный класс включает в себя как личную, так и общую части базового класса. При этом важно, в какую часть производного класса, личную или общую, они попадут. От этого зависит доступность элементов базового класса, как из функций-элементов производного класса, так и извне - через объекты производного класса. Здесь возможны следующие варианты:
-личная часть базового класса A всегда включается в личную часть производного класса B, но при этом непосредственно недоступна в классе B. Это соответствует общему принципу защиты данных класса от вмешательства извне.
-по умолчанию, то есть при использовании заголовка class B : A { } общая часть класса A попадает в личную часть класса B. Это значит, что функции-элементы класса A доступны из функций -элементов класса B, но не могут быть вызваны извне при обращении к объектам класса B. То есть для внешнего пользователя класса B интерфейс класса A закрывается;
-при использовании заголовка class B : public A { } общая часть класса A попадает в общую часть класса B, и внешнему пользователю при работе с объектами класса B доступны интерфейсы обоих классов;
-и наконец, в определении общей части класса B можно явно указать функции-элементы (а также данные) общей части базового класса A, которые попадают в общую часть класса B
class B : A
{
public:
public A::fun;
} ;
Из рассмотренных вариантов видно, что личная часть базового класса недоступна в любом производном классе -это естественно следует из свойств закрытости определения класса.
Однако по аналогии с дружественностью базовый класс может разрешить доступ к своим элементам личной части в производных классах. Это делается при помощи объявления защищенных (protected) элементов.
Элемент с меткой protected в базовом классе входит в личную часть базового класса. Кроме того, он доступен и в личной части производного класса. Если же базовый класс включается в производный как public, то защищенный элемент становится защищенным и в производном классе, то есть может использоваться в последующих производных классах.
Сказанное поясним примером:
class A
{
int a1; // Обычный личный элемент
protected:
int a2; // Защищенный личный элемент
public:
};
//----- Вариант 1: наследование без public ---------------
class B : A // a1,a2 в личной части B
{
void x();
};
void B::x()
{
a1 = 5; // Ошибка: a1 недоступен в B
a2 = 3; // a2 доступен в личной части B
}
//----- Вариант 2: наследование с public ------------------
class B : public A // a2 доступен и защищен в личной
{ // части B, неявно имеет место protected: int a2;
};
Применительно к базовому и производному классу можно сказать, что, преобразуя указатель на объект производного класса к указателю на объект базового класса, мы получаем доступ к вложенному объекту базового класса. Но при такой трактовке преобразования типа указателя транслятору необходимо учитывать размещение объекта базового класса в производном, что он и делает. В результате значение указателя (адрес памяти) на объект базового класса может оказаться не равным исходному значению указателя на объект производного. Ввиду " особости" такого преобразования оно может быть выполнено в Си++ неявно (остальные преобразования типов указателей должны быть явными).
Побочный эффект такого преобразования состоит в том, что транслятор "забывает" об объекте производного класса и вместо переопределенных в нем функций вызывает функции базового:
class A
{
public: void f1();
};
class B : A
{
public: void f1(); // Переопределена в классe B
void f2();
};
A *pa;
B *pb;
B x;
pa = &x;
// Неявное преобразование указателя на объект
// класса B в указатель на объект класса A
pa->f1();
// Вызов функции из вложенного объекта базового
// класса A::f1(), хотя она переопределена
Обратное преобразование от указателя на базовый класс к указателю на производный может быть сделано только явно. Преобразование будет корректно, если данный объект базового класса действительно входит в объект того производного класса, к типу указателя которого оно выполняется:
pb = (B*) pa; // Обратное преобразование - явное
pb ->f2(); // Корректно, если под "pa" был объект класса "B"
С понятием производных классов тесно связан ПОЛИМОРФИЗМ. Прежде всего, полиморфизм -это свойство функции определенной в множестве производных классов, построенных на основе общего базового. В каждом из классов функция может быть переопределена, а может быть унаследована из базового. Свойство полиморфности заключается в том, что при отсутствии полной информации о том, к какому из классов относится объект, функция в состоянии идентифицировать его класс и корректно выполниться в этом классе. Важнейшее следствие полиморфности -возможность организовать регулярный процесс обработки объектов группы производных классов.
Сформулируем теперь свойство полиморфности уже с использованием терминов Си++. Пусть имеется базовый класс A и производные классы B,C. В классе А определена функция -элемент f(), в классах B,C -унаследована и переопределена. Пусть теперь имеется массив указателей на объекты базового класса -p. Он инициализирован как указателями на объекты класса A, так и на объекты производных классов B,C (точнее, на вложенные в них объекты базового класса A):
class a
{ ... void f(); };
class b : public a
{ ... void f(); };
class c : public a
{ ... void f(); };
a A1;
b B1;
c C1;
a *p[3] = { &B1, &C1, &A1 };
Как будет происходить вызов обычной неполиморфной функции при использовании указателей из этого массива ? Очевидно, что транслятор, располагая исключительно информацией о том, что указуемыми переменными являются объекты базового класса A (что следует из определения массива), вызовет во всех случаях функцию a::f().
То же самое произойдет, если обрабатывать массив указателей в цикле:
p[0]->f(); // Вызов a::f()
p[1]->f(); // во всех трех случаях
p[2]->f(); // по указателю на объект базового класса
for (i=0; i<=2; i++)
p[i]->f();
Наличие указателя на объект базового класса A свидетельствует о том, что в данной точке программы транслятор не располагает информацией о том, объект какого из производных классов расположен под указателем. Тем не менее, если функция является полиморфной, то при вызове ее по указателю на объект базового класса она должна идентифицировать его производный класс и вызвать переопределенную функцию именно для этого класса:
p[0]->f(); // вызов b::f() для B1
p[1]->f(); // вызов c::f() для C1
p[2]->f(); // вызов a::f() для A1
for (i=0; i<=2; i++) // вызов b::f(),c::f(),a::f()
p[i]->f(); // в зависимости от типа объекта
В Си++ полиморфная функция называется ВИРТУАЛЬНОЙ ФУНКЦИЕЙ.
Наиболее содержательным синонимом к термину полиморфная (виртуальная) функция является термин l " многоликая" . Действительно создается этот механизм виртуальной функции создает иллюзию функции " единой во многих лицах" - в каждом из производных классов. В то же время базовый класс позволяет объединить эти все разнородные функции под одним общим началом - объектом базового класса, включенным во все производные. Объект базового класса должен быть доступен через указатель только по той причине, что это единственный в Си механизм, позволяющий ссылаться на объекты неопределенного вида (объект одного из производных классов).
Таким образом, если при преобразовании типа " указатель на производный класс" к типу " указатель на базовый класс" происходит потеря информации о типе объекта производного класса, то при вызове виртуальной функции происходит обратный процесс неявного восстановления типа объекта.
Принцип реализации механизма виртуальных функций заключается в том, что объект базового класса должен содержать в себе информацию о преобразовании указателя на базовый класс в указатель на производный (смещение) и о функциях-элементах объекта производного класса (указатели на функции).
В простейшем случае это реализуется через массив указателей на функции. Для каждой пары производный класс -базовый класс транслятором генерируется свой массив указателей, каждой виртуальной функции соответствует в нем свое значение индекса. Указатель на массив (начальный адрес) записывается в объект базового класса в момент конструирования объекта производного класса. Проиллюстрируем сказанное средствами "классического" Си:
// Компоненты, создаваемые транслятором, обозначены " * **"
class A
{
void (**ftable)(); //* ** Указатель на массив
public: // указателей виртуальных функций
virtual void x();
virtual void y();
virtual void z();
A();
~A();
};
#define vx 0 //* ** Индексы в массиве
#define vy 1 //* ** указателей на
#define vz 2 //* ** виртуальные функции
// Массив адресов функций класса А
void (*TableA[])() = { A::x, A::y, A::z }; //***
A::A()
{ ftable = TableA; //* ** Установка массива для класса А
}
class B : public A
{
public:
void x();
void z();
B();
~B();
};
// Массив адресов функций класса A в B
// A::y - наследуется из А, B::x - переопределяется в B
void (*TableB[])() = { B::x, A::y, B::z }; //***
B::B()
{ A::ftable = TableB; // *** Установка таблицы для класса B
}
void main()
{
A* p; // Указатель p базового класса A
B nnn; // ссылается на объект производного класса B
p = &nnn;
p->z(); // *** реализация - (*(p->ftable[vz]))();
}
Виртуальной может быть не только обычная функция-элемент, но и переопределяемая операция.
Если базовый класс используется только для порождения производных классов, то виртуальные функции в базовом классе могут быть "пустыми", поскольку никогда не будут вызваны для объекта базового класса. Базовый класс в котором есть хотя бы одна такая функция, называется АБСТРАКТНЫМ. Виртуальные функции в определении класса обозначаются следующим образом:
class base
{
public:
virtual print()=0;
virtual get() =0;
};
Определять тела этих функций не требуется.
Множественным наследованием называется процесс создания производного класса из двух и более базовых. В этом случае производный класс наследует данные и функции всех своих базовых предшественников. Существенным для реализации множественного наследования является то, что адреса объектов второго и последующих базовых классов не совпадают с адресом объекта производного класса. Этот факт должен учитываться транслятором при преобразовании указателя на производный класс в указатель на базовый и наоборот:
class d : public a,public b, public c { };
d D1;
pd = &D1; // #define db sizeof(a)
pa = pd; // #define dc sizeof(a)+sizeof(b)
pb = pd; // pb = (char*)pd + db
pc = pd; // pc = (char*)pd + dc
pc
Такое действие выполняется компилятором как явно при преобразовании в программе типов указателей, так и неявно, когда в объекте производного класса наследуется функция из второго и последующих базовых классов. Для вышеуказанного примера при определении в классе bb функции f() и ее наследовании в классе "d" вызов D1.f() будет реализован следующим образом:
this = &D1; // Указатель на объект производного класса
this = (char*)this + db // Смещение к объекту базового класса
b::f(this); // Вызов функции в базовом классе
Механизм виртуальных функций при множественном наследовании имеет свои особенности. Во-первых, на каждый базовый класс в производном классе создается свой массив виртуальных функций (в нашем случае -для aa в d, для bb в d и для cc в d ). Во-вторых, если функция базового класса переопределена в производном, то при ее вызове требуется преобразовать указатель на объект базового класса в указатель на объект производного. Для этого транслятор включает соответствующий код, корректирующий значение this в виде "заплаты", передающей управление командой перехода к переопределяемой функции, либо создает отдельные таблицы смещений.
В процессе иерархического определения производных классов может получиться, что в объект производного класса войдут несколько экземпляров объектов базового класса, например:
class base {}
class aa : public base {}
class bb : public base {}
class cc : aa, bb {}
В классе cc присутствуют два объекта класса base. Для исключения такого дублирования объект базового класса должен быть объявлен виртуальным:
class a : virtual public base {}
class b : virtual public base {}
class c : public a, public b {}
a A1;
b B1;
c C1;
Объект обычного базового класса располагается, как правило, в начале объекта производного класса и имеет фиксированное смещение. Если же базовый класс является виртуальным, то требуется его динамическое размещение. Тогда в объекте производного класса на соответствующем месте размещается не сам объект базового класса, а указатель на него, который устанавливается конструктором. Для вышеприведенного примера получим такую картину:
Один из наиболее распространенных приемов использования виртуальных функции - создание базовых классов, объединяющих в единую группу различные классы на основе некоторого общего свойства. Базовый класс при этом заключает в себе общие свойства группы, а весь набор действий, которые одинаково применимы к объектам из любого класса, реализуется через виртуальные функции.
В качестве примера рассмотрим группу классов - типов данных. Допустим, проектируется база данных, предназначенная для хранения произвольных объектов (типов данных). Прежде всего, определяется ряд общих действий, которые обязательно должны быть выполнимы к объекте любого класса, чтобы он мог включаться в базу данных.
class ADT
{
public:
virtual int Get(char *)=0; // Загрузка объекта из строки
virtual char *Put()=0; // Выгрузка объекта в строку
virtual long Append(BinFile&)=0; // Добавить объект в двоичный файл
virtual int Load(BinFile&)=0; //
virtual int Type()=0; // Возвращает идентификатор
// типа объекта
virtual char *Name()=0; // Возвращает имя типа объекта
virtual int Cmp(ADT *)=0; // Сравнивает значения объектов
virtual ADT *Copy()=0; // Создает динамический объект -
// копию с себя
virtual ~ADT(){}; // Виртуальный деструктор
};
Как видим, базовый класс получился абстрактным, то есть его объект не содержит данных, а функции являются " пустыми" . Это значит, что объекты базового класса не могут создаваться в программе, а сам класс создан исключительно как " объединяющая идея" некоторого типа данных. В принципе, базовый класс может содержать данные и непустые функции, если в самой группе классов можно выделить некоторую общую часть.
Естественно, что при проектировании любого производного класса должен соблюдаться приведенный шаблон, то есть в первую очередь в нем должны быть реализованы виртуальные функции, которые поддерживают в нем перечисленные действия. Остальная часть класса может быть какой угодно, естественно, что она уже не может быть использована в общих функциях работы с базой данных.
Базовый класс и набор виртуальных функций используются как общий интерфейс доступа к объектам - типам данных при проектировании базы данных. Любое множество объектов, для которых осуществляются основные алгоритмы базы данных (хранение, включение, исключение, сортировка, поиск и т.д.) будут представлены как множество указателей на объекты базового класса ADT , за которыми могут " скрываться" объекты любых производных классов. Естественно, что все действия, выполняемые над объектами будут осуществляться через перечисленные виртуальные функции. В качестве примера рассмотрим фрагмент класса - массив указателей, который здесь выступает аналога базы данных.
#include "ADT.h"
// Динамический массив указателей ADT*
class MU
{
int sz;
ADT **p;
public:...
ADT *min(); // Поиск минимального
void sort(); // Сортировка
int test(); // Проверка на идентичность типов
};
// Вызов виртуальных функций отмечен " ***"
int MU::test()
{
for (i=1; p[i]!=NULL; i++)
if (p[i]->Type()!=p[i-1]->Type()) return 0; //***
return 1;
}
ADT *MU::min()
{ ADT *pmin; int i;
if (p[0]==NULL || !test()) return NULL;
for (i=0, pmin=p[0]; p[i]!=NULL; i++)
if (pmin->Cmp(p[i]) > 0) pmin=p[i]; //***
return pmin;
}
void MU:sort()
{ int d,i; void *q;
if (p[0]==NULL || !test()) return;
do {
for (d=0, i=1; p[i]!=NULL; i++)
if (p[i-1]->Cmp(p[i]) > 0) //***
{d++; q=p[i-1]; p[i-1]=p[i]; p[i]=q; }
}
while (d!=0);}
Двоичные файлы произвольного доступа
Двоичный файл - неограниченный массив байтов
-двоичный файл произвольного доступа представляет собой неограниченный массив байтов внешней памяти;
-форма представления данных в оперативной памяти компьютера (переменные) и в двоичном файле полностью идентичны ;
-программа имеет возможность при помощи функций ввода-вывода копировать любую область файла в любую область памяти без преобразования ("байт в байт"). Таким образом, можно разместить в любом месте файла любую переменную из памяти программы в том виде, в каком она присутствует в памяти и прочитать ее обратно;
-в отличие от памяти программы, которая распределяется частично транслятором (обычные переменные), частично библиотечными функциями (динамические переменные), распределение памяти в файле производится самой программой. Только она определяет способ размещения данных, метод доступа к ним и несет ответственность за корректность этого размещения.
// Открыть существующий как двоичный для чтения и записи
FILE *fd; fd = fopen("a.dat","rb+wb");
// Создать новый как двоичный для записи и чтения
fd = fopen("a.dat","wb+");
Из того, что функции fread,fwrite копируют данные из памяти в файл без преобразования, "байт в байт", следует естественный способ сохранения в файле переменной любого типа данных, основанный на использовании операции sizeof для определения ее размерности:
long a; // Записать в файл переменную типа long,
fseek (fd, 20L, SEEK_SET); // начиная с позиции 20
fwrite (&a, sizeof(long),1,fd);
struct man b; // Добавить в файл переменную типа man
fseek (fd,0L,SEEK_END);
fwrite (&b, sizeof b,1,fd);
double *pd; // Прочитать с начала файла динамический
pd = new double[n]; // массив в n переменных типа double
fseek(fd,0L,SEEK_SET); //
fread((void*)pd, sizeof(double),n,fd);
Номер байта (позицию) в файле, начиная с которого размещается переменная в дальнейшем будем называть также СМЕЩЕНИЕМ или АДРЕСОМ этой переменной в файле.
fseek(fd,20L,SEEK_SET);
fwrite((void*)&a, sizeof(long),1,fd);
Нетрудно заметить, что в управлении внутренней памятью (переменные, память программы) и внешней памятью (файлы) много общего. Используя возможности адресной арифметики и преобразования типов указателей, можно произвольным образом планировать память программы, размещая в ней различные переменные (см. 4.5). Аналогичная "свобода выбора" имеет место и при работе с файлами: программист имеет право произвольным образом строить в файле любые структуры данных подобно тому, как он это делает в памяти. Но с небольшой разницей: если в памяти программы структуры данных можно организовать используя обычные переменные языка, динамические переменные, указатели и стандартные операции над ними, то при работе с файлом программист всего этого лишен. Он не может присвоить имя переменной в файле и пользоваться им, он не может выполнить над ней никаких операций, кроме как прочитав ее в память программы в переменную такого же типа. Короче говоря, программа вынуждена работать со структурами данных в файле на уровне физических адресов, не имея соответствующей поддержки транслятора.
Вообще, способы распределения памяти в файле могут быть довольно сложными. Для соответствующих разъяснений следует обратиться к алгоритмам распределения памяти. Однако есть один очень простой и естественный способ - для размещения переменной в файле достаточно добавить ее в конец файла. Для этого нужно установиться на конец файла и получить значение позиционера, после чего записать в файл саму переменную.
int a;
long pos;
fseek(fd,0L,SEEK_END);
pos=ftell(fd);
fwrite((void*)&a, sizeof(int),1,fd);
Если в процессе работы с переменной в файле ее размерность не будет меняться, то можно просто переписывать обновленное значение переменной на то же самое место .
a++;
fseek(fd,pos,SEEK_SET);
fwrite((void*)&a, sizeof(int),1,fd);
Если размерность переменной увеличится, то можно еще раз добавить ее в конец файла. Проблема утилизации получающихся свободных мест ("сбор мусора") достаточно сложна, чтобы рассматривать ее в простых примерах. В качестве наиболее удобного решения можно предложить периодическое переписывание всей структуры данных в новый файл (сжатие).
Двусвязные списки
//------------------------------------------------------bk53-05.cpp
struct list2
{
list2 *next, *pred;
int val;
};
list *InsertSort(list2 *ph, int v)
{
list2 *q , *p=new list;
p->val = v;
p->pred = p->next = NULL;
if (ph == NULL) return p; // включение в пустой список
for (q=ph; q !=NULL && v > q->val; q=q->next) ;
// поиск места включения - q
if (q == NULL) // включение в конец списка
{ // восстановить указатель на
for (q=ph; q->next!=NULL; q=q->next) ;
p->pred = q; // последний
q->next = p;
return ph;
} // включить перед текущим
p->next=q; // l следующий за новым = текущий
p->pred=q->pred; // l предыдущий нового = предыдущий
// l текущего
if (q->pred == NULL) // включение в начало списка
ph = p;
else // включение в середину
q->pred->next = p; // l следующий за предыдущим l =l новый
q->pred=p; // l предыдущийl l текущего = новый
return ph;
}
Например, если p - указатель на новый элемент, а q - указатель на текущий, то выражение q ->pred->next=p формально интерпретируется как " указателю на следующий элемент списка в предыдущем от текущего присвоить указатель на новый" .
2. Графическая интерпретация присваивания указателя :
-операция перемещения указателя реализуется через операцию присваивания одному указателю значения другого. На рисунке это соответствует перенесению (копированию) требуемого указателя из одной ячейки (2) в другую (1) ;
-в левой части операции присваивания должно находиться обозначение ячейки, в которую заносится новое значение указателя (2). Причем она может быть достижима только через имеющиеся рабочие указатели. На рисунке этому соответствует цепочка операций q ->pred->next ;
-в правой части операции присваивания должно находиться обозначение ячейки, из которой берется значение указателя (1) - на рисунке - p.
3. . Адресная интерпретация присваивания указателя . Содержимым указателя является адрес указуемой переменной. В свете этой фразы предыдущая картинка может стать более понятной.
Если все сразу
Exampl-
Назад: Некоторые полезные программные " заготовки"
Файл Си-программы как элемент модульного программирования
int a = 5 , B[10]={ 1,6,3,6,4,6,47,55,44,77 };
-задан тип переменной;
-задано имя переменной;
-определяется размерность и резервируется память. Размерность массивов в определении обязательно должна быть задана;
-производится инициализация памяти;
-для доступа к переменной из других модулей в текущем модуле может быть создана точка входа.
int strcmp(char *s, char *d) { ... }
-задан тип результата;
-задано имя функции;
-задан список формальных параметров и их типов;
-транслируется тело функции;
-для вызова функции из других модулей в текущем модуле может быть создана точка входа.
Переменная или функция при объявлении во внутреннее представление не переводятся, транслятору сообщается лишь факт их существования, имя и тип. Это необходимо для формирования правильного обращения к переменной или к функции.
extern int a,B[];
В объявлении переменной:
-задан тип переменной;
-задано имя переменной.
-запоминается факт наличия переменной с указанными именем и типом. Размерность массивов в объявлении может отсутствовать.
int strcmp();
int strcmp(char*, char*);
extern int strcmp();
extern int strcmp(char*, char*);
B объявлении функции:
-задается тип функции;
-задается имя функции;
-может быть задан список типов формальных параметров (прототип);
-запоминается факт наличия функции с указанными именем, результатом и, возможно, параметрами.
Различие между определением и объявлением имеет принципиальное значение: с определением связан процесс создания программного объекта, соответствующего переменной или функции, превращение их описания в программе во внутреннее представление в памяти (трансляция). Объявление - всего лишь подтверждение программистом факта существования переменной или функции, которые по каким-либо причинам транслятор в данный момент " не видит" . Объявлений одной и той же переменной или функции в программе может быть сколь угодно много, а определение -всегда одно. Кроме того, объявления транслятор всегда " принимает на веру" , поэтому ответственность за корректность объявлений несет программист.
Файл записей фиксированной длины
nrec size 0 1 2 m nrec-1
____________________________________________________
int int double double ... ... ... ...
____________________________________________________
2 * sizeof(int)+ m * size - адрес (смещение) записи
//---------------------------------------------- bk591.cpp
int nrec,size;
FILE *fd;
//---------------------------------------------------------
// Создать пустой файл
int Create(char *name, int sz)
{
if ((fd=fopen(name,"wb"))==NULL) // Создать новый для записи
return 0;
size=sz; nrec=0;
fwrite((void*)&nrec,sizeof(int),1,fd); // Записать в файл nrec и size
fwrite((void*)&size,sizeof(int),1,fd); //
fclose(fd);
return 1;
}
//----------------------------------------------------------------
// Открыть файл
int Open(char *name)
{
if ((fd=fopen(name,"rb+wb"))==NULL) // Открыть существующий
return 0; // для чтения и записи
fwrite((void*)&nrec,sizeof(int),1,fd); // Читать из файла nrec и size
fwrite((void*)&size,sizeof(int),1,fd); //
fseek(fd,0L,SEEK_ END); // Соответствует ли длина файла
if (ftell(fd)!=2*sizeof(int)+(long)nrec*size)
{ fclose(fd); return 0; } // значениям nrec и size?
return 1;
}
//----- Загрузить из файла запись в динамическую переменную
// n - номер записи
#include <alloc.h>
void *Get(int n)
{
void *pp;
if (fd==NULL) return NULL; // Файл не открыт
if (n >= nrec) return NULL; // Номер записи некорректен
pp = ( void*) new char [size]; // Создать динамическую переменную
if (fseek(fd, 2*sizeof(int) + n*size, SEEK_SET) ==EOF)
{ delete pp; return NULL; } // Ошибка позиционирования
if (fread(pp, size, 1, fd) !=1)
{ delete pp; return NULL; } // Ошибка чтения
return pp;
}
//-------------------------------------------------------------
// Добавить запись
int Append(void *pp)
{
if (fd==NULL) return 0; // Файл не открыт
fseek(fd,0L,SEEK_ END); // Установиться на конец файла
if (fwrite(pp,size,1,fd)!=1) return 0; // Ошибка
nrec++;
fseek(fd,0L,SEEK_ SET); // Обновить переменную nrec в файле
if (fwrite((void*)&nrec,sizeof(int),1,fd)!=1)
return 0; // Ошибка
return 1; }
// ----------------------------------------------------------------
// Пример работы с файлом переменных типа double
void main() {
double a,*pd [20];
if (!Create("a.dat",sizeof(double))
return; // Создать файл
if (!Open("a.dat")) return; // Открыть файл
for (int i=0; i< 20; i++) // Добавить 20 переменных
{ a=i; Append((void*)&a);
for (int i=0; i< 20; i++) // Прочитать в обратном порядке
pd[i]=Get(19- i); // в динамические переменные
} } // и сформировать массив указателей
Файл записей переменной длины
-во втором случае каждая запись предваряется переменной-счетчиком, который содержит длину этой записи. Содержимое записи при этом может быть любым, поскольку явно выделенные коды-ограничители отсутствуют. В качестве примера рассмотрим запись переменной длины, которая содержит в себе структурированную переменную и строку неограниченной длины, связанную с ней указателем.
//------------------------------------------------------bk59-01.cpp
#include <stdio.h>
#include <alloc.h>
struct vrec
{
int dd,mm,yy;
char name[20]; // Строка фиксированной длины
char *addr; // Строка переменной длины
};
#define VSZ sizeof(vrec)
v rec *get(FILE *fd) // Чтение из файла в динамические
{ // переменные
int size;
vrec *p;
fread(&size,sizeof(int),1,fd); // Чтение счетчика
if (size == 0) return NULL; // EOF
if (size < VSZ) return NULL; // Короткая запись
if ((p = new vrec) ==NULL) return NULL;
fread((void*)p,VSZ,1,fd); // Постоянная часть записи
size -= VSZ; // Остаток записи - строка
if ((p->addr = new char[size]) ==NULL)
return NULL;
fread((void*)p->addr,size,1,fd); // Переменная часть записи
return p;
}
void put(FILE *fd, vrec *p) // Добавление в файл
{
int size;
fseek(fd,-(long)sizeof(int),SEEK_END); // Установить на запись EOF
size = VSZ + strlen(p->addr) + 1; // Общая длина записи (счетчик)
fwrite((void*)&size, sizeof(int), 1, fd); // Запись счетчика
f write((void*)p, VSZ, 1, fd); // Запись постоянной части
size -=VSZ;
fwrite((void*)p->addr,size,1,fd); // Запись переменной части
size = 0; // Логический EOF-запись нулевой длины
fwrite((void*)&size, sizeof(int), 1, fd);
}
Файловые потоки
ifstream - файл ввода, производный от istream
ofstream - файл вывода, производный от ostream
fstream - файл ввода-вывода, производный от iostream
enum ios::open_mode
{
in = 0x01, // Открыть файл только для чтения
out = 0x02, // Открыть файл только для записи
ate = 0x04, // При открытии позиционироваться в конец файла
app = 0x08, // Открыть существующий для дополнения
trunc = 0x10, // Создание нового файла взамен существующего
nocreate=0x20, // Не создавать новый файл при его отсутствии
noreplace=0x40, // Не создавать новый файл, если он существует
binary= 0x80 // Двоичный файл ("прозрачный" ввод-вывод без
// преобразования символов конца строки)
};
ifstream(); // Без открытия файлов
ifstream( // С открытием файла в заданном
char *name, // режиме imode
int imode=ios::in,
int prot=filebuf::openprot);
ifstream(int fd); // С присоединенем файла с дескрип-
// тором fd
ifstream( // То же, с явно заданным буфером
int fd,
char *buf, int sz);
void ifstream::open(
char *name, // Открытие файла в заданном режиме
int imode=ios::in,
int prot=filebuf::openprot);
void close(); // Закрыть файл
void setbuf(
char *p,int sz); // Установить буфер потока
int fd(); // Дескриптор открытого в потоке файла
int is_rtl_open(); // 1 - файл открыт в потоке
fstream ss;
if (ss) ... или if (!ss) ...
// Чтение указанного количества символов (байтов)
// из потока в буфер
istream& read( char *p, int n);
istream& read( signed char *p, int n);
istream& read( unsigned char *p, int n);
// Запись указанного количества символов (байтов)
// из буфера в поток
ostream& write( char *p, int n);
ostream& write( signed char *p, int n);
ostream& write(unsigned char *p, int n);
// Чтение-запись символа из потока
istream& get( char &p);
istream& get( signed char &p);
istream& get( unsigned char &p);
ostream& put( char c);
// Чтение строки из потока (n-длина, c-ограничитель,
// из потока не извлекается)
istream& get(char *p, int n, char c='\n');
// Чтение строки из потока (n-длина, c-ограничитель,
// из потока извлекается, в буфер не пишется)
istream& getline(char *p, int n, char c='\n');
// Пропуск n символов при вводе (c - ограничитель,
// извлекается из потока)
istream& ignore(int n=1, int d=EOF);
// Число символов, прочитанных последней функцией
// неформатированного ввода
int gcount();
// Чтение символа без извлечения из потока
int peek();
// Возвращение символа во входной поток
istream& putback(char c);
// Позиционирование в потоке и чтение текущей позиции
enum ios::seek_dir
{
beg = 0, // От начала файла
cur = 1, // От текущей позиции
end = 2 // От конца файла
};
typedef long streampos;
typedef long streamoff;
istream& seekg(streampos);
istream& seekg(streamoff, ios::seek_dir);
ostream& seekp(streampos);
ostream& seekp(streamoff, ios::seek_dir);
streampos istream::tellg();
streampos ostream::tellp();
Философские аспекты рекурсии
-рекурсивная функция разрабатывается как обобщенный шаг процесса, который вызывается в произвольных начальных условиях и который приводит к следующему шагу в некоторых новых условиях ;
-обобщенные начальные условия шага - формальные параметры функции ;
-начальные условия следующего шага - фактические параметры рекурсивного вызова ;
- рекурсивная функция должна проверять условия завершения рекурсии, при которых следующий шаг процесса не выполняется ;
-локальными переменными функции должны быть объявлены все переменные, которые имеют отношение к протеканию текущего шага процесса.
И последнее. В Нагорной проповеди Нового Завета Иисус высказал одну из заповедей блаженства : будьте как птицы небесные, не заботьтесь о завтрашнем дне, пусть он заботится сам о себе. Это ни в коей мере не означает - живите " сегодняшним днем" , а после нас - хоть потоп. Наоборот. Если хочешь всю жизнь прожить в благодати, будь достоин сегодняшнего для, не объясняй своих слабостей прошлым, не уповай на исправление сегодняшних ошибок в будущем. Сосредоточься на сегодняшнем дне, и тогда цель в будущем будет достигнута сама собой. То же самое - и в проектировании рекурсивной функции : не думай о результате, сосредоточь внимание на текущем шаге рекурсии, если ее " сегодняшний" вызов корректен и все твои действия приводят к такому же корректному вызову ее " завтра" , то цель рано или поздно будет достигнута сама собой.
"Философский" смысл технологии ООП
Философский смысл указателя
Финал-апофеоз: классов, функций, строк
Постановка задачи
Шаг 1: Ударим классом по двоичному файлу
Шаг 2: Что такое произвольный элемент коллекции
Шаг 3: Строки -элементы таблицы
Шаг 4: Промежуточный финиш. Работаем с файлом строк
Шаг 5: Массив файловых указателей
Шаг 6: Файловая коллекция однотипных элементов
Шаг 7: Пример использования: ввод, сохранение и сортировка строк
Формальные и фактические параметры
void f(char **p, int A[], void (*f)())
{...}
// Прототип
int strcmpn(char *p1,char *p2,int n){___}
void f(void){___} // или
void f(){___}
int printf(char *s, ...){___}
extern double sin(double);
extern void f(void);
extern int printf(char*,...);
// Традиционный синтаксис
int strcmpn(p1,p2,n)
char *p1,*p2;
int n;
{___}
void f() {___}
int printf(s,a)
char *s;
int a;
{___}
extern double sin();
extern void f();
extern int printf();
-в заголовке определения функции по прототипу формальные параметры указаны в скобках в контексте, определяющем их тип. В традиционном синтаксисе в скобках указаны только имена формальных параметров, а контекстное определение их типов выполняется отдельными строками перед открывающейся скобкой тела функции ("{");
-при объявлении внешней функции в традиционном синтаксисе список параметров вообще отсутствует, в то время как в объявлении с использованием прототипа в скобках перечисляются абстрактные типы данных формальных параметров или их контекстные определения;
-если функция допускает переменное количество параметров, то переменная часть списка в определении и в объявлении функции по прототипу обозначается псевдо-параметром " ... ";
Формат входных документов
Стиль | Назначение |
заголовок 1; | Заголовок уровня 1 | заголовок 2; | Заголовок уровня 2 | заголовок 3; | Заголовок уровня 3 | текст; | Обычный форматируемый текст с отступом | от края; | Обычный форматируемый текст без отступа | определение; | Определение выделено цветом и шрифтом | основной шрифт абзаца; | программа; | Форматируемая по правилам синтаксиса Си-программа | список 1; | Перечисление (ненумеровнный список) с "точкой" | список; | Перечисление (список) без "точки" | без формата; | Неформатируемый фрагмент (как есть) | стихи; | Неформатируемый фрагмент (как есть) | эпиграф; | Выровнен по левому краю, выполнен курсивом |
обычный; |
Форматирование данных в потоках
Флаги форматирования - битовые поля в переменной типа long .
enum ios::io_format
{
skipws= 0x0001, // Игнорирование пробелов при вводе
left = 0x0002, // Выравнивание по левой границе поля
right = 0x0004, // Выравнивание по правой границе поля
internal= 0x0008, // Знак выводится по левому краю поля,
// само число выравнивается по правому
dec = 0x0010, // Десятичная система счисления
oct = 0x0020, // Восьмеричная система счисления
hex = 0x0040, // Шестандатеричная система счисления
showbase= 0x0080, // Вывод индикатора системы счисления
// (0... или 0x...)
showpoint= 0x0100, // Обязательный вывод десятичной точки
uppercase= 0x0200, // Верхний регистр символов:
// 0X00FF, 0.5E2
showpos= 0x0400, // Обязательный вывод "+"
scientific= 0x0800, // Обязательный вывод порядка числа
fixed = 0x1000, // Вывод с фиксированной точкой nnn.mmm
unitbuf= 0x2000, // Очистка буфера после каждого вывода
stdio = 0x4000 // Очистка потоков stdout, stderr
// после каждого вывода (flush)
};
Функции форматирования в классе ios :
long flags(); // Чтение флагов форматирования
long flags(long); // Присваивание флагов форматирования
// (нулевых и единичных)
// (возвращает старые значения флагов)
long setf(long); // Установка флагов форматирования
// (установленных в маске в 1)
long unsetf(long); // Сброс флагов форматирования
// (установленных в маске в 1)
long setf(long,long); // Установка флагов форматирования
// из первого параметра по маске
// второго параметра
char fill(); // Чтение символа заполнения (пробел)
char fill(char); // Установка символа заполнения
int precision(); // Чтения точности представления
// float и double
int precision(int); // Установка точности представления
int width(); // Чтение текущей ширины поля
int width(int); // Установка текущей ширины поля
static long bitalloc(); // Чтение маски флагов
Форматирование группы документов
Форматирование может выполняться как для отдельного документа, так и для всех файлов выбранного каталога. В этом случае просмотр файлов выполняется в алфавитном порядке. В обоих случаях создается страница, содержащая текст 0-го уровня вложенности и ссылки на страницы глав документа. Его имя совпадает с именем первого форматируемого файла. Имя файла HTML-страницы состоит из имени файла исходного документа и последовательного номера главы, раздела или параграфа, полученного в процессе форматирования.
Форматирование Си-программ
Текст, выполненный в стиле " Программа" , рассматривается как фрагмент Си-программы и переформатируется особым образом путем формирования отступов, соответствующих уровню вложенных скобок " {}" и также отдельных оператов, если они являются телом операторов if и for. Если же фрагмент начинается с комментария вида " //---------------------------" , то он дополнительно копируется в виде файла с расширением " . cpp" в подкаталог EXAMPLES выходного каталога и ему присваивается очередное имя вида файл документа -nnn.cpp. В форматируемом тексте на нее ставится ссылка. Данные о программе заносятся также в таблицы ПРОГРАММЫ и ТЕРМИНЫ.
Форматирование строки
Форматирование строки -размещение ее в выходном массиве заданной размерности таким образом, чтобы интервалы между соседними словами отличались не более, чем на 1.
Шаг 1: Исходные данные и результат. Входная строка произвольной длины в массиве IN[], отформатированная строка длины n в массиве OUT[]. Выходная строка отвечает следующим требованиям:
-слово -любая последовательность символов, кроме пробела ;
-после форматирования число пробелов между словами различается не более чем на 1; -первое и последнее слово расположены по краям строки. Шаг 2: Форматирование включает в себя последовательность из трех действий:
void format(char IN[], char OUT[], int n)
{
// Собрать исходные данные по строке,
// необходимые для форматирования;
// Проверить возможность форматирования;
// Разместить слова в выходной строке.
}
Шаг 3: Данные по строке, необходимые для форматирования: -количество слов в строке -nw; -общее количество символов во всех словах -ns; -стандартное количество пробелов между словами при форматировании -np; -оставшееся количество пробелов, добавляемых по одному между словами -nr. На этом шаге детализируется проверка возможности форматирования и определяются взаимосвязанные параметры.
void format(char IN[], char OUT[], int n)
{ int nw, ns, np, nr;
// Определение nw, ns ...
OUT[0] = '\0';
if (nw < 2) return; // Мало слов в строке
np = (n - ns) / (nw - 1);
if (np <= 0) return; // Много символов в словах
nr = (n - ns) % (nw - 1); // Остаточное число пробелов
// Размещение слов в выходной строке
}
Шаг 4: Просмотр строки при определении параметров и форматировании можно выполнить, используя:
-цикл в цикле: цикл просмотра всех слов, в который включен цикл посимвольного просмотра интервала между словами и самого слова;
-цикл посимвольного просмотра строки, с использованием признака нахождения внутри слова или вне его - inword.
Выберем второй вариант. При этом необходимо учесть, что символ конца строки может рассматриваться как конец слова (разделитель), поэтому проверку на выход из цикла нужно делать после выполнения тела цикла.
Шаг 5: Определение ns,nw:
for (i=0,ns=0,nw=0,inword =0; ; i++)
{
// Анализ символа IN[i] и подсчет параметров;
if (IN[i] =='\0') break;
}
Шаг 6: Размещение слов в выходной строке:
{
for (i=0,j=0,inword =0; ; i++)
{
// Анализ символа IN[i] и форматирование
// (перенос в OUT[j]);
}
if (IN[i] =='\0') break;
} }
Шаг 7: Анализ символа состоит в выделении 4 вариантов по двум сравнениям -признака inword и типа символа IN[i] - разделителя или символа слова:
{
if (IN[i]==' ' || IN[i]=='\0')
if (inword)
{ nw++; inword =0; } // Конец слова
else
{} // Продолжение разделителя
else
{ ns++;
if (inword)
{} // Продолжение слова
else
{ inword =1; } // Начало слова
} }
Шаг 8: Анализ символа IN[i] и форматирование:
{
if (IN[i]==' ' || IN[i]=='\0')
if (inword)
{ // Конец слова
for (k=0; k<np; k++) // Включение "np"
OUT[j++]=' '; // пробелов
if (nr-- > 0) // Включение
OUT[j++]=' '; // дополнительного пробела
}
else
{} // Продолжение разделителя
else
if (inword)
OUT[j++]=IN[i]; // Продолжение слова
else
{ // Начало слова
OUT[j++]=IN[i];
inword =1;
} }
Окончательный вариант с оптимизацией (исключение лишних ветвей):
//------------------------------------------------------bk34-12.cpp
//-----Форматирование строки
void format(char IN[], char OUT[], int n)
{
int nw, ns, np, nr, i, j, k, inword;
for (i=0,ns=0,nw=0,inword =0; ; i++)
{
if (IN[i]==' ' || IN[i]=='\0')
{ if (inword) { nw++; inword =0; } }
else
{ ns++; inword=1; }
if (IN[i] =='\0') break;
}
OUT[0] = '\0';
if (nw < 2) return;
np = (n - ns) / (nw - 1);
if (np <= 0) return;
nr = (n - ns) % (nw - 1);
for (i=0,j=0,inword=0; ; i++)
{
if (IN[i]==' ' || IN[i]=='\0')
{ if (inword)
{
for (k=0; k<np; k++) OUT[j++]=' ';
if (nr-- > 0) OUT[j++]=' ';
}
}
else
{ OUT[j++]=IN[i]; inword =1; }
if (IN[i] =='\0') break;
}
}
Форматирование таблиц
Форматер распознает прямоугольные таблицы, не содержащие объединенных ячеек. Кроме обычного переформатирования он заносит при ПРОХОДЕ0 данные о ней в таблицу ТЕРМИНЫ (метку и имя раздела, в котором она встречается), а при ПРОХОДЕ1 заполняет таблицы ТАБЛИЦЫ И ДАННЫЕ. В первую он заносит имена и метки самой таблицы и ее заголовков (ячейки первой строки и первого столбца). Во вторую содержимое всех остальных ячеек таблицы.
Формирование массивов указателей
-указуемые элементы (переменные) могут быть как обычными (статическими) переменными, создаваемые транслятором, так и динамическими переменными, создаваемыми в процессе работы программы;
-указатели (связи между элементами) могут быть либо инициализированы (установка начальных значений при трансляции), так и назначены в процессе выполнения программы.
В результате получаются структуры данных, единственное различие которых заключается во времени их формирования: от момента трансляции (инициализация) до момента выполнения.
Вариант 1. Формирование структуры данных при трансляции: переменные определяются статически, а указатели инициализируются. Такие структуры данных включены непосредственно в программный код и "готовы к работе":
double a1,a2,a3, *pd[] = { &a1, &a2, &a3, NULL};
Вариант 2. Переменные определяются статически, указатели устанавливаются программно. Этот вариант наиболее часто используется, когда указуемые переменные представлены массивом:
double d[19], *pd[20];
for (i=0; i< 19; i++) pd[i] = &d[i];
pd[i] = NULL;
Примечание: указатель NULL обычно используется в массиве в качестве ограничителя последовательности указателей, если их количество меняется.
Вариант 3. Указуемые переменные создаются динамически, массив указателей -статически:
double *p, *pd[20];
for (i=0; i< 19; i++)
{
p = new double; *p = i; pd[i] = p;
}
pd[i] = NULL;
Вариант 4. Все переменные, в том числе и массив указателей, создаются динамически. Результатом работы является указатель на создаваемый массив указателей (адрес массива указателей) (см. ниже " динамические массивы указателей " ):
double **pp, *p;
pp = new double *[20]; // память под массив
for (i=0; i< 19; i++) // из 20 указателей типа double*
{
p = new double;
*p = i; pp[i] = p;
}
pp[i] = NULL;
Функции для работы с терминалом в текстовом режиме (conio.h)
void clreol(void);
// Очистка строки от текущей позиции курсора до конца
votd clrscr(void);
// Очистка экрана
char *cgets(char *str);
// Чтение строки с терминала в str
// str[0] - максимальная длина строки
// str[1] - действительная длина строки
// str[2]... и далее - символы строки
// Возвращает &str[2]
int cprintf(char *fmt,...);
// Аналогична функции форматированного вывода printf, но
// работает по отношению к окну, созданному функцией window
int cputs(char *str);
// Вывод строки на терминал
int cscanf(char *fmp,...);
// Аналогична функции форматированного ввода scanf
void delline(void);
// Удаление строки, в которой находится курсор
void gotoxy(int x, int y);
// Установка курсора в позицию с координатами (x,y).
// Координаты курсора отсчитываются от левого верхнего
// угла, начиная с 1
void highvideo(void);
// Установка режим повышенной яркости выводимых символов
int movetext(int x0,int y0,int x1,int y1,int x, int y);
// Перенос содержимого текстового окна с координатами
// (x0,y0)(x1,y1) в область экрана с координатами левого
// верхнего угла (x,y)
void normvideo(void);
// Установка режима обычной яркости выводимых символов
void textattr(int attr);
// Установка атрибутов (цвет символов и фона) выводимых
// символов
void textbackground(int c);
// Установка цвета фона выводимых символов и очищаемого
// экрана
void textcolor(int c);
// Установка цвета выводимых символов
void textmode(int c);
// Установка текстового режима
int wherex(void);
int wherey(void);
// Значение координаты курсора
void window(int x0,int y0, int x1, int y1);
// Создание текстового окна с координатами (x0,y0)(x1,y1)
Функции для работы со строками (string.h, stdlib.h)
double atof(char *s);
int atoi(char *s);
long atol(char *s);
// преобразование строки в вещественное, целое и длинное целое
char *itoa(int v, char *s, int n);
char *ltoa(long v, char *s, int n);
char *ultoa(unsigned long v, char *str, int n);
// преобразование целого, длинного целого со знаком и без
// знака v в строку str в системе счисления по основанию n.
// Возвращает копию значения str
char *strcat(char *dst, char *src);
// Присоединение строки src к строке dst. Возвращает копию
// значения dst
char *strchr(char *str, int c);
// Поиск первого вхождения символа c в строку str. Возвращает
// указатель на найденный символ или NULL
int strcmp(char *s1, char *s2);
// Сравнение строк s1 и s2 по значениям кодов символов (без
// знака). Результат:
// -1 - s1 < s2
// 0 - s1 = s2
// 1 - s1 > s2
char *strcpy(char *dst, char *src);
// Копирование строки src в строку dst. Возвращает копию
// значения dst
int strcspn(char *str, char *slist);
// Количество символов в строке str (подряд от начала), не
// совпадающих с символами в строке slist
char *strdup(char *str);
// Копия строки str в динамической памяти
unsigned strlen(char *str);
// Длина строки str
char *strlwr(char *str);
// Преобразование в строке str латинских букв нижнего
// регистра в буквы верхнего ("маленьких - в большие")
char *strncat(char *dst, char *src, int n);
// Присоединение n символов строки src к строке dst.
// Возвращает копию значения dst
int strncmp(char *s1, char *s2, int n);
// Сравнение строк s1 и s2 по значениям кодов символов (без
// знака). Сравнение производится для n первых символов.
// Результат аналогичен strcmp
char *strncpy(char *dst, char *src, int n);
// Копирование n символов из строки src в строку dst
int strnicmp(char *s1, char *s2, int n);
// То же, что и strncmp, но без различия верхнего и нижнего
// регистра для латинских букв
char *strnset(char *str, int c, int n);
// Замена n первых символов в строке str на символ c
char *strpbrk(char *str, char *slist);
// Поиск первого символа в str, входящего в строку slist.
// Возвращает указатель на найденный символ
char *strrchr(char *str, int c);
// Поиск последнего вхождения символа c в строку str.
// Возвращает указатель на найденный символ
char *strset(char *str, int c);
// Заполнение строки str символом c
int strspn(char *str, char *slist);
// Количество символов в строке str (подряд от начала),
// совпадающих с символами в строке slist
char *strstr(char *str, char *ss);
// Поиск вхождения подстроки ss в строке str. Возвращает
// указатель на начало найденного фрагмента в str или NULL
double strtod(char *str, char **endptr);
long strtol(char *str, char **endptr, int n);
// Чтение из строки str вещественного числа или длиного
// целого в системе счисления с основанием n. endptr -
// адрес указателя, где сохраняется указатель на символ
// в str, на котором завершилось чтение строки
char *strtok(char *str, char *s);
// Выделение слова в строке str. Слово - любая последова-
// тельность символов, кроме символов-разделителей, пере-
// численных в строке s.
// При первом вызове возвращает указатель на первый символ
// строки. При последующих вызовах с str=NULL возвращает
// указатель на очередное слово
char *strupr(char *str);
// Замена в строке str латинмких букв нижнего регистра на
// верхний ("маленьких на большие")
Функции форматированного ввода-вывода
.
------------------------------- Форматированный вывод --¬
¦ int printf(char *format,...) ¦
¦ - стандартный вывод ¦
¦ int fprintf(FILE *fd, char *format,...) ¦
¦ - явно указанный файл ¦
¦ int sprintf(char *str, char *format,...) ¦
¦ ¦ - строка в памяти ¦
¦ L--- число выведенных байтов или EOF ¦
L--------------------------------------------------------
------------------------------- Форматированный ввод ---¬
¦ int scanf(char *format,...) ¦
¦ - стандартный ввод ¦
¦ int fscanf(FILE *fd, char *format,...) ¦
¦ - явно указанный файл ¦
¦ int sscanf(char *str, char *format,...) ¦
¦ ¦ - строка в памяти ¦
¦ L--- число фактических параметров, для которых ¦
¦ введены значения, или EOF ¦
L--------------------------------------------------------
Форматная строка определяет количество и типы фактических параметров из переменного списка. Последовательность символов, идущая за "%" (спецификация формата), определяет тип и формат ввода-вывода очередного фактического параметра. При этом в функциях форматированного вывода все символы форматной строки выводятся в выходной файл за исключением спецификаций формата, которые заменяются значениями соответствующих фактических параметров. В функциях форматированного вывода тип параметра должен совпадать с заданным в спецификации, в функциях форматного ввода - быть указателем на этот тип (то есть должен передаваться адрес переменной, а не ее значение).
.
-- форматная строка ---¬ -------¬ фактические
printf("....%***...%***...%***",p1,p2,p3); параметры
¦ ¦ L->>-¦--¦--- переменного
¦ L--->>------¦--- списка
L----->>------------
Спецификация формата ввода-вывода фактического параметра ([] обозначают необязательную часть) .
.
% [флаги][ширина][.точность][модификатор] тип ------------¬
¦ ¦ ¦ +- "F" far-указатель ¦
¦ ¦ ¦ +- "N" near-указатель ¦
¦ ¦ ¦ +- "h" параметр типа short ¦
¦ ¦ ¦ L- "l" параметр типа long ¦
¦ ¦ ¦ или double (для scanf)¦
¦ ¦ +- "0" без дробной части ¦
¦ ¦ +- n не более n знаков после точки ¦
¦ ¦ L- * задается следующим фактическим ¦
¦ ¦ параметром функции ¦
¦ +- n минимальная ширина поля n (незаполненные ¦
¦ ¦ позиции - пробелы) ¦
¦ +- 0n то же, незаполненные позиции - нули ¦
¦ L- "*" задается следующим фактическим ¦
¦ параметром функции ¦
+- "-" выравнивание по левому краю поля ¦
¦ (по умолчанию - по правому) ¦
+- "+" выводится знак числа ("+" или "-") ¦
+- " " выводится пробел перед положительным числом ¦
L- "#" выводится идентификатор системы счисления ¦
("0" -восьмеричная, "0x" -шестнадцатеричная, ¦
"." -для float) char "c" --------+
десятичное int "d" --------+
то же "i" --------+
десятичное unsigned "u" --------+
восьмеричное unsigned "o" --------+
шестнадцатеричное unsigned "x" --------+
со строчными и прописными А..F "X" --------+
float в формате ***.*** "f" --------+
float в формате *.***e*** "e" --------+
float в формате *.***E*** "E" --------+
в зависимости float ("e" или "f") "g" --------+
от точности float ("E" или "F") "G" --------+
char * "s" ---------
(ввод по точности или до \n, вывод по
точности, до \n или до \0)
Функции -элементы структуры
Рассмотрим пример со структурой, представляющей дату:
struct dat
{
int day,month,year;
int TestData(); // Проверка даты
void NextData(); // Добавить 1 день
void PlusData(int n) // Добавить n дней
{
while(n-- !=0) NextData();
}
};
static int mm[] = {31,28,31,30,31,30,31,31,30,31,30,31};
//----------- Проверка на корректность ------------------
int dat::TestData()
{
if (month ==2 && day==29 && year %4 ==0) return(1);
if (month ==0 || month > 12 || day ==0 || day >mm[month])
return(0);
return(1);
}
//----------- Следующая дата ----------------------------
void dat::NextData()
{
day++;
if (day <= mm[month]) return;
if (month ==2 && day==29 && year %4 ==0) return;
day=1;
month++;
if (month !=13) return;
month=1;
year++;
}
//--------- Основная программа --------------------------
void main()
{
dat a;
do cin << a.day << a.month << a.year;
while(a.TestData() ==0);
a.PlusData(17);
}
Как видно из примера, в качестве элементов структуры могут выступать функции. Такие элементы-функции имеют следующие особенности:
-тело функции может быть определено в самой структуре (функция PlusData ). В этом случае функция имеет стандартный вид;
-в определении структуры дается только прототип функции (заголовок с перечислением типов формальных параметров). Определение самой функции дается отдельно, при этом полное имя функции имеет вид
. имя_структуры::имя_функции
-в теле функции неявно определен один формальный параметр с именем this -указатель на структуру, для которой вызывается функция (В нашем примере это будет struct dat *this ). Элементы этой структуры доступны через явное использование этого указателя или неявно:
this->month = 5;
this->day++;
month = 5;
day++;
-для структурированной переменной вызов функции - элемента этой структуры имеет вид:
. имя_переменной.имя_функции (список_параметров)
Приведем фрагмент этой же программы в виде эквивалента на " классическом" Си:
void dat_PlusData(dat *this, int n)
//---------- Добавить n дней ----------------------------
{
while(n-- !=0) NextData(this);
}
void dat_NextData(dat *this)
//----------- Следующая дата ----------------------------
{
this->day++;
...
this->month=1;
this->year++;
}
//--------- Основная программа --------------------------
void main()
{ dat a;
...
dat_PlusData(&a,17);
}
Функции открытия-закрытия файла
.
--------------------------------------------------------¬
¦ режим работы с файлом --------¬ ¦
¦ имя файла --------¬ ¦ ¦
¦ ¦ ¦ ¦
¦ FILE *fopen(char *name, char *mode)- открыть файл ¦
¦ ¦ ¦
¦ L--- указатель на дескриптор файла или NULL ¦
¦ ¦
¦ int fclose(FILE *fd) - закрыть файл ¦
¦ ¦ ¦
¦ L--- 0 или EOF (ошибка) ¦
¦ ¦
¦ int fcloseall(void) - закрыть все ¦
¦ ¦ ¦
¦ L--- число закрытых файлов или EOF ¦
¦ ¦
¦ FILE *freopen(char *name, char *mode, FILE *fd) ¦
¦ - закрыть и открыть повторно ¦
¦ ¦
¦ FILE *tmpfile(void) - создать и открыть временный ¦
¦ файл с уникальным именем ¦
L--------------------------------------------------------