Указатели и адреса

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

Унарный оператор & выдает адрес объекта, так что инструкция

p=&c;

присваивает адрес ячейки с переменной p. Говорят, что р указывает на с. Унарный оператор * есть оператор раскрытия ссылки. Примененный к указателю он выдает объект, на который данный указатель ссылается. Предположим, что х и у - целые, а ip – указатель на int. Рассмотрим пример.

int x=1,y=2,z[10];

int *ip; // ip – указатель на int

ip=&x; // теперь ip указывает на х

y=*ip; // y теперь равен 1

*ip=0; // x теперь равен 0

ip=&z[0]; // ip теперь указывает на z[0]

Функции в языке Си в качестве своих аргументов получают значения параметров. Поэтому прямой возможности, находясь в вызванной функции, изменить переменную вызывающей функции нет. Например, в программе сортировки может понадобится функция swap, переставляющая местами два неупорядоченных элемента. Для их перестановки недостаточно написать swap(a,b); где функция swap определена следующим образом:

void swap(int x, int y) // НЕВЕРНО

{

int temp; temp=x;

x=y; y=temp;

}

Поскольку swap получает лишь копии значений переменных a и b, она не может повлиять на переменные a,b той программы, которая к ней обратилась. Чтобы получить желаемый эффект, надо вызывающей программе передать указатели на те значения, которые должны быть изменены: swap(&a,&b); Так как оператор & получает адрес переменной, &a – есть указатель на а. В самой функции swap параметры должны быть описаны как указатели, при этом доступ к значениям параметров будет осуществляться через них косвенно.

void swap(int *px, int *py)

{

int temp;

temp=*px;

*px=*py;

*py=temp;

}

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

double *dp, atof(char *);

означает, что выражение *dp и atof(s) имеют тип double, а аргумент функции atof есть указатель на char. Полезно знать, что указателю разрешено ссылаться только на объекты заданного типа. Существует одно исключение, указатель на void может ссылаться на объекты любого типа, но к такому указателю нельзя применять оператор раскрытия ссылки. Если ip ссылается на х целого типа, то *ip можно использовать в любом месте, где допустимо применение х. Например, оператор

*ip = *ip + 10;

увеличивает содержимое ячейки с адресом ip на 10. Унарные операторы * и & имеют более высокий приоритет, чем арифметические операторы. Указатели сами являются переменными. В тексте программы они могут встречаться и без оператора раскрытия ссылки. Например, если iq есть указатель на int, то можно использовать оператор

i p = iq;

который копирует содержимое iq в ip, чтобы iq и ip ссылались на один и тот же объект.

Рассмотрим связь указателей и массивов. Декларация

int a[10];

определяет массив а размера 10, то есть блок из 10 последовательных объектов с именами а[0],a[1],…a[9]. Запись a[i] отсылает к i –у элементу массива. Если pa есть указатель на int, то есть определен как

int *pa;

то в результате присваивания

pa = &a[0];

pa будет указывать на нулевой элемент а, иначе говоря, будет содержать адрес элемента а[0]. Между индексированием и арифметикой с указателями существует очень тесная связь. По определению имя переменной или выражения типа массив есть адрес нулевого элемента массива. После присваивания

ра = &a[0];

рa и a имеют одно и то же значение. Поскольку имя массива есть не что иное, как адрес его начального элемента, присваивание pa=&a[0 ]; можно также записать в следующем виде: pa=a; Интересно знать, что a[i] можно записать как *(a+i).

В языке Си++ используется понятие адреса переменных. Работа с адресами досталась Си++ в наследство от языка Си. Предположим, что в программе определена переменная типа int:

int x;

Можно определить переменную типа "указатель" на целое число:

int* xp;

и присвоить переменной xpt адрес переменной x:

xp = &x;

Операция &, примененная к переменной, – это операция взятия адреса. Операция *, примененная к адресу, – это операция обращения по адресу. Таким образом, два оператора эквивалентны:

int y = x; // присвоить переменной y значение xint y = *xp; /* присвоить переменной y значение, находящееся по адресу xp */

С помощью операции обращения по адресу можно записывать значения:

*xp = 10; // записать число 10 по адресу xp

После выполнения этого оператора значение переменной x станет равным 10, поскольку xp указывает на переменную x.

Указатель – это не просто адрес, а адрес величины определенного типа. Указатель xp – адрес целой величины. Определить адреса величин других типов можно следующим образом:

unsigned long* lP; // указатель на целое число без знака char* cp; // указатель на байт Complex* p; // указатель на объект класса Complex

Если указатель ссылается на объект некоторого класса, то операция обращения к атрибуту класса вместо точки обозначается "->", например, p->real. Например, обратимся к операции сложения двух комплексных чисел:

Void сomplex::аdd(сomplex x){ this->real = this->real + x.real; this->imaginary = this->imaginary + x.imaginary;}

Здесь this – это указатель на текущий объект, т.е. объект, который выполняет метод add.

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

Для чего нужны указатели? Указатели появились, прежде всего, для нужд системного программирования. Поскольку язык Си предназначался для "низкоуровневого" программирования, на нем нужно было обращаться, например, к регистрам устройств. У этих регистров вполне определенные адреса, т.е. необходимо было прочитать или записать значение по определенному адресу. Благодаря механизму указателей, такие операции не требуют никаких дополнительных средств языка.

int* hardwareRegiste =0x80000;*hardwareRegiste =12;

Однако использование указателей нуждами системного программирования не ограничивается. Указатели позволяют существенно упростить и ускорить ряд операций.

Упомянутые примеры использования указателей никак не связаны с объектно-ориентированным программированием. Казалось бы, объектно-ориентированное программирование должно уменьшить зависимость от низкоуровневых конструкций типа указателей. На самом деле программирование с классами нисколько не уменьшило потребность в указателях, и даже наоборот, нашло им дополнительное применение.

Адресная арифметика

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

int x = 10;int y = 10;int* xptr = &x; // указатель на хint* yptr = &y; // указатель на y // сравниваем указатели if (xptr == yptr) { cout << "Указатели равны" << endl;} else { cout << "Указатели не равны" << endl;}// сравниваем значения, на которые указывают указатели if (*xptr == *yptr) { cout << "Значения равны" << endl;} else { cout << "Значения неравны" << endl;}

Кроме того, над указателями можно выполнять ограниченный набор арифметических операций. К указателю можно прибавить целое число или вычесть из него целое число. Результатом прибавления к указателю единицы является адрес следующей величины типа, на который ссылается указатель, в памяти. Поясним это на рисунке. Пусть xPtr – указатель на целое число типа long, а cp – указатель на тип char. Начиная с адреса 1000, в памяти расположены два целых числа. Адрес второго — 1004 (в большинстве реализаций Си++ под тип long выделяется четыре байта). Начиная с адреса 2000, в памяти расположены объекты типа char.

Размер памяти, выделяемой для числа типа long и для char, различен. Поэтому адрес при увеличении xPtr и cp тоже изменяется по-разному. Однако и в том, и в другом случае увеличение указателя на единицу означает переход к следующей в памяти величине того же типа. Прибавление или вычитание любого целого числа работает по тому же принципу, что и увеличение на единицу. Указатель сдвигается вперед (при прибавлении положительного числа) или назад (при вычитании положительного числа) на соответствующее количество объектов того типа, на который показывает указатель. Адрес просто увеличивается или уменьшается на необходимую величину. На самом деле значение указателя ptr всегда изменяется на число, кратное sizeof(*ptr).

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

Связь между массивами и указателями

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

long array[100];long sum = 0;for (int i = 0; i < 100; i++) sum += array[i];

То же самое можно сделать с помощью указателей:

long array[100];long sum = 0;for (long* ptr = &array[0]; ptr < &array[99] + 1; ptr++) sum += *ptr;

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

for (long* ptr = array; ptr < &array[99] + 1; ptr++) sum += *ptr;

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

Нулевой указатель

В языке Си++ определена символическая константа NULL для обозначения нулевого значения указателя.

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


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



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