Указатели в языке Си

Тема указателей довольно сложна и мне часто присылают вопросы по ней. В этой статье мы подробно разберем тему указателей.

Рекомендуемая работа с памятью

Перед тем, как разбираться с указателями, сначала посмотрим, как лучше работать с памятью в Си.

Рекомендуемый способ — это прямо описать ту область памяти, которую вы хотите использовать.

Например:

В этом примере мы задали целое число, массив и вещественное число. То есть мы явно дали имя элементу данных.

Если мы теперь будем использовать этот элемент данных, то достаточно указать его имя.

Почему рекомендуется делать именно так? Три причины:

Причина 1. Соблюдение принципа изоляции кода

В языке Си данные в функцию передаются по значению. Эти значит, что копия входных параметров размещается на стеке. После выхода из функции эти данные уничтожаются. Поэтому программист уверен, что никакого влияния на остальную программу эти данные не окажут.

Причина 2. Простой вызов в отладчике

Просто добавьте имя элемента данных и вы можете легко наблюдать, что с ним происходит во время отладки.

Причина 3. Самодокументируемый код

Если мы дадим объектам данных осмысленные имена, то код будет хорошо читаться. Например:

Но так как язык Си универсален и есть много способов писать программы, то есть и такой способ как указатели.

Что такое указатели?

Указатель — это переменная, которая содержит адрес некоторого объекта в памяти.

Для работы с указателями используются два оператора:
& — получить адрес переменной (&x — адрес переменной x)

* — получить значение переменной по адресу (*px — значение по адресу px)

Рассмотрим участок памяти. Предположим, что по адресу 54100 размещена символьная переменная char x;

При заведения указателя мы сразу говорим компилятору, что мы завели указатель на объект типа char. Чтобы не было путаницы в именах рекомендуется указатель начинать с символа «p».

Важный момент. Когда комплятор выделяет память под «char x», то выделяется один байт, потому что x — это символ, то есть это однобайтовая переменная. Но когда компилятор выделяет память под «char *px», то выделяется обычно 4 байта, так как адрес (в 32-х битовой системе) занимает 4 байта.

Как читать указатели
char *px — читается как «значение по адресу, xранящемуся в px, имеет тип char»

*px — читается как «взять значение по адресу, xранящемуся в px»

Теперь нам нужно записать:

  • в переменную x некоторое значение,
  • a в указатель px — адрес этого значения.

Для этого мы пишем следующие строки:

После этого мы можем работать с этим адресом в памяти как через имя переменной, так и через указатель. Получение значение через указатель называется разыменование указателя.

Но тут нужно быть осторожным. Если в указателе лежит адрес, который не выделен программе, то эта ситуация называется висячий указатель.

Предположим, что px — это висячий указатель. Действия с самим указателем px могут быть любыми, программа не пострадает. Но если мы выполним действие *px над памятью, которая не была выделена программе, то операционная система прекратит действие программы и напишет что-то вроде: «Программа выполнила недопустимую операцию и будет закрыта».

Преимущество указателей

Преимущество указателей в том, что они позволяют передать в функцию значение по ссылке. Слово «ссылка» означает, что мы не передаем значение, а ссылаемся на адрес этого значения. В этом случае можно внутри функции изменять значение элемента данных. Хотя указатель и будет уничтожен после выхода из функции, но мы изменили значение памяти по указателю и это изменение значения сохранится после выход из функции.

Именно поэтому их часто используют.

Недостатки указателей

Главные недостатки указателей:

1. Нарушение принципов изоляции кода

Ошибка в указателе может привести к тому, чтобы будет испорчена память в случайном месте. Хорошо еще, если повезет и программа рухнет, тогда программист сразу заметит ошибку. Но если программа продолжит работу, то найти ошибку будет очень сложно, ведь она не сразу проявляется.

2. Отвлечение внимание на детали реализации

При использовании указателей программисту нужно держать в уме принципы работы с памятью, а это отвлекает от сути задачи, которая решает программист. При правильном подходе к программированию программист должен думать только решаемой задаче, и не отвлекаться на посторонние детали.

3. Плохая читаемость кода

Прямое использование переменной является самоочевидной вещью. Если мы видим x++, то сразу понимаем, что происходит, а вот если мы видим (*px)++ или *px++, то чтобы понять процесс, нужно вдумываться.

Сравним два варианта кода. Код с переменными:

и код с указателями

Мы видим, что с указателями читаемость кода хуже, а преимуществ никаких мы не получили.

Указатели лучше вообще не использовать.

Конечно же, на это последует вопрос: «А как тогда изменять значения внутри функции?»

Например, так:

Этот код поменяет значения любых элементов массива.

Если же речь идет именно о переменных, то правильный ответ такой: если вам нужно изменять внешние значения переменных внутри функции — у вас неверно спроектирована программа.

В правильно спроектированной программе есть три вида элемента данных:

  • Элементы общего назначения — этих данных немного и они должны быть глобальными для всей программы. (например, идентификатор открытой базы данных)
  • Элементы модуля — сюда рекомендуется выносить все данные, которые нужны модулю, чтобы не раздувать передаваемые параметры (например, идентификаторы окон).
  • Временные данные — они должны быть описаны внутри функций и удаляться при выходе.

Для указателей при таком подходе места нет. Но так как указатели широко используются в различных библиотеках, то работать с ними надо уметь.

Ответы на вопросы

Вопрос

Мы сначала передали адрес a в функцию AddFive, затем создали указатель int px(но почему именно в аргументе функции?), далее значение по адресу указателя увеличили на 5. Но тут непонятно, разве так будет работать? То есть, нужно сначала адрес присвоить указателю, как Вы показывали ранее в статье. Получится вот так:

Ответ

В аргументах функции ничего не создается. В аргументах указываются типы, чтобы компилятор мог проверить их при вызове функции. Адрес указателю px не надо присваивать внутри функции, так как этот адрес уже передан в качестве аргумента при вызове функции. То есть во время работы функции указатель px уже указывает на нужный адрес.

Вопрос

Я проверил код в CodeBlocks.  Если мы укажем, например, вот так:

 

то возникнет ошибка: «px undeclared». Т.е. как видите указатель px не объявлен. А чтобы всё работало мы одновременно, c одной стороны, объявляем указатель в аргументе функции AddFive, а с другой стороны, записываем в указатель адрес a. Поэтому, непонятно почему Вы считаете, что ничего там не создаётся. Ведь память под указатель выделилась, так? И как раз, так как мы создали указатель, пусть и в аргументе функции, программа и работает.

Ответ

На этапе компиляции программы при проверке аргументов функции компилятор ничего не создает, а только проверяет соответствие типов аргументов в описании функции и при ее вызове. В данном примере в описании функции нет аргументов, а при вызове передается адрес — это первая ошибка в данном фрагменте. Вторая ошибка заключается в том, что идет обращение к переменной px, но она не объявлена, поэтому компилятор пишет, что «px undeclared», то есть «переменная px не объявлена».

Когда ошибки будут устранены и программа будет запущена, то в момент вызова AddFive(&a) произойдет следующее:

  1. Программа считает адрес переменной a и передаст управление функции AddFive.
  2. Аргументы функции (в данном случае адрес) будут размещены на стеке функции (это временное хранилище данных).
  3. Во время выполнения функции данные будут взяты со стека и обработаны.
  4. После выхода из функции стек будет очищен.

То есть в данном примере память для указателя специально не создается, а используется обычный стек для аргументов функции.

Вопрос

При передаче аргументов по значению в функцию передается копия значения переменной х=5, которая хранится в памяти по некоторому адресу, и функция выполняет над ним необходимые операции. Оригинал же значения х по этому адресу остается неизменным. Как бы понятно. Но тогда возникает попутный вопрос – а зачем нужна такая функция, если результат ее выполнения уничтожается при выходе из функции (т.е. при возврате в основную программу) и далее не используется?
 

Ответ

Результат выполнения функции — это возвращаемое значение.
 
Например
 
Его легко видно, оно идет после слова return.
Любой другой результат работы функции является дополнительным, но главное, что он не очевиден.
То есть при работе с указателями внутри функции нам надо помнить, что эта функция еще что-то меняет в данных.
Когда таких функций две-три — это терпимо. А если более десятка, то уже есть риск ошибиться.
 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт использует Akismet для борьбы со спамом. Узнайте, как обрабатываются ваши данные комментариев.