Язык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents
Скачать 2.33 Mb.
|
Упражнение 1.11. Как протестировать программу подсчета слов? Какой ввод вероятнее всего обнаружит ошибки, если они были допущены? Упражнение 1.12. Напишите программу, которая печатает содержимое своего ввода, помещая по одному слову на каждой строке. 1.6. Массивы А теперь напишем программу, подсчитывающую по отдельности каждую цифру, символы-разделители (пробелы, табуляции и новые-строки) и все другие символы. Это несколько искусственная программа, но она позволит нам в одном примере продемонстрировать еще несколько возможностей языка Си. Имеется двенадцать категорий вводимых символов. Удобно все десять счетчиков цифр хранить в массиве, а не в виде десяти отдельных переменных. Вот один из вариантов этой программы: #include /* подсчет цифр, символов-разделителей и прочих символов */ main() { int с, i, nwhite, nother; int ndigit[10]; nwhite = nother = 0; for (i = 0; i < 10; ++i) ndigit[i]= 0; while ((c = getchar()) != EOF) if (c >= '0' && с <= '9' ) ++ndigit[c - '0' ]; else if (c == ' ' || с == '\n' || с == '\t') ++nwhite; else ++nother; printf ("цифры ="); for (i=0; i < 10; ++i) printf(" %d", ndigit[i]); printf (", символы-разделители = %d, прочие = %d\n", nwhite, nother); } В результате выполнения этой программы будет напечатан следующий результат: цифры = 9 3 0 0 0 0 0 0 0 1 , символы-разделители = 123, прочие = 345 Объявление int ndigit[10]; объявляет ndigit массивом из 10 значений типа int . В Си элементы массива всегда нумеруются начиная с нуля, так что элементами этого массива будут ndigit[0] , ndigit[1] ,…, ndigit[9] , что учитывается в for -циклах (при инициализации и печати массива). Индексом может быть любое целое выражение, образуемое целыми переменными (например, 1) и целыми константами. Приведенная программа опирается на определенные свойства кодировки цифр. Например, проверка if (с >= '0' && с <= '9' ) ... определяет, является ли находящийся в c символ цифрой. Если это так, то c - '0' есть числовое значение цифры. Сказанное справедливо только в том случае, если для ряда значений '0' , '1' ,..., '9' каждое следующее значение на 1 больше предыдущего. К счастью, это правило соблюдается во всех наборах символов. По определению, значения типа char являются просто малыми целыми, так что переменные и константы типа char в арифметических выражениях идентичны значениям типа int . Это и естественно, и удобно; например, c - '0' есть целое выражение с возможными значениями от 0 до 9, которые соответствуют символам от '0' до '9' , хранящимся в переменной c . Таким образом, значение данного выражения является правильным индексом для массива ndigit Следующий фрагмент определяет, является символ цифрой, символом-разделителем или чем-нибудь иным. if (с >= '0' && с <= '9') ++ndigit[c - '0']; else if (с ==' ' || с == '\n' || е == '\t') ++nwhite; else ++nother; Конструкция вида if (условие 1 ) инструкция 1 else if (условие 2 ) инструкция 2 else инструкция n часто применяется для выбора одного из нескольких альтернативных путей, имеющихся в программе. Условия вычисляются по порядку в направлении сверху вниз до тех пор, пока одно из них не будет удовлетворено; в этом случае будет выполнена соответствующая ему инструкция, и работа всей конструкции завершится. (Любая из инструкций может быть группой инструкций в фигурных скобках.) Если ни одно из условий не удовлетворено, выполняется последняя инструкция, расположенная сразу за else , если таковая имеется. Если же else и следующей за ней инструкции нет (как это было в программе подсчета слов), то никакие действия вообще не производятся. Между первым if и завершающим else может быть сколько угодно комбинаций вида else if (условие) инструкция Когда их несколько, программу разумно форматировать так, как мы здесь показали. Если же каждый следующий if сдвигать вправо относительно предыдущего else , то при длинном каскаде проверок текст окажется слишком близко прижатым к правому краю страницы. Инструкция switch , речь о которой пойдет в главе 3, обеспечивает другой способ изображения многопутевого ветвления на языке Си. Он более подходит, в частности, тогда, когда условием перехода служит совпадение значения некоторого выражения целочисленного типа с одной из констант, входящих в заданный набор. Вариант нашей программы, реализованной с помощью switch , приводится в параграфе 3.4. Упражнение 1.13. Напишите программу, печатающую гистограммы длин вводимых слов. Гистограмму легко рисовать горизонтальными полосами. Рисование вертикальными полосами — более трудная задача. Упражнение 1.14. Напишите программу, печатающую гистограммы частот встречаемости вводимых символов. 1.7. Функции Функции в Си играют ту же роль, что и подпрограммы и функции в Фортране или процедуры и функции в Паскале. Функция обеспечивает удобный способ отдельно оформить некоторое вычисление и пользоваться им далее, не заботясь о том, как оно реализовано. После того, как функции написаны, можно забыть, как они сделаны, достаточно знать лишь, что они умеют делать. Механизм использования функции в Си удобен, легок и эффективен. Нередко вы будете встречать короткие функции, вызываемые лишь единожды; они оформлены в виде функции с одной-единственной целью — получить более ясную программу. До сих пор мы пользовались готовыми функциями вроде main , getchar и putchar , теперь настала пора нам самим написать несколько функций. В Си нет оператора возведения в степень вроде ** в Фортране. Поэтому проиллюстрируем механизм определения функции на примере функции power(m, n) , которая возводит целое m в целую положительную степень n . Так, power(2, 5) имеет значение 32. На самом деле для практического применения эта функция малопригодна, так как оперирует лишь малыми целыми степенями, однако она вполне может послужить иллюстрацией. (В стандартной библиотеке есть функция pow(x, у) , вычисляющая х у .) Итак, мы имеем функцию power и главную функцию main , пользующуюся ее услугами, так что вся программа выглядит следующим образом: #include /* тест функции power */ main() { int i; for (i = 0; i < 10; ++i) printf("%d %d %d\n", i, power(2,i), power(-3, i)); return 0; } /* возводит base в п-ю степень; n >= 0 */ int power(int base, int n) { int i, p; P = 1; for (i = 1; i <= n; ++i) p = p * base; return p; } Определение любой функции имеет следующий вид: тип-результата имя-функции (список параметров, если он есть) { объявления инструкции } Определения функций могут располагаться в любом порядке в одном или в нескольких исходных файлах, но любая функция должна быть целиком расположена в каком-то одном. Если исходный текст программы распределен по нескольким файлам, то, чтобы ее скомпилировать и загрузить, вам придется сказать несколько больше, чем при использовании одного файла; но это уже относится к операционной системе, а не к языку. Пока мы предполагаем, что обе функции находятся в одном файле, так что будет достаточно тех знаний, которые вы уже получили относительно запуска программ на Си. В следующей строке из функции main к power обращаются дважды. printf("%d %d %d\n", i, power(2,i), power(-3,1)); При каждом вызове функции power передаются два аргумента, и каждый раз главная программа main в ответ получает целое число, которое затем приводится к должному формату и печатается. Внутри выражения power(2,1) представляет собой целое значение точно так же, как 2 или i . (Не все функции в качестве результата выдают целые значения; подробно об этом будет сказано в главе 4.) В первой строке определения power: int power(int base, int n) указываются типы параметров, имя функции и тип результата. Имена параметров локальны внутри power , это значит, что они скрыты для любой другой функции, так что остальные подпрограммы могут свободно пользоваться теми же именами для своих целей. Последнее утверждение справедливо также для переменных i и р : i в power и i в main не имеют между собой ничего общего. Далее параметром мы будем называть переменную из списка параметров, заключенного в круглые скобки и заданного в определении функции, а аргументом — значение, используемое при обращении к функции. Иногда в том же смысле мы будем употреблять термины формальный аргумент и фактический аргумент. Значение, вычисляемое функцией power, возвращается в main с помощью инструкции return. За словом return может следовать любое выражение: return выражение; Функция не обязательно возвращает какое-нибудь значение. Инструкция return без выражения только передает управление в ту программу, которая ее вызвала, не передавая ей никакого результирующего значения. То же самое происходит, если в процессе вычислений мы выходим на конец функции, обозначенный в тексте последней закрывающей фигурной скобкой. Возможна ситуация, когда вызывающая функция игнорирует возвращаемый ей результат. Вы, вероятно, обратили внимание на инструкцию return в конце main . Поскольку main есть функция, как и любая другая, она может вернуть результирующее значение тому, кто ее вызвал, — фактически в ту среду, из которой была запущена программа. Обычно возвращается нулевое значение, что говорит о нормальном завершении выполнения. Ненулевое значение сигнализирует о необычном или ошибочном завершении. До сих пор ради простоты мы опускали return в main , но с этого момента будем задавать return как напоминание о том, что программы должны сообщать о состоянии своего завершения в операционную систему. Объявление int power(int m, int n); стоящее непосредственно перед main , сообщает, что функция power ожидает двух аргументов типа int и возвращает результат типа int . Это объявление, называемое прототипом функции, должно быть согласовано с определением и всеми вызовами power. Если определение функции или вызов не соответствует своему прототипу, это ошибка. Имена параметров не требуют согласования. Фактически в прототипе они могут быть произвольными или вообще отсутствовать, т.е. прототип можно было бы записать и так: int power(int, int); Однако удачно подобранные имена поясняют программу, и мы будем часто этим пользоваться. Историческая справка. Самые большие отличия ANSI-Си от более ранних версий языка как раз и заключаются в способах объявления и определения функций. В первой версии Си функцию power требовалось задавать в следующем виде: /* power: возводит base в n-ю степень; n >= 0 */ /* (версия в старом стиле языка Си) */ power(base, n) int base, n; ( int i, p; P = 1; for (i = 1; i <= n; ++i) p = p * base; return p; } Здесь имена параметров перечислены в круглых скобках, а их типы заданы перед первой открывающей фигурной скобкой. В случае отсутствия указания о типе параметра, считается, что он имеет тип int . (Тело функции не претерпело изменений.) Описание power в начале программы согласно первой версии Си должно было бы выглядеть следующим образом: int power(); Нельзя было задавать список параметров, и поэтому компилятор не имел возможности проверить правильность обращений к power. Так как при отсутствии объявления power предполагалось, что функция возвращает значение типа int , то в данном случае объявление целиком можно было бы опустить. Новый синтаксис для прототипов функций облегчает компилятору обнаружение ошибок в количестве аргументов и их типах. Старый синтаксис объявления и определения функции все еще допускается стандартом ANSI, по крайней мере, на переходный период, но если ваш компилятор поддерживает новый синтаксис, мы настоятельно рекомендуем пользоваться только им. Упражнение 1.15. Перепишите программу преобразования температур, выделив само преобразование в отдельную функцию. 1.8. Аргументы. Вызов по значению Одно свойство функций в Си, вероятно, будет в новинку для программистов, которые уже пользовались другими языками, в частности Фортраном. В Си все аргументы функции передаются "по значению". Это следует понимать так, что вызываемой функции посылаются значения ее аргументов во временных переменных, а не сами аргументы. Такой способ передачи аргументов несколько отличается от "вызова по ссылке" в Фортране и спецификации var при параметре в Паскале, которые позволяют подпрограмме иметь доступ к самим аргументам, а не к их локальным копиям. Главное отличие заключается в том, что в Си вызываемая функция не может непосредственно изменить переменную вызывающей функции: она может изменить только ее частную, временную копию. Однако вызов по значению следует отнести к достоинствам языка, а не к его недостаткам. Благодаря этому свойству обычно удается написать более компактную программу, содержащую меньшее число посторонних переменных, поскольку параметры можно рассматривать как должным образом инициализированные локальные переменные вызванной подпрограммы. В качестве примера приведем еще одну версию функции power, в которой как раз использовано это свойство. /* power: возводит base в n-ю степень: n >= 0; версия 2 */ int power(int base, int n) { int p; for (p = 1; n > 0; --n) p = p * base; return p; } Параметр n выступает здесь в роли временной переменной, в которой циклом for в убывающем порядке ведется счет числа шагов до тех пор, пока ее значение не станет нулем. При этом отпадает надобность в дополнительной переменной i для счетчика цикла. Что бы мы ни делали с n внутри power , это не окажет никакого влияния на сам аргумент, копия которого была передана функции power при ее вызове. При желании можно сделать так, чтобы функция смогла изменить переменную в вызывающей программе. Для этого последняя должна передать адрес подлежащей изменению переменной (указатель на переменную), а в вызываемой функции следует объявить соответствующий параметр как указатель и организовать через него косвенный доступ к этой переменной. Все, что касается указателей, мы рассмотрим в главе 5. Механизм передачи массива в качестве аргумента несколько иной. Когда аргументом является имя массива, то функции передается значение, которое является адресом начала этого массива; никакие элементы массива не копируются. С помощью индексирования относительно полученного значения функция имеет доступ к любому элементу массива. Разговор об этом пойдет в следующем параграфе. 1.9. Символьные массивы Самый распространенный вид массива в Си — массив символов. Чтобы проиллюстрировать использование символьных массивов и работающих с ними функций, напишем программу, которая читает набор текстовых строк и печатает самую длинную из них. Ее схема достаточно проста: while (есть ли еще строка?) if (данная строка длиннее самой длинной из предыдущих) запомнить ее запомнить ее длину напечатать самую длинную строку Из схемы видно, что программа естественным образом распадается на части. Одна из них получает новую строку, другая проверяет ее, третья запоминает, а остальные управляют процессом вычислений. Поскольку процесс четко распадается на части, хорошо бы так и перевести его на Си. Поэтому сначала напишем отдельную функцию getline для получения очередной строки. Мы попытаемся сделать эту функцию полезной и для других применений. Как минимум getline должна сигнализировать о возможном конце файла, а еще лучше, если она будет выдавать длину строки — или нуль в случае исчерпания файла. Нуль годится для признака конца файла, поскольку не бывает строк нулевой длины; даже строка, содержащая только один символ новой строки, имеет длину 1. Когда мы обнаружили строку более длинную, чем самая длинная из всех предыдущих, то нам надо будет где- то ее запомнить. Здесь напрашивается вторая функция, сору, которая умеет копировать новую строку в надежное место. Наконец, нам необходима главная программа, которая бы управляла функциями getline и copy . Вот как выглядит наша программа в целом: #include #define MAXLINE 1000 /* максимальный размер вводимой строки */ int getline(char linef], int MAXLINE); void copy(char to[], char fromf]); /* печать самой длинной строки */ main() { int len; /* длина текущей строки */ int max; /* длина максимальной из просмотренных строк */ char line[MAXLINE]; /* текущая строка */ char longest[MAXLINE]; /* самая длинная строка */ max = 0; while ((len = getline(line, MAXLINE)) > 0) if (len > max) { max = len; copy(longest, line); } if (max > 0) /* была ли хоть одна строка? */ printf("%s", longest); return 0; } /* getline: читает строку в s, возвращает длину */ int getline(char s[], int lim) { int c, i; for (i = 0; i < lim-1 && (c = getchar()) != EOF && с != '\n'; ++i) s[i] = c; if (c == 'n'; { s[i] = c; ++i; } s[i] = '\0'; return i; } /* copy: копирует из 'from' в 'to'; to достаточно большой */ void copy(char to[], char from[]) { int i; i = 0; while ((to[i] = from[i]) != '\0') ++i; } Мы предполагаем, что функции getline и copy , описанные в начале программы, находятся в том же файле, что и main Функции main и getline взаимодействуют между собой через пару аргументов и возвращаемое значение. В getline аргументы определяются строкой int getline(char s[], int lim) Как мы видим, ее первый аргумент s есть массив, а второй, lim , имеет тип int . Задание размера массива в определении имеет целью резервирование памяти. В самой getline задавать длину массива s нет необходимости, так как его размер указан в main . Чтобы вернуть значение вызывающей программе, getline использует return точно так же, как это делает функция power . В приведенной строке также сообщается, что getIine возвращает значение типа int , но так как при отсутствии указания о типе подразумевается int , то перед getline слово int можно опустить. Одни функции возвращают результирующее значение, другие (такие как copy ) нужны только для того, чтобы произвести какие-то действия, не выдавая никакого значения. На месте типа результата в copy стоит void Это явное указание на то, что никакого значения данная функция не возвращает. Функция getline в конец создаваемого ею массива помещает символ '\0' (null-символ, кодируемый нулевым байтом), чтобы пометить конец строки символов. То же соглашение относительно окончания нулем соблюдается и в случае строковой константы вроде "hello\n" В данном случае для него формируется массив из символов этой строки с '\0' в конце. h e l l o \n \0 Спецификация %s в формате printf предполагает, что соответствующий ей аргумент — строка символов, оформленная указанным выше образом. Функция сору в своей работе также опирается на тот факт, что читаемый ею аргумент заканчивается символом '\0' , который она копирует наряду с остальными символами. (Все сказанное предполагает, что '\0' не встречается внутри обычного текста.) Попутно стоит заметить, что при работе даже с такой маленькой программой мы сталкиваемся с некоторыми конструктивными трудностями. Например, что должна делать main , если встретится строка, превышающая допустимый размер? Функция getline работает надежно: если массив полон, она прекращает пересылку, даже если символа новой строки не обнаружила. Получив от getline длину строки и увидев, что она совпадает с MAXLINE , главная программа main могла бы "отловить" этот особый случай и справиться с ним. В интересах краткости описание этого случая мы здесь опускаем. Пользователи getline не могут заранее узнать, сколь длинными будут вводимые строки, поэтому getline делает проверки на переполнение. А вот пользователям функции copy размеры копируемых строк известны (или они могут их узнать), поэтому дополнительный контроль здесь не нужен. |