Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ЯП - ПОИТ (Бахтизин) часть 1 редакт.doc
Скачиваний:
1
Добавлен:
01.04.2025
Размер:
1.76 Mб
Скачать

6. Строки

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

В строках с упреждающей длиной в памяти перед областью, байты которой интерпретируются как символы строки, находится число, определяющее реальную длину строки. Именно такой метод используется в языке Pascal для хранения переменных типа string. Там для строковой переменной по умолчанию отводилось 256 байт в памяти. Первый байт хранил реальную длину строки, а остальные – её символы. Именно с тем, что для хранения реальной длины использовался один байт и связано ограничение на максимальную длину строки в Pascal’е.

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

В виду того, что специального строкового типа нет, а Си-строка является массивом элементов типа char, логично при хранении строк использовать тип char* (или char [], что, в сущности, то же самое).

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

char *str = “Hello world”;

или аналогичное ему

char str[] = “Строка”;

(разница будет заключаться в том, что в первом случае переменную str можно будет изменить (как указатель), а во втором нельзя (имя массива)), однако, если написать так

char *const str = “Hello world”;

то и это отличие исчезает.

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

Первый способ:

#include <string.h>

char str[100]; // Любые строки до 99 символов

strcpy(str, “Строка”); // Побайтое копирование строки

Второй способ:

char *str;

str = (char*) malloc(100);

strcpy(str, “Строка”);

free(str);

Использованная стандартная функция strcpy копирует строку по адресу src, в строку по адресу dest, и возвращает указатель dest:

// Прототип функции strcpy

char* strcpy(char* dest, const char* src);

char buf[15] = “BufString”;

char str* = “String”;

strcpy(buf, str);

Предположим, что области памяти, выделенные компилятором для массива buf, и строкового литерала “String” смежные. Длина массива больше длины строки, которой он инициализирован, на 5 байт – это значит, что в них может находиться что угодно. На рисунке показано, как изменится область памяти, содержащая строки, после вызова strcpy.

Приведем пример функции, которая делает в точности то же самое, что и strcpy:

char* user_strcpy(char* dest, const char* src)

{

char *result = dest;

do

{

*dest++ = *src;

}

while (*src++);

return result;

}

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

Другой необходимой и часто используемой функцией работы со строками, является функция определения их длины:

size_t user_strlen(const char* src)

{

size_t result = 0;

while (*src++) result++;

return result;

}

или аналогичная ей функция из стандартной библиотеки:

size_t strlen(const char* src);

Тип size_t определен в string.h и представляет собой псевдоним целого типа без знака.

Из всего сказанного видно, что использовать операторы присваивания и сложения для копирования и конкатенации строк (как это делалось в языке Pascal) нельзя:

char *str1 = “Студент”;

char *str2 = “ БГУИР”;

char *str3;

str3 = str1 + str2; // НЕ верно!

В данном случае выражение str1 + str2 даже не будет скомпилировано, компилятор выдаст ошибку: «Неверное использование указателей». Действительно, пусть строка «Студент» находится в памяти, например, по адресу 1000, а «БГУИР» 1009. Арифметическая сумма этих чисел даст 2009. Что находится по этому адресу сказать нельзя. Присваивание же str3 адреса строки приведет к тому, что оба указателя будут указывать на одну и ту же область в памяти, и изменение строки str3 повлечет за собой соответствующее изменение str1 и str2, чего пользователь явно не ожидает. Выражение

str3 = “Студент” + “ БГУИР”;

по сути, ничем не отличается от str3 = str1 + str2: строковые литералы размещаются в памяти по адресам, которые вместо них компилятор подставит в выражение, а складывать два адреса нельзя.

С другой стороны, в стандартной библиотеке string.h, есть целый набор функций работы со строками. Рассмотрим некоторые из них:

// Прототип функции конкатенации строк

char* strcat(char* dest, const char* src);

// Пользовательская реализация

char* user_strcat(char* dest, const char* src)

{

strcpy(dest + strlen(dest), src);

return dest;

}

// Прототип функции поиска первого вхождения символа

char* strchr(char* s, int c);

// Пользовательская реализация

char* user_strchr(char* s, int c)

{

do

{

if (*s == c) return s;

}

while (*s++);

return 0;

}

// Прототип функции создания дубликата строки

char* strdup(const char* s);

// Пользовательская реализация

char* user_strdup(const char* s);

{

/* malloc – функция динамического выделения

памяти, рассматривается в главе указатели */

return strcpy((char *) malloc(strlen(s)+1), s);

}

// Использование strdup

char* some_function(const char* str)

{

char* temp = strdup(str);

// ...

free(temp); //Обязательное освобождение памяти!

}

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

// Прототип функции сравнения строк

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

// Пользовательская реализация

int user_strcmp(const char* s1, const char* s2)

{

int r;

do

{

r = *s1 - *s2;

}

while (*s1++ && *s2++ && !r);

return r;

}

// Использование strcmp

char* some_function(char* str1, char* str2, char* str3)

{

printf("\n%d", strcmp(str1, str2));

printf("\n%d", strcmp(str2, str1));

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

}

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