Язык программирования Си Брайан Керниган, Деннис Ритчи 3е издание Версия 1 Table of Contents
Скачать 2.33 Mb.
|
Упражнение 1.16. Перепишите main предыдущей программы так, чтобы она могла печатать самую длинную строку без каких-либо ограничений на ее размер. Упражнение 1.17. Напишите программу печати всех вводимых строк, содержащих более 80 символов. Упражнение 1.18. Напишите программу, которая будет в каждой вводимой строке заменять стоящие подряд символы пробелов и табуляций на один пробел и удалять пустые строки. Упражнение 1.19. Напишите функцию reverse(s) , размещающую символы в строке s в обратном порядке. Примените ее при написании программы, которая каждую вводимую строку располагает в обратном порядке. 1.10. Внешние переменные и область видимости Переменные line , longest и прочие принадлежат только функции main , или, как говорят, локальны в ней. Поскольку они объявлены внутри main , никакие другие функции прямо к ним обращаться не могут. То же верно и применительно к переменным других функций. Например, i в getline не имеет никакого отношения к i в copy . Каждая локальная переменная функции возникает только в момент обращения к этой функции и исчезает после выхода из нее. Вот почему такие переменные, следуя терминологии других языков, называют автоматическими. (В главе 4 обсуждается класс памяти static , который позволяет локальным переменным сохранять свои значения в промежутках между вызовами.) Так как автоматические переменные образуются и исчезают одновременно с входом в функцию и выходом из нее, они не сохраняют своих значений от вызова к вызову и должны устанавливаться заново при каждом новом обращении к функции. Если этого не делать, они будут содержать "мусор". В качестве альтернативы автоматическим переменным можно определить внешние переменные, к которым разрешается обращаться по их именам из любой функции. (Этот механизм аналогичен области COMMON в Фортране и определениям переменных в самом внешнем блоке в Паскале.) Так как внешние переменные доступны повсеместно, их можно использовать вместо аргументов для связи между функциями по данным. Кроме того, поскольку внешние переменные существуют постоянно, а не возникают и исчезают на период выполнения функции, свои значения они сохраняют и после возврата из функций, их установивших. Внешняя переменная должна быть определена, причем только один раз, вне текста любой функции; в этом случае ей будет выделена память. Она должна быть объявлена во всех функциях, которые хотят ею пользоваться. Объявление содержит сведения о типе переменной. Объявление может быть явным, в виде инструкции extern , или неявным, когда нужная информация получается из контекста. Чтобы конкретизировать сказанное, перепишем программу печати самой длинной строки с использованием line , longest и max в качестве внешних переменных. Это потребует изменений в вызовах, объявлениях и телах всех трех функций. #include #define MAXLINE 1000 /* максимальный размер вводимой строки */ int max; /* длина максимальной из просмотренных строк */ char line[MAXLINE]; /* текущая строка */ char longest[MAXLINE]; /* самая длинная строка */ int getline(void); void copy(void); /* печать самой длинной строки; специализированная версия */ main () { int len; extern int max; extern char longest[]; max = 0; while ((len = getline()) > 0) if (len > max) { max = len; copy(); } if (max > 0) /* была хотя бы одна строка */ printf("%s", longest); return 0; } /* getline: специализированная версия */ int getline(void) { int c, i; extern char line[]; for (i=0; i < MAXLINE-1 && (c=getchar()) != EOF && с != '\n'; ++i) line[i] = c; if(c == '\n') { line[i]= c; ++i; } line[i] = '\0'; return i; } /* copy: специализированная версия */ void copy (void) { int i; extern char line[], longest[]; i = 0; while ((longest[i] = line[i]) != '\0') ++i; } Внешние переменные для main , getline и copy определяются в начале нашего примера, где им присваивается тип и выделяется память. Определения внешних переменных синтаксически ничем не отличаются от определения локальных переменных, но поскольку они расположены вне функций, эти переменные считаются внешними. Чтобы функция могла пользоваться внешней переменной, ей нужно прежде всего сообщить имя соответствующей переменной. Это можно сделать, например, задав объявление extern , которое по виду отличается от объявления внешней переменной только тем, что оно начинается с ключевого слова extern В некоторых случаях объявление extern можно опустить. Если определение внешней переменной в исходном файле расположено выше функции, где она используется, то в объявлении extern нет необходимости. Таким образом, в main , getline и copy объявления extern избыточны. Обычно определения внешних переменных располагают в начале исходного файла, и все объявления extern для них опускают. Если же программа расположена в нескольких исходных файлах и внешняя переменная определена в файле 1, а используется в файле 2 и файле З, то объявления extern в файле 2 и файле З обязательны, поскольку необходимо указать, что во всех трех файлах функции обращаются к одной и той же внешней переменной. На практике обычно удобно собрать все объявления внешних переменных и функций в отдельный файл, называемый заголовочным (header-файлом), и помещать его с помощью #include в начало каждого исходного файла. В именах header-файлов по общей договоренности используется суффикс .h . В этих файлах, в частности в , описываются также функции стандартной библиотеки. Более подробно о заголовочных файлах говорится в главе 4, а применительно к стандартной библиотеке — в главе 7 и приложении В. Так как специализированные версии getline и сору не имеют аргументов, на первый взгляд кажется, что логично их прототипы задать в виде getline() и copy() . Но из соображений совместимости со старыми Си-программами стандарт рассматривает пустой список как сигнал к тому, чтобы выключить все проверки на соответствие аргументов. Поэтому, когда нужно сохранить контроль и явно указать отсутствие аргументов, следует пользоваться словом void . Мы вернемся к этой проблеме в главе 4. Заметим, что по отношению к внешним переменным в этом параграфе мы очень аккуратно используем понятия определение и объявление. "Определение" располагается в месте, где переменная создается и ей отводится память; "объявление" помещается там, где фиксируется природа переменной, но никакой памяти для нее не отводится. Следует отметить тенденцию все переменные делать внешними. Дело в том, что, как может показаться на первый взгляд, это приводит к упрощению связей — ведь списки аргументов становятся короче, а переменные доступны везде, где они нужны; однако они оказываются доступными и там, где не нужны. Так что чрезмерный упор на внешние переменные чреват большими опасностями — он приводит к созданию программ, в которых связи по данным не очевидны, поскольку переменные могут неожиданным и даже таинственным способом изменяться. Кроме того, такая программа с трудом поддается модификациям. Вторая версия программы поиска самой длинной строки хуже, чем первая, отчасти по этим причинам, а отчасти из-за нарушения общности двух полезных функций, вызванного тем, что в них вписаны имена конкретных переменных, с которыми они оперируют. Итак, мы рассмотрели то, что можно было бы назвать ядром Си. Описанных "кирпичиков" достаточно, чтобы создавать полезные программы значительных размеров, и было бы чудесно, если бы вы, прервав чтение, посвятили этому какое-то время. В следующих упражнениях мы предлагаем вам создать несколько более сложные программы, чем рассмотренные выше. Упражнение 1.20. Напишите программу detab, заменяющую символы табуляции во вводимом тексте нужным числом пробелов (до следующего "стопа" табуляции). Предполагается, что "стопы" табуляции расставлены на фиксированном расстоянии друг от друга, скажем, через n позиций. Как лучше задавать n — в виде значения переменной или в виде именованной константы? Упражнение 1.21. Напишите программу entab, заменяющую строки из пробелов минимальным числом табуляций и пробелов таким образом, чтобы вид напечатанного текста не изменился. Используйте те же "стопы" табуляции, что и в detab. В случае, когда для выхода на очередной "стоп" годится один пробел, что лучше — пробел или табуляция? Упражнение 1.22. Напишите программу, печатающую символы входного потока так, чтобы строки текста не выходили правее n-й позиции. Это значит, что каждая строка, длина которой превышает n, должна печататься с переносом на следующие строки. Место переноса следует "искать" после последнего символа, отличного от символа-разделителя, расположенного левее n-й позиции. Позаботьтесь о том, чтобы ваша программа вела себя разумно в случае очень длинных строк, а также когда до n-й позиции не встречается ни одного символа пробела или табуляции. Упражнение 1.23. Напишите программу, убирающую все комментарии из любой Си-программы. Не забудьте должным образом обработать строки символов и строковые константы. Комментарии в Си не могут быть вложены друг в друга. Упражнение 1.24. Напишите программу, проверяющую Си-программы на элементарные синтаксические ошибки вроде несбалансированности скобок всех видов. Не забудьте о кавычках (одиночных и двойных), эскейп-последовательностях ( \... ) и комментариях. (Это сложная программа, если писать ее для общего случая.) 2. Типы, операторы и выражения Переменные и константы являются основными объектами данных, с которыми имеет дело программа. Переменные перечисляются в объявлениях, где устанавливаются их типы и, возможно, начальные значения. Операции определяют действия, которые совершаются с этими переменными. Выражения комбинируют переменные и константы для получения новых значений, Тип объекта определяет множество значений, которые этот объект может принимать, и операций, которые над ними могут выполняться. Названные "кирпичики" и будут предметом обсуждения в этой главе. Стандартом ANSI было утверждено значительное число небольших изменений и добавлений к основным типам и выражениям. Любой целочисленный тип теперь может быть со знаком, signed , и без знака, unsigned . Предусмотрен способ записи беззнаковых констант и шестнадцатеричных символьных констант. Операции с плавающей точкой допускаются теперь и с одинарной точностью. Введен тип long double , обеспечивающий повышенную точность. Строковые константы конкатенируются ("склеиваются") теперь во время компиляции. Частью языка стали перечисления ( enum ), формализующие для типа установку диапазона значений. Объекты для защиты их от каких-либо изменений разрешено помечать как const . В связи с введением новых типов расширены правила автоматического преобразования из одного арифметического типа в другой. 2.1. Имена переменных Хотя мы ничего не говорили об этом в главе 1, но существуют некоторые ограничения на задание имен переменных и именованных констант. Имена составляются из букв и цифр; первым символом должна быть буква. Символ подчеркивания "_" считается буквой; его иногда удобно использовать, чтобы улучшить восприятие длинных имен переменных. Не начинайте имена переменных с подчеркивания, так как многие переменные библиотечных программ начинаются именно с этого знака. Большие (прописные) и малые (строчные) буквы различаются, так что х и X — это два разных имени. Обычно в программах на Си малыми буквами набирают переменные, а большими — именованные константы. Для внутренних имен значимыми являются первые 31 символ 4 . Для имен функций и внешних переменных число значимых символов может быть меньше 31, так как эти имена обрабатываются ассемблерами и загрузчиками и языком не контролируются. Уникальность внешних имен гарантируется только в пределах 6 символов, набранных безразлично в каком регистре. Ключевые слова if , else , int , float и т. д. зарезервированы, и их нельзя использовать в качестве имен переменных. Все они набираются на нижнем регистре (т. е. малыми буквами). Разумно давать переменным осмысленные имена в соответствии с их назначением, причем такие, чтобы их было трудно спутать друг с другом. Мы предпочитаем короткие имена для локальных переменных, особенно для счетчиков циклов, и более длинные для внешних переменных. 2.2. Типы и размеры данных В Си существует всего лишь несколько базовых типов: char — единичный байт, который может содержать один символ из допустимого символьного набора; int — целое, обычно отображающее естественное представление целых в машине; float — число с плавающей точкой одинарной точности; double — число с плавающей точкой двойной точности. 4 Это, как и другие ограничения на длину имен, является минимальными требованиями стандарта. Компиляторы могут поддерживать и имена большей длины. — Примеч. корр. Имеется также несколько квалификаторов, которые можно использовать вместе с указанными базовыми типами. Например, квалификаторы short (короткий) и long (длинный) применяются к целым: short int sh; long int counter; В таких объявлениях слово int можно опускать, что обычно и делается. Если только не возникает противоречий со здравым смыслом, short int и long int должны быть разной длины, a int соответствовать естественному размеру целых на данной машине. Чаще всего для представления целого, описанного с квалификатором short , отводится 16 битов, с квалификатором long — 32 бита, а значению типа int — или 16, или 32 бита. Разработчики компилятора вправе сами выбирать подходящие размеры, сообразуясь с характеристиками своего компьютера и соблюдая следующие ограничения: значения типов short и int представляются по крайней мере 16 битами; типа long — по крайней мере 32 битами; размер short не больше размера int , который в свою очередь не больше размера long Квалификаторы signed (со знаком) или unsigned (без знака) можно применять к типу char и любому целочисленному типу. Значения unsigned всегда положительны или равны нулю и подчиняются законам арифметики по модулю 2 n , где n — количество битов в представлении типа. Так, если значению char отводится 8 битов, то unsigned char имеет значения в диапазоне от 0 до 255, a signed char — от -128 до 127 (в машине с двоичным дополнительным кодом). Являются ли значения типа просто char знаковыми или беззнаковыми, зависит от реализации, но в любом случае коды печатаемых символов положительны. Тип long double предназначен для арифметики с плавающей точкой повышенной точности. Как и в случае целых, размеры объектов с плавающей точкой зависят от реализации; float , double и long double могут представляться одним размером, а могут двумя или тремя разными размерами. Именованные константы для всех размеров вместе с другими характеристиками машины и компилятора содержатся в стандартных заголовочных файлах и (см. приложение В). Упражнение 2.1. Напишите программу, которая будет выдавать диапазоны значений типов char , short , int и long , описанных как signed и как unsigned , с помощью печати соответствующих значений из стандартных заголовочных файлов и путем прямого вычисления. Определите диапазоны чисел с плавающей точкой различных типов. Вычислить эти диапазоны сложнее. 2.3. Константы Целая константа, например 1234, имеет тип int . Константа типа long завершается буквой l или L , например 123456789L ; слишком большое целое, которое невозможно представить как int , будет представлено как long . Беззнаковые константы заканчиваются буквой u или U , а окончание ul или UL говорит о том, что тип константы unsigned long Константы с плавающей точкой имеют десятичную точку ( 123.4 ), или экспоненциальную часть ( 1е-2 ), или же и то и другое. Если у них нет окончания, считается, что они принадлежат к типу double . Окончание f или F указывает на тип float , а l или L — на тип long double Целое значение помимо десятичного может иметь восьмеричное или шестнадцатеричное представление. Если константа начинается с нуля, то она представлена в восьмеричном виде, если с 0х или с 0Х , то — в шестнадцатеричном. Например, десятичное целое 31 можно записать как 037 или как 0X1F . Записи восьмеричной и шестнадцатеричной констант могут завершаться буквой L (для указания на тип long ) и U (если нужно показать, что константа беззнаковая). Например, константа 0XFUL имеет значение 15 и тип unsigned long Символьная константа есть целое, записанное в виде символа, обрамленного одиночными кавычками, например 'х' . Значением символьной константы является числовой код символа из набора символов на данной машине. Например, символьная константа '0' в кодировке ASCII имеет значение 48, которое никакого отношения к числовому значению 0 не имеет. Когда мы пишем '0' , а не какое-то значение (например, 48), зависящее от способа кодировки, мы делаем программу независимой от частного значения кода, к тому же она и легче читается. Символьные константы могут участвовать в операциях над числами точно так же, как и любые другие целые, хотя чаще они используются для сравнения с другими символами. Некоторые символы в символьных и строковых константах записываются с помощью эскейп- последовательностей, например \n (символ новой строки); такие последовательности изображаются двумя символами, но обозначают один. Кроме того, произвольный восьмеричный код можно задать в виде '\ooo' где ooo — одна, две или три восьмеричные цифры (0…7) или '\xhh' где hh — одна, две или более шестнадцатеричные цифры (0…9, а…f, А…F). Таким образом, мы могли бы написать #define VTAB '\013' /* вертикальная табуляция в ASCII */ #define BELL '\007' /* звонок В ASCII */ или в шестнадцатеричном виде: #define VTAB '\xb' /* вертикальная табуляция в ASCII */ #define BELL '\x7' /* звонок в ASCII */ Полный набор эскейп-последовательностей таков: \а сигнал-звонок \\ обратная наклонная черта \b возврат-на-шаг (забой) \? знак вопроса \f перевод-страницы \’ одиночная кавычка \n новая-строка \” двойная кавычка \r возврат-каретки \ooo восьмеричный код \t горизонтальная-табуляция \xhh шестнадцатеричный код \v вертикальная-табуляция Символьная константа '\0' — это символ с нулевым значением, так называемый символ null . Вместо просто 0 часто используют запись '\0' , чтобы подчеркнуть символьную природу выражения, хотя и в том и другом случае запись обозначает нуль. Константные выражения — это выражения, оперирующие только с константами. Такие выражения вычисляются во время компиляции, а не во время выполнения, и поэтому их можно использовать в любом месте, где допустимы константы, как, например, в #define MAXLINE 1000 char line[MAXLINE+1]; или в #define LEAP 1 /* in leap years - в високосные годы */ int days[31+28+LEAP+31+30+31+30+31+31+30+31+30+31]; Строковая константа, или строковый литерал, — это нуль или более символов, заключенных в двойные кавычки, как, например, "Я строковая константа" или "" /* пустая строка */ Кавычки не входят в строку, а служат только ее ограничителями. Так же, как и в символьные константы, в строки можно включать эскейп-последовательности; \" , например, представляет собой двойную кавычку. Строковые константы можно конкатенировать ("склеивать") во время компиляции; например, запись двух строк "Здравствуй," " мир!" эквивалентна записи одной следующей строки: "Здравствуй, мир!" Указанное свойство позволяет разбивать длинные строки на части и располагать эти части на отдельных строчках. Фактически строковая константа — это массив символов. Во внутреннем представлении строки в конце обязательно присутствует нулевой символ '\0' , поэтому памяти для строки требуется на один байт больше, чем число символов, расположенных между двойными кавычками. Это означает, что на длину задаваемой строки нет ограничения, но чтобы определить ее длину, требуется просмотреть всю строку. Функция strlen(s) вычисляет длину строки s без учета завершающего ее символа '\0' . Ниже приводится наша версия этой функции: /* strlen: возвращает длину строки s */ int strlen(char s[]) { int i; i = 0; while (s[i] != '\0') ++i; return i; } Функция strlen и некоторые другие, применяемые к строкам, описаны в стандартном заголовочном файле Будьте внимательны и помните, что символьная константа и строка, содержащая один символ, не одно и то же: 'х' не то же самое, что "х" . Запись 'х' обозначает целое значение, равное коду буквы х из стандартного символьного набора, а запись "х" - массив символов, который содержит один символ (букву х) и '\0' В Си имеется еще один вид константы — константа перечисления. Перечисление — это список целых констант, как, например, в enum boolean { NO, YES }; Первое имя в enum 5 имеет значение 0, следующее — 1, и т. д. (если для значений констант не было явных спецификаций). Если не все значения специфицированы, то они продолжают прогрессию, начиная от последнего специфицированного значения, как в следующих двух примерах: enum escapes { BELL = '\а', BACKSPACE = '\b', TAB = '\t', NEWLINE = '\n', VTAB = '\v', RETURN = '\r' }; enum months { JAN = 1, FEB, MAR, APR, MAY, JUN, JUL, AUG, SEP, ОСТ, NOV, DEC }; /* FEB есть 2, MAR есть 3 и т.д. */ Имена в различных перечислениях должны отличаться друг от друга. Значения внутри одного перечисления могут совпадать. Средство enum обеспечивает удобный способ присвоить константам имена, причем в отличие от #define значения констант при этом способе могут генерироваться автоматически. Хотя разрешается объявлять переменные типа enum , однако компилятор не обязан контролировать, входят ли присваиваемые этим переменным значения в их тип. Но сама возможность такой проверки часто делает enum лучше, чем #define . Кроме того, отладчик получает возможность печатать значения переменных типа enum в символьном виде. |