Пусть в программе определена некоторая переменная, например,
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, просмотров: 219.