Text
                    Язык Си.
Книга ответов


The С Answer Book Second Edition Solutions to the Exercises in The С Programming Language, Second Edition by Brian W. Kernighan and Dennis M. Ritchie Clovis L. Tondo International Business Machines Corporation Scott E. Gimpel PRENTICE HALL, Englewood Cliffs, New Jersey 07632
К.Тондо С.Гимпел Язык Си. Книгаответов Перевод с английского И.В. Остроухова Решения к упражнениям из книги Б. Кернигана, Д. Ритчи "Язык программирования Си", выпущенной издательством "Финансы и статистика" в 1992 г. МОСКВА "ФИНАНСЫ И СТАТИСТИКА" 1994
ББК 24.4.9 Т57 Тондо К., Гимпел С. Т57 Язык Си. Книга ответов: Пер. с англ. - М.: Финансы и статистика, 1994 - 160 с. ISBN 5-279-01261-0 Настоящее издание содержит ответы и пояснения к упражнениям, приведенным в популярной книге Б.Кернигана, Д.Ритчи "Язык программирования Си", выпущенной в издательстве "Финансы и статистика" в 1992 г. Для широкого круга пользователей профессиональных ПК. _, 2404010000-028 t .„ ft- ББК 24.4.9 Т 010(0l)-94 167~92 ISBN 0-13-109653-2 (США) <§> 1989 by Prentice-Hall, Inc. ISBN 5-279-01261-0 (РФ) © И.В.Остроухов, перевод, 1994
Предисловие Перед'вами Книга ответов, вкоторой приводятся решения всех упражнений второго издания книга "Язык программирования Си" Б.Кернигана и Д.Ритчи (Prentice Hall, 1988) (далее КР). Американский национальный институт стандартов (American National Standards Institute, ANSI) выпустил для языка Си стандарт ANSI, а Керниган л Ритчи изменили первое издание книги "Язык программирования Си". Мы переписали решения, чтобы привести их в соответствие как со стандартом ANSI, так и со вторым изданием КР. Тщательное изучение второго издания Книги ответов в сочетании с книгой Кернигана и Ритчи поможет вам понять язык Си и научит вас искусству хорошо программировать на Си. Пользуйтесь книгой Кернигана и Ритчи для изучения Си, выполняйте упражнения, затем изучайте представленные здесь решения. Мы строили решения, используя конструкции языка, известные на момент появления упражнения в КР. Цель этого — следовать темпу КР. Позже, коща больше узнаете о языке Си, вы сможете дать, возможно, лучшие решения. Например, до тех пор, пока оператор if (выражение) оператор-1 else оператор-2 не объяснен на с. 29 КР, мы его не используем. Однако, используя этот оператор, вы могли бы улучшить решения упражнений 1.8, 1.9 и 1.10 (с. 28 КР). Иногда мы также приводим не ограниченные синтаксически решения. Мы разъясняем решения, предполагая, что вы прочитали материал КР, относящийся к упражнению, м стараемся не повторять КР, а описывать основные моменты каждого решения. Невозможно изучить язык программирования, только читая конструкции языка. Необходимо также программировать — писать свои программы и изучать чужие. Мы используем хорошие стороны языка, делаем наши про- 5
граммы модульными, ширсжо используем библиотечные функции и форматируем программы, чтобы помочь вам увидеть логическую последовательность. Надеемся, что эта книга поможет вам стать профессионалом в Си. Мы благодарим друзей, которые помогли нам выпустить второе издание: Брайан Керниган, Дон Костуч, Брюс Ленг, Стив Мэкки, Джоан Маграби, Джулиа Мистрелло, Розмари Морриси, Эндрю Натансон, Софи Папаниколау, Дейв Пермин, Карлос Тондо, Джон Уэйт и Эден Юнт. КловисЛ. Тондо
Глава 1 Обучающее введение Упражнение 1.1 (сЛ7 КР) Запустите программу "Привет, мир" на вашей системе. Поэкспериментируйте, удаляя части программы, чтобы посмотреть, какие сообщения об ошибках вы получите. #include <stdio.h> main 0 { printf ("Привет, мир"); } В этом примере отсутствует символ новой строки CW), вследствие чего курсор остается в конце строки. ♦include <stdio.h> main О printf ("Привет, мир\п") } Во втором примере нет точки с запятой после printf О. Самостоятельные операторы Си завершаются точками с запятой (сЛ8 КР). Компилятор должен определить отсутствие точки с запятой и вывести соответствующее сообщение. ♦include <stdio.h> main О printf ("Привет, мир\п'); } В третьем примере двойная кавычка " после \п ошибочно напечатана как одиночная. Эта кавычка вместе с закрывающей скобкой и точкой с запятой принимается за часть строки. Компилятор должен распознать это как ошибки и сообщить, что отсутствует двойная кавычка, что перед закрывающей фигурной скобкой нет закрывающей круглой, строка слишком длинна или что в ней есть символ новой строки. 7
Упражнение 1.2 (с. 17 КР) Попробуйте выяснить, что случится, если строка-аргумент printf () содержит \с, где с — некоторый символ, не описанный выше. ♦include <stdio.h> main () { printf ("Привет, мир\уи); printf ("Привет, мир\7"); printf ("Привет, мир\?"); } В справочном руководстве (приложение А, с. 186 КР) говорится если символ, следующий за \, не является одним из заданного набора, реакция не определена. Результат этого эксперимента зависит от компилятора. Одним из возможных результатов может быть Привет, мируПривет, мир<СИГНАЛ>Привет, мир? где <СИГНАЛ> — короткий звуковой сигнал, выдаваемый символом ASCII 7. Для представления символа можно использовать \, за которым следует до 3 восьмеричных цифр. \7 определен в наборе символов ASCII как звуковой сигнал. Упражнение 1.3 (с.22 КР) Измените программу пересчета температур так, чтобы она печатала заголовок вверху таблицы. #include <stdio.h> /* печать таблицы соответствия температур по Фаренгейту */ /* температурам по Цельсию */ /* для fahr - 0, 20,..., 300; вариант с плавающей точкой */ main О { float fahr, celsius; int lower, upper, step; lower - 0; /* нижний предел таблицы температур */ upper - 300; /* верхний предел */ step - 20; /* величина шага */ printf ("Fahr CelsiusW'); fahr-lower; while (fahr Supper) { celsius- (5.0/9.0) * (fahr-32.0); printfC%3.0f %6.1f\nM, fahr, celsius); fahr-fahr+step; } } Добавление printf ("Fahr Celsius\n-); перед циклом порождает заголовок над соответствующими колонками. Мы также добавили два пробела между 8
%3.0f и %6.1f, чтобы выровнять выводимые значения с заголовком. Остаток программы точно такой же, как на C.20KP. Упражнение 1.4 (с.22 КР) Напишите программу печати таблицы соответствия температур по Цельсию температурам по Фаренгейту. #include <stdio.h> /* печать таблицы соответствия температур по Цельсию */ /* температурам по Фаренгейту */ /* для celsius - 0, 20,..., 300; вариант с плавающей точкой */ main О { float fahr, celsius; int lower, upper, step; lower - 0; /* нижний предел таблицы температур */ upper - 300; /* верхний предел */ step - 20; /* величина шага */ printf ("Celsius Fahr\nn); celsius - lower; while (celsius <= upper) { fahr - (9.0*celsius) / 5.0 + 32.0; printfC%3.0f %6.1f\iT, celsius, fahr); celsius - celsius + step; } } Программа выдает таблицу, содержащую температуры в градусах Цельсия (0 — 300) и эквивалентные значения температур по Фаренгейту. Градусы Фаренгейта вычисляются с помощью оператора fahr - (9.0*celsius) / 5.0 + 32.0 Решение следует той же логике, которая используется в программе, печатающей таблицу перевода градусов Фаренгейта в градусы Цельсия. Целочисленные переменные lower, upper и step относятся к нижнему пределу, верхнему пределу и величине шага переменной celsius соответственно. Переменная celsius инициализируется нижним пределом, и внутри цикла while вычисляется эквивалентная температура по Фаренгейту. Программа печатает температуру по Цельсию и Фаренгейту и увеличивает переменную celsius на величину шага. Цикл while повторяется, пока переменная celsius не превысит свой верхний предел. L 9
Упражнение 1.5 (с.23 КР) Измените программу пересчета температур так, чтобы она печатала таблицу в обратном порядке, т.е. от 300 до 0. ♦include <stdio.h> /* печать таблицы соответствия температур по Цельсию V /* температурам по Фаренгейту в обратном порядке */ main О { intfahr; for (fahr - 300; fahr >- 0; fahr - fahr — 20) printfC%3d %6.1f\n*\fahr, (5.0/9.0) * (fahr-32.0)); } Единственное изменение: * for (fahr - 300; fahr >- 0; fahr - fahr — 20) Первая часть оператора fahr-300 инициализирует переменную Фаренгейта (fahr) верхним пределом. Вторая часть, или условие, управляющее циклом for, fahr>-0 проверяет, верно ли, что переменная Фаренгейта больше или равна своему нижнему пределу. Цикл for продолжается, пока оператор истинен. Выражение шага fahr -fahr — 20 уменьшает переменную Фаренгейта на величину шага. Упражнение 1.6 (с.26 КР) Проверьте, что выражение getchar О I-EOF равно 0 или 1. ♦include <stdio.h> main () { hue; while (c-ge1ehar() !-EOF) printfC%d\nw,c); printf C%d — at EOFV, c); } Выражение с-getchar() f-EOF эквивалентно c-(getchar()!-EOF)
(c.25 KP). Программа читает символы со стандартного ввода и использует вышеуказанное выражение. Пока getchar может прочитать символ, она не возвращает конец файла и выражение geleharO !-EOF истинно. Таким образом, переменной с присваивается 1. Когда программа наталкивается на конец файла, выражение ложно. Тоща с присваивается 0, и цикл завершается. Упражнение 1.7 (с.26 КР) Напишите программу, печатающую значение EOF. #include <stdio.h> main () { printfCEOF- %d\n", EOF); } Символическая константа EOF определена в <stdio.h>. EOF снаружи двойных кавычек в printf О заменяется любым текстом, следующим за #defineEOF в подключаемом файле. В нашей системе EOF равна -1, но может различаться от системы к системе. Поэтому стандартные символические константы наподобие EOF помогают сделать программу переносимой. Упражнение 1.8 (с.28 КР) Напишите программу подсчета пробелов, символов табуляции и новой строки. ♦include <stdk).h> /* подсчет пробелов, табуляций и новых строк */ main () { int с, nb, nt, nl; nb - 0; /* количество пробелов */ nt - 0; /* количество табуляций */ nl - 0; /* количество новых строк */ while ((с - getchar О)!- EOF) { if (с--") -Hub; if <c — *\tf) -Hut; if(c--'W) ++nl; printfC%d %d %d\n", nb, nt, nl); } 11
Целочисленные переменные nb, nt и nl используются для подсчета пробелов, табуляций и новых строк соответственно. Первоначально эти три переменные устанавливаются в 0. В теле цикла while регистрируется появление каждого пробела, символа табуляции или новой строки. Все операторы if выполняются при каждом проходе цикла. Если полученный символ — любой, кроме пробела, символа табуляции или новой строки, то не выполняется никакихг действий. Если же полученный символ один из этих трех, увеличивается соответствующий счетчик. Программа печатает результаты, коща завершается цикл while (getchar возвращает EOF). Оператор if-else не представлен до с.29 КР. С использованием этого оператора решение может быть таким: #include<stdio.h> /* подсчет пробелов, табуляций и новых строк */ main О { int с, nb, nt, nl; nb - 0; /* количество пробелов */ nt - 0; /* количество табуляций */ nl - Q; /* количество новых строк */ while ((c-getcharO) !-EOF) { if(c—-") ++nb; else if (с — V) ++nt; else if (c — >W) ++nl; printfC%d %d %d\nH, nb, nt, nl); } Упражнение 1.9 (c.28 KP) Напишите программу копирования ввода на вывод, заменяющую каждую строку из одного или более пробелов единственным. #include<stdio.h> #define NONBLANK V /* заменить строку пробелов единственным V main О { int с, lastc; laste- NONBLANK; while ((c-getchar()) l-EOF) { if(c!-") putchar(c); if(c--") 12
if (lastc!-**) putchar(c); lastc-c; } } Целочисленная переменная с содержит ASCII-код текущего символа, полученного со стандартного ввода, а lastc — ASCII-код предыдущего символа. Символическая константа NONBLANK инициализирует lastc произвольным символом (не пробелом). Первый оператор if в теле цикла while обрабатывает символы; не являющиеся пробелами, т.е. печатает их. Второй оператор if обрабатывает пробелы, а третий проверяет на предмет одиночного или первого из строки пробелов. Наконец, обновляется lastc, и процесс повторяется. Оператор if-else не представлен до с.29 КР. С использованием этого оператора решение могло бы быть таким: #include<stdio.h> #define NONBLANK V /* заменить строку пробелов единственным */ main () { int с, lastc; lastc-NONBLANK; while «c-geteharO) !-EOF) { if (с!-") putchar(c); else if (lastc!-") putchar(c); lastc -c; Операция логическое ИЛИ (II) также не представлена до с.29 КР. С использованием этой операции решение мо- I жет быть таким: I I tinclude <stdio.h> ] #define NONBLANK V 1 /* заменить строку пробелов единственным */ main () { } int с, lastc; lastc-NONBLANK; while ((с - geteharO) 8 - EOF) { j if(c!-" Illaste!-") J putchar(c); lastc -c; 13
Упражнение МО <с.28 КР) Напишите программу копирования ввода на вывод» заменяющую каждый символ табуляции символами \t, шага назад — \Ь и обратной косой черты — \\. Это делает табуляции и наклонные черты видимыми однозначным образом. ♦include <stdio.h> /♦заменить символы табуляции и шага назад видимыми символами*/ main () { intc; whUe <(c-getehar()) !-EOF) { if(c--V) printf CWO; lf(c~f\bf) printf r\\b"); if(c--'W) printf C\\\V); if(c!-'\b*) if(c!-'\t') if(c!-'\V) putchar(c); } } Символ» полученный на вводе, может быть табуляцией, символом шага назад, обратной косой чертой или каким угодно другим. Если это табуляция, заменяем ее на \t, шаг назад — на \Ь, обратная наклонная черта — на \\. Остальное печатается как есть. Символ обратной косой черты представляется в Си как 'W. Мы печатаем две черты, передавая функции printf строку "\\\\м. Оператор if-else не представлен до с.29 КР. С использованием этого оператора решение могло бы быть таким: #include<Stdio.h> /* заменить символы табуляции и шага назад видимыми символами */ main () { intc; while «c-geteharO) !-EOF) { if(c--'\f) printf r\\D; else if (c — *\b*) printf C\\b">; else if (c — »\V) printf Г WW); else putchar(c); } 14
Упражнение 1.11 (с.29 КР) Как бы вы тестировали программу подсчета слов? Какие виды входной информации вероятнее всего выявят ошибки, если таковые имеются? Чтобы проверить программу подсчета слов, сначала попробуйте не вводить ничего. Результат должен быть: 0 0 0 (нуль новых строк, нуль слов, нуль символов). Затем введите слово из одного символа. Результат должен быть: 112 (одна новая строка, одно слово, два символа — буква, а за ней символ новой строки). Затем попробуйте слово из двух символов. Результат должен быть: 113 (одна новая строка, одно слово, три символа — два обычных и новая строка). В дополнение попробуйте ввести 2 слова из одного символа (результат должен быть 1 2 4) и 2 односимвольных слова — по слову на строку (результат должен быть 2 2 4). Видами входной информации, скорее всего выявляющими ошибки, являются те, которые проверяют граничные условия. Вот некоторые границы: — никакого ввода; — никаких слов — только новые строки; — никаких слов — только пробелы, табуляции и новые строки; — одно слово на строку — без пробелов и табуляций; — слово, начинающееся в начале строки; — слово, начинающееся после нескольких пробелов. Упражнение 1.12 (с.29 КР) Напишите программу, которая печатает вводимую информацию по одному слову на строке. «include <stdio.h> ♦define IN 1 /* внутри слова V #define OUT 0 /* снаружи слова */ /* печатать ввод по одному слову на строке */ main О { int с, state; state-OUT; while ((c-getcharO) !-EOF) { if (c — " lie—fW He — 9\tf){ if (state — IN){ putchar('W); Л закончить слово */ state-OUT; } } else if (state--OUT) { state - IN; /* начало слова */ putchar(c); 15
}else putchar(c); Л внутри слова V } } state — целочисленная логическая переменная, которая фиксирует, находится программа внутри слова или нет. В начале программы state присваивается значение OUT (снаружи), так как не было обработано никаких данных. Первый оператор if if (с — " Мс — 'W lie — »\f> выясняет, является ли с разделителем слов. Если да, то второй оператор if if (state — IN) определяет, означает ли этот разделитель конец слова. Если означает, печатается символ новой строки и обновляется state; иначе не производится никаких действий. Если с не разделитель, тоща он либо первый символ слова, либо очередной символ внутри слова. Если это начало слова, программа обновляет state. В противном случае символ печатается. Упражнение 1.13 (с.32 КР) Напишите программу печати гистограммы длин вводимых слов. С помощью горизонтальных столбцов нарисовать гистограмму легко; вертикальная ориентация посложнее. ♦include <stdio.h> ♦define MAXHIST15 /* максимальная длина гистограммы */ ♦define MAXWORD 11/* максимальная длина слова */ ♦define IN 1 /* внутри слова */ ♦define OUT 0 /* снаружи слова */ /* печать горизонтальной гистограммы */ main О { int с, i, nc, state; int len; /* длина каждого столбца */ int maxvalue; /* максимальное значение для wl[i] */ int ovflow; /* количество слишком длинных слов •/ int wl [MAXWORD]; /* счетчики длин слов */ state-OUT; nc - 0; /* количество символов в слове */ ovflow - 0;/* количество слов >-MAXWORD */ for (i - 0; i < MAXWORD; + +i) wl[i]-0; while ((c-getchar()) !-EOF) { if (c — • • 11 с — »W 11 c—f\f> { state-OUT; if (nc>0) 16
if (nc < MAXWORD) ++wl[nc]; else + + ovftow; nc-0; } else if (state — OUT) { state-IN; nc -1; /* начало нового слова */ }else + + nc; /* внутри слова */ } maxvalue - 0; for (i -1; i < MAXWORD; + + i) if (wi[i] > maxvalue) maxvalue-wl[i]; for (i-1; i<MAXWORD; ++i) { printfC%5d - %5d: ", i, wl[i]); if(wl[i]>0){ if (den-wl[i] *MAXHIST / maxvalue) <=0) len-1; }else len-0; while den >0){ putehar ('*•); len; } putehar('W); } if (ovflow>0) printf ("Обнаружено %dслов >- %dV\ ovflow, MAXWORD); } Пробел, новая строка или табуляция отмечают конец слова. Если имеется слово (пс > 0) и его длина меньше максимальной (пс < MAXWORD), программа увеличивает счетчик слов соответствующей длины (+ + wl[nc]). Если длина слова вне диапазона (nc >~ MAXWORD), программа увеличивает переменную ovflow, которая сохраняет количество слов, больших или равных MAXWORD. Когда все слова считаны, программа определяет максимальное значение (maxvalue) из массива wl. Переменная len масштабирует значение wl[i] согласно MAXHIST и maxvalue. Когда wl [i ] больше 0, печатается по крайней мере одна звездочка. ♦include <stdio.h> #define MAXHIST 15 /* максимальная длина гистограммы */ #define MAXWORD 11/* максимальная длина слова */ #define IN 1 /* внутри слова */ #define OUT 0 /* снаружи слова */ /* печать вертикальной гистограммы */ main О { 17
int c, i, j, nc, state; int maxvalue; /* максимальное значение для wl [i] */ int ovflow; /* количество слишком длинных слов V int wl [MAXWORD]; /* счетчики длин слов */ state - OUT; nc - 0; /* количество символов в слове */ ovflow - 0; /* количество слов >- MAXWORD */ for (i - 0; i < MAXWORD; ++i) wl[i]-0; while ((c-getcharO) f-EOF) { if (c — •» lie — 'W И с — V){ state-OUT; if (nc>0) if (nc< MAXWORD) + + wl[nc]; else ++ovflow; nc-0; } else if (state --OUT) { state-IN; nc -1; /* начало нового слова */ }else + + nc; /* внутри слова */ } maxvalue - 0; for(i-l;i<MAXWORD;+ + i) if (wl[i] > maxvalue) maxvalue -wl[i]; for (i - MAXHIST; i > 0; i) { for (j -1; j < MAXWORD;++j) if (wl [j] * MAXHIST / maxvalue >r i) printfC*"); else printfC); putchar('W); } for (i -1; i < MAXWORD; ++i) printfC%4da,i); putchar('W); for (i -1; i < MAXWORD; ++i) printfC%4da,wl[i]); putcharCW); if (ovflow>0) printf("Обнаружено %d слов >- %d\nH, ovflow, MAXWORD); } Эта программа печатает вертикальную гистограмму. Она похожа на предыдущую до места, ще определяется maxvalue. Затем нужно масштабировать элементы массива wl и проверить, для каждого ли должна быть напечатана звездочка. Эта проверка необходима, так как все столбцы печатаются одновременно (гистограмма вертикальная). Последние два цикла for печатают индекс и значение каждого элемента массива wl. 18
Упражнение 1.14 (с.32 КР) Напишите программу, печатающую гистограмму частот встречаемости различных вводимых символов. #include<8tdio.h> #include <ctype.h> #define MAXHIST15 Л максимальная длина гистограммы */ #define MAXCHAR 128 /* количество различных символов */ /* печать горизонтальной гистограммы частот встречаемости */ /* различных символов V main () { int с, i; int len; Л длина каждого столбца */ int maxvalue; /* максимальное значение для cc[i] */ int ее [MAXCHAR]; /* счетчики символов */ for(i-0;i<MAXCHAR;++i) ссШ-0; while ((c-getcharO) !-EOF) if (с < MAXCHAR) + + cc[c]; maxvalue - 0; for(i-l;i < MAXCHAR;++i) if (cc[ij > maxvalue) maxvalue -cc[i]; for <i-l;i< MAXCHAR;+ + i){ if (isprint(i)) printf(H%5d - %c - %5d: ", i, i, cc[i]); else printf C%5d %5d: «f i, cc[i]); if (cc[i]>0){ if (den - cc[i] * MAXHIST / maxvalue) <=0), len-1; }else len-0; while (len >0){ putcharC*'); len; putchar('W); } } Эта программа аналогична горизонтальной гистограмме в упражнении 1.13. Теперь мы считаем частоты различных символов, для чего используем массив счетчиков символов, состоящий из MAXCHAR элементов, и игнорируем символы с кодами больше или равными MAXCHAR, если они существуют в используемом наборе символов. Другое отличие состоит в том, что мы используем макроопределение, чтобы узнать, является ли символ печатаемым. Подключаемый файл <ctype.h> обсуждается на с. 50 КР. Макроопределение isprint описано на с.248 КР (приложение В: стандартная библиотека). 19
Упражнение 1.15 <с.35 КР) Перепишите программу преобразования температур п. 1.12 так» чтобы использовать функцию преобразования. #include<8tdk>.h> float celsiusCfloat fahr); /* печать таблицы соответствия температур по Фаренгейту V /* температурам по Цельсию */ /* для fahr - 0,20,..., 300; вариант с плавающей точкой */ main О { float fahr, celsius; int lower, upper, step; lower - 0; /* нижний предел таблицы температур V upper - 300; /* верхний предел */ step - 20; /* величина шага */ fahr-lower, while (fahr <=upper) { printfC%3.0f %6.1fW\ fahr, celsius (fahr)); fahr- fahr+step; } /* celsius: преобразовать градусы Фаренгейта в градусы Цельсия */ float celsiusCfloat fahr) { return (5.0/9.0) * (fahr-32.0); } Мы используем функцию для преобразования температуры по Фаренгейту в температуру по Цельсию. Имя функции — celsius, она получает значение с плавающей точкой и возвращает другое значение того же типа. Функция возвращает значение выражения с помощью оператора return. Иногда это выражение — простая переменная, как в функции power (с.32 КР). Иноща мы используем более сложное выражение, как в функции celsius, поскольку все можно сделать в операторе return. Мы объявили float celsius (float fahr); потому что функция ожидает значения с плавающей точкой в качестве аргумента и возвращает другое значение с плавающей точкой после выполнения преобразования. Упражнение 1.16 (с.38 КР) Переделайте функцию main программы нахождения самой длинной строки так, чтобы она правильно печатала длину сколь угодно длинных вводимых строк и сколько возможно символов. 20
#include<stdk>.h> #deflne MAXLINE1000 /• максимальный размер входной строки */ int getline (char line [], int maxline); void copy(chartoU> char from[]); /* печатать самую длинную строку ввода V main О int len; Л текущая длина строки */ int max; /* максимальная длина на данный момент •/ char line [MAXLINE]; /* текущая строка ввода */ char longest [MAXLINE]; /* самая длинная строка, сохраненная здесь*/ тах-0; while (Gen - getline (line,MAXLINE)) > 0) { printf(H%d, %8\ ten, line); if (len > max) { max - len; copy (longest, line); } } if (max > 0) /* строка появлялась */ printf(H%sMongest); return 0; /* getline: считывать строку в s, вернуть длину */ int getline (char s[], int lim) { intc.ij; j-0; for (i-0; (c-getchar()) !-EOF**c!-,V;++i) if (i<lim-2){ s[j] — с; /* строка все еще в границах V ++j; } if(c—-'V){ s[j]-c; ++i; } ■Ш-Аоч return i; } /* copy: скопировать 'from' в 'to'; считать, что to достаточно велика */ void copy (char to [], char from []) { inti; i-0; while ((to[i] -from[i]) l-'MT) ++Ц } Единственная переделка в функции main printfC%d, %sH, len, line); с помощью которой печатаются длина вводимой строки (len) и столько символов, сколько можно поместить л массиве line. 21
Функция getline подверглась небольшим изменениям for (i - 0; (с - geteharО)!-EOF && с!- * V; -Ж) Проверка в цикле на предельное количество символов больше не проводится. Это количество не является условием завершения, так как'теперь getline возвращает длину сколь угодно длинных вводимых строк и принимает столько символов, сколько возможно. Оператор if (i<lim-2) выясняет, есть ли место в массиве (который все-таки ограничен) . Исходная проверка в цикле for была i < lim-l Она изменена, потому что последний индекс массива s есть lim-l поскольку в массиве s имеется lim элементов и мы уже прочитали входной символ. Так что i<lim-2 оставляет место для возможного символа новой строки s[lim-2]-'W и для метки конца строки sllim-U-'XO' Длина строки возвращается в переменной i; в j хранится количество символов, скопированных в строку s. Упражнение 1.17 <с.38 КР) Напишите программу печати всех вводимых строк, которые длиннее 80 символов. ♦include <stdio.h> ♦define MAXUNE 1000 /* максимальный размер входной строки */ ♦define LONGLINE 80 int getline (char line [], int maxline); /* печатать самую длинную строку ввода */ main () int len; /* текущая длина строки */ char line [MAXUNE]; /* текущая строка ввода V while <(len-getline(line,MAXUNE)) >0) if (len > LONGLINE) printfC%sn,line); return 0; } Чтобы прочитать входную строку» программа вызывает getline. Функция getline возвращает длину строки н столь-
ко символов, сколько возможно. Если длина больше 80 символов (LONGLINE), программа печатает введенную строку. В противном случае никакие действия не проводятся. Цикл повторяется, пока getline не вернет нулевую длину. Функция getline та же самая, что в упражнении 1.16. Упражнение 1.18 (с-38 КР) Напишите программу, удаляющую хвостовые пробелы и табуляции у каждой вводимой строки и уничтожающую строки, состоящие из одних пробелов. #include<stdio.h> #define MAXLINE 1000 /* максимальный размер входной строки V int getline (char line [], int maxline); int remove (char s []); /* удалить "хвостовые" пробелы и табуляции, уничтожить пустые строки*/ main О { char line [MAXLINE]; /* текущая строка ввода */ while (getlinedine,MAXLINE) >0) if (remove (line) >0) printfC%sMine); return 0; } /* удалить "хвостовые" пробелы и табуляции из символьной строки s */ int remove (char s []) { inti; i-0; while (s [i] !-' W) /* поиск символа новой строки */ ++i; i; /* смещаемся назад от 'V */ while (i >-0 && (s[i] —" 11 i[i] - - »\f)) if (i >- 0) { /* это не пустая строка? •/ ++i; s [i] - 'W; /* возвращаем символ новой строки */ ■ж; s[i] - *\0*; /* завершаем строку */ } return i; } Функция remove удаляет хвостовые пробелы и табуляции из символьной строки line и возвращает ее новую длину. Если эта длина больше 0, то в line имеются символы, отличные от пробелов и табуляций, и программа печатает эту строку. В противном случае line полностью состоит из пробелов и табуляций и таким образом игнорируется. Это обеспечивает подавление печати полностью пробельных строк. 23
Функция remove находит символ новой строки и сдвигается назад на одну позицию. Затем движется назад по пробелам и табуляциям, пока не найдет какой-либо другой символ или символов больше не останется (КО). Если i >- 0, то остался по крайней мере один. Функция remove ставит в конец символы новой строки и конца строки, затем возвращает i. Функция getline та же, что в упражнении 1.16. Упражнение 1.19 (с.38 КР) Напишите функцию reverse(s), переворачивающую символьную строку s (располагает символы в обратном порядке). Используйте ее для написания программы, которая переворачивает вводимые символьные строки. «include <stdio.h> #define MAXLINE 1000 /* максимальный размер входной строки */ int getline (char line [], int maxllne); void reverse (char s []); /* разворачивать входные строки, по строке за один прием */ main О char line [MAXLINE]; /* текущая строка ввода */ while (den-getline(line,MAXLINE)) >0) { reverse (line); printfC%sMine); } /* reverse: перевернуть строку */ void reverse (char s []) { intij; char temp; i-0; while (s[ij !- *\0*) /* ищем конец строки */ ++i; i; /* смещаемся назад от *\0* */ if(s[i]--'W) i; /* оставляем символ новой строки на месте V j - 0; /* начало новой строки s */ while (j<i){ temp-s[j]; s [j] - s [i]; /* обмениваем символы V s[i]-temp; i; } } Сначала функция reverse находит конец строки s. Затем она смещается на позицию назад от Э\0Э так, чтобы 24
первым символом не стал символ конца строки. Если присутствует '\п\ функция смещается еще на одну позицию, поскольку, как и '\0\ этот символ должен остаться в конце строки. j — индекс первого символа строки, a i — последнего. В процессе обмена символов j увеличивается (продвигается к концу строки), a i уменьшается (движется к началу). Процесс продолжается, пока j меньше i. Функция main читает за один прием по строке, разворачивает полученную и печатает перевернутую строку. Функция getline та же, что в упражнении 1.16. Упражнение 1.20 (с.41 КР) i Напишите программу detab, которая заменяет символы табуляции в стандартном вводе точным количеством пробелов, чтобы пропустить место до следующей позиции табуляции. Считайте, что количество позиций табуляции фиксировано, пусть между ними имеется п колонок. Должно ли п быть переменной или символическим параметром? ♦include <stdio.h> #define TABINC 8 /* шаг увеличения позиции табуляции */ /* заменить табуляции нужным количеством пробелов */ main О { int с, nb, рое; nb - 0; /* необходимое количество пробелов */ рое -1; /* позиция символа *в строке */ while ((c-geleharO) !-EOF) { if (с - - *\f) { /* символ табуляции •/ nb -TABINC — (pos — 1) % TABINC; while (nb>0){ (nifcharf); ++pos; nb; } else if (c - - '\n*> { /* символ новой строки */ putchar(c); pos-1; } else { /* все другие символы */ putchar(c); ++pos; } } } Позиции табуляции находятся на каждой TABINC позиции. В этой программе TABINC определена как 8. Пере- 25
менная pos — позиция в строке текста, ще программа находится в данный момент. Если данный символ — табуляция, программа вычисляет количество пробелов, нужное для того, чтобы добраться до следующей позиции табуляции. Это значение определяет оператор nb-TABINC — (рое — 1) % TABINC Если же данный символ — новая строка, он печатается, и переменная pos устанавливается в начало строки (pos - 1), Любой другой символ просто печатается, при этом увеличивается pos (+ + pos). TABINC — символическая константа. В гл. 5 вы узнаете, как передавать аргументы функции main, и, может быть, захотите позволить пользователю устанавливать количество колонок между позициями табуляции. Затем вы, наверное, захотите сделать TABINC переменной. Программа detab расширяется в упражнениях 5.11 и 5.12. Упражнение 1.21 (с.41 КР) Напишите программу detab, заменяющую строки пробелов минимальным количеством пробелов и табуляций, нужным, чтобы достичь такого же пустого места. Используйте те же позиции табуляции, что и в detab. Чему лучше отдать предпочтение: коща достаточно символа табуляции или пробела, чтобы достичь позиции табуляции? ♦include <stdio.h> #define TABINC 8 /* шаг увеличения позиции табуляции */ /* заменить строки пробелов табуляциями и пробелами */ main () { int с, nb, nt, pos; nb - 0; /* необходимое количество пробелов */ nt - 0; /* необходимое количество табуляций •/ for (pos -1; (с - getchar О)! - EOF; ++pos) if(c--"){ if (pos % TABINC !-0) ■Hub; /* увеличиваем число пробелов */ else{ nb - 0; /* сбрасываем число пробелов V -Hut; /* еще одна табуляция */ } }etee{ for(;nt>0; nt) putchar('\t*); /* выводим табуляции */ 26
if (с - - *\f) /* забываем о пробелах •/ nb-0; else /* выводим пробелы */ for(;nb>0; nb) putcharC'); putchar(c); if(c--'V) pos-O; else if (c — *\f) pos-pos+ (TABINC — (pos-1) % TABINC) — 1; } } Целочисленные переменные nb и nt — минимальные количества пробелов и табуляций, необходимые для замены строки пробелов. Переменная pos - позиция в строке текста, где в данный момент находится программа. Идея состоит в том, чтобы находить все пробелы. Строка пробелов заменяется символом табуляции каждый раз, когда pos достигает значения, делящегося без остатка на TABINC. Когда программа находит символ, не являющийся пробелом, она печатает накопленные табуляции и пробелы, а затем этот символ. Программа устанавливает nb и nt в нуль, а если текущий символ — новая строка, то и pos — в начало строки. Если встретившийся символ является табуляцией, программа печатает только накопленные табуляции, а следом него. Когда одиночного пробела достаточно для достижения позиции табуляции, легче заменить его символе»! табуляции, так как тоща мы избегаем особых случаев. Программа entab расширяется в упражнениях 5.11 и 5.12. Упражнение 1.22 <с.41 КР) Напишите программу, "складывающую" длинные вводимые строки в две или более короткие строки после первого символа, не являющегося пробелом, который встретится перед n-й колонкой ввода. Убедитесь, что ваша программа делает что-нибудь разумное с очень длинными строками, даже если до указанной колонки нет ни пробелов, ни табуляций. #include <stdio.h> #define MAXCOL10 /* максимальная колонка ввода */ #define TABINC 8 /* шаг увеличения позиции табуляции */ char line [MAXCOL]; /* вводимая строка */ intexptab(uitpos); 27
intfindblnk(intpos); intnewposGntpos); void printKint pos); /* располагать длинные входные строки в две или более короткие */ main О { intcpos; pos - 0; /* позиция в строке */ while ((c-geteharO) !-EOF) { line [pos] - с; /* записываем текущий символ */ if (с -- >\t') /* расширяем символ табуляции */ pos-exptab(pos); else if (с — *W) { printl(pos); /* печатаем текущую введенную строку */ pos-0; } else if (++pos >-MAXCOL) { pos-findblnk(pos); printKpos); pos-newpos (pos); } } } /* printl: печатать строку до колонки pos */ void printKint pos) { inti; for(i-0;i<pos;+-H) putchar(lme[i]); if (pos > 0) /* печатались ли какие-либо символы? */ puteharfV); /* exptab: расширить табуляцию пробелами */ int exptab (int pos) line [pos] -''; /* табуляция дает по крайней мере один пробел */ for (++pos; pos < MAXCOL && pos % ТАВШС !-0; ++ров) line[pos]-"; if (pos < MAXCOL) /* в текущей строке осталось место*/ return pos; else { Л текущая строка полна */ printl (pos); return 0; Л сбрасываем текущую позицию */ } Л findbink: найти позицию пробела */ intfmdblnk(intpos) { while (pos > 0 && line [pos]!-") pos; if (pos--0) /• в строке нет пробелов?*/ return MAXCOL; else /* по крайней мере один пробел */ return pos+1; /* позиция за пробелом */ /* newpos: перерасположить строку с новой позиции */ int newpos (int pos) 28
{ inti.j; if (рое <=0 11 pos >-MAXCOL), return 0; /* нечего перерасполагать */ else{ i-0; for (j-pos; j <MAXCOL; ++j) { line [i]-line [j]; ++i; } return i; /* новая позиция в строке */ } } MAXCOL — символическая константа» она указывает n-ю колонку ввода. Целочисленная переменная pos указывает на позицию в строке текста, вде программа находится в данный момент. Программа складывает вводимые строки перед n-й колонкой ввода. Программа расширяет символы табуляции, печатает текущий ввод, коща находит символ новой строки, и складывает вводимую строку, коща pos достигает MAXCOL. . Функция findblnk ищет пробел, начиная с индекса pos. Она возвращает позицию после пробела или MAXCOL, если пробела нет. Функция printl печатает символы между позициями 0 и pos-1. Функция newpos перерасполагает строку, т.е. копирует символы, начиная с pos, в начало строки, затем возвращает новое значение pos. Упражнение 1.23 (с.41 КР) Напишите программу, удаляющую все комментарии из программы на Си. Не забудьте корректно обрабатывать строки в кавычках и символьные константы. Комментарии в Си не вкладываются друг в друга. #incltide<stdk>.h> void rcommentGnt с); void in_comment(void); void echo_quote(int c); /* удалить все комментарии из нормальной программы на Си */ mainO { int с, d; while «c-getcharO) !-EOF) rcomment(c); return 0; /* rcomment: считывать каждый символ, удалять комментарии */ void rcomment(int с) { 29
intd; if(c--V) if((d-gelcharO) — •*•> in comment О; /* начало комментария V else if (d — V) { putchar (c); /* вторая косая черта */ rcomment(d); }else{ putchar(c); /* не комментарий */ putchar(d); } else if (с — 'V lie — "") echo_quote(c); /* начало кавычек */ else putchar (с); /* не комментарий •/ } /* in_comment: внутри нормального комментария V void in_comment(void) { intc, d; с - getchar (); /* предыдущий символ */ d - getchar (); /* текущий символ */ while (с!- »•• 11 d!- 7') { /• поиск конца •/ c-d; d-gelehar(); } } /* echo_quote: отображать символы внутри кавычек */ void echo_quote(int с) { intd; putchar (с); while ((d - getchar ())!- c) { /* поиск конца */ putchar(d); if(d--'W) putchar (getchar О); /* игнорируем управляющую */ /* последовательность */ } putchar (d); } Программа считает, что на входе — нормальная программа на Си. Функция rcomment ищет начало комментария (/*), а когда находит — вызывает in_comment. Эта функция ищет конец комментария. Таким образом, вся процедура гарантирует, что комментарий будет игнорироваться. Функция rcomment ищет также одиночные и двойные кавычки и если находит, вызывает echo_quote. Аргумент echo—quote показывает, встретилась одиночная или двойная кавычка. Функция echo_quote гарантирует, что все внутри кавычек отображается точно и не принимается по ошибке за комментарий. Функция echo_quote не считает 30
кавычку, следующую за обратной косой чертой, завершающей (см. обсуждение управляющих последовательностей на с. 16 КР и в упражнении 1.2). Любой другой символ печатается как есть. Программа завершается, коща getchar возвращает символ конца файла. Упражнение 1.24 (с.41 КР) Напишите программу, проверяющую программу на Си на элементарные синтаксические ошибки вроде непарных круглых, квадратных или фигурных скобок. Не забывайте о кавычках как двойных, так и одиночных, управляющих последовательностях и комментариях. (Эта программа трудна, если делать ее для всех случаев.) #include <stdio.h> int brace, brack, paren; void in_quote (int c); void in_comment(void); void search (int c); /* программа, проверяющая элементарные ошибки в программах на Си*/ main () { int с; extern int brace, brack, paren; while ((c-getchar()) !-EOF) { If (c--•/•){ if ((c-getchar()) — •••) in_comment(); /* внутри комментария */ else search (с); } else if (с — 'V lie —9Hf) in_quote(c); /* внутри кавычек */ else search (с); if (brace < 0) { /• вывод ошибок */ printf ("Непарные фигурные скобкиХп"); brace-0; } else if (brack <0){ printf ("Непарные квадратные скобкиХп"); brack -0; } else if (paren <0){ printf ("Непарные круглые скобкиХп"); paren-0; } } if (brace >0) printf ("Непарные фигурные скобкиХп"); if (brack >0) printf ("Непарные квадратные скобкиХп"); if (paren >0) printf ("Непарные круглые скобкиХп"); 31
} /* search: поиск элементарных синтаксических ошибок */ void search (int с) { extern int brace, brack, paren; if(c--T) ++brace; else if (c — T) brace; else if (c — ++brack; else if (c — T> T> brack; else if (c— -Hparen; else if (c — fC> T) paren; /* in_comment внутри нормального комментария */ void in comment (void) { intc, d; с - getchar (); /* предыдущий символ V d - getchar (); /* текущий символ */ while (с!- **Ч I d!- V) { /* поиск конца */ c-d; d-getchar О; } } /* in_quote: внутри кавычек */ voidin_quote(intc) { intd; while ((d - getchar О)!- с) /* поиск конца кавычек */ if(d--'W) getchar (); /* игнорируем управляющую */ /* последовательность */ } Это решение сделано не для всех случаев. Программа проверяет синтаксические ошибки трех видов: непарные круглые, квадратныеЪли фигурные скобки. Все остальное считается нормальным. Функция search увеличивает переменную brace, когда символ - открывающая фигурная скобка, и уменьшает ее, когда закрывающая. Переменные brack (для квадратных скобок) и рагеп (для круглых) обрабатываются аналогично. В процессе поиска допускается переменным brace, brack и paren быть больше или равными нулю. Если brace, brack или paren становятся отрицательными, это ошибка; программа печатает соответствующее сообщение. [ [ [ (brack равна 3) допустимо в какой-то момент, поскольку 3 пар- 32
ные закрывающие скобки могут быть найдены далее. ] ] ] (brack равна -3) - недопустимо, так как ранее не было открывающих скобок, парных этим трем закрывающим; если же они были, brack должна равняться 0. Операторы if (brace <0){ printf ("Непарные фигурные скобкиХп"); brace ™ 0* } else if (brack <0){ printf ("Непарные квадратные скобкиХп"); brack-0; } else if (paren < 0) { printf ("Непарные круглые скобкиХп"); paren-0; } необходимы, потому что без них ) (, ]]][[[ или }}{{ считались бы парными. Функция main ищет*комментарии, одиночные и двойные кавычки и пропускает символы внутри них. Ни круглым, ни квадратным, ни фигурным скобкам нет необходимости быть парными внутри комментариев или кавычек. Программа делает окончательную проверку у EOF, чтобы определить, есть ли еще открытые скобки. Если есть, она печатает соответствующее сообщение. О -7КО
Глава 2 Типы, операции и выражения Упражнение 2.1 (с.43 КР) Напишите программу, определяющую диапазон значений переменных типа char, short, int и long как signed (со знаком), так и unsigned (беззнаковых) путем печатания соответствующих значений из стандартных заголовочных файлов или непосредственным вычислением. Вычислять труднее; определите диапазоны значений различных типов с плавающей точкой. ♦include <stdio. h> ♦include <limits. h> /* определить диапазоны типов */ main () { /* знаковые типы */ printf ("signed char min - %d\n", SCHAR_MIN); printf ("signed char max - %d\n", SCHAR_MAX); printf ("signed short min - %d\n", SHRTJ4IN); printf ("signed short max - %d\nH, SHRTJ4AX); printf ("signed int min - %dW\ INT_MIN); printf ("signed int max - %d\nH, INT_MAX); printf ("signed long min - %ld\n", LONG_MIN); printf ("signed long max - %ld\n", LONG_MAX); /* беззнаковые типы */ printf ("unsigned char min - %d\n", UCHAR.MAX); printf ("unsigned short max - %d\n", VSHRTJAAX); printf ("unsigned int min - %d\n", UINT^MAX); printfCunsigned long max- %d\n",ULONG_MAX); } ANSI-стандарт языка Си указывает, что диапазоны должны быть определены в <limits...h>. Диапазоны могут отличаться в зависимости от использования машины, так как размеры short, int и long отличаются на разных аппаратных средствах. #include <stdio. h> /* определить диапазоны типов •/ main О /* знаковые типы •/
printf ("signed char min - %d\nN, -(char)«unsignedchar) ~0»1)); printf ("signed char max - %d\n", (char) ((unsignedchar) ~0»1)); printf ("signed short min - %d\n\ -(short) ((unsignedshort) ~0»1)); printf ("signed short max - %d\nH, (short) ((unsigned short) ~0 » D); printf ("signed int min - %d\nH, -(int) ((unsigned int) ~0» D); printf ("signed int max - %d\nH, (int) ((unsigned int) ~0 » D); printf ("signed long min - %ld\n", -(long) ((unsigned long) ~0 » D); printf ("signed long max - %ld\n", (long)((unsignedlong) ~0» D); /* беззнаковые типы */ printf ("unsigned char min - %d\n", (unsigned char) ~0); printf ("unsigned short max - %d\n", (unsigned short) -0); printf ("unsigned int min - %d\n*\ (unsigned int) ~0); printf ("unsigned long max - %d\n\ (unsigned long) ~0); } В другом возможном решении используются битовые операции (с.54 КР). Например, выражение (char) ((unsigned char) ~0 »1) берет 0 и преобразует каждый бит в единицу ~о затем полученное значение преобразует в unsigned char (unsigned char) ~0 и сдвигает unsigned char на одну позицию вправо, чтобы очистить бит знака (unsigned char) ~0 »1 наконец, преобразует значение к char (char) ((unsigned char) ~0 » 1) Это максимальное значение для символа со знаком (signed). Упражнение 2.2 (с.49 КР) Напишите цикл, эквивалентный указанному циклу for без использования && и 11. Оригинал: for (i-0; i < lim-1 && (c-geteharO)!- • W &&с НEOF; ++i) ' 35
Эквивалентный цикл: enum loop { NO, YES }; enum loop okloop - YES; i-0; while (okloop—YES) if (i>-lim-l) okloop-NO; else if «c-geteharO) — »W) okloop-NO; else if (c — EOF) okloop-NO; else{ s[i]-c; ++i; } Без использования && и 11 придется разбить исходный цикл for на последовательность операторов if. В этом случае мы изменяем условия. Например, в исходном цикле i<lim-l означает, что i все еще внутри границ. В эквивалентном операторе i>-lim-l означает, что i вышла за границы и цикл должен завершиться. Переменная okloop — перечислимого типа. Как только одно из условий удовлетворяется, okloop устанавливается в N0 и цикл завершается. Упражнение 2.3 (с.52 КР) Напишите программу htoi(s), которая преобразует строку шестнадцатеричных цифр (возможно, включающую Ох или ОХ) в эквивалентное ей целочисленное значение. Допустимые цифры — 0 — 9, a — f и А — F. #defineYESl #defineNO0 /* htoi: преобразовать шестнадцатеричную строку s в целое число */ inthtoi(chars[]) { int hexdigit, i, inhex, n; i-0; if (s [i] - - '0*) { /* пропускаем необязательные Ох или ОХ */ ++i* if(s[i]--V lls[i]—'X') ++i; n - 0; /* целочисленное значение, которое будет возвращено */ inhex -YES; /* ожидаем нормальную шестнадцатеричную цифру */ for (; inhex — YES;++i){
if(s[i]>-,<r&&s[i] ^'9') hexdigit-s[i] — 4V; else if (s[i] >V44s[i] <=T) hexdigit - 8 [i]—V +10; else if <s[i] >->A> &* s[i] <='F> hexdigit-s[i]—'A' + IO; else inhex - NO; Л не шестнадцатеричная цифра */ if (inhex—YES) n-16 *n +hexdigit; } return n; } Оператор for (; inhex—YES;++i> управляет функцией. Целочисленная переменная i — индекс в массиве s. Пока s[i] является шестнадцатеричной цифрой, inhex остается равным YES и цикл продолжается. Переменная hexdigit принимает численное значение от 0 до 15, Оператор if (inhex — YES) гарантирует, что s[i] — действительно шестнадцатеричная цифра и ее значение находится в hexdigit. Коща цикл завершается, htoi возвращает значение переменной п. Эта функция аналогична atoi (с. 49 КР). Упражнение 2.4 (с.54 КР) Напишите альтернативную версию функции squeeze (si, s2), уничтожающей каждый символ в строке si, который совпадает с некоторым символом в строке s2. /* squeeze: уничтожить каждый символ в строке s 1, имеющийся в s2*/ void squeeze(char si [], char s2 []) { inti,j,k; for (i - k - 0; si [i]!- 9\W; H+) { forq-0;s2[j] !-'\0'&*s2U] !-sl[i];)4+) » if (s2 Ц] - - 40*) /• конец строки — совпадения нет •/ sl[k++]-sl[i]; sl[k]-'\0'; } Первый оператор for (i - k - 0; si [i] !- Л0»; H+>
инициализирует ink — индекс» si и выходной строки (тоже si) соответственно. Каждый символ в si, совпадающий с любым символом в s2, уничтожается. Цикл продолжается до конца строки si. Второй оператор for сравнивает каждый символ s2 с символом si [i ]. Этот цикл завершается, когда в s2 кончаются символы или происходит совпадение. В случае, если совпадения не произошло, sl[i] копируется в выходную строку. Если совпадение есть, оператор не выполняется и si [i ] не копируется (выбрасывается). Упражнение 2.5 (с-54 КР) Напишите функцию any(sl,s2), возвращающую первую позицию в строке si, в которой встретился любой символ из строки s2, или -1, если si не содержит символов из s2 (функция стандартной библиотеки strbrk делает то же самое, но возвращает указатель на эту позицию). /* any: вернуть первую позицию в si, где встретился любой символ из 82 V tatany(charsl(],chars2f)) { inti,j; for(i-0;sl[i]!-v\0';H+) for <)-О; ЯШ *-'№;!*+) if (sl(i] —s2[j]> /• совпадение? */ return i; /* позиция первого совпадения */ return -1; /* иначе совпадения нет V } Оператор for<i-0;sl[i]!-'\<r;i++) управляет циклом. Когда цикл завершается нормально (в si кончаются символы), any возвращает -1, чтобы показать, что в si не было найдено ни одного символа из s2. Второй оператор for forq-0;s2Q]!-'\0';j++) выполняется для каждого значения L Он сравнивает каждый символ s2 с si [i ]. Когда символ в s2 совпадает с si [i ], функция возвращает i — первую позицию в строке si, в которой встретился символ из строки s2. Упражнение 2.6 (с.55 КР) Напишите функцию setbits(x,p,n,y), которая возвращает х, где п битов, начиная с позиции р, установлены так, зя
A как п самых правых битов в у, а остальные биты не изменились. /* setbits: установить п битов в х в позиции р битами у */ unsigned setbits (unsigned x, kit p, hit n, unsigned у) { return x & ~(~<-G« n) « (p+l-n)) I (y & ~<~0« n)) « (p+l-n); } Чтобы установить n битов в х так, как установлены п правых битов у, ХХХ...ХПППХ...ХХХ X ууу ynnny нужно очистить эти п битов в х, очистить все биты у, кроме п правых, и сдвинуть их в позицию р, а затем проделать над полученными значениями операцию битового ИЛИ. xxx...x000x...xxx х 000...0nnn0...000y ХХХ...ХПППХ...ХХХ X Чтобы очистить эти п битов в х,. выполняем побитовое И с числом, состоящим из начинающихся с позиции р п нулевых битов и единиц в остальных битах. ~0«п сдвигает число из всех единиц на п позиций влево, оставляя п нулей на правых позициях. ~(~0«п) ставит единицы в п правые позиции и нули в остальные. ~(~0«п)«(р+1-п) сдвигает эти п единиц в позицию р, а ~(~(~0« n) « (p+1-n)) устанавливает в нули п битов, начинающихся с позиции р, и в 1 все остальные биты, х & ~<~<~о « п)«(p+1-n)) мы выполняем битовое И этого значения с х, чтобы очистить п битов х, начиная с позиции р. Чтобы очистить все биты в у, кроме п правых, выполняем битовое И с п единицами, остальные нули. ~(~0«п) устанавливает п правых битов в 1, остальные в (К у&~(~0«п) 3$
выбирает п правых битов из у. И (у & ~(~о « п» «(p+1-n) помещает эти п битов в позицию р. х & ~(~(~0 « n) « (p+1-n)) I (у & ~<~0 « п)) « (p+1-n) выполняем битовое ИЛИ двух значений, чтобы установить п битов в х, начиная с позиции р, так, как п правых битов в у, не изменяя остальные биты. Упражнение 2.7 (с.55 КР) Напишите функцию invert(х,р,п), которая возвращает х, ще п битов, начиная с позиции р, инвертированы (т.е. 1 становится 0 и наоборот), а остальные биты не изменились. /* invert: инвертировать п битов в х, начиная с позиции р */ unsigned invert (unsigned x, int p, int n) return x л (-(-0 « n) « (p+l-n); } (~0«n) сдвигает все единицы на п позиций влево, оставляя п нулей в правых позициях. -(H) <<п) помещает в правые позиции единицы, в остальные нули. (~(~0«п)«(р+1-п) сдвигает эти п единиц в позицию р. х~(~(~0«п)«(р+1-п) битовая операция "исключающее ИЛИ" С4) дает 1, если два бита различны, иначе 0. Так как наша цель — инвертировать п битов, начиная с позиции р, то достаточно сделать исключающее ИЛИ с числом из п единиц в тех же позициях (остальные биты нулевые). Если исходный бит О, исключающее ИЛИ с 1 дает 1 — бит инвертируется. Если исходный бит 1, исключающее ИЛИ с 1 дает 0 - бит тоже инвертируется. Позиции снаружи n-битового поля подвергаются исключающему ИЛИ с нулями: 0^0 (биты одинаковые) дает 0 — ничего не меняется; 1^0 (биты разные) дает 1 — ничего не меняется. Инвертируются только нужные п битов. Упражнение 2.8 (с.55 КР) Напишите функцию rightrot(x,n), которая возвращает значение целочисленного х, "повернутого" вправо на п позиций. 40
/* rightrot: "вращать" х вправо на п позиций */ unsigned rightrot (unsigned x, int n) { int wordlength (void); int rbit; /* самый правый бит */ while (n >0){ rbit - (x * 1) « (wordlengthO — 1); x - x » 1; /* сдвигаем х на позицию вправо */ x - x I rbit; /* завершаем поворот на одну позицию */ } return x; } Переменная rbit берет самый правый бит х и сдвигает его в самую левую позицию (wordlengthO — 1). Далее мы сдвигаем х на позицию вправо и делаем битовое ИЛИ с rbit, завершая тем самым поворот на одну позицию. Функция rightrot поворачивает х п раз. Функция wordlengthO подсчитывает длину слова на данной машине. /* wordlength: подсчитывает длину машинного слова */ int wordlength (void) { inti; unsigned v - (unsigned) ~0; for(i-l;(v-v>l)>0;i++) return i; } Здесь приведено другое решение: /* rightrot: "вращать" х вправо на п позиций */ unsigned rightrot (unsigned x, int n) { int wordlength (void); unsigned rbits; if ((n-n % wordlengthO) > 0) { rbits - ~(~0< n) A x; /* n самых правых битов х */ /* сдвигаем в крайнюю левую позицию */ rbits - rbits < (wordlengthO — n); x - x »1; /* сдвигаем х на n позиций вправо */ х - х I rbit; /* поворот завершен */ } return x; } Если число позиций (п), на которые нужно повернуть вправо, равно числу битов в беззнаковом целом, то ничего не меняется, так как значение х будет таким же, как и до "вращения". Если меньше — нужно повернуть на а позиций. Если же п превышает число битов в беззнаковом целом, число поворотов является остатком от деления 41
(операция деления по модулю) п на длину машинного слова. В результате отпадает необходимость в цикле. ~0 « п все единицы сдвинуты на п позиций влево, оставляя п нулей в правых позициях. ~(~0 « п) все единицы — в п правых позициях. Коща мы делаем битовое И этого значения с х, п правых битов х присваиваются переменной rbits. Затем сдвигаем rbits в самую левую позицию. Сдвигаем хна о позиций вправо. Проделываем битовое ИЛИ нового значения х с rbits, чтобы завершить поворот беззнакового х на п позиций вправо. Упражнение 2.9 (с.57 КР) В двоично-дополнительной системе счисления х &«(х- 1) уничтожает самый правый единичный бит в х. Объясните, почему. Используйте это наблюдение для написания более быстрой версии программы bitcount. /* bitcount: подсчитывает единичные биты в х—более быстрая версия */ hit bitcount(unsigned x) { into; for (b-0; x!- 0; x *- (x-D) ++b; return b; } Возьмите, например, для значения х-1 двоичное число 1010 (десятичное 10). (х-1) +1 даетх: двоичное десятичное 1010 х-1 10 + + 1 1 1011 х 11 Мы берем (х-1) и добавляем 1, чтобы получить х. Самый правый нулевой бит значения х-1 превращается в 1 в значении х. Таким образом, у самого правого единичного бита в х имеется соответствующий нулевой бит в значении (х-1). Вот почему х & (х-1) в двоично-дополнительной системе счисления уничтожает самый правый единичный бит в х. Рассмотрим беззнаковый набор 4 бит. Чтобы подсчитать все единичные биты в этом наборе, исходная bitcount делает четыре сдвига для проверки самого правого бита. Дру- 42
гой способ состоит в том, что х & (х-1) удаляет самый правый единичный бит в х. Например, если переменная х равна 9 1001 число 9 в двоичной записи (х) 1000 число 8 (х-1) 1000 х& (х-1) в результате самый правый единичный бит в х уничтожен. Полученное значение — 1000 в двоичной записи, десятичное 8. Повторяем процедуру 1000 число 8 в двоичной записи <х) 0111 число 7 (х-1) 0000 х* (х-1) и самый правый единичный бит в х уничтожен. Полученное значение — 0000 двоичное, десятичное 0. В х больше нет единичных битов, и процесс завершается. В худшем случае все биты — единицы, тоща количество битовых И равно количеству сдвигов в bitcount. В целом же эта версия работает быстрее. Упражнение 2.10 (с-58 КР) Перепишите функцию lower, преобразующую заглавные буквы в строчные, так, чтобы она содержала условное выражение вместо if-else. /• lower: преобразовать с в строчную букву (только для кодов ASCII) */ intlower(intc) { returnc>«,A,&&c<«'Z'? c + V — *A*:c; } Когда условие c>-'A,&&c<-,Z' истинно, с является заглавной буквой (только в кодах ASCII). В этом случае вычисляется выражение c + V — 'А* и lower возвращает строчную букву. Иначе lower возвращает символ, не изменяя его. 43
Глава 3 Управление Упражнение 3.1 (с-63 КР) Наш бинарный поиск выполняет две проверки внутри цикла, в то время как достаточно одной (за счет большего количества проверок снаружи). Напишите версию с проверкой внутри цикла и измерьте разницу во времени работы. /*ЫпаеагсН:найтихву[0] <-v[l] <-...<- v[n-l] V int binsearchdnt x, int v[], int n) { int low, high, mid; low-O; high-n —1; mid-(lowfhigh)/2; while (low<-high &&x! -v[mid]) { if(x<v[mid]) high-mid-1; else low - mid+ 1; mid-(low+high)/2; } if (x — v[mid]) return mid; /* совпадение найдено */ else return -1; /* нет совпадения */ } Мы изменили выражение в цикле while с low <- high на low <- high && х!- v [mid], так, что внутри цикла понадобится, только один оператор if. Под этим подразумевается, что мы должны вычислять mid до начала цикла и каждый раз, когда цикл выполняется. Необходима также проверка снаружи цикла while, чтобы определить, завершился ли цикл вследствие того, что значение х оказалось в массиве v. Если это так, linsearch возвращает mid, иначе -1.
Разница во времени работы минимальна. Мы не выиграли существенно в производительности и потеряли в читабельности программы. Исходная программа на с.62 КР читается лучше от начала до конца. Упражнение 3.2 (с.64 КР) Напишите функцию escape( s, t), которая преобразует символы вроде новой строки или табуляции в видимые escape-последовательности, такие, как \п или \t, копируя строку t в строку s. Используйте оператор switch. Напишите также обратную функцию, преобразующую escape-последовательности в настоящие символы. /* escape: расширить символы новой строки и табуляции в видимые •/ Л последовательности в процессе копирования строки t в s */ void escape (char a [], char t []) { inti,j; for(i-j-0;t[i]!-,\0,;l++) switch (t[i]){ case' W: /* символ новой строки •/ st^H-l-'W; s[j++]-V; break; case '\f: /• символ табуляции */ 8[*н-]-'\\'; break; default: /* все другие символы */ s[}++J-t[i]; break; s[j]-'\0'; } Оператор for(i-j-0;t[i]!-'\<r;R+) управляет циклом. Переменная i — индекс исходной строки t, a j — измененной строки s. В операторе switch три варианта:' W для символа новой строки, '\t' для символа табуляции и по умолчанию (default). Если символ t[i] не совпадает ни с одним из первых двух, escape выполняет вариант, обозначенный default: копирует t[i ] в строку s. /* unescape: преобразовать управляющие последовательности */ /* в настоящие символы в процессе копирования строки t в s */ void unescape (char s [], char t []) { intij; forG-j-O-.tmi-'XO'iH-i-) 45
sU++]-tM; else /• это обратная косая черта •/ switch(t[i]){ case V: /* настоящий символ новой строки */ s[)++]-'V; break; case 'f: /* настоящий символ табуляции */ 8[j++]-'\t'; break; default: /* все другие символы */ sU++]-»\V; slj++]-tM; break; stfl-'W; Если символ t[i] — обратная косая черта, используется оператор switch, чтобы преобразовать \п в символ новой строки или \t в табуляцию, default обрабатывает обратную косую черту, за которой следует что-либо другое — копирует косую черту и t [i 1 в строку s. Операторы switch могут быть вложенными. Ниже приводится другое решение той же самой задачи. /* unescape: преобразовать управляющие последовательности V /* в настоящие символы в процессе копирования строки t в s */ void unescape (char s [], char t []) { inti.j; fard-j-0;t[i]!-'\0';i++) switch (t[i]){ case *\V: /* обратная косая черта */ switch (t [++!]){ case V: /* настоящий символ новой строки */ s[J++]-'V; break; case't': /* настоящий символ табуляции */ s[j++]-'\t'; break; default: /* все другие символы */ stff+l-'W; s[}++]-t[i]; break; } break; default: /* это не обратная косая черта */ eO++]-t[i]; break; sUJ-AO'; } 46
Внешний оператор switch обрабатывает обратную косую черту или любой другой символ (default). Случай обратной косой черты использует другой оператор switch, как и в предыдущем решении. Упражнение 3.3 (с-67 КР) Напишите функцию expand( si, s2), которая расширяет краткие записи вида a-z в строке si в соответствующий полный список abc.xyz в строке s2. Должны восприниматься как строчные, так и прописные буквы, а также цифры, и будьте готовы обрабатывать случаи вроде а-Ь-с, a-zO-9 или -a-z. Пусть ведущий или "хвостовой" дефис берется буквально. /* expand: расширить краткую запись, находящуюся в s 1, в строку s2 */ void expand (char si [], char s2 []) { char c; intij; i-j-0; while ((c - si [i++])!- '\0*) /* извлекаем символ из si */ if (si [i] —•-» &*sl[i+l] >-c) { H+; while (c < si [i]) /* расширяем краткую запись V в2Ц*+.]-с++; }else s2 Ц++] - с; /* копируем символ */ 82Ш-*\(Г; } Функция берет символ из si, сохраняет его в с, а затем проверяет следующий символ. Если им является дефис, а символ за ним больше или равен символу в с, expand переходит к расширению краткой записи. В противном случае expand копирует символ в s2. Функция expand работает для символов ASCII. Краткая запись a-z расширяется в эквивалентный список abc.xyz. Запись!— расширяется в !"#..ABC..XYZ..abc..xyz.. I}~. Это решение предоставлено Акселем Шрайнером (Ос- набрукский университет), Германия. Упражнение 3.4 (с-68 КР) В двоично-дополнительном представлении чисел наша версия itoa не обрабатывает самое большое отрицательное число, т.е. значение n - -(2/4(wordsize-D). Объясните, почему. Измените программу так, чтобы она печатала это 47
значение верно независимо от машины, на которой она работает. ♦define abe(x) ((x) < 0 ? (х): (х)) /* itoa: преобразовать число п в символы в строке s — измененная версия */ void itoa (int n, char s []) { int i, sign; void reverse (char s []); sign - n; /* записываем знак V i-0; do { /* выдаем цифры в обратном порядке */ s[i++] - abe(n % 10) + 'О*; У* получаем следующую цифру */ }while <(n/-10) !-0); /• удаляем ее*/ if (sign < 0) s[i++]-'-'; s[i]-*\0'; reverse (s); } -(2~(wordsize-l)) не может быть преобразовано в положительное число, как в п--п; потому что самое большое положительное число в двоично-дополнительном представлении: (2~(wordsize-l)) —1. Переменная sign сохраняет исходное значение п. Макроопределение abs находит абсолютную величину п%10. Это позволяет избежать проблемы -(2~(wordsize-D) потому что положительным делается только результат операции деления по модулю. Выражение условия в операторе do-while изменено с <п/-10)>0 на (п/-10)!-0 так как если бы п оставалась отрицательной везде в цикле, он был бы бесконечным. Упражнение 3.5 (с-68 КР) Напишите функцию itob( n, s, b), которая преобразует целочисленное п в символьное представление по основа- 48
нию Ь в строке s. В частности, itob( n, s, 16) выдает п в формате шестнадцатеричного целого в s. /* itob: преобразовать число п в символы в строке s — по основанию b •/ void itoa (int n, char s [], int b) { int i, j, sign; void reverse (char s []); if ((sign - n) < 0) /* записываем знак */ n - -n; /* делаем п положительным */ i-0; do { /* выдаем цифры в обратном порядке */ j - n % b; /* получаем следующую цифру */ s[i++] - (j <- 9) ? j+'O': j+V-10;, } while ((n /- b) > 0); /* удаляем ее */ if (sign <0) s[i++]-'-'; s[i]-'\0'; reverse (s); Содержимое п преобразуется по основанию b так: n°/„b возвращает значение между 0 и b-1, a n/-b удаляет это число из п. Цикл продолжается, пока п/Ь больше 0. Упражнение 3.6 (с.68 КР) Напишите версию itoa, которая принимает три аргумента вместо двух. Третий аргумент — минимальная длина поля; преобразованное число должно быть дополнено пробелами слева, если необходимо сделать его достаточно широким. #defineabs(x) ((x) < 0 ? -(х): (х)) /* itoa: преобразовать число п в символы в строке s, шириной w символов*/ void itoa (int n, char s[], int w) { int i, sign; void reverse (char s []); sign - n; /* записываем знак */ i-0; do { /* выдаем цифры в обратном порядке V s [i++] - abs(n % 10) + '0'; /* получаем следующую цифру */ }while ((n/-10) !-0);/* удаляем ее*/ if (sign < 0) s[i++]-'-*; do
while (i < w) /* заполняем пробелами V s[l++]-"; s[i]-'\0»; reverse (s); } Эта функция похожа на itoa в упражнении 3.4. Необходимое изменение: while 0 <w) | s[l++]-"; Цикл while, если нужно, дополняет строку s пробелами. I [ .
Глава 4 Функции и структура программы Упражнение 4 Л (сЛ4КР) Напишите функцию strrindex (s,t), которая возвращает позицию самого правого вхождения строки t в строку s или -1, если t не входит в s. /* strrindex: возвращает позицию самого правого вхождения строки t V /* в строку s или -1, если t не входит в s */ int strrindex (char s [], char t []) { int i, j, k, pos; pos--l; for(i-0;s[i]!-,\0,;i++){ for (j-i,k-0;t[k] •-'ХО'ЛгЛвЦ]—t[k];fH-,k++) if(k>0*&t[k] — 'NO') pos-i; } return pos; } Функция strrindex аналогична strindex (c.73 KP). Если strindex находит совпадение, она возвращает позицию первого элемента t в s. Напротив, strrindex записывает эту позицию и продолжает поиск, так как функция ищет последнее (самое правое) вхождение t в s: if (k>0&&t[k]—'\<Г) pos-i; Вот другое возможное решение: ♦include <string.h> /* strrindex: возвращает позицию самого правого вхождения строки t */ /* в строку s или -1, если t не входит в s */ int strrindex (char s [], char t []) { inti.j.k; for (i-strlen(s) — strien(t); i> — 0; i—) { for (j4, k-O; t[k] !-'\0' &&*[}] — t[k]; j++, k++) 51
if(k>0&&t[k]--»\0') return i; } return-1; } Это более эффективное решение той же задачи. Программа начинает работу в позиции "конец строки s" минус длина строки t. Если совпадения нет» strrindex делает шаг назад на одну позицию и пробует снова. Как только stnindex находит t в s, она возвращает i; i — это уже самая правая позиция, где встречается t. Упражнение 4.2 (сЛб КР) Расширьте функцию atof так, чтобы она обрабатывала научную запись вида 123.45е-6 где за числом с плавающей точкой может следовать е или Е и показатель степени (возможно, со знаком). #include <ctype.h> /* atof: преобразовать строку s в число типа double •/ double atof (char s []) { double val, power, int exp, i, sign; for (i - 0; isspace(s [i]); i++) /* пропускаем пробелы */ sign'- <s[i] --'-•)?-1:1; if(s[i]—44 I s[i]--'-') for (val-0.0; iadigit(s[i]); i++) val-10.0* val+(s[i] — ЧГ); if(s[i]--V) for (power-1.0; iedigit(s[i]); H+) { val-10.0 *val+(s[i]—ЧГ); power*-10.0; } val - sign * val / power; if(s[i]—V Ms[i]—9E'){ sign- (s[+4i] --»-•)?-1:1; if(s[i]—'+' lls[i]—'-') Hi* for (exp-0; isdigit(s[i]); i-и-) ~" exp -10 * exp + (s[i] — '0»); if (sign — 1) while (exp—> 0) /* показатель степени положительный */ val*-10; else while (exp—> 0) /* показатель степени отрицательный */ val /-10; SI
} return val; } Первая половина программы — копия atof (c.75 KP). Функция пропускает символы-разделители, учитывает знак и вычисляет значение. К этому моменту исходная atof возвращает число, измененная же версия обрабатывает научную запись. Вторая половина функции обрабатывает необязательную экспоненту. Если экспоненты нет, atof возвращает число, находящееся в переменной val. Если же она существует, ее знак сохраняется в переменной val, а значение показателя степени вычисляется и записывается в ехр. Заключительная операция if (sign—1) while (ехр — >0) val»-10; else while (ехр — > 0) val /-10; корректирует число согласно значению показателя степени. Если оно положительное, число умножается на 10 ехр раз. Если отрицательное, делится. После этого val содержит окончательное значение, которое возвращается в вызывающую программу. Переменная val делится на 10, а не умножается на 0.1, так как 0.1 — не точная часть в двоичном представлении. В большинстве машин 0.1 представляется несколько меньшим числом и поэтому 10 раз по 0.1 редко дает 1.0. Повторяющееся деление на 10 лучше, чем умножение на 0.1, но и тоща остается потеря точности. Упражнение 4.3 (с.81 КР) Коща дана основная структура, открыт путь для расширения калькулятора. Добавьте оператор деления по модулю (%) и позаботьтесь об отрицательных числах. ♦include <stdio.h> ♦include <mafh.h> Л для atof О */ ♦define MAXOP100 /• максимальный размер операнда или оператора •/ ♦define NUMBER '0' Л признак того, что найдено число •/ intgetop(char []); void push (double); double pop (void); /* калькулятор, использующий польскую инверсную запись */ .main () 5А
{ int type; double op2; chars[MAXOP]; while ((type-getop(s)) !-EOF) { switch (type) { case NUMBER: push(atof(s)); break; caseM-': push(pop()+popO); break; case**': push (pop О *pop()); break; case*-»: op2-pop(); push (pop О — op2); break; case 7': op2-pop(); if(op2!-0.0) pueh(pop() /op2); else printf ("Ошибка: нулевой делитель\п"); break; саве'Уо*: op2-pop(); if(op2!-0.0) push(fmod(pop(), op2)); else printf ("Ошибка: нулевой делитель\п"); break; case *\n': printf C\t%.8g\iT, popO); break; default: printf ("Ошибка: неизвестная команда %s\n", s); break; } } return 0; } Мы внесли изменения в функции main и getop. Функции push и pop (c.80 KP) остались без изменений. Оператор деления по модулю (%) обрабатывается как оператор деления (/). Библиотечная функция fmod вычисляет остаток от деления друг на друга двух верхних элементов стека. Ниже помещена измененная getop: ♦include <stdio.h> ♦include <string.h> ♦include <ctype.h>
#define NUMBER *0* /* признак того, что было найдено число */ intgetch(void); voidungetch(int); /* getop: получить следующий знак операции или числовой операнд */ intgetop(chars[]) { intc, i; while ((s[0] -c-gelchO) — • • 11 c — V) stlj-'W; i-0; if (!isdigit(c) && с!- V && с!-'-') return с; /* не число */ if(c--V) if (isdigit(c-getchO) 11 c — V) s [++i] - с; /* отрицательное число */ else{ if (с!-EOF) ungetch(c); return •-•; /* знак минус */ } if (isdigit(c)) /* отбираем целую часть */ while Osdigit(s[++i] -c-getch())) » if (c - - V) /* отбираем дробную часть */ while (isdigit(s[++i] -c-getch())) ■ М-'МГ; if (с!-EOF) ungetch(c); return NUMBER; } Функция getop просматривает один символ после знака минус, чтобы определить, не отрицательное ли это число. Например, — 1 означает знак минус, а за ним число, но -1.23 означает отрицательное число. Расширенный калькулятор обрабатывает: 1 -1 + -ю з % Первое выражение дает 0:1+(-1). Второе выражение дает-1. Упражнение 4.4 (с.82 КР) Добавьте команды печати верхнего элемента стека без извлечения его, для дублирования этого элемента и для обмена двух верхних элементов. Добавьте команду очистки стека. 55
У #include <stdio.h> #include<math.h>/*AnflatofO */ #define MAXOP 100 /* максимальный размер операнда или оператора #define NUMBER '0' /* признак того, что найдено число */ int getop(char []); void push (double); double pop(void); void clear (void); /* калькулятор, использующий польскую инверсную запись V main О { int type; double opl,op2; chars [MAXOP]; while ((type-getop(s)) !-EOF) { switch (type) { case NUMBER: push(atof(s)); break; caseM-': push(pop()+pop()); break; case •*•: pu8h(pop()*pop0); break; case •-•: op2-pop(); push(pop() — op2); break; case 7': op2-pop(); if(op2!-0.0) push(pop() /op2); else printf ("Ошибка: нулевой делитель\п"); break; case •?•: /* напечатать верхний элемент стека */ ор2-рор(); printf C\t%.8g\n*,op2); push(op2); break; case V: /* очистить стек */ clear О; break; case 'd': /* продублировать верхний элемент стека */ ор2-рор(); push(op2); push(op2); break; case V: /* поменять местами два верхних элемента */ ор1-рор(); ор2-рор(); push(opl); push(op2); break; 56
case *\n': printf(H\t%.8g\nH, popO); break; default: printf ("Ошибка: неизвестная команда %s\n", s); break; } } return 0; } Оператор "новая строка*1 извлекает верхний элемент стека и печатает его. Мы добавили новый оператор'?', который извлекает верхний элемент из стека, печатает его и заносит обратно в стек. Мы не извлекаем верхний элемент навсегда, как оператор "новая строка", а используем последовательность pop, print и push, чтобы не давать функции main знать о стеке или переменных позиции стека. Чтобы дублировать верхний элемент стека, извлекаем этот элемент и заносим обратно дважды. Два верхних элемента стека меняем местами, извлекая их и занося в обратном порядке. Очистить стек легко, достаточно установить sp в 0. Так что мы добавили новую функцию, которая выполняет именно это, и поместили ее с push и pop. Таким образом, только функции, поддерживающие стек, имеют отношение к стеку и переменным позиции стека. /* clear: очистить стек •/ void clear(void) { sp-0; } Упражнение 4.5 (с.82 КР) Добавьте доступ к библиотечным функциям, таким, как sin, exp и pow (см. <math.h> в приложении В, раздел 4 (С.250КР)). ♦include <stdio.h> ♦include <string.h> ♦include <math.h> /♦ для atof 0 */ ♦define MAXOP100 /* максимальный размер операнда или оператора */ ♦define NUMBER *0* /* признак того, что найдено число V ♦define NAME V /* признак того, что найдено имя */ intgetop(char []); void push (double); double pop (void); void mathfnc (char []); /* калькулятор, использующий польскую инверсную запись */ main О { 57
int type; double op2; chars[MAXOP]; while ((type-getop(s)) !-EOF) { switch (type) { case NUMBER: push(atof(s)); break; case NAME: mathfnc (s); break; савеЧ»: pusMpopO+popO); break; push(pop()*popO); break; case*-': op2-pop(); push(pop()—op2); break; case 7': op2-pop(); if (op2!-0.0) push (pop 0/op2); else printf ГОшибка: нулевой делительХгГ); break; case • W: printf C\t%.8g\nH, popO); break; default: printf ("Ошибка: неизвестная команда %s\n", s); break; } } return 0; /* mathfnc: проверить строку s на наличие поддерживаемой */ /* математической функции */ void mathfnc (char s []) { double op2; if (strcmp(s, a8in,,)—0) push (sin (pop ())); else if (strcmpfe, "сое") — О) pu8h(cos(pop0)); else if (strcmp(s, aexpw) -- 0) push(exp(pop())); else if (strcmp(s, Mpoww) — 0) { op2-pop(); push(pow(pop(),op2)); }else printf ("Ошибка: %s не поддерживается\п", s); } 58
Исходный файл для измененной getop: ♦include <stdio.h> ♦include <string.h> ♦include <ctype.h> ♦define NUMBER '0' /* признак того, что было найдено число */ ♦define NAME V /* признак того, что было найдено имя V hit getch (void); void ungetch (int); /* getop: получить следующий знак операции, числовой операнд */ /* или математическую функцию V int getop (char s[J) { intc,i; while ((s[0] -c-getch()) — '41 с — *\t'> s[lj-'\0'; i-0; if (islower(c)) { /* команда или имя */ while (islower(s[++i] -c-getehO)) s[i}'-'\0'; if<c!-EOF) ungetch (c); /* продвинулись на символ дальше, чем нужно */ if (stden(s) > 1) return NAME; /* > 1 символа, это имя V else return с; /* возможно, это команда */ } if <!isdigit(c> 2t& с!- •/) return с; /* не число */ if (isdigit(c)) /* отбираем целую часть */ while Gsdigit(s[++i] -c-getch())) » if (с - - V) Л отбираем дробную часть */ while Gsdigit(s[4-H] -c-getehO)) s[i]-'\0'; if (с!-EOF) ungetch (с); return NUMBER; } Мы изменили getop так, чтобы можно было получить строку из строчных букв и возвратить ее с типом NAME. Функция main рассматривает NAME как один из возможных типов и вызывает mathfnc. Функция mathfnc новая. Она выполняет последовательность операторов if, пока не найдет имя функции, совпадающее со строкой s, или сообщит об ошибке. Если строка s — одна из поддерживаемых функций, mathfnc извлекает нужное количество операндов из стека и вызывает математическую функцию, которая возвращает значение, а mathfnc заносит его обратно в стек. 5*
Например» sin ожидает аргумента в радианах и синус PI / 2 равен единице. 3.14159265 2/sin Первая операция делит PI на 2 и заносит результат обратно в стек. Функция sin извлекает, верхний элемент из стека, вычисляет синус и также заносит результат обратно. Результат равен единице. 3.14159265 2/sin 0 cos + дает 2, так как синус PI / 2 равен 1 и косинус 0 равен единице. Другой пример, 52pow42pow + возводит 5 в степень 2, 4 — в степень 2 и складывает два значения. Функция getop ничего не знает о специфических именах функций, она только возвращает строки в том виде, в каком получает их. Таким образом, открыт путь для расширения mathfnc добавлением новых функций. Упражнение 4.6 (с.82 КР) Добавьте команды для работы с переменными. (Нетрудно организовать двадцать шесть переменных с именами, состоящими из одной буквы.) Добавьте переменную для последнего напечатанного значения. #include <stdio.h> ♦include <math.h> /* для atof О */ #define МАХОР 100 /* максимальный размер операнда или оператора •/ #define NUMBER '0' /* признак того, что найдено число */ int getop (char []); void push (double); double pop (void); /* калькулятор, использующий польскую инверсную запись V main () { int i, type, var-0; double op2, v; chars[MAXOP]; double variable [26]; for(i-0;i<26;H+) variable [i] -0.0; while ((type-getop(s)) !-EOF) { switch (type) { case NUMBER: push (atof (s)); break; case'V: 60
push(pop()+pop()); break; case***: push(pop() *pop()); break; case •-•: op2-pop(); push (pop () —op2); break; case'/': op2-pop(); lf(op2!-0.0) push (pop ()/op2); else prlntf ("Ошибка: нулевой делитель\1Г); break; case»-': popO; if (var > - 'A' && var < - fZf\ variable [var-'A*] - popO; else printf ("Ошибка: отсутствует имя переменной\п"); break; case An': v-popO; prlntf (H\t%,8g\nw,v); break; default if (type > - 'A* && type < - 'Z'). pu8h(variable[type-,A>]); else if (type—V) push(v); else prlntf ("Ошибка: неизвестная команда %s\iT, s); break; } var-type; } return 0; } Переменные, которые были добавлены, — заглавные буквы от А до Z. Буквы служат индексами в массиве переменных. Мы ввели переменную, обозначаемую строчной буквой v, содержащую последнее напечатанное значение. Как только программа находит имя переменной (A-Z или v), она заносит ее значение в стек. Имеется также новый оператор *-\ который присваивает элемент из стека предшествующему имени переменной. Например, присваивает значение 3 переменной А. Затем 61
добавляет 2 к 3 (значению» присвоенному переменной А). По оператору "новая строка" программа печатает 5, а также присваивает 5 переменной v. Если следующая операция vl + она дает 6:5 + 1. Упражнение 4.7 (с.82 КР) Напишите функцию ungets (s), которая возвратит целую строку на ввод. Должна ли ungets знать о переменных buf и bufp или использовать только функцию ungetch? ♦include <8tring.h> /* ungets: возвратить строку на ввод */ void ungets (char s [)) { intlen-strien(s); void ungetch (int); • while(len>0) ungetch (s[—ten]); } Переменная len содержит количество символов в строке s (кроме завершающего '\0'), которое определяется функцией strlen (с.45 КР). Функция ungets вызывает подпрограмму ungetch (с.81 КР) len раз» каждый раз возвращая символ из строки s на ввод, и возвращает строку в обратном порядке. Функции ungets не требуется знать о buf или bufp. Функция ungetch обрабатывает buf, bufp и осуществляет проверку ошибок. Упражнение 4.8 (с.82 КР) Допустим, что никоща не будет возвращаться более одного символа. Измените в соответствии с этим getch и ungetch. ♦include <stdio.h> char buf-0; /* getch: получить (возможно, возвращенный) символ */ int getch (void) { int с; if(buf!-0) с-buf; else c-getchar(); buf-0; return c; } 1 &2
/* ungetch: возвращает символ на ввод V voidungetch(intc) { if(buf!-0) printf ("ungetch: слишком много символов\пи); else buf-с; } Буфер buf больше не является массивом, так как в любой момент времени в буфере не может быть более одного символа. Переменная buf инициализируется нулем в момент загрузки, и getch сбрасывает buf в 0 каждый раз, коща получает символ. Если буфер не пуст, ungetch выдает сообщение об ошибке. Упражнение 4.9 (с.82 КР) Функции getch и ungetch не обрабатывают возвращаемый EOF правильно. Придумайте, какими свойствами должны они обладать, если возвращен EOF, и реализуйте ваше решение. ♦include <stdio.h> #defineBUFSIZE100 int buf [BUFSIZE]; /• буфер для ungetch */ int bufp - 0; /* очередная свободная позиция в buf V /* getch: получить (возможно, возвращенный) символ */ int getch (void) { return (bufp >0) ? buf [—bufp] : getcharO; /* ungetch: возвращает символ на ввод */ void ungetch (int с) { if (bufp >-BUFSIZE) printf ("ungetch: слишком много символов\п"); else buf[bufp++]-c; } В функциях getch и ungetch (c.81 КР) буфер buf объявлен как массив символов: char buf [BUFSIZE]; Язык программирования Си не требует от символьной переменной быть со знаком (signed) или без него (unsigned) (см. с.43 КР). Если символ преобразуется в целое, он чаще всего не должен давать отрицательное число. На некоторых машинах, если самый левый бит символа равен 1, он даст отрицательное число при преобразовании
в int. На других машинах тип char преобразуется в int путем добавления нулей слева. Такое преобразование всегда даст положительное число независимо от того, был ли левый бит единицей. В шестнадцатеричной нотации -1 — это OxFFFF (для 16 бит). Если OxFFFF записывается в переменную типа char, на самом деле записывается число OxFF. Когда OxFF преобразуется в int, оно может дать OxOOFF, т.е. 255, или OxFFFF, т.е. -1 отрицательное число (-1) -> символ -> целое OxFFFF OxFF OxOOFF (255) OxFFFF OxFF OxFFFF(-l) Если мы собираемся обрабатывать EOF (-1) как любой другой символ, buf должен быть объявлен как массив целых: intbuf[BUFSIZE]; Таким образом, не будет происходить никаких преобразований и EOF (-1) или любое другое отрицательное число будет обработано переносимым образом. Упражнение 4.10 (с.82 КР) Для считывания строки со ввода альтернативная организация использует getline, вследствие чего getch и ungetch не нужны. Переделайте программу-калькулятор, чтобы использовать этот подход. #include<stdio.h> ♦include <ctype.h> #defineMAXUNE100 ♦define NUMBER '0' /* признак того, что было найдено число */ int getline(char line [], int limit); int li - 0; /* индекс входной строки */ char tine [MAXUNE]; /* входная строка •/ /* getop: получить следующий знак операции или числовой операнд */ int getop (char s[]) { intc, i; if(line[li]--»\0') if (getline (line, MAXUNE) —0) return EOF; else li-0; while <<s[0] -c-line[li++]) —'» 11 c-- Af) s[l]'-'\0'; i-0; if(!tedigit(c)**c!-V) return с; /* не число */ 64
i-0; if (isdigit(c)) /* отбираем целую часть */ while (isdigit(s[4-H] -c-line[lH-+])) t if (c - -V) /* отбираем дробную часть */ while <isdigit(s[-H-i] -c-tine[li++])) s[i]-'W; U—; return NUMBER; } Вместо getch и ungetch в функции getop мы используем getline. line — это массив, содержащий введенную за один раз строку целиком; li — индекс для следующего символа в line. Мы сделали line и li внешними переменными, чтобы они сохраняли свои значения между вызовами, в отличие от локальных переменных. Если getop находится в конце строки (или строки еще нет) if (line [И]—»\0») она вызывает getline, чтобы получить строку. В исходной getop (с.80 КР) функция вызывает getch каждый раз, коща ей нужен очередной символ. В этой версии мы получаем символ в позиции li в line, а затем увели-, чиваем Н. В конце функции вместо того, чтобы вызвать ungetch для возврата символа, мы уменьшаем li, так как продвинулись дальше на один символ. Помните, что любая функция может использовать и изменять внешние переменные, так что li и line могут быть изменены другой функцией, чем getop. Иногда нужно предотвратить такой случай, тоща следует объявить переменные как Static. Мы не сделали этого, поскольку статические переменные не обсуждаются до с.84 КР. Упражнение 4.11 (с-86 КР) Измените getop так, чтобы не нужно было использовать ungetch. Подсказка: используйте статическую переменную. ♦include <stdio.h> ♦include <ctype.h> \ ♦define NUMBER 'О' Л признак того, что было найдено число */ int getch (void); /* getop: получить следующий знак операции или числовой операнд */ int getop (char s[]) { int с, i; static int lastc-0; 1 3—752 65
if (teste — 0) c-geteh(); else{ с - lastc; lastc-0; while ((s[0] -c) —•» 11 c — V) c-getehO; s[l]-'\0'; if (!isdigit(c)&&c!-V) return с; /* не число */ i-0; if (isdigit(c)) /• отбираем целую часть */ while <isdigit(s[++i] -c-geteh())) if (c - - V) /* отбираем дробную часть */ while (isdigit(s[++i] -c-getch())) •ra-'W; if (c!-EOF) teste-c; return NUMBER; } Мы изменили getop так, чтобы там была статическая переменная, запоминающая последний символ, который следует вернуть на ввод. Так как функцию ungetch не используем, то сохраняем этот символ в lastc. Коща вызывается функция getop, она проверяет, есть ли в lastc предыдущий символ. Если нет, функция вызывает getch, чтобы получить новый. Если есть, getop копирует его в с и обнуляет lastc. Первый оператор while немного изменился. Причина в том, что функции нужен следующий символ только после того, как она исследует текущий символ в с. Упражнение 4.12 (с.90 КР) Примените идеи printd для написания рекурсивной версии itoa, т.е. преобразуйте целое число в строку, вызывая рекурсивную процедуру. #include <math.h> /* itoa: преобразовать п в символы в строке s; рекурсивная * / void itoaOnt n, char s []) { static int i; if (n / 10) itoa(n/10,s); else{ i-0; if(n<0) s[i++]-'-'; } 66
stf++]-abe(n) %10 + '0'; s[i]-'\0'; } Функция itoa получает два аргумента: целое п и массив символов s. Если результат целочисленного деления п/10 не нуль, itoa вызывает с аргументом п/10: if (п/10) itoa(n/10,s); Коща п/10 равно нулю в одном из рекурсивных вызовов, смотрим на самую значащую (самую левую) цифру в п. Мы используем статическую переменную i как индекс массива s. Если п отрицательно, ставим знак минус в первую позицию массива s и увеличиваем i. По мере возвращения из рекурсивных вызовов itoa вычисляет цифры слева направо. Обратите внимание, что каждый уровень добавляет *\0\ чтобы завершить строку, а следующий записывает поверх '\0\ кроме последнего. Упражнение 4.13 (с.90 КР) Напишите рекурсивную версию функции reverse(s), которая переворачивает строку s на месте. #include <string.h> /* reverse: перевернуть строку s на месте */ void reverse (char s []) { void reverser (char s [], int i, int len); reverser (s, 0, strlen(s)); } /* reverser: перевернуть строку s на месте; рекурсивная */ void reverser (char s [], int i, int len) { intcj; j-len —(i+1); if(i< j){ c-s[i]; s[i]-s[j]; sUl-c; reverser (s, +-H, len); } } Необходимо поддерживать один и тот же пользовательский интерфейс с функцией reverse вне зависимости от конкретной реализации. Поэтому мы передаем reverse только символьную строку. Функция reverse определяет длину строки, а затем вызывает reverser, которая переворачивает строку s, оставляя ее на месте. 67
функция reverser получает три аргумента: s — строку, которую нужно развернуть, i — индекс левой стороны строки и len —длину строки (strien(s),c.45KP). Сначала i равняется 0. Переменная j — индекс правой стороны строки — вычисляется как j-ten — 0+1); Символы в строке меняются местами в порядке снаружи вовнутрь. Например, первыми обмениваются s[0] и s[len-l ]. Вторая переставляемая пара символов — s [1 ] и s [1еп-2 ]. Индекс левой стороны i увеличивается на 1 каждый раз, когда вызывается reverser: rcvcracr(sv +"Hf ten)* Перестановки продолжаются до тех пор, пока либо два индекса не укажут на один и тот же символ (i - - j), либо индекс левой стороны не укажет на символ правее индекса левой (i> j). Это не лучшее применение рекурсии. Некоторые задачи сами напрашиваются на рекурсивное решение — см., например, функцию treeprint на с. 138 КР. Другие лучше всего решаются без рекурсии. Это была одна из таких. Упражнение 4.14 (с-92 КР) Сделайте макроопределение swap(t,x,y), которое обменивает значения двух аргументов типа t. (Будет полезна блочная структура.) #define swap(t, х, у) {t _z; \ _z-y;\ у-х;\ Для определения нового блока используются фигурные скобки. В начале блока можно объявлять локальные переменные. _ z — локальная переменная типа t, помогающая обменять два аргумента. Данное макроопределение работает, если ни один из аргументов не является переменной _ z. Если один из аргументов имеет имя _z 8wap(int,x,_z); то, когда макроопределение расширяется, оно превращается в {int je; jl - jl\ je - x; x - je; } и аргументы не обмениваются. Предполагается, что jl не будет использоваться как имя переменной. 68
Глава 5 Указатели и массивы Упражнение 5.1 (с.98 КР) • Написанная функция getint рассматривает знак + или -, за которым не следует цифра» как допустимое представление нуля. Исправьте функцию так, чтобы она возвращала такой символ на ввод. #inchide<stdio.h> ♦include <ctype.h> int getch (void); void ungetch (int); /* getint: получить следующее целое число с ввода: в *рп */ int getint (int *рп) { int c, d, sign;. while (iaspacete - getchO)) Л пропускаем пробелы */ if (!iedigit(c) && с !-EOF && с!- Ч> && с !-*-') { ungetch(c); Л это не число •/ return 0; } sign-(с — •-»>?-1:1; if (с—Ч> Мс—'-•){ d - с; /* запоминаем символ знака */ if (!isdigit(c-getch())){ if (с!-EOF) ungetch(c); /* это не цифра — возвращаем на ввод */ ungetch(d); /* возвращаем символ знака на ввод */ return d; /* возвращаем символ знака */ } } for (*pn-0; isdigit(c); c-getchO) *pn-10**pn+(c —Ч>*>; *pn *- sign; if (с!-EOF) ungetch(c); return c; } При появлении символа знака getint сохраняет его в переменной d и принимает следующий символ. Если он не цифра и не EOF, getint возвращает его на ввод. Затем фун- 69
кция возвращает символ знака на ввод, а также в вызывающую программу, чтобы обозначить ситуацию. Упражнение 5.2 (с.98 КР) Напишите функцию getfloat, аналог getint для чисел с плавающей точкой. Какого типа значение, возвращаемое функцией getfloat? #include <stdio.h> ♦include <ctype.h> int getch (void); void ungetch (int); /* getfloat: получить с ввода следующее число с плавающей точкой V int getfloat(float *pn) { int c, sign; float power, while (isspace(c - getchO)) /• пропускаем пробелы */ if (lisdigit(c) && с!- EOF && с!- •+• && c!-'-*&&c!-V){ ungetch (c); /* это не число V return 0; N } sign-(с — ,-,)?-l:l; if (c — •+• lie — »-•> с ™ Ketch О * for (*pn-0.0;fisdigit(c); c-getchO) *pn -10.0 * *pn + (c — *0*); /* целая часть */ if(c--V) c-getch(); for (power-1.0; isdigit(c); c-getch()) { *pn -10.0 * *pn + (c —• '0»); /• дробная часть */ power *-10.0; } *pn *- sign / power; /* окончательное число */ if (с!-EOF) ungetch (с); return с; } Функция getfloat аналогична getint (с. 97 Kb*). Она пропускает пробелы, записывает -знак и сохраняет целую часть числа по адресу в рп. Функция getfloat обрабатывает также дробную часть числа (но не научную запись). Дробная часть добавляется к *рп тем же самым способом, что и целая: ' *рп-10.0**рп+(с — »<Г); Для каждой цифры, найденной после десятичной точки, переменная power умножается на 10. Например, если 70
за десятичной точкой следует 0 цифр, power равняется 1, если одна — 10, если три — 1000. Затем функция getfloat умножает окончательное значение *рп на sigh/power, чтобы получить значение с плавающей точкой. Она возвращает либо EOF, либо код ASCII первого символа после числа с плавающей точкой. Тип функции — int. Упражнение 5.3 (с.106 КР) Напишите работающую с указателями версию strcat (функция показана в гл. 2: strcat(s,t) копирует строку t в конец s). /* strcat: скопировать строку t в конец строки s; V /* версия, работающая с указателями */ void strcat (char *s, char *t) { while (*s) while (*s++-*H+) » } Вначале s и t указывают на начало символьных строк. Первый цикл while увеличивает указатель s, пока не найдет метку конца строки С\0'). Оператор while (*s) истинен, пока текущий символ не является символом конца строки. Второй цикл while добавляет строку t к строке s: while (**♦+- *Н+) » Этот оператор присваивает *s любой символ, находящийся в *t, увеличивает оба указателя и продолжает так до тех пор, пока t не укажет на символ конца строки. Упражнение 5.4 (с-107 КР) Напишите функцию strend(s,t), которая возвращает 1, если строка t содержится в конце строки s, и 0 — в противном случае. /* strend: возвратить 1, если строка t содержится в конце строки s •/ /* и 0 в противном случае */ int strend (char *s, char *t) { char *be - s; /* записываем начало строк */ 71
char*bt-t; for (; *s; s++) /* конец строки s */ for (; *t; H+) /* конец строки t */ for(;*s — *t;s—,t—) if(t—btlls—be) break; Л начало какой-либо строки V if C*s—*t&&t—Ы&&*в!-'\<Г) return 1; else return 0; } Мы сохраняем адреса начала строк в указателях bs и bt, а затем находим конец каждой строки так же, как делали это в функции strcat. Чтобы определить, содержится ли строка t в конце строки s, мы начинаем сравнивать последний символ s с последним символом t и движемся к началу строк. Функция strend возвращает 1 (строка t содержится в конце строки s), когда символы t совпадают с символами s, указатель t снова в начале строки и строки не пустые: if(*s — *t&*t—bt&&*s!-,\0,> return 1; Упражнение 5.5 (c.107 КР) Напишите версии библиотечных функций strncpy, strncat, strncmp, которые работают с не более чем п символами строк-аргументов. Например, strncpy(s,t,n) копирует не более чем п символов из t в s. Полные описания функций находятся в приложении В (с.248 КР). Функции strncpy и strncat имеют тип void, как strcpy на с. 105 КР. Библиотечные версии этих функций возвращают указатель на начало полученной строки. /* strncpy: скопировать п символов из строки t в строку s */ void strncpy (char *s, char *t, int n) { while (*t*&n— 0) *s+f-*H+; while (n—0) •s+f-'VT; /* strncat: скопировать п символов из строки t в конец строки s */ void strncat(char *s, char *t, int n) { void strncpy (char *s, char *t, int n); int strlen (char*); strncpy (s+strlen (s), t, n); }
/* strncmp: сравнить не более чем п символов из строк t и s •/ int strncmp (char *s, char *t, int n) { for(;*s — *t;e++,H+) if (*s — '\<V II — n<-0 return 0; return *s — *t; } Функция strncpy копирует не более чем п символов из t в s. Если в строке t меньше чем п символов, замыкаем строку s нулем (*\0'). Функция strncat вызывает strncpy, чтобы скопировать не более п символов из строки t, начиная с конца строки s. Функция strncmp сравнивает не более п символов строки t с символами s. Она похожа на strcmp на с. 106 КР. Теперь мы заканчиваем сравнение, когда находим концы строк или успешно сравним п символов: if(*s — *\0* II—п<-0) return 0; Упражнение 5.6 (сЛ07 КР) Перепишите подходящие программы из предыдущих глав и упражнений с использованием указателей вместо индексирования массивов. Неплохие возможности содержатся в getline (гл. 1 и 4), atoi, itoa и их вариантах (гл. 2,3 и 4), reverse (гл. 3), strindex и getop (гл. 4). «include <stdio.h> /* getline: считывать строку в s, вернуть длину */ int getline (char *s, int lim) { int c; char *t ™ 8* while (—lim> 0&& (c-getcharO) !-EOF&*c!-'V) *s++-c; if(c--*V) *s++-c; •s-'MT; return s — t; } Функция getline получает указатель на массив символов. Мы используем *s вместо s[i], чтобы обратиться к элементу массива, и увеличиваем указатель s, чтобы двигаться по массиву вперед. Оператор s[i++]-c; эквивалентен *8++-с; 73
Вначале функция s указывает на начало массива, и мы сохраняем этот адрес в указателе t: char*t-s; После того, как getline считает строку, s указывает на замыкающий символ С\0'). t по-прежнему указывает на первый символ в строке, так что длина строки равна s-t. #include <ctype.h> /* atoi: преобразовать строку s в целое число; */ /* версия, работающая с указателями */ intatoi (char's) { int n, sign; for (; iaspace(*s); s++) /* пропускаем пробелы */ sigii-<*s—f-f)?-l:l; if (*s — »+4l*8 — '-') /* пропускаем знак */ с I | - for(n-0;isdigit(*s);s++) n-10*n + *s —ЧГ; return sign * n; Запись s [i ] эквивалентна *s, a s [i++ ] эквивалентна *s++. void reverse (char *); /• itoa: преобразовать п в символы в строке s */ /* версия, работающая с указателями */ void itoa (int n, char *s) { int sign; char *t - s; /* сохраняем указатель на строку s */ if ((sign - n) < 0) /* записываем знак */ n - -n; /* делаем п положительным */ do { /* выдаем цифры в обратном порядке */ *s++ - п % 10 + '0'; /* получаем следующую цифру */ } while ((n /-10)!- 0); /* удаляем ее */ If (sign < 0) *»♦+- '-•; •■-АО'; reverse (t); Указатель на символьную переменную t инициализируется так, чтобы он указывал на первый элемент строки s: char*t-s; Оператор •■++- п % 10+ ЧГ; эквивалентен ■П+Ч-пЗИО + ЧГ;
Мы вызываем функцию reverse с указателем на начало строки s. ♦include <string.h> /* reverse: перевернуть строку s на месте */ void reverseCchar *s) { intc; char*t; for(t-s+(strlen(s)-l);s <t;s+M—){ с - *s; *s-*t; •1-е; } } Указатель на символьную переменную s установлен на первый символ строки, а указатель t мы устанавливаем на последний символ (не считая завершающий '\0'): t-s+(strlen(s)-l) Запись *s эквивалентна s [i ], a *t эквивалентна t [i ]. Условие в цикле for s<t эквивалентно условию Выражение s++ дает тот же эффект, что и увеличение индекса i (i++), a t — тот же эффект, что и уменьшение индекса j (j—). /* strindex: возвратить индекс строки t в строке 8, */ /*-1, если t не входит в s */ int strindex (char *s, char *t) char *b - s; /* начало строки 8 */ char *p, *r; for(;*s!-,\0,;s++){ for (p-s, r-t; *r!- '\<Г && *p - - *r; p++, r++) if(r>t*&*r—'\0f> return s — b; } return -1; } Запись s[i] заменена на *s, s[j] — на *p, a t[k] — на *v. Переменная b — указатель на символ, всегда установленный на первый элемент строки s (s[0]). Выражение p-s эквивалентно i - j, a r -1 эквивалентно к - 0. Если истинен оператор if if (r>t*&*r—9\0>)
то, совпадение есть и strindex возвращает индекс t в строке s: return s — b; ♦include <ctype.h> Л atof: преобразовать строку s в число типа double V /* версия, работающая с указателями */ double atof (char's) { double val, power, intsign; fori; isspaceCs); 8+*) /• пропускаем пробелы •/• 8ign-(*s — >-')?-l:l; if(*s—4' 11*8 —'-») И I I - for (val - 0.0; tedigit(*s); s++) val-10.0 *val+(*s — '0Г); if(*s*—V> for (power-1.0; isdigitCs); 8++> { val-10.0 * val + (*s — '0'); power*-10.0; return sign * val / power; Выражение s [i++) эквивалентно *s++. ♦include <stdk).h> ♦include <ctype.h> , ♦define NUMBER '0' /* признак того, что было найдено число */ intgetch(void); void ungeteh(int); /* getop; получить следуюои4йзшк операции или числовой операнд*/ /• версия, работающая с указателями •/ intgetop(char*s) { intc; while (<*8-c-£etch()>"-V He — '\f> » *(s+l) —'\0'* if (!iedifit(c) && с !- V && с!-*-') return с; /* не число */ • if (isdigit(c)) /• отбираем целую часть •/ while (isdigit<*++*-c-getch())) » if (с — V) /• отбираем дробную часть */ while (iadigit(H4«-c-£etchO)) •s-'MT; if (с!-EOF) ungetch(c); return NUMBER;
Мы используем указатели для замены обращений к элементам массива. Изменения очевидны. Вместо ■Ш-»\а»; используется •(s+D-'V)'; чтобы поместить метку конца строки на вторую позицию в массиве без изменения указателя. Упражнение 5.7 (с.110 КР) Перепишите функцию readlines так, чтобы записывать строки в массив, отведенный в main, а не вызывать alloc для получения памяти. Насколько быстрее становится программа? #include <string.h> „ #deflne MAXLEN1000 /* максимальная длина строки */ #define MAXSTOR 5000 /* размер имеющейся памяти •/ intgetline(char*,int); /* readlines: считывать вводимые строки */ int reaglinesCchar *Uneptr [], char *linestor, int maxlines) { int len, nlines; char line [MAXLEN]; char *p-linestor; char *linestop - linestor+MAXSTOR; nlines-0; while (den-getUneOine, MAXLEN)) > 0) if (nlines>-maxlines 1I p+len >linestop) return -1; etee{ line[len-l]-9\0'; /• уничтожаем символ новой строки */ strcpy(p,line); lineptr[nllnes++]-p; р-н-len; } return nlines; Функция main отводит массив linestor, куда readlines записывает текст построчно. Указатель на символ р устанавливается на первый элемент linestor: char *p-linestor, Исходная функция readlines (с.108 КР) использует функцию alloc (с.102 КР): if (nlines >- maxlines II (p-aUocden))— NULL) В этой версии readlines записывает line в linestor, начиная с позиции р. Оператор if (nlines>-maxlines 11 p + ten > linestor) 77
выясняет, есть ли в массиве linestor свободное место. Эта версия readlines работает несколько быстрее исходной. Упражнение 5.8 (с.112 КР) В функциях day_of_year и month_day отсутствует проверка ошибок. Исправьте этот недостаток. static char day tab [2] [13] -{ { 0, 31,28, 31,30,31, 30, 31,31,30,31,30,31}, {О, 31,29, 31, 30,31,30, 31,31, 30, 31, 30,31}, }; /* day_of_year: установить порядковый номер дня в году */ /* по месяцу и числу •/ int day_of_year(int year, int month, int day) { int i, leap; leap-year%4 — 0 && year % 100 !-0 11 year%400 — 0; if (month < 1 II month > 12) return -1; if (day < 1 11 day > day tab [leap] [month]) return-1; for (i-l;i< month; Ж-) day 4- day tab [leap] [i]; return day; } /* month_day: установить месяц и число по порядковому номеру дня в году*/ void month day (int year, int yearday, int *pmonth, int *pday) { int i, leap; if(year<l){ *pmonth--l; *pday--l; return; } leap-year%4 — 0&&year%100!-0 11 year%400--0; for (i -1; i < -12 && yearday > daytab [leap] [i]; H+), yearday -— daytab [leap] [i]; if (i > 12 && yearday > daytab [leap] [i]) { *pmonth--l; *pday--l; }etee{ ♦pmonth - i; ♦pday - yearday; } } В функции day_of_year мы проверяем, разумные ли значения находятся в month и day. Если month меньше единицы или больше двенадцати, day_of_year возвращает -1. В функции mohth_day мы сначала проверяем, не отрицательна ли переменная year. Далее начинаем уменьшать 78
year_day, проверяя, не превысил ли месяц (индекс i) 12. Если цикл завершается с месяцем, равным 13, а значение yearday превышает число дней в последнем месяце года, это значит, что переменная yearday начала с неправильного значения и мы присваиваем месяцу и дню значение -1. В противном случае функция month_day получила правильные значения. Упражнение 5.9 (сЛ13 КР) Перепишите функции day_of_year и month day с использованием указателей вместо индексации. static char day tab [2] [13] - { {О, 31,28,31,30,31,30,31,31,30,31,30,31}, {О, 31, 29,31, 30,31,30,31,31,30, 31,30, 31}, /* day_of_year: установить порядковый номер дня в году */ /* по месяцу и числу */ int day_of_year (int year, int month, int day) { int leap; char *p; leap - year%4 — 0&&year% 100!- 0 11 year % 400—0; p- day tab [leap]; while (—month) day-fr- *++p; return day; /* month_day: установить месяц и число по порядковому номеру дня в году*/ void month_day (int year, int yearday, int *pmonth, int *pday) { int leap; char *p; leap-year%4 — 0*&year%100!-0 11 year%400—0; p-day tab [leap]; while (yearday > *++p) yearday ■— *p; ♦pmonth - p — * (daytab + leap); ♦pday-yearday; } Указатель p установлен на первую или вторую строку массива daytab в зависимости от значения leap, р-daytab [leap]; Цикл for в исходной функции day_of_year for (i-l;i< month; i++) day-fr-daytab [leap] [i]; эквивалентен циклу while while (—month) day-fr-*++p;
в переделанной day of year. Цикл for в исходной функции month day for (i-1; yearday > daytab[teap] [i]; i++) yearday -— daytabpeap] [i]; эквивалентен операторам p-daytab[leap]; while (yearday > *++p) yearday ■— *p; в переделанной month_day. Упражнение 5.10 (c.l 17 KP) Напишите программу ехрг, которая вычисляет польское инверсное выражение из командной строки, ще каж-. дый операнд или знак операции является отдельным аргументом. Например, ехрг 2 3 4+* вычисляет 2 х (3 + 4). ♦include <stdio.h> ♦include <math.h> /* для atof О */ ♦define МАХОР 100 /* максимальный размер операнда или оператора •/ ♦define NUMBER '0' /* признак того, что найдено число */ intgetop(char []); void ungets(char []); void push (double); double pop (void); /* калькулятор, использующий польскую инверсную запись; V /• использует командную строку V main(int argc, char *argv[]) { chars[MAXOP]; double op2; while (—argc>0){ ungetsC "); /* "возвращаем** на ввод признак конца аргумента •/ ungets(*++argv);/* "возвращаем" сам аргумент */ switch (getop(s)) { case NUMBER: push (atof (s)); break; case •+•: push (pop 0 + pop 0); break; case,*,: push(pop()*pop()); break; case'-': op2-pop(); push (pop() — op2); break; ч
caseV: op2-pop(); If (op2!-0.0) pushCpopO/op2); else printf ("ошибка: нулевой делительХп"); break; default printf ("ошибка: неизвестная команда %s\n", s); argc-1; break; } printf ("\t%.8g\n", popO); return 0; } Это решение основывается на калькуляторе со с.79 КР, использующем польскую инверсную запись. Здесь применяются функции push и pop (с.80 КР). Мы используем функцию ungets для возвращения метки конца аргумента и самого аргумента в буфер ввода. Таким образом, можем оставить функцию getop без изменений. Функция getop вызывает getch для считывания символов и получает новый знак операции или числовой операнд. Если в процессе считывания аргументов должна произойти ошибка, переменная argc устанавливается в 1. Условие цикла while в функции main while (—argc>0) становится ложным, и программа завершается. Для правильного выражения результат оказывается на вершине стека, и мы печатаем этот результат, когда заканчивается список аргументов. Упражнение 5.11 (сЛ17КР) Измените программы entab и detab (написанные в качестве упражнений к гл. 1) так, чтобы они принимали список позиций табуляции как аргументы. Если аргументов нет, используйте список позиций табуляции по умолчанию. ♦include < stdio.h> ♦define MAXUNE 100 /* максимальный размер строки •/ ♦define TABINC 8 /* шаг увеличения позиции табуляции по умолчанию*/ ♦define YES 1 ♦define NO 0 void settabOnt argc, char *argv[], char *tab); \ 81
void entaMchar *tab); int tabposGnt pos, char *tab); /* заменить строки пробелов табуляциями */ main (int argc, char *argv[]) { char tab[MAXUNE+l]; settab(argc, argv, tab); /* расставить позиции табуляции */ entab(tab); /* заменить пробелы табуляциями */ return 0; ( } /* entab: заменить строки пробелов табуляциями и пробелами V void entab (char *tab) { int с, pos; int nb - 0; /* # необходимое количество пробелов */ int nt - 0; /* # необходимое количество табуляций V for (pos-1; (c-getchar()) !-EOF; pos++) if (c — "){ if (tabpos(pos, tab) — NO) ■Hub; /* увеличиваем число пробелов */ else{ nb - 0; /* сбрасываем число пробелов */ •Hut; /* еще одна табуляция */ } }else{ for(;nt> 0;nt—) putchar('\t'); /* выводим табуляции */ if (с -- 'NO /• забываем о пробелах */ nb-0; else /* выводим пробелы */ for(;nb>0;--nb) putcharC'); putchar(c); if (с — *W) poe-0; else if (с—f\f) while (tabpos(pos, tab)!-YES) ■Hpos; } } Исходный файл settabx: #include<stdlib.h> #define MAXUNE 100 /* максимальный размер строки */ #define TABINC 8 /* шаг увеличения позиции табуляции по умолчанию*/ #defineYESl #defineNO0 void settabOnt argc, char *argv[], char *tab) { int i, pos; if (argc <-1) /* позиции табуляции по умолчанию */, for (i -1; i <- MAXUNE; H+), if (i% TABINC — 0) tab[i] -YES; 82
else tab[i] -NO; else { /* позиции указаны пользователем */ for (i -1; i <- MAXLINE; 1++), tab[i] - NO; /* убираем все позиции */ while (— argc > 0) { /* движемся по списку аргументов */ pos - atoi (*++argy); if (pos>0&&pos MAXLINE), tab [pos] -YES; } } } Исходный файл tabpos.c: #define MAXLINE 100 /* максимальный размер строки V #defineYESl /* tabpos: определить, является ли pos позицией табуляции */ int tabpos (int pos, char *tab) { if (pos > MAXLINE) return YES; else return tab [pos]; В основу этого решения положена программа entab в книге Кернигана и Плоджера, Software Tools (Addison- Wesley,1976). Каждый элемент в массиве tab соответствует позиции в строке, т.е. tab[l ] соответствует первой позиции (pos равняется 1). Если данная позиция — позиция табуляции , соответствующий элемент tab [i ] равен YES, иначе NO. Позиции табуляции устанавливаются в функции settab. Если аргументов нет (переменная argc равна 1), ставим их на каждой TABINC позиции. Если аргументы присутствуют, ставим позиции табуляции так, как указал пользователь. Программа entab аналогична упражнению 1.21. Функция tabpos определяет, является ли данная позиция позицией табуляции. Функция возвращает YES, если pos превысит MAXLINE, иначе — tab [pos ]. #include<stdio.h> #define MAXLINE 100 /* максимальный размер строки */ #define TABINC 8 /* шаг увеличения позиции табуляции по умолчанию*/ #defineYESl #defineNO0 void settabttnt argc, char *argv[], char *tab); void detab (char *tab); int tabposGnt pos, char *tab); /* заменить табуляции пробелами */
main (int argc, char *argy []) { chartab[MAXUNE+l]; settab(arfc argv, tab); /* расставить позиции табуляции */ detab (lab); /• заменить табуляции пробелами •/ return 0; /* detab: заменить табуляции пробелами */ voiddetab(char*tab) { intc, pos-1; whUe ((c-getcharO) !-EOF) { if (c -- *\f) { /* символ табуляции */ do puteharC •); while (tabpoe(poa++, tab)!-YES); } else if (c - - *W) { /* символ новой строки */ putchar(c); pos-1; } else { /* все другие символы */ putchar(c); ++pos; } } } За основу этого решения взята программа detab в книге Кернигана и Плоджера, Software Tools (Addison- Wesley,1976). Функции tabpos и settab те же, что и в первой части этого упражнения. Программа detab аналогична упражнению 1.20. Упражнение 5.12 (с.117 КР) Расширьте entab и detab так, чтобы они принимали краткую запись entab-m+n означающую расстановку позиции табуляции через каждые п колонок, начиная с колонки т. Выберите удобное (для пользователя) поведение по умолчанию. #include<stdk>.h> #deflne MAXUNE100 /* максимальный размер строки */ #define ТАВШС 8 /* шаг увеличения позиции табуляции по умолчанию •/ #defineYESl #defineNO0 void esettab(int argc, char *argv[], char *tab); void entab (char *tab); /* заменить строки пробелов табуляциями V mainGnt argc, char *argv[]) {
chartab[MAXUNE+l]; esettaMargc, argv, lab); /* расставить позиции табуляции */ entab(tab); /• заменить пробелы табуляциями */ return О; } Исходный файл esettab.c: #include<stdlib.h> #define MAXUNE100 Л максимальный размер строки */ #define TABINC 8 /* шаг увеличения позиции табуляции по умолчанию */ #defineYESl ♦define NO О /* esettab: расставить позиции табуляции в массиве tab */ void esettabGnt argc, char *argv[], char *tab) { inti, inc, рое; if (argc <-1) /* позиции табуляции по умолчанию */, for (i-1; i<-MAXUNE; 1++), if (i% TABINC ~0) tabfi] -YES; else tab[i]-NO; else if (argc - - 3 && /* пользователем указаны размеры */ *argv[ll ~ •-» && *argv[2] — •+') { poe-atoi(&(*+4«rgy)[l]); inc-atoi(*(*Hargv)[l]); for (i-1; I <-MAXUNE; H+), if(i!-poe) tab[i]-NO; else{ tab[i] -YES; pos-H-inc; , } else { /* пользователем указаны лоэиции */ for (l-1; i <-MAXLINE; H4), tab [i] - NO; /* убираем все позиции */ while (—argc > 0) { /* движемся по списку аргументов */ pos-atoi(*++argy); if (ров > 0 && рое <-MAXUNE), tab[pos] -YES; } } } За основу этого решения взята программа entab в книге Кернигана и Плоджера, Software Tools (Addison- Wesley,1976). Решение аналогично программе entab в упражнении 5.11. Единственное изменение — функция settab заменена на esettab (расширенная settabK 85
Функция esettab воспринимает краткую запись -т +п. Операторы poe-atoi(&(*+ftrgv)[l]); inc - atoi(*(*++argv) [1]); делают переменную pos равной первой позиции табуляции, a inc — шагу увеличения. Таким образом, позиции табуляции начинаются в pos и находятся на каждой inc позиции. ♦include < stdio.h > #define MAXUNE 100 /* максимальный размер строки V- #define TABINC 8 /* шаг увеличения позиции табуляции по умолчанию*/ #defineYESl #defineNO0 void esettab(int argc, char *argv [], char *tab); void detabCchar *tab); /* заменить табуляции пробелами */ * mainGnt argc, char *argy[]) { char tab [MAXLINE+1]; esettab (argc, argv, tab); /* расставить позиции табуляции V detab(tab); /* заменить табуляции пробелами */ return 0; } За основу этого решения взята программа detab в книге Кернигана и Плоджера, Software Tools (Addison- Wesley,1976). Решение аналогично программе detab в упражнении 5.11 и использует функцию esettab из первой части данного упражнения. Упражнение 5.13 (с.117 КР) Напишите программу tail, которая печатает последние п строк ее ввода. По умолчанию п равно, скажем, 10, но может быть изменено необязательным аргументом, так что tail -n печатает последние п строк. Программа должна вести себя разумно независимо от того, насколько неразумен ввод или значение п. Пишите программу так, чтобы она наилучшим способом использовала доступную память. Строки должны сохраняться так, как в программе сортировки в п. 5.6 КР, а не в двумерном массиве фиксированного размера. 86
#include<stdio.h> #include<stdlib.h> ♦include <string.h > #define DEFUNES10 Л число печатаемых строк по умолчанию */ #define LINES 100 /* максимальное число печатаемых строк */ #define MAXLEN 100 /* максимальная длина входной строки */ * void error (char*); intgetline(char*, int); /* печатать последние п строк ввода */ mainGnt argc, char *argv[]) { char *p; char *buf; /* указатель на большой буфер */ char *bufend; /* конец буфера */ char line [MAXLEN]; /* текущая входная строка */ char *lineptr[UNES]'»/* указатели на считанные строки V int first, i, last, len, n, nlines; if (argc - -1) /* аргументы отсутствуют */ n - DEFUNES; /* выбираем число строк по умолчанию */ else if (argc — 2 && (*++argv) [0] - - »-•> n-atoi(argv[0]+l); else реггогГВызов: tail [-n]H); if (n < 1 11 n > LINES) /* некорректное значение п? */ n- LINES; for(i-0;i<UNES;rH-) lineptr[i]-NULL; if ((p-buf-maUocdJNES* MAXLEN))—NULL) error Ctail: не могу выделить буфер"); bufend - buf+LINES * MAXLEN; last - 0; /* индекс последней считанной строки */ nlines - 0; /* количество считанных строк */ while (Oen-getline (line, MAXLEN)) >0) { if (p +len+1>-bufend) p-buf; /* буфер оборачивается */ lineptr[last]-p; strcpy (lineptr [last], line); if (++last>-LINES) last-0; /* указатели на буфер оборачиваются */ p-fr-len + 1; nlines+f; } if (n > nlines) /* требуется вывести больше строк, чем считано? */ n - nlines; first-last — n; if (first < 0) Л указатель обошел вокруг списка */ first-fr-UNES; for (i - first; n -^0; i - (i +1) % LINES) printfC%sH,lineptr[i]); return 0; /* error печать сообщения об ошибке и выход из программы */ void error(char *s) { printfr%s\n",s); exit(l); } 87
Программа печатает последние п строк своего ввода. Коща переменная argc равна 1, п имеет значение по умолчанию — DEFLINES. Коща argc равна 2, значение п получают из командной строки. Если argc больше 2, это ошибка. Цикл while (den-getline(line, MAXLEN))>0) получает строку за обращение, пока getline (упражнение 1.16) не найдет конец ввода. Для каждой прочитанной строки программа использует место в выделенном буфере. Оператор if (р + len +1 >- bufend) p-buf; переустанавливает указатель р на начало буфера, коща не хватает места, чтобы скопировать текущую строку. Элементы массива lineptr являются указателями на символы и указывают на последние LINES строк, прочитанных к этому моменту. Индексом для этого массива служит переменная last. Коща last становится равна LINES, она устанавливается в О, при этом элементы lineptr и их буфера используются снова. Общее число строк — nlines. Программа печатает последние п строк, так что число требуемых строк не может превысить количество полученных: if (n> nlines) n-nlines; Если общее число строк превышает LINES, индекс last устанавливается в 0 и начальный индекс должен быть скорректирован: if (first <0) first+-LINES; Затем программа печатает последние п строк: for (i-first; n-^0; i- (i+1) % LINES) printf(H%8H,lineptr[i]); Поскольку переменная i изменяется шагом в п элементов начиная со значения first, она может и превысить значение LINES-1. Операция деления по модулю (взятие остатка) удерживает i между значениями 0 и LINES-1: i-(i + l)%UNES 88
Стандартная библиотечная функция exit (с. 158 КР) завершает программу, если произошла ошибка. Функция exit возвращает 1, обозначая ошибочную ситуацию. Упражнение 5.14 (с.120 КР) Измените программу сортировки так, чтобы обрабатывать ключ -г, который означает сортировку в обратном (убывающем) порядке. Убедитесь, что -г работает в сочетании с -п. ♦include <stdio.h> ♦include <string.h> ♦define NUMERIC 1 /* сортировка чисел */ ♦define DEGR 2 /* сортировка по убыванию */ ♦define LINES 100 /* максимальное количество сортируемых строк */ int numcmpCchar *, char *); int readlinesCchar *lineptr [j, int maxlines); void qsort(char *v[], int left, int right, int (*comp) (void *, void *)); void writelinesCchar *lineptr [], int nlines, int deer); static char option - 0; /* сортировать входные строки */ main (int argc, char *argv []) char *lineptr [LINES]; /* указатели на строки текста V int nlines; /* количество считанных входных строк */ int с, гс-0; while (—argc > 0 && (*++argv) [0] ~ '-') while(c-*+4argv[0]) switch (с) { case V : /* сортировка чисел */ option I-NUMERIC; break; case V: /* сортировка по убыванию */ option I-DEGR; break; default: printf ("sort неверный ключ %с\п", с); argc-1; ГС--1; break; } if (argc) printf ("Вызов: sort -nr\nH); else if ((nlines- readlinesdineptr, LINES)) > 0) { if (option & NUMERIC) qsort((void **) lineptr, 0, nlines-1, (int (*) (void *, void *)) numemp); else qsort((void ••) lineptr, 0, nlines-1, (int (*) (void *, void *)) stremp); writelinesOineptr, nlines, option & DECR); 89
}else{ printf ("Слишком много информации\п"); rc--l; } return re; } /* writelines: вывести строки-результаты */ void writelines (char *lineptr[], int nlines, int deer) { inti; if (deer) /* печатать в убывающем порядке */ for (i - nlines-1; i >- 0; i —) printf C%s\nH, HneptrM); else for (i — 0; i < nlines; H+) printf(H%s\nH, lineptr[i]); } Биты статической переменной option определяют, какие возможности затребованы: 0-й бит «■ 0 — сортировка символьной строки, * 1 — сортировка чисел (-п), 1-й бит "0 — сортировка по возрастанию, "1 — сортировка по убыванию (-г). Если указан какой-либо ключ, операция битовое ИЛИ (I) устанавливает соответствующий бит в переменной option. Оператор option l-DECR; эквивалентен option - option I 2; Десятичное число 2 эквивалентно 00000010 в двоичной записи. Поскольку 1 ИЛИ бит-с-любым-значением -1 вышеуказанный оператор Си устанавливает 1-й бит в символьной переменной option в 1 (биты нумеруются 0,1, 2,... справа налево). Чтобы определить, указан ли ключ, мы используем операцию побитовое И (&). Выражение option &DECR истинно, если ключ -г задан, и ложно, если нет. Функция writelines изменена и принимает третий аргумент — deer. Переменная deer — результат выражения option & DECR, который определяет, должен ли отсортированный список печататься в убывающем или возрастающем порядке. 90
Функции strcmp, пшпстр, swap, qsort и readlines те же, что используются в программе sort (с.118 КР). Упражнение 5.15 (с-120 КР) Добавьте ключ -f, чтобы совмещать заглавные и строчные буквы и не различать их в процессе сортировки, например, буквы а и А при сравнении эквивалентны. #include < stdio.h > #include < string.h > ♦include < ctype.h > ♦define NUMERIC 1 /* сортировка чисел */ ♦define DECR 2 /* сортировка по убыванию */ ♦define FOLD 4 /* совмещение заглавных и строчных букв */ ♦define LINES 100 /• максимальное количество сортируемых строк */ int charcmp(char *, char *); int numcmp(char *, char *); int readlines (char *lineptr [], int maxlines); void qsortCchar Ml, int left, int right, int (*comp) (void *, void *)); void writelines(char *lineptr[], int nlines, int order); static char option - 0; /* сортировать входные строки */ main (int argc, char *argv []) char *lineptr [LINES]; /* указатели на строки текста */ int nlines; /* количество считанных входных строк */ int с, гс-0; while (- -argc > 0 && (*+4argv) [0] —'-') while(c-*++argv[0]) switch (с) { case T : /* совмещение заглавных и строчных букв */ option I-FOLD; break; case V : /* сортировка чисел */ option I-NUMERIC; break; case V : /* сортировка по убыванию */ option I-DECR; break; default: printfCsort: неверный ключ %c\n", с); argc-1; rc--l; break; } if (argc) printf ("Вызов: sort -fnr\nH); else{ if ((nlines-readlines(lineptr, LINES)) >0) { if (option & NUMERIC) qsort ((void **) lineptr, 0, nlines-1, (int (*) (void *, void *)) numcmp); 91
eke if (option A FOLD) qnrtttvoid ••) UnepnvO, nlines-1, Oat <*) (void •, void *)) charcmp); eke qtortCCvold ••> ttneptev 0, alioet-l, (tat <•) (void •, void •)) ttrcmp); writelinesflineptr, nlinea, option Л DECK); >eke{ printf ("Слишком много информацииХп"); ic—1; > return re; } /• charcmp: пернуть значение <0, если *<t, 0, если •--1, X), ecjms>t •/ intcharcmp(char4char*t) { for (; tokwerPs) ~ tok»wer(*t); a++, н+) lf(*8—»\0') return 0; return tolower(*s) — tolower(*t); } За основу этого решения взято упражнение 5.14. 2-й бит « 0 — нет совмещения, ■ 1 — совмещение (-D. Если пользователь укажет ключ совмещения, 2-й бит в переменной option должен быть установлен в 1: option l-FOLD; Константа FOLD (десятичное 4) — 00000100- в двоичной записи (биты нумеруются 0,1,2,3,... справа налево). Функция charcmp сравнивает строки как stremp (с. 106 КР). Она преобразует буквы в строчные, чтобы поддерживать возможность совмещения, и сравнивает их. Функции numemp, swap, qsort, readlines и writelines те же, что используются в упражнении 5Л 4. Упражнение 5Л 6 (с.120 КР) Добавьте ключ -d (алфавитный порядок), при указании которого сравнение делается только по буквам, цифрам и пробелам. Убедитесь, что ключ работает в сочетании с -f. ♦include < stdio.h > ♦include < ctype.h > ♦define NUMERIC 1/* сортировка чисел V ♦define DECR 2 /.* сортировка по убыванию */ ♦define FOLD 4 /* совмещение заглавных и строчных букв V ♦define DR 8 Л алфавитный порядок V - ♦define LINES 100 /* максимальное количество сортируемых строк ?/ int charcmp(char •, char *); in! numcmp(char *, char *); 92
tatradltoes(ciiar*toieptr[], tatmaxntiea); void qsort(char *v[l, hit left, lot right, bit <*comp) (void *, void •)>; void writelines(cnar *flneptr[], tat nlines, tat order); static char option-0; /* сортировать входные строки*/ mainOnt argc, char *argv[]) { char *lineptr[UNES]; /* указатели на строки тексте •/ tat nlines;/* количество считанных входных строк*/ intc, гс-О; whiie (--art* > 0 && (*++argv) [0] -- '-') while(c-*++argv[0]) switch (с) { case 'd': /* алфавитный порядок */ option l-DIR; break; case T: /* совмещение заглавных и строчных букв */ option 1-FOLD; break; case V: /* сортировка чисел */ option ««NUMERIC; break; case V: /* сортировка по убыванию */ option I-DECK; break; default: printf Caort: неверный ключ %c\n", с); argc-1; ic—1; break; > if (arte) printf ("Вызов: sort -dfar\n"); etoe{ if «nlines - readitaesOineptr, LINES)) > 0) { if (option & NUMERIC) qeort((void **) lineptr, 0, nlines-1, (tat (*) (void *, void *)) numemp); else qsort((void **) lineptr, 0, nlines-1, (tat (*) (void *, void *)) charemp); writettnesdineptr, nlines, option Л DECR); }ebe{ printf ("Слишком много информации\п"); ГС--1; } } return re; } /* charemp: вернуть значение <0, если s<t, 0, если s—-t, X), если s>t */ tat charemp (char *s, char *t) { char a, b; tat fold- (option & FOLD) ? 1:0; tat dir - (option & DR) ? 1:0; 93
do{ if (dir) { while (!isalnum(*s) && *s!- " && *s!- '\0') ' д | | - whlle'<!lialiiiim(*0 && *t!-* • && *t!- *\Of) t++; } a - fold ? tolower(*s): *s; b-'fold?tolower(*t):*t; «и- if (a — bft* a —'NO') return 0; }while(a — b); return a — b; } В основу этого решения положены упражнения 5.14 и 5.15. 3-й бит "О — обычный способ сравнения, "1 — только алфавитный порядок (-d). Если пользователь затребует только алфавитный порядок, 3-й бит в переменной option устанавливается в 1 option l-DIR; Константа DIR (десятичное 8) — 00001000 в двоичной записи (биты нумеруются 0,1,2,3,... справа налево). Функция charcmp (упражнение 5.15) была изменена так, чтобы она обрабатывала ключи совмещения и алфавитного порядка. Если пользователь затребует алфавитный порядок, цикл while while (!isalnum(*s) && *s!- •' && *s 8» »\<Г) 8» •» изучает каждый символ в строке s и пропускает символы, не являющиеся буквавш, цифрами или пробелами. Макроопределение isalnum находится в заголовочном файле ctype.h. Оно проверяет наличие алфавитных символов (а- z, A-Z) и цифр (0-9). Если *s — алфавитный символ или цифра, то значение isalnum не 0, иначе isalnum (*s) дает 0. Следующий цикл while while (!isalnum(*t) && *t!- • • && *t 8- 9\0*) t++; изучает каждый символ в строке t и пропускает символы, не являющиеся буквами, цифрами или пробелами. Если в строке s находят букву, цифру или пробел net 94
также находят букву, цифру или пробел, функция charcmp сравнивает два символа. Мы могли бы выбрать другой подход и тоща создали бы вместо charcmp три функции: foldcmp, dircmp и folddircmp. Функция foldcmp совмещала бы и сравнивала символы, как charcmp в упражнении 5.15. Функция dircmp сравнивала бы символы в алфавитном порядке, а folddircmp совмещала бы и сравнивала в алфавитном порядке. Каждая отдельная функция была бы быстрее, чем имеющаяся charcmp. Мы предпочли усложнить charcmp, a не создавать больше функций. Функции numcmp, swap, qsort, readlines и writelines те же, что используются в упражнении 5.14. Упражнение 5.17 (сЛ20 КР) Добавьте возможность обработки полей, чтобы можно было сортировать по полям, входяпщм в строки, причем каждое поле сортировалось бы по независимому набору ключей. (Алфавитный указатель для этой книги был отсортирован с ключом -df для пунктов указателя и с ключом -п — для номеров страниц.) ♦include < stdio.h > ♦include < ctype.h > ♦define NUMERIC 1 /* сортирбвка чисел */ ♦define DECR 2 /* сортировка по убыванию */ ♦define FOLD 4 /* совмещение заглавных и строчных букв */ ♦define DIR 8 /* алфавитный порядок */ ♦define LINES 100 /* максимальное количество сортируемых строк */ Int charcmp (char *, char *); void error (char*); int numcmp (char *, char *); void readargsOnt argc, char *argv[]); int readlines(char *lineptr [], int maxlines); void qsort(char ML tot left, int right, int (*comp) (void *, void *)); void writelines(char *lineptr[], int nlines, int deer); char option-0; int posl - 0; /* поле начинается в позиции posl */ int pos2 - 0; /• и кончается точно перед pos2 •/ /• сортировать входные строки */ main (int argc, char *argv[]) { char *lineptr [LINES]; /* указатели на строки текста */ int nlines; /* количество считанных входных строк */ int с, гс-0; readargs(argc, argv); if ((nlines-readlinesdineptr, LINES)) > 0) { if (option & NUMERIC) 95
qsort((void **) lineptr, 0, nHnes-1, (int (*) (void *, void *)) numcmp); else qsort((void **) lineptr, 0, nlines-1, (int (*) (void *, void *)) charcmp); writelines (lineptr, nlines, option & DECR); }else{ printf ("Слишком много информации\п"); ic—1; } return re; } /* readargs: считывать аргументы программы */ void readargs (int argc, char *argv[]) { intc; int atoi (char*); while (—argc>0&& (c- (*+4argv)[0]) — •-• 11 с — •+•) { if (c — •-• && !isdigit(*(argv[0]+l))) while (c-*++argv[0]> switch (c) { case 'd*: /• алфавитный порядок */ , option l-DIR; break; case T: /* совмещение заглавных и строчных букв */ option I-FOLD; break; case V: /* сортировка чисел */ option I-NUMERIC; break; case V: /* сортировка по убыванию V option I-DECR; break; default: printf ("sort: неверный ключ %с\п", с); error ("Вызов: sort -dnfr [+posl] [-pos2]"); break; } else if (c--'-') pos2-atoi(argv[0]+l); else if ((posl-atoi(argv[0]+l))< 0) еггогСВызов: sort-dnfr [+poslJ [-pos2]"); } if (argc II posl >pos2) error ("Вызов: sort -dnfr [-bposl] [-ро82]"); } Исходный файл numempx: #include < math.h > #include < ctype.h > ♦include < string.h > #defineMAXSTR100 void substr(char *s, char *t, int maxstr); 96
/* numcmp: сравнить строки si и s2 как числа */ int numcmp (char *sl, char *s2) { double vl,v2; char str [MAXSTR]; substr(sl, str, MAXSTR); vl-atof(str); substr(s2, str, MAXSTR); v2-atof(str); if (vl<v2) return-1; else if (vl>v2) return 1; else return 0; #define FOLD 4 /* совмещение заглавных и строчных букв */ #define DIR 8 /* алфавитный порядок */ /* charcmp: вернуть значение <0, если s<t, 0, если ат -t, >0, если s>t */ int charcmp (char *s, char *t) { char a, b; int i, j, endpos; extern char option; extern int posl, pos2; int fold - (option & FOLD) ? 1:0; int dir - (option & DIR) ? 1:0; i-j-posl; if(pos2>0) endpos - pos2; else if ((endpos - strlen(s)) > strlen(O) endpos - strien(t); do{ if (dir) { while (i < endpos && !isalnum(s[i]) && s[i]!-,,*&s[i]!-,\0,) H [• while* (j < endpos && !isalnum(t [j]) && tOlI-'^^tmi-'W) } if (i < endpos && j < endpos) { a - fold ? tolower(s[i]): s [i]; li I - b-fold?tolower(t[j]): t[B; t++* if (a — Ъ&&& — *\0*) return 0; } } while (a - - b && i < endpos && j < endpos); return a — b; Исходный файл substrx: I ** 4—752 97
#include < string.h > void error (char*); /* substr: получить и записать в str подстроку из строки s */ void substr(char *s, char *str) { int i, j, len; extern int posl, pos2; len-strlen(s); if (pos2 > 0 && len > pos2) len - pos2; else if (pos2 >0&& len < pos2) error ("substr: слишком короткая строка"); for q-0, i-posl; i<len; R+, j++) 8trO]-sM; strOl-AO*; } За основу этого решения взяты упражнения 5.14,5.15 и 5.16. Синтаксис команды sort следующий: sort -dnfr [+posl] [-pos2] Если вы хотите сортировать по полям внутри строк, можно указать posl и pos2. Поле сортировки начинается на позиции posl и кончается непосредственно перед pos2. В противном случае posl и pos2 равны 0 in ключом для сортировки является вся строка. Функция readargs считывает аргументы командной строки. Цикл while в readargs истинен, гожа есть аргументы и пока аргументу предшествует знак минус. Первый оператор if if (с — •-» && !isdigit(*(argv[0]+l))) истинен, если аргумент представляет собой знак минус, за которым следует не цифра. Оператор switch обрабатывает эти аргументы так же, как и в упражнениях 5.14,5.15 и 5.16. Следующий оператор else-if else if (с — *-9) истинен только, если данный аргумент — необязательный -pos2. Заключительный оператор else-if обрабатывает аргумент +posl и проверяет, что число posl больше 0. Функция charcmp — измененная версия этой функции из предыдущих упражнений. Эта версия обрабатывает поля. Функция numcmp сравнивает числа, как и предыдущая ее версия, но требует введения новой функции substr, по- 98
скольку atof не принимает начало строки и ее ддину в качестве аргументов. Безопаснее ввести новую функцию substr, чем изменять интерфейс часто используемой функции вроде atof. Функции swap» qsort, readlines и writelines те же, что используются в упражнении 5.14. Функция error — из упражнения 5.13. Упражнение 5.18 (с. 124 КР) Организуйте в функции del проверку ошибок во входной информации. ♦include < stdio.h > ♦include < strlng.h > ♦include < ctype.h > enum { NAME, PARENS, BRACKETS }; enum{NO,YES}; void del (void); void dirdcl (void); void errmsg (char *); intgettoken(void); extern int tokentype; /* тип последней лексемы */ extern char token []; /* строка, содержащая последнюю лексему */ extern char name []; /* идентификатор */ extern char out []; extern int prevtoken; /* del: осуществляет грамматический разбор декларатора */ void del (void) { intns; for (ns - 0; gettokenO --'•';)/• подсчитываем звездочки */ ns++; dirdclO; while (ns—X» strcat(out,u указ. на"); } /* dirdcl: разбор непосредственного декларатора */ void dirdcl (void) { int type; if (tokentype — * О {/*( del) •/ dclO; if (tokentype !-T) emMgCOuiHOKa: отсутствует )\nH); } else if (tokentype - - NAME) /* имя переменной •/ strcpy (паше, token); else errmsg ("Ошибка: ожидается имя или (dcl)\nn); 99
while ((type-gettokenO) —PARENS 11 type — BRACKETS) if (type — PARENS) 9trcat(out,м функ. возвращ."); else{ strcat(out, м массив"); strcat (out, token); strcat (out, "из"); } } /• errmsg: напечатать сообщение об ошибке и указать, */ /* что лексема доступна V void errmsg(char *mag) { printfr%a-,mtg); prevtoken-YES; } Исходный файл gettoken.c: #include < ctype.h > ♦include <string.h> enum { NAME, PARENS, BRACKETS }; enum{NO,YES}; extern int tokentype; /• тип последней лексемы */ extern char token []; /* строка, содержащая последнюю лексему */ int prevtoken - NO; /* предыдущей лексемы нет */ /* gettoken: возвращает следующую лексему •/ int gettoken (void) { intc,getch(void); void ungetch (int); char *p - token; if (prevtoken—YES) { prevtoken-NO; return tokentype; while ((c-getehO) — »• lie — Af); if(c —»0{ if ((c-getch ()) — »)»){ strcpy (token, a()w); return tolentype - PARENS; }else{ ungetch (c); return tokentype -' ('; } }elseif (c--T){ for (*p++-c; (*p++-KetehO) «-»]•;) •p-'W; return tokentype - BRACKETS; }elself (isalpha(c)){ 100
for (♦p++-c;isalnum(c-getch());) V+-c; *p-'\0'; ungetch(c); return tokentype - NAME; }else return tokentype - c; Мы немного изменили dirdcl, потому что эта функция ожидает одну из двух лексем:')' после вызова del или имя. Если встретившаяся лексема — ни та, ни другая, мы вызываем errmsg вместо printf. Функция errmsg выдает сообщение об ошибке и устанавливает переменную prevtoken так, чтобы указать функции gettoken, что лексема уже имеется. В начале функции gettoken есть новый оператор if: if (prevtoken—YES) { prevtoken-NO; return tokentype; } To есть, если лексема уже имеется, новую пока не принимать. Наша новая версия не является "пуленепробиваемой", но ее способность обрабатывать ошибки улучшена. Упражнение 5.19 (с.124 КР) Модифицируйте функцию undcl, чтобы она не добавляла к декларации лишних скобок. #include < stdio.h > #include < string.h > ♦include < ctype.h > #define MAXTOKEN100 enum { NAME, PARENS, BRACKETS }; void del (void); void dirdcl (void); int gettoken (void); int nexttoken(void); int tokentype; /* тип последней лексемы */ char token [MAXTOKEN]; /* строка,содержащая последнюю лексему */ char out [1000]; / * Mndcl: преобразовать словесное описание в декларацию */ main () { int type; НИ
char temp [MAXTOKEN]; while (gettokenO !-EOF) { strq>y (out, token); while ((type-gettokenO) !-'W) if (type — PARENS 11 type — BRACKETS) strcat (out, token); else if (type — •*•) { if ((type-nexttokenO) --PARENS 11 type — BRACKETS) sprintfOemp, tt(*%s)w, out); else sprintf(temp, a*%sw, out); strcpy(out, temp); } else if (type—NAME){ sprintf(temp, u%s %sw, token, out); sifcpy (cut, temp); }else printf ("Ошибка во входных данных: %s\n", token); printf(H%s\nH,out); } return 0; } enum{NO,YES}; intgettoken(void); /* nexttoken: получить следующую лексему и вернуть ее */ int nexttoken (void) { int type; extern int prevtoken; type-gettokenO; prevtoken-YES; return type; } Для получения описания "х: указ. на char" функции del вводится х * char а функция undcl выдает ♦char (*x) Скобки здесь лишние. На самом деле скобки требуются только тоща, когда следующая лексема — О или [ ]. Для примера "daytab: указ. на массив [13 ] из int" функции del вводится daytab* [13] int а функция undcl выдает int(*daytab)[13] 102
и это правильно. С другой стороны, для примера "daytab: массив [13 ] указ. на int" вводится day tab [13] * in t и функция undcl выдает int(*daytab[13]) В этом случае скобки лишние. Мы изменили функцию undcl, чтобы она проверяла, не является ли следующая лексема О или [ ]. Бели является, скобки необходимы, в противном случае излишни. Перед тем, как принять решение о добавлении скобок, мы просматриваем выражение на одну лексему вперед. Мы создали простую функцию nexttoken, которая вызывает gettoken, фиксирует факт, что лексема уже доступна, и возвращает тип лексемы. Функция gettoken (из упражнения 5.18) перед тем, как получить новую лексему с ввода, проверяет, доступна ли лексема. Модифицированная undcl не выдает лишних скобок. Например, для ввода х * char результатом будет char *x Для выражения daytab* [13] hit выдается int(*daytab)[13] а для daytab [13] * int выдается int*daytab[13] Упражнение 5.20 (сЛ24 КР) Расширьте функцию del, чтобы она обрабатывала описания с типами аргументов функций, описателями вроде const и т.д. ♦include < stdio.h > ♦include < string.h > ♦include < ctype.h > 103
enum { NAME, PARENS, BRACKETS }; enum { NO, YES }; void del (void); void dirdcl (void); void errmsg(char *); intgettoken(void); extern int tokentype; /* тип последней лексемы •/ extern char token []; /* строка, содержащая последнюю лексему */ extern char name []; /* идентификатор */ extern char datatype []; /* тип данных - int, char и т.п. V extern char out []; ' • % ' extern int prevtoken; /* del: осуществляет грамматический разбор декларатора */ void del (void) { int ns; for (ns- 0; gettokenO -- '•';) /* подсчитываем звездочки */ ns++; dirdclO; while (ns— 0) 8trcat(out, м указ. на"); /* dirdcl: разбор непосредственного декларатора */ void dirdcl (void) { int type; void parmdcKvoid); if (tokentype —' О { /* (del) */ dclO; if (tokentype !-T) errmsg ("Ошибка: отсутствует )\nn); } else if (tokentype - - NAME) { /• имя переменной */ if (name [0]—Л0') strcpy (name, token); }else prevtoken-YES; while ((type-gettokenO) —PARENS 11 type — BRACKETS 11 type--•(') if (type — PARENS) strcat(out,м функ. возвращ."); else if (type — IV) { strcat(out,м функ. принимающ.w); parmdcK); strcat(out, u и возвращ."); }else{ strcat(out, м массив"); strcat (out, token); strcat(out, "из"); } /* errmsg: напечатать сообщение об ошибке и указать, */
/* что лексема доступна V void errmsg(char *msg) { printf(N%sM, msg); prevtoken-YES; } Исходный файл parmdclx: ♦include < stdio.h > ♦include < string.h > ♦include <stdlib.h> ♦include < ctype.h > ♦define MAXTOKEN 100 enum { NAME, PARENS, BRACKETS }; enum{NO,YES}; void del (void); void errmsg(char *); void dclspec (void); inttypespec(void); inttypequal(void); int compareCchar **, char ••); intgettoken(void); extern int tokentype; /* тип последней лексемы */ extern char token []; /* строка» содержащая последнюю лексему */ extern char name[]; /* идентификатор */ extern char datatype []; /* тип данных - int, char и т.п. */ extern char out []; extern int prevtoken; /• parmdcl: разбор декларатора с параметрами V void parmdcl (void) { do{ dclspec О; } while (tokentype — 7); if (tokentype !-V) ernnsgrOrcyTCTByeT) в декларации параметров\п"); } /* dclspec: спецификация декларации •/ void dclspec (void) { char temp [MAXTOKEN]; temp[0]-'\0'; gettokenO; do{ if (tokentype!-NAME) { prevtoken-YES; dclO; } else if <typespec() — YES) { strcat(temp, un); strcat(temp, token); 105
gettokenO; } else if (typequalO — YES) { strcat(temp,aw); strcat(temp, token); gettokenO; }else emnsg ("Неизвестный тип в списке параметров\п"); } while (tokentype!-V && tokentype!- T); strcat (out, temp); if (tokentype — 7) strcat(out, V); } /* typespec: возвращает YES, если данная лексема—*/ /* спецификатор типа V int typespec (void) { static char 'types [} - { "char", ainf, "void" }; char *pt- token; if (bsearch(&pt, types, sizeof (types)/sizeof (char *), sizeof (char •), compare) - - NULL) return NO; else return YES; } /* typequal: возвращает YES, если данная лексема — */ /• описатель типа */ int typequal (void) static char *typeq [] - { "const*, "volatile* }; char *pt- token; if (bsearch(&pt, typeq, sizeof (typeq) /sizeof (char *), sizeof (char *), compare) - - NULL) return NO; else return YES; } /* coapare: сравнить две строки для функции ЬсеагсЬ*/ int comparetehar ••s, char ^Й) return strcmp(*s, *t); } Мы расширили грамматику на с. 121 КР, чтобы она включала декларации параметров: del: необязательные) * direct-del 106
direct-del: имя (del) direct-del (необязательный parm-dcl) direct-del [необязательный размер ] parm-dcl: parm-dcl, del-spec del del-spec: type-spec del-spec type-qual del-spec Это сокращенный вариант части грамматики, которая описывает декларации. Распознается несколько спецификаторов типа, описанных на с.206 КР. Например, декларация void *(*comp) (int *, char *, int (*fnc) ()) имеет результат comp: указ. на функ. ожидающ. указ. на Int, указ. на char, указ. на функ. возвращ. int и возвращ. указ. на void Мы изменили функцию dirdcl и добавили функции parmdcl и dclspec. В этом упражнении используется потенциальная возможность, созданная в упражнении 5.18. Иногда нужно извлечь лексему перед тем, как решить, что предпринять. Иноща имеется лексема, которую еще нельзя использовать, и мы возвращаем ее назад. Следующий вызов функции gettoken в любом месте программы грамматического раэбора извлечет эту же лексему снова и использует ее. Функция bsearch — из стандартной библиотеки, она осуществляет бинарный поиск.
Глава 6 Структуры Упражнение 6.1 (сЛЗЗ КР) Наша версия функции getword не обрабатывает надлежащим образом символы подчеркивания, строковые константы, комментарии и управляющие строки препроцессора. Напишите лучшую версию. ♦include < stdio.h > ♦include < ctype.h > /* getword: получить со ввода следующее слово или символ */ int getword (char *word, int lim) { int c, d, comment(void), getch(void); void ungetch (int); char *w- word; while (isspacete-fetch ())) if (c!-EOF) *w++-c; if (isaipha(c) 11 с — • • 11 c — '#') { for(;—lim>0;w4+) if (!isalnum(*w-geteh()) &&*w!-»J) { ungetch (*w); break; } }elseif(c — *\" He — »"•) { for(;—lim>0;w++) if((*w-geteh())~ '\V) *++w-geteh(); else if (*w— c) { *-{+; break; }elseif(*w—EOF) break; } else if (c — V) if ((d-getch()) — ••') с - comment 0; else ungetch (d); *w-'\0'; 108
return с; } /* comment: пропустить комментарий и вернуть символ V int comment (void) { int с; while ((c-getchO) !-EOF) if(c — •••> if«c-geteh<))—-V) break; else ungetch(c); return c; } Для обработки символов подчеркивания и команд препроцессора мы изменили if («isalpha(c)) на if (isalpha(c) 11 с — 9J 11 c — '#') Последующие алфавитно-цифровые символы и символы подчеркивания рассматриваются как часть слова. Строковые константы могут появляться внутри одиночных или двойных кавычек. При нахождении кавычки берем символы, пока не встретится закрывающая кавычка или EOF. Игнорируем комментарии и возвращаем закрывающий символ косой черты. Эта часть программы аналогична упражнению 1.24. Упражнение 6.2 (с. 139 КР) Напишите программу, которая читает программу на Си и печатает в алфавитном порядке каждую группу имен переменных, совпадающих в первых б символах, но различающихся где-либо дальше. Не рассматривайте слова внутри символьных строк и комментариев. Сделайте число 6 параметром, который может быть задан из командной строки. #include < stdio.h > #include < ctype.h > ♦include < string, h > ♦include < stdlib.h > struct tnode { /* узел дерева: */ char *word; /* указатель на текст */ int match; /* признак совпадения */ struct tnode *left; /* левый потомок V struct tnode *right; /* правый потомок */ }; 109
#define MAXWORD 100 ♦define YES 1 ♦define NO 0 struct tnode *addtreex (struct tnode *, char *, int, int *); void treexprint (struct tnode *); int getword(char *, int); /* печатать в алфавитном порядке каждую группу имен переменных, V /* совпадающих в первых num символах */ main (int argc, char *argv[]) { struct tnode *root; char word [MAXWORD]; int found - NO; /* YES, если было найдено совпадение */ int num; /* число первых сравниваемых символов */ num - (—argc && (*++argv) [0] —'-') ? atoi(argv[0]+l): 6; root-NULL; while (getword (word, MAXWORD) !-EOF) { if (isalpha(word[0]) &&strlen(word) >-num) root - addtreex (root, word, num, Afound); found-NO; } treexprint (root); return 0; } struct tnode *talloc(void); int compare (char *, struct tnode *, int, int *); /* addtreex: добавить узел со словом w к указателю р или ниже V struct tnode *addtreex (struct tnode *p, char *w, int num, int *found) int cond; if (p - - NULL) { /* появилось новое слово */ p - tallocO; /* создаем новый узел V p->word - strdup(w); p->match - 'found; p->lef t - p->right - NULL; } else if ((cond - compare(w, p, num, found)) < 0) p->left - addtreex (p->left, w, num, found); else if (cond > 0) p->right - addtreex (p->right, w, num, found); return p; } /* compare: сравнить слова и обновить p->match */ int compare (char *s, struct tnode *p, int num, int *found) { int i; char*t-p->word; for (i - 0; *s — *t; i++, s++, H+) if (*s — *\09)
return 0; if (i >- num) { /* совпадают в первых num символах? */ •found-YES; p->ma1eh-YES; } return *s — *t; } /• treexprint: печатать дерево р по порядку, если p->match - - YES •/ void treexprint(stmct tnode *p) { if (p!-NULL) { treexprint(p->left); if (p->match) printfC%s\nH, p->word); treexprint (p->right); } } Программа печатает имена переменных, совпадающие по первым num символам. Если число символов не указано из командной строки, оно устанавливается равным б num - (—argc && (*++argv) [0] —'-') ? atoi(argv[0]41): 6; Переменная found — логическая, found равна YES, если слово совпадает в num символах с некоторым словом в дереве, иначе — N0. Программа помещает слово в дерево, если первый символ слова алфавитный и длина слова больше или равна num. getword — функция из упражнения 6.1. Функция addtreex, являющаяся модификацией addtree (с. 137 КР), вставляет слово в дерево. Функция compare сравнивает слово, вставляемое в дерево, с некоторым словом, уже находящимся там. Если есть совпадение в первых num символах, *found и элемент, отражающий совпадение (p->match), соответствующий слову в дереве, устанавливаются равными YES. if (i>-num){ •found-YES; p->mateh-YES; } Функция treeprint печатает слова в дереве, совпадающие в первых num символах по крайней мере еще с одним словом. Упражнение 6.3 (с. 139 КР) Напишите программу — формирователь "перекрестных ссылок", печатающую список всех слов в документе и для каждого слова — список номеров строк, в которых оно ш
встречается. Не рассматривайте такие часто встречающиеся слова, как "the", "and" и т.п. #include < stdio.h > ♦include < string.h > ♦include <ctype.h> ♦include < stdlib. h > ♦define MAXWORD 100 struct linklist { /* связанный список номеров строк */ int lnum; struct linklist *ptr; }; struct tnode { /* узел дерева: */ char *word; /* указатель на текст •/ struct linklist 'lines; /* номера строк */ struct tnode *left; /* левый потомок */ struct tnode *right; /* правый потомок V }; struct tnode *addtreex(struct tnode *, char *, int); int getword(char *, int); int noiseword(char *); void treexprint (struct tnode *); /* программа печати перекрестных ссылок */ main () { struct tnode *root; char word [MAXWORD]; int linenum -1; root-NULL; while (getword(word, MAXWORD)!- EOF) if (isalpha (word [0]) && noiseword (word) - - -1) root - addtreex (root, word, linenum); else if (word [0]—*V) linenum-H-; treexprint (root); return 0; } struct tnode *talloc(void); struct linklist ♦lalloc(void); void addin (struct tnode *, int); /* addtreex: добавить узел со словом w к указателю р или ниже */ struct tnode 'addtreex (struct tnode *p, char *w, int linenum) { int cond; if (p - - NULL) { /* появилось новое слово */ p - tallocO; /* создаем новый узел */ p->word - strdup (w); p->lines-lalloc(); p->lines->lnum - linenum; 112
p->lines->ptr - NULL; p->lef t - p->right - NULL; } else if ((cond - strcmp (w, p->word)) - - 0) addln (p, linenum); else if (cond< 0) p->left - addtreex (p->left, w, linenum); else p->right - addtreex(p->right, w, linenum); return p; } /* addln: добавить номер строки к связанному спи&у V void addln (struct mode *p, int linenum) { struct linklist *temp; temp - p->lines; while (temp->ptr!- NULL && temp->lnum!- linenum) temp - temp->ptr, if (temp->lnum!-linenum) { temp->ptr-lalloc(); temp->ptr~>lnum -* linenum; temp->ptr->ptr - NULL; } } /* treexprint: печатать дерево р по порядку */ void treexprint(struct mode *p) { struct linklist *temp; if (p!-NULL) { treexprint (p->left); printf (H%10s: tt, p->word); for (temp - p->lines; temp!- NULL; temp - temp->ptr) printfC%4d a, temp->lnum); printf CV); treexprrat(p->right); } } /* lalloc: создать узел связанного списка */ struct linklist *lalloc(void) return (structlinklist*) malloc(sizeof(structlinklist)); } /* noiseword: опознавать слишком часто встречающиеся слова */ int noiseword (char *w) { static char *nw[] - {ш aaw, aanw, "are", "in", * aisw, аоГ, "or", 113
"that", "the", "this", u\on }; int cond, mid; intlow-0; int high - sizeof (nw) / sizeof (char *) — 1; while (low <-high) { mid - (low + high)/2; if ((cond - strcmp(w, nw [mid])) < 0) high - mid—1; else if (cond > 0) low - mid + 1; else return mid; } return -1; } Дерево содержит один узел для отдельного слова. В каждый узел входят: указатель на текст слова (word); указатель на связанный список номеров строк (lines); указатель на левый узел-потомок (left); указатель на правый узел-потомок (right). Каждый элемент связанного списка номеров строк является структурой типа linklist. Каждая структура содержит номер строки и указатель на следующий элемент связанного списка. Если в списке больше нет элементов, этот указатель равен NULL. Функция addtreex — измененная версия addtree (с. 137 КР). Функция addtreex вставляет слово в дерево и номер строки в соответствующий связанный список. Если это новое слово, номер строки присваивается первому элементу связанного списка: p->lines->lnum - Hnenum; Если слово уже есть в дереве ((cond - strcmp (w, p->word)) - - 0) функция addlen добавляет номер строки в связанный список. Функция addlen пересекает связанный список в поисках того же самого номера строки или NULL: while (temp->ptr!- NULL && temp->lnum!- Hnenum) temp - temp->ptr; Если данного номера строки нет в списке, функция добавляет номер в конец связанного списка: if (temp->lnum !- Hnenum) { 114 \
temp->ptr - lallocO; temp->ptr->lniim - linenum; temp->ptr->ptr - NULL; } Функция treexprint — модифицированная версия treeprint (с. 138 KP). Функция treexprint печатает дерево в алфавитном порядке. Для каждого слова в дереве функция печатает собственно слово и все номера строк, в которых встречается слово. Функция noiseword ищет слово в статическом массиве слишком часто встречающихся слов. Если слово не является одним из них, она возвращает -1. Вы можете добавлять собственные слова в nw[ ], лишь бы массив оставался отсортированным по возрастанию кодов ASCII. Мы изменили getword таким образом, чтобы она возвращала ' \п\ благодаря чему можно следить за номерами строк: while (isspacete - gelchO) && с!- ' V) Упражнение 6.4 (сЛ39 КР) Напишите программу, которая печатает отдельные слова, поступающие на ввод, отсортированными в порядке убывания частоты встречаемости. Пусть каждому слову предшествует его счетчик. #include<stdio.h> #include < ctype.h > ♦define MAXWORD 100 #define NDISTINCT1000 struct tnode { /* узел дерева: */ char *word; /* указатель на текст */ int count; /* сколько раз встретилось слово */ struct tnode *left; /* левый потомок */ struct tnode 'right; /* правый потомок */ }; struct tnode *addtree (struct tnode *, char *); int getword (char *, int); void sortlist (void); void treestore (struct tnode *); struct tnode *list [NDISTINCT]; /* указатели на узлы дерева •/ int ntn - 0; /* количество узлов дерева */ /* печатать слова, отсортированные по убыванию частоты встречаемости*/ main О { struct tnode *root; 115
char word[MAXWORD]; inti; root-NULL; while (getword(word,MAXWORD) !-EOF) lf(isalpha(word[0])) root - add tree (root, word); treestore (root); sortlist 0; for<i-0;i<ntn;l++) prlntfr%2d:%20B\nwf list[i]->count, list[i]->word); return 0; } /* treestore: записать в массив list[] указатели на узлы дерева*/ void treestore(stnict mode *p) { if (p!-NULL) { treestore (p->left); if (ntn< NDISTINCT) list[ntn++]-p; treestore (p->right); } } /* sortlist: отсортировать список указателей на узлы дерева */ void sortlist 0 { intgap, i,j; struct tnode *temp; for (gap - ntn/2; gap > 0; gap /- 2) for(i-gap;i<ntn;H-»-) for g - Hgap; j >-0; h-gap) { if «listQ] ->count) >- (list[}fgap]->count)) break; temp-list Ш; list[j]-list[}+gap]; listЦ+gap] -temp; } } Максимальное количество различных слов — NDISTINCT. Структура tnode — та же, что используется на с. 136 КР. Массив list состоит из указателей, где каждый указывает на структуру типа tnode. Переменная ntn содержит количество узлов в дереве. Программа читает каждое слово и помещает его в дерево. Затем функция treestore помещает каждый указатель на tnode в массив list. Функция sortlist — модификация shellsort (c.66 KP). Функция sortlist сортирует массив list в порядке убывания частоты встречаемости. Пб
Упражнение 6.5 (сЛ42 КР) Напишите функцию undef, которая будет удалять имя и определение из таблицы, поддерживаемой функциями lookup и install. unsigned hash (char *); /* undef: удалить имя и определение из таблицы */ void undef (charts) { inth; struct nlist *prev, *np; prev-NULL; h - hashCs); /• хэш-код строки s •/ for (np-hashtab[h]; np!-NULL; np- np->next) { if (stremp (s, np->name) - - 0) break; prev - np; /* запоминаем предыдущий элемент •/ if (np!-NULL) {/* имя найдено*/ if (prev-- NULL) /* это первое имя в хэш-таблице? */ hashtab [h] -np->next; else Л нет, не первое V prev->next-np->next;. frec((yoid •) np->name); free((void *) np->defn); free ((void •) np); /* освобождаем выделенную структуру */ } } Функция undef ищет в таблице строку s, после ее нахождения происходит выход из цикла: if (strcmpCs, np->name) --0) break; Если строки s нет в таблице, цикл for завершается, когда указатель пр становится равным NULL Если пр не равен NULL, то существуют имя и определение, которые нужно удалить из таблицы. Элемент в hashtab указывает на начало связанного списка, пр указывает на элемент, который нужно удалить, a prev — на предыдущий. Если prev равен NULL, то пр является первым элементом связанного списка, начинающегося в hashtab [h]: if (prev—NULL) hashtab [h] -np->next; else prev->next - np->next; После удаления элемента пр место, выделенное для ^ 117
имени, определения и самой структуры, освобождается (free,c.l82KP). free ((void *) np->name); free((void *) np->defn); free((void*) np); Упражнение 6.6 (с Л 42 KP) Сделайте простую версию обработчика #define (т.е. без аргументов), подходящего для работы с программами на Си и основанного на программах этого раздела. Вам также могут оказаться полезными функции getch и ungetch. #include < stdio.h > #include < ctype.h > #include < string.h > ♦define MAXWORD 100 struct nlist { /* элемент таблицы: */ struct nlist *next; /• следующий элемент в цепочке */ char *name; /* определяемое имс •/ char *defn; /• заменяющий текст */ void error (int, char *); int getch (void); void getdef (void); int getword(char *, int); struct nlist 'install (char *. char *); struct nlist 'lookup (char *); void skipblanks(void); void undef (char •); void ungetch (int); void ungets (char •); /* простая версия обработчика #deflne */ main () { char w [MAXWORD]; struct nlist *p; while (getword(w, MAXWORD) !-EOF) if (strcmp(w,N#N) — 0) /* начало директивы */ getdef О; elseif(!isalpha(w[0])) printf C%sH, w); /* не может быть определено •/ else if ((p-lookup(w))—NULL) printf C%sH, w); /* не определено */ else ungets(p->defn); /* возвращаем определение */ return 0; 118
/* getdef: получить определение и вставить его в таблицу */ void getdef (void) { intc,i; char def [MAXWORD], dir[MAXWORD], name [MAXWORD]; skipblanksO; if (!isalpha(getword(dir, MAXWORD))) error (dir[0], "getdef: после # ожидается директива"); else if (strcmp(dir, "define") —0) { skipblanksO; if (!isalpha(getword(name, MAXWORD))) error (name [0], "getdef: не буква — ожидается имя"); else{ skipblanksO; for (i - 0; i < MAXWORD; i++) if ((def[i] -getchO) —EOF 11 def[i] —An') break; /* конец определения */ def[i]-'\0'; if (i<- 0) /* нет определения? •/, errorC\n\"getdef: незавершенное определение**); else /• вставляем определение в таблицу */ install (name, deD; } } else if (strcmp(dir, "under) — 0) { skipblanksO; if (!isalpha(getword(name, MAXWORD))) error (name [0], "getdef: не буква в директиве undef); else undef (name); }else error(dir[0], "getdef: после # ожидается директива"); } /* error: напечатать сообщение об ошибке и пропустить остаток строки*/ void error (int с, char *s) рНШГОшибка: %s\nH,s); while (с!- EOF && с!- * W) c-getch(); } /• skipblanks: пропустить символы пробела и табуляции */ voidskipblanks(void) { int с; while ((c-getchO) —f f 11 c — '\f) » ungetch(c); } Функция main содержит тело этого простого обработчика. Ожидается, что директивы (define, undef) следуют за - 119
#, и функция getdef проверяет это. Если getword не возвращает алфавитный символ, значит, слово не могло быть определено и программа печатает его. В противном случае программа ищет возможное определение слова. Коща такое определение есть, функция ungets (упражнение 4.7) возвращает слово в обратном порядке во входной поток. Функция getdef обрабатывает директивы: #define имя определение #undef имя Ожидается, что имя является алфавитно-цифровым. В директиве #define цикл for 0 - 0; i < MAXWORD-1; R+) if ((def[i] -getehO) —EOF 11 def[i] —»W) break; принимает определение, пока не найдет конец строки или конец файла. Если определение есть, getdef вставляет его в таблицу, используя функцию install (c.141 KP). Директива undef удаляет имя из таблицы (упражнение 6.5). Мы изменили функцию getword, чтобы она возвращала пробелы, так что вывод напоминает входные данные.
Глава 7 Ввод и вывод Упражнение 7.1 (сЛ49 КР) Напишите программу, которая преобразует заглавные буквы в строчные или строчные в заглавные в зависимости от имени, которым она вызвана, находящегося в argv[0 ]. ♦include < stdio.h > ♦include < string.h > ♦include <ctype.h> /* lower: преобразует заглавные в строчные */ /* upper преобразует строчные в заглавные */ mainGnt argc, char *argv[]) { intc; if <strcmp(argv[0], "lower") —0)- while ((c-getchar()) !-EOF) putchar(1olower (c)); else while ((c-getchar()) HEOF) putchar (toupper(c)); return 0; } Если программа вызывается именем lower, она преобразует заглавные буквы в строчные. В противном случае — строчные в заглавные. Функция strcmp возвращает нуль, когда argv[0] представляет собой строку lower. Оператор if (strcmp(argv[0], "lower") — 0) работает в системе UNIX, так как argv [0 ] — имя программы в том виде, в котором напечатал его пользователь. В некоторых операционных системах argv[0] — полный путь местонахождения программы, а не то, что вводил пользователь. Программа использует функции tolower и toupper из < ctype.h >. 121
Упражнение 7.2 (сЛ51 КР) Напишите программу, которая печатает произвольный ввод разумным образом. Как минимум, она должна печатать неграфические символы в восьмеричном или шест- надцатеричном виде (как принято на конкретной машине) и разрезать длинные строки текста. #include<8tdio.h> ♦include < ctype.h > #define MAXUNE 100 /• максимальное количество символов */ /* в одной строке •/ ♦define OCTLEN 6 /* длина восьмеричного значения */ /* печатать произвольный ввод разумным образом V main О { int с, рое; intincOntpos, intn); рое - 0; /* позиция в строке */ while ((c-getcharO) !-EOF) if (tocntrKc):: с — • '){ /* неграфический символ или пробел */ рое - inc(pos. OCTLEN); printfC\\%03oa,c); if (с - - *W) { /* символ новой строки */ pos - 0; putcharfW); } else { /* графический символ */ pos-inc(pos, 1); putchar(c); } return 0; } /♦ inc: увеличить счетчик позиции вывода */ int incdnt pos, int n) { if (pos + n< MAXUNE) return pos+n; ebe{ putcharCNn*); return n; } } Длина строки вывода — MAXLINE. Макроопределение iscntrl сделано в <ctype.h>. Макроопределение iscntrl нахо- 122
дит неграфические символы: символ удаления (0177 восьмеричное) и обычные управляющие символы (коды меньше, чем 040 восьмеричное). Разделители тоже считаются неграфическими символами. Неграфические символы печатаются в восьмеричной форме (числу предшествует пробел и \, за числом также следует пробел) с использованием OCTLEN позиций. Символ новой строки сбрасывает переменную pos: if(c--'W){ pos-0; puteharCW); } Функция inc возвращает последнюю использованную позицию и переносит строку, если нет п знакомест, доступных для вывода. Упражнение 7.3 (с.152 КР) Переделайте функцию minprintf, чтобы она обрабатывала больше возможностей функции printf. ♦include < stdio.h > ♦include < stdarg.h > ♦include <ctype.h> ♦define LOCALFMT100 /* minprintf: мини-printf с переменным списком аргументов V void minprintf (char *fmt,...) va Jist ар; /* указывает на каждый аргумент без имени */ char *p, *sval; char localfmt[LOCALFMT]; inti, ival; unsigned uval; double dval; va_8tart(ap, fmt); /* устанавливаем ар на первый аргумент без имени*/ for(p-fmt;*p;p++){ if <*p !-'%'>< putchar(*p); continue; } i-0; tocalf mt [KH-] -*%•;/* начинаем локальный фермат */ while <*(p+l) && !isalpha<*(p+l))) localfmt[i++] - *Kp; /* отбираем символы */ localfmt[i++] - *<p+i); /* буква формата */ localfmtM-'VO'; . switch (*++p) { /♦ буква формата */ case'd': case Г: 125
ival-va arg(ap,int); printf (localfmt, ival); break; case V: case'X': case V: caseV: uval-va arg(ap, unsigned); printf (localfmt, uval); break; caseT: dval - va_arg(ap, double); printf (localfmt, dval); break; case's': sval - va_arg(ap, char •); printf (localfmt, sval); break; } va end(ap); /* заканчиваем •/ > Функция minprintf движется по списку аргументов, а printf осуществляет печать для поддерживаемых возможностей. Чтобы обрабатывать больше возможностей функции printf, мы собираем в массиве localfmt символ % и любые другие символы до алфавитного — буквы формата. Строка localfmt — аргумент формата для printf. Упражнение 7.4 (сЛ55 КР) Напишите собственную версию функции scanf, аналогичную minprintf из предыдущего раздела. ♦include <stdio.h> ♦include < stdarg.h > ♦include < ctype.h > ♦define LOCALFMT 100 /* minscanf: мини-scanf с переменным списком аргументов */ void minscanf (char *fmt,...) va Jist ар; /* указывает на каждый аргумент без имени */ char *p, *sval; char localfmt [LOCALFMT]; intc,i,*ival; unsigned *uval; double *dval; i - 0; /* индекс для массива localfmt */ va_start(ap, fmt); /* устанавливаем ар на первый аргумент без имени*/ 124
for(p-fmt;*p;P++){ if (*p !-•%*) < localfmt[i++] - *p; /* отбираем символы */ continue; } ' localfmt[i++] -*%'; /* начинаем формат */ while (*<p+l) && !iaalpha(*(p+l))) localfmt[t++] - *++p; /* отбираем символы •/ localfmt[H+] - *<p+l); /* буква формата */ localfmt[i]-'\<r; switeh(*++p) { /* буква формата •/ case'd': caseV: ival - va_arg(ap, int *); scanf (iocalfmt, ival); break; case V: case'X': case V: 'о': uval - va_arg(ap, unsigned *); scanf (Iocalfmt, uval); break; caseT: dval - va_arg(ap, double *); scanf (Iocalfmt, dval); break; case's': aval - va_arg(ap, char *); scanf (localfmt, sval); break; i - 0; /• сбрасываем индекс */ } va_end(ap); /• заканчиваем */ } Функция minscanf аналогична minprintf. Эта функция берет символы из строки формата, пока не найдет алфавитный символ после %. Это и будет localfmt, передаваемый scanf вместе с соответствующим указателем. Аргументы scanf являются указателями: указатель на строку формата и на переменную, которая получает значение от scanf. Мы используем va_arg, чтобы получить значение указателя, копируем его в локальный указатель и вызываем scanf. Затем scanf считывает значение в переменную пользователя. Упражнение 7.5 (с. 155 КР) Переделайте постфиксный калькулятор из гл. 4 с использованием scanf и/или sscanf для ввода и преобразования чисел. 125
♦include <stdio.h> ♦include < ctype.h > #define NUMBER '0' /* признак того, что было найдено число */ /* getop: получить следующий знак операции или числовой операнд */ int getop (char s[]) { int с, i, re; staticcharlas1e[]-a,,; sscanf(laste,a%cw,&c); lastc [0] - • •; /• стираем последний символ */ while ((s[0] -с) — "II с — V) if (scanf C%cH, *c) —EOF) с-EOF; ■ Ш-ЛСГ; if (!isdigit(c) &<Src!- .») return с; /* не число */ i-0; if (isdigit(c)) /* отбираем целую часть */ do{ re - scanf Г%сн, &c); if (!isdigit(s[-H-i]-c)) break; }while (re!-EOF); if (c - - V) /* отбираем дробную часть */ do{ rc-scanf(H%cH,&c); if (!isdigit(s[4-Hl-c)) break; }while (re!-EOF); s[i]-'\0'; if (re!-EOF) lastc [0]-c; return NUMBER; } Функция getop (c.80 KP) — единственная измененная подпрограмма. Кое-что нужно помнить между вызовами getop — это символ, следующий за числом. Массив lastc — статический, из двух элементов, запоминающий последний считанный символ (функции sscanf требуется символьная строка). Вызов sscanf(laste,a%cw,£c) считывает символ, помещенный в lastc [0]. Вы могли бы использовать с-lastc [0] вместо вызова sscanf. Функция scanf возвращает количество успешно под- 126
ошедших и присвоенных пунктов ввода (с. 153 КР). По концу файла она возвращает EOF. Мы также изменили выражение isdigit(s[4-H] -c-gelch()) rc-8canf(H%cH, &c); if (!isdigit(s[-H-i]-c)) break; так как нам нужно вызвать scanf, добавить символ в строку s, а затем проверить на цифру. Вполне возможно, что scanf обнаружила EOF и, следовательно, не изменила переменную с. Именно поэтому мы проверяем условие rc!-EOF Функция scanf не помогает улучшить исходную getop, если мы считываем с помощью scanf один символ за обращение. Вот другое возможное решение: ♦include < stdio.h > ♦include < ctype.h > ♦define NUMBER '0' /* признак того, что было найдено число */ /* getop: получить следующий знак операции или числовой операнд */ int getop (char s[]) { int с, гс; float f; while ((rc-scanfC%cw, &c)) !-EOF) 1М(8[0]-с)!-"**с!-'\О break; s[l]-'\0'; if (re — EOF) return EOF; else if (!isdigit(c)*&c!-V) return с; /* не число */ ungetc(c, stdin); scanf(HO/or,&f); sprintf(s,a%r,f); return NUMBER; } Мы считываем по символу за обращение, пока не обнаружим символ, не являющийся ни пробелом, ни табуляцией. Этот цикл также может завершиться по концу файла. Если символ — цифра или десятичная точка, возвращаем ее на ввод, используя библиотечную функцию ungetc. Затем считываем число. Поскольку getop возвращает чис- 127
ло как значение с плавающей точкой, мы используем sprintf, чтобы преобразовать значение f в символьную строку в массиве s. Упражнение 7.6 (с. 161КР) Напишите программу сравнения двух файлов, печатающую первую строку, в которой они отличаются. ♦include < stdio.h > #include<stdlib.h> ♦include < string.h > ♦define MAXLINE 100 /* comp: сравнить два файла, напечатать первую строку, */ /* в которой они различаются */ main(int argc, char *argv[]) { FILE*fpl,*fp2; void filecomp(FILE *fpl, FILE *fp2); if (argc!- 3) { /* неверное количество аргументов? */ fprintf (stderr, "comp: нужно указать два имени файлов\п"); exit(l); }etoe{ if ((fpl -fopen(*+4argv, V» —NULL) { fprintf (stderr, "comp: не могу открыть %s\n*\ *argv); exit(l); }elself ((fp2-fopen(*++argv, "г"» —NULL) { fprintf (stderr, "comp: не могу открыть %s\nw, *argv); exit(l); } else { /* файлы найдены и открыты */ filecomp(fpl,fp2>; fclose(fpl); fclose(fp2); exit(O); } } } /* filecomp: сравнить два файла построчно */ void filecomp (FILE *fpl, FILE *fp2) { char linel [MAXLINE], line2 [MAXLINE]; char*lpl,*lp2; do{ lpl -fgetsdinel, MAXLINE, fpl); lp2 - fgets(line2, MAXUNE, fp2); if (lpl —linel &&lp2--line2) { if (strcmpdinel, Hne2)!- 0) { printf ("Первое отличие в строке\п%s\nH, linel); lpl-Ip2-NULL; } } else if (lpl!- linel && lp2 — Hne2) 128
printf ("Конец первого файла в строке\п%8\пн, 11пе2); else if dpi — linel && 1р2!- Iine2) printf ("Конец второго файла в crpoice\n%s\n", linel); } while dpi —linel &&lp2— Iine2); } Число аргументов должно быть равно трем: имя программы и два имени файлов. Программа открывает файлы, и filecomp сравнивает их построчно. Функция filecomp считывает по строке из обоих файлов. Функция fgets возвращает указатель на прочитанную строку или NULL, когда встречает конец файла. Если lpl и 1р2 указывают на соответствующие им строки, значит, файл не кончился и linecomp сравнивает две строки. Если строки не совпадают, filecomp печатает строку, в которой отличаются файлы. Если lpl или 1р2 не указывают на соответствующие им строки, то один из файлов закончился (EOF) и файлы отличаются. Если ни lpl, ни 1р2 на строки не указывают, то оба файла закончились (EOF) и не отличаются. Упражнение 7.7 (с.161 КР) Модифицируйте программу поиска по образцу из гл. 5 так, чтобы брать входные данные из указанных файлов или, если не указано файлов-аргументов, из стандартного ввода. Должно ли печататься имя файла, если найдена подходящая строка? ♦include < stdio.h > ♦include < string.h > ♦include < stdlib.h > ♦define MAXUNE 1000 /* максимальная длина входной строки */ /* find: печатает строки, в которых содержится */ /* образец из первого аргумента */ main (int argc, char *argv []) { char pattern [MAXUNE]; int c, except - 0, number - 0; FILE *fp; void fpat(FILE *fp, char *fname, char 'pattern, int except, int number); while (—argc > 0 && (*++argv) [0] —'-') while <c-»+4argv[0]) switch (c) { case V: except-1; break; 5-752 129
case V: number-1; break; default: printf ("find: неизвестный ключ %с\п", с); argc-O; break; } if(arfc>-l) strcpy (pattern, *argv); else{ printf ("Вызов: find [-x] [-n] образец [файл ...]\nN); exit(l); if (argc --1) Л считываем со стандартного ввода */ fpat(stdin,ttW, pattern, except, number); else while (- -argc 0) /• получаем имя файла */ if ((fp-fopen(*+4ar£v, V)X — NULL) { fprintf(stderr, "find: не могу открыть %s\n", *argv); exit(l); } else { /* файл открыт */ fpat(fp, *argv, pattern, except, number); fcloee(fp); } return 0; } /* fpat: ищет образец в строке */ void fpat(FILE *fp, char *fhame, char «pattern, int except, int number) { char line [MAXUNE]; longlineno-0; while (fgetsdine, MAXUNE, fp) I-NULL) { ++Uneno; , if ((strstrOine, pattern) Ь-NULL) Ь-except) { if (*fname) Л есть имя файла •/ printfC%s--a.niame); if (number) Л печатать номер строки */ printfC%ld: Mineno); printf r%tMme); } } } Функция main обрабатывает необязательные аргументы, как в гл. 5 (с.113 КР). Кроме того, она ожидает по крайней мере еще один аргумент-образец. Если за образцом не следуют имена файлов, программа использует стандартный ввод. Если следуют, она открывает указанный файл. В том и другом случае программа вызывает fpat. 130
Текст большей части функции fpat похож на текст main. Функция считывает строку за обращение, пока fgets (с. 160 КР) не вернет NULL Функция fpat ищет заданный образец в каждой строке. Результаты могут быть: (stretrdine, pattern)!- NULL) !- 0 (образец не найден) !- 1 (образец найден) !- 0 (образец не найден) !- 1 (образец найден) !- ехсерИкроме) 0 (не указано) 0 (не указано) 1 (указано) I (указано) результат ложь истина истина ложь Когда результат этого выражения истинен, fpat печатает имя файла (если это не стандартный ввод), при необходимости — номер строки и саму строку. Упражнение 7.8 (с.161 КР) Напишите программу, которая печатает набор файлов, начиная каждый на новой странице, с заголовком и счетчиком страниц для каждого файла. #include < stdio.h > #include < stdlib.h > #define MAXBOT 3 Л максимальное число строк внизу страницы •/ #define MAXHDR 5 /* максимальное число строк вверху страницы */ #define MAXTJNE 100 /* максимальный размер одной строки */ #define MAXPAGE 66 /* максимальное число строк на странице */ /* print: печатать файлы, каждый следующий—на новой странице */ main (int argc, char *argv[]) { FILE*fp; void fileprint<FILE *fp, char *faame); if (argc - -1) /* аргументов нет; печатаем стандартный ввод */ fileprint(stdin, un); else /* печатаем файл (ы) */ while (—argc>0) if «fp-fopen(*++argv, V)) —NULL) { fprintf (stderr, "print: не могу открыть %s\nw, *argv); exit(l); }ebe{ fileprint(fp, *argv); fclose(rp); } return 0; } /* fileprint: печатать файл fname */ void fileprint(FILE *fp, char *fname) { int lineno, pageno -1; char line [MAXUNE]; int heading (char *fname, int pageno); 13b
lineno - heading (fname, pageno*-»-); while (fgetedine, MAXLINE, fp)!- NULL) { if (lineno—1){ fprintf(stdout, -\Г); lineno - heading(fname( pageno++); fputsdine^stdout); if (++Uneno < MAXPAGE — MAXBOT) lineno-1; fprintf (sldout, a\D; /* прогон страницы после файла */ « } ' /• heading: печатает заголовок и достаточно пустых строк */ int heading(char *fname, int pageno) { int In-3; fprintf (stdout, a\nV); fprintf(stdout, M%sстраница %d\n*\ fname, pageno); while (ln++< MAXHDR) fprintfCstdout, "\nw); return In; } Эта программа аналогична cat (с. 155 KP). функция fileprint принимает два аргумента: указатель на открытый файл и указатель на имя файла (на пустую строку, когда файл является стандартным вводом). Функция считывает и печатает строки. Символ \f — подача бланка. Переменная lineno подсчитывает число строк на странице. Длина страницы — MAXPAGE. Когда lineno равна 1, fileprint печатает символ подачи бланка, новый заголовок и переустанавливает lineno. Мы также печатаем символ подачу бланка в конце последней страницы каждого файла. функция heading печатает имя файла и номер страницы, затем столько символов новой строки, чтобы вверху страницы было MAXHDR строк. МАХВОТ — количество пустых строк внизу страницы. Упражнение 7.9 (с.163 КР) функции, подобные isupper, могут быть реализованы так, чтобы они экономили место в памяти или время. Исследуйте обе возможности. /* isupper: возвращает 1 (истину), если с — заглавная буква */ int isupper (char с) { If (c>-,A,*Ac<-^Z»)l 132
return 1; else return 0; } Эта версия isupper — простая конструкция if-else, которая проверяет символ. Если он находится в диапазоне заглавных букв в кодах ASCII, она возвращает 1 (истину), иначе 0 (ложь). Эта версия экономит память. ♦define isupperCc) «с)>- 'А* && (с)<- *Т) ? 1:0, Эта версия isupper экономит время, так как не тратит время на вызов функции, но использует больше места, истому что макроопределение расширяется в каждой строке, где оно задается. Другое, о чем нужно помнить, — возможная проблема, если аргумент вычисляется дважды. Например, char *p - "This is a string"; if (isupper (*p++)) макроопределение расширяется в выражение ((*р+0 >- 'А' && (*р++) <- »Г) ? 1:0, которое в зависимости от значения *р увеличит указатель р дважды. Заметьте, что этого второго увеличения не произойдет, когда isupper является функцией, потому что аргумент функции вычисляется один раз. Обычно это неожиданное второе увеличение указателя р ведет к неправильным результатам. Одно из возможных решений: char *p - "This is a string"; if (isupper(*p)) p++; Нужно быть осторожными с макроопределениями, которые могут вычислить аргумент более одного раза. Пример^** являются макроопределения toupper и tolower в < ctype.h >. 133
Глава 8 Интерфейс системы UNIX Упражнение 8.1 (сЛ69 КР) Перепишите программу cat из гл. 7 с использованием read, write, open и close вместо их эквивалентов из стандартной библиотеки. Проведите эксперименты по определению относительных скоростей двух версий. ♦include < stdio.h > #include< fcntl.h > #inchide "syscaUs.lT void error (char *fmt,...); /* cat: сцепить файлы - read / write / open / close */ main (int argc, char *argv[]) { intfd; void filecopy (int ifd, int ofd); if (argc - -1) /* аргументов нет; копируем стандартный ввод V filecopyCO, l); else while (—argc 0) if ((fd-open(*++argv, 0_RDONLY)) 1) error ("cat: не могу открыть %sH, *argv); else{ filecopy(fd, i); close(fd); } return 0; } /* filecopy: скопировать файл ifd в файл ofd */ void filecopy (int ifd, int ofd) { intn; char buf [BUFSIZ]; while ((n - read (ifd, buf, BUFSIZ)) > 0) if (write (ofd, buf, n)!- n) error ("cat: ошибка записи"); 134
Оператор if «fd-open(*++argv, O.RDONLY)) 1) открывает файл для чтения и возвращает дескриптор файла (целое число); если произошла ошибка» возвращает -1. Функция filecopy считывает BUFSIZ символов» используя дескриптор ifd. Функция read возвращает счетчик байтов действительно прочитанных символов. Пока этот счетчик больше 0, ошибок нет; 0 означает конец файла, а -1 — ошибку. Функция write пишет п байтов, в противном случае произошла ошибка. Функция error — со с.168 КР. Эта версия работает примерно в два раза быстрее, чем старая версия в гл. 7 КР. Упражнение 8.2 (с.172 КР) Перепишите fopen и _fillbuf с полями вместо явных битовых операций. Сравните размеры программ и скорость выполнения. #include<fcntl.h> #includett8yscalls.h" #definePERMS 0666 /* чтение/запись для владельца, группы, прочих */ /* fopen: открыть файл, вернуть файловый указатель */ FILE *fopen (char *name, char *mode) { intfd; FlLE*fp; lf(*mode!-V&&*modc?-V&A*mode'-V) return NULL; for(fp- iob;fp< lob+OPEN MAX;fp++> if (fp^>flag.is_read --0&& fp->flag.is_write ~ 0) break; /* найдено свободное "гнездо" •/ If (fp > - Job+OPENJIAX) return NULL; /* свободных мест нет */ if <*mode - - W) /• создать файл */ fd - creat(name( PERMS); else if (♦mode — V){ if ((fd -openGiame, OJVRONLY, 0)) 1) fd - creat(name, PERMS); beek(fd,0L,2); }else f d - open (name, 0_RDONLY, 0); if (fd - - -1) /* не удалось открыть файл name */ return NULL; fp->fd-fd; fp->cnt-0; fp->baae-NULL; fp->flag.is__unbuf - 0; 135
fp->flag.is_buf-l; fp->flagvis_eof-0; fp->flag.is_eiT - 0; if (*mode — V) { /* чтение •/ fp->flag.is_read -1; fp->flag.is_write - 0; } else {/* запись*/ fp->flag.is_read - 0; fp->flag.is_write -1; } return fp; /* _fillbuf: выделить и заполнить буфер ввода */ int Jillbuf (FILE *fp) { Int bufsize; if (fp->flag.is_read— 0 11 fp->flag.is_eof--l 11 fp->flag.is_err - -1) return EOF; bufsize- (fp->flag.is_unbuf— 1) ? 1: BUFSIZ; if (fp->base - - NULL) /* буфера еще нет */ if ((fp->base- (char *)maUoc(bufsize)) —NULL) return EOF; /* невозможно выделить память */ fp->ptr - fp->base; fp->cnt - read(fp->fd, fp->ptr, bufsize); if (—fp->cnt<0){ if (fp->cnt-—1) fp->flag.is_eof -1; else "" fp->flag.is_err-l; fp->cnt-0; return EOF] return (unsigned char) *fp->ptr++; Декларатор typedef для struct _iobuf появляется на с. 170 КР. Одним из элементов _iobuf является int flag; Переменная flag переопределена в терминах битовых полей: struct flagjield { unsigned is_read: 1; unsigned is_write: 1; unsigned is_unbuf: 1; unsigned isjwif: 1; unsigned is_eof: 1; unsigned is err: 1;
В операторе if ((fp->flag & (JREAD : JNRTTE)) — 0) break; над значениями _READ и _WRITE делается ИЛИ: (_READ:JWRITE) 01 I 02 восьмеричное 01 I 10 двоичное 11 результат Это означает, что оператор if истинен, коща оба младших бита flag сброшены (ни чтение, ни запись). Оператор if проверяет, что элемент _iob не используется для чтения или записи. Битовые поля явно проверяют условие: if (fp->flag.i8_read — 0&& fp->flag.is_write - - 0) break; Следующая модификация явно устанавливает биты: fp->flag.is_unbuf - 0; fp->flag.i8jHif-l; fp->flag.is_eof-0; fp— >flag.is_err - 0; Далее fp->flag - (♦mode - - V) ? _READ: JWRTTE; устанавливает flag согласно mode. Если это V, оператор делает flag равным JREAD, иначе _WRITE. С битовыми полями, если режим — V, бит is read уста- навливаетсяв 1. Если нет, устанавливается в 1бит is_write: if(*mode — V){ fp->flag.is_read -1; fp->flag.iSLwrite - 0; }etee{ fp->flag.is_read - 0; fp->flag.ie_wrile -1; } Функция _fillbuf изменяется аналогично. Функция fillbuf возвращает EOF в следующих ситуациях: файл не был открыт для чтения, встретился конец файла или была обнаружена ошибка. if ((fp->flag & ( JREAD: _EOF: _ERR)) !- JREAD) Это условие проверяется с помощью битовых полей: if (fp->flag.is_read — 0 11 fp->flag.is_eof—1 II fp->flag.is_err - -1) 137
Далее bufsize - (fp->nag & JJHBXJF) ? 1: BUFSIZ; меняется на bufsize- (fp->flag.is__unbuf--1) ? 1: BUFSIZ; a fp->f!ag:-_EOF; else fp->flag:-_ERR; становится fp->flag.is_eof-l; else fp->flag.is_err -1; Размер программы измененной функции был больше, и функции работали медленнее. Битовые поля машинно-зависимы и могут замедлить выполнение программы. Упражнение 8.3 (с.173 КР) Разработайте и напишите функции _flushbuf, fflush и fclose. ♦include "syscatls.ir i4 /* _flushbuf: выделяет или сбрасывает выходной буфер */ Int _flushbuf (int x, FILE *fp) { unsigned nc; /* количество сбрасываемых символов V int bufsize; /* размер выделяемого буфера V if (fp < Job:: fp >- Job + OPENJIAX) return EOF; /* ошибка: неверный указатель V if <(fp->f1ag * ( JtfRITE: .ERR)) !-JJVRITE) return EOF; bufsize- (fp->flag*_UNBUF) ? 1: BUFSIZ; if (fp->base--NULL) { /* буфера еще нет */ if «fp->base- (char ♦) ma lloc (bufsize)) —NULL) { fp->flag:-JBRR; return EOF; /* невозможно выделить память */ } else {•/* буфер уже существует •/ nc - fp->ptr - fp->base; if (write(fp->fd, fp->base, nc)!- he) { fp->flag:-_ERR; return EOF; /* ошибка, возвращаем EOF */ } fp->ptr - fp->base; /* начало буфера */ *fp->ptrf+ - (char) x; /* сохраняем текущий символ */ fp->cnt - bufsize -1; 138
return x; } /* fclose: закрыть файл •/ intfck»e(FILE*fp) int re; /* код возврата */ if ( (re-fflush (fp)) !-EOF) { /• что-то нужно сбросить? */ free(fp->base); /* освобождаем выделенный буфер */ fp->ptr-NULL; fjr-Wmt-в; fp->base-NULL; fp->flag6—<_READ:_WRITE); } return re; } /* fflush: сбросить буфер, соотгРтстьующий файлу fo */ lntffhish<FILE*fr.v. J - "" intrc-O; if <fp< Job:: fp>-Job + OPEN_MAX> return EOF; /• ошибка: неверный указатель •/ *f<fp->flag*_WRITE) rc-_flushbuf(0,fp); fp->ptr - fp->base; fp->cnt - (fp->flag * _UNBUF) ? 1: BUFSIZ; return re; } Функция jRushbuf возвращает EOF, если файл не был открыт для записи или произошла ошибка: if <(fp->flag & ( _WRITE: _ERR))!- _WMTE) return EOF; Если буфера еще нет, он выделяется, как в _fillbuf (с. 172 КР). Если буфер существует, его символы сбрасываются в поток. Следующий шаг — сохранить аргумент в буфере: *fp->ptH-f-(char)x; При этом число возможных символов в буфере (fp->cnt) на один меньше размера буфера из-за только что сохраненного символа. Функция fclose вызывает fflush. Если файл был открыт для записи, может появиться необходимость сбросить некоторое количество символов. Функция fclose переустанавливает элементы структуры jtobuf так, что fopen не может наткнуться на бессмысленные значения на свободном месте. Код возврата — 0, если нет ошибок. Функция fflush проверяет корректность файлового ук#- 139
зателя и вызывает _flushbuf, если файл был открыт для записи. Затем ffhish переустанавливает ptr и cnt и возвращает гс. у Упражнение 8.4 (с Л 73 КР) Функция стандартной библиотеки int fseek(FILE *fp, long offset, int origin) идентична lseek, за исключением того, что fp — файловый указатель вместо дескриптора, а возвращаемое значе- х::? — статус типа int, а не позиция. Напишите fseek. Убедитесь, чти ваша fseek должным образом согласуется с буферизацией, организованной для других функций стандартной библиотеки. ♦include "8yscalls.hw /* lseek: произвольный доступ с использованием файлового указателя •/ int faeek(FILE *fp, long onset, int origin) unsigned nc; /* количество сбрасываемых символов */ long re - 0; /• код возврата V if(fp->flag&_READ){ if (origin - -1) /* от текущей позиции? */ onset—fp->cnt; /* помним о символах в буфере */ гс - lseek(fp->fd, onset, origin); fp->int - 0; /* в буфере нет символов */ } else if (fp->flag * JVRTTE) { if ((nc-fp->ptr-fp->base) >0) if <write(fp->fd, fp->base, nc)!- nc) ГС--1; if (re!- -1) /* ошибок еще нет? */ гс - teeek(fp->fdf offset, origin); } return (re---1) ?-1:0; } Переменная с содержит код возврата. Бели происходит ошибка» она устанавливается в -1. В функции fseek бывает две ситуации: файл открыт для чтения или файл открыт для записи. Когда файл открыт для чтения и origin равна 1, смещение (offset) отсчитывается от текущей позиции (другие случаи: origin 0, смещение отсчитывается от начала файла; origin 2, смещение отсчитывается от конца файла). Чтобы отмерить смещение от текущей позиции, fseek учитывает символы, уже находящиеся в буфере: 140
if (origin —1) offset--fp->cnt; Затем fseek вызывает lseek и удаляет символы в буфере: re - lseek(fp->fd, offset, origin); fp-Xmt-0; Коща файл открыт для записи, fseek сначала сбрасывает символы из буфера, если они есть: if <<nc-fp->ptr-fp->base) >0) if (write (fp->fd, fp->base, nc)!- nc) ic—1; Если ошибок нет, fseek вызывает lseek: if(rc!—1) re - lseek(fp->fd, offset, origin); Функция fseek возвращает О для правильных перемещений. Упражнение 8.5 (сЛ 78 КР) Измените программу fsize, чтобы она печатала и другую информацию, содержащуюся в inode. ' #include<stdio.h> #include<striBg.h> #include <fcntl.h> /• флаги для функций read и write*/ #include <eys/types.h> /• определения типов (typedef) */ #inciude <eys/stath> /* струпура/возвращаемая функцией stat */ #indudeMo4renth" int stat(char *, struct stat *); void dirwalk(char •, void (*fcn) (char*»; /• fsize: распечатать номер inode-узла, режим доступа, ссылки */ , /• и размер файла паше */ void feizeCchar *name) { struct stat stbuf; if (statdiame, ftstbuf) -—1) { fprintfCstderr, "fsize: доступ к файлу %s невозможенW\ name); return; if ((sttaf.st_mode & SJFMT) ~ SJFDffi) dirwalk(name, fsize); printf C%5u &6o %3u %81d %s\nw, stbuf.stjno, stbuf .st_mode, stbuf.st_iilink, stbuf .stjrize, name); Мы изменили fsize так, чтобы она печатала номер inode, режим доступа к файлу в восьмеричном виде, коли- 141
чество ссылок на файл» размер и имя файла. Вы можете выбрать для распечатки больше информации — в зависимости от того, что для вас важно. Функция dirwalk появляется на с. 176 КР. Упражнение 8.6 (сЛ82 КР) Функция стандартной библиотеки calloc (n, size) возвращает указатель на п объектов размера size, память обнуляется. Напишите calloc, вызывая malloc или изменяя ее. ♦include "syscalls.h" /* calloc: выделить п объектов размера size */ void *calloc (unsigned n, unsigned size) { unsigned i, nb; char *p, *q; nb-n*size; if «p-q-maUoc(nb)) !-NULL) for(i-0;i<nb;i++) *P++-0; return q; } Функция calloc выделяет п объектов размера size. Общее число байтов, которое должно быть выделено, — nb: nb - n * size; Функция malloc возвращает указатель на область памяти в nb байтов. Указатели р и q запоминают начало этой выделенной области памяти. Если выделение было успешным, nb выделенных байтов инициализируются 0: for(i-0;i<nb;H+) *р++-0; calloc возвращает указатель на начало выделенной и инициализированной области памяти. Упражнение 8.7 (с. 182 КР) Функция malloc принимает требование размера без проверки его правдоподобия; функция free предполагает, что блок, который требуется освободить, содержит правильное поле размера. Улучшите эти функции, чтобы прилагалось больше усилий к проверке ошибок. ♦include "syscalls.h** #define MAXBYTES (unsigned) 10240 1в.
static unsigned maxalloc; /* максимальное число вьщеленных блоков */ static Header base; /* пустой список, чтобы начать •/ static Header *freep - NULL; /* начало списка свободных блоков V /* malloc: функция выделения памяти */ void *malloc (unsigned nbytes) { Header *p, *prevp; static Header *morescore (unsigned); unsigned nunits; if (nbytes > MAXBYTES) { /* не больше, чем MAXBYTES */ fprintf(stderr, "alloc: не могу выделить больше, чем %u байт\п", MAXBYTES); return NULL; nunits - (nbytes + sizeof (Header) -1) / sizeof (Header) +1; /*...*/ /* далее см. C.180KPV } #define NALLOC 1024 /* минимальное число блоков для запроса */ /* morescore: запросить у системы больше памяти */ static Header *morescore (unsigned) { char *cp, *sbrk(int); Header *up; if (nu< NALLOC) nu-NALLOC; cp - sbrk(nu * sizeof (Header)); If (cp—— (char *) -1) /* места нет совсем •/ return NULL; up - (Header *) cp; up->s.8ize-nu; maxalloc - (up->s.8ize > maxalloc) ? up->s.size: maxalloc; free((void*)(up+l)); return freep; } /* free: поместить блок ар в список свободных блоков •/ void free (void *ар) { Header *bp, *p; bp - (Header *) ap -1; /* указывает на заголовок блока */ if (bp->s.8ize - - 0:: bp->s.8ize > maxalloc) { fprintf (stderr, "free: не могу освободить %u байт\п", bp->s.8ize); return; } for (p - freep;! (bp > p && bp < p->s.ptr); p - p->s.ptr) /*...*/; /* далее см. С.182КР */
Функция malloc сравнивает требуемое число байтов с некоторой произвольной константой MAXBYTES. Выберите значение для MAXBYTES, которое лучше всего работает в вашей системе. Когда morescore выделяет новый блок, статическая переменная maxalloc запоминает размер самого большого используемого блока. Таким образом, функция free может проверить, что значение размера не 0 и не больше, чем самый большой выделенный блок. Упражнение 8.8 (с. 182 КР) Напишите функцию bfree(p,n), которая освободит произвольный блок р из п символов в список свободных блоков, поддерживаемый функциями malloc и free. Используя bfree, можно в любой момент добавить статический или внешний массив к списку свободных блоков. #include "syscalls.h" /* bfree: освободить произвольный блок р из п символов */ unsigned bfree (char *p, unsigned n) { Header *hp; if (n<sizeof (Header)) return 0; /* блок слишком мал для использования */ hp - (Header *) р; hp->s.size - n / sizeof (Header); free((void*)(hp+D); return hp->s.8ize; } Функция bfree принимает два аргумента: указатель р и количество символов п. Она освободит блок, если его размер по крайней мере sizeof (Header), иначе вернет 0. Указатель р преобразуется к типу Header и присваивается hp: hp - (Header *) р; Размер блока в единицах sizeof (Header): hp->s.8ize - n / sizeof (Header); Последний шаг вызывает функцию free. Так как free ожидает, что указатель установлен как раз за область заголовка, мы используем (hp+1) как morecore и преобразуем его к типу (void *). Функция bfree возвращает О, если блок слишком мал, иначе размер блока — в единицах sizeof (Header). 144
Предметный указатель А алфавитный порядок сортировки 92 аргументы командной строки 80, 81, 83, 84,86,109, 121, 127 аргументы scanf 125 Б беззнаковый символ 34 библиотечная функция bsearch 106,107 close 133 cos 58 exit 87,89,127 exp 58 fclose 128,131 _ fgets 130,131 fopen 127,131 fprintf 127,131 fputs 131 free 117,118 getchar 10 isalnum 94,108 isalpha 109,110,123,124 iscntrl 122 isdigit 55 islower 59 isprint 19 isspace 108 145
isupper 132 malloc 135,137,141 open 133,134 pow 58 read 133,135 scanf 125,126,127 sin 58 sprintf 102,127 sscanf 125,126 strcpy 129 strien 59 strstr 130 tolower 121 toupper 121 ungetc 127 va_arg 123,124 va_end 123,125 vajstart 123,124 write 133,134,137 бинарное дерево НО, 112 бинарный поиск 44 бинарный поиск, использование 114 битовое И операция 39, 42 битовое ИЛ И операция 39 битовое исключающее ИЛИ операция 40 битовое & операция 39,42 битовое ^ операция 40 битовое I операция 39 битовые поля 135 буфер односимвольный 63 В вектор аргументов, argv 80, 81, 83, 84, 86,109,121,127 вертикальная гистограмма 17 вложенные операторы switch 46 возвращение строки на ввод 62 возвращенный на ввод EOF 63 восьмеричная константа 8 восьмеричный вывод 122 вращение целого числа вправо 40, 41
г гистограмма 16,17 гистограмма вертикальная 17 гистограмма горизонтальная 16,18 гистограмма частот 18 граничное условие 15 Д двоично-дополнительная система счисления 42, 47 дерево, рекурсивный обход 111 диапазоны типов 34 директива #define 120 #undef 120 3 заголовочный файл <ctype.h> 19 <limits.h> 34 <math.h> 57 <stdarg.h> 122,124 <stdio.h> 11 <stdlib.h> 87 <string.h> 51 звуковой сигнал 8 <# И И битовая операция, & 39, 42 И логическая операция, && 35 ИЛИ битовая операция, I 39 ИЛИ исключающее битовая операция," 40 ИЛИ логическая операция, II 13,15 исключающее ИЛИ операция, " 40 К командная строка, аргументы 80,81,83,84,86,109, 121,127 147
л логическое И операция, && 35 логическое ИЛИ операция, II 13,15 логическое && операция 35 логическое 11 операция 13,15 М макроопределение abs 48,49 isupper 132 swap 68 tolower 133 toupper 133 массив указателей 106,115 H научная запись 52 неграфические символы, печать 122 обратный порядок сортировки 89 односимвольный буфер 63 оператор do 48,49 do-while 48,49 for 10 if 11 if-else 12,13,14 switch 46 switch, вложенный 46 while 8,9,11 операции битовые 35 операция битовое И, & 39,42 битовое ИЛИ, I 39 битовое исключающее ИЛИ, ~ 40 деления по модулю, % 41, 48,53 148
дополнения до 1, - 35,39,41,42 логическое И, && 35 логическое ИЛИ, 11 13,15 получения адреса, & 86 преобразования типа 35 сдвиг влево,« 39,40,41 сдвиг вправо,» 35,41 sizeof 106,114,142,143 П парные скобки 31 переворачивание символьной строки 24 перечислимый тип 36,100,104 печать неграфических символов 122 подача бланка, \f 132 подчеркивания символ, _ 68 позиция табуляции 25; 26 поиск бинарный 44 польская инверсная запись, калькулятор 80 преобразование строчных букв в заглавные 121 заглавных букв в строчные 43,121 ASCII в число с плавающей точкой 52 ASCII в целое число 74 преобразование типа, операция 35 пробелы "хвостовые" 23 программа калькулятор 53,55,60 обработчик #define 118 печати самой длинной строки 28 печати файлов 130 подсчета слов 14 поиска образца 128 преобразования температур 8,9,19 "складывания" длинных строк 27 сортировки 89,91,92,95 сравнения файлов 127 формирователь перекрестных ссылок 111 cat 133 detab 25 entab 26 149
expr 80 sort 89,91,92,95 tail 86 undcl 101 разделитель слов 15 рекурсивная функция itoa 66 рекурсивный обход дерева 111 самое правое вхождение строки 51 связанный список 112,113 сдвиг влево, операция,« 39, 40,41 сдвиг вправо, операция,» 35, 41 символ беззнаковый (unsigned char) 35 звукового сип*ала: \7 S :к*2Сй строки, \п 7,23,24,45 * нулевой, \0 24 обратной косой черты, W 13 подачи бланка, \t 132 подчеркивания, _ 68 со знаком (signed char) 35 табуляции, \t 13,25,26,45 управляющий, \ 8 шага назад, \Ь 13 символическая константа 11,12,26, 27, 29 синтаксис, элементарная проверка 31 системный вызов creat 134 lseek 134,139,140 stat 140 скобки парные 31 сортировка в алфавитном порядке 123,124 в обратном порядке 127 по полям 95 с совмещением 91 список аргументов переменной длины 123,124 стандартный ввод (stdin) 127 150
стандартный вывод (stdout) 131 стандартный вывод ошибок (stderr) 127 статические переменные 65,67 счетчик аргументов, argc 80,81,83,84,86,109,121, 127 тернарный оператор, ?: 43,132 типы, диапазоны 34 точка с запятой 7 удаление комментариев 29 указатели, массив 106,115 условное выражение ?: 43,132 Ф файлы, печать 130 функция библиотечная, см. библиотечная функция addln 113 addtreex 110,112 any 38 atof 52 atof, версия с указателями 76 btree 143 binsearch 44 bitcount 42 calloc 141 charcmp 92,93,97 clear 57 comment 109 compare 106,110 copy 21 day.crf.year 78 day of_year, версия с указателями 79 dd~99,104 151
dclspec 105 detab 84 dirdcl 99,104 echo_quote 30 entab 82 errmsg 100,105 error 87,119 escape 45 esettab 85 expand 47 exptab 28 fclose 138 fflush 138 filecomp 128 filecopy 133 fileprint 131 Jillbuf 135 findblnk 28 Jlushbuf 137 fopen 134 fpat 129 free 142 fseek 139 fsize 140 getch 62,63 getdef 119 getfloat 70 getint 69 getline, версия с указателями 73 getop, версия с указателями 76 gettoken 100 getword 108 heading 131 htoi 36 inc 122 invert 40 in_comment 30,32 in_quote 32 itoa 48,49 itoa, версия с указателявш 74 itoa, рекурсивная 66
itob 49 lalloc 113 lower 43 malloc 142 mathfnc 58 minprintf 122 minscanf 124 month_day 78 month_day, версия с указателями 79 morecore 142 newpos 28 nexttoken 102 noiseword 113 numcmp 97 parmdcl 105 printl 28 rcomment 29 readargc 96 readlines 77 remove 23 reverse 24, 67 reverse, версия с указателями 75 reverser 67 rightrot 41 search 31 setbits 39 settab 82 skipblanks 119 sortlist 116 squeeze 37 strcat, версия с указателями 71 strend 71 strindex, версия с указателями 75 strncat 72 strncmp 75 strncpy 72 strrindex 51 substr 98 tabpos 83 treestore 116 treexprint 111, 113 153
typequal 106 typespec 106 undef 117 unescape 45,46 ungetch 63 ungets 62 wordlength 41 writelines 90 X "хвостовые** пробелы 23 хэш-таблица 117 Ц цифры шестнадцатеричные 36 Ч частота встречаемости 18 частоты, гистограмма 18 числа с плавающей точкой, преобразование из ASCII 52 Ш шестнадцатеричное целое 48 шестнадцатеричные цифры 36 Э элементарный синтаксис, проверка 31 #define, директива 120 #undef, директива 120 % операция деления по модулю 41,48,53 & операция битовое И 39, 42 & операция получения адреса 86 && операция логическое И 35 « операция сдвиг влево 39, 40,41 » операция сдвиг вправо 35, 41 ?: условное выражение 43,132 \ управляющий символ 8 \0 нулевой символ 24 154
\7 символ звукового сигнала 8 \\ символ обратной косой черты 13 \Ь символ шага назад 13 \f символ подачи бланка 132 \п символ новой строки 7,23,24,45 \t символ табуляции 13,25, 26, 45 74 операция битовое исключающее ИЛИ 40 ^символ подчеркивания 68 JERR 136 _READ 136,137 .WRITE 136,137 _iobuf, struct 135 I операция битовое ИЛИ 39 II операция логическое ИЛИ 13,15 ~ операция дополнения до 1 35,39, 41, 42 abs, макроопределение 48, 49 argc, счетчик аргументе» 80,81,83,84,86,109,121, 127 argv, вектор аргументов 80, 81,83,84, 86,109,121,127 ASCII в число с плавающей точкой 52 В BUFSIZ 133 char, диапазон 34 creat, системный вызов 134 <ctype.h> 19 155
D #define, директива 120 do, оператор 48,49 do-while, оператор 48,49 E enum 36,100,104 EOF 10,11 EOF. возвращенный на ввод 63 ERR 137 FILE 127 for, оператор 10 I if, оператор 11 if-else, оператор 12,13,14 int, диапазон 34 _iobuf, struct 135 isupper, макроопределение 132 L <limits.h> 34 long, диапазон 34 lseek, системный вызов 134,139,140 M <math.h> 57 N NULL 110,112,113,116 О 0_RDONLY 133,134 OJWRONLY 134
R .READ 136,137 S scanf, аргументы 125 short, диапазон 34 signed char 63 sizeof, операция 106,114,142,143 stat, системный вызов 140 static, переменная 65,67 <stdarg.h> 122,124 stderr 127 stdin 127 <stdio.h> 11 <stdlib.h> 87 stdout 131 <string.h> 51 strucMobuf 135 swap, макроопределение 68 switch, оператор 45,46 switch, оператор вложенный 46 T tolower, макроопределение 133 toupper, макроопределение 133 U #undef, директива 120 unsigned char 63 W while, оператор 8,9,11 .WRITE 136,137 157
Оглавление Предисловие 5 Глава 1. Обучающее введение 7 Глава 2. Типы, операции и выражения 34 Глава 3. Управление 44 Глава 4. Функции и структура программы 51 Глава 5. Указатели и массивы 69 Глава 6. Структура 108 Глава 7. Ввод и вывод 121 Глава 8. Интерфейс системы UNIX 133 Предметный указатель 145
Тондо, Гимпел Язык СИ. Книга, ответов * Редактор Т.А. Петрова, ЛЛ. Табакова Редактор АИС Е.Ф. Тимохина Худож. редактор Ю.И. Аршюхов Техн. редактор Л.Г. Челышева Корректоры Т.М. Колпакова, Н.П. Сперанская Обложка художника В. А. Тогобщкого Репродуцируемый оригинал-макет изготовлен Т.М. Кудиновой, Е.Ф. Тимохиной ИБ№3126 Подписано в печать 8.02.94. Формат 84 х 108 1/32. Бумага офсетная. Гарнитура Тайме. Печать офсетная. Усл. п. л. 8,4. Уч.-изд. л. 7,99. Усл. кр.-отт. 8,65. Тираж 30 000 экз. Заказ 752 .Хп028. Издательство "Финансы и статистика" 101000, Москва, ул. Чернышевского, 7. Отпечатано в типографии издательства "Новости" 107005, Москва, ул. Ф. Энгельса, 46.
Вниманию читателей! Издательство "Финансы и статистика" выпустило в свет новую литературу по вычислительной технике: Липаев В.В. Управление разработкой программных средств: методы, стандарты, технология Макетирование, проектирование и реализация диалоговых информационных систем / Л.И. Гуков, Б.И. Ло- мако, А.В. Морозов и др. Канатников А.Н., Ткачев СБ. Программирование в среде Clipper. Версия 5.0 и особенности версии 5.01. От Си К C++ / Е.Н. Козелл, Т.В. Русс, С.Г. Свитковский и др. Мишенин А.И. Теория экономических информационных систем: Учебник. Экономическая информатика и вычислительная техника: Учебник / А.Ю. Королев, Н.Г. Черняк, Г.А. Тито- ренко и др. Фигурнов В.Э. IBM PC для пользователей. Издание четвертое, переработанное и дополненное