arrays, minor fixes

This commit is contained in:
Ivan I. Ovchinnikov 2021-08-25 16:52:36 +03:00
parent d2bb51931c
commit 5109faf4ed
5 changed files with 124 additions and 92 deletions

Binary file not shown.

147
main.tex
View File

@ -23,97 +23,74 @@
\import{sections/}{06-cycles}
% 07 functions
\import{sections/}{07-functions}
\section{Указатели}
Вот и пришла пора поговорить о серьёзном низкоуровневом программировании. О том, от чего стараются оградить программистов языки высокого уровня и современные фреймворки. Об указателях, что такое указатели и как они соотносятся с остальными переменными, что такое передача аргумента по значению и по указателю.. Этого разговора боятся все начинающие программисты и не без причин: работа с указателями на память может не только навредить программе, но и, например, оказать влияние на операционную систему (автор знает, что этот тезис не всегда справедлив, также, как тезис со стр. \hyperref[text:simplify]{\pageref{text:simplify}}, но мы снова идём на такое упрощение ради того, чтобы было понятно, насколько это мощный инструмент). Также сразу отметим, что указателям достался свой собственный раздел в этом документе, хотя формально это просто ещё один тип данных.
Как мы, наверняка, помним, все переменные и константы, используемые в программе, хранятся в оперативной памяти. Оперативная память разделена на несколько участков, но не это для нас сейчас важно. Важно то, что у каждой переменной и константы в памяти есть свой собственный адрес. Адреса принято показывать на экране в виде шестнадцатиричных чисел. Этот адрес выдаётся нашей программе операционной системой, а язык С позволяет использовать его на усмотрение программиста. Иными словами в языке С есть возможность получить доступ к переменной не только по имени, но и по адресу. Получение доступа к значению переменной по адресу называется \textbf{разыменованием}. Давайте выведем в консоль всю имеющуюся информацию о переменной \code{а}. Мы знаем, что это целочисленная переменная значением 50, которая хранится по какому-то адресу.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int a = 50;
printf("value of 'a' is %d \n", a);
printf("address of 'a' is %p \n", &a);
\end{lstlisting}
\end{figure}
Адрес переменной может храниться в специальной переменной, которая называется указатель. Для объявления указателя пишут тип переменной, адрес которой будет храниться в указателе, знак звёздочки и имя указателя. Такому указателю можно присвоить значение адреса существующей переменной, также как мы делали это раньше с другими типами данных. Для наглядности снова выведем всю имеющуюся у нас на данный момент информацию на экран. Напомню, для вывода адреса используется заполнитель \code{\%p}. Выведем в консоль десятичное значение переменной \code{pointer} и адрес переменной \code{pointer}. Увидим, что значение переменной \code{pointer} является как будто бы совершенно случайным числом, но ниже мы представим это значение не в виде обычного целого числа в десятичной системе счисления, а в виде адреса (заполнитель \code{\%p}) то всё встанет на свои места.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int * pointer;
pointer = &a;
printf("value of 'pointer' is %d \n", pointer);
printf("address of 'pointer' is %p \n", &pointer);
printf("value of 'pointer' is %p \n", pointer);
\end{lstlisting}
\end{figure}
Так, объединённый вывод двух предыдущих листингов будет примерно такой, и можно явно увидеть, что адрес \code{а} - это значение переменной \code{pointer}:
\begin{verbatim}
value of 'a' is 50
address of 'a' is 000000000061FE1C
value of 'pointer' is 6422044
address of 'pointer' is 000000000061FE10
value of 'pointer' is 000000000061FE1C
\end{verbatim}
В общем-то, пока что ничего необычного, все эти операции мы выполняли на предыдущих уроках. Но поскольку \code{pointer} это немного необычная переменная, а указатель, то мы можем получить не только её значение, но и \textit{значение переменной, на которую она указывает}, именно этот процесс называется разыменованием указателя. Давайте запишем, вывести в консоль <<переменная pointer указывает на такое-то значение>> и разыменуем \code{pointer}. То есть получим доступ к значению переменной, на которую ссылается указатель \code{pointer}.
\begin{lstlisting}[language=C,style=CCodeStyle]
printf("variable 'pointer' points at: %d", *pointer);
\end{lstlisting}
Таким образом, получается, что в указателе хранится ссылка на значение некоторой переменной, и мы можем это значение изменить. Давайте изменим значение переменной \code{а}, не на прямую, а с использованием указателя. Как видим, значение переменной изменилось.
\begin{lstlisting}[language=C,style=CCodeStyle]
*pointer = 70;
printf("value of a is %d \n", a);
\end{lstlisting}
То есть указатель - это простейший \textbf{ссылочный тип данных}. Без указателей невозможно себе представить создание классов, и всеми любимого объектно-ориентированного программирования, даже массивов или строк. Теперь, когда мы знаем об указателях, и умеем получать значения переменных, на которые они указывают, а также изменять их, перед нами открываются невообразимые ранее перспективы. Мы можем писать функции не создавая в них копии переменных, а передавать в них указатели на уже существующие переменные, тем самым экономя память, и ускоряя выполнение программы. Например, не составит труда написать \textit{программу}, которая бы меняла местами значения двух переменных. Но написать \textit{функцию}, которая бы проделывала тоже самое невозможно без применения указателей. Почему? Очень просто - в параметре функции создаются свои собственные переменные, значения которых задаются копированием аргументов вызова, и меняются местами именно эти, скопированные значения в локальных переменных. И даже если мы вернём одно из этих значений как быть со вторым? А получить доступ к значению второй переменной мы не можем, поскольку, помним, она находится в области видимости функции и недоступна извне. Такая передача аргументов называется \textit{передачей по значению} (мы берём значение некоторой переменной и копируем внутрь функции, иногда такую передачу значений ещё называют \textit{передачей копированием}). Т.е. мы берем значения некоторых переменных в функции \code{int main (int argc, char *argv[])} и передаем их в функцию, где создаём новые переменные с этими, переданными, значениями.
Как решить эту проблему? Передавать не значения переменных, а их адрес, тем самым сообщив функции, что нужно не создавать новые копии переменных, а сделать что-то с уже существующими, и, естественно указать адрес, с какими именно. Передача в качестве аргумента адреса, и создание в теле функции нового указателя называется \textit{передачей по указателю}.
\frm{Для языка С также справедливо выражение <<передача по ссылке>>, поскольку в языке С нет отдельной операции передачи по ссылке. Так, например, в языке С++ передача по ссылке и передача по указателю - это разные операции.}
То есть функция будет ссылаться на переменные, на которые мы укажем и оперировать их значениями. Давайте немного модифицируем нашу программу обмена значениями внутри двух переменных (\hyperref[code:programswap]{\ref{code:programswap}}): опишем её в виде функции, принимающей в качестве параметров два указателя на целые числа типа \code{char}, и передадим адреса созданных в \code{int main (int argc, char *argv[])} переменных. Внутри функции, при её вызове, у нас будут создаваться не переменные, а указатели на переменные, то есть мы будем ссылаться на те самые переменные, созданные вне функции, и будем менять именно их (тех переменных) значения. Таким образом, нам не нужно ничего возвращать, потому что в функции ничего не создавалось, и типом возвращаемого значения функции должен быть \code{void}.
\begin{multicols}{2}
\lstinputlisting[language=C,style=CCodeStyle]{../sources/swapfunc.c}
\columnbreak
\lstinputlisting[language=C,style=CCodeStyle]{../sources/swapprog.c}
\end{multicols}
Применение такого подхода открывает перед нами широкие возможности, некоторые из них мы рассмотрим в следующих разделах.
% 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. Весьма удобно, но этим можно не ограничиваться, мы можем попросить компилятор заменить вызовы функций и операторы на короткие, удобные нам слова. Важно помнить, что директивы препроцессора работают с текстом программы, поэтому не осуществляют никаких дополнительных проверок. Это сложный и мощный инструмент, который чаще всего используется для решения нетривиальных задач, например, выбор кода, который попадёт в компиляцию в зависимости от операционной системы. Иногда в программах можно встретить описание недостающего но такого привычного булева типа при помощи директив препроцессора:
\begin{lstlisting}[language=C,style=CCodeStyle]
#define bool int
#define true 1
#define false 0
\end{lstlisting}
Но нам пока что достаточно умения создать глобальную именованную константу. Код ниже демонстрирует, что директивы не обязательно группировать именно в начале файла, а можно использовать там, где это удобно и уместно, так мы можем объявить константу с длиной массива в начале файла, а можем прямо внутри функции \code{int main (int argc, char *argv[])}.
% Помимо уже хорошо знакомой вам директивы #include естественно, существуют и другие. Некоторые из них ограничивают импорт описанных в заголовочном файле функций, некоторые описывают какие то константы и даже действия.
% СЛАЙД С ОПРЕДЕЛЕНИЕМ ДИРЕКТИВЫ ПРЕПРОЦЕССОРА, ОБЩИЙ ВИД, ПРИМЕРЫ
% Вот, директиву ОПИСАТЬ мы и рассмотрим подробнее. Она не зря называется директивой препроцессора, поскольку даёт указание не процессору во время выполнения программы выделить память, присвоить значения, а непосредственно компилятору - заменить в тексте программы одни слова на другие. Например, таким образом можно задавать константы проекта, и даже целые действия. Например, напишем #define ARRAY_LENGTH 50 и это будет означать, что компилятор, перед тем как запустить нашу программу заменит все слова ARRAY_LENGTH на цифру 50. Весьма удобно, но этим можно не ограничиваться, мы можем попросить компилятор заменить целые вызовы функций и операторы на короткие, удобные нам слова. Иногда в программах можно встретить описание недостающего но такого привычного булевого типа при помощи директив препроцессора #define boolean int #define true 1 #define false 0. Но нам с вами пока что достаточно умения создать глобальную именованную константу.
% #define ARRAY_LENGTH 50
% int a = ARRAY_LENGTH;
\begin{figure}[h!]
\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}
\end{figure}
\subsection{Массивы}
Вступление про директивы препроцессора напрямую не связано с темой массивов, но директива \code{\#define} для объявления размера массива применяется чрезвычайно часто. Рассмотрим природу этого явления чуть позже.
\frm{Массив это множество данных одного типа, расположенных в памяти подряд.}
Язык С строго типизирован, поэтому невозможно создать массив из разных типов данных. На данном этапе мы рассматриваем только простые типы данных, поэтому и массивы будем рассматривать статические. Статическим массивом называют массив, количество элементов которого заранее известно и не изменяется за время работы программы. Альтернативой статическому массиву является динамический, таких массивов в языке С не существует, но всегда можно самостоятельно описать такую структуру данных, которая будет хранить значения, динамически расширяясь и сужаясь. Также для начала ограничим нашу беседу одномерными массивами, то есть такими, которые можно записать в виде значений через запятую. Статические одномерные массивы принято объявлять двумя способами:
\begin{itemize}
\item простое объявление с указанием размера;
\item объявление, совмещённое с инициализацией
\end{itemize}
Для примера объявим массив, содержащий элементы типа \code{int}, дадим ему идентификатор или имя массива \code{arr} (сокращённо от англ array), укажем максимальное количество элементов которые может вместить в себя массив, например, пять.
% #define boolean int
% #define true 1
% #define false 0
% Перейдем к обещанным массивам. Массив это множество данных одного типа. Язык С строго типизирован, поэтому невозможно создать массив из разных типов данных. На данном этапе мы рассматриваем только простые типы данных, поэтому и массивы будем рассматривать статические. Статическим массивом называют массив, количество элементов которого заранее известно и не изменяется за время работы программы.
% СЛАЙД ПРО МАССИВЫ
% Статические массивы принято объявлять двумя способами: здесь мы объявляем массив, содержащий элементы типа int, идентификатор или имя массива arr, максимальное количество элементов которые может вместить в себя массив 3.
% Как уже говорилось массив это множество данных или элементов. К каждому элементу массива можно обратиться по его номеру, который принято называть индексом. Индексация элементов начинается с нуля.
% int arr[3];
% Давайте заполним наш массив значениями типа int. Для этого последовательно обратимся к каждому элементу и присвоим значение:
% arr[0] = 10;
% arr[1] = 20;
% arr[2] = 30;
% Обратите внимание, что язык С не гарантирует что инициализационное значение элементов массива будет равно нулю, если это не указано явно.
\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}
При этом, если сразу заполняются все элементы, размерность можно не указывать. Итак, мы научились создавать и заполнять значениями массивы. Теперь общее правило объявления массивов в С: при объявлении массива нужно указать его имя, тип элементов, количество элементов, опционально - указать сами эти элементы. Количество элементов есть натуральное число, то есть целое положительное, ноль не может быть количеством элементов. Нельзя задавать переменное количество элементов массива.
% Второй способ объявления и инициализации массива используют, если массив сравнительно небольшой и его значения заранее известны, например:
% int arr[6] = {1, 1, 2, 3, 5, 8};
% Итак, мы научились создавать и заполнять значениями массивы. Теперь общее правило объявления массивов в Си: при объявлении массива нужно указать его имя, тип элементов, количество элементов. Количество элементов есть натуральное число, т.е. целое положительное. Ноль не может быть количеством элементов. Нельзя задавать переменное количество элементов массива.
% int nArr[100]; // Объявлен массив, предназначенный для хранения ста целых чисел;
% float fArr[5]; // Объявлен массив, предназначенный для хранения 5-ти чисел типа float;
% char cArr[2]; // Объявлен массив, предназначенный для хранения двух символов;
% int varElem;
% int nArr[varElem]; // Ошибка! Количество элементов нельзя задавать переменной;
% Теперь давайте научимся получать доступ к элементам массива. Нет ничего проще, тем более, что мы это уже делали когда заполняли массив. для доступа к конкретному элементу массива нужно указать имя массива и индекс элемента:
% int a = arr[0] получить значение 0-го элемента массива arr и присвоить его переменной а.
% printf(“lets see whats in 0-th element: %d”, a)
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int nArr[100]; // An array for 100 int's;
float fArr[5]; // An array for 1 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}
% При помощи массивов решают множество задач, таких как поиск, сортировка, составление таблиц соответствия, создание частотных диаграмм. На основе массивов создают более сложные структуры данных. Для примера давайте напишем программу, которая будет печатать наш массив в консоль.
% #include <stdio.h>

View File

@ -38,6 +38,7 @@ non-windows $ ./program
\paragraph{Комментарии} Некоторые среды разработки оставляют в шапке файла комментарии об авторе и дате создания файла. Некоторые команды разработки регламентируют такие комментарии и рекомендуют их написание каждым участником. Комментарий это любой текст, написанный для удобства программиста и игнорируемый компилятором. Комментарии бывают как однострочные, так и многострочные. В редких случаях можно встретить внутристрочные комментарии, но их лучше стараться не использовать, они считаются дурным тоном, поскольку резко снижают читаемость кода.
\frm{\textbf{Комментарий} - это фрагмент текста программы, который будет проигнорирован компилятором языка.}
Очень старые компиляторы допускали только комментарии в стиле \code{/* xxx */}, сейчас допустим также стиль \code{// xxx}, завершается такой комментарий концом строки (то есть вся оставшаяся строка, после символов \code{//} будет проигнорирована компилятором). Комментарии в коде важны, особенно для описания и пояснения неочевидных моментов, но важно соблюсти баланс и не превратить программу в один сплошной комментарий, иногда прерывающийся на работающий код.
\label{text:directive}
\paragraph{Директивы препроцессора} это такие команды, которые будут выполняться не просто до запуска программы, но даже до компиляции.
\frm{Есть мнение, что С/С++ программисты - это не программисты на языке С/С++, а программисты на языке препроцессора используемых ими компиляторов.}
В директивах препроцессора подключаются внешние заголовочные файлы, и определяются некоторые абсолютные значения проекта. Обратите внимание, что директивы препроцессора это достаточно сложный инструмент, и использовать его, например, только для определения константных значений - не лучшее архитектурное решение. Для нашего проекта нам понадобится директива \code{\#include <stdio.h>} - эта директива подключит библиотеку стандартного ввода вывода в наш проект, что позволит нам "общаться" с пользователем нашей программы, используя терминал операционной системы (командную строку в терминах Windows)

View File

@ -1,8 +1,8 @@
\section{Функции}
\subsection{Понятие функции, параметры и аргументы}
Функция - это такая обособленная часть кода, которую можно выполнять любое количество раз. У функций обязательно в таком порядке должны быть описаны: тип возвращаемого значения, название, параметры и так называемое тело, т есть собственно исполняемый код. Рассмотрим более детально функцию \code{int main (int argc, char *argv[])}: \code{int} - это \textit{тип возвращаемого значения}, то есть на том месте, откуда будет вызвана эта функция, в результате её работы по выполнении оператора \code{return;}, появится некое целое число. Возвращаемые значения могут быть любых типов. В случае же когда функция не должна возвращать результат своей работы, или никакого возвращаемого результата не предполагается, указывается ключевое слово \code{void} (англ. - пустота). То есть на месте вызова функции, в результате её выполнения, не появится никакого значения (обычно, таким значением бывает rvalue). Оператор \code{return;} обязателен для не-void функций, а в \code{void} функциях может присутствовать или нет, но никогда не содержит возвращаемого значения. \code{main} - это \textit{название функции}. Функция именно с таким названием, написанным с маленькой буквы, всегда является точкой входа в программу (\hyperref[text:main]{\ref{text:main}}). Операционная система ищет именно эту функцию, когда получает команду на выполнение программы.
Функция - это такая обособленная часть кода, которую можно выполнять любое количество раз. У функций обязательно в таком порядке должны быть описаны: тип возвращаемого значения, название, параметры и так называемое тело, то есть, собственно, исполняемый код. Рассмотрим более детально функцию \code{int main (int argc, char *argv[])}: указанный тип \code{int} - это \textit{тип возвращаемого значения}, то есть на том месте, откуда будет вызвана эта функция, в результате её работы по факту выполнения оператора \code{return;}, появится некое целое число. Возвращаемые значения могут быть любых типов. В случае же когда функция не должна возвращать результат своей работы, или никакого возвращаемого результата не предполагается, указывается ключевое слово \code{void} (англ. - пустота). То есть на месте вызова функции, в результате её выполнения, не появится никакого значения (обычно, таким значением бывает rvalue). Оператор \code{return;} обязателен для не-void функций, а в \code{void} функциях может присутствовать или нет, но никогда не содержит возвращаемого значения. Написанное с маленькой буквы слово \code{main} - это \textit{название функции}. Функция именно с таким названием, написанным с маленькой буквы, всегда является точкой входа в программу (\hyperref[text:main]{\ref{text:main}}). Операционная система ищет именно эту функцию, когда получает команду на выполнение программы.
\frm{Названия функций в рамках одной программы не должны повторяться и не должны начинаться с цифр или спецсимволов, также, как и названия переменных (см стр. \hyperref[text:naming]{\pageref{text:naming}}) никаких других ограничений на название функций не накладывается.}
Конструкция в круглых скобках \code{(int argc, char *argv[])} - это \textit{параметры функции}. Параметры функции - это такие переменные, которые создаются при вызове функции и существуют только внутри неё. С их помощью можно передать в функцию какие-то аргументы и исходные данные для работы. Параметры пишутся в круглых скобках сразу после названия функции. В случае если функция не принимает параметров необходимо поставить после названия пустые круглые скобки (\code{()}). Весь код, содержащийся в фигурных скобках после параметров функции называется \textit{телом функции}. Это те операторы и команды, которые будут последовательно выполнены при вызове функции. В теле функции мы можем \textbf{вызывать} другие функции, но \textbf{никогда не можем объявлять, описывать или создавать в теле функции другие функции}. Никаких других ограничений на написание тела функции язык не накладывает. Таким образом, общий вид функции имеет следующий вид:
Конструкция в круглых скобках \code{(int argc, char *argv[])} - это \textit{параметры функции}. Параметры функции - это такие переменные, которые создаются при вызове функции и существуют только внутри неё. С их помощью можно передать в функцию какие-то аргументы и исходные данные для работы. Параметры пишутся в круглых скобках сразу после названия функции. В случае если функция не принимает параметров необходимо поставить после названия пустые круглые скобки (\code{()}). Весь код, содержащийся в фигурных скобках после параметров функции называется \textit{телом функции}. Это те операторы и команды, которые будут последовательно выполнены при вызове функции. В теле функции мы можем \textbf{вызывать} другие функции, но \textbf{никогда не можем объявлять, описывать или создавать в теле функции другие функции}. Никаких других ограничений на написание тела функции язык не накладывает. Таким образом, общий вид функции следующий:
\begin{figure}[h!]
\begin{verbatim}
@ -13,7 +13,7 @@
}
\end{verbatim}
\end{figure}
Далее приведём небольшой пример, который призван продемонстрировать, как выглядит простейшее \textit{объявление} и \textit{описпание} функций (function declaration and definition), а также их вызов из функции \code{int main (int argc, char *argv[])}.
Далее приведём небольшой пример, который призван продемонстрировать, как выглядит простейшее \textit{объявление} и \textit{описание} функций (function declaration and definition), а также их вызов из функции \code{int main (int argc, char *argv[])}.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
@ -51,8 +51,8 @@ $
\end{verbatim}
\end{figure}
Функции принято разделять на проверяющие, считающие и выводящие, и каждая из вышеописанных функций не должна нести дополнительной нагрузки. То есть, функция не должна знать откуда в программе появились её параметры, и где будет использован результат её работы. То есть сам язык таких ограничений не накладывает, но такой подход к написанию функций делает их значительно более гибкими и даёт им возможность быть переиспользованными. Без применения такого подхода было бы невозможно писать абстрактные библиотеки и фреймворки.
\frm{\textbf{Параметры функции} - это те переменные, которые указываются в круглых скобках при определении или описании функции. Параметры функции существуют как локальные переменные в кодовом блоке тела функции.\textbf{Аргументы функции} - это те значения переменных или литералов, которые указываются в круглых скобках при выхове функции.}
Для примера опишем функцию, суммирующую два числа. Для простоты, в качестве аргументов она будет принимать целые числа и возвращать целочисленный результат. Обратите внимание что функция не <<знает>> откуда взялись эти числа, мы можем их прочитать из консоли, можем задать в виде констант или получить в результате работы какой-то другой функции. Внутри функции \code{int main (int argc, char *argv[])} программа вызывает нашу функцию \code{sum(int x, int y)} суммирующую два числа и передаём в качестве аргументов эти числа.
\frm{\textbf{Параметры функции} - это те переменные, которые указываются в круглых скобках при определении или описании функции. Параметры функции существуют как локальные переменные в кодовом блоке тела функции. \textbf{Аргументы функции} - это те значения переменных или литералов, которые указываются в круглых скобках при вызове функции.}
Для примера опишем функцию, суммирующую два числа. Для простоты, в качестве аргументов она будет принимать целые числа и возвращать целочисленный результат. Обратите внимание, что функция не <<знает>> откуда взялись эти числа, мы можем их прочитать из консоли, можем задать в виде констант или получить в результате работы какой-то другой функции. Внутри функции \code{int main (int argc, char *argv[])} программа вызывает нашу функцию \code{sum(int x, int y)} суммирующую два числа и передаём в качестве аргументов эти числа.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
@ -79,7 +79,7 @@ x = 110
$
\end{verbatim}
\end{figure}
Как уже было сказано, параметры - это переменные, которые хранят в себе некоторые начальные значения вызова функции. Параметризация позволяет использовать одни и те же функции с разными исходными данными. Приглядимся повнимательнее к хорошо знакомой нам функции \code{printf();}. Строка, которую мы пишем в круглых скобках в двойных кавычках - это аргумент функции. То есть мы знаем, что функция умеет выводить на экран строки, как именно - нам нет дела, а какие именно строки - мы указываем в качестве аргумента. Функция \code{printf();} примечательна еще и тем, что она может принимать в себя нефиксированное количество аргументов. Описание работы таких функций, а также их написание выходит далеко за пределы основ языка, нам важно помнить что мы можем это использовать. В аргументе функции \code{printf()} мы можем написать заполнитель соответствующего типа и, например, вызвать нашу функцию \code{sum}.
Как уже было сказано, параметры - это переменные, которые хранят в себе некоторые начальные значения вызова функции. Параметризация позволяет использовать одни и те же функции с разными исходными данными. Приглядимся повнимательнее к хорошо знакомой нам функции \code{printf();}. Строка, которую мы пишем в круглых скобках в двойных кавычках - это аргумент функции. То есть мы знаем, что функция умеет выводить на экран строки, как именно - нам нет дела, а какие именно строки - мы указываем в качестве аргумента. Функция \code{printf();} примечательна еще и тем, что она может принимать в себя нефиксированное количество аргументов. Описание работы таких функций, а также их написание выходит далеко за пределы основ языка, нам важно помнить что мы можем это использовать. В аргументе функции \code{printf()} мы можем написать заполнитель соответствующего типа и, например, вызвать нашу функцию \code{sum()}.
\subsection{Оформление функций. Понятие рефакторинга}
Теперь мы без проблем можем оформить уже существующие у нас программы в виде функций. Например, оформим в виде функции программу проверки простоты числа. Для этого опишем функцию которая возвращает целое число, назовем ее \code{isPrime()}, в качестве параметра она будет принимать целое число, назовем его \code{number}. Найдем в предыдущих разделах (стр. \hyperref[code:isPrime]{\pageref{code:isPrime}}) программу определения простоты числа и скопируем в тело функции. Внесем небольшие правки, уберем вывод так как это будет, можно сказать, классическая проверяющая функция, вывод оставим для функции \code{int main (int argc, char *argv[])}, пусть о наличии у нас терминала <<знает>> только она.
\frm{Такой процесс, перенос участков кода между функциями, выделение участков кода в функции, синтаксические, стилистические и другие улучшения, называетя \textbf{рефакторингом}. Обычно, рефакторингом занимаются сами разработчики в свободное от основной деятельности времени, в периоды код ревью или по необходимости улучшить читаемость/повторяемость собственного кода.}
@ -150,4 +150,4 @@ int main(int argc, char *argv[]) {
\begin{lstlisting}[language=C,style=CCodeStyle]
int isPrime(int number);
\end{lstlisting}
Из таких определений часто составляют так называемые \textit{заголовочные файлы}. Заголовочные файлы это мощный инструмент модульной разработки. Мы уже неоднократно видели подключение заголовочного файла \code{stdio.h}, Обнаружив данный файл на диске компьютера, мы увидим, что в нём содержатся другие подключения библиотек, директивы препроцессора (о которых более подробно мы будем говорить на следующих занятиях) и прототипы функций (например, так часто используемой нами \code{printf()}). Заголовочным этот файл называется, потому что его обычно пишут в коде программы в самом верху, и фактически, компилятор просто вставляет его содержимое в текст программы. Расширение файла (\code{.h}) является сокращением от английского слова header, заголовок. Обратите внимание, что подключая заголовочный файл \code{stdio.h} мы получаем вообще всю функциональность стандартного ввода-вывода, то есть, например, работу с файлами, которую можем и не использовать. В стандарте С++20 было принято решение о переходе для поддержки повторяемости кода от заголовочных файлов к целостным модулям, импортируемым отдельно. Это позволяет интегрировать в программу только нужный функционал, игнорируя всю остальную библиотеку.
Из таких определений часто составляют так называемые \textit{заголовочные файлы}. Заголовочные файлы это мощный инструмент модульной разработки. Мы уже неоднократно видели подключение заголовочного файла \code{stdio.h}, Обнаружив данный файл на диске компьютера, мы увидим, что в нём содержатся другие подключения библиотек, директивы препроцессора (о которых более подробно мы будем говорить в следующих разделах) и прототипы функций (например, так часто используемой нами \code{printf()}). Заголовочным этот файл называется, потому что его обычно пишут в коде программы в самом верху, и, фактически, компилятор просто вставляет его содержимое в текст программы. Расширение файла (\code{.h}) является сокращением от английского слова header, заголовок. Обратите внимание, что подключая заголовочный файл \code{stdio.h} мы получаем вообще всю функциональность стандартного ввода-вывода, то есть, например, работу с файлами, которую можем и не использовать. В стандарте С++20 было принято решение о переходе для поддержки повторяемости кода от заголовочных файлов к целостным модулям, импортируемым отдельно. Это позволяет интегрировать в программу только нужный функционал, игнорируя всю остальную библиотеку.

54
sections/08-pointers.tex Normal file
View File

@ -0,0 +1,54 @@
\section{Указатели}
Вот и пришла пора поговорить о серьёзном низкоуровневом программировании. О том, от чего стараются оградить программистов языки высокого уровня и современные фреймворки. Об указателях, что такое указатели и как они соотносятся с остальными переменными, что такое передача аргумента по значению и по указателю.. Этого разговора боятся все начинающие программисты и не без причин: работа с указателями на память может не только навредить программе, но и, например, оказать влияние на операционную систему (автор знает, что этот тезис не всегда справедлив, также, как тезис со стр. \hyperref[text:simplify]{\pageref{text:simplify}}, но мы снова идём на такое упрощение ради того, чтобы было понятно, насколько это мощный инструмент). Также сразу отметим, что указателям достался свой собственный раздел в этом документе, хотя формально это просто ещё один тип данных.
Как мы, наверняка, помним, все переменные и константы, используемые в программе, хранятся в оперативной памяти. Оперативная память разделена на несколько участков, но не это для нас сейчас важно. Важно то, что у каждой переменной и константы в памяти есть свой собственный адрес. Адреса принято показывать на экране в виде шестнадцатиричных чисел. Этот адрес выдаётся нашей программе операционной системой, а язык С позволяет использовать его на усмотрение программиста. Иными словами в языке С есть возможность получить доступ к переменной не только по имени, но и по адресу. Получение доступа к значению переменной по адресу называется \textbf{разыменованием}. Давайте выведем в консоль всю имеющуюся информацию о переменной \code{а}. Мы знаем, что это целочисленная переменная значением 50, которая хранится по какому-то адресу.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int a = 50;
printf("value of 'a' is %d \n", a);
printf("address of 'a' is %p \n", &a);
\end{lstlisting}
\end{figure}
Адрес переменной может храниться в специальной переменной, которая называется указатель. Для объявления указателя пишут тип переменной, адрес которой будет храниться в указателе, знак звёздочки и имя указателя. Такому указателю можно присвоить значение адреса существующей переменной, также как мы делали это раньше с другими типами данных. Для наглядности снова выведем всю имеющуюся у нас на данный момент информацию на экран. Напомню, для вывода адреса используется заполнитель \code{\%p}. Выведем в консоль десятичное значение переменной \code{pointer} и адрес переменной \code{pointer}. Увидим, что значение переменной \code{pointer} является как будто бы совершенно случайным числом, но ниже мы представим это значение не в виде обычного целого числа в десятичной системе счисления, а в виде адреса (заполнитель \code{\%p}) то всё встанет на свои места.
\begin{figure}[h!]
\begin{lstlisting}[language=C,style=CCodeStyle]
int * pointer;
pointer = &a;
printf("value of 'pointer' is %d \n", pointer);
printf("address of 'pointer' is %p \n", &pointer);
printf("value of 'pointer' is %p \n", pointer);
\end{lstlisting}
\end{figure}
Так, объединённый вывод двух предыдущих листингов будет примерно такой, и можно явно увидеть, что адрес \code{а} - это значение переменной \code{pointer}:
\begin{verbatim}
value of 'a' is 50
address of 'a' is 000000000061FE1C
value of 'pointer' is 6422044
address of 'pointer' is 000000000061FE10
value of 'pointer' is 000000000061FE1C
\end{verbatim}
В общем-то, пока что ничего необычного, все эти операции мы выполняли на предыдущих уроках. Но поскольку \code{pointer} это немного необычная переменная, а указатель, то мы можем получить не только её значение, но и \textit{значение переменной, на которую она указывает}, именно этот процесс называется разыменованием указателя. Давайте запишем, вывести в консоль <<переменная pointer указывает на такое-то значение>> и разыменуем \code{pointer}. То есть получим доступ к значению переменной, на которую ссылается указатель \code{pointer}.
\begin{lstlisting}[language=C,style=CCodeStyle]
printf("variable 'pointer' points at: %d", *pointer);
\end{lstlisting}
Таким образом, получается, что в указателе хранится ссылка на значение некоторой переменной, и мы можем это значение изменить. Давайте изменим значение переменной \code{а}, не на прямую, а с использованием указателя. Как видим, значение переменной изменилось.
\begin{lstlisting}[language=C,style=CCodeStyle]
*pointer = 70;
printf("value of a is %d \n", a);
\end{lstlisting}
То есть указатель - это простейший \textbf{ссылочный тип данных}. Без указателей невозможно себе представить создание классов, и всеми любимого объектно-ориентированного программирования, даже массивов или строк. Теперь, когда мы знаем об указателях, и умеем получать значения переменных, на которые они указывают, а также изменять их, перед нами открываются невообразимые ранее перспективы. Мы можем писать функции не создавая в них копии переменных, а передавать в них указатели на уже существующие переменные, тем самым экономя память, и ускоряя выполнение программы. Например, не составит труда написать \textit{программу}, которая бы меняла местами значения двух переменных. Но написать \textit{функцию}, которая бы проделывала тоже самое невозможно без применения указателей. Почему? Очень просто - в параметре функции создаются свои собственные переменные, значения которых задаются копированием аргументов вызова, и меняются местами именно эти, скопированные значения в локальных переменных. И даже если мы вернём одно из этих значений как быть со вторым? А получить доступ к значению второй переменной мы не можем, поскольку, помним, она находится в области видимости функции и недоступна извне. Такая передача аргументов называется \textit{передачей по значению} (мы берём значение некоторой переменной и копируем внутрь функции, иногда такую передачу значений ещё называют \textit{передачей копированием}). Т.е. мы берем значения некоторых переменных в функции \code{int main (int argc, char *argv[])} и передаем их в функцию, где создаём новые переменные с этими, переданными, значениями.
Как решить эту проблему? Передавать не значения переменных, а их адрес, тем самым сообщив функции, что нужно не создавать новые копии переменных, а сделать что-то с уже существующими, и, естественно указать адрес, с какими именно. Передача в качестве аргумента адреса, и создание в теле функции нового указателя называется \textit{передачей по указателю}.
\frm{Для языка С также справедливо выражение <<передача по ссылке>>, поскольку в языке С нет отдельной операции передачи по ссылке. Так, например, в языке С++ передача по ссылке и передача по указателю - это разные операции.}
То есть функция будет ссылаться на переменные, на которые мы укажем и оперировать их значениями. Давайте немного модифицируем нашу программу обмена значениями внутри двух переменных (\hyperref[code:programswap]{\ref{code:programswap}}): опишем её в виде функции, принимающей в качестве параметров два указателя на целые числа типа \code{char}, и передадим адреса созданных в \code{int main (int argc, char *argv[])} переменных. Внутри функции, при её вызове, у нас будут создаваться не переменные, а указатели на переменные, то есть мы будем ссылаться на те самые переменные, созданные вне функции, и будем менять именно их (тех переменных) значения. Таким образом, нам не нужно ничего возвращать, потому что в функции ничего не создавалось, и типом возвращаемого значения функции должен быть \code{void}.
\begin{multicols}{2}
\lstinputlisting[language=C,style=CCodeStyle]{../sources/swapfunc.c}
\columnbreak
\lstinputlisting[language=C,style=CCodeStyle]{../sources/swapprog.c}
\end{multicols}
Применение такого подхода открывает перед нами широкие возможности, некоторые из них мы рассмотрим в следующих разделах.