basic-c/sections/08-pointers.tex

65 lines
13 KiB
TeX
Raw Permalink Normal View History

2021-08-25 16:52:36 +03:00
\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{значение переменной, на которую она указывает}, именно этот процесс называется разыменованием указателя.
\begin{figure}[h!]
\input{../schemes/pointer}
\caption{Отношения указателей, адресов, идентификаторов и значений}
\label{fig:dereference}
\end{figure}
Давайте запишем, вывести в консоль <<переменная pointer указывает на такое-то значение>> и разыменуем \code{pointer}. То есть получим доступ к значению переменной, на которую ссылается указатель \code{pointer}.
2021-08-25 16:52:36 +03:00
\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}.
2022-02-02 00:08:39 +03:00
\begin{figure}[H]
\setlength{\columnsep}{30pt}
\begin{multicols}{2}
\lstinputlisting[language=C,style=CCodeStyle]{../sources/swapfunc.c}
\columnbreak
\lstinputlisting[language=C,style=CCodeStyle]{../sources/swapprog.c}
\end{multicols}
\end{figure}
2021-09-27 08:48:12 +03:00
Применение такого подхода открывает перед нами широкие возможности. Важно, на схеме со стр. \pageref{fig:dereference}, что указатель - это тоже переменная, поэтому мы можем создавать указатели на указатели, и так далее, указатели любой сложности, тем самым увеличивая уровень абстракции программы.