basic-c/sections/10-strings.tex

253 lines
30 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

\section{Строки}
\subsection{Основные понятия}
Получив в предыдущих разделах представление об указателях и массивах, и вскользь несколько раз упомянув строки, пришла пора изучить их подробнее.
Итак, что же такое \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.
\subsection{Особенности}
Немного вспомнив, что такое символы, переходим к строкам. Строками мы пользуемся с самых первых страниц этого документа: написав в двойных кавычках <<Привет, Мир>>, мы использовали строку, а если точнее, строковый литерал. Строки иногда называют типом данных, но в языке С строка это указатель на последовательно записанный набор символов, поэтому работать с ним можно, как с массивами. Строки в языке С можно описать двумя способами: как указатель и как массив из переменных типа 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}
\subsection{Строки и функции}
Но довольно об ограничениях. Указатели на строки не такие уж бесполезные, мы можем, например, возвращать их из функций. То есть, мы можем объявить тип возвращаемого из функции значения как указатель \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}.
\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}
Запускаем, и \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();} возвращают указатель на получившуюся строку. Мы перестали возвращать указатель на получившуюся строку, поскольку никто не гарантирует, что он просуществует достаточно долго, и тут встаёт вопрос о вызывающем функцию контексте, нужен ли этот указатель вызывающему. В случае необходимости, конечно, его можно вернуть. Работа со строками в С до сих пор является очень и очень актуальной темой на программистских форумах, можете удостовериться в этом самостоятельно.
Раз уж заговорили о стандартной библиотеке, рассмотрим ещё пару-тройку интересных функций. Например, сравнение строк: функция \code{strcmp();} допустим, я хочу, чтобы именно меня программа приветствовала как-то иначе. Функция возвращает отрицательные значения, если первая строка меньше второй, положительные, если первая больше второй, и ноль, если строки равны. Это функция, которую удобно применять в условиях. Если строки будут действительно равны, мы скопируем в строку с именем слово \code{Creator}.
\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}
Из всех функций для работы со строками чрезвычайно часто используются \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} для хранения результата работы функции преобразования. Затем, давайте умножим результат сам на себя, чтобы убедиться, что это и правда число, причём именно то, которое мы ввели, и выведем окончательное число в консоль.
\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}
Полный список функций для работы со строками можно посмотреть в заголовочном файле \code{string.h}. Описание и механика их работы легко гуглится, документации по языку очень много.
\subsection{Работа с символами}
В завершение беседы о строках и манипуляциях с ними, скажем ещё пару слов об обработке символов. Функции для работы с символами содержатся в заголовочном файле \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}