C. Глава 3. Строки

Feb 19, 2018 20:33


"Текстовый поток - это последовательность символов, разбитая на строки, каждая из которых содержит нуль или более символов и завершается символом новой строки. Обязанность следить за тем, чтобы любой поток ввода-вывода отвечал этой модели, возложена на библиотеку: программист, пользуясь библиотекой, не должен заботиться о том, в каком виде строки представляются вне программы."

(Б. Керниган, Д. Ритчи "Язык программирования C")

1. Самый распространенный вид массива
Это массив элементов типа char. Другими словами, строки. Каждая строка заканчивается символом новой строки только в файловом потоке. В памяти дело обстоит немного иначе, строка заканчивается нулем. На длину строки не накладывается никаких ограничений. Что касается байтов, то здесь ограничения есть. В текстовых строках можно использовать только печатаемые символы. Такими символами являются буквы, цифры, знаки препинания, пробелы и иногда символы псевдографики (теперь устарели). Символы могут быть многобайтовыми (UNICODE).

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

Любой массив имеет такую важную характеристику, как число элементов в нем. Для строки, заданной явно (при помощи индексного оператора), мы можем определить число элементов, точнее, байтов, с помощью оператора sizeof. Другое дело, если строка доступна по указателю. Здесь невозможно сказать, какую длину она имеет. Поэтому нам будет очень полезна функция, измеряющая длину строки. Она имеет очень простое устройство:

//strings.c - работа со строками

int strlen( char *s )
{
char *p = s;
while( *p ) p++;
return p - s;
}

char *empty = "";
char str[] = "Test string";
char *ptr = "Another test string";

main()
{
puts( itoa( strlen( empty ), 10 ) );
puts( itoa( strlen( str ), 10 ) );
puts( itoa( strlen( ptr ), 10 ) );
}
Лст. 3-1. Функция strlen.

Цикл внутри функции использует дополнительный указатель, который проходит строку до тех пор, пока не обнаружит завершающий ноль, байт 0x00 или '\0', что то же самое. Эта ситуация приводит к тому, что в скобках while выражение становится ложным и цикл прекращается, то есть, указатель не будет перемещен на следующий байт. Затем остается немного адресной арифметики - мы находим разницу между указателями, исходным и текущим, и возвращаем ее как целое число:

$ m strings; strings
0
11
19
$
Лст. 3-2. Тест strlen.

Стандартная библиотека языка содержит целое семейство функций str... из которых мы рассмотрим самые часто встречающиеся.

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

char *strcpy( char *d, char *s )
{
char *dptr = d;
while( *s ) *d++ = *s++;
*d = '\0';
return dptr;
}

char buffer1[8] = "data";
char buffer2[10] = "something";
int val = 2018;

main()
{
puts( buffer1 );
puts( buffer2 );
puts( itoa( val, 10 ) );
putchar( '\n' );

strcpy( buffer1, "Misplaced string here" );

puts( buffer1 );
puts( buffer2 );
puts( itoa( val, 10 ) );
}
Лст. 3-3. Ошибка в программе, вызвавшей функцию.

$ m strings; strings
data
something
2018

Misplaced string here
d string here
101
$
Лст. 3-4. Ошибка в программе, вызвавшей функцию (вывод).

Функция strcpy слишком очевидна, чтобы быть написанной неправильно. В самом деле, пусть s указывает на '\0'. Тогда условие ложно и while не выполнится ни разу. Целевая строка не изменится. Пусть s указывает на первый байт строки, состоящей из одного байта. Тогда цикл выполнится один раз, по адресу d будет записан байт из s (*s), второй раз цикл не будет выполнен, так как s указывает на нуль. И так далее. strcpy возвращает указатель на целевую строку, чтобы можно было использовать функцию в выражениях.

Тест функции направлен не на проверку ее правильности, а на демонстрацию того, что может быть с данными при неаккуратном программировании. Чтобы переменные все попали в сегмент данных последовательно, они инициализированы. Сначала мы показываем их содержимое, которое выглядит в точности, как было в исходном коде. Затем вызывается strcpy, которая записывает (копирует) в buffer1 строку слишком большого размера. Это приводит к затиранию соседних данных и порче двух переменных, расположенных подряд.

Хакеры могут использовать такие вещи, называемые уязвимостями, для получения контроля над программами. Например, можно сделать так, чтобы в переменной val оказалось число 1000, и при этом содержимое buffer1 и buffer2 оставалось неизменным. Для того, чтобы это сделать, достаточно просто сформировать строку из нужных байтов. Точно такие же вещи проделываются с сегментами кода. Программа подменяется в памяти и ведет себя так, как нужно злоумышленникам. Поэтому надо писать ее так, чтобы уязвимостей не было. Измененная программа может быть даже сохранена на диске.

И прежде, чем мы напишем следующую из str... функций, давайте поговорим о функции gets. Это функция, обратная puts, она читает данные из потока stdin. Поскольку эта функция также не контролирует длину поступающих данных, как и strcpy, то она гораздо более опасна, чем strcpy. Здесь уже не программист, а злоумышленник получает прямой доступ к переменным программы. Никогда не следует использовать эту функцию, и мы не будем писать ее, хотя ясно как это сделать.

Существует гораздо более безопасная функция fgets, которая читает из любого файлового потока. fgets очень близка к gets, но позволяет остановить копирование в любом случае. Если мы пишем программу, которая должна принимать данные в буфер размером n байтов, то мы должны использовать функцию, которая копирует из потока ровно такое же число, или меньше, но ни одним байтом больше.

2. Функция fgets
Этот небольшой раздел создан, чтобы отделить функцию от основной темы. Разберем ее код:

char *fgets( char *s, int count, int fd )
{
char *buff = s;
int i = 0;
char c;
c = getc( fd ); // чтение первого символа
if( c == -1 ) return 0; // если читать нечего
else *s++ = c; // иначе продолжаем
i++;
while(1)
{
c = getc( fd );
if( c == -1 || c == 0x0A || i == count )
{
*s = '\0';
return buff;
}
*s++ = c; // добавление очередного символа
i++; // подсчет символов
}
}
Лст. 3-5. Код функции fgets.

Функция читает строку из файлового потока. Она изменяет данные по указателю s. Во втором аргументе функции передается число наибольшее байтов, которое следует скопировать. И наконец, последний аргумент - это дескриптор файла.

Сначала функция проверяет первый байт в файловом потоке. Если он вернул EOF, то есть, -1, то fgets возвращает 0, говорящий о том, что копировать нечего. В противном случае, байт из потока сохраняется по адресу s, и указатель передвигается дальше. Скопированный символ также подсчитывается в переменной i.

Затем начинается цикл чтения из потока. Каждый раз проверяются три условия: EOF - конец файла, символ новой строки, и ограничение на длину, заданное при вызове функции. Если выполняется хотя бы одно из этих условий, строка s завершается нуль-терминатором и мы возвращаем указатель на нее, ненулевое значение которого означает, что в s что-то присутствует. Символ новой строки, таким образом, из потока удаляется.

Проверим, как функция работает. Пусть, например, она читает строки и печатает их, до тех пор, пока не обнаружит, что в потоке оказался символ EOF.

main()
{
char buffer[24];
char *p = buffer;

while( 1 )
{
p = fgets( buffer, 24, 0 );
if( !p ) return;
puts( buffer );
puts( "------------" );
}
}
Лст. 3-6. Тестовая программа для fgets.

$ m strings
strings.o: In function `main':
strings.c:(.text+0x195): undefined reference to `__stack_chk_fail'
$
Лст. 3-7. Привет от компилятора.

Чего-то подобного давно следовало ожидать в результате нашего стиля его использования. Компилятор, обнаружив что мы чем-то обременили стек, вызывает функцию-обработчик для данной ситуации и помещает ее в конце (это приблизительно видно по смещению в сегменте кода: .text+0x195). Это все забота о безопасности (защита стека). Помнится, что в старых версиях этого не было. Но бог с ней. Не будем глубоко копать этот вопрос. Нам ничто не мешает объявить данные в сегменте данных. Я пишу этот пример для тех, кто может столкнуться с подобным поведением компилятора.

Вот вторая версия, которая наверняка заработает:

char buffer[24];

main()
{
char *p = buffer;

while( 1 )
{
p = fgets( buffer, 24, 0 );
if( !p ) return;
puts( buffer );
puts( "------------" );
}
}
Лст. 3-8. Новая попытка.

Мы читаем данные с клавиатуры, пока пользователь не отправит в поток EOF, с помощью комбинации клавиш Ctrl-D. Тогда функция вернет нулевой указатель и мы сможем проверить это оператором if. Поскольку p == 0, то есть, выражение вычисляется как ложное, то оператор НЕ: ! делает его истинным и происходит возврат из функции main.

$ m strings
$ strings
hello
hello
------------
hell
hell
------------
hh
h
------------
123456789012345678901234567890
123456789012345678901234
------------
67890
------------
$
Лст. 3-9. Тест fgets.

fgets исправно реагирует на строки, на ограничение длины строк, и на Ctrl-D. Хвост из файла stdin попадает на экран монитора, но fgets тут ни причем. Он появляется после отправки туда EOF. Самое главное - содержимое буфера не переполняется после вызова fgets. Просили 24 символа, - получили 24. Вообще-то, во избежание переполнения буфера, надо было объявлять или массив из 25 символов, либо ограничить копирование 23-мя символами. Если мы используем нелатинские символы (utf-8), то в буфер поместится только 12 букв, например, русского алфавита:

$ strings
Гуляй, Вася!
Гуляй, Вася!
------------
Ты не понял, что сказал Шекспир??!!
Ты не понял, ч
------------
�о сказал Шек
------------
пир??!!
------------
$
Лст. 3-10. Реакция на символы UNICODE.

Функция fgets также исправно работает с дисковыми файлами, можно использовать перенаправление и конвейер.

$ cat file
hello!
$ strings < file
hello!
------------
$ cat file | strings
hello!
------------
$
Лст. 3-11. fgets читает из файла на диске.

Теперь есть пара функций: puts и fgets, которые можно использовать для работы с файлами stdin и stdout. Функцию fputs разберем позже, когда специально займемся файлами.

3. Функции str...
Две, наиболее часто используемые функции уже написаны: strlen и strcpy. Есть еще довольно много других, из которых выберем:

strcat - дописывает строку другой строкой
strset - заполняет строку заданным символом
strncpy - копирует строку до n символов
strncat - соединяет строки до n символов
strchr - пытается найти в строке заданный символ
strstr - пытается найти в строке заданную подстроку
strcmp - сравнивает две строки между собой
strtok - разделяет строку на части по заданным разделителям

Есть еще несколько функций str... в которых используются ограничения на длину строк, есть функция stricmp, для сравнения строк, но безразличная к регистру символов. Мы их рассматривать не будем. По крайней мере, пока.

Функция strcat соединяет две строки. Принимающая строка должна быть достаточно длинной, чтобы в нее поместилась копируемая строка.

char *strcat( char *d, char *s )
{
strcpy( d + strlen( d ), s );
return d;
}

char source[] = "abracadabra";
char dest[16] = "demo";
char near[] = "2018";

main()
{
puts( strcat( dest, source ) );
puts( near );
}
Лст. 3-12. Функция strcat.

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

$ m strings; strings
demoabracadabra
2018
$
Лст. 3-13. Тест strcat.

В первом аргументе strcat используется адресная арифметика: strlen возвращает длину строки, которую мы прибавляем к указателю строки назначения и получаем адрес нуль-терминатора этой строки. Затем просто копируем строку-источник. А вот что может быть в том случае, если строка-приемник слишком коротка. Например, используем в источнике текст abracadabradыbra:

$ m strings
$ strings
demoabracadabradыbra
ыbra
$
Лст. 3-14. Второй тест strcat.

Произошло то же самое, что и в предыдущем случае (см. strcpy). За это язык C не любят (ленивые и небрежные програмеры). Керниган сравнил его с острым ножом: который в руках хирурга полезен, но может быть и очень вредным для здоровья, в руках идиота. Так вот, просто не надо быть идиотом, а острый нож в хозяйстве всегда полезен. В конце-концов, без языка C у нас не было бы операционных систем, которые все практически исключительно написаны на языке C, а без них не было бы ни интернета, ни игр, ни многих других полезных вещей.

Теперь, чтобы просто показать силу функций, напишем новую версию программы из главы 1.3 (Рис. 1-2). Она делает то же самое: спрашивает, как зовут пользователя и приветствует его.

char ask[] = "Как вас зовут?";
char end[] = ", как дела?";
char answer[128];
char buffer[ sizeof(ask) +
sizeof(end) + 128 ] = "Привет, ";

main()
{
puts( ask );
if( fgets( answer, 128, 0 ) )
{
strcat( buffer, answer );
strcat( buffer, end );
puts( buffer );
}
}
Лст. 3-15. Hello 3.0.

Программа стала намного понятнее чем на Рис. 1-2. из первой главы.

Теперь рассмотрим функцию strset. Она заполняет строку заданным символом. Иногда такое бывает нужно. Например, чтобы забить звездочками информацию, которую другим видеть нежелательно: телефонный номер, адрес, e-mail и т.д.

char *strset( char *s, int c )
{
char *p = s;
while( *p ) *p++ = c;
return s;
}

char buff[16] = "qwerty";

main()
{
puts( buff );
puts( strset( buff, '*' ) );
}

. . . . . . . . . .

$ strings
qwerty
******
$
Лст. 3-16. Функция strset.

Функция strncpy аналогична strcpy, но выполняет копирование не всей строки-источника, а только n первых символов из нее.

char *strncpy( char *d, char *s, int n )
{
char *dptr = d;
if( n < 1 ) return d;
while( *s && n-- ) *d++ = *s++;
*d = '\0';
return dptr;
}
Лст. 3-17. Функция strncpy.

В strncpy есть небольшая разница с strcpy. Во-первых, мы проверяем значение n, не окажется ли оно бессмысленным: нормальный человек не будет копировать 0 символов или их отрицательное число. Даже нормальный математик потребовал бы определить, что значит скопировать -n символов.

Второе отличие в том, что теперь, одновременно с проверкой строки-источника на нуль-терминатор мы проверяем счетчик заданного числа байтов. Условие оператора while требует, чтобы одновременно и байт был не нуль-терминатор, и счетчик непуст, - они связаны логическим И.

Функция strncat очень похожа на strcat, только в ней вместо strcpy вызывается strncpy:

char *strncat( char *d, char *s, int n )
{
strncpy( d + strlen( d ), s, n );
return d;
}
Лст. 3-18. Функция strncat.

Здесь нечего комментировать, если strncpy работает правильно, то и strncat будет работать так же.

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

Кстати, давно следовало об этом сказать, что нулевой указатель используется только для явной проверки успешности работы функции, т.е. в условных операторах. При попытке что-то сделать по этому адресу, программа завершается с ошибкой Segmentation fault. Нельзя лезть в чужую память.

char *strchr( char *s, int c )
{
char *p = s;
while( *p != c && *p != '\0' ) p++;
return *p == c ? p : 0;
return 0;
}
Лст. 3-19. Функция strchr.

Цикл while ищет заданный символ или завершающий ноль строки и заканчивается в зависимости от того, что раньше наступит. Пока символ НЕ является искомым И НЕ является нулевым, поиск продолжается. Следующий оператор проверяет условие совпадения с символом. Если оно истинно, возвращается указатель на него, иначе ноль. Если обнаружен конец строки, то тройной условный оператор не будет выполнен, на этот случай мы явно возвращаем ноль.

Функция strchr, определенная таким образом, не будет работать с "широкими" символами UNICODE, например, кириллицей. В этом случае символ не будет обнаружен, даже если он есть в строке, поскольку мы проверяем только один байт. Функция вернет ноль.

Будет ли функция работать правильно во всех случаях? Например, что произойдет, если функция так и не обнаружит заданный символ? Не проскочит ли поиск за пределы строки? Это можно легко проверить на следующем примере:

char *strchr( char *s, int c )
{
char *p = s;
while( *p != c && *p != '\0' ) p++;

putchar(*(p-2)); // предпоследний
putchar(*(p-1)); // последний
exit(); //

return *p == c ? p : 0;
}

char test[] = "bcd"; // строка из трех символов

main()
{
strchr( test, 'a' ); // ищем символ, которого нет
}

. . . . . . .

$ m strings
$ strings
cd$

Лст. Проверка функции strchr.

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

Небольшое замечание о скобках в скобках. Мы имеем тут дело с темой, которой я не касался, надеясь на интерес читателей к книге Кернигана и Ричи. Но пару слов сказать надо. Выражение *(p-1) будет правильным, потому, что приоритет вычитания ниже, чем приоритет разыменования указателя (извлечения данных по указателю). Выполнение *p-1 должно дать нам 'a', если *p указывает на 'b'. В самом деле, пусть *p == 'b', но b следует за a ровно на единицу. То есть, сначала мы получаем байт, потом отнимаем от него единицу и получаем предшествующую фактической букву.

В действительности результат может оказаться еще загадочней. Попробуем переписать аргумент putchar как *p-1 (ну, и *p-2 уже тогда) и запустим программу:

$ m strings
$ strings
��$ strings | hd
00000000 fe ff |..|
00000002
$
Лст. -21. Компилятор забыл о *p.

Мы получим вообще непонятные символы!

Посмотрим их коды, направив поток данных из программы по конвейеру в утилиту hd. Нетрудно видеть (если вы читали предыдущую главу), что это просто числа -2 и -1 в байтовом представлении. Вот такие вещи может выдать компилятор из-за путаницы в контексте операторов.

Можете попробовать сами. Только не забудьте потом убрать из функции отладочный код.

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

Зачем нужна функция strchr? Она может быть полезна во многих случаях. Вот один из них:

char buff[] = "Заголовок
"
"
Абзац текста...
";

main()
{
char *start = buff, *stop;

start = strchr( start, '>' ) + 1;
stop = strchr( start, '<' );
*stop = '\0';

puts( start );
}

. . . . . . . . .

$ m strings
$ strings
Заголовок
$
Лст. 3-22. Разбор строки с тегами html.

Мы ищем пару угловых скобок, которыми ограничено содержимое контейнера из парных тегов. Указатель start инициализируется сначала адресом буфера, строки, в которой будет производиться поиск. Затем этот указатель используется при вызове функции, мы также передаем ей символ, который хотим найти. Единица прибавляется к значению, возвращаемому функцией, чтобы указать на следующий за '>' символ. Аналогично отыскивается место, где необходимо остановиться, и туда мы записываем нуль-терминатор (вместо '<'). Затем остается просто напечатать строку. Эта версия кода не гарантирована от ошибок, но это лишь иллюстрация принципа, а не рабочий инструмент.

Возможно, это и не самый удачный пример. Но функция strchr может понадобиться и внутри другой str... функции:

char *strstr( char *s, char *n )
{
char *f, *p;
f = strchr( s, *n ); // находим 1-е совпадение
if( !f ) return 0; // нет? выходим
p = f; // иначе сохраняем указатель
while( *p++ == *n++ // цикл сравнивает все
&& *p != '\0' // последующие символы
&& *n != '\0' ); // до конца (одного из)
if( *n == '\0' ) return f; // нашлось, указываем
return 0; // не нашлось, возвращаем NULL
}

char buff[] = "The Time Machine by H.G. Wells";
char *empty = "";

main()
{
char *start;

start = strstr( empty, "" );
if( start ) puts( start );

start = strstr( empty, "something" );
if( start ) puts( start );

start = strstr( buff, "Timer" );
if( start ) puts( start );

start = strstr( buff, "by" );
if( start ) puts( start );

start = strstr( buff, "Wellsss" );
if( start ) puts( start );
}

. . . . . . . .

$ strings

by H.G. Wells
$
Лст. 3-23. Функция strstr поиска подстроки в строке.

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

1. Поиск пустой подстроки в любой строке (пустой или непустой). Его правильность обеспечивается функцией strchr.

2. Перекрытие строк. Это когда мы ищем подстроку, более длинную, чем оставшаяся часть строки, в которой ищем. Пример в самом конце листинга 3-23. Поскольку мы явно проверяем, не осталось ли еще символов в строке n, то это гарантируется, так как мы проверили в n все символы и все они совпали.

Если мы нашли подстроку в строке, то, зная длину подстроки, например, найдя ее при помощи strlen, мы можем скопировать ее в отдельный буфер при помощи strncpy.

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

Алгоритм сравнения строк мы уже использовали в предыдущем примере, теперь ему нужно дать явное описание.

#define UCHAR unsigned char
int strcmp( UCHAR *s0, UCHAR *s1 )
{
while( *s0 == *s1
&& *s0 != '\0'
&& *s1 != '\0' )
{ s0++; s1++; }
return *s0 - *s1;
}

int strncmp( UCHAR *s0, UCHAR *s1, int n )
{
while( *s0 == *s1
&& *s0 != '\0'
&& *s1 != '\0'
&& !n )
{ s0++; s1++; n--; }
return *s0 - *s1;
}
#undef UCHAR

char *str0 = "кол"; // младше в словаре
char *str1 = "кот"; // старше в словаре
char *str2 = "котел";

main()
{
int comp = strcmp( str0, str1 );

puts( itoa( comp, 10 ) );

puts( itoa( strcmp( str1, str2 ), 10 ) );
puts( itoa( strcmp( str2, str0 ), 10 ) );

puts( itoa( strncmp( str0, str1, 2 ), 10 ) );

}

. . . . . . . .

$ m strings
$ strings
-1 # кол < кот
-208 # кот < котел
1 # котел > кот
0 # ко == ко
$ echo кот | hd
00000000 d0 ba d0 be d1 82 0a |.......|
00000007
$ echo кол | hd
00000000 d0 ba d0 be d0 bb 0a |.......|
00000007
$
Лст. 3-24. Функции сравнения strcmp и strncmp.

Макроопределение UCHAR вводится лишь для сокращения длинной строки unsigned char. Затем его действие отменяется. Этим занимается препроцессор, часть компилятора, не связанная с языком C, как таковым. Но иногда очень удобная.

Символы в этом случае выгодно иметь как беззнаковые. Все дело в UNICODE. Символы алфавита в таблицах следуют по возрастающей, монотонно, это относится как к первому байту кодировки, так и второму. Поэтому результаты сравнения строк должны быть правильными. На примере сравнения слов "кот" и "кол": разница обнаруживается в байтах с адресами ..004: 0xD0 и 0xD1. Эта разница равна -1, что подчеркивает тот факт, что буква "л" в русском алфавите стоит раньше буквы "т". Слова с латинскими буквами сравниваются также правильно.

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

Функция strncmp аналогична strcmp, в ней лишь добавлено ограничение на число сравниваемых байтов.

И у нас осталась последняя, довольно интересная, функция strtok. При помощи этой функции можно разделять строку текста на части, используя список разделителей из другой строки. strtok сначала вызывается с адресом строки для обработки, и строкой разделителей. Если один из разделителей будет найден в целевой строке, содержащей также и другие символы, функция вернет указатель на самую левую часть строки, иначе вернет нулевой указатель. При каждом последующем вызове функции передается NULL в первом аргументе и она возвращает очередной фрагмент разделенной строки (токен или лексему, что то же самое).

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

//strtok.c

char *strtok( char *s, char *d )
{
static char *p, end;

if( s ) { p = s; end = 0; }
if( end ) return 0;
while( strchr( d, *p ) && *p != '\0' ) p++;
if( *p == '\0' ) { end = 1; return 0; }
s = p;
while( !strchr( d, *p ) && *p != '\0' ) p++;
if( *p == '\0' ) end = 1;
*p = '\0'; p++;
return s;
}

char str[] = " ,ab cde Вася ef,dh xy z, xyz,ab z";
char *ptr;

#define NULL 0 // можно использовать и 0

main()
{
ptr = strtok( str, " ," );

while( ptr )
{
puts( ptr );
ptr = strtok( NULL, " ," );
}
}

char str[] = " ,ab cde Вася ef,dh xy z, xyz,ab z";
char *ptr;

#define NULL 0 // можно использовать и 0

main()
{
ptr = strtok( str, " ," );

while( ptr )
{
puts( ptr );
ptr = strtok( NULL, " ," );
}
}
Лст. 3-25. Код и тестирование функции strtok.

Статический указатель и еще один байт нужны в функции, чтобы она помнила данные между вызовами. При инициализации мы считаем, что конец еще не достигнут (end = 0, ложное условие). При первом вызове указатель s ненулевой (содержит адрес строки для разбора) и поэтому выполняется инициализирующий блок с условным оператором. В нем мы инициализируем указатель начала разбора, он совпадает с началом строки.

Затем выполняется оставшаяся часть функции. Каждый из символов разбираемой строки проверяется на предмет того, не является ли он одним из разделителей. За это отвечает функция strchr, которой передается список разделителей как первый аргумент. Второй аргумент - это очередной байт из разбираемой строки. Это, так сказать "транспозиция" использования strchr по сравнению с ее обычным использованием. Одновременно мы проверяем: нет ли конца строки? - чтобы "не проехать станцию" и не получить ошибку сегментации памяти. Если обнаружился нуль-терминатор, то мы устанавливаем флаг end, и выходим. Дальнейшая работа функции для заданной строки запрещена.

Если мы еще в строке, то все указывает на то, что обнаружен первый байт первого токена. Сохраним его адрес в s.

Конец токена отыскивается по совершенно аналогичному алгоритму, какой описан только что, разница только в том, что инвертируется условие поиска символа. И флаг конца, который мы устанавливаем - он служит ярким красным фонарем - на этот раз не прерывает работу функции нулевым указаелем, так как есть еще что выдать в качестве последнего результата. После записи очередного завершающего нуля, мы продвигаем указатель. Это обеспечивает продолжение работы функции, а "пролететь на красный свет" (что является преступлением на железной дороге) при очередном вызове функции нам не угрожает, так как флаг end уже установлен.

Скомпилируем программу и запустим тест:

$ strings
ab
cde
Вася
ef
dh
xy
z
xyz
ab
z
$
Лст. 3-26. Тест функции strtok.

Именно так и должна работать strtok. Приятной особенностью функции является то, что она позволяет работать и с нелатинскими символами. Впрочем, так и должно быть. Однако, их не удастся использовать в качестве разделителей. Хотя это вряд ли потребуется. Функция также не может использоваться рекурсивно, по понятным, можно сказать очевидным, причинам.

На этом тему строк можно закончить. Как обычно, нарежьте функции на отдельные файлы .c, скомпилируйте их в объектные модули, но не добавляйте в ваш скрипт m. В следующей главе мы сделаем кое-что получше и интереснее.

И в качестве десерта, хочу представить самый лучший (по моему мнению) редактор текста: scite. Это для тех, кому надоело пользоваться редактором в терминале.



Рис. 3-1. Вся необходимая "среда разработки".

Редактор имеет огромное число настроек, и очень простой интерфейс. Все настройки в конфигах, их несколько, и пользовательский можно держать в домашнем каталоге. Можно даже иметь настройки для отдельного каталога. Очень гибкий редактор, для тех, кто не поленится поизучать его многочисленные параметры в файлах конфигурации.


Дальше

строки, #define, функции str..., c, #undef

Previous post Next post
Up