8.2  Указатели на объекты

При объявлении указателя на объект типа "type" необходимо указать тип значения, на которое ссылается указатель, и звёздочку (*) перед именем переменной (одну или несколько). Формат объявления указателя:

Спецификатор_типа  * описатель;

Спецификатор_типа задает тип объекта и может быть любого встроенного типа (например, int, char, float, double и т.д.) или типа, созданного пользователем (например, struct, union, class и т.д.). Спецификатором_типа может быть также ключевое слово void, в этом случае переменная может быть использована для ссылки на объект любого типа. Однако для того, чтобы можно было выполнить арифметические и логические операции над указателями или над объектами, на которые они указывают, необходимо при выполнении каждой операции явно определить тип объектов. Такие определения типов могут быть выполнены с помощью операции приведения типов.

Описатель – это имя переменной, объявленной как указатель.

Приведем примеры объявления указателей:

unsigned int * a; /* переменная  а  представляет собой указатель

на тип unsigned int (целые числа без знака) */

double * x;           /* переменная  х  указывает  на  тип  данных с плавающей  точкой  удвоенной  точности      */

char * ptr ;            /*  объявляется  указатель с именем ptr, который указывает на  переменную типа char */

double nomer;

void *addres;

addres = & nomer;

/* Переменная addres объявлена как указатель на объект любого типа.    

Поэтому ей можно присвоить адрес любого объекта (& – операция  взятия адреса). Однако ни одна арифметическая операция не может быть выполнена над указателем,  пока  не будет явно определен тип данных,  на которые он указывает. Это  можно сделать,  используя операцию приведения типа (double *) для преобразования addres к указателю на тип double, а затем увеличение адреса. */

(double ) addres ++; /* приведение к типу  double */

unsigned char * const w = &obj.

/* Переменная w объявлена как константный указатель на данные типа unsigned char.  Это означает, что на протяжение всей программы  w  будет указывать на одну и ту же область памяти.  Содержание же  этой области может быть изменено. */

Таким образом, если type есть любой предопределенный или определенный пользователем тип, включая void, то объявление

type *ptr;      /* Опасно – неинициализированный указатель */

объявит ptr как "указатель на тип type". К объявленному таким образом объекту ptr применимы все правила, связанные с контекстом, продолжительностью и видимостью.

Перед использованием указателя (как и любой переменной) его необходимо инициализировать. При инициализации указателя ему в действительности присваивается некоторый адрес. В ряде заголовочных файлов (например, stdio.h) определена константа NULL, обозначающая условный нулевой адрес, которая также может быть значением указателя.

Рассмотрим следующую программу, иллюстрирующую различные способы инициализации указателей:

#include<stdlib.h>

#include <stdio.h>

void main(void)

{

//  Объявление переменных:

int data=101;

float summa=123.321E-6;

//  Объявление не инициализированных указателей:

int *ui,*vi;

float *uf,*a;

//  Способы инициализации указателей:

char *letter=NULL;  //  При инициализации указателя на строку;

char *m=”строка символов”;

ui=&data;                 // Присвоение указателю адреса переменной;

uf=&summa

vi=ui;                        // Присвоение указателю адреса другого указателя;

a=(float*)(calloc(10,sizeof(float))); // Выделение динамической памяти

//   указателю;

//  Вывод адреса указателя и содержимого по этому адресу:

printf(“Адрес data – %pnЗначение указателя *ui- %pnЗначение по адресу указателя *ui – %d”, &data, ui, *ui );

printf(“Адрес summa – %pnЗначение указателя *uf- %pnЗначение по адресу указателя *uf – %f”, &summa, uf, *uf);

printf(“Значение указателя letter – %p; Значение m=%s”, letter,m);

}

Из этой программы видно:% объявленным указателям ui, uf присваиваются адреса переменных data, summa соответствующего типа, т.е. выполняется инициализация указателей. Указателю letter присваивается нулевой адрес. Указатель m инициализируется при объявлении, указатель vi получает адрес от другого указателя. Наконец, для создания одномерного массива из 10 элементов можно воспользоваться динамическим распределением памяти (для создания массива а[10] использована функция calloc()).    

Для доступа к значению, на который указывает указатель, используется операция звёздочка (*). Используя данную операцию можно прочитать или записать значение, на которое указывает указатель. Например:

#include <stdio.h>

void main(void)

{

int data=101;

int *iptr;           // объявлен указатель типа int

iptr=&data;

printf(“Адрес в iptr – %pnЗначение по указателю – %dn”, iptr, *iptr);

*iptr=777;        // изменение значения по указателю

printf(“Новое значение по указателю iptr – %dn”, *iptr);

}

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

Поскольку указатель сам по себе является объектом, то можно установить указатель на указатель (и т.д.). В языке С (но не в С++) можно назначить указатель void* на указатель, не имеющий тип void*. Указатель типа "указатель на void" не следует путать с нулевым (NULL) указателем.

Предложение

void *vptr;

объявляет, что vptr – это родовой указатель, которому может быть присвоено любое значение "указатель на тип type" без выдачи компилятором сообщений.

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

double  mas[5] = { 5.02, 12.4, -1.12, 0.35, 8.12};

const double *ptr = mas; // ptr указывает на начало массива mas;

Указатель ptr нельзя использовать для изменения значений массива:

*ptr = 10.04; // Недопустимо;

mas[0] = 10.04; // Допускается;

Однако можно заставить указатель ptr указывать на другой элемент массива. Например:

ptr ++;  // Перемещение указателя ptr к элементу массива mas[1] допустимо;

Указателю на константу можно присваивать адрес как постоянных, так и модифицируемых данных. Например:

double  mas[5] = { 5.02, 12.4, -1.12, 0.35, 8.12};  // Модифицируемые данные

const double array[4] = { 0.08, 0.075, 0.070, 0.065}; // Немодифицируемые данные

const double *ptr = mas; // Допустимо;

ptr = array;                        //  Допустимо;

ptr = &mas[3];                   //  Допустимо;

Однако обычным указателям (без ключевого слова const) могут присваиваться адреса только данных, не являющихся константами. Например:

double  mas[5] = { 5.02, 12.4, -1.12, 0.35, 8.12};  // Модифицируемые данные

const double array[4] = { 0.08, 0.075, 0.070, 0.065}; // Немодифицируемые данные

double *p = mas; // Допустимо;

p = array;              // Недопустимо;

p = mas+3;           // Допустимо;

Можно объявить и инициализировать указатель так, чтобы он не мог указывать на какой-либо другой объект. Все дело в позиции ключевого слова const. Например:

double  mas[5] = { 5.02, 12.4, -1.12, 0.35, 8.12};  // Модифицируемые данные

double * const p = mas; // p указывает на начало массива;

p = &mas[3];           // Недопустимо;

*p = 10.04;              //  Допустимо, изменяет mas[0];

Такой указатель может использоваться для изменения значений, но он может указывать только на первоначально присвоенный ему адрес.

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

double  mas[5] = { 5.02, 12.4, -1.12, 0.35, 8.12};  // Модифицируемые данные

const double * const p = mas; // p указывает на начало массива;

p = &mas[3];           // Недопустимо;

*p = 10.04;              //  Недопустимо изменять mas[0];

Кроме модификатора «постоянство» (const) имеется модификатор «изменяемость» (volatile), который указывает компилятору, что значение переменной может изменяться с помощью средств, которые не относятся к программе. Например, адрес может содержать текущее время или использоваться для приема данных от другого компьютера. Синтаксис объявления такой же, как и модификатора const. Например:

volatile int loc;  // объявлена переменная, изменяемая аппаратными

// средствами компьютера

volatile int *ploc;  // объявлен указатель на переменную типа int, адрес которой

// может меняться аппаратными средствами компьютера

Значение может одновременно иметь модификаторы const и volatile. Например, установка системных часов обычно не должна меняться программой, но изменяется помимо программы. Например:

const volatile int loc

volatile const int *ploc

В языке С/С++ допустимы следующие операции над указателями:

· присваивание;

· приведение типов;

· получение адреса самого указателя;

· аддитивные операции;

· получение значения того объекта, на который ссылается указатель;

· операции сравнений.

Рассмотрим перечисленные операции подробней.

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

p=&mas[3];

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

char *z;           // указатель на символ

int *k;               // указатель на целое

z=(char *)k;    // преобразование указателя k

Подобно любым переменным переменная типа указатель имеет имя, собственный адрес в памяти и значение. Значение можно использовать, например, печатать или присваивать другому указателю. Адрес указателя может быть получен с помощью всё той же операции &. Выражение &имя_указателя определяет, по какому адресу памяти размещён указатель. Содержимое этого участка памяти является значением указателя.

С помощью унарных операций «++» и «—» арифметические значения указателей меняются по-разному в зависимости от типа данных, с которыми связаны эти указатели. Если указатель связан с типом char (тип требует 1 байт памяти), то при выполнении операций «++» и «—» его числовое значение изменяется на 1. Если указатель связан с типом int (требует 4 байта памяти), то операторы i++, ++i, —k, k – изменяют числовые значения указателей на 4. Указатель, связанный с типом float или long (оба типа требуют по 4 байта памяти), операциями «++» или «—» изменяется также на 4. Таким образом, при изменении указателя на единицу указатель будет ссылаться на следующую (или предыдущую) ячейку памяти той длины, которую задаёт тип данных.

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

В отличие от операции сложения операция вычитания применима не только к указателю и целой величине, но и к двум указателям на объекты одного типа. С её помощью можно находить разность (причём со знаком) двух указателей и, тем самым, определять «расстояние» между размещением в памяти двух объектов. При этом «расстояние» вычисляется в единицах, кратных длине отдельного элемента данных того типа, на который ссылается указатель. Например, если ptr1 указывает на третий элемент массива, а ptr2 на десятый, то результатом выполнения вычитания ptr2-ptr1 будет 7.

Добавление целочисленного значения n к указателю, адресующему некоторый элемент массива, приводит к тому, что указатель получает значение адреса того элемента, который отстоит от текущего на n элементов. Если длина элемента массива равна d байтов, то численное значение указателя изменяется на (d*n). Когда с "указателем на тип type" выполняется операция сложения или вычитания целого числа, то результат также будет "указателем на тип type".

Операция звёздочка (*) позволяет получить значение того объекта, на который указывает указатель.

К указателям применяются операции сравнения; «>», «>=», «!=», «= =», «<=», «<». Таким образом, указатели можно использовать в отношениях. Но сравнивать указатели допустимо только с другими указателями того же типа или с константой NULL, обозначающей значение условного нулевого адреса.

В число прочих объектов, на которые обычно устанавливается указатель, входят массивы, структуры, объединения и классы.