13. Символьные строки и функции над строками

СИМВОЛЬНЫЕ CTРOKИ

ИНИЦИАЛИЗАЦИЯ СИМВОЛЬНЫХ СТРОК

ВВОД-ВЫВОД СТРОК

ИСПОЛЬЗОВАНИЕ ФУНКЦИЙ, РАБОТАЮЩИХ CO CТРОKAMИ

АРГУМЕНТЫ КОМАНДНЫХ СТРОК

СИМВОЛЬНЫЕ CTРOKИ

Символьные строки представляют один из наиболее полезных и важных типов данных языка Си. Хотя до сих пор все время применялись символьные строки, мы еще не все знаем о них. Конечно, нам уже известно самое главное: символьная строка является массивом типа char, который заканчивается нуль-символом ('\0'). В этой главе мы больше узнаем о структуре строк, о том, как описывать и инициализировать строки, как их вводить или выводить из программы, как работать со строками.

На рис. 13.1 представлена работающая программа, которая иллюстрирует несколько способов создания строк, их чтения и вывода на печать. Мы используем две новые функции: gets( ), которая получаст строку, и puts( ), которая выводит строку. (Вы, вероятно, заметили сходство их имен с функциями getchar( ) и putchar( ).) В остальном программа выглядит достаточно привычно.

/* работа со строками */

#include

#deline MSG "У вас, наверное, много талантов.

Расскажите о некоторых" .

/* константа символьной строки */

#define NULL 0

#define LIM 5

#define LINLEN 81 /* максимальная длина строки + 1 */

char ml[ ] = " Только ограничьтесь одной строкой.";

/* инициализация внешнего символьного массива */

char *m2 = " Если вы не можете вспомнить что-нибудь, придумайте.";

/* инициализация указателя внешнего символьного массива */

main( )

{

char name[LlNLEN];

static char talents[LINLEN];

int i;

int count = 0;

char *m3 = " \n Достаточно обо мне -- Как вас зовут?";

/* инициализация указателя */

static char *mytal[LlM] = ("Быстро складываю числа",

"Точно умножаю",

"Записываю данные",

"Правильно выполняю команды",

"Понимаю язык Си"};

/* инициализация массива строк */

printf("Привет! Я Клайд, компьютер.

У меня много талантов.\n");

printf("%s \n", "Позвольте рассказать о некоторых из них.");

puts(" Каковы они? Ах да, вот их неполный перечень.");

for(i = 0; i

puts(mytal[i]); /* печатает перечень талантов компьютера */

puts(m3);

gets(name);

printf(" Хорошо, %s, %s\n" , name, MSG);

printf(" %s\n %s\n", m1, m2);

gets(talents);

puts(" Давайте, посмотрим, получил ли я этот перечень:");

puts(talents);

printf(" Спасибо за информацию, %s \n" , name);

}


PИC. 13.1. Программа, использующая строки.

Чтобы помочь вам разобраться в том, что делает эта программа, мы приводим результат ее работы:

Привет, я Клайд, компьютер. У меня много талантов.

Позвольте рассказать о некоторых из них.

Каковы они? Ах да, вот их неполный перечень.

Быстро складываю числа.

Точно умножаю.

Записываю данные.

Правильно выполняю команды команды.

Понимаю язык Си.

Достаточно обо мне - Как вас зовут? Найджел Барнтвит

Хорошо, Найджел Барнтвит, у вас, наверное, много талантов.

Расскажите о некоторых.

Только ограничтесь одной строкой.

Если вы не можете вспомнить что-нибудь, придумайте.

Фехтование, пение тирольских песен, симуляция, дегустация сыра.

Давайте посмотрим, получил ли я этот перечень.

Фехтование, пение тирольских песен, симуляция, дегустация сыра.

Спасибо за информацию, Найджел Барнтвит.

Тщательно исследуем программу. Но вместо того чтобы просматривать строку за строкой, применим более общий подход. Сначала рассмотрим способы определения строк в программе. Затем выясним, что нужно для чтения строки в программе. И наконец, изучим способы вывода строки.

ОПРЕДЕЛЕНИЕ СТРОК В ПРОГРАММЕ

Строковые константы

Всякий раз, когда компилятор встречается с чем-то, заключенным в двойные кавычки, он определяет это как строковую константу. Символы, заключенные в кавычки, плюс завершающий символ '\0', записываются в последовательные ячейки памяти. Компилятор подсчитывает количество символов, поскольку ему нужно знать размер памяти, необходимой для запоминания строки. Наша программа использует несколько таких строковых констант, чаще всего в качестве аргументов функций printf( ) и puts( ). Заметим также, что мы можем определять строковые константы при помощи директивы #define.

Если вы хотите включить в строку символ двойной кавычки, ему должен предшествовать символ обратной дробной черты:

рrintf("\"Беги, Спот, беги!\" - сказал Дик.\n");

В результате работы этого оператора будет напечатана строка:

"Беги, Cпот, беги! - "сказал Дик.

Cтроковые константы размещаются в статической памяти. Вся фраза в кавычках является указателем на место в памяти, где записана строка. Это аналогично использованию имени массива, служащего указателем на расположение массива. Если это действительно так, то как выглядит оператор, который выводит строку?

/* строки в качестве указателей */

main( )

{

printf("%s, %u, %c \n", "We", "love", *"figs");

Итак, формат %s выводит строку We. Формат %u выводит целое без знака. Если слово "love" является указателем, то выдается его значение, являющееся адресом первого символа строки. Наконец, *"figs" должно выдать значение, на которое ссылается адрес, т. е. первый символ строки "figs". Произойдет ли это на самом деле? Да, мы получим следующий текст:

We, 34, f

Ну, вот! Давайте теперь вернемся к строкам, находящимся в символьных массивах.

Массивы символьных строк и их инициализация

При определении массива символьных строк необходимо сообщить компилятору требуемый размер памяти. Один из способов сделать это - инициализировать массив при помощи строковой константы. Так как автоматические массивы нельзя инициализировать, необходимо для этого использовать статические или внешние массивы. Например, оператор

char m1[ ] = "Только ограничьтесь одной строкой.";

инициализировал внешний (по умолчанию) массив m1 для указанной строки. Этот вид инициализации является краткой формой стандартной инициализации массива

char m1[ ] = {'Т', 'о', 'л', 'ь', 'к', 'о', ' ',

'о', 'г', 'р', 'а', 'н', 'и', 'ч',

'ь', 'т', 'e', 'с', 'ь', ' ', 'о',

'д', 'н', 'о', 'й', ' ', 'с', 'т',

'р' 'о', 'к', 'о', 'й', ' .', '\0'};

(Обратите внимание на замыкающий нуль-символ. Без него мы имеем массив символов, а не строку.) Для той и другой формы (а мы рекомендуем первую) компилятор подсчитывает символы и таким образом получает размер массива.

Как и для других массивов, имя m1 является указателем на первый элемент массива:


m1 == &m1[0], *m1 == 'Т', и *(m1 + l) == m1[1] == 'о',


Действительно, мы можем использовать указатель для создания строки. Например:


char *m3 = " \n Достаточно обо мне - как вас зовут?";


Это почти то же самое, что и


static char m3[ ] = "\n Достаточно обо мне - как вас зовут?" ;

Оба описания говорят об одном: m3 является указателем строки со словами " Как вас зовут?" . В том и другом случае сама строка определяет размер памяти, необходимой для ее размещения. Однако вид их не идентичен.

Массив или указатель

В чем же тогда разница между этими двумя описаниями? Описание с массивом вызывает создание в статической памяти массива из 38 элементов (по одному на каждый символ плюс один на завершающий символ '\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');


в результате получаем

Я люблю Я люблю

Но только в случае с указателем можно использовать операцию увеличения:

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 задан размер:


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]

однако, определяет "рваный" массив, где длина каждого "ряда" определяется той строкой, которая этот "ряд" инициализировала. Рваный массив не тратит память напрасно.

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);

}

Взглянув на эту программу, вы можете подумать, что она копирует строку "Не делай глупостей!", и при беглом взгляде на вывод вам может показаться правильным это предположение:

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.

В нашей программе для 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( ) предоставляет вам два способа ввода строки!


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( ) имеет два варианта. Для любого из них строка начинается с первого встретившегося непустого символа. Если вы используете формат %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."

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( )

Мы уже обсуждали функцию printf( ) довольно основательно. Подобно puts( ), она использует указатель строки в качестве аргумента. Функция printf( ) менее удобна, чем puts( ), но более гибка.

Разница заключается в том, что printf( ) не выводит автоматически каждую строку текста с новой строки. Вы должны указать, что хотите выводить с новых строк. Так,

printf(" %s\n" , string);

дает то же самое, что и

puts(string);

Вы можете видеть, что первый оператор требует ввода большего числа символов и большего времени при выполнении на компьютере. С другой стороны, printf( ) позволяет легко объединять строки для печати их в одной строке. Например:

printf(" Хорошо, %s, %s \n", name, MSG);

объединяет " Хорошо" с именем пользователя и c символьной строкой MSG в одну строку.

СОЗДАНИЕ СОБСТВЕННЫХ ФУНКЦИЙ

Не ограничивайте себя при вводе и выводе только этими библиотечными функциями. Если у вас нет нужной функции, или она вам не нравится, можно создавать свои собственные версии, используя для этого getchar( ) и putchar( ).

Предположим, у вас нет функции puts( ). Вот один из путей ее создания:

/* put1 - печатает строку */

put1(string);

char *string;

{

while(*string != '\0') putchar(*string++);

putchar('\n');

}


Символьный указатель string вначале ссылается на первый элемент вызванного аргумента. После печати его содержимого указатель увеличивается и ссылается уже на следующий элемент. Это продолжается до тех пор, пока указатель не дойдет до элемента, содержащего нуль-символ. Затем в конце строки будет поставлен символ новой строки.

Предположим, у вас есть puts( ), но вам нужна функция, которая, кроме того, сообщает, сколько напечатано символов. Эту возможность легко добавить:

/* put2- - печатает строку и считывает символы */

put2 (string);

char *string;

{

int count = 0;

while(*string != '\0') {

putchar(* string++);

count++;

putchar('\n');

return(count);

}

Вызов:

put2(" пицца" );

печатает строку пицца, в то время как оператор

num = puts(" пицца");

передаст, кроме того, количество символов в num; в данном случае это число 5. Вот несколько более сложный вариант, показывающий вложенные функции:

/* вложенные функции */

#include

main( )

{

put1("Если бы я имел столько денег, сколько могу потратить,");

рrintf("Я считаю %d символа.\n",

put2(" Я никогда бы нe жаловался, что приходится чинить старые стулья.");

}


(Мы включили в программу при помощи директивы #include файл stdio.h, потому что в нашей системе в нем определена функция putchar( ), а она используется в нашей новой функции.)

Да-а, мы используем функцию printf( ) для печати значения put2( ), но в процессе нахождения значения put2( ) компьютер должен сначала заставить ее поработать - напечатать строку. Вот что получается при этом:

Если бы я имел столько денег, сколько могу потратить,

Я никогда бы нe жаловался, что приходится чинить старые стулья.

Я считаю 63 символа.

Теперь вы можете построить работающую версию функции gets( ); она должна быть похожа на нашу функцию getint( ) из гл. 10, но гораздо проще ее.

ФУНКЦИИ, РАБОТАЮЩИЕ СО СТРОКАМИ

Большинство библиотек языка Си снабжено функциями, работающими со строками. Рассмотрим четыре наиболее полезных и распространенных: strlen( ), strcat( ), strcmp( ) и strcpy( ).

Мы уже применяли функцию strlen( ), которая находит длину строки. Используем ее в нижеследующем примере функции, укорачивающей длинные строки.

Функция strlen( )

/* Функция Прокруста */

fit(string, size)

char *string;

int size;

{

if(strlen(string) > size)

*(string + size) = '\0';

}

Проверьте ее в "деле" в этой тестовой программе:

/* тест */

main( ) {

static char mesg[ ] = "Ну, теперь держитесь, компьютероманы.";

puts(mesg);

fit(mesg, 10);

puts(mesg);

}

Программа выдает:

Ну, теперь держитесь, компьютероманы.

Ну, теперь

Наша функция помещает символ '\0' в одиннадцатый элемент массива, заменяя символ пробела. Остаток массива остается на старом месте, но puts( ) прекращает работу на первом нуль-символе и игнорирует остаток массива.

Функция strcat( )

Вот что умеет делать функция strcat( ):

/* объединение двух строк */

#include

< stdio.h>

main( )

{

static char flower [80];

static char addon[ ] = "ы пахнут старыми ботинками.";

puts(" Назовите ваш любимый цветок." );

gets(flower);

strcat (flower, addon);

puts(flower);

puts(addon);

}

Получаем на экране:

Назовите ваш любимый цветок.

Ирис

Ирисы пахнут старыми ботинками.

Очевидно, что strcat( ) (string concatenation) использует в качестве аргументов две строки. Копия второй строки присоединяется к концу первой, и это объединение становится новой первой строкой. Вторая строка не изменяется.

Внимание! Эта функция не проверяет, умещается ли вторая строка в первом массиве. Если вы ошиблись при выделении памяти для первого массива, то у вас возникнут проблемы. Конечно, можно использовать strlen( ) для определения размера строки до объединения.

/* Объединение двух строк, проверка размера первой */

#include

#define SIZE 80

main( )

{

static char flower[SIZE];

static char addon[ ] = " ы пахнут старыми ботинками." ;

puts(" Назовите ваш любимый цветок. ");

gets(flower);

if((strlen(addon) + strlen(flower) + 1) < SIZE)

strcat (flower, addon);

puts(flower);

}

Мы добавляем 1 к объединенной длине для размещения нуль-символа.

Функция strcmp( )

Предположим, что вы хотите сравнить чей-то ответ со строкой, находящейся в памяти:

/* Будет ли это работать? */

#include

#define ANSWER " Грант"

main( )

{

char try [40];

puts(" Кто похоронен в могиле Гранта?" );

gets(try);

while(try != ANSWER)

puts(" Нет, неверно. Попытайтесь еще раз." );

gets(try);

} puts(" Правильно.");

}


Хотя эта программа и смотрится неплохо, она не будет работать правильно, try и ANSWER на самом деле являются указателями, поэтому сравнение (try != ANSWER) спрашивает не о том, одинаковы ли эти две строки, а одинаковы ли два адреса, на которые ссылаются try и ANSWER. Так как ANSWER и try запоминаются в разных ячейках, эти два указателя никогда не могут быть одним и тем же, и пользователю всегда сообщается, что программа неверна. Такие программы обескураживают людей.

Нам нужна функция, которая сравнивает содержимое строк, а не их адреса. Можно было бы придумать ее, но это уже сделала за нас функция strcmp( ) (string comparision).

Теперь исправим нашу программу:

/* это будет работать */

#includе

#define ANSWER " Грант"

main( )

{

char try [40];

puts(" Кто похоронен в могиле Гранта?" );

gets(try);

while(strcmp(try, ANSWER) != 0)

{ puts(" Нет, неверно. Попытайтесь еще раз.");

gets(try);

} puts(" Правильно!");

}

Так как ненулевые значения интерпретируются всегда как "true", мы можем сократить оператор while do while(strcmp(try, ANSWER)).

Из этого примера можно сделать вывод, что strcmp( ) использует два указателя строк в качестве аргументов и возвращает значение 0, если эти две строки одинаковы. Прекрасно, если вы придете к такому выводу.

Хорошо, что Strcmp( ) сравнивает строки, а не массивы. Поэтому, хотя массив try занимает 40 ячеек памяти, а " Грант" - только 6 (не забывайте, что одна нужна для нуль-символа), сравнение выполняется только с частью try, до его первого нуль-символа. Такую функцию strcmp( ) можно использовать для сравнения строк, находящихся в массивах разной длины.

А что если пользователь ответил " ГРАНТ" или " грант" или "Улиссес С. Грант" ? Хорошо, если пользователю сказали, что он ошибся? Чтобы сделать программу гибкой, вы должны предусмотреть несколько допустимых правильных ответов. Здесь есть некоторые тонкости. Вы могли бы в операторе #define определить в качестве ответа " ГРАНТ" и написать функцию, которая превращает любой ответ только в это слово. Это устраняет проблему накопления, но остаются другие поводы для беспокойства.

Между прочим, какое значение возвращает strcmp( ), если строки не одинаковы? Вот пример:

/* возвраты функции strcmp */

#include

main( )

{

printf(" %d \n" , strcmp( "A" , " A" ));

printf(" %d \n" , strcmp( "A" , " B" ));

printf(" %d \n" , strcmp( "B" , " A" ));

printf(" %d \n" , strcmp( "C" , "A" ));

printf(" %d \n" , strcmp(" apples", " apple"));

}

В результате получаем

0 -1

1

2 115


Как мы и предполагали, сравнение "А" с самим собой возвращает 0. Сравнение "А" с "В" дает -1, а "В" с "А" дает 1. Это наводит на мысль, что strcmp( ) возвращает отрицательное число, если первая строка предшествует второй в алфавитном порядке, или положительное число, если порядок иной. Кроме того, сравнение "С" с "А" дает 2 вместо 1. Картина проясняется: функция возвращает разницу между двумя символами в коде ASCII. В более общем смысле strcmp() передвигается вдоль строк до тех пор, пока не находит первую пару не совпадающих символов; затем она возвращает разницу в кодах ASCII. Например, в самом последнем примере "apples" и "apple" совпадают, кроме последнего символа 's', в первой строке. Он сопоставляется с шестым символом в "apple", который является нуль-символом (0 в ASCII).

Возвращается значение

's' - '\0' = 115 - 0 = 115,

где 115 является кодом буквы 's' в ASCII.

Обычно вам не нужно точно знать возвращаемое значение. Чаще всего вы только хотите знать, нуль это или нет, т. е. было ли совпадение. Или, может быть, вы пытаетесь отсортировать строки в алфавитном порядке и хотите узнать, в каком случае сравнение дает положительный, отрицательный или нулевой результат.

Можно использовать эту функцию, чтобы проверить, остановится ли программа, читая вводимую информацию:

/* Начало какой-то программы */

#include &stdio.h&

#define SIZE 81

#define LIM 100

#define STOP " " /* нулевая строка */

main( )

{

static char input[LIM][SIZE];

int ct = 0;

while(gets(input[ct]) != NUL && strcmp(input[ct].STOP) !=0

&& ct++ < LIM)

...

}


Программа прекращает чтение вводимой строки, если встречает символ EOF [в этом случае gets( ) возвращает NULL], или если вы нажимаете клавишу [ввод] в начале строки (т.e. введете пустую строку), или если вы достигли предела LIM. Чтение пустой строки дает пользователю простой способ прекращения ввода.

Давайте перейдем к последней из обсуждаемых нами функций, работающих со строками.

Функция strcpy( )

Мы уже говорили, что если pts1 и pts2 являются указателями строк, то выражение

pts2 = ptsl;

копирует только адрес строки, а не саму строку. Предположим, что вы все же хотите скопировать строку. В этом случае можно использовать функцию strcpy( ). Она работает примерно так:

/* демонстрация strcpy( ) */

#include

#define WORDS "Проверьте, пожалуйста, вашу последнюю чапись."

main( ) {

static char *orig = WORDS;

static char copy [40];

puts(orig);

puts(copy);

strcpy(copy, orig);

puts(orig);

puts(copy);

}

Вот результат:

Проверьте, пожалуйста, вашу последнюю запись.

Проперьтe, пожалуйста, пашу последнюю запись.

Проверьте. пожалуйста, пашу последнюю запись.

Очевидно, что строка, на которую указывает второй аргумент (orig) функции strcpy( ), скопирована в массив, на который указывает первый аргумент (copy). Порядок аргументов функции такой же, как в операторе присваивания: строка, получающая значение, стоит слева. (Пустая строка является результатом печати массива copy до копирования, и она говорит о том, что статические массивы .инициализируются нулями, т. е. нуль-символами в символьном виде.)

Нужно обеспечить, чтобы размер массива, принимающего строку, был достаточен для ее размещения. Поэтому мы используем описание

static char copy [40];

а не

static char *copy; /* не выделяет память для строки */

Короче говоря, strcpy() требует два указателя строк в качестве аргументов. Второй указатель, ссылающийся на исходную строку, может быть объявленным указателем, именем массива или строковой константой. А первый указатель, ссылающийся на копию, должен ссылаться на массив или часть массива, имеющего размер, достаточный для размещения строки.

Теперь, когда мы описали несколько функций, работающих со строками, рассмотрим целую программу, работающую со строками.

ПРИМЕР: СОРТИРОВКА СТРОК

Возьмем реальную задачу сортировки строк в алфавитном порядке. Эта задача может возникнуть при подготовке списка фамилий, при создании алфавитного указателя и во многих других ситуациях. В такой программе одним из главных инструментов является функция strcmp( ), так как ее можно использовать для определения старшинства двух строк. Последовательность наших действий будет состоять из считывания массива строк, их сортировки и последующего вывода. Совсем недавно мы показали последовательность действий для считывания строк, и сейчас мы начнем программу таким же образом.

/* считывает строки и сортирует их */

#include

#define SIZE 81 /* предельная длина строки, включая \0 */

#define LIM 20 /* максимальное количество считываемых строк */

#define HALT " " /* нулевая строка для прекращения ввода */

main( )

{

static char input[LIM][SIZE]; /* массив для запоминания вводимых строк */

char *ptstr[LIM]; /* массив переменных типа указатель */

int ct = 0; /* счетчик вводимых строк */

int k; /* счетчик выводимых строк */

printf(" Введите до %d строк и я их отсортирую.\n" , LIM);

printf(" Для прекращения ввода нажмите клавишу [ввод] в начале строки.\n");

while((gets(input[ct])!= NULL) && strcmp(input[ct], HALT)

!= 0 && ct++ < LIM)

ptstr[ct - 1] = input[ct - 1]; /*указывает на еще не

отсортированный ввод */

stsrt(ptstr, ct); /* сортировка строк */

puts(" \n Вот отсортированный список строк:\n");

for(k = 0; k < ct; k++)

puts(ptstr[k]); /* указатели на отсортированные строки */

}

/* функция сортировки-строк-с-использованиeм-указатeлeй */

stsrt(strings, num)

char *strings[ ];

int num;

{ char *temp;

int top, seek;

for(top = 0; top < num-1; top++)

for(seek = top + 1; seek < num; seek++)

if(strcmp(strings[top], strings[seek]) > 0)

{ temp = strings [top];

strings [top] = strings [seek];

strings [seek] = temp;

} }

РИС. 13.4. Программа чтения и сортировки строк.

Вывод строк на печать не составляет проблемы, а для сортировки можно взять тот же алгоритм, который использовался раньше для чисел. Сейчас мы применим один хитрый трюк: посмотрим, сможете ли вы его заметить.

Для проверки возьмем детский стишок.

Введите 20 строк, и я их отсортирую.

Для прекращения ввода нажмите клавишу [ввод] в начале строки.

Жил на свете человек

Скрюченные ножки

И гулял он целый век

По скрюченной дорожке

Вот отсортированный список строк

Жил на свете человек

И гулял он целый век

По скрюченной дорожке

Скрюченные ножки

Детские стишки не кажутся слишком искаженными после сортировки их по алфавиту.

Трюк состоит в том что вместо перегруппировки самих строк мы перегруппировали их указатели. Разберемся в этом. В начале ptrst[0] ссылается на input[0] и т. д. Каждый input[ ] является массивом из 81 элемента, а каждый элемент ptrst[ ] является отдельной переменной. Процедура сортировки перегруппировывает ptrst, нe трогая input. Если, например, input[l] стоит перед input[0] по алфавиту, то программа переключает указатели ptrst, в результате чего ptrst[0] ссылается на input[1], a ptrst[1] на input[0]. Это гораздо легче, чем, используя strcpy( ), менять местами две введенные строки. Просмотрите еще раз этот процесс на рисунке.

И наконец, давайте попытаемся заполнить пробелы, оставшиеся в нашем описании, а именно "пустоту" между скобками в функции main( ).


АРГУМЕНТЫ КОМАНДНОЙ СТРОКИ

Командная строка - это строка, которую вы печатаете на клавиатуре, чтобы запустить вашу программу. Это нетрудно. Предположим, у нас есть программа в файле с именем fuss. В этом случае командная строка выглядела бы так:

РИС. 13.5. Указатели сортируемых строк.

или, может быть,

А > fuss

с использованием двух системных приглашений.

Аргументы командной строки являются дополнительными элементами в той же самой строке:

%fuss - r Ginger

Следует заметить, что программа на языке Си может вводить информацию в эти элементы и применять их для собственных нужд. Этот механизм предназначен для использования аргументов функции main( ). Вот типичный пример:

/* main( ) с аргументами */

main(argc, argv)

int argc;

char *argv[ ];

{

int count;

for(count = 1; count < argc; count++)

printf(" %s", argv[count]);

printf("\n");

}

Поместите эту программу в выполняющий файл, названный echo, и вот что произойдет:

А > echo

Я мог бы воспользоваться небольшой помощью.

Я мог бы воспользоваться небольшой помощью.

Вероятно, вы видите, почему функция называется echo, но еще не можете понять, как она работает. Может быть, это объяснение поможет вам (мы надеемся).

Компиляторы Си предполагают наличие у main( ) двух аргументов. Первый аргумент представляет количество строк, следующих за командным словом. Обычно (но не обязательно) этот аргумент типа int называется argc (argument count). Система использует пробелы, чтобы сообщить о конце одной строки и начале следующей. Так, наш пример с echo имеет шесть строк, а пример с fuss имел две строки. Второй аргумент является массивом указателей строк. Каждой строке, входящей в командную строку, присваивается ее

собственный указатель. По соглашению, этот массив указателей называется argv (argument values). Если можно (некоторые операционные системы не позволяют этого), элементу argv[0] присваивается имя самой программы. В этом случае аргументу argv[l] присваивается первая следующая строка и т. д. Для нашего примера имеем

argv[0] ссылается на echo (для большинства систем)

argv[1] ссылается на я

argv [2] ссылается на мог

argv [6] ссылается на помощью

Поскольку вы используете эти обозначения, то можете легко проследить остаток программы.

РИС. 13.6. Аргументы командной строки.

Многие программисты используют для argv и другие обозначения:

main(arge, argv)

int arge;

char **argv;

Описание argv на самом деле эквивалентно char *аrgv[ ];. Читая его, вы могли бы сказать, что argv является указателем на указатель на типchar. Наш пример дает то же самое. У нас был массив из семи элементов. Имя массива является указателем на его первый элемент. Поэтому argv ссылается на argv[0], a argv[0] является указателем на тип char. Следовательно, даже с исходным определением argv является указателем на указатель на тип char. Вы можете использовать любую из этих форм, но видно, что первая проще для понимания.

Очень часто аргументы командных строк используются для указания возможностей программы. Например, можно применять комбинацию символов - r, чтобы заставить программу выполнять сортировку в обратном порядке. Обычно альтернативы задаются при помощи дефиса и буквы, как - r. Эти "флажки" ничего нс означают в языке Си; вы должны сами запрограммировать их распознавание.

Вот очень простой пример, показывающий, как программа может проверять и использовать флажок.

/* обычное начало */

#define YES 1

#define NO 0

main(argc, argv)

int argc;

char *argv[ ];

{

float array[100];

int n;

int flag = NO;

if(argv[l][0] == '-' && argv[l][l] == 'r')

flag = YES;

...

if flag = NO

sort1(array, n);

else

sort2(array, n);

...

}

Эта программа проверяет первую строку после имени командного файла, чтобы посмотреть, начинается ли она с дефиса. Затем она проверяет, является ли следующий символ кодом буквы r. Если это так, то устанавливается флажок, приводящий к использованию другой программы сортировки. Строки после первой игнорируются. Как мы уже сказали, этот пример достаточно прост.

Если вы использовали систему UNIX, то, вероятно, заметили, что команды UNIX предоставляют разнообразные варианты командной строки и ее аргументов. Эти примеры показывают, как использовать аргументы командной строки языка Си, поскольку большая часть системы UNIX написана на языке Си.

Аргументы командных строк могут быть также именами файлов, и вы можете использовать их вместо операторов переключения, чтобы указать программе, над какими файлами надо работать. Мы покажем вам, как это делается, в гл. 15.

ЧТО ВЫ ДОЛЖНЫ БЫЛИ УЗНАТЬ В ЭТОЙ ГЛАВЕ

Как объявить строку символов: static char fun[ ] и т. д.

Как инициализировать строку символов: static char *p0 = "0!"

Как использовать gets( ) и puts( )

Как использовать strlen( ), strcmp( ), strcpy( ) и strcatf( )

Как использовать аргументы командной строки.

В чем сходство и различие описателей char *bliss и char bliss[ ]

Как создать строковую константу: "используя кавычки".

ВОПРОСЫ И ОТВЕТЫ

Вопросы

1. Что неправильно в этой попытке описания символьной строки?

main( ) {

char name[ ] = {'F', 'с', 's', 's'};

2. Что напечатает эта программа?

#include

main( )

{

static char note[ ] = "До встречи в буфете." ;

char *ptr;

ptr = note;

puts(ptr);

puts(++ptr);

note[7] = '\0';

puts(note);

puts(++ptr);

}

3. Что напечатает эта программа?

main( )

{ static char food[ ] = "Йумми";

char *ptr;

ptr = food + strlen(food);

while(--ptr >= food) puts(ptr);

}

4. Что напечатает нижеследующая программа?

main( )

{

static char goldwyn[28] = " аз я считываю"

static char samuel[40] = " Каждый р" ;

char *quote = " часть строки."

strcat(goldwyn, quote);

strcat(samuel, goldwyn);

puts(samuel);

}


5. Создайте функцию, которая использует указатель строки в качестве аргумента и возвращает указатель, ссылающийся на первый пробел в строке в указанном месте или после него. Если она не находит ни одного пробела, то пусть возвращает NULL-указатель.

Ответы

1. Класс памяти должен быть extern или static; инициализация должна включать символ '\0'.

2.

До встречи в буфете.

о встречи в буфете.

До вст

вст

3.

и

ми

мми

умми

Йумми

4.

Каждый раз я считываю часть строки.

5.

char *strblk(string)

char *string; {

while(*string != ' ' && *string != '\0')

string++; /* останавливается на первом пробеле или нуль-символе */

if(*string == '\0')

return(NULL); /* NULL = 0 */

else return(string); }


УПРАЖНЕНИЯ

1. Создайте функцию, которая считывает очередные n символов при вводе, включая символы пробелов, табуляции и новой строки.

2. Модифицируйте последнюю функцию таким образом, чтобы она останавливалась после ввода n символов или после первого символа пробела, табуляции или но вой строки независимо от того, какой из них идет первым [только не используйте функцию scanf( )].

3. Создайте функцию, которая считывает очередное слово при вводе; определите слово как последовательность символов, не включающую символы пробела, табуляции или новой строки.

4. Создайте функцию, которая ищет первое появление определенного символа в определенной строке. Функция должна возвращать указатель, ссылающийся на этот символ, в случае успешного поиска или NULL, если символ в строке не найден.


Загрузка...