basic-c/sections/08-pointers.tex

61 lines
13 KiB
TeX
Raw Blame History

This file contains ambiguous Unicode characters

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

\section{Указатели}
Вот и пришла пора поговорить о серьёзном низкоуровневом программировании. О том, от чего стараются оградить программистов языки высокого уровня и современные фреймворки. Об указателях, что такое указатели и как они соотносятся с остальными переменными, что такое передача аргумента по значению и по указателю.. Этого разговора боятся все начинающие программисты и не без причин: работа с указателями на память может не только навредить программе, но и, например, оказать влияние на операционную систему (автор знает, что этот тезис не всегда справедлив, также, как тезис со стр. \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}.
\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}
Применение такого подхода открывает перед нами широкие возможности. Важно, на схеме со стр. \pageref{fig:dereference}, что указатель - это тоже переменная, поэтому мы можем создавать указатели на указатели, и так далее, указатели любой сложности, тем самым увеличивая уровень абстракции программы.