2.11.1. Что такое указатель
Когда переменная объявляется в программе, то записывается ее имя и тип. При этом
подразумевается, что переменная будет принимать значения из некоторого
множества. На этапе выполнения программы каждой переменной соответствует
некоторый блок памяти. Этот блок имеет адрес (целое положительное число,
обозначающее номер первого байта данного блока) и размер, который зависит от
типа принимаемых значений. Например, для целой переменной выделяется 2 байта.
Запишем следующее объявление переменной:
-
int k;
Рассматривая часть «int» оператора объявления, компилятор будет знать, что для
переменной «k» требуется два байта оперативной памяти для хранения целых
значений. При трансляции само имя «k» будет записано в таблицу символов, а
после трансляции данному имени будет сопоставлен адрес блока памяти, длина
которого известна и соответствует заданному типу (для нашего примера это 2
байта). Позже, когда мы запишем
-
k=2;
Мы ожидаем, что во время выполнения программы целое значение 2 будет записано в
блок памяти, который выделен под переменную k. Таким образом, k воспринимается
как объект, с которым ассоциируется два значения: одно – это число, которое
хранится в выделенном блоке памяти, и второе число - это адрес данного блока
памяти. В тех случаях, когда необходимо взять значение объекта k, то говорят
«rvalue» (правое значение, это означает, что k стоит справа от знака присвоения
«=»). В тех случаях, когда мы хотим присвоить значение k, то говорят о «lvalue»
(левое значение, это означает, что k стоит слева от знака присвоения «=»).
Таким образом /Керниган и Ритчи/:
-
объект есть поименованная область памяти, имеющая адрес и размер;
-
rvalue объекта - это значение, которое хранится в объекте;
-
lvalue объекта - это выражение, ссылающееся на объект (адрес объекта).
Приведем следующий текст фрагмента программы:
-
int k, j; //1
k=2; //2
j=7; //3
k=j; //4
В строке 3 для переменной j берется lvalue, и присваивается значение 7. В
строке 4 для j берется rvalue (ее значение 7) и присваивается переменной k, для
k берется lvalue (ее адрес, куда будет записано значение 7).
Теперь нам бы
хотелось иметь переменную, значением которой был бы адрес некоторого объекта
(иными словами, lvalue некоторого объекта). Такой переменной стала переменная
типа указатель или просто указатель. Указатель объявляется с помощью звездочки,
например:
-
int * ptr;
В этом примере ptr - это имя переменной (такое, как “k” в предыдущем примере);
Символ “*” говорит о том, что переменная ptr - указатель, т.е. значение данной
переменной есть адрес некоторого блока ОП. Ключевое слово int сообщает
компилятору о том, что наша переменная содержит адрес блока памяти, в котором
будет храниться целое число. При этом компилятор заранее знает, какой объем
памяти будет занимать указатель (это зависит от процессора и системы
программирования (см. модель памяти компилятора)). Таким образом, можно
сказать, что указатель установлен на целое (int). Однако когда мы записываем
int k; мы не присваиваем значения переменной k. Если это объявление сделано вне
какой либо функции, то компилятор инициирует ее в ноль. В противном случае k
может иметь любое значение.
В нашем примере переменная ptr не имеет значения, это означает, что указатель
ptr не содержит адреса на блок памяти, где записано целое число. В случае, если
указатель объявлен вне тела какой либо функции, ему присваивается специальное
значение, которое называется null-указатель. Это значение гарантирует, что
указатель, имеющий данное значение, не ссылается ни на какой-либо объект или
функцию.
В действительности реальное значение null не обязательно должно
равняться нулю. Все зависит от специфики компьютера и операционной системы. Для
того, чтобы сделать Си-программы совместимыми между разными типами компьютеров,
ввели специальный макрос для представления null-указателя. Этот макрос имеет
имя NULL. Таким образом, оператор присваивания ptr=NULL; гарантирует, что
указатель ptr примет null значение. Для проверки значения мы можем написать
следующий условный оператор
-
if (ptr==NULL) {...}
Для присвоения значения переменной ptr, т.е. присвоения адреса некоторого
объекта, хранящего целое число, например адрес переменной k, необходимо
воспользоваться унарной операцией &. Для нашего примера это выглядит
следующим образом:
-
ptr=&k;
Операция & обеспечивает определение lvalue переменной k в случае, если k
стоит справа от знака присвоения “=”. После выполнения оператора присвоения
говорят, что ptr указывает на объект k.
Для того, чтобы присвоить значение
объекту через указатель, используют специальную операцию унарная звездочка “*”.
Рассмотрим следующий пример:
-
*ptr=7;
При выполнении этого оператора значение 7 будет записано в блок памяти, адрес
которого записан в указателе. Таким образом, если ptr указывает на k (адрес
переменной k хранится в ptr), то переменной k будет присвоено значение 7. В
этом примере операция “*” означает, что lvalue вычисляется с помощью указателя
ptr. Теперь, если мы запишем
-
j=*ptr;
,то переменной j будет присвоено значение 7. В данном случае *ptr означает
rvalue, т.е. будет взято значение объекта, адрес которого хранится в указателе
ptr. Ниже приведен простейший пример для пояснения идеи указателя.
-
#include <stdio.h>
int j,k;
int *ptr;
int main()
{
j=1;
k=2;
ptr=&k;
printf("\n ++++++ Пример №1 +++++++\n");
printf("j имеет
значение %d и хранится по адресу %p\n", j,(void
*)&j);
printf("k имеет значение %d и
хранится по адресу %p\n", k,(void
*)&k);
printf("ptr имеет значение %p и
хранится по адресу %p\n", ptr,(void
*)&ptr);
printf("значение целого, на
которое указывает ptr равно %d\n" ,*ptr);
return
0;
}