basic-c/sections/10-strings.tex

256 lines
30 KiB
TeX
Raw Permalink Normal View History

2021-09-29 14:11:47 +03:00
\section{Строки}
2021-09-30 16:01:28 +03:00
\subsection{Основные понятия}
2021-09-29 14:11:47 +03:00
Получив в предыдущих разделах представление об указателях и массивах, и вскользь несколько раз упомянув строки, пришла пора изучить их подробнее.
Итак, что же такое \textbf{строка}. В повседневной жизни строка \textit{это набор или последовательность символов}. Так вот в языке С строка - это тоже последовательность символов. А последовательности значений, в том числе символьных, в языке С представлены, как вы, уже знаете, массивами и указателями. Никакого примитива \code{string} в языке С нет. Как бы нам этого не хотелось - его нет. Но есть и хорошая новость, примитива \code{string} не существует и в других языках (здесь имеются ввиду, конечно, си-подобные языки, то есть примерно треть вообще всех языков программирования, известных сегодня). Раз строка - это массив или указатель, это всегда ссылочный тип данных. Например, строку можно объявить двумя способами:
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
// строка
char str[50] =
{'h','e','l','l','o',' ','w','o','r','l','d','\0'};
// литерал
char* str = "Hello world";
\end{lstlisting}
\end{figure}
Раз строка - это набор символов, давайте немного вспомним что такое сиволы и как с ними работать. Как вам уже известно, символьная переменная это переменная типа \code{char}. В отличие от строки это примитивный, числовой, тип данных, и к нему применимы все операции допустимые для примитивов, такие как присваивание, сложение, вычитание, умножение, деление, хотя не все имеют смысл, так автор, например с трудом может представить ситуацию в которой необходимо умножать или делить коды символов, кроме, пожалуй, криптографии. Обратим внимание на следующую запись:
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
char c0 = 75;
сhar с1 = К;
\end{lstlisting}
\end{figure}
Здесь значением \code{c0} является \code{75}, что абсолютно эквивалентно значению переменной \code{c1}, равной символу \code{K}. То есть можно сказать, что преобразование чисел в символы и наоборот согласно таблицы, наподобие ASCII, частично приведённой на стр. \hyperref[table:ascii]{\pageref{table:ascii}}, встроена в работу компилятора языка С. Однако для улучшения читаемости кода лучше использовать вариант \code{sym = 'K'}.
\begin{figure}[h!]
\begin{tabular}{||c|c|c||c|c|c||c|c|c||c|c|c||}
\hline
dec & hex & val & dec & hex & val & dec & hex & val & dec & hex & val \\
\hline
000 & 0x00 & (nul) & 032 & 0x20 & \textvisiblespace & 064 & 0x40 & @ & 096 & 0x60 & \textquoteleft \\
001 & 0x01 & (soh) & 033 & 0x21 & ! & 065 & 0x41 & A & 097 & 0x61 & a \\
002 & 0x02 & (stx) & 034 & 0x22 & " & 066 & 0x42 & B & 098 & 0x62 & b \\
003 & 0x03 & (etx) & 035 & 0x23 & \# & 067 & 0x43 & C & 099 & 0x63 & c \\
004 & 0x04 & (eot) & 036 & 0x24 & \$ & 068 & 0x44 & D & 100 & 0x64 & d \\
005 & 0x05 & (enq) & 037 & 0x25 & \% & 069 & 0x45 & E & 101 & 0x65 & e \\
006 & 0x06 & (ack) & 038 & 0x26 & \& & 070 & 0x46 & F & 102 & 0x66 & f \\
007 & 0x07 & (bel) & 039 & 0x27 & \textquotesingle & 071 & 0x47 & G & 103 & 0x67 & g \\
008 & 0x08 & (bs) & 040 & 0x28 & ( & 072 & 0x48 & H & 104 & 0x68 & h \\
009 & 0x09 & (tab) & 041 & 0x29 & ) & 073 & 0x49 & I & 105 & 0x69 & i \\
010 & 0x0A & (lf) & 042 & 0x2A & * & 074 & 0x4A & J & 106 & 0x6A & j \\
011 & 0x0B & (vt) & 043 & 0x2B & + & 075 & 0x4B & K & 107 & 0x6B & k \\
012 & 0x0C & (np) & 044 & 0x2C & \textquoteright & 076 & 0x4C & L & 108 & 0x6C & l \\
013 & 0x0D & (cr) & 045 & 0x2D & - & 077 & 0x4D & M & 109 & 0x6D & m \\
014 & 0x0E & (so) & 046 & 0x2E & . & 078 & 0x4E & N & 110 & 0x6E & n \\
015 & 0x0F & (si) & 047 & 0x2F & / & 079 & 0x4F & O & 111 & 0x6F & o \\
016 & 0x10 & (dle) & 048 & 0x30 & 0 & 080 & 0x50 & P & 112 & 0x70 & p \\
017 & 0x11 & (dc1) & 049 & 0x31 & 1 & 081 & 0x51 & Q & 113 & 0x71 & q \\
018 & 0x12 & (dc2) & 050 & 0x32 & 2 & 082 & 0x52 & R & 114 & 0x72 & r \\
019 & 0x13 & (dc3) & 051 & 0x33 & 3 & 083 & 0x53 & S & 115 & 0x73 & s \\
020 & 0x14 & (dc4) & 052 & 0x34 & 4 & 084 & 0x54 & T & 116 & 0x74 & t \\
021 & 0x15 & (nak) & 053 & 0x35 & 5 & 085 & 0x55 & U & 117 & 0x75 & u \\
022 & 0x16 & (syn) & 054 & 0x36 & 6 & 086 & 0x56 & V & 118 & 0x76 & v \\
023 & 0x17 & (etb) & 055 & 0x37 & 7 & 087 & 0x57 & W & 119 & 0x77 & w \\
024 & 0x18 & (can) & 056 & 0x38 & 8 & 088 & 0x58 & X & 120 & 0x78 & x \\
025 & 0x19 & (em) & 057 & 0x39 & 9 & 089 & 0x59 & Y & 121 & 0x79 & y \\
026 & 0x1A & (eof) & 058 & 0x3A & : & 090 & 0x5A & Z & 122 & 0x7A & z \\
027 & 0x1B & (esc) & 059 & 0x3B & ; & 091 & 0x5B & [ & 123 & 0x7B & \char`\{ \\
028 & 0x1C & (fs) & 060 & 0x3C & < & 092 & 0x5C & \char`\\ & 124 & 0x7C & | \\
029 & 0x1D & (gs) & 061 & 0x3D & = & 093 & 0x5D & ] & 125 & 0x7D & \char`\} \\
030 & 0x1E & (rs) & 062 & 0x3E & > & 094 & 0x5E & \^{} & 126 & 0x7E & \~{} \\
031 & 0x1F & (us) & 063 & 0x3F & ? & 095 & 0x5F & \char`\_ & 127 & 0x7F & \DEL \\
\hline
\end{tabular}
\caption{Фрагмент таблицы ASCII}
\label{table:ascii}
\end{figure}
Таких таблиц кодировок несколько, а может, даже и несколько десятков. Разные операционные системы и разные приложения используют разные кодировки, например, в русскоязычной версии ОС Windows по-умолчанию используется cp1251, в то время как в командной строке этой же ОС используется cp866. Файлы можно сохранить в Unicode или ANSI. UNIX-подобные ОС, такие как Linux и Mac OS X обычно используют UTF-8 или UTF-16, а более ранние операционные системы и интернет пространства в русскоязычном сегменте использовали KOI8-R.
2021-09-30 16:01:28 +03:00
\subsection{Особенности}
2021-09-29 14:11:47 +03:00
Немного вспомнив, что такое символы, переходим к строкам. Строками мы пользуемся с самых первых страниц этого документа: написав в двойных кавычках <<Привет, Мир>>, мы использовали строку, а если точнее, строковый литерал. Строки иногда называют типом данных, но в языке С строка это указатель на последовательно записанный набор символов, поэтому работать с ним можно, как с массивами. Строки в языке С можно описать двумя способами: как указатель и как массив из переменных типа char.
\frm{Объявление строки как указателя на символы в языке С++ полностью заменили на указатель на константный набор символов, чтобы подчеркнуть неизменяемость литерала. То есть, если в языке С считается нормальной запись \code{char* s = "Hello";} то в С++ это можно записать \textbf{только} как \code{const char* s = "Hello";} при этом в обоих языках поведение такого указателя будет обинаковым.}
Давайте создадим строку в виде массива из \code{char} назовем ее \code{string1} и запишем внутрь литерал \code{This is a string!} - это строка. Также создадим указатель, назовем его \code{string2} и запишем в него литерал \code{This is also a string!} это тоже строка. Выведем наши строки в консоль и убедимся, что автор не ошибся и их действительно можно так объявлять.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
char string1[256] = "This is a string!";
char* string2 = "This is also a string!";
printf("%s \n", string1);
printf("%s \n", string2);
\end{lstlisting}
\end{figure}
У каждого из способов есть свои особенности. Так, например в \textit{массиве} из переменных типа \code{char} мы можем изменять символы. Всё работает. Попробовав изменить какой-нибудь символ в \code{string1}, например, пятый, на символ \code{X} можно будет убедиться, что объявленная таким образом строка изменяемая, в отличие от строкового литерала, попытка изменить который приведёт к ошибке во время исполнения программы. Указатель на \code{char} нам не даёт возможности частично менять содержимое внутри строки, и, получается, представляет собой \textbf{immutable string} неизменяемую строку.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
string1[5] = 'X';
printf("%s\n", string1);
string2[5] = 'X';
printf("%s\n", string2);
\end{lstlisting}
\end{figure}
Обратите внимание, что компилятор не считает такую запись неверной и ошибка проявляет себя только во время исполнения программы. Таким образом, становится очевидно, что программисту недостаточно просто убедиться в том, что его код компилируется, но необходимо и проверить его работу во время исполнения. Часто в этом помогают Unit-тесты. Среди нерадивых программистов бытует мнение, что вообще все тесты должны писать специалисты в тестировании, но на примере выше, когда код компилируется, а программа всё равно не работает должным образом, мы видим, что некоторая более тщательная проверка своей работы должна быть выполнена именно программистом, а тестирование - это лишь инструмент, помогающий автоматизировать такие проверки.
\begin{verbatim}
$ ./program
Hello, world!
Hello, world!
HelloX world!
zsh: bus error ./program
$
\end{verbatim}
2021-09-30 16:01:28 +03:00
\subsection{Строки и функции}
2021-09-29 14:11:47 +03:00
Но довольно об ограничениях. Указатели на строки не такие уж бесполезные, мы можем, например, возвращать их из функций. То есть, мы можем объявить тип возвращаемого из функции значения как указатель \code{char*}, вернуть из нее строку и, например, вывести в консоль. Это открывает перед нами широчайшие возможности по работе с текстами.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
char* helloFunction() {
return "Hello!";
}
int main() {
printf("%s \n",* helloFunction ());
}
\end{lstlisting}
\end{figure}
Этот код выведет в консоль ожидаемое приветствие. Параллельно с написанием функции, приветствующей мир, предлагаю изучить некоторые стандартные возможности языка С для работы со строками. Например, специальную функцию, которая призвана выводить строки в консоль: \code{puts();} работает она очень похожим на \code{printf();} образом, но может выводить только строки, без каких-то других параметров, и всегда добавляет символ конца строки. Также изучим специальную функцию \code{gets();} которая призвана считывать строки из консоли и записывать их в переменные.
\frm{Функция \code{gets();} некоторыми компиляторами признана небезопасной, её использование рекомендуется заменить на \code{gets\_s();} В С11 и позднее небезопасная функция была вовсе удалена из стандартной библиотеки языка и перестала поддерживаться всеми производителями компиляторов.}
Создадим изменяемую строку типа \code{char[]}, назовём её \code{name}, передадим эту строку в функцию \code{gets();} и выведем на экран результат, полученный из консоли. Это будет очень полезная заготовка для дальнейшего общения с пользователем.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
char name[255];
gets(name);
puts(name);
\end{lstlisting}
\end{figure}
Теперь, мы можем поприветствовать пользователя нашей программы как следует, по имени. В нашей существовавшей функции приветствия внесём небольшие изменения. Создадим строку, в которой будем хранить приветственное слово, и в которую будет дописываться имя пользователя. Применим функцию склеивания строк. Поскольку склеивание - ненаучный термин, будем использовать слово \textbf{конкатенация}. И именно это слово подсказывает нам название функции, которую мы будем использовать: \code{strcat();}
\frm{Для использования функции \code{strcat();} необходимо подключить в программу заголовочный файл, содержащий функции, работающие со строками \code{\#include <string.h>}}
Функция принимает на вход два параметра - строку, \textit{к которой} нужно что-то прибавить, и строку, \textit{которую} нужно прибавить. Логично предположить, что первая строка должна быть изменяемой (то есть являться массивом символов, а не литералом). Функция прибавит все символы второй строки в первую (если в массиве хватит места) и вернёт указатель на изменённую строку. Очень удобно. Запустим наш проект, введем имя и убедимся что всё \textbf{сломалось}.
\begin{lstlisting}[language=C,style=CCodeStyle]
char* helloFunction(char* name) {
char welcome[255] = "Hello, ";
return strcat(welcome, name);
}
int main(int argc, const char* argv[]) {
char name[256];
gets(name);
puts(helloFunction(name));
return 0;
}
\end{lstlisting}
Что же случилось? Мы можем возвращать из функции только фиксированные строки, как в предыдущем примере. То есть, получается, нужно писать кейс, в котором содержатся все возможные имена, и оператором вроде \code{switch()} перебирать все возможные варианты ввода, и описывать все возможные приветствия пользователей, иначе мы устраиваем утечку памяти, создавая болтающийся в воздухе указатель, который никак не удалим? Нет, нас это, естественно, не устраивает. Что делать? Какой бы мы ни создали указатель в функции - он перестанет существовать, как только мы выйдем из области видимости этой функции.
\frm{В некоторых случаях может показаться, что никакой проблемы нет, поскольку написанная таким образом программа благополучно поприветствует пользователя, но такое поведение не гарантируется ни одним компилятором и ни одной операционной системой, поскольку возвращаемый таким образом указатель может быть переписан абсолютно любой следующей инструкцией кода. Такое \textit{исчезающее} значение называется \textbf{xvalue}.}
Выход очень простой: раз указатель не идёт в \code{int main (int argc, char *argv[])}, надо чтобы \code{int main (int argc, char *argv[])} дал нам указатель. Добавим в параметры функции указатель на выходную строку, и напишем что для начала сложить строки и положить в локальный массив \code{strcat(welcome, name)}. Добавим в основную функцию массив \code{char result[]}, который будет хранить результат и передадим в функцию \code{helloFunction} аргументы \code{name} и \code{result}. А раз функция больше ничего не возвращает, вполне легально сделать её \code{void}.
2022-02-02 00:08:39 +03:00
\begin{figure}[H]
\begin{lstlisting}[language=C,style=CCodeStyle]
void helloFunction(char* name, char* out) {
char welcome[255] = "Hello, ";
strcat(welcome, name);
out = welcome;
}
int main(int argc, const char* argv[]) {
char name[256];
char result[256];
gets(name);
helloFunction(name, result);
puts(result);
return 0;
}
\end{lstlisting}
\end{figure}
2021-09-29 14:11:47 +03:00
Запускаем, и \textbf{снова не работает}, да ещё и как интересно, смотрите! Предупреждение, ладно, понятно, мы о нём говорили, но дальше, когда мы вводим имя на выходе получается какая-то совсем уж непонятная строчка, совсем не похожая на приветствие.
\begin{verbatim}
$ ./program
warning: this program uses gets(), which is unsafe.
Ivan
??:
$
\end{verbatim}
А все дело в том, что строк в языке С за время повествования не появилось, и все манипуляции со строками - это довольно сложные алгоритмы работы с памятью и массивами символов. Поэтому, работая со строками, мы должны использовать библиотечные функции, например, есть функция \code{strcpy()}, которая не просто перекладывает указатель в определенную переменную, а копирует строку.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
void helloFunction (char* name, char* out) {
char welcome[255] = “Hello, ”;
strcat(welcome, name);
strcpy(out, welcome);
}
int main(int argc, const char* argv[]) {
char name[256];
char result[256];
gets(name);
helloFunction(name, result);
puts(result);
return 0;
}
\end{lstlisting}
\end{figure}
Если присмотреться, то можно заметить, что все функции работающие со строками, именно так и делают - запрашивают источник данных и конечную точку, куда данные нужно положить. А кто мы такие, чтобы спорить со стандартными библиотечными функциями? Обратите внимание на то, что функции \code{strcat();} и \code{strcpy();} возвращают указатель на получившуюся строку. Мы перестали возвращать указатель на получившуюся строку, поскольку никто не гарантирует, что он просуществует достаточно долго, и тут встаёт вопрос о вызывающем функцию контексте, нужен ли этот указатель вызывающему. В случае необходимости, конечно, его можно вернуть. Работа со строками в С до сих пор является очень и очень актуальной темой на программистских форумах, можете удостовериться в этом самостоятельно.
2021-09-30 16:01:28 +03:00
Раз уж заговорили о стандартной библиотеке, рассмотрим ещё пару-тройку интересных функций. Например, сравнение строк: функция \code{strcmp();} допустим, я хочу, чтобы именно меня программа приветствовала как-то иначе. Функция возвращает отрицательные значения, если первая строка меньше второй, положительные, если первая больше второй, и ноль, если строки равны. Это функция, которую удобно применять в условиях. Если строки будут действительно равны, мы скопируем в строку с именем слово \code{Creator}.
2021-09-29 14:11:47 +03:00
\begin{lstlisting}[language=C,style=CCodeStyle]
void helloFunction (char* name, char* out) {
char welcome[255] = “Hello, ”;
if (strcmp("Ivan", name) == 0)
strcpy(name, "Creator");
strcat(welcome, name);
strcpy(out, welcome);
}
\end{lstlisting}
2021-09-30 16:01:28 +03:00
Из всех функций для работы со строками чрезвычайно часто используются \code{atoi();} и \code{atof();} переводящие написанные в строке цифры в численные переменные внутри программы. \code{atoi();} переводит в \code{int}, а \code{atof();} во \code{float}, соответственно.
\frm{Существует несколько десятков функций, разнообразно преобразующих одни типы данных в другие и обратно, например, функции \code{atoi();} и \code{atof();} имеют ответные \code{itoa();} и \code{ftoa();}. Такие функции пишутся и для высокоуровневых библиотек, например, преобразование строковой записи в понятный компьютеру для соединения IP-адрес.}
Для примера объявим переменную \code{num}, предложим пользователю ввести цифру, естественно в виде строки. Будем принимать ее при помощи небезопасной функции \code{gets();}, хотя как мы помним, могли бы и \code{scanf();} который сразу бы преобразовал строку согласно использованного заполнителя. Заведем переменную \code{int number} для хранения результата работы функции преобразования. Затем, давайте умножим результат сам на себя, чтобы убедиться, что это и правда число, причём именно то, которое мы ввели, и выведем окончательное число в консоль.
2021-09-29 14:11:47 +03:00
\begin{lstlisting}[language=C,style=CCodeStyle]
char num[64];
puts("Enter a number: ");
gets(num);
int number = atoi(num);
number *= number;
printf("We powered your number to %d", number);
\end{lstlisting}
2021-09-30 16:01:28 +03:00
2021-09-29 14:11:47 +03:00
Полный список функций для работы со строками можно посмотреть в заголовочном файле \code{string.h}. Описание и механика их работы легко гуглится, документации по языку очень много.
2021-09-30 16:01:28 +03:00
\subsection{Работа с символами}
2021-09-29 14:11:47 +03:00
В завершение беседы о строках и манипуляциях с ними, скажем ещё пару слов об обработке символов. Функции для работы с символами содержатся в заголовочном файле \code{stdlib.h}. Естественно, наша программа может получить от пользователя какие-то значения в виде строк. Не всегда же есть возможность использовать \code{scanf();} например, считывание из графических полей ввода или потоковый ввод данных из сети даёт нашей программе значения в виде строк. Стандартная библиотека языка С предоставляет нам функции для работы с каждым символом строки, например:
\begin{itemize}
\item \code{isalpha();} возвращает истину, если символ в аргументе является символом из алфавита;
\item \code{isdigit();} возвращает истину, если символ в аргументе является цифрой;
\item \code{isspace();} проверяет, является ли переданный в аргументе символ пробельным;
\item \code{isupper();} \code{islower();} находится ли переданный в аргументе символ в верхнем или нижнем регистре;
\item \code{toupper();} \code{tolower();} переводят символ в верхний или нижний регистр, соответственно.
\end{itemize}
Можем использовать одну из них соответственно нашей задаче, допустим, пользователь может вводить своё имя как с заглавной буквы, так и всеми строчными. Уравняем оба варианта для нашей проверки одной строкой \code{name[0] = tolower(name[0]);} а после проверки вернём заглавную букву на место \code{name[0] = toupper(name[0]);} и удостоверимся что даже если мы напишем своё имя с маленькой буквы - программа напишет его с большой.
\begin{lstlisting}[language=C,style=CCodeStyle]
void helloFunction (char* name, char* out) {
char welcome[255] = “Hello, ”;
name[0] = tolower(name[0]);
if (strcmp("ivan", name) == 0)
strcpy(name, "Creator");
name[0] = toupper(name[0]);
strcat(welcome, name);
strcpy(out, welcome);
}
\end{lstlisting}