Работа с указателями. Все данные (переменные, константы и др.) хранятся в памяти

Понятие указателя

Все данные (переменные, константы и др.) хранятся в памяти. Память представляет собой непрерывную последовательность ячеек (байтов), каждая из которых имеет свой номер – адрес:

                                       
                                       
                                   
                                         

При определении, например, некоторой переменной, она располагается в памяти по определенному адресу и занимает столько ячеек, сколько требует тип этой переменной. Пусть, например, имеется переменные int A = 2351 и double B = 3.1 и пусть они располагаются в памяти так:

      А = 2351 B = 3.14          
                                       
                                   
                                                                               

Говорят, что переменная А располагается по адресу 101 и занимает 4 байта, а переменная B имеет адрес 105 и занимает 8 байт памяти.

Для получения адреса какого-либо программного объекта используется оператор &. Например, если выполнить фрагмент следующей программы (в предположении, что переменные A и B располагаются в памяти, как это показано на предыдущем рисунке):

int A = 2351;

double B = 3.14;

cout << “Значение переменной А: ” << A << endl;

cout << “Адрес переменной А: ” << &A << endl;

cout << “Значение переменной В: ” << В << endl;

cout << “Адрес переменной В: ” << &В << endl;

получим следующий результат:

Значение переменной А: 2351

Адрес переменной А: 101

Значение переменной В: 3.14

Адрес переменной В: 105

Правда, значения адресов переменных будут выведены в шестнадцатеричном формате.

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

Указатели определяются в программе следующим образом:

<тип данных> *<имя переменной>

Здесь < тип данных > определяет так называемый базовый тип указателя.

<Имя переменной> является идентификатором переменной-указателя.

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

Например:

int *p1;

double *p2;

Здесь определены две переменные-указатели (или просто – два указателя). Указатель p1 является переменной-указателем на базовый тип int (или, как говорят, переменная p1 указывает на int - значение), а указатель p2 указывает на double – значение.

Иными словами, переменная p1 предназначена для хранения адресов участков памяти, размер которых соответствует типу int (4 байта), а переменная p2 - для хранения адресов участков памяти, размер которых соответствует типу double (8 байт).

Формально указатели представляют собой обычные целые значения типа int и занимают в памяти 4 байта не зависимо от базового типа указателя. Значения указателей при их выводе на экран представляются как целые значения в шестнадцатеричном формате.

Присвоить указателю адрес некоторой переменной можно инструкцией присваивания и операцией &, например, так (возьмем предыдущий пример):

int A = 2351, *p1;

double B = 3.14, *p2;

p1 = &A; // Указателю p1 присваивается адрес переменной А

p2 = &B; // Указателю p2 присваивается адрес переменной В

cout << “Значение переменной А: ” << A << endl;

cout << “Адрес переменной А: ” << p1 << endl;

cout << “Значение переменной В: ” << В << endl;

cout << “Адрес переменной В: ” << p2 << endl;

Результат выполнения этого фрагмента программы будет таким же, как и раньше.

Однако, использование самих указателей (т. е. адресов) само по себе представляет сомнительный интерес. Более важно узнать значение того или иного объекта, на который ссылается указатель.

Получить значение объекта, на который ссылается некоторый указатель можно с помощью операции * (эту операцию обычно называют разыменованием указателя):

int A = 2351, *p1;

double B = 3.14, *p2;

p1 = &A; // Указателю p1 присваивается адрес переменной А

p2 = &B; // Указателю p2 присваивается адрес переменной В

cout << “Значение переменной А: ” << *p1 << endl;

cout << “Адрес переменной А: ” << p1 << endl;

cout << “Значение переменной В: ” << *p2 << endl;

cout << “Адрес переменной В: ” << p2 << endl;

Результат выполнения этого фрагмента программы будет таким же, как и в предыдущем примере.

Обращение к указателю с помощью оператора * (например, *p1) означает следующее: взять из памяти по адресу, хранящемуся в указателе (p1 равно 101), столько байт памяти, сколько требуется базовому типу указателя (в данном случае базовый тип указателя int, следовательно – взять 4 байта) и работать с этими байтами, как со значением базового типа указателя (в нашем примере это значение 2351 типа int). Таким образом, *p1 – это (в нашем примере) обычное значение типа int и с ним можно работать как с обычным целым числом.

С помощью указателей можно не только получать значения, расположенные по адресам, хранящимся в указателях, но и записывать нужные значения по этим адресам. Например: выполнение инструкции

*p1 = 4211;

приведет к тому, что переменная A, на которую ссылается указатель p1, станет равна 4211, а не 2351.

Указатели могут использоваться в различных выражениях наравне с обычными переменными и константами:

B = (*p1 – 1000) * 2; // Переменная В станет равна значению (4211 - 1000) * 2 = 6422.0

Или так:

*p2 = (*p1 – 1000) * 2; // Переменная B также будет равна значению 6422.0

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

Значения переменных указателей можно инициализировать при их определении, как обычные переменные:

int A = 5, B = 10, *p1 = &A, *p2 = &B;

double D = 3.14, *d = &D;

Здесь указатели p1 и p2 указывают на переменные A и B соответственно. Указатель d – на переменную D.

Указателю можно присвоить значение другого указателя, если совпадают их базовые типы:

p1 = p2; // Теперь оба указателя p1 и p2 ссылаются на переменную B

d = p1; // Ошибка – базовые типы не совпадают

Последней ошибки можно было бы избежать с помощью явного преобразования типов, например, так:

d = (double *) p1;

Но теперь указатель d будет содержать адрес переменной B типа int, а попытка взять значение по этому адресу через указатель d приведет к тому,что с этого адреса будут взяты не 4, а 8 байт памяти и полученное значение *d типа double будет непредсказуемым. Еще более неприятная ситуация может возникнуть, если попытаться записать данные по указателю d. Такая попытка может завершиться тем, что будут изменены данные, расположенные за переменной B, а это приведет к неправильной работе программы. Обнаружить такие ошибки бывает очень сложно.

Явное преобразование указателей нежелательно. Но если это необходимо, то делать это надо очень аккуратно.

Хотя формально указатели представляют собой целые значения, присваивать им произвольные целые значения нельзя. Например, попытка присвоить указателю p1 значение 10000 (p1 = 10000;) приведет к возникновению ошибки на этапе компиляции программы. Единственным исключением является присвоение указателю нулевого значения:

p1 = 10000; // Ошибка

p1 = 0; // Все правильно

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

Указатели можно сравнивать с помощью операций отношения ==, !=, >, <. С помощью таких сравнений можно характеризовать взаимное расположение объектов, на которые ссылаются сравниваемые указатели, в памяти. Но сравнение указателей с помощью операций >, < имеет смысл только в том случае, если сравниваемые указатели содержат адреса связанных между собой переменных (например, элементов массива).


Понравилась статья? Добавь ее в закладку (CTRL+D) и не забудь поделиться с друзьями:  



double arrow
Сейчас читают про: