3.3. Указатели
3.3. Указатели
Указатели и динамическое выделение памяти были вкратце представлены в разделе 2.2. Указатель – это объект, содержащий адрес другого объекта и позволяющий косвенно манипулировать этим объектом. Обычно указатели используются для работы с динамически созданными объектами, для построения связанных структур данных, таких, как связанные списки и иерархические деревья, и для передачи в функции больших объектов – массивов и объектов классов – в качестве параметров.
Каждый указатель ассоциируется с некоторым типом данных, причем их внутреннее представление не зависит от внутреннего типа: и размер памяти, занимаемый объектом типа указатель, и диапазон значений у них одинаков . Разница состоит в том, как компилятор воспринимает адресуемый объект. Указатели на разные типы могут иметь одно и то же значение, но область памяти, где размещаются соответствующие типы, может быть различной:
* указатель на int, содержащий значение адреса 1000, направлен на область памяти 1000-1003 (в 32-битной системе);
* указатель на double, содержащий значение адреса 1000, направлен на область памяти 1000-1007 (в 32-битной системе).
Вот несколько примеров:
int *ip1, *ip2;
complexdouble *cp;
string *pstring;
vectorint *pvec;
double *dp;
Указатель обозначается звездочкой перед именем. В определении переменных списком звездочка должна стоять перед каждым указателем (см. выше: ip1 и ip2). В примере ниже lp – указатель на объект типа long, а lp2 – объект типа long:
long *lp, lp2;
В следующем случае fp интерпретируется как объект типа float, а fp2 – указатель на него:
float fp, *fp2;
Оператор разыменования (*) может отделяться пробелами от имени и даже непосредственно примыкать к ключевому слову типа. Поэтому приведенные определения синтаксически правильны и совершенно эквивалентны:
string *ps;
string* ps;
Однако рекомендуется использовать первый вариант написания: второй способен ввести в заблуждение, если добавить к нему определение еще одной переменной через запятую:
//внимание: ps2 не указатель на строку!
string* ps, ps2;
Можно предположить, что и ps, и ps2 являются указателями, хотя указатель – только первый из них.
Если значение указателя равно 0, значит, он не содержит никакого адреса объекта.
Пусть задана переменная типа int:
int ival = 1024;
Ниже приводятся примеры определения и использования указателей на int pi и pi2:
//pi инициализирован нулевым адресом
int *pi = 0;
// pi2 инициализирован адресом ival
int *pi2 = ival;
// правильно: pi и pi2 содержат адрес ival
pi = pi2;
// pi2 содержит нулевой адрес
pi2 = 0;
Указателю не может быть присвоена величина, не являющаяся адресом:
// ошибка: pi не может принимать значение int
pi = ival
Точно так же нельзя присвоить указателю одного типа значение, являющееся адресом объекта другого типа. Если определены следующие переменные:
double dval;
double *ps = dval;
то оба выражения присваивания, приведенные ниже, вызовут ошибку компиляции:
// ошибки компиляции
// недопустимое присваивание типов данных: int* == double*
pi = pd
pi = dval;
Дело не в том, что переменная pi не может содержать адреса объекта dval – адреса объектов разных типов имеют одну и ту же длину. Такие операции смешения адресов запрещены сознательно, потому что интерпретация объектов компилятором зависит от типа указателя на них.
Конечно, бывают случаи, когда нас интересует само значение адреса, а не объект, на который он указывает (допустим, мы хотим сравнить этот адрес с каким-то другим). Для разрешения таких ситуаций введен специальный указатель void, который может указывать на любой тип данных, и следующие выражения будут правильны:
// правильно: void* может содержать
// адреса любого типа
void *pv = pi;
pv = pd;
Тип объекта, на который указывает void*, неизвестен, и мы не можем манипулировать этим объектом. Все, что мы можем сделать с таким указателем, – присвоить его значение другому указателю или сравнить с какой-либо адресной величиной. (Более подробно мы расскажем об указателе типа void в разделе 4.14.)
Для того чтобы обратиться к объекту, имея его адрес, нужно применить операцию разыменования, или косвенную адресацию, обозначаемую звездочкой (*). Имея следующие определения переменных:
int ival = 1024;, ival2 = 2048;
int *pi = ival;
мы можем читать и сохранять значение ival, применяя операцию разыменования к указателю pi:
// косвенное присваивание переменной ival значения ival2
*pi = ival2;
// косвенное использование переменной ival как rvalue и lvalue
*pi = abs(*pi); // ival = abs(ival);
*pi = *pi + 1; // ival = ival + 1;
Когда мы применяем операцию взятия адреса () к объекту типа int, то получаем результат типа int*
int *pi = ival;
Если ту же операцию применить к объекту типа int* (указатель на int), мы получим указатель на указатель на int, т.е. int**. int** – это адрес объекта, который содержит адрес объекта типа int. Разыменовывая ppi, мы получаем объект типа int*, содержащий адрес ival. Чтобы получить сам объект ival, операцию разыменования к ppi необходимо применить дважды.
int **ppi = pi;
int *pi2 = *ppi;
cout "Значение ival "
"явное значение: " ival " "
"косвенная адресация: " *pi " "
"дважды косвенная адресация: " **ppi " "
endl;
Указатели могут быть использованы в арифметических выражениях. Обратите внимание на следующий пример, где два выражения производят совершенно различные действия:
int i, j, k;
int *pi = i;
// i = i + 2
*pi = *pi + 2;
// увеличение адреса, содержащегося в pi, на 2
pi = pi + 2;
К указателю можно прибавлять целое значение, можно также вычитать из него. Прибавление к указателю 1 увеличивает содержащееся в нем значение на размер области памяти, отводимой объекту соответствующего типа. Если тип char занимает 1 байт, int – 4 и double – 8, то прибавление 2 к указателям на char, int и double увеличит их значение соответственно на 2, 8 и 16. Как это можно интерпретировать? Если объекты одного типа расположены в памяти друг за другом, то увеличение указателя на 1 приведет к тому, что он будет указывать на следующий объект. Поэтому арифметические действия с указателями чаще всего применяются при обработке массивов; в любых других случаях они вряд ли оправданы.
Вот как выглядит типичный пример использования адресной арифметики при переборе элементов массива с помощью итератора:
int ia[10];
int *iter = ia[0];
int *iter_end = ia[10];
while (iter != iter_end) {
do_something_with_value (*iter);
++iter;
}
Упражнение 3.8
Даны определения переменных:
int ival = 1024, ival2 = 2048;
int *pi1 = ival, *pi2 = ival2, **pi3 = 0;
Что происходит при выполнении нижеследующих операций присваивания? Допущены ли в данных примерах ошибки?
(a) ival = *pi3; (e) pi1 = *pi3;
(b) *pi2 = *pi3; (f) ival = *pi1;
(c) ival = pi2; (g) pi1 = ival;
(d) pi2 = *pi1; (h) pi3 = pi2;
Упражнение 3.9
Работа с указателями – один из важнейших аспектов С и С++, однако в ней легко допустить ошибку. Например, код
pi = ival;
pi = pi + 1024;
почти наверняка приведет к тому, что pi будет указывать на случайную область памяти. Что делает этот оператор присваивания и в каком случае он не приведет к ошибке?
Упражнение 3.10
Данная программа содержит ошибку, связанную с неправильным использованием указателей:
int foobar(int *pi) {
*pi = 1024;
return *pi;
}
int main() {
int *pi2 = 0;
int ival = foobar(pi2);
return 0;
}
В чем состоит ошибка? Как можно ее исправить?
Упражнение 3.11
Ошибки из предыдущих двух упражнений проявляются и приводят к фатальным последствиям из-за отсутствия в С++ проверки правильности значений указателей во время работы программы. Как вы думаете, почему такая проверка не была реализована? Можете ли вы предложить некоторые общие рекомендации для того, чтобы работа с указателями была более безопасной?
Более 800 000 книг и аудиокниг! 📚
Получи 2 месяца Литрес Подписки в подарок и наслаждайся неограниченным чтением
ПОЛУЧИТЬ ПОДАРОКЧитайте также
Указатели
Указатели Указатель в С++ — это переменная, содержащая не сам объект, а адрес памяти, где располагается объект. Java и C# имеют аналогичную концепцию «ссылки» при другом синтаксисе. Мы начнем с рассмотрения придуманного нами примера, иллюстрирующего применение указателей:01
R.8.2.1 Указатели
R.8.2.1 Указатели В описании T D, в котором D имеет вид* список-спецификаций-cv opt D1тип описываемого идентификатора есть"… список-спецификаций-cv указатель на T". Конструкция список-спецификаций-cv относится к указателю, а не к указуемому объекту.Например, в описанияхconst ci = 10, *pc =
R.8.2.3 Указатели на члены
R.8.2.3 Указатели на члены В описании T D, в котором D имеет видполное-имя-класса :: * список-спецификаций-cv opt D1тип описываемого идентификатора есть "… список-спецификаций-cv указатель на член класса полное-имя-класса типа T".Например, во фрагментеclass X {public: void f(int); int a;};int X::* pmi =
12. Массивы и указатели
12. Массивы и указатели МАССИВЫ. МНОГОМЕРНЫЕ МАССИВЫ. ИНИЦИАЛИЗАЦИЯ МАССИВОВ. УКАЗАТЕЛИ И ОПЕРАЦИИ НАД УКАЗАТЕЛЯМИ. СВЯЗЬ МЕЖДУ МАССИВОМ И УКАЗАТЕЛЕМ. ОПЕРАЦИИ & * (унарные)Между массивами и указателями существует очень тесная связь, поэтому обычно их рассматривают
УКАЗАТЕЛИ МАССИВОВ
УКАЗАТЕЛИ МАССИВОВ Как было сказано в гл. 9, указатели позволяют нам работать с символическими адресами. Поскольку в реализуемых аппаратно командах вычислительной машины интенсивно используются адреса, указатели предоставляют возможность применять адреса
Указатели и строки
Указатели и строки Возможно, вы заметили периодическое упоминание указателей в нашем рассказе о строках. Большинство операции языка Си, имеющих дело со строками, работает с указателями. Например, рассморим приведенную ниже бесполезную, но поучительную программу /*
УКАЗАТЕЛИ НА СТРУКТУРЫ
УКАЗАТЕЛИ НА СТРУКТУРЫ Любители указателей будут рады узнать, что указатели можно использовать и для структур. Это хорошо по крайней мере по трем причинам. Во-первых, точно так же как указатели на массивы, они легче в использовании (скажем, в задаче сортировки), чем
3.3. Указатели
3.3. Указатели Указатели и динамическое выделение памяти были вкратце представлены в разделе 2.2. Указатель – это объект, содержащий адрес другого объекта и позволяющий косвенно манипулировать этим объектом. Обычно указатели используются для работы с динамически
7.9. Указатели на функции
7.9. Указатели на функции Предположим, что нам нужно написать функцию сортировки, вызов которой выглядит так:sort( start, end, compare );где start и end являются указателями на элементы массива строк. Функция sort() сортирует элементы между start и end, а аргумент compare задает операцию сравнения
2.3.7 Указатели и Вектора
2.3.7 Указатели и Вектора Указатели и вектора в С++ связаны очень тесно. Имя вектора можно использовать как указатель на его первый элемент, поэтому пример с алфавитом можно было написать так:char alpha[] = «abcdefghijklmnopqrstuvwxyz»; char* p = alpha; char ch;while (ch = *p++) cout «„ chr(ch) „« " = " «« ch «« « = 0“ ««
5.4.5 Указатели на Члены
5.4.5 Указатели на Члены Можно брать адрес члена класса. Получение адреса функции члена часто бывает полезно, поскольку те цели и причины, кторые приводились в #4.6.9 относительно указателей на фунции, в равной степени применимы и к функциям членам. Однако, на настоящее время в
5.4.5 Указатели на Члены
5.4.5 Указатели на Члены Можно брать адрес члена класса. Получение адреса функции члена часто бывает полезно, поскольку те цели и причины, кторые приводились в #4.6.9 относительно указателей на фунции, в равной степени применимы и к функциям членам. Однако, на настоящее время в
7.2.4 Указатели
7.2.4 Указатели Если производный класс derived имеет открытый базовый класс base, то указатель на derived можно присваивать перменной типа указатель на base не используя явное преобразовние типа. Обратное преобразование, указателя на base в указтель на derived, должно быть явным.
6.4 Указатели и Целые
6.4 Указатели и Целые Выражение целого типа можно прибавить к указателю или вычесть из него. В таком случае первый преобразуется, как указывается при обсуждении операции сложения.Можно производить вычитание над двумя указателями на объекты одного типа; в этом случае
Указатели мыши
Указатели мыши Механизм настройки мыши в Windows Vista остался таким же, как в Windows XP, поэтому рассмотрим возможности его настройки кратко.Для доступа к настройке параметров мыши запустите элемент Указатели мыши компонента Персонализация.Откроется окно, содержащее несколько