Язык Си - Уэйт, Прата, Мартин. M. уэит с. Прата д. Мартин
Скачать 4.69 Mb.
|
\0'. Каждый элемент инициализируется соответствующим символом. В дальнейшем компилятор будет рассматривать имя m3 как синоним адреса первого элемента массива, т. е. &m3[0]. Следует отметить, что m3 является константой указателя. Вы не можете изменить m3, так как это означало бы изменение положения (адрес) массива в памяти. Можно использовать операции, подобные m3+1, для идентификации следующего элемента массива, однако нe разрешается выражение ++m3. Опeратор увеличения можно использовать с именами переменных, но не констант. Форма с указателем также вызывает создание в статической памяти 38 элементов для запоминания строки. Но, кроме того, выделяется еще одна ячейка памяти для переменной m3, являющейся указателем. Сначала эта переменная указывает на начало строки, но ее значение может изменяться. Поэтому мы можем использовать операцию увеличения; ++m3 будет указывать на второй символ строки (Д). Заметим, что мы не объявили *m3 статической неременной, потому что мы инициализировали не массив из 38 элементов, а одну переменную типа указатель. Не существует ограничений на класс памяти при инициализации обычных переменных, не являющихся массивом. Существенны ли эти отличия? Чаще всего нет, но все зависит от того, что вы пытаетесь делать. Посмотрите несколько примеров, а мы возвращаемся к вопросу выделения памяти для строк. Массив и указатель: различия В нижеследующем тексте мы обсудим различия в использовании описаний этих двух видов: static char heart[ ] ="Я люблю Тилли !"; char *head ="Я люблю Милли!"; Основное отличие состоит в том, что указатель heart является константой, в то время как указатель head - переменной. Посмотрим, что на самом деле даст эта разница. Вo-пepвых, и в том и в другом случае можно использовать операцию сложения с указателем. for(i = 0; i < 6; i++ ) putchar(*(heart + i)); putchar('\n'); for(i = 0; i < 6; i++ ) putchar(*(head + i)); putchar('\n'); 254 в результате получаем Я люблю Я люблю Но только в случае с указателем можно использовать операцию увеличения: while( *(head) != '\0') /* останов и конце строки */ putchar(*(head++ )); /* печать символа и перемещение указателя */ дают в результате: Я люблю МИЛЛИ! Предположим, мы хотим заменить head на heart. Мы можем cказать head = heart /* теперь head указывает на массив hеart */ но теперь мы можем сказать heart = head; /* запрещенная конструкция */ Ситуация аналогична х = 3 или 3 = х; левая часть оператора присваивания должна быть именем переменной. В данном случае head = heart; не уничтожит строку Милли, а только изменит адрес, записанный в head. Вот каким путем можно изменить обращение к heart и проникнуть в сам массив: heart[8] = 'М'; или *(heart + 8) = 'М'; Элементы массива (но не имя) являются переменными. Явное задание размера памяти Далее Содержание Иной путь выделения памяти заключается в явном ее задании. Во внешнем описании мы могли бы скачать: char m1[44] = "Только ограничьтесь одной строкой."; вместо char m1[ ] = "Только ограничьтесь одной строкой."; Можно быть уверенным, что число элементов по крайней мере на один (это снова нуль-символ) больше, чем длина строки. Как и в других статических или внешних массивах, любые неиспользованные элементы автоматически инициализируются нулем (который в символьном виде является нуль-символом, а не символом цифры нуль). РИС. 13.2. Инициализация массива. Отметим, что в нашей программе массиву name задан размер: 255 char name [81]; Поскольку массив name должен читаться во время работы программы, у компилятора нет другого способа узнать заранее, сколько памяти нужно выделить для массива. Это нс символьная константа, в которой компилятор может посчитать символы. Поэтому мы предположили, что 80 символов будет достаточно, чтобы поместить в массив фамилию пользователя. Массивы символьных строк Далее Содержание Обычно бывает удобно иметь массив символьных строк. В этом случае можно использовать индекс для доступа к нескольким разным строкам. Покажем это на примере: static char *mytal[LIM] = {"Быстро складываю числа", "Точно умножаю", "Записываю данные", "Правильно выполняю команды", "Понимаю язык Си"}; Разберемся в этом описании. Вспомним, что LIM имеет значение 5, мы можем сказать, что mytal является массивом, состоящим из пяти указателей на символьные строки. Каждая строка символов, конечно же, представляет собой символьный массив, поэтому у нас есть пять указателей на массивы. Первым указателем является mytal[0], и он ссылается на первую строку. Второй указатель mytal[1] ссылается на вторую строку. Каждый указатель, в частности, ссылается на первый символ своей строки: *mytal[0] == 'Б', *mytal[1] == 'Т', mytal[2] == 'З' и т. д. Инициализация выполняется по правилам, определенным для массивов. Тексты в кавычках эквивалентны скобочной записи {{...}, {...}, ..., {...}}; где многоточия подразумевают тексты, которые мы поленились напечатать. В первую очередь мы хотим отметить, что первая последовательность, заключенная в двойные кавычки, соответствует первым парным скобкам и используется для инициализации первого указателя символьной строки. Следующая последовательность в двойных кавычках инициализирует второй указатель и т. д. Запятая разделяет соседние последовательности. Кроме того, мы могли бы явно задавать размер строк символов, используя описание, подобное такому: static char mytal[LIM][LINLIM]; Разница заключается в том, что второй индекс задает "прямоугольный" массив, в котором все "ряды" (строки) имеют одинаковую длину. Описание static char *mytal [LIM] однако, определяет "рваный" массив, где длина каждого "ряда" определяется той строкой, которая этот "ряд" инициализировала. Рваный массив не тратит память напрасно. 256 PИС. 13.3. Прямоугольный массив или pваный. Указатели и строки Далее Содержание Возможно, вы заметили периодическое упоминание указателей в нашем рассказе о строках. Большинство операции языка Си, имеющих дело со строками, работает с указателями. Например, рассморим приведенную ниже бесполезную, но поучительную программу /* указатели и строки */ #define PX(X) printf("X = %s; значение = %u; &X = %u\n", X, X, &X) main( ) { static char *mesg = "He делай глупостей!"; static char *copy; copy = mesg; printf(" %s \n" , copy); PX(mesg); PX(copy); } Взглянув на эту программу, вы можете подумать, что она копирует строку "Не делай глупостей!", и при беглом взгляде на вывод вам может показаться правильным это 257 предположение: He делай глупостей! mesg = He делай глупостей!; значение = 14; &mesg = 32 copy = He делай глупостей!; значение = 14; &сору = 34 Но изучим вывод РХ(). Сначала X, который последовательно является mesg и сору, печатается как строка (%s). Здесь нет сюрприза. Все строки содержат "Не делай глупостей!". Далее ... вернемся к этому несколько позднее. Третьим элементом в каждой строке является &X, т. е. адрес X. Указатели mesg и copy записаны в ячейках 32 и 34 соответственно. Теперь о втором элементе, который мы называем значением. Это сам X. Значением указателя является адрес, который он содержит. Мы видим, что mesg ссылается на ячейку 14, и поэтому выполняется сору. Смысл заключается в том, что сама строка никогда не копируется. Оператор copy=mesg; создаст второй указатель, ссылающийся на ту же самую строку. Зачем все эти предосторожности? Почему бы не скопировать всю строку? Хороню, а что эффективнее - копировать один адрес или, скажем, 50 отдельных элементов ? Часто бывает, что адрес это все, что необходимо для выполнения работы. Теперь, когда мы обсудили определение строк в программе, давайте займемся вводом строк. ВВОД СТРОК Далее Содержание Процесс ввода строки выполняется за два шага: выделение памяти для запоминания строки и применение функции ввода для получения строки. Выделение памяти Далее Содержание Сначала следует определить место для размещения строки при вводе. Как было отмечено раньше, это значит, выделить память, достаточную для размещения любых строк, которые мы предполагаем читать. Не следует надеяться, что компьютер подсчитает длину строки при ее вводе, а затем выделит для нес память. Он нe будет этого делать (если только вы не напишите программу, которая должна это выполнять). Если вы попытаетесь сделать что-то подобное static char *name; scanf(" %s", name); компилятор, вероятно, выполнит нужные действия. Но при вводе имя будет записываться на данные или текст вашей программы. Большинство программистов считает это очень забавным, но только в чужих программах. Проще всего включить в описание явный размер массива: char name[81]; Можно также использовать библиотечные функции языка Си, которые распределяют намять, и мы рассмотрим их в гл. 15. 258 В нашей программе для name использовался автоматический массив. Мы смогли это сделать, потому что не требовалось инициализации массива. Кaк только выделена память для массива, можно считывать строку. Мы уже упоминали, что программы ввода не являются частью языка. Однако большинство систем имеют две библиотечные функции scanf( ) и gets( ), которые могут считывать строки. Чаще всего используется функция gets( ), поэтому мы вначале расскажем о ней. Функция gets( ) Далее Содержание Эта функция считывания строки очень удобна для диалоговых систем. Она получает строку от стандартного устройства ввода вашей системы, которым, как мы предполагаем, является клавиатура. Поскольку строка не имеет заранее заданной длины, функция gets( ) должна знать, когда ей прекратить работу. Функция читает символы до тех пор, пока ей не встретится символ новой строки ('\n'), который вы создаете, нажимая клавишу [ввод]. Функция берет все символы до (но не включая) символа новой строки, присоединяет к ним нуль-символ ('\0') и передает строку вызывающей программе. Вот простой способ использования функции. /* получение имени1 */ main( ) { char name[81]; /* выделение памяти */ printf(" Привет, как вас зовут?\n"); gets(name); /* размещение введенного имени в строку "name" */ printf(" Хорошее имя, %s. \n" , name); } Функция примет любое имя (включая пробелы) длиной до 80 символов. (Не забудьте запасти один символ для '\0'.) Отметим, что мы хотели при помощи функции gets( ) воздействовать на нечто (name) в вызывающей программе. Значит, нужно использовать указатель в качестве аргумента; а имя массива, конечно, является его указателем. Функция gets( ) обладает большими возможностями, чем показано в последнем примере. Взгляните на эту программу: /* получение имени2 */ main( ) { char name [80]; char *ptr, *gets( ); printf(" Привет, как вас зовут?\n"); ptr = gets(name); printf(" %s? Ax! %s!\n", name, ptr); } Получился диалог: Привет, как вас зовут? Тони де Туна Тони де Туна? Ах! Тони де Туна! Функция gets( ) предоставляет вам два способа ввода строки! 259 1. Использует метод указателей для передачи строки в name. 2. Использует ключевое слово return для возврата строки в ptr. Напомним, что ptr является указателем на тип char. Это означает, что gets( ) должна вернуть значение, которое является указателем на тип char. И в приведенном выше изложении вы можете увидеть, что мы так и описали gets( ). Описание вида char *gets( ); говорит о том, что gets( ) является функцией (отсюда круглые скобки) типа "указатель на тип char" (поэтому * и char). В примере получение имени1 мы обходились без этого описания, потому что мы никогда не пытались использовать возвращенное значение функции gets( ). Между прочим, вы можете также описать указатель на функцию. Это выглядело бы следующим образом: char (*foop)( ); и foop был бы указателем на функцию типа char. Мы расскажем немного подробнее о таких причудливых описаниях в гл. 14. Структура функции gets( ) выглядела бы примерно так: char *gets(s); char *s; { char *p; return(p); } На самом деле структура немного сложнее, и для gets( ) есть две возможности возврата. Если все идет хорошо, она возвращает считанную строку, как мы уже сказали. Если что-то неправильно или если gets( ) встречает символ EOF, она возвращает NULL, или нулевой адрес. Таким образом gets( ) включает разряд проверки ошибки. Поэтому данная функция удобна для использования в конструкциях, подобных while(gets(name) != NULL) где NULL определен в файле stdio.h как 0. При помощи указателя массиву name присваивается значение. Наличие возврата позволяет присваивать значение всей gets(name) и выполнять проверку на EOF. Этот двоякий подход более компактен, чем использование функции getchar( ), которая имеет возврат без аргумента. while((ch = getchar( )) != EOF) Функция scanf( ) Далее Содержание Мы уже использовали ранее функцию scanf( ) и формат %s для считывания строки. Основное различие между scanf( ) и gets( ) заключается в том, как они определяют, что достигли конца строки: scanf( ) предназначена скорее для получения слова, а не строки. Функция gets( ), как мы уже видели, принимает все символы до тех пор, пока нс встретит первый символ "новая строка". Функция scanf( ) имеет два варианта. Для любого из них 260 строка начинается с первого встретившегося непустого символа. Если вы используете формат %s, строка продолжается до (но не включая) следующего пустого символа (пробел, табуляция или новая строка). Если вы определяете размер поля как %10s, то функция scanf( ) считает нe более 10 символов или же считает до любого пришедшего первым пустого символа. Функция scanf( ) возвращает целое значение, равное числу счи танных символов, если ввод прошел успению, или символ EОF, ее ли он встретился. /* scanf( ) и подсчет количества */ main( ) { static char name1[40], name2[11]; int count; printf(" Введите, пожалуйста, 2 имени.\n"); count = scanf(" %s %10s", name1, name2); printf(" Я считал %d имен %s и %s.\n", count, name1, name2); } Вот два примера работы программы: Введите, пожалуйста, два имени. Джсссика Джукс. Я считал два имени Джсссика и Джукс. Введите, пожалуйста, 2 имени. Лиза Апплеботтхэм Я считал 2 имени Лиза и Апплеботтхэм. Во втором примере были считаны только первые 10 символов от Апплеботтхэм, так как мы использовали формат %10s. Если вы получаете только текст с клавиатуры, лучше применять, функцию gets( ). Она проще в использовании, быстрее и более компактна. Функция scanf( ) предназначена в основном для ввода смеси типов данных в некоторой стандартной форме. Например, если каждая вводимая строка содержит наименование инструмента, количество его на складе и стоимость каждого инструмента, вы можете использовать функцию scanf( ) или можете создать свою собственную функцию, которая выполняет проверку некоторых ошибок при вводе. Теперь давайте рассмотрим процесс вывода строк. ВЫВОД СТРОК Далее Содержание Опять мы должны полагаться на библиотечные функции, которые могут немного изменяться от системы к системе. Функции puts( ) и printf( ) - две рабочие лошадки, используемые при выводе строк. Функция puts( ) Далее Содержание Это очень простая функция; у нее есть только один аргумент, являющийся указателем строки. Нижеследующий пример иллюстрирует некоторые из многих способов ее применения. /* простые выдачи */ #include #define DEF "Я строка #define." 261 main( ) { static char str1[ ] = "Массив инициализирован мной."; static char *str2 = "Указатель инициализирован мной."; puts(" Я аргумент функции puts( )." ); puts(DEF); puts(str1); puts(str2); puts(&str1[4]); puts(str2 + 4); } В результате работы программы получаем Я аргумент функции puts( ). Я строка #define. Массив инициализирован мной. Указатель инициализирован мной. ив инициализирован мной. атель инициализирован мной. Этот пример напоминает нам, что фразы в кавычках и имена строк символьных массивов являются указателями. Обратите внимание на два последних оператора. Указатель &strl[4] ссылается на пятый элемент массива str1. Этот элемент содержит символ 'и', и функция puts( ) использует его в качестве начальной точки. Аналогично str2 + 4 ссылается на ячейку памяти, содержащую 'а' в "указателе", и с нее начинается вывод строки. Как puts( ) узнает, когда остановиться? Она прекращает работу, если встречает нуль-символ, поэтому лучше, чтобы он был. Не пытайтесь делать так! /* нет строки! */ main( ) { static char dont[ ] = (' H', ' Г , ' ! ', ' ! '); puts(dont); /* dont не является строкой */ } Поскольку в dont отсутствует завершающий нуль-символ, она не является строкой. Так как нуль-символ отсутствует, puts( ) не знает, когда ей останавливаться. Она будет просто перебирать ячейки памяти, следующие за dont до тех пор, пока не найдет где-нибудь нуль-символ. Если повезет, она, может быть, найдет его в ближайшей ячейке, но может и нe повезти. Обратите внимание, что любая строка, вводимая функцией puts( ), начинается с новой строки. Если puts( ) в конце концов находит завершающий нуль-символ, она заменяет его символом "новой строки" и затем выводит строку. Функция printf( ) Далее Содержание Мы уже обсуждали функцию |