Стандартная C библиотека для Linux, часть 7: Работа со строками

Автор: (C) James M Rogers
Перевод: (C)Андрей Киселев


Наконец нашлось время (через несколько лет), чтобы передать Linux-сообществу следующую серию статей, посвященных стандартной библиотеке C. Надеюсь вам понравится.

Предыдущая моя статья была посвящена библиотеке <assert.h>. В этой статье мы рассмотрим <string.h> -- работу со строками. Для работы со строками язык C приспособлен немногим лучше, чем низкоуровневые языки программирования, так что программисты, работающие с такими языками, будут чувствовать себя как дома. string.h имеет множество ограничений и узких мест, которые мы обсудим в при описании соответствующих функций.

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

Пример rogers_example07.c был написан для демонстрации работы каждой из строковых функций. Скомпилировав и запустив его, можно наблюдать работу кода.

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

ВНИМАНИЕ: Копирование строк -- один из самых опасных разделов программирования в C. C не предусматривает проверку на выход за границы массивов, поэтому очень легко "выскочить" за пределы строки (это называется "переполнение буфера"), "затирая" другие переменные и даже вызвать крах программы. Крякеры используют это свойство языка и неопытность кодировщиков, преднамеренно вызывая переполнение буфера, заставляя программу "вывалиться" в командную строку, тем самым получая доступ к учетной записи, под которой выполнялась программа, а ведь на серверах множество программ исполняются с правами root!

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

char string[17];

Здесь выделяется место для 16-ти символов и одного символа -- признака конца строки.

strcpy ( string, "This is a string" );

Этот вызов копирует строку "This is a string" в ранее зарезервированное место. Копируемая строка содержит 16 символов и завершается специальным символом -- ASCII nil. В данном случае в массиве string достаточно места для ее размещения. Nil обычно представляется числом ноль, или символом '\0', или '\000'.

Самое удивительное заключается в том, что иногда этот код будет работать, даже когда будет копироваться строка, содержащая более 17 символов:

strcpy ( string, "This is a long string" );

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

#define  MAX_STRING_LENGTH  17
char string[MAX_STRING_LENGTH];
strncpy ( string, "This is a long string", MAX_STRING_LENGTH );

string[MAX_STRING_LENGTH-1] = '\000';

Причина, по которой я определил макрос MAX_STRING_LENGTH для указания длины строки, в том, что это значение может использоваться во многих местах программы и, если вдруг вздумается изменить длину строки, то не придется искать число 17 по всему тексту программы, чтобы его изменять: достаточно будет изменить это число в одном месте. Иногда числовое значение может использоваться в разных контекстах (т.е. обозначать не только размер буфера, но и что нибудь еще). Однако, если литерал используется как число в одном и том же контексте, но в разных местах, то определение макроса делает его легко заметным в тексте программы и позволяет вносить изменения достаточно простым образом, в данном случае -- легко изменить размер буфера строки.

Обратите внимание на последнюю строку string[MAX_STRING_LENGTH-1] = '\000';, для чего она? Дело в том, что при копировании строки, содержащей бОльшее количество символов, чем передано функции strncpy, то завершающий ноль не будет записан в строку-результат. Такая "незавершенная" строка может привести к краху программы и очень трудно обнаруживаемой ошибке.

Существует возможность динамического выделения памяти для строк с помощью функций malloc, realloc и calloc.  Эти функции выделяют память по запросу во время исполнения программы. Это более сложный, но вместе с тем и более гибкий и мощный способ.

#define STATIC_STRING "This is a long string that will be copied into\
                       a location during runtime"

char *string;
int string_length;

string_length = strlen(STATIC_STRING);

if ( !(string = (char *) malloc ( string_length )) ) {
    /* no memory left, die */
    exit (1);
}

strncpy( string,  STATIC_STRING, string_length);
string[string_length] = '\000';

/* операции над строкой */

free(string);

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


<string.h> имеет массу проблем.

Самая большая проблема в том, что эта библиотека не является окончательно завершенной и полностью устойчивой. В действительности <string.h> является коллекцией функций, написанных различными разработчиками в разное время и объединенных в библиотеку.

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


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


Копирование

#include <string.h>

void *memcpy(void *dest, const void *src, size_t n);
void *memmove(void *dest, const void *src, size_t n);
char *strncpy(char *dest, const char *src, size_t n);
char *strcpy(char *dest, const char *src);
 

void *dest: указатель на массив-приемник, сюда копируются данные.
char *dest: указатель на строку-приемник, сюда копируются данные.
const void *src:  указатель на массив-источник, данные копируются отсюда.
const char *src: указатель на строку-источник, данные копируются отсюда.
size_t n: количество копируемых символов.

Эти функции возвращают указатель на dest, что довольно странно, поскольку этот указатель и так известен.

memcpy копирует n символов из области памяти, адресуемой указателем src, в область памяти, адресуемую указателем dest. Эта функция не предусматривает копирование перекрывающихся областей памяти.

memmove также копирует n символов из области памяти, адресуемой указателем src, в область памяти, адресуемую указателем dest. Но сначала данные копируются во временную область памяти, а затем в память, адресуемую указателем dest, так что эта функция может использоваться для копирования данных в перекрывающиеся области памяти.

strncpy Копирует не более чем n символов из области памяти, адресуемой указателем src, в область памяти, адресуемую указателем dest. Функция завершает свою работу по тому из двух условий, которое выполнится первым: либо встретится завершающий null-символ, либо будет скопировано заданное количество символов. Если скопировано n символов, а null символ не встретился, то в строку-приемник завершающий null символ не записывается, т.е. строка остается "открыто", поэтому желательно всегда записывать ноль в конец строки приемника после выполнения копирования.

strcpy Копирует строку, адресуемую указателем src, в строку, адресуемую указателем dest, включая завершающий null-символ.
Осторожно! Старайтесь не пользоваться этой функцией для копирования данных, пришедших из внешнего мира!!! 
Наибольшая опасность этой функции заключается в том, что она будет продолжать копировать до тех пор, пока либо не встретит завершающий null-символ, либо пока не будет достигнут предел памяти, к которой программа имеет доступ. В последнем случае программа получит от системы сигнал SEGV (segfault) Программист может перехватить этот сигнал в программе, но в данной ситуации единственное, что имеет смысл сделать -- записать дамп памяти программы(core dump).

Я уже показывал, как используются функции strcpy и strncpy, функции memcpy и memmove используются аналогичным образом, с той лишь разницей, что последние две могут копировать любые блоки данных, а не только строки.


Слияние (конкатенация) строк

#include <string.h>

char *strcat(char *dest, const char *src);
char *strncat(char *dest, const char *src, size_t n);

char *dest : указатель на строку-приемник (результат).
const char *src:  указатель на строку-источник (присоединяемая строка).
size_t n:  количество копируемых символов.

strcat Содержимое строки src, включая завершающий символ '\0', копируется в конец строки dest. Копирование начинается в позицию символа '\0' строки-приемника.

strncat Аналогично strcat, но копируется не более чем n символов. Функция добавляет символ '\0' в конец строки-приемника после копирования.

И strcat , и strncat возвращают указатель на строку dest. Опять же, функции не производят проверку на выход за пределы строки-приемника, так что всегда проверяйте достаточно ли места зарезервировано для строки-приемника.


Сравнение

#include <string.h>

int memcmp(const void *s1, const void *s2, size_t n);
int strcmp(const char *s1, const char *s2);
int strncmp(const char *s1, const char *s2, size_t n);
int strcoll(const char *s1, const char *s2);
size_t strxfrm(const char *s1, const char *s2, size_t n);

(В man описание функции strxfrm дается так: strxfrm(char *s1, const char *s2, size_t n); прим. перев.)
const char *s1: указатель на первую строку.
const void *s1: указатель на первую область памяти.
const char *s2: указатель на вторую строку.
const void *s2: указатель на вторую область памяти.
size_t n:  количество сравниваемых символов.

memcmp лексикографически сравнивает n байт. Если s1 меньше чем s2, то возвращается число меньшее нуля. Если s1 равно s2, то возвращается ноль. Если s1 больше чем s2, то возвращается число большее нуля.

strcmp сравнивает две строки s1 и s2. Строки должны завершаться нулевым символом. Если s1 меньше чем s2, то возвращается число меньшее нуля. Если s1 равно s2, то возвращается ноль. Если s1 больше чем s2, то возвращается число большее нуля. Сравнение основано на числовых значениях ASCII символов.

strncmp очень похожа на memcmp, за исключением того, что сравниваются две строки. Функция производит сравнение не более чем n символов. Если строки (или одна из строк) короче n, то сравнение заканчивается по достижению завершающего нулевого символа Если s1 меньше чем s2, то возвращается число меньшее нуля. Если s1 равно s2, то возвращается ноль. Если s1 больше чем s2, то возвращается число большее нуля.

strcoll сравнивает две строки s1 и s2. Если s1 меньше чем s2, то возвращается число меньшее нуля. Если s1 равно s2, то возвращается ноль. Если s1 больше чем s2, то возвращается число большее нуля. Сравнение производится с учетом настройки локали (национального алфавита и других языковых особенностей), устанавливаемой вызовом функции setlocale() из библиотеки <locale.h>. Более подробно об этой функции мы поговорим в одной из следующих статей.

strxfrmФункция strxfrm() преобразует строку s2 в такую форму, что выполнение strcmp() над двумя такими строками, преобразованными посредством strxfrm(), будет таким же, как и выполнение strcoll над исходными строками. Первые n символов преобразованной строки помещаются в s1. Преобразование основывается на настройках категории текущей локали LC_COLLATE. Функция strxfrm() возвращает количество байтов, скопированных в s1, без учета завершающего символа '\0'. Если возвращенное значение равно n или больше этой величины, то содержимое s1 не определено и должно трактоваться как ошибка.


Поиск

#include <string.h>

void *memchr(const void *s, int c, size_t n);
char *strchr(const char *s, int c);
size_t *strcspn(const char *s, const char *reject);
size_t *strspn(const char *s, const char *accept);
char *strpbrk(const char *s, const char *accept);
char *strchr(const char *s, int c);
char *strrchr(const char *s, int c);
char *strstr(const char *s, const char *substring);
char *strtok(char *s, const char *delim);

const void *s: указатель на массив, в котором производится поиск.
int c: искомый символ.
size_t n: количество просматриваемых символов.

memchr ищет первое вхождение символа c в массиве s, просматривая при этом не более n символов. Возвращает указатель на символ c или NULL, если таковой не найден.

strcspn возвращает длину начального сегмента строки s, состоящего только из символов, не указанных в строке reject.

strspn возвращает длину начального сегмента строки s, состоящего только из символов строки accept.

strpbrk возвращает указатель на первое вхождение в строке s любого символа из строки accept. Если таких символов не обнаружено, то возвращается NULL.

strchr возвращает указатель на местонахождение первого совпадения с символом c в строке s или NULL, если такой символ не найден.

strrchr возвращает указатель на местонахождение последнего совпадения с символом c в строке s (т.е. поиск производится с конца строки в направлении к ее началу прим. перев.). Если символ не найден, то возвращается NULL.

strstr возвращает указатель на первую встретившуюся подстроку substring в строке s или NULL, если таковая не встретилась.

Man page strtok не рекомендует использовать эту функцию из-за некоторых связанных с ней проблем. Функция strtok делит исходную строку на элементарные подстроки-токены. На первом вызове функции передается указатель на строку и функция возвращает указатель на первый токен. На каждом последующем вызове, функции, в качестве первого аргумента передается NULL, а функция будет возвращать токен за токеном до тех пор, пока не вернет NULL в качестве результата Разделители могут отличаться при каждом последующем вызове. Функция имеет множество ограничений: она изменяет оригинальную строку s, строка разделителей не сохраняется от вызова к вызову и функция не может работать со строками-константами. (т.е. вызов типа strtok("This is a string of tokens", " "); работать не будет прим. перев.).


Разное

#include <string.h>

void *memset(void *s, int c, size_t n);
char *strerror(int errnum);
size_t *strlen(const char *s);
 

void *s
int c
size_t n
int errnum
const char *s

memset заполняет массив размером n значением c и возвращает указатель на начало массива.

strerror возвращает указатель на строку с описанием кода ошибки, переданного в аргументе errnum или сообщение о неопознанной ошибке, если код ошибки неизвестен. Работает с различными кодами ошибок, присутствующими в библиотеках <stdio.h> и <error.h>. В одной из следующих статей мы коснемся этой темы глубже.

strlen возвращает количество символов в строке s. Завершающий символ '\0'не учитывается.


Непереносимые функции

Библиотека для работы со строками GNU имеет ряд функций, не поддерживаемых стандартом C. Описание их взято из man pages. Если необходимо, чтобы программа работала на любой UNIX системе, использования этих функций следует избегать. Они, однако, могут служить прекрасным руководством для создания функций в ваших собственных переносимых программах.

int strcasecmp(const char *s1, const char *s2);

strcasecmp сравнивает строки s1 и s2 без учета регистра символов. Если s1s2, то возвращается число меньшее нуля. Если s1 равно s2, то возвращается ноль. Если s1 больше чем s2, то возвращается число большее нуля.

int strncasecmp(const char *s1, const char *s2, size_t n);

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

strcasecmp и  strncasecmp возвращают целое число. Если s1 меньше чем s2, то возвращается число меньшее нуля. Если s1 равно s2, то возвращается ноль. Если s1 больше чем s2, то возвращается число большее нуля.

char *strdup(const char *s);

Я реализовал эту функцию у себя, поскольку ничего не знал о ней! Я постоянно нахожу для себя что-то новенькое в Linux.

strdup возвращает указатель на новую строку, которая является точной копией s. Память под вновь созданную строку выделяется с помощью функции malloc(3), и может быть освобождена вызовом free(3).

strdup возвращает указатель на новую строку или NULL в случае нехватки памяти.

char *strfry(char *string);

strfry изменяет порядок расположения элементов (символов) в строке при помощи rand(3). В результате получается анаграмма строки.

strfry возвращает указатель на переупорядоченную строку.

char *strsep(char **stringp, const char *delim);

strsep возвращается указатель на следующий токен из строки stringp который ограничен символом из строки delim. Токен завершается символом '\0', а stringp обновляется так, чтобы указывать на место сразу после извлеченного элемента. Похожа на функцию strtok(), но является непереносимой.

strsep возвращает указатель на элемент строки (токен), если символ из delim не найден, то возвращает исходное значение *stringp.

char *index(const char *s, int c);

index возвращает указатель на первый встретившийся в строке s символ c. Для поиска символа в строке лучше использовать функцию strchr(), которая является переносимой.

char *rindex(const char *s, int c);

rindex возвращает указатель на последний символ c в строке s. Завершающий символ '\0' рассматривается как часть строки. Для поиска символа в строке лучше использовать функцию strrchr(), которая является переносимой.

index и rindex возвращают указатель на найденный символ или NULL, если символ не найден.


Исправления ошибок, встретившихся в предыдущих статьях:

Все правильно! Я наконец передаю в публикацию накопленные исправления ошибок, допущенных в предыдущих статьях. Вы только посмотрите, какие ошибки я допустил! Спасибо тем, кто нашел время отправить мне сообщение.

Subject: The Standard C Library for Linux, Part Three"
Date: Wed, 12 Aug 1998 11:27:08 +0200
From: Lars Hesdorf <hesdorf@ibm.net>

Hej James M. Rogers

Вы написали в The Standard C Library for Linux, Part Three

"putchar записывает символ в стандартный поток вывода. putchar(x) это то же самое, что и
fputc(x, STDIN)"

Вероятно Вы имели ввиду "...fputc(x, STDOUT)

Lars Hesdorf
HESDORF@IBM.NET

Ответ:

Я уверен, что употребление заглавных символов (STDOUT) неверно. Я полагаю, что должно быть "fputc(x, stdout)". В примере программы все указано правильно, поскольку я ее тестировал на корректность.

Subject: The Standard C Library for Linux, Part Two
Date: Wed, 04 Aug 1999 21:00:59 +1000
From: 32000151 <32000151@snetmp.cpg.com.au>
Organization: Student of Computer Power Institute

Уважаемый,

в The Standard C Library for Linux, Part Two Вы написали

" char *fgets(char *s, int n, FILE *stream);

char *s строка, в которую заносится результат.
int n - максимальное число символов, которое может быть прочитано.
FILE *stream - уже существующий поток.
.
.
.

fgets считывает до n символов из потока в строку.

char s[1024];
FILE *stream;
if((stream = fopen ("filename", "r")) != (FILE *)0) {
while((fgets(s, 1023, stream)) != (char *)0 ) {
<обработка каждой строки>
}
} else {
<обработка ошибки fopen>
} "

но fgets() в действительности читает до n-1 символов, так что не следует специально резервировать место для символа \0 (если n установить равным размеру строки).

Tim McCormack
32000151@bran.snetmp.cpg.com.au

Ответ:
Спасибо, я обязательно проверю работу этой функции в моем примере.

Subject:  snprintf in Article C Library for Linux?
Date: Tue, 01 Sep 1998 17:53:19 +0200
From: Renaud Hebert <hebert@bcv01y01.vz.cit.alcatel.fr>

Я не знаком с функцией snprintf, но на мой взгляд использование этой функции
более предпочтительно , чем sprintf() (много лучше).

Но я впервые увидел ее в библиотеке C только для LINUX
или она появилась достаточно недавно, потому, что она
не включена в HP-UX, например.

Полагаю Вы могли бы в Ваших статьях отмечать функции, характерные только для
Стандартной библиотеки C в Linux.

Все равно snprintf -- "Хорошая штука" TM.

Спасибо за Ваши статьи. Очень хорошо написаны и очень
информативны.
--
__________________________________________________________________
Renaud HEBERT CR2A-DI
Software Developer

Ответ:
Я думаю, что эта функция имеется только в GNU. Так что Вы можете не пользоваться функцией snprintf, если Вы хотите чтобы Ваши программы работали не только в GNU среде. Я нашел целую связку очень удачных строковых функций в GNU и, следуя Вашему совету, впредь постараюсь отмечать функции, имеющиеся только в Linux.

Subject: Standard C Programming Library Part 3
Date: Sun, 20 Sep 1998 09:52:29 -0400
From: Laurin Killian <lek@uconect.net>
Organization: Streamlined Development

Так как Вы просили исправления....
Имеется пара опечаток в Ваших примерах:

------------Вы написали:
float x=99.1234;
sprintf(string, "%d", x)
------------должно быть...
sprintf(string, "%f", x);
^
------------Вы написали:
float x=99.1234;
return Value=sprintf(string, 4, "%d", x)
------------должно быть...
return Value=snprintf(string, 5, "%f", x);
^ ^
(чтобы в результате получить "99.1" - необходимо предусмотреть место для завершающего строку нулевого символа)

Все параметры, передаваемые функции "scanf" должны быть указателями т.е. предваряться символом (&):
scanf("%f%2d%d", &float1, &int1, &int2);

Надеюсь, что это поможет
-Laurin

Ответ:
Помогло, спасибо Вам!

Subject: character handling program
Date: Mon, 15 Mar 1999 13:31:41 +0100
From: jorgen.tegner@sundsdefibrator.com

Привет!

в Ваших программах, в Linux gazette отсутствует вызов setlocale()
Что не очень хорошо, когда требуется обрабатывать строки, содержащие
символы с кодом выше 127, поскольку программы стартует в локали по-умолчанию "C". Из-за этого, функции isalpha(), toupper()
и tolower() ограничиваются только диапазоном A-Z a-z.

С уважением,
Jorgen Tegnur

Ответ:
Совершенно верно, я расскажу о функции setlocale() в статье, которая будет посвящена <locale.h>. :)


Библиография:

The ANSI C Programming Language, Second Edition, Brian W. Kernighan, Dennis M. Ritchie, Printice Hall Software Series, 1988

The Standard C Library, P. J. Plauger, Printice Hall P T R, 1992

The Standard C Library, Parts 1, 2, and 3, Chuck Allison, C/C++ Users Journal, January, February, March 1995

STRING(3), BSD MANPAGE, Linux Programmer's Manual


Предыдущие статьи "The Standard C Library for Linux"

The Standard C Library for Linux, stdio.h, January 1998
The Standard C Library for Linux, stdio.h, August 1998
The Standard C Library for Linux, stdio.h, September 1998
The Standard C Library for Linux, ctype.h, March 1999
The Standard C Library for Linux, stdlib.h, April 1999
The Standard C Library for Linux, assert.h, May 1999


James Rogers

Джеймс Роджерс -- системный программист, специализирующийся в области маршрутизаторов Cloverleaf для HL7. Сейчас он также работает над открытой библиотекой для работы с HL7. (вообще, HL7 -- это вроде бы всегда был один из стандартов на обмен данными в организациях здравоохранения. Интересно... прим. ред.). C помощью этой библиотеки он надеется создать открытый "движок" для работы с HL7.


Copyright © 2002, James M Rogers.
Copying license http://www.linuxgazette.com/copying.html
Published in Issue 76 of Linux Gazette, March 2002

Вернуться на главную страницу