Адресная арифметика, типы указателей и операции над ними
Операции, определенные над указателями:
операция разыменования или доступа по адресу (*);
преобразования типов или приведения типов (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 = π
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;