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

Пусть в программе определена некоторая переменная, например,

int a = 10;

Компилятор выделяет память в соответствии с типом (int) и инициализирует переменную указанным значением (10). Все обращения к переменной в программе по ее имени (а) заменяются компилятором на адрес области памяти, в которой хранится значение переменной.

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

- указатели на объект,

- указатели на функцию,

- указатели на void.

Указатели разных типов отличаются свойствами и набором допустимых операций.

Указатель на функцию содержит адрес в сегменте кода, по которому располагается исполняемый код функции, то есть адрес, по которому передается управление при вызове функции. Указатели на функции используются для косвенного вызова функции (не через ее имя, а через обращение к переменной, хранящей ее адрес), а также для передачи имени функции в другую функцию в качестве параметра. Указатель функции имеет тип “указатель функции, возвращающей значение заданного типа и имеющей аргументы заданного типа”:

тип (*имя) (список_формальных_параметров);

Например, объявление

int (*fun) (double, double);

задает указатель с именем fun на функцию, возвращающую значение типа int и имеющую два аргумента типа double.

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

тип *имя;

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

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

int *a, b, *c;

Здесь описываются два указателя на целое с именами a и c, а также целая переменная b.

Размер указателя зависит от модели памяти. Адрес в общем случае занимает 4 байта и хранится как два слова: одно слово определяет сегмент, другое – смещение по сегменту.

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

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

Указатель может быть константой или переменной, а также указывать на константу или переменную.

Примеры.

int i;                                            // целая переменная

const int c1 = 1;                          // целая константа

int *p;                                          // указатель на целую переменную

const int *pi;                                // указатель на целую константу

int *const cp = &i;                       // указатель-константа на целую переменную

const int *const cpc = &c1;         // указатель-константа на целую константу

Как видно из примеров, модификатор const, находящийся между именем указателя и звездочкой, относится к самому указателю и запрещает его изменение, а const, стоящий слева от звездочки, задает постоянство значения, на которое он указывает.

Для инициализации указателей использована операция взятия адреса &.

Инициализация указателей

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

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

Рассмотрим способы инициализации указателей.

1) Присваивание указателю адреса существующего объекта:

- с помощью операции получения адреса

int a = 5;

int *p = &a; // в указатель записывается адрес а

int *p (&a);   // то же самое другим способом

                     - с помощью значения другого инициализированного указателя

                          int *r = p;

                     - с помощью имени массива или функции, которые трактуются как адрес

                           int b[10];   // массив

                           int *t = b;   // присваивание адреса начала массива

                            …

                            void f(int a) {/* …*/}  // определение функции

                            void (*pf) (int);              // указатель на функцию

                            pf = f;                             // присваивание адреса функции

2) Присваивание указателю адреса области памяти в явном виде:

char *vp = (char *)0×B8000000;

Здесь 0×B8000000 – шестнадцатеричная константа, (char *) – операция приведения типа: константа преобразуется к типу “указатель на char”.

3) Присваивание указателю пустого значения или NULL.

int * u1 = NULL;

int * u2 = 0;

NULL – это пустой указатель, указатель, равный 0, заготовка для ключа, который похож на ключ, но им невозможно открыть ящик с динамической переменной. Рекомендуется в С++ использовать просто 0, так как это значение типа int будет правильно преобразовано в соответствии с контекстом. Объектов с нулевым адресом нет, это гарантируется, поэтому пустой указатель можно использовать для проверки, ссылается указатель на конкретный объект или нет.

4) Выделение памяти в куче и присваивание адреса первого байта этой памяти указателю.    

- с помощью операции new (C++)

int *n = new int;                  // 1

float *t = new float(25.172) // 2

int *m = new int (10);         // 3

int *q = new int [10];           // 4

В операторе 1 new выполняет выделение достаточного для размещения величины типа int участка динамической памяти и записывает адрес первого байта этого участка в переменную n. Память под указатель n выделяется на этапе компиляции.

В операторах 2 и 3 кроме описанных выше действий производится инициализация динамических переменных.

В операторе 4 new выполняет выделение памяти под 10 величин типа int (массив из 10 элементов) и записывает адрес начала этого участка в переменную q, которую можно трактовать как имя массива. Через имя можно обращаться к любому элементу массива обычным образом.

Освобождение памяти, выделенной с помощью операции new, должно выполняться с помощью операции delete. При этом переменная-указатель сохраняется и может инициализироваться повторно.

Например, освобождение памяти может происходить так:

delete n; delete m; delete []q; free (u);

Если память выделялась как new[], то освобождать ее надо как delete [], при этом размерность массива не указывается. Если квадратных скобок нет, то ошибок не выдается, а помечен как свободный будет только первый элемент массива, а остальные элементы станут мусором, так как доступа к ним не будет.

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

int *( *p[10])( ); //

Здесь объявляется массив из 10 указателей на функции без параметров, возвращающих указатели на int.

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

При интерпретации сложных описаний придерживаются правила “изнутри наружу”:

- если справа от имени есть квадратные скобки, это массив, если круглые – это функция,

- если слева есть звездочка, это указатель на проинтерпретированную ранее конструкцию,

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

- в последнюю очередь интерпретируется спецификатор типа.

 

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

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

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

Пример

#include <iostream>

int main ( )

{ int x = 1;

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

uk = &x; // положили в переменную-указатель адрес переменной x

x *= 2;   // x = x * 2;

*uk *= 2;   // удвоение х путем использования указателя на х

*uk = *uk + 10; // увеличение *uk, а следовательно и x на 10

// отобразить значение переменной x на экране можно двумя способами

cout << *uk << endl;    // через указатель

cout << x << endl;        // непосредственно

return 0;

}

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

Инкремент перемещает указатель к следующему элементу массива, декремент – к предыдущему. Фактически значение указателя изменяется на величину sizeof(тип).

Разность двух указателей – это разность их значений, деленная на размер типа в байтах ( в применении к массивам разность указателей, например, на третий и шестой элементы равна трем). Суммирование двух указателей не допускается.

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

Пример

*p++ = 10;

Операции разыменования и инкремента имеют одинаковый приоритет. Инкремент постфиксный, он выполняется после выполнения операции присваивания. Таким образом, сначала по адресу, записанному в указателе p, будет записано значение 10, а затем указатель будет увеличен на количество байт, соответствующее его типу. То есть,

*p = 10; p++;

Выражение (*p)++, напротив, инкрементирует значение, на которое ссылается указатель.

Указатели и массивы

В языке С++ связь между указателями и массивами настолько тесная, что программисты чаще предпочитают работать с массивами путем использования указателей. В С/C++ имя массива – это его адрес, который также является адресом его первого элемента.

Мы работали с массивом, индексируя его.

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

ukm        ukm + 1     ukm + 2     

 

m[0] m[1] m[2] m[3] m[4]

            m

 

int m[5];  // // объявляется массив из 5 элементов

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

ukm = &m[0]; // ukm будет указывать на нулевой элемент массива m или

                       // ukm будет содержать адрес элемента m[0]

Таким образом, в определенном смысле выражение m[0] является аналогом разыменованного указателя ukm.

Если ukm указывает на некоторый элемент массива, то ukm + 1 указывает на следующий элемент, ukm + i – на i-тый после ukm, ukm – i - на i-тый элемент перед ukm. Другими словами, если ukm указывает на m[0], то

*(ukm+i) – есть содержимое m[i]. Приоритет операции * выше, чем операции +, поэтому скобки необходимы.

Тип и размер элементов массива при этом не имеют значение, так как мы говорим здесь о памяти, занимаемой объектом массив. Смысл слов “добавить единицу к указателю” заключается в том, что переменная-указатель будет указывать на следующий элемент объявленного типа, а не на следующую ячейку памяти.

Под массив компилятор выделяет кусок памяти и сопоставляет ему идентификатор m. m – это указатель-константа, который содержит адрес нулевого элемента массива m, изменять этот адрес вы не можете, но разыменовывать указатель-константу вы, разумеется, можете.

Результаты выражений m[i], *(m+i), *(ukm+i) будут одинаковы. Однако можно написать выражение *(ukm++), но нельзя написать *(m++) именно потому, что ukm – это переменная-указатель, а m – это указатель-константа.

Пример

#include <iostream>

const int M = 10;

int mas[M];                     // глобальный массив

 

int main ()

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

ukm = mas;

for (int i = 0; i < M; i++)

mas[i] = i;            // инициализация элементов массива с помощью индексов

for (int i = 0; i < M; i++)

cout << *ukm++ << ‘ ‘; // вывод значений массива с помощью указателей

cout << endl;

for (int i = 0; i < M; i++)

{ ukm = &mas[i];

   cout << *ukm << ‘ ‘;

}

cout << endl;

for (int i = 0; i < M; i++)

cout << *(mas+i) << ‘ ‘;     

cout << endl;

return 0;

}

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

Указатели можно сравнивать с помощью выражений вида (p1 < p2) или (p1 <= p2). Однако эти операторы сравнения работают корректно только при сравнении ближних указателей. В случае дальних указателей гарантировать правильность работы этих выражений нельзя.

Дата: 2019-04-23, просмотров: 188.