Адресная арифметика, типы указателей и операции над ними


Операции, определенные над указателями:

операция разыменования или доступа по адресу (*);

преобразования типов или приведения типов (type *);

присваивания (=);

получения адреса (&);

сложения и вычитания (аддитивные операции);

инкремент (++);

декремент(--);

операции отношения (операции сравнения).

Операция разыменования или доступа по адресу (*)

В синтаксисе языка над указателями определена операция * (доступа по адресу, разыменования). Чтобы ею воспользоваться, необходимо перед именем указателя поставить звездочку (*имя_указателя). С помощью этой операции через указатель можно получить доступ к объекту в памяти, на который этот указатель настроен. То есть *имя_указателя является полным синонимом имени объекта; над ним определено то же самое множество операций, что и над объектом, и оно может использоваться в любых выражениях вместо объекта (переменной). Кроме этого, тип выражения *имя_указателя совпадает с типом объекта и, следовательно, все операции преобразования типа над *имя_указателя работают точно так же, как и над объектом.

Пример:

long a = 1, b = 2;

long * pi1=&b, *pi2 = &a; //настройка pi1и pi2 на a и b ;

*pi2 = *pi2 + 2**pi2/*pi1; // Это эквивалентно выражению а=а+2*а/b

cout<<”\n a = “<<a<<”\t *pi2 = “<<*pi2;

 

Результаты работы программы

a = 2 *pi2 = 2

В примере определены и инициализированы две переменные a и b типа long и два указателя pi1 и pi2 типа long*. Поскольку указатели настроены на переменные a и b, то, следовательно, в них хранятся адреса этих переменных. И так как указатели имеют тип long*, то, следовательно, они, начиная с записанных в них адресов, будут трактовать информацию в следующих 4-х байтах как значения переменных типа long. Именно это обеспечивает правильность выполнения выражения.

Операция (=) присваивания.

Язык Си++ не поддерживает преобразование типов по умолчанию при выполнении операции (=) присваивания над указателями, в отличии от основных типов данных (за исключением указателя типа void*). То есть компилятор выдаст ошибку, если тип указателя не совпадет с типом объекта, на который его настраивают.

Пример:

int a = 2, b = 1;

double c = 1, *pd;

c = a; /* В этой строке происходит преобразование типа по умолчанию при выполнении операции присваивания. Создаётся неименованная переменная типа double, которая инициализируется значением переменной a, и только после этого происходит присваивание переменной c типа double значения неименованной переменной типа double.*/

pd = &a; /*Ошибка. Указатель pd определен как указатель на объект типа double, а не указатель на объект типа int. */

pd = &c; // Ошибки нет. Указатель pd настраивается на переменную c.

Пример можно переписать, исправив ошибку с помощью операции явного преобразования типов, записав

pd = (double*) &a; /*Указателю pd присваивается значение адреса переменной a*/

В этом случае создаётся неименованный указатель типа (double*), который инициализируется адресом переменной a, и только после этого указателю pd присваивается значение неименованного указателя. То есть с помощью явного преобразования типа удаётся обойти правило выполнения операции присваивания над указателями, которое гласит: Тип указателя и тип объекта, на который он указывает, должны совпадать. Обходя это правило, компилятор генерирует код, предполагая, что программист знает, что он делает и знает, какие негативные последствия могут быть, если далее в программе будет использовано выражение *pd.

Возможные негативные последствия использования *pd.

Поскольку переменная а – это переменная типа int, то следовательно, она занимает в памяти 2 байта. В соответствии с синтаксисом операции доступа по адресу *pd будет считаться, что, начиная с адреса содержащегося в pd, в следующих 8 байтах памяти размещён объект типа double. То есть содержимое этих 8 байт будет трактоваться как значение объекта double. Значение первых двух байт известно, но значение следующих 6 байт непредсказуемо, и, следовательно, значение выражения *pd также не известно. Поэтому использование *pd в арифметических выражениях не имеет смысла.

Ещё хуже обстоит дело, если будет попытка выполнить операцию *pd = выражение. Поскольку при компиляции компилятор отвел только два байта под переменную a типа int, и что расположено в следующих 6 байтах (значение другой переменной этой же программы или вообще системные данные операционной системы) неизвестно, то перезапись этих 6 байт может привести либо к появлению труднообнаружимой ошибки, либо вообще к зависанию компьютера.

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

int *p;/* Указатель р определен, но не инициализирован, в нем находится случайное число */

cout<<*p; /* Это случайное число рассматривается как адрес в памяти объекта типа int. Что на самом деле находится по этому адресу не известно, поэтому будет выведено какое-то произвольное число. */

*p = 1; /*Компилятор не найдет ошибки в этом коде, но в этом месте программа скорей всего будет зависать или операционная система будет выдавать сообщение: «Программа выполнила некорректную операцию и будет закрыта» (произведена попытка записи 1 в память по адресу, находящемуся в р). */

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

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

int *p = NULL;

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

Есть ситуации, когда применение явного преобразования типа над указателями абсолютно необходимо. В качестве примера можно привести следующие случаи:

Известно, например, что в системной памяти по адресу 0х0417 находится байт состояния клавиатуры. Для того чтобы иметь возможность прочитать или изменить его значение, необходимо написать код char* pc = (char*)0x0417; cout<<*pc; т.е. явно указать, что 0x0417 не шестнадцатеричная константа, а адрес в памяти на переменную типа char.

В языке Си++ есть функция void* malloc(unsigned s), которая возвращает указатель на блок динамически распределяемой памяти длиной s байт. При неудачном завершении возвращает значение NULL и функция void free(void* bl), которая освобождает ранее выделенный блок динамически распределяемой памяти с адресом первого байта bl.

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

//Программа 5.1

#include "stdafx.h"

#include <iostream>

void main(){

unsigned long L = 0x12345678; /* определяется и инициализируется шестнадцатеричной /константой переменная L типа unsigned long*/

char* cp = (char * ) &L;

int * ip = (int*) &L;

long * lp = (long*) &L; /*определяется указатели cp, ip, lp типа char*, int*, long* который инициализируется с помощью явного преобразования типа адресом переменной L */

std::cout<<std::hex; /* Шестнадцатеричное представление выводимых значений*/

std::cout<<"\n Address L, i.e. &L = "<<&L<< "\t L = "<<L;/*выводится адрес в памяти, начиная с которого размещены 4 байта отведенные для переменной L, и значение самой переменной L*/

std::cout<<"\n value of pointer cp = " << (void *) cp;

std::cout<<"\t and *cp = 0x"<<(int)*cp;

std::cout<<"\n value of pointer ip = " << ip;

std::cout<<"\t and *ip = 0x"<<*ip;

std::cout<<"\n value of pointer lp = " << lp;

std::cout<<"\t and *lp = 0x"<<*lp;/*вывод на экран значений указателей cp, ip, lp и результатов выполнения операции разыменования над этими указателями*/

getchar();

}

Результат выполнения программы:

Адрес L т. е. &L = 0x1Е290ffС L = 0x12345678

Значение указателя cp = 0x1Е290ffС и *cp = 0x78

Значение указателя ip = 0x1Е290ffС и *ip = 0x5678

Значение указателя lp = 0x1Е290ffС и *lp = 0x12345678

Из результатов выполнения программы видно, что все указатели имеют одно и то же значение, которое совпадает с адресом переменной L. При применении операции разыменования (*) над указателями cp, ip, lp в соответствии с правилом выполнения этой операции первый, первый - второй и первый - четвертый байты памяти, начиная с записанного в этих указателях адреса, воспринимаются как объекты типа char, int, long соответственно. В соответствии с принятой для IBM PC стандартом, размещение числовых кодов в памяти начинается с младшего адреса. За счет этого пары младших разрядов шестнадцатеричного числового кода размещаются в байтах памяти с меньшими адресами (рис. 5.1).

 

Рис. 5.1. Схема размещения в памяти IBM PC переменной L - типа unsigned long

 

Операция получения адреса операнда (&)

Унарная операция получения адреса операнда &операнд ( не путать с бинарной операцией поразрядной конъюнкции операнд&операнд ) позволяет получить адрес размещения объекта в памяти компьютера. Другими словами, эта операция возвращает число, которое является номером байта в памяти компьютера, начиная с которого в памяти выделено место под хранение значения операнда. По умолчанию результат выполнения операции &операнд (число) имеет тип тип_операнда*. Пример:

int s = 8;

int* ps=&s;/*Результатом выполнения операции &s будет являться число, являющееся адресом байта, начиная с которого в памяти компьютера размещена переменная s. Это число имеет тип int*. Далее выполняется операция присваивания переменной ps типа int* значение типа int*. Получается, что с обеих сторон от операции присваивания стоят операнды, имеющие одинаковый тип. Поэтому операция выполняется корректно, без преобразований типов. */

 

Сложения и вычитания (аддитивные операции над указателями)

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

int ar[5]; /* Пример определения массива, состоящего из пяти элементов типа int. Обращаться к элементам массива можно с помощью выражений ar[0], ar[1], …, ar[4]*/

int * pi1 = &ar[4], pi2 = &ar[0]; /* Определение и инициализация указателей. */

int raz = pi1 – pi2; /*Разность двух указателей одного типа присваивается переменной */

Чему равна эта разность? Она равна разности значений указателей, деленной на размер в байтах объекта того типа, к которым отнесен указатель. То есть если значение pi1 = 0x00fa (адрес ar[4]), а значение pi2 = 0х00f2 (адрес ar[0]), то значение разности будет (0x00fa - 0х00f2) / sizeof(int) = 4.

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

(long) pi1 – (long) pi2; /* Явное преобразование типа. Создаются неименованные переменные типа long, которые инициализируются значениями указателей, и только затем находится разность двух неименованных переменных типа long. Результат этой разности равен, естественно, 8. Безусловно, это же значение можно было найти, вычислив (pi1 – pi2) *sizeof (int), но в этом случае надо быть абсолютно уверенным, что между объектами, на которые указывают указатели, лежат объекты того же типа. В данном случае это так, так как указатели настроены на элементы одного и того же массива, а синтаксис языка гарантирует, что элементы массива располагаются в памяти последовательно друг за другом.*/

При вычитании или прибавлении константы k к указателю, значение указателя изменяется на величину k*sizeof(type), где type - тип объекта, к которому отнесен указатель. Продолжая предыдущий пример, можно записать:

pi1 = pi1+3;/* Значение pi1 увеличится не на три, а на величину, равную 3*sizeof(int)= 6 */

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

Операции инкремента и декремента над указателями

Операции инкремента и декремента определены над указателями. При инкременте или декременте указателя его значение, как и в случае с операциями сложения и вычитания указателя и константы, увеличивается/уменьшается не на 1, а на величину sizeof(type), где type - тип объекта, к которому отнесен указатель.

Работая с адресами и указателями, нужно внимательно относиться к последовательности выполнения унарных операций *, ++, --, &, так как они в выражениях могут употребляться в самых разнообразных сочетаниях. Напомним также, что ассоциативность операций *, ++, --, & направлена справа налево.

Пример:

//Программа 5.2

#include "stdafx.h"

#include <iostream>

void main(){

int i[3] = {10,20,30}; // Определение и инициализация массива.

int *p = &i[1]; // Определение и инициализация указателя.

std::cout<<"\n *&i[1] = " << *&i[1]; /* Сначала находим адрес i[1], а потом применяем операцию доступа по адресу и в результате печатается значение i[1]. */

std::cout<<"\n *&++i[1] = " << *&++i[1]; /*Значение i[1] увеличивается на 1 и печатается.*/

std::cout<<"\n *p = "<<*p; //Печатается значение i[1]

std::cout<<"\n *p++ = "<<*p++; /*Печатается значение i[1], p увеличивается на 1, т.е. настраивается на i[2]. В данном контексте ++ постфиксная операция, поэтому она выполняется, после того как p было использовано в выражении. */

std::cout<<"\n *p = "<<*p; //Печатается значение i[2]

std::cout<<"\n ++*p = "<<++*p; /*Печатается значение i[2] увеличенное на 1.*/

std::cout<<"\n *--p = "<<*--p; /*Сначала р уменьшается на 1 и настраивается на i[1], а затем печатается значение i[1].*/

std::cout<<"\n ++*--p = "<<++*--p; /*Сначала р уменьшается на 1 и настраивается на i[0], затем i[0] увеличивается на 1 и печатается. */

getchar();

}

Результаты выполнения программы:

*&i[1] = 20

*&++i[1] = 21

*p = 21

*p++ = 21

*p = 30

++*p = 31

*--p = 21

++*--p = 11

Операции отношения над указателями

Операции отношения над указателями не имеют никаких характерных особенностей по сравнению с операциями над переменными основных типов данных. Результат выполнения операции отношения над указателями всегда будет либо 0, либо 1, а операндами - значения указателей, т.е. числа.

Пример: Записывает строку Hellow world в обратном порядке.

char str[13] = “Hellow world”;

char *pch = & str[0];

char temp;

for(int i = 0; pch <= &str[11-i]; i++, pch++) { /* Цикл будет выполняться до тех пор, пока значение указателя pch меньше или равно адресу &str[12-i].*/

temp = str[11-i];

str[11-i] = *pch;

*pch = temp;

}

Операция sizeof над указателями

Указатели – это объекты в памяти состоящие из 4 байт, в которых содержится число, воспринимающееся компилятором как адрес другого объекта в памяти. Над указателем определена операция sizeof(имя_указателя ) или sizeof(тип_указателя ). Поскольку любой указатель занимает в памяти 4 байта, то, следовательно, результатом выполнения операции sizeof() над указателями всегда будет число 4. Пример:

cout<<”\n sizeof(void*) = “ << sizeof(void*);

cout<<”\n sizeof(long*) = “ << sizeof(long*);

cout<<”\n sizeof(char*) = “ << sizeof(char*);

cout<<”\n sizeof(long double*) = “ << sizeof(long double*);

void* pv; cout<<”\n sizeof(pv) = “ << sizeof(pv);

int* pi; cout<<”\n sizeof(pi) = “ << sizeof(pi);

long* pl; cout<<”\n sizeof(pl) = “ << sizeof(pl);

float* pf; cout<<”\n sizeof(pf) = “ << sizeof(pf);

Во всех случаях операция sizeof() от указателя или типа будет возвращать значение 4.

Раз указатель - это объект в памяти, то можно найти адрес этого объекта в памяти. Для хранения адреса указателя используется тип (type**) указатель на указатель, который в свою очередь, тоже является объектом в памяти и, следовательно, можно найти и его адрес и т .д. Пример:

//Программа 5.3

#include "stdafx.h"

#include <iostream>

void main(){

int i = 88;

int *pi = &i;

int **ppi = &pi;

int ***pppi = &ppi;

std::cout<<"\n ***pppi = "<<***pppi;

getchar();

}

Результат выполнения программы:

***pppi = 88;

 

5.3. Свойства указателя типа void*

Тип void (пустота, пробел) является основным типом языка программирования Си++, но в отличие от других основных типов с помощью ключевого слова void нельзя создать объект типа void. В основном ключевое слово void используется для того, чтобы указать на отсутствие параметров функции и/или возвращаемого функцией значения. Например:

void printStr(void){ cout<<“Здравствуй Мир!!”;}

Эта функция не принимает никаких параметров и не возвращает никакого значения.

Указатель типа void* - это четырехбайтовый объект, предназначенный для хранения адреса любого типа объектов или адреса фрагмента памяти, т.е. он может указывать на всё что угодно. С этим указателем не связывается информация о том, какой объект находится в памяти, начиная с записанного в нем адреса. Именно поэтому нельзя применить к указателю типа void* операцию доступа по адресу (*). У компилятора нет информации, сколько байт отведено под объект, на который указывает указатель типа void*, и как информацию в этих байтах интерпретировать. Указатель типа void* единственный тип указателя, для которого определено преобразование типа по умолчанию. Пример:

void* buf; //Определение указателя buf типа void*.

int a = 5;

double k = 5.5;

buf = &a; /*Преобразование типа по умолчанию. На самом деле выполняется выражение buf = (void*)&a; компилятор по умолчанию добавляет (void*). В buf записывается адрес переменной а. И теперь только программист знает, что buf указывает на объект типа int. */

cout<<*buf; /*ошибка, операция разыменования не определена над указателями типа void*/

cout<<(*(int*) buf); /*Явное преобразование типа. Создаётся неименованный указатель типа int, который инициализируется адресом, хранящимся в buf. К неименованному указателю int* применяется операция разыменования и на экране печатается значение переменной a.*/

buf = &k; /*Преобразование типа по умолчанию. В buf записывается адрес переменной k.*/

cout<<*(double*)buf;//На экране печатается значение переменной k.

cout<<*(int*) buf; /*На экране печатается неизвестно, что, так как buf в данный момент указывает на объект типа double, а не на объект типа int.*/

double * pd;// определение указателя pd типа double*

pd = buf; /*Ошибка. Операция неявного преобразования типа над указателями типа double* не определена. Компилятор выдаст сообщение «Не могу переконвертировать void* в double*.*/

pd = (double*) buf; //Ошибки нет. Явное преобразование типа.

cout<< *pd; //Печать значения переменной k

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

Таблица 5.1

Способы определения указателя

type* имя_указателя = инициализатор; ни указатель, ни объект, на который он указывает, константами не являются, их можно изменять int i = 1; int* pi = &i; pi = 2; /* операция разрешена, pi указывает не на константу; pi = pi +1; операция разрешена, pi не константа */  
type* const имя_указателя = инициализатор; указатель является константой т.е. его нельзя перестроить на другой объект; объект, на который настроен указатель, изменить можно int i = 1; int* const pi = &i; *pi = 2; ошибки нет, значение переменной i изменено с помощью указателя pi (i = 2); pi = pi +1; ошибка, значение указателя константа, его изменить нельзя
type const* имя_указателя = инициализатор; указатель не является константой; объект, на который настроен указатель, изменить нельзя он константа int i = 1; int const * pi = &i; *pi = 2; ошибка, значение переменной i изменить с помощью указателя нельзя; pi = pi +1; операция разрешена pi не константа
type const * const имя_указателя = инициализатор; указатель и объект, на который он указывает являются константами их изменить нельзя int i = 1; int const* const pi = &i; *pi = 2; ошибка, значение переменной i изменить с помощью указателя нельзя; pi = pi +1; ошибка, pi константа

 

 



Дата добавления: 2020-12-11; просмотров: 571;


Поиск по сайту:

Воспользовавшись поиском можно найти нужную информацию на сайте.

Поделитесь с друзьями:

Считаете данную информацию полезной, тогда расскажите друзьям в соц. сетях.
Poznayka.org - Познайка.Орг - 2016-2024 год. Материал предоставляется для ознакомительных и учебных целей.
Генерация страницы за: 0.035 сек.