finished arrays, wrote strings

This commit is contained in:
ivan-igorevich 2021-09-29 14:11:47 +03:00
parent 11de104fc4
commit 0d00dcecb6
5 changed files with 639 additions and 530 deletions

Binary file not shown.

View File

@ -13,6 +13,12 @@
\usepackage{titlesec}
\usepackage{hyperref}
\usepackage{tikz}
\usepackage{ascii}
\usepackage{textcomp}
\usepackage{eurosym}
\usepackage{cclicenses}
\usetikzlibrary{positioning}
\usepackage[lmargin=1.5cm,rmargin=2.5cm,tmargin=2.5cm,bmargin=2.5cm,paperheight=240mm,paperwidth=170mm]{geometry}

534
main.tex
View File

@ -25,536 +25,10 @@
\import{sections/}{07-functions}
% 08 pointers
\import{sections/}{08-pointers}
\section{Массивы}
В этом разделе нас с вами ждут массивы. Много массивов. И ещё пара слов о директивах компилятору, иногда также называемых директивами препроцессора. С них и начнём.
\subsection{Директива \code{\#define}}
Помимо уже хорошо знакомой вам директивы \code{\#include}, частично описанной в разделе \hyperref[text:directive]{\ref{text:directive}}, естественно, существуют и другие. Некоторые из них ограничивают импорт описанных в заголовочном файле функций, некоторые <<\textbf{описывают}>> какие-то константы и даже действия. Вот, директиву \textbf{описать} мы и рассмотрим подробнее. Она не зря называется директивой препроцессора, поскольку даёт указание не процессору во время выполнения программы выделить память, присвоить значения, а непосредственно компилятору: заменить в тексте программы одни слова на другие. Таким образом можно задавать константы проекта, и даже делать сокращённые записи целых действий. Например, написав \code{\#define ARRAY\_LENGTH 50} мы предпишем компилятору, перед запуском трансляции нашего кода заменить все слова \code{ARRAY\_LENGTH} на цифру 50. В такой записи, слово \code{ARRAY\_LENGTH} будет называться \textit{макроконстантой}.
\frm{Обратите внимание, что директива пишется немного не так, как обычный оператор языка, хоть и может находиться в любом месте кода. В конце директивы не ставится точка с запятой. Это важно именно потому что директивы работают с текстом программы, то есть если точка с запятой всё же будет поставлена, текст программы будет всегда содержать вместо макроконстанты число и точку с запятой, что может в корне изменить смысл программы.}
Весьма удобно, но этим можно не ограничиваться, мы можем попросить компилятор заменить вызовы функций и операторы на короткие, удобные нам слова. Важно помнить, что директивы препроцессора работают с текстом программы, поэтому не осуществляют никаких дополнительных проверок. Это сложный и мощный инструмент, который чаще всего используется для решения нетривиальных задач, например, выбор кода, который попадёт в компиляцию в зависимости от операционной системы. Иногда в программах можно встретить описание недостающего, но такого привычного булева типа при помощи директив препроцессора:
\begin{lstlisting}[language=C,style=CCodeStyle]
#define bool int
#define true 1
#define false 0
\end{lstlisting}
Но нам пока что достаточно умения создать глобальную именованную константу. Код ниже демонстрирует, что директивы не обязательно группировать именно в начале файла, а можно использовать там, где это удобно и уместно, так мы можем объявить константу с длиной массива в начале файла, а можем прямо внутри функции \code{int main (int argc, char *argv[])}.
\begin{lstlisting}[language=C,style=CCodeStyle]
int main(int argc, char* argv[]) {
#define ARRAY_LENGTH 50
int a = ARRAY_LENGTH;
printf("a = %d", a);
return 0;
}
\end{lstlisting}
Результатом работы этой функции будет ожидаемое:
\begin{verbatim}
$ ./program
a = 50
\end{verbatim}
\subsection{Массивы}
Вступление про директивы препроцессора напрямую не связано с темой массивов, но директива \code{\#define} для объявления размера массива применяется чрезвычайно часто. Рассмотрим природу этого явления чуть позже.
\frm{Массив это множество данных одного типа, расположенных в памяти подряд.}
Язык С строго типизирован, поэтому невозможно создать массив из разных типов данных. На данном этапе мы рассматриваем только простые типы данных, поэтому и массивы будем рассматривать статические. Статическим массивом называют массив, количество элементов которого заранее известно и не изменяется за время работы программы. Альтернативой статическому массиву является динамический, таких массивов в языке С не существует, но всегда можно самостоятельно описать такую структуру данных, которая будет хранить значения, динамически расширяясь и сужаясь. Также для начала ограничим нашу беседу одномерными массивами, то есть такими, которые можно записать в виде значений через запятую. Статические одномерные массивы принято объявлять двумя способами:
\begin{itemize}
\item простое объявление с указанием размера;
\item объявление, совмещённое с инициализацией
\end{itemize}
Для примера объявим массив, содержащий элементы типа \code{int}, дадим ему идентификатор или имя массива \code{arr} (сокращённо от англ array), укажем максимальное количество элементов которые может вместить в себя массив, например, пять.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[5];
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
\end{lstlisting}
\end{figure}
Как уже говорилось массив это множество данных или элементов. К каждому элементу массива можно обратиться по его номеру, который принято называть индексом. Индексация элементов начинается с нуля. Давайте заполним наш массив значениями типа \code{int}. Для этого последовательно обратимся к каждому элементу и присвоим значение. Обратите внимание, что язык С не гарантирует что инициализационное значение элементов массива будет равно нулю, если это не указано явно, поэтому выведя на экран содержимое массива, мы можем гарантировать значения только первых трёх элементов, которые мы указали в коде. Второй способ объявления, совмещённый с инициализацией массива используют, если массив сравнительно небольшой и его значения заранее известны, например:
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[6] = {1, 1, 2, 3, 5, 8};
\end{lstlisting}
При этом, если сразу заполняются все элементы, размерность можно не указывать. Итак, мы научились создавать и заполнять значениями массивы. Теперь общее правило объявления массивов в С: при объявлении массива нужно указать его имя, тип элементов, количество элементов, опционально - указать сами эти элементы. Количество элементов есть натуральное число, то есть целое положительное, ноль не может быть количеством элементов. Нельзя задавать переменное количество элементов массива.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int nArr[100]; // An array for 100 int's;
float fArr[5]; // An array for 5 float's;
char cArr[2]; // An array for 2 char's;
int varElem;
int nArr[varElem]; // Compile error! Number of elements must be constant;
\end{lstlisting}
\end{figure}
Так мы обязаны создавать массивы только с точно указанным числом элементов. Для языка С это позволено сделать объявлением константы времени исполнения \code{const int elements; int arr[elements]}, но, например, в С++ такая запись вызовет ошибку компиляции, поэтому там необходимо строго указывать размер числовым литералом или объявив его директивой \code{\#define}, что, фактически, одно и тоже.
\frm{В более поздних стандартах С++ появилось ключевое слово \code{constexpr}, позволяющее объявлять константы времени компиляции и отказаться от объявления размеров массива только литералом}
Теперь давайте научимся получать доступ к элементам массива. Нет ничего проще, тем более, что мы это уже делали объявляли массив и для примера его заполняли. Для доступа к конкретному элементу массива нужно указать имя массива и индекс элемента в квадратных скобках. Квадратные скобки - это тоже оператор языка, он называется оператором индексного доступа:
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int a = arr[0];
printf("lets see whats in 0-th element: %d", a);
\end{lstlisting}
\end{figure}
При помощи массивов решают множество задач, таких как поиск, сортировка, составление таблиц соответствия, создание частотных диаграмм. На основе массивов создают более сложные структуры данных. Для короткого минимального примера, давайте напишем программу, которая будет печатать наш массив в консоль.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
#include <stdio.h>
int main(int argc, char *argv[]) {
int arr[5];
int i;
printf("Your array is: ");
for (i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
return 0;
}
\end{lstlisting}
\end{figure}
Такая несложная программа даст нам следующий результат (Обратите внимание, что при такой инициализации, а точнее её отсутствии, значения внутри массива не гарантируются. В результате запуска программы на компьютере автора первые четыре индекса оказались равными нулю, а пятый принял странное отрицательное целочисленное значение):
\begin{verbatim}
$ ./program
Your array is 0 0 0 0 -497067408
\end{verbatim}
Мы научились создавать, инициализировать массивы и обращаться к его элементам. Теперь решим задачу посложнее: напишем программу, которая проверит насколько статистически хорош описанный в стандартной библиотеке (языка С) генератор псевдо-случайных чисел (функция \code{rand();}). Для такой статистической проверки нам понадобится сформировать так называемый \textit{частотный массив}, массив, в котором будет содержаться информация о том, сколько раз то или иное число появилось во множестве значений, полученном при помощи генератора псевдослучайных чисел, частота вхождения значений. Сама генерация псевдослучайных чисел происходит при помощий функции \code{rand();} которая создаёт целое число типа \code{int}. Но, поскольку целое число в таком диапазоне нам не нужно, мы его сократим при помощи оператора получения остатка от деления.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ARRAY_LENGTH 10
#define NUMBERS_AMOUNT 1000000
int main( int argc, char *argv[]){
srand(time(NULL)); // initialize PRNG
int frequency[ARRAY_LENGTH] = {0};
int a;
int i;
for (i = 0; i < NUMBERS_AMOUNT; i++) {
a = rand() % ARRAY_LENGTH;
frequency[a]++;
}
for (i = 0; i < ARRAY_LENGTH; i++) {
printf("Number %d generated %6d (%5.2f%%) times\n",
i,
frequency[i],
((float)frequency[i] / NUMBERS_AMOUNT * 100));
}
return 0;
}
\end{lstlisting}
\end{figure}
Обратите внимание на 14ю строку: для сгенерированного на 13й строке числа \code{0} увеличим значение в 0-й ячейке массива, для числа \code{1} - в 1-й, и т.д. Данная программа наглядно демонстрирует не только работу с массивами, но и то, что генератор псевдослучайных чисел в языке С генерирует статистически верную последовательность случайных чисел. Инициализация генератора псевдослучайных чисел на восьмой строке \code{srand(time(NULL));} происходит значением текущего времени системы, то есть мы гарантируем, что начальное значение генератора будет отличаться от запуска к запуску, а значит наше исследование будет чуть более достоверным.
\begin{figure}[h!]
\begin{verbatim}
$ ./program
Number 0 generated 99955 (10.00%) times
Number 1 generated 99977 (10.00%) times
Number 2 generated 100156 (10.02%) times
Number 3 generated 99952 (10.00%) times
Number 4 generated 100212 (10.02%) times
Number 5 generated 100713 (10.07%) times
Number 6 generated 99418 ( 9.94%) times
Number 7 generated 99768 ( 9.98%) times
Number 8 generated 99918 ( 9.99%) times
Number 9 generated 99931 ( 9.99%) times
\end{verbatim}
\end{figure}
Запуск программы (даже несколько запусков) показывает, что всех значений получилось около десяти процентов, что говорит о том, что последовательность псевдослучайных чисел статистически верна.
\subsection{Идентификатор массива}
Это будет непросто, но мы поговорим о том, что из себя представляет идентификатор массива, чем чреват выход за пределы массива, затронем тему арифметики указателей и научимся передавать указатели на массивы в функции. Как упоминалось ранее, массив - это ссылочный тип данных. То есть в идентификаторе хранится адрес, ссылка на первый байт первого элемента массива, дальнейший доступ к элементам осуществляется посредством \textit{смещения относительно этого байта}. Таким образом запись вида \code{array[0]} говорит нам о том, что нужно взять адрес массива и сместить указатель на \code{0} элементов того типа, из которых состоит массив. Отсюда становится ясно, почему \textbf{индексирование массивов начинается с нуля}.
Давайте попробуем визуализировать положение вещей в массивах. На рисунке со стр. \hyperref[pic:arrays]{\pageref{pic:arrays}} можно увидеть создание массива на первой строке, далее доступ к двум его ячейкам для записи в них значений.
\begin{figure}[h!]
\begin{multicols}{2}
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[ARRAY_LENGTH];
arr[0] = 20;
arr[1] = 50;
\end{lstlisting}
\columnbreak
\begin{tikzpicture}
\draw[step=0.5cm,gray,very thin] (0,-0.5) grid (5, 2);
\draw(0, -0.5) -- (5, -0.5) -- (5, 0) -- (0, 0) -- cycle;
\draw(0, 0.5) -- (5, 0.5) -- (5, 1) -- (0, 1) -- cycle;
\draw(0, 1.5) -- (5, 1.5) -- (5, 2) -- (0, 2) -- cycle;
\fill[blue!40!white] (0, 0.5) rectangle (0.5, 1);
\fill[blue!40!white] (0.5, -0.5) rectangle (1, 0);
\end{tikzpicture}
\end{multicols}
\caption{Визуализация создания и индексации массива}
\label{pic:arrays}
\end{figure}
Соответственно, когда мы берем элемент с нулевым смещением, то есть по нулевому индексу, это будет ячейка памяти, находящаяся ровно по адресу массива. Все остальные ячейки мы просто игнорируем. Далее, берем следующий элемент массива. Для этого возьмем смещение в единицу, допустим, это будет число 50. Соответственно смещение будет на одну ячейку типа \code{int}. То есть мы игнорируем нулевой элемент и берем первый. Становится очевидно, что если мы в своём коде напишем такой индекс, который находится за пределами описанного массива - мы просто получим какое-то значение, которое никак не можем прогнозировать.
Относительно выхода за пределы массива надо сказать, что ни компилятор, ни тем более операционная система никаких проверок не делают, поэтому такие проверки (чаще всего самопроверки на этапе написания кода) полностью ложатся на плечи программиста. Язык С не предоставляет никаких сред виртуализации, никаких подсистем исключений, только выдача случайных данных, которые могут попасться нашей программе. Или запись в ячейки, которые совершенно не гарантированно останутся пустыми.
\frm{В связи с тем, индекс массива - это значение смещения, относительно его начала, и, как следствие, индексы массива всегда отсчитываются с нуля, важно помнить, что при создании массива из, например, десяти элементов десятый индекс будет находиться за пределами массива.}
Надо сказать, что всё-таки б\'{o}льшая часть значений за пределами массива будет равна нулю, но всё равно лишний раз экспериментировать не стоит.
\begin{figure}[h!]
\begin{multicols}{2}
\begin{lstlisting}[language=C,style=CCodeStyle]
arr[11] = 60;
\end{lstlisting}
\columnbreak
\begin{tikzpicture}
\draw[step=0.5cm,gray,very thin] (0,-0.5) grid (6, 0);
\draw(0, -0.5) -- (5, -0.5) -- (5, 0) -- (0, 0) -- cycle;
\fill[red!40!white] (5.5, -0.5) rectangle (6, 0);
\end{tikzpicture}
\end{multicols}
\caption{Выход за пределы массива}
\label{pic:outofbounds}
\end{figure}
Как мы уже знаем, в идентификаторе массива хранится ссылка на первый байт первого элемента массива, т.е. идентификатор является, по сути, указателем. Но существует несколько отличий: указатель - это переменная, к ней применимы, например, операции инкремента и декремента, чего конечно нельзя делать с идентификатором массива.
\frm{Идентификатор массива \textbf{не является} lvalue, Но один элемент массива является lvalue. Так, записи вида \code{arr++;} или \code{arr = 5} будут являться ошибочными, поскольку в них происходит обращение не к конкретному элементу с целью его изменения, а к массиву целиком.}
Обратившись к идентификатору массива мы можем получить доступ к элементам массива не только при помощи записи индекса в квадратных скобках, но и при помощи так называемой арифметики указателей. Мы знаем, что массив - это единая область памяти, и значения в нём располагаются подряд по очереди, значит, отсчитав от указателя на первый индекс нужное количество байт - мы получим указатель на второй индекс. Давайте для примера подсчитаем среднее арифметическое всех чисел в массиве, с использованием арифметики указателей.
Будем запрашивать значения для расчётов у пользователя. Создадим вспомогательную переменную \code{float result;}, для хранения результата и в цикле будем запрашивать у пользователя числа. Количество введенных цифр должно соответствовать количеству элементов массива, поэтому условием выхода из цикла будет равенство счётчика и последнего индекса массива, то есть, длины массива минус единица.
\frm{Обратите внимание, что в коде мы указали условием выхода из цикла \textbf{строгое неравенство}, то есть со значением \code{i} равным длине массива в тело цикла мы не попадём.}
Помним, что индексация массива начинается с нуля, поэтому длина массива всегда на единицу больше последнего индекса. Выведем в консоль надпись <<введите значение>>, при помощи функции \code{scanf();} считаем его и сразу привычным образом, оператором индексного доступа, положим в массив.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[ARRAY_LENGTH];
int i = 0;
float result = 0;
while (i < ARRAY_LENGTH) {
printf("Enter value %d: ", i);
scanf("%d", arr[i]);
i++;
}
\end{lstlisting}
\end{figure}
Выведем в консоль получившийся массив при помощи цикла \code{for(;;)} и привычной нам функции \code{printf();}. Следом напишем ещё один цикл в котором подсчитаем среднее арифметическое. Для этого к результату будем прибавлять существующий результат и значение массива на которое указывает уже не такая привычная, как квадратные скобки конструкция \code{*(arr + i)}, которую сразу и разберём.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
printf("Your array is: ");
for (i = 0; i < ARRAY_LENGTH; i++)
printf("%d ", arr[i]);
printf("\nAnd the average is: ");
for (i = 0; i < ARRAY_LENGTH; i++)
result += *(arr + i);
printf("%f\n", result / ARRAY_LENGTH);
\end{lstlisting}
\end{figure}
Как вы видите, некоторые подсчеты программа выполняет за нас - мы прибавляем к указателю единицу, двойку, тройку и т.д, а программа понимает, что надо взять не следующий по счёту байт, а следующий указатель. Так как в данном примере мы используем массив в котором хранятся значения типа \code{int}, а как вы помните \code{int} в подавляющем большинстве случаев - это четыре байта, то при увеличении указателя на единицу, мы обратимся к области памяти находящейся на четыре байта дальше идентификатора, при увеличении на двойку на восемь байт и так далее. Подсчитать среднее арифметическое не составит труда, этот алгоритм нам знаком со средней школы. Далее при помощи функции \code{printf();} выведем в консоль среднее арифметическое. Запустим, повводим цифры и убедимся что все работает.
\begin{verbatim}
$ ./program
Enter value 0: 1
Enter value 1: 2
Enter value 2: 3
Enter value 3: 4
Enter value 4: 5
Enter value 5: 6
Enter value 6: 7
Enter value 7: 8
Enter value 8: 9
Enter value 9: 10
Your array is: 1 2 3 4 5 6 7 8 9 10
And the average is: 5.500000
\end{verbatim}
Внимательный читатель мог заметить, что мы применяем операцию \textit{разыменования}. Что происходит, когда мы таким образом обращаемся к массиву? Операция разыменования получает доступ к значению, находящемуся по адресу. Адресс массива - это адрес его первого элемента, поэтому конструкция \code{*arr} вернёт значение нулевого элемента массива. А прибавление значений к этому указателю будет смещать его также, как это делает оператор квадратных скобок.
Как уже упоминалось, идентификатор массива - это не обычный указатель. Обычный указатель хранит в себе адрес какой-то другой переменной, и сам где-то хранится. Указатель на начало массива хранит в себе адрес массива, то есть адрес его нулевого элемента, и сам этот указатель находится в этом самом месте. На первый взгляд сложновато? Но пусть Вас это не сбивает с толку, на деле всё не так жутко. На деле это означает, что при передаче массива (читай идентификатора массива) в функцию в качестве аргумента, мы не должны использовать оператор взятия адреса, поскольку идентификатор массива сам по себе является указателем на собственное начало. Это открывает для нас широкие возможности по написанию функций, работающих с массивами данных. В только что написанной нами программе оформим вывод массива на экран и поиск среднего арифметического в виде функции. Опишем функции \code{printArray()} и \code{average()} в которые передадим указатель на массив и его длину, т.к. в массиве не содержится сведений о его размере.
Поскольку мы передаём в функцию указатель, то все действия которые описаны в этой функции будут происходить с массивом который мы создали в основной части программы через этот указатель, который мы передали, никакого копирования значений или чего то подобного. Для корректной работы наших функций объявим в них счётчик и изменим названия переменных на названия параметров.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
void printArray(int* array, int length) {
int i;
for (i = 0; i < length; i++)
printf("%d ", array[i]);
}
float average(int* array, int length) {
float result = 0;
int i;
for (i = 0; i < length; i++)
result += *(array + i);
return result / length;
}
\end{lstlisting}
\end{figure}
Так, полный листинг этого примера на стр. \hyperref[code:arrayaverage]{\pageref{code:arrayaverage}}.
\begin{figure}[h!]
\lstinputlisting[language=C,style=CCodeStyle]{../sources/arrayaverage.c}
\label{code:arrayaverage}
\end{figure}
\newpage
\subsection{Многомерные массивы}
Массив в языке С может иметь сколько угодно измерений. Все массивы, с которыми мы имели дело до этого момента - одномерные, их легко визуализировать в виде простого перечисления элементов, возможно, как строки или как таблицы, состоящей из одной строки. Самые распространённые многомерные массивы - это двумерные и трёхмерные, которые легко себе представить в виде таблицы или куба соответственно. Итак, массив это структура, содержащая элементы. Двумерный массив - это массив из массивов, содержащих элементы. Трёхмерный - это массив из массивов, содержащих массивы, которые содержат элементы. И так далее. В массиве могут находиться любые типы данных, мы, для удобства, будем рассматривать работу массивов с числами.
Попробуем визуализировать двумерный массив. Создадим двумерный массив в коде, например, 5х5 элементов. Массив 5х5 это 5 столбцов и 5 строчек. Соответственно, \textit{каждая строчка это будет у нас младший индекс, а каждый столбец старший индекс}. Трехмерный массив может быть, например, 3х3х3 его можно визулизировать как всем известный кубик Рубика то есть, это три стоящих друг за другом таблицы 3х3. Также опишем его в коде ниже. Получается, что мы к таблице (ширине и высоте) добавили третье \textbf{измерение}, поэтому и массив получается \textbf{многомерным}, в данном случае, \textbf{трёхмерным}. Массивы б\'{о}льших размерностей тоже можно встретить в программах, но значительно реже, только лишь потому, что их действительно немного сложнее представить себе.
\begin{figure}[h!]
\begin{multicols}{2}
\begin{lstlisting}[language=C,style=CCodeStyle]
int twoDimensional[5][5];
\end{lstlisting}
\begin{center}
\begin{tabular}{||c|c|c|c|c||}
\hline
\hline
0,0 & 0,1 & 0,2 & 0,3 & 0,4 \\
\hline
1,0 & 1,1 & 1,2 & 1,3 & 1,4 \\
\hline
2,0 & 2,1 & 2,2 & 2,3 & 2,4 \\
\hline
3,0 & 3,1 & 3,2 & 3,3 & 3,4 \\
\hline
4,0 & 4,1 & 4,2 & 4,3 & 4,4 \\
\hline
\hline
\end{tabular}
\end{center}
\columnbreak
\begin{lstlisting}[language=C,style=CCodeStyle]
int threeDimensional[3][3][3];
\end{lstlisting}
\begin{tikzpicture}[every node/.style={minimum size=1cm},on grid]
\begin{scope}[every node/.append style={yslant=-0.5},yslant=-0.5]
\node at (0.5,2.5) {2,0,0};
\node at (1.5,2.5) {1,0,0};
\node at (2.5,2.5) {0,0,0};
\node at (0.5,1.5) {2,1,0};
\node at (1.5,1.5) {1,1,0};
\node at (2.5,1.5) {0,1,0};
\node at (0.5,0.5) {2,2,0};
\node at (1.5,0.5) {1,2,0};
\node at (2.5,0.5) {0,2,0};
\draw (0,0) grid (3,3);
\end{scope}
\begin{scope}[every node/.append style={yslant=0.5},yslant=0.5]
\node at (3.5,-0.5) {0,0,0};
\node at (4.5,-0.5) {0,0,1};
\node at (5.5,-0.5) {0,0,2};
\node at (3.5,-1.5) {0,1,0};
\node at (4.5,-1.5) {0,1,1};
\node at (5.5,-1.5) {0,1,2};
\node at (3.5,-2.5) {0,2,0};
\node at (4.5,-2.5) {0,2,1};
\node at (5.5,-2.5) {0,2,2};
\draw (3,-3) grid (6,0);
\end{scope}
\begin{scope}[every node/.append style={yslant=0.5,xslant=-1},yslant=0.5,xslant=-1]
\node at (3.5,2.5) {2,0,0};
\node at (3.5,1.5) {1,0,0};
\node at (3.5,0.5) {0,0,0};
\node at (4.5,2.5) {2,0,1};
\node at (4.5,1.5) {1,0,1};
\node at (4.5,0.5) {0,0,1};
\node at (5.5,2.5) {2,1,2};
\node at (5.5,1.5) {1,1,2};
\node at (5.5,0.5) {0,1,2};
\draw (3,0) grid (6,3);
\end{scope}
\end{tikzpicture}
\end{multicols}
\end{figure}
Как работать с многомерными массивами мы рассмотрим на примере двумерного массива. Поставим для себя задачу - сформировать таблицу Пифагора (раньше такие на тетрадях в клетку печатали на обратной стороне). Таблица подразумевает наличие двух измерений - строк и колонок. Для этого объявим константы rows и cols и присвоим им значения 10, rows это количество строк, а cols - соответственно столбцов. Создадим двумерный массив, table[rows][cols]. Итак, мы создали массив размером rows, в каждом элементе которого содержится ссылка на массив размером cols - т.е. массив массивов содержащих непосредственные значения.
% Таблица Пифагора представляет собой таблицу, где строки и столбцы озаглавлены множителями, а в ячейках таблицы находится их произведение. Вот это самое произведение мы и будем выводить.
% Заполнение таких массивов значениями ничем не отличается от заполнения одномерных массивов. Заполним нашу матрицу поэлементно: напишем двойной цикл который будет заполнять нашу таблицу.
% Объявим переменные итераторы. И с помощью внешнего цикла фор пройдемся по всем строкам массива, а с помощью вложенного по всем столбцам массива, при этом будем записывать в каждый элемент массива результат умножения. Формула (r + 1) * (c + 1) позволяет исключить 0 из нашей таблицы. Давайте разберем более подробно работу такой конструкции. Внешний цикл при каждой итерации перемещает нас на одну строчку вниз. Вложенный, при каждой итерации, перемещает нас на одно значение вправо. Важно понять, что на одну итерацию внешнего цикла приходится cols итераций вложенного. Т.е. с помощью такой конструкции мы поочередно перебираем все элементы массива.
% Также есть способ заполнять многомерные массивы посредством конструкции в фигурных скобках, этот подход используется когда мы заранее знаем все значения. Например, вот так:
% int arr[3][4] = {{0,1,2,3}, {4,5,6,7}, {8,9,10,11}} - объявляем и инициализируем двумерный массив размером 3х4.
% Далее напишем такой же двойной цикл, который будет выводить на экран наши двумерные массивы в удобном для нас виде.
% Запустим нашу программу и увидим, что все отлично работает.
% void advancedArraysMain() {
% printf(“Multi-dimensional Arrays! \n”);
% const int rows = 10, cols = 10;
% int table[rows][cols];
% // int arr[3][4] = {{0,1,2,3}, {4,5,6,7}, {8,9,10,11}}
% int r, c;
% for(r = 0; r < rows; r++) {
% for(c = 0; c < cols; c++)
% table[r][c] = (r+1)*(c+1);
% }
% for(r = 0; r < rows; r++) {
% for(c = 0; c < cols; c++)
% printf("%3d ", table[r][c]);
% printf("\n");
% }
% Как уже говорилось, массивы могут содержать данные любых типов, в том числе и указатели. Именно это позволяет массиву хранить другие массивы, строки и прочие ссылочные типы данных. Используя массивы указателей, мы можем создать, например, массив строк.
% Давайте запишем char* stringArray[3] = {"Hello", "C", "World"};
% Это указатели на строки, такой тип данных является указателем. И мы можем создать из этих указателей массив. Используя массивы указателей, мы можем создать, например, двумерный массив, где каждый элемент не обязан быть того же размера, что и остальные.
% Но строки и сложно-составленные указатели - это темы, которые очень сильно выходят за рамки Основ языка, но это не помешает нам разобраться со строками на следующих уроках
% char* stringArray[3] = {"Hello", "C", "World"};
% for(r = 0; r < 3; r++)
% printf("%s ", stringArray[r]);
% На этом уроке мы познакомились с многомерными массивами и узнали о том, что массив может быть не только из примитивов, но и из указателей. До новых встреч на следующих уроках, коллеги.
% СЛАЙД С ИТОГАМИ
\section{Строки}
% Здравствуйте, уважаемые коллеги. Рад всех приветствовать на очередном видео курса Основы языка С. Получив на предыдущих уроках представление об указателях и массивах, и вскользь упомянув строки, пришла пора изучить их подробнее.
% Итак, что же такое строка. В повседневной жизни строка это набор или последовательность символов. Так вот в языке Си строка это тоже последовательность символов. А последовательности в языке Си представлены, вы, наверное, уже догадались, массивами и указателями. Никакого примитива Стринг в языке Си нет. Как бы нам этого не хотелось - его нет. Но есть одна хорошая новость, примитива Стринг не существует и в других языках (я имею ввиду, конечно, си-подобные языки, то есть примерно треть вообще всех языков программирования). Раз строка - это массив или указатель - это всегда ссылочный тип данных.
% СЛАЙД ПРО Си-СТРОКИ
% Раз строка - это набор символов, давайте немного вспомним что такое сиволы и как с ними работать. Как вам уже известно, символьная переменная это переменная типа char. В отличие от строки это примитивный числовой тип данных, и к нему применимы все операции допустимые для примитивов, такие как присваивание, +, -, *, / - хотя не все имеют смысл (я например с трудом могу представить ситуацию в которой необходимо умножать или делить коды символов).
% Здесь значением sym является 65 в кодовой таблице ASCII. Т.е. запись sym = 65 и sym = 'A' - абсолютно эквивалентны. Однако для улучшения читаемости кода лучше использовать вариант sym = 'A'.
% СЛАЙД ПРО ТО ЧТО СИМВОЛ ЭТО ЦИФРА. ТАБЛИЦА АСКИИ
% Ну что, немного вспомнили, что такое символы, теперь переходим к строкам. Строками мы пользуемся с самого первого занятия: написав в двойных кавычках «Привет Мир», мы использовали строку. Строки иногда называют типом данных, но в языке С строка это массив, массив символов, поэтому и работать с ней можно, как с массивами. Строки в языке С можно описать двумя способами - как указатель и как массив из переменных типа char. Давайте создадим строку в виде массива из char назовем ее string1 и запишем This is a string!" это строка. Также создадим указатель, назовем его string2 и запишем This is also a string! это тоже строка.
% У каждого из способов есть свои особенности. Так, например в массиве из переменных типа char мы можем изменять символы. Давайте выведем наши строки в консоль и убедимся, что я не ошибся и их действительно можно так объявлять. Все работает. Теперь давайте изменим какой-нибудь символ в одной из строк. Например, string1[5] = X; и символ с пятым индексом изменится.
% int main (int argc, const char* argv[]) {
% char string1[256] = "This is a string!";
% char* string2 = "This is also a string!";
% printf("%s \n", string1);
% printf("%s \n", string2);
% string1[5] = 'X';
% printf("%s \n", string1);
% Указатель на тип char нам такой возможности не даёт и, получается, представляет собой immutable string неизменяемую строку. Зато указатели на строки мы можем возвращать из функций. Т.е. мы можем объявить функцию как указатель на char вернуть из нее строку и вывести в консоль.
% И это открывает перед нами широчайшие возможности по работе с текстами.
% char* helloFunction () {
% return "Hello!";
% }
% printf("%s \n",* helloFunction ());
% Параллельно с написанием нашей функции, приветствующей мир, давайте изучим некоторые стандартные возможности языка С для работы со строками. Например, специальную функцию, которая призвана выводить строки в консоль puts(); работает она очень похожим на printf() образом, но может выводить только строки, без каких-то других параметров, и добавляет символ конца строки. А также изучим специальную функцию gets() которая призвана считывать строки из консоли и записывать их в переменные.
% Создадим изменяемую строку типа массив из char, назовём её name, передадим эту строку в функцию gets(), и выведем на экран результат, полученный из консоли. Запустим нашу программу, напишем имя и увидим его в консоли.
% char name[255];
% gets(name);
% puts(name);
% Теперь, мы можем поприветствовать пользователя нашей программы как следует, по имени. В нашей существовавшей функции приветствия внесём небольшие изменения. Создадим строку, в которой будем хранить приветственное слово, и в которую будет дописываться имя пользователя. Применим функцию склеивания строк. Поскольку склеивание - ненаучный термин, будем использовать слово конкатенация. И это слово подсказывает нам название функции, которую мы будем использовать : strcat(). Функция принимает на вход два параметра - строку, к которой нужно что-то прибавить, и строку, которую нужно прибавить. Логично предположить, что первая строка должна быть изменяемой (то есть являться массивом символов). Функция прибавит все символы второй строки в первую и вернёт указатель на изменённую строку. Очень удобно. Запустим наш проект, введем имя и убедимся что все сломалось.
% char* helloFunction(char* name) {
% char welcome[255] = "Hello, ";
% return strcat(welcome, name);
% }
% int main(int argc, comst char* argv[]) {
% char name[256];
% gets(name);
% puts(helloFunction(name));
% return 0;
% }
% Что же случилось? Мы можем возвращать из функции только фиксированные строки, как в предыдущем примере. То есть, получается, нужно писать кейс, в котором содержатся все возможные имена, и оператором switch перебирать все возможные варианты ввода, иначе мы устраиваем утечку памяти, создавая болтающийся в воздухе указатель? Нет, нас это не устраивает. Что же делать? Какой бы мы ни создали указатель в функции - он перестанет существовать, как только мы выйдем из функции. Выход очень простой - раз указатель не идёт в мэйн, надо чтобы мэйн дал нам указатель.
% Добавим в аргументы функции указатель на выходную строку, и напишем что для начала сложить строки и положить в локальный массив strcat(welcome, name). Добавим в основную функцию массив char result [], который будет хранить результат и передадим в функцию helloFunction char name и char result. Запускаем, и не работает. А все дело в том, что мы не используем библиотечные функции, есть функция strcpy, которая не просто перекладывает указатель в определенную переменную, а копирует строку, теперь все работает.
% char* helloFunction (char* name, char* out) {
% char welcome[255] = “Hello, ”;
% strcat(welcome, name);
% strcpy(out, welcome);
% }
% int main(int argc, comst char* argv[]) {
% char name[256];
% char result[256];
% gets(name);
% helloFunction(name, result)
% puts(result);
% return 0;
% }
% Если присмотреться - то все функции работающие со строками, именно так и делают - запрашивают источник данных и конечную точку, куда данные нужно положить. А кто мы такие, чтобы спорить с библиотечными функциями? Обратите внимание на то, что функции strcat() и strcpy() возвращает указатель на получившуюся строку. Мы также возвращаем указатель на строку, и тут встаёт вопрос о вызывающем функцию контексте, нужен ли этот указатель вызывающему. Работа со строками в Си до сих пор является очень и очень актуальной темой на программистских форумах, можете удостовериться.
% Давайте рассмотрим ещё пару функций. Например, сравнение строк, функция strcmp, допустим, пусть именно меня программа приветствует как-то иначе. Функция возвращает отрицательные значения, если первая строка меньше второй, положительные, если первая больше второй, и 0 если строки равны. Это функция, которую удобно применять в условиях. И копирование строк, функция strcpy, также принимающую на вход две переменных - куда копировать, и что копировать.
% Если строки будут действительно равны, мы скопируем в строку с именем слово Master!. Скомпилируем, запустим, введем имя и все работает.
% void helloFunction (char* name, char* out) {
% char welcome[255] = “Hello, ”;
% if (strcmp("Ivan", name) == 0)
% strcpy(name, "Master!");
% strcat(welcome, name);
% strcpy(out, welcome);
% }
% Из всех функций для работы со строками чрезвычайно часто используются atoi(); atof(); переводящие написанные в строке цифры в численные переменные внутри программы. atoi() переводит в int, atof() во float.
% Объявим переменную num, предложим пользователю ввести цифру, естественно в виде строки. Будем принимать ее при помощи функции gets, хотя как мы помним, могли бы и scanf() Заведем переменную int number для хранения результата работы функции atoi. Затем давайте умножим результат сам на себя и выведем окончательное число в консоль. Запустим проект и убедимся, что все работает.
% Полный список функций для работы со строками можно посмотреть в заголовочном файле string.h. Описание и механика их работы легко гуглится, документации по языку очень много.
% char num[64];
% puts("Enter a number: ");
% gets(num);
% int number = atoi(num);
% number *= number;
% printf("We powered your number to %d", number);
% И напоследок еще пару слов об обработке символов. Функции для работы с символами содержатся в заголовочном файле stdlib.h. Естественно, наша программа может получить какие-то значения в виде строк. Не всегда же есть возможность использовать scanf(); - например, считывание из графических полей ввода даёт нашей программе значения в виде строк. В языке С есть функции для работы с каждым символом строки, например:
% isalpha() возвращает истину или ложь если символ является символом из алфавита;
% isdigit() если символ является цифрой;
% isspace() является ли символ пробелом;
% isupper(), islower() находится ли символ в верхнем или нижнем регистре; toupper(), tolower() переводят символ в верхний или нижний регистр.
% Можем использовать одну из них соответственно нашей задаче, допустим, пользователь может вводить своё имя как с заглавной буквы, так и всеми строчными. Уравняем оба варианта для нашей проверки одной строкой name[0] = tolower(name[0]); а после проверки вернём заглавную букву на место name[0] = toupper(name[0]); и удостоверимся что даже если мы напишем своё имя с маленькой буквы - программа напишет его с большой. Запустим
% СЛАЙД С ФУНКЦИЯМИ STDLIB.H
% void helloFunction (char* name, char* out) {
% char welcome[255] = “Hello, ”;
% name[0] = tolower(name[0]);
% if (strcmp("ivan", name) == 0)
% strcpy(name, "Master!");
% name[0] = toupper(name[0]);
% strcat(welcome, name);
% strcpy(out, welcome);
% }
% На этом уроке мы познакомились со строками, способами работы с ними и немного улучшили понимание механизма работы программ. Попробовали в деле некоторые функции из стандартной библиотеки. До встречи на следующем уроке.
% 09 arrays
\import{sections/}{09-arrays}
% 10 strings
\import{sections/}{10-strings}
\section{Структуры}
% Коллеги, здравствуйте. Рад вас приветствовать на 12м уроке Основы Языка С. На этом занятии мы поговорим о структурах данных в языке С.

378
sections/09-arrays.tex Normal file
View File

@ -0,0 +1,378 @@
\section{Массивы}
В этом разделе нас с вами ждут массивы. Много массивов. И ещё пара слов о директивах компилятору, иногда также называемых директивами препроцессора. С них и начнём.
\subsection{Директива \code{\#define}}
Помимо уже хорошо знакомой вам директивы \code{\#include}, частично описанной в разделе \hyperref[text:directive]{\ref{text:directive}}, естественно, существуют и другие. Некоторые из них ограничивают импорт описанных в заголовочном файле функций, некоторые <<\textbf{описывают}>> какие-то константы и даже действия. Вот, директиву \textbf{описать} мы и рассмотрим подробнее. Она не зря называется директивой препроцессора, поскольку даёт указание не процессору во время выполнения программы выделить память, присвоить значения, а непосредственно компилятору: заменить в тексте программы одни слова на другие. Таким образом можно задавать константы проекта, и даже делать сокращённые записи целых действий. Например, написав \code{\#define ARRAY\_LENGTH 50} мы предпишем компилятору, перед запуском трансляции нашего кода заменить все слова \code{ARRAY\_LENGTH} на цифру 50. В такой записи, слово \code{ARRAY\_LENGTH} будет называться \textit{макроконстантой}.
\frm{Обратите внимание, что директива пишется немного не так, как обычный оператор языка, хоть и может находиться в любом месте кода. В конце директивы не ставится точка с запятой. Это важно именно потому что директивы работают с текстом программы, то есть если точка с запятой всё же будет поставлена, текст программы будет всегда содержать вместо макроконстанты число и точку с запятой, что может в корне изменить смысл программы.}
Весьма удобно, но этим можно не ограничиваться, мы можем попросить компилятор заменить вызовы функций и операторы на короткие, удобные нам слова. Важно помнить, что директивы препроцессора работают с текстом программы, поэтому не осуществляют никаких дополнительных проверок. Это сложный и мощный инструмент, который чаще всего используется для решения нетривиальных задач, например, выбор кода, который попадёт в компиляцию в зависимости от операционной системы. Иногда в программах можно встретить описание недостающего, но такого привычного булева типа при помощи директив препроцессора:
\begin{lstlisting}[language=C,style=CCodeStyle]
#define bool int
#define true 1
#define false 0
\end{lstlisting}
Но нам пока что достаточно умения создать глобальную именованную константу. Код ниже демонстрирует, что директивы не обязательно группировать именно в начале файла, а можно использовать там, где это удобно и уместно, так мы можем объявить константу с длиной массива в начале файла, а можем прямо внутри функции \code{int main (int argc, char *argv[])}.
\begin{lstlisting}[language=C,style=CCodeStyle]
int main(int argc, char* argv[]) {
#define ARRAY_LENGTH 50
int a = ARRAY_LENGTH;
printf("a = %d", a);
return 0;
}
\end{lstlisting}
Результатом работы этой функции будет ожидаемое:
\begin{verbatim}
$ ./program
a = 50
\end{verbatim}
\subsection{Массивы}
Вступление про директивы препроцессора напрямую не связано с темой массивов, но директива \code{\#define} для объявления размера массива применяется чрезвычайно часто. Рассмотрим природу этого явления чуть позже.
\frm{Массив это множество данных одного типа, расположенных в памяти подряд.}
Язык С строго типизирован, поэтому невозможно создать массив из разных типов данных. На данном этапе мы рассматриваем только простые типы данных, поэтому и массивы будем рассматривать статические. Статическим массивом называют массив, количество элементов которого заранее известно и не изменяется за время работы программы. Альтернативой статическому массиву является динамический, таких массивов в языке С не существует, но всегда можно самостоятельно описать такую структуру данных, которая будет хранить значения, динамически расширяясь и сужаясь. Также для начала ограничим нашу беседу одномерными массивами, то есть такими, которые можно записать в виде значений через запятую. Статические одномерные массивы принято объявлять двумя способами:
\begin{itemize}
\item простое объявление с указанием размера;
\item объявление, совмещённое с инициализацией
\end{itemize}
Для примера объявим массив, содержащий элементы типа \code{int}, дадим ему идентификатор или имя массива \code{arr} (сокращённо от англ array), укажем максимальное количество элементов которые может вместить в себя массив, например, пять.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[5];
arr[0] = 10;
arr[1] = 20;
arr[2] = 30;
\end{lstlisting}
\end{figure}
Как уже говорилось массив это множество данных или элементов. К каждому элементу массива можно обратиться по его номеру, который принято называть индексом. Индексация элементов начинается с нуля. Давайте заполним наш массив значениями типа \code{int}. Для этого последовательно обратимся к каждому элементу и присвоим значение. Обратите внимание, что язык С не гарантирует что инициализационное значение элементов массива будет равно нулю, если это не указано явно, поэтому выведя на экран содержимое массива, мы можем гарантировать значения только первых трёх элементов, которые мы указали в коде. Второй способ объявления, совмещённый с инициализацией массива используют, если массив сравнительно небольшой и его значения заранее известны, например:
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[6] = {1, 1, 2, 3, 5, 8};
\end{lstlisting}
При этом, если сразу заполняются все элементы, размерность можно не указывать. Итак, мы научились создавать и заполнять значениями массивы. Теперь общее правило объявления массивов в С: при объявлении массива нужно указать его имя, тип элементов, количество элементов, опционально - указать сами эти элементы. Количество элементов есть натуральное число, то есть целое положительное, ноль не может быть количеством элементов. Нельзя задавать переменное количество элементов массива.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int nArr[100]; // An array for 100 int's;
float fArr[5]; // An array for 5 float's;
char cArr[2]; // An array for 2 char's;
int varElem;
int nArr[varElem]; // Compile error! Number of elements must be constant;
\end{lstlisting}
\end{figure}
Так мы обязаны создавать массивы только с точно указанным числом элементов. Для языка С это позволено сделать объявлением константы времени исполнения \code{const int elements; int arr[elements]}, но, например, в С++ такая запись вызовет ошибку компиляции, поэтому там необходимо строго указывать размер числовым литералом или объявив его директивой \code{\#define}, что, фактически, одно и тоже.
\frm{В более поздних стандартах С++ появилось ключевое слово \code{constexpr}, позволяющее объявлять константы времени компиляции и отказаться от объявления размеров массива только литералом}
Теперь давайте научимся получать доступ к элементам массива. Нет ничего проще, тем более, что мы это уже делали объявляли массив и для примера его заполняли. Для доступа к конкретному элементу массива нужно указать имя массива и индекс элемента в квадратных скобках. Квадратные скобки - это тоже оператор языка, он называется оператором индексного доступа:
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int a = arr[0];
printf("lets see whats in 0-th element: %d", a);
\end{lstlisting}
\end{figure}
При помощи массивов решают множество задач, таких как поиск, сортировка, составление таблиц соответствия, создание частотных диаграмм. На основе массивов создают более сложные структуры данных. Для короткого минимального примера, давайте напишем программу, которая будет печатать наш массив в консоль.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
#include <stdio.h>
int main(int argc, char *argv[]) {
int arr[5];
int i;
printf("Your array is: ");
for (i = 0; i < 5; i++) {
printf("%d ", arr[i]);
}
return 0;
}
\end{lstlisting}
\end{figure}
Такая несложная программа даст нам следующий результат (Обратите внимание, что при такой инициализации, а точнее её отсутствии, значения внутри массива не гарантируются. В результате запуска программы на компьютере автора первые четыре индекса оказались равными нулю, а пятый принял странное отрицательное целочисленное значение):
\begin{verbatim}
$ ./program
Your array is 0 0 0 0 -497067408
\end{verbatim}
Мы научились создавать, инициализировать массивы и обращаться к его элементам. Теперь решим задачу посложнее: напишем программу, которая проверит насколько статистически хорош описанный в стандартной библиотеке (языка С) генератор псевдо-случайных чисел (функция \code{rand();}). Для такой статистической проверки нам понадобится сформировать так называемый \textit{частотный массив}, массив, в котором будет содержаться информация о том, сколько раз то или иное число появилось во множестве значений, полученном при помощи генератора псевдослучайных чисел, частота вхождения значений. Сама генерация псевдослучайных чисел происходит при помощий функции \code{rand();} которая создаёт целое число типа \code{int}. Но, поскольку целое число в таком диапазоне нам не нужно, мы его сократим при помощи оператора получения остатка от деления.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#define ARRAY_LENGTH 10
#define NUMBERS_AMOUNT 1000000
int main( int argc, char *argv[]){
srand(time(NULL)); // initialize PRNG
int frequency[ARRAY_LENGTH] = {0};
int a;
int i;
for (i = 0; i < NUMBERS_AMOUNT; i++) {
a = rand() % ARRAY_LENGTH;
frequency[a]++;
}
for (i = 0; i < ARRAY_LENGTH; i++) {
printf("Number %d generated %6d (%5.2f%%) times\n",
i,
frequency[i],
((float)frequency[i] / NUMBERS_AMOUNT * 100));
}
return 0;
}
\end{lstlisting}
\end{figure}
Обратите внимание на 14ю строку: для сгенерированного на 13й строке числа \code{0} увеличим значение в 0-й ячейке массива, для числа \code{1} - в 1-й, и т.д. Данная программа наглядно демонстрирует не только работу с массивами, но и то, что генератор псевдослучайных чисел в языке С генерирует статистически верную последовательность случайных чисел. Инициализация генератора псевдослучайных чисел на восьмой строке \code{srand(time(NULL));} происходит значением текущего времени системы, то есть мы гарантируем, что начальное значение генератора будет отличаться от запуска к запуску, а значит наше исследование будет чуть более достоверным.
\begin{figure}[h!]
\begin{verbatim}
$ ./program
Number 0 generated 99955 (10.00%) times
Number 1 generated 99977 (10.00%) times
Number 2 generated 100156 (10.02%) times
Number 3 generated 99952 (10.00%) times
Number 4 generated 100212 (10.02%) times
Number 5 generated 100713 (10.07%) times
Number 6 generated 99418 ( 9.94%) times
Number 7 generated 99768 ( 9.98%) times
Number 8 generated 99918 ( 9.99%) times
Number 9 generated 99931 ( 9.99%) times
\end{verbatim}
\end{figure}
Запуск программы (даже несколько запусков) показывает, что всех значений получилось около десяти процентов, что говорит о том, что последовательность псевдослучайных чисел статистически верна.
\subsection{Идентификатор массива}
Это будет непросто, но мы поговорим о том, что из себя представляет идентификатор массива, чем чреват выход за пределы массива, затронем тему арифметики указателей и научимся передавать указатели на массивы в функции. Как упоминалось ранее, массив - это ссылочный тип данных. То есть в идентификаторе хранится адрес, ссылка на первый байт первого элемента массива, дальнейший доступ к элементам осуществляется посредством \textit{смещения относительно этого байта}. Таким образом запись вида \code{array[0]} говорит нам о том, что нужно взять адрес массива и сместить указатель на \code{0} элементов того типа, из которых состоит массив. Отсюда становится ясно, почему \textbf{индексирование массивов начинается с нуля}.
Давайте попробуем визуализировать положение вещей в массивах. На рисунке со стр. \hyperref[pic:arrays]{\pageref{pic:arrays}} можно увидеть создание массива на первой строке, далее доступ к двум его ячейкам для записи в них значений.
\begin{figure}[h!]
\begin{multicols}{2}
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[ARRAY_LENGTH];
arr[0] = 20;
arr[1] = 50;
\end{lstlisting}
\columnbreak
\begin{tikzpicture}
\draw[step=0.5cm,gray,very thin] (0,-0.5) grid (5, 2);
\draw(0, -0.5) -- (5, -0.5) -- (5, 0) -- (0, 0) -- cycle;
\draw(0, 0.5) -- (5, 0.5) -- (5, 1) -- (0, 1) -- cycle;
\draw(0, 1.5) -- (5, 1.5) -- (5, 2) -- (0, 2) -- cycle;
\fill[blue!40!white] (0, 0.5) rectangle (0.5, 1);
\fill[blue!40!white] (0.5, -0.5) rectangle (1, 0);
\end{tikzpicture}
\end{multicols}
\caption{Визуализация создания и индексации массива}
\label{pic:arrays}
\end{figure}
Соответственно, когда мы берем элемент с нулевым смещением, то есть по нулевому индексу, это будет ячейка памяти, находящаяся ровно по адресу массива. Все остальные ячейки мы просто игнорируем. Далее, берем следующий элемент массива. Для этого возьмем смещение в единицу, допустим, это будет число 50. Соответственно смещение будет на одну ячейку типа \code{int}. То есть мы игнорируем нулевой элемент и берем первый. Становится очевидно, что если мы в своём коде напишем такой индекс, который находится за пределами описанного массива - мы просто получим какое-то значение, которое никак не можем прогнозировать.
Относительно выхода за пределы массива надо сказать, что ни компилятор, ни тем более операционная система никаких проверок не делают, поэтому такие проверки (чаще всего самопроверки на этапе написания кода) полностью ложатся на плечи программиста. Язык С не предоставляет никаких сред виртуализации, никаких подсистем исключений, только выдача случайных данных, которые могут попасться нашей программе. Или запись в ячейки, которые совершенно не гарантированно останутся пустыми.
\frm{В связи с тем, индекс массива - это значение смещения, относительно его начала, и, как следствие, индексы массива всегда отсчитываются с нуля, важно помнить, что при создании массива из, например, десяти элементов десятый индекс будет находиться за пределами массива.}
Надо сказать, что всё-таки б\'{o}льшая часть значений за пределами массива будет равна нулю, но всё равно лишний раз экспериментировать не стоит.
\begin{figure}[h!]
\begin{multicols}{2}
\begin{lstlisting}[language=C,style=CCodeStyle]
arr[11] = 60;
\end{lstlisting}
\columnbreak
\begin{tikzpicture}
\draw[step=0.5cm,gray,very thin] (0,-0.5) grid (6, 0);
\draw(0, -0.5) -- (5, -0.5) -- (5, 0) -- (0, 0) -- cycle;
\fill[red!40!white] (5.5, -0.5) rectangle (6, 0);
\end{tikzpicture}
\end{multicols}
\caption{Выход за пределы массива}
\label{pic:outofbounds}
\end{figure}
Как мы уже знаем, в идентификаторе массива хранится ссылка на первый байт первого элемента массива, т.е. идентификатор является, по сути, указателем. Но существует несколько отличий: указатель - это переменная, к ней применимы, например, операции инкремента и декремента, чего конечно нельзя делать с идентификатором массива.
\frm{Идентификатор массива \textbf{не является} lvalue, Но один элемент массива является lvalue. Так, записи вида \code{arr++;} или \code{arr = 5} будут являться ошибочными, поскольку в них происходит обращение не к конкретному элементу с целью его изменения, а к массиву целиком.}
Обратившись к идентификатору массива мы можем получить доступ к элементам массива не только при помощи записи индекса в квадратных скобках, но и при помощи так называемой арифметики указателей. Мы знаем, что массив - это единая область памяти, и значения в нём располагаются подряд по очереди, значит, отсчитав от указателя на первый индекс нужное количество байт - мы получим указатель на второй индекс. Давайте для примера подсчитаем среднее арифметическое всех чисел в массиве, с использованием арифметики указателей.
Будем запрашивать значения для расчётов у пользователя. Создадим вспомогательную переменную \code{float result;}, для хранения результата и в цикле будем запрашивать у пользователя числа. Количество введенных цифр должно соответствовать количеству элементов массива, поэтому условием выхода из цикла будет равенство счётчика и последнего индекса массива, то есть, длины массива минус единица.
\frm{Обратите внимание, что в коде мы указали условием выхода из цикла \textbf{строгое неравенство}, то есть со значением \code{i} равным длине массива в тело цикла мы не попадём.}
Помним, что индексация массива начинается с нуля, поэтому длина массива всегда на единицу больше последнего индекса. Выведем в консоль надпись <<введите значение>>, при помощи функции \code{scanf();} считаем его и сразу привычным образом, оператором индексного доступа, положим в массив.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[ARRAY_LENGTH];
int i = 0;
float result = 0;
while (i < ARRAY_LENGTH) {
printf("Enter value %d: ", i);
scanf("%d", arr[i]);
i++;
}
\end{lstlisting}
\end{figure}
Выведем в консоль получившийся массив при помощи цикла \code{for(;;)} и привычной нам функции \code{printf();}. Следом напишем ещё один цикл в котором подсчитаем среднее арифметическое. Для этого к результату будем прибавлять существующий результат и значение массива на которое указывает уже не такая привычная, как квадратные скобки конструкция \code{*(arr + i)}, которую сразу и разберём.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
printf("Your array is: ");
for (i = 0; i < ARRAY_LENGTH; i++)
printf("%d ", arr[i]);
printf("\nAnd the average is: ");
for (i = 0; i < ARRAY_LENGTH; i++)
result += *(arr + i);
printf("%f\n", result / ARRAY_LENGTH);
\end{lstlisting}
\end{figure}
Как вы видите, некоторые подсчеты программа выполняет за нас - мы прибавляем к указателю единицу, двойку, тройку и т.д, а программа понимает, что надо взять не следующий по счёту байт, а следующий указатель. Так как в данном примере мы используем массив в котором хранятся значения типа \code{int}, а как вы помните \code{int} в подавляющем большинстве случаев - это четыре байта, то при увеличении указателя на единицу, мы обратимся к области памяти находящейся на четыре байта дальше идентификатора, при увеличении на двойку на восемь байт и так далее. Подсчитать среднее арифметическое не составит труда, этот алгоритм нам знаком со средней школы. Далее при помощи функции \code{printf();} выведем в консоль среднее арифметическое. Запустим, повводим цифры и убедимся что все работает.
\begin{verbatim}
$ ./program
Enter value 0: 1
Enter value 1: 2
Enter value 2: 3
Enter value 3: 4
Enter value 4: 5
Enter value 5: 6
Enter value 6: 7
Enter value 7: 8
Enter value 8: 9
Enter value 9: 10
Your array is: 1 2 3 4 5 6 7 8 9 10
And the average is: 5.500000
\end{verbatim}
Внимательный читатель мог заметить, что мы применяем операцию \textit{разыменования}. Что происходит, когда мы таким образом обращаемся к массиву? Операция разыменования получает доступ к значению, находящемуся по адресу. Адресс массива - это адрес его первого элемента, поэтому конструкция \code{*arr} вернёт значение нулевого элемента массива. А прибавление значений к этому указателю будет смещать его также, как это делает оператор квадратных скобок.
Как уже упоминалось, идентификатор массива - это не обычный указатель. Обычный указатель хранит в себе адрес какой-то другой переменной, и сам где-то хранится. Указатель на начало массива хранит в себе адрес массива, то есть адрес его нулевого элемента, и сам этот указатель находится в этом самом месте. На первый взгляд сложновато? Но пусть Вас это не сбивает с толку, на деле всё не так жутко. На деле это означает, что при передаче массива (читай идентификатора массива) в функцию в качестве аргумента, мы не должны использовать оператор взятия адреса, поскольку идентификатор массива сам по себе является указателем на собственное начало. Это открывает для нас широкие возможности по написанию функций, работающих с массивами данных. В только что написанной нами программе оформим вывод массива на экран и поиск среднего арифметического в виде функции. Опишем функции \code{printArray()} и \code{average()} в которые передадим указатель на массив и его длину, т.к. в массиве не содержится сведений о его размере.
Поскольку мы передаём в функцию указатель, то все действия которые описаны в этой функции будут происходить с массивом который мы создали в основной части программы через этот указатель, который мы передали, никакого копирования значений или чего то подобного. Для корректной работы наших функций объявим в них счётчик и изменим названия переменных на названия параметров.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
void printArray(int* array, int length) {
int i;
for (i = 0; i < length; i++)
printf("%d ", array[i]);
}
float average(int* array, int length) {
float result = 0;
int i;
for (i = 0; i < length; i++)
result += *(array + i);
return result / length;
}
\end{lstlisting}
\end{figure}
Так, полный листинг этого примера на стр. \hyperref[code:arrayaverage]{\pageref{code:arrayaverage}}.
\begin{figure}[h!]
\lstinputlisting[language=C,style=CCodeStyle]{../sources/arrayaverage.c}
\label{code:arrayaverage}
\end{figure}
\newpage
\subsection{Многомерные массивы}
Массив в языке С может иметь сколько угодно измерений. Все массивы, с которыми мы имели дело до этого момента - одномерные, их легко визуализировать в виде простого перечисления элементов, возможно, как строки или как таблицы, состоящей из одной строки. Самые распространённые многомерные массивы - это двумерные и трёхмерные, которые легко себе представить в виде таблицы или куба соответственно. Итак, массив это структура, содержащая элементы. Двумерный массив - это массив из массивов, содержащих элементы. Трёхмерный - это массив из массивов, содержащих массивы, которые содержат элементы. И так далее. В массиве могут находиться любые типы данных, мы, для удобства, будем рассматривать работу массивов с числами.
Попробуем визуализировать двумерный массив. Создадим двумерный массив в коде, например, 5х5 элементов. Массив 5х5 это 5 столбцов и 5 строчек. Соответственно, \textit{каждая строчка это будет у нас младший индекс, а каждый столбец старший индекс}. Трехмерный массив может быть, например, 3х3х3 его можно визулизировать как всем известный кубик Рубика то есть, это три стоящих друг за другом таблицы 3х3. Также опишем его в коде ниже. Получается, что мы к таблице (ширине и высоте) добавили третье \textbf{измерение}, поэтому и массив получается \textbf{многомерным}, в данном случае, \textbf{трёхмерным}. Массивы б\'{о}льших размерностей тоже можно встретить в программах, но значительно реже, только лишь потому, что их действительно немного сложнее представить себе.
\begin{figure}[h!]
\begin{multicols}{2}
\begin{lstlisting}[language=C,style=CCodeStyle]
int twoDimensional[5][5];
\end{lstlisting}
\begin{center}
\begin{tabular}{||c|c|c|c|c||}
\hline
\hline
0,0 & 0,1 & 0,2 & 0,3 & 0,4 \\
\hline
1,0 & 1,1 & 1,2 & 1,3 & 1,4 \\
\hline
2,0 & 2,1 & 2,2 & 2,3 & 2,4 \\
\hline
3,0 & 3,1 & 3,2 & 3,3 & 3,4 \\
\hline
4,0 & 4,1 & 4,2 & 4,3 & 4,4 \\
\hline
\hline
\end{tabular}
\end{center}
\columnbreak
\begin{lstlisting}[language=C,style=CCodeStyle]
int threeDimensional[3][3][3];
\end{lstlisting}
\begin{tikzpicture}[every node/.style={minimum size=1cm},on grid]
\begin{scope}[every node/.append style={yslant=-0.5},yslant=-0.5]
\node at (0.5,2.5) {2,0,0};
\node at (1.5,2.5) {1,0,0};
\node at (2.5,2.5) {0,0,0};
\node at (0.5,1.5) {2,1,0};
\node at (1.5,1.5) {1,1,0};
\node at (2.5,1.5) {0,1,0};
\node at (0.5,0.5) {2,2,0};
\node at (1.5,0.5) {1,2,0};
\node at (2.5,0.5) {0,2,0};
\draw (0,0) grid (3,3);
\end{scope}
\begin{scope}[every node/.append style={yslant=0.5},yslant=0.5]
\node at (3.5,-0.5) {0,0,0};
\node at (4.5,-0.5) {0,0,1};
\node at (5.5,-0.5) {0,0,2};
\node at (3.5,-1.5) {0,1,0};
\node at (4.5,-1.5) {0,1,1};
\node at (5.5,-1.5) {0,1,2};
\node at (3.5,-2.5) {0,2,0};
\node at (4.5,-2.5) {0,2,1};
\node at (5.5,-2.5) {0,2,2};
\draw (3,-3) grid (6,0);
\end{scope}
\begin{scope}[every node/.append style={yslant=0.5,xslant=-1},yslant=0.5,xslant=-1]
\node at (3.5,2.5) {2,0,0};
\node at (3.5,1.5) {1,0,0};
\node at (3.5,0.5) {0,0,0};
\node at (4.5,2.5) {2,0,1};
\node at (4.5,1.5) {1,0,1};
\node at (4.5,0.5) {0,0,1};
\node at (5.5,2.5) {2,1,2};
\node at (5.5,1.5) {1,1,2};
\node at (5.5,0.5) {0,1,2};
\draw (3,0) grid (6,3);
\end{scope}
\end{tikzpicture}
\end{multicols}
\end{figure}
Как работать с многомерными массивами мы рассмотрим на примере двумерного массива. Поставим для себя задачу: сформировать таблицу Пифагора (раньше такие на тетрадях в клетку печатали на обратной стороне). Таблица подразумевает наличие двух измерений: строк и колонок. Для этого объявим константы rows и cols и присвоим им значения 10, \code{rows} это количество строк, а \code{cols}, соответственно, столбцов. Создадим двумерный массив, \code{table[rows][cols]}. Таким образом, мы создали массив размером rows, в каждом элементе которого содержится ссылка на массив размером cols, т.е. массив массивов содержащих непосредственные значения.
\frm{Важно помнить, что описанное строение массива является понятийным, и физически в памяти элементы хранятся иначе, но на основе этого понятия можно изучить работу оператора разыменования указателей в чати работы с многомерными массивами, так для получения доступа к значению с индексом \code{cube[1][1][1]} нужно трижды разыменовать \code{cube}:\newline \code{*( *( *(cube + 1) + 1) + 1)}}
Таблица Пифагора представляет собой таблицу, где строки и столбцы озаглавлены множителями, а в ячейках таблицы находится их произведение. Вот это самое произведение мы и будем сначала считать и записывать в массив, а затем выводить на экран. Заполнение многомерных массивов значениями ничем не отличается от заполнения одномерных массивов. Заполним нашу матрицу поэлементно: напишем двойной цикл который будет заполнять таблицу. Объявим переменные-итераторы. И с помощью внешнего цикла \code{for(;;)} пройдемся по всем строкам массива, а с помощью вложенного - по всем столбцам каждой строки массива, при этом будем записывать в каждый элемент (ячейку таблицы) результат умножения. Формула \code{(r + 1) * (c + 1)} позволяет исключить нули из нашей таблицы умножения.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
const int rows = 10, cols = 10;
int table[rows][cols];
int r, c;
for (r = 0; r < rows; r++) {
for (c = 0; c < cols; c++)
table[r][c] = (r + 1) * (c + 1);
}
\end{lstlisting}
\end{figure}
И ещё раз: внешний цикл при каждой итерации перемещает нас на одну строчку вниз. Вложенный, при каждой итерации, перемещает нас на одно значение вправо. Важно понять, что на одну итерацию внешнего цикла приходится \code{cols} итераций вложенного. Т.е. с помощью такой конструкции мы всегда поочередно переберём все элементы массива.
Сразу отметим, что помимо такого, поэлементного способа, также есть способ заполнять многомерные массивы посредством конструкции в фигурных скобках, этот подход часто используется когда мы заранее знаем все значения и их не очень много. Например, вот так объявляем и инициализируем двумерный массив размером 3х4:
\begin{lstlisting}[language=C,style=CCodeStyle]
int arr[3][4] = {
{0, 1, 2, 3},
{4, 5, 6, 7},
{8, 9, 10, 11}
};
\end{lstlisting}
Как и в случае такой инициализации одномерного массива, указание размерности в квадратных скобках не обязательно. Обратите внимание, что при такой инициализации удобно писать каждую строку массива отдельной строкой в коде, соответвтующим образом расставляя фигурные скобки и запятые, такое оформление кода значительно улучшает его читаемость. Вернёмся к таблице Пифагора и напишем такой же (как и для заполнения) двойной цикл, который будет выводить на экран наши двумерные массивы в удобном для нас виде.
\begin{lstlisting}[language=C,style=CCodeStyle]
for (r = 0; r < rows; r++) {
for (c = 0; c < cols; c++)
printf("%3d ", table[r][c]);
printf("\n");
}
\end{lstlisting}
Запустив нашу программу с формированием таблицы умножения увидим, что все отлично работает. Полный листинг программы приводить не целесообразно, поскольку это цикл с заполнением и цикл с выводом, полностью привед§нные выше. К тому же, такой код носит исключительно академический характер, и в случае действительной необходимости формирования таблицы Пифагора на экране промежуточное заполнение массива будет излишним, результат умножения целесообразнее сразу выводить на экран.
Как уже говорилось, все массивы могут содержать данные любых типов, в том числе и указатели. Именно это позволяет массиву хранить другие массивы, строки и прочие ссылочные типы данных. Используя массивы указателей, мы можем создать, например, массив строк.
\begin{lstlisting}[language=C,style=CCodeStyle]
char* stringArray[3] = {"Hello", "C", "World"};
int r;
for (r = 0; r < 3; r++)
printf("%s ", stringArray[r]);
\end{lstlisting}
Это указатели на строки, а точнее, на строковые литералы. Такой тип данных (строковый литерал) является указателем. И мы можем создать из этих указателей массив. Используя массивы указателей, мы можем создать, например, двумерный массив, где каждый элемент не обязан быть того же размера, что и остальные (но обязан быть того же типа, как мы помним). Но строки и сложно составленные указатели - это темы, которые очень сильно выходят за рамки Основ языка, хотя, конечно, это не помешает нам немного подробнее разобраться со строками в следующем разделе.

251
sections/10-strings.tex Normal file
View File

@ -0,0 +1,251 @@
\section{Строки}
Получив в предыдущих разделах представление об указателях и массивах, и вскользь несколько раз упомянув строки, пришла пора изучить их подробнее.
Итак, что же такое \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.
Немного вспомнив, что такое символы, переходим к строкам. Строками мы пользуемся с самых первых страниц этого документа: написав в двойных кавычках <<Привет, Мир>>, мы использовали строку, а если точнее, строковый литерал. Строки иногда называют типом данных, но в языке С строка это указатель на последовательно записанный набор символов, поэтому работать с ним можно, как с массивами. Строки в языке С можно описать двумя способами: как указатель и как массив из переменных типа 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}
Но довольно об ограничениях. Указатели на строки не такие уж бесполезные, мы можем, например, возвращать их из функций. То есть, мы можем объявить тип возвращаемого из функции значения как указатель \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{figure}[h!]
\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}
\end{figure}
Из всех функций для работы со строками чрезвычайно часто используются \code{atoi();} и \code{atof();} переводящие написанные в строке цифры в численные переменные внутри программы. \code{atoi();} переводит в \code{int}, а \code{atof();} во \code{float}, соответственно. Для примера объявим переменную \code{num}, предложим пользователю ввести цифру, естественно в виде строки. Будем принимать ее при помощи небезопасной функции \code{gets();}, хотя как мы помним, могли бы и \code{scanf();} который сразу бы преобразовал строку согласно использованного заполнителя. Заведем переменную \code{int number} для хранения результата работы функции преобразования. Затем, давайте умножим результат сам на себя, чтобы убедиться, что это и правда число, причём именно то, которое мы ввели, и выведем окончательное число в консоль.
\begin{figure}[h!]
\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}
\end{figure}
Полный список функций для работы со строками можно посмотреть в заголовочном файле \code{string.h}. Описание и механика их работы легко гуглится, документации по языку очень много.
В завершение беседы о строках и манипуляциях с ними, скажем ещё пару слов об обработке символов. Функции для работы с символами содержатся в заголовочном файле \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{figure}[h!]
\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}
\end{figure}