Кратко об указателях в Си: присваивание, разыменование и перемещение по массивам
Время на прочтение
6 мин
Количество просмотров 49K
Приветствую вас, дорогие читатели. В данной статье кратко описаны основные сведения об указателях в языке Си. Кроме основных операций с указателями (объявление, взятие адреса, разыменование) рассмотрены вопросы безопасности типов при работе с ними. К сожалению, в данной статье вы не найдёте информацию по операциям сравнений указателей. Однако, статья будет полезна новичкам, а также тем, кто работает с массивами. Все примеры в данной статье компилировались компилятором gcc (восьмой версии).
Введение
Указатель – переменная, которая хранит адрес сущностей (т.е. других переменных любого типа, будь то структура, или массив), и над которой возможно выполнять операцию разыменования (dereferencing). Адрес обычно выражен целым положительным числом. Диапазон адресов зависит от архитектуры компьютера. Указателю надо указать тип переменной, адрес которой он хранит, или же использовать ключевое слово void, для обозначения указателя, хранящего адрес чего-угодно (т.е. разрешён любой тип). Указатели объявляются как и обычные переменные, с той разницей, что имя типа переменной указателя имеет префикс, состоящий как минимум из одной звёздочки (*). Например:
int a = 12; /* usual variable */
int * ptr = &a; /* ptr-variable which contains address of variable a */
int **pptr = &ptr; /* ptr-variable which contains address of variable ptr */
int aval = **pptr; /* get value by adress which is contained in pptr. */
int aval2 = *ptr; /* get value of a by address (value of ptr) */
Количество звёздочек лишь указывает на длину цепочек хранимых адресов. Поскольку указатель также является переменной и имеет адрес, то его адрес также можно хранить в другом указателе. В выше приведённом примере адрес переменной a сохраняется в переменной-указателе ptr. Адрес же самой переменной ptr сохраняется в другом указателе pptr. Чтобы получить адрес переменной, перед её именем надо поставить знак амперсанда (&). Наконец, чтобы выполнить обратную операцию, т.е. получить значение (содержимое) по адресу, хранимому в указателе, имя указателя предваряется звёздочкой, почти как при объявлении. Почти, потому что одной звёздочки достаточно чтобы “распаковать” указатель. Поскольку pptr указывает по адресу на значение, хранимое в ptr, то необходимо два раза применить операцию разыменования.
Указатели в предыдущем примере хранят адрес переменной определённого типа. В случае, когда применяются указатели типа void (любого типа), то прежде чем распаковать значение по адресу, необходимо выполнить приведение к типизированному указателю. Следующий пример является версией предыдущего, но с использованием указателя любого типа.
int b = 0xff;
void *pb = &b;
void **ppb = &pb;
int bval1 = *((int *) pb);
int bval2 = *((int *) *ppb);
В данном примере адреса хранятся в указателе типа void. Перед получением значения по адресу, хранимым в pb, необходимо привести указатель pb к типу int*. Затем, воспользоваться стандартной операцией разыменования. Что касается указателя ppb, то он разыменовывается два раза. Первый раз до приведения к типу, для получения содержимого переменной pb, на которую он указывает. Второй раз – после приведения к типу int*.
Изменения значения переменной через указатель.
Так как указатель хранит адрес переменной, мы можем через адрес не только получить значение самой переменной, но также его изменить. Например:
char a = 'x';
char *pa = &a; /* save address of a into pa */
*pa = 'y'; /* change content of variable a */
printf("%cn", a); /* prints: y */
Как было сказано выше, указатели хранят адреса. Естественно, что адреса могут указывать не только на ячейки данных переменных в вашей программе, но и на другие вещи: адрес стека процедур, адрес начала сегмента кода, адрес какой-то процедуры ядра ОС, адрес в куче и т. д. Логично, что не все адреса можно использовать напрямую в программе, поскольку некоторые из них указывают на те участки памяти, которые нельзя изменять (доступ для чтения), или которые нельзя затирать. В случае, при обращении к участку, доступному только для чтения, при попытке изменить значение получим ошибку Segmentation Fault (SF).
Кроме того, в языке Си определён макрос с именем NULL, для обозначения указателя с нулевым адресом. Данный адрес обычно используется операционной системой для сигнала об ошибке при работе с памятью. При попытке что либо читать по этому адресу, программа может получить неопределённое поведение. Поэтому ни в коем случае не пытайтесь извлечь значение по пустому указателю.
И ещё, указатели могут указывать на один и тот же объект. Например:
int a = 123;
int *p1 = &a;
//Теперь p2 хранит тот же адрес, что и p1.
int *p2 = &a;
*p1 -= 3; // a = 123 - 3.
printf("*p2 = %dn", *p2); //Выведет 120
Этот простой пример показывает, что через адреса можно менять содержимое простых переменных, а также остальных указателей, ссылающихся на тоже самое. Таким образом, указатель p2 как бы является псевдонимом (alias) для p1.
Передача параметров через указатели.
Параметры функций могут быть указателями. В случае вызова таких функций, они копируют значения аргументов в свои параметры как обычно. Единственное отличие здесь в том, что они копируют адреса, содержащиеся в указателях параметрах. И с помощью полученных адресов, можно изменять объекты, на которые указывают параметры. Ниже приведена стандартная процедура обмена значений между двумя целочисленными переменными.
int swap(int *a, int *b){
if(a == NULL || b == NULL)
return -1;
int temp = *a;
*a = *b;
*b = temp;
return 0;
}
Здесь переменные а и b меняются своими значениями друг с другом (при условии, что параметры содержат не нулевой адрес). Отметим ещё раз, что мы можем изменить содержимое, указываемое по параметру-указателю методов. И, конечно, мы можем стереть данный адрес, присвоив параметру новое значение.
Проверка типов и массивы
Как было сказано, указатели хранят адреса переменных. Несмотря на указание типа для переменной указателя, это не мешает присвоить ему адрес переменной другого типа, если вы компилируете БЕЗ флагов. Например, следующий код не скомпилируется, если вы включили флаги -Werror -Wall
.
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char **argv){
int *ptr = NULL;
float a = 23.2;
ptr = &a;
printf("%.1fn", *ptr);
return 0;
}
Конечно, компилятор gcc и без -Wall
заметит недопустимую операцию в 7 строке кода. Флаг -Wall
покажет все предупреждения компилятора. Главный флаг -Werror
не позволит компилировать код, если есть предупреждения.
Что же касается массивов, то для массива не нужно предварять имя переменной амперсандом, поскольку компилятор автоматически при присваивании адреса массива присвоит адрес первого его элемента в указатель. Для многомерных массивов потребуются указатели на массивы, а не массивы указателей. Первые имеют форму объявления вида int (*arr)[]
, а вторые вида int *arr[]
. В квадратных скобках обязательно нужно указать размер массива. Для трёхмерных массивов потребуется уже две пары скобок, например int (*arr)[2][2]
. Для четырёхмерных – три и так далее.
// В ПУСТОМ теле метода main.
int A[2] = {40, 20};
// A -> (int *) ptr to A[0] element, &A -> (int (*)[]) -> ptr to whole Array.
int *ptr = A;
printf("ptr -> A[1] = %dn", *(ptr + 1)); // A[1] => 20.
//Illegal usage of A.
// int a_2 = ++A; //expected lvalue.
//But with ptr you can do this.
int b_2 = *++ptr; //Now ptr contains address of A[1]. (b_2 = A[1]);
int (*ptr2)[2] = &A; //ptr to array, not to literal element.
//*ptr2 => get array.
//**ptr2 => get first element of array.
//*ptr2 + 1 => get address of second element of array.
printf("ptr2 -> A[1] = %dn", *( *ptr2 + 1) );
int M[2][2] = { {1, 2} , {3, 4} };
// (*mp)[k] => (*mp)[k] => mp[0][k].
int (*mp)[2] = M; //again you must not add '&' to variable M.
printf("M[0][0] = %dn", **mp);//get array and extract it first element
printf("M[1][0] = %dn", **(mp + 1));//move to the address of second element
printf("M[1][1] = %dn", *( *(mp + 1) + 1));
В выше приведённом коде даны примеры для работы с массивами (одномерными и двумерными). В квадратных скобках указывается размер последнего измерения. Важно помнить, что первое разыменование приводит вас ко всему массиву (т. е. к типу int *
). А второе разыменование распаковывает элемент данного массива. В случае одномерного массива, у нас всего одна ячейка, и указатель ссылается на неё. В случае двумерного массива, у нас две ячейки – массивы, а указатель ссылается на первую. Для перемещения на второй массив, достаточно прибавить единицу к адресу, хранимому в переменной mp, например, так mp + 1
. Чтобы получить первый элемент второго массива, надо два раза распаковать указатель с соответствующим адресом массива, т.е. **(mp + 1)
.
Постоянные (const) и указатели.
Напомним, чтобы сделать переменную с постоянным, фиксированным значением, надо добавить ключевое слово const перед её именем (до имени типа или после). Например:
const int i1 = 10;
int const i2 = 222;
// Warning: variable e3 is unitialized. With -Werror it won't be compiled.
// (Внимание: переменной e3 не присвоено значение. С флагом gcc -Werror
// данный код не скомпилируется).
// const int e3;
Для объявления указателя на постоянное значение, ключевое слово const должно быть ПЕРЕД звёздочкой.
int A[2] = {100, 200};
const int *a0 = A;
printf("content of a0 = %dn", *a0);
//*a0 *= 10; //error: cannot change constant value.
a0 = (A + 1); // A[1]
printf("content of a0 = %dn", *a0); //prints: A[1]
В примере выше была создана переменная-указатель, ссылающееся на постоянное значение. Слово const перед звёздочкой указывает, что нельзя менять содержимое напрямую (путём разыменования, обращения к ячейке). Но сама переменная указатель постоянной не является. А значит, ей можно присвоить новый адрес. Например, адрес следующей ячейки в массиве.
Чтобы запретить менять адрес (значение переменной) указателя, надо добавить слово const ПОСЛЕ звёздочки. Кроме того, можно добавить ключевые слова const перед и после '*'
, чтобы сделать переменную фиксированной ещё сильнее, например так:
// Переменная с постоянным адресом и постоянным содержимым.
const int *const ptr = A; // constant address with constant content
// Переменная с постоянным адресом (содержимое можно менять)
int *const ptr2 = A; // constant address only.
// Переменная с постоянным содержимым, но с изменяемым адресом (значение справа)
const int *ptr3 = A; // constant content only (can change address (rvalue))
Переменные, адреса и указатели
Переменная — это область памяти, к которой мы обращаемся за находящимися там данными, используя идентификатор (в данном случае, имя переменной). При этом у этой помеченной именем области есть еще и адрес, выраженный числом и понятный компьютерной системе. Этот адрес можно получить и записать в особую переменную. Переменную, содержащую адрес области памяти, называют указателем.
Когда мы меняем значение обычной переменной, то, можно сказать, просто удаляем из конкретной области памяти данные и записываем туда новые. Когда мы меняем значение переменной-указателя, то начинаем работать с совершенно иным участком памяти, т.к. меняем содержащийся в ней адрес.
Тема указателей тесно связана с темой динамических типов данных. Когда программа компилируется, то под объявленные переменные так или иначе (в зависимости от того, где они были объявлены) выделяются участки памяти. Потом размер этих участков не меняется, может меняться только их содержимое (значения или данные). Однако именно с помощью указателей можно захватывать и освобождать новые участки памяти уже в процессе выполнения программы. Динамические типы данных будут рассмотрены позже.
Прежде чем перейти к рассмотрению объявления и определения переменных-указателей, посмотрим, что из себя представляет адрес любой переменной и как его получить.
int i = 0; printf ("i=%d, &i=%p n", i, &i);
В результате выполнения данного программного кода на экране появляется примерно следующее (шестнадцатеричное число у вас будет другим):
i=0, &i=0x7fffa40c5fac
Знак амперсанда (&) перед переменной позволяет получить ее адрес в памяти. Для вывода адреса переменной на экран используется специальный формат %p
. Адреса обычных переменных (не указателей) в процессе выполнения программы никогда не меняются. В этом можно убедиться:
int a = 6; float b = 10.11; char c = 'k'; printf("%5d - %pn", a, &a); printf("%5.2f - %pn", b, &b); printf("%5c - %pn", c, &c); a = 2; b = 50.99; c = 'z'; printf("%5d - %pn", a, &a); printf("%5.2f - %pn", b, &b); printf("%5c - %pn", c, &c);
Результат:
6 - 0x7fff653532e0 10.11 - 0x7fff653532e4 k - 0x7fff653532df 2 - 0x7fff653532e0 50.99 - 0x7fff653532e4 z - 0x7fff653532df
Как мы видим, несмотря на то, что значения переменных поменялись, ячейки памяти остались прежними.
Зная адрес, можно получить значение, которое находится по этому адресу, поставив знак * перед адресом:
int a = 8; printf("%d n", *&a);
На экране будет выведено число 8.
Однако запись типа &a
не всегда возможна или удобна. Поэтому существует специальный тип данных — указатели, которым и присваивается адрес на область памяти.
Указатели в языке C, как и другие переменные, являются типизированными, т.е. при объявлении указателя надо указывать его тип. Как мы узнаем позже, с указателями можно выполнять некоторые арифметические операции, и именно точное определение их типа позволяет протекать им корректно. Чтобы объявить указатель, надо перед его именем поставить знак *. Например:
int *pi; float *pf;
Обратите внимание на то, что в данном случае * говорит о том, что объявляется переменная-указатель. Когда * используется перед указателем не при его объявлении, а в выражениях, то обозначает совсем иное — “получить значение (данные) по адресу, который присвоен указателю”. Посмотрите на код ниже:
int x = 1, y, z = 3; int *p, *q; p = &x; printf("%dn", *p); // 1 y = *p; printf("%dn", y); // 1 *p = 0; printf("%d %dn", x, y); // 0 1 q = &z; printf("%dn", *q); // 3 p = q; *p = 4; printf("%dn", z); // 4 printf("%p %pn", p, q); // p == q
С помощью комментариев указаны текущие значения ячеек памяти. Подробно опишем, что происходит:
- Выделяется память под пять переменных: три типа
int
и два указателя наint
. В ячейки x и z записываются числа 1 и 3 соответственно. - Указателю p присваивается адрес ячейки x. Извлекая оттуда значение (
*p
), получаем 1. - В область памяти, которая названа именем у, помещают значение равное содержимому ячейки, на которую ссылается указатель p. В результате имеем две области памяти (x и y), в которые записаны единицы.
- В качестве значения по адресу p записываем 0. Поскольку p указывает на x, то значение xменяется. Переменная p не указывает на y, поэтому там остается прежнее значение.
- Указателю q присваивается адрес переменной z. Извлекая оттуда значение (
*q
), получаем 3. - Переменной p присваивается значение, хранимое в q. Это значит, что p начинает ссылаться на тот же участок памяти, что и q. Поскольку q ссылается на z, то и p начинает ссылаться туда же.
- В качестве значения по адресу p записываем 4. Т.к. p является указателем на z, следовательно, меняется значение z.
- Проверяем, p и q являются указателями на одну и туже ячейку памяти.
Под сам указатель (там, где хранится адрес) также должна быть выделена память. Объем этой памяти можно узнать с помощью функции sizeof()
:
int *pi; float *pf; printf("%lun", sizeof(pi)); printf("%lun", sizeof(pf));
Под указатели всех типов выделяется одинаковый объем памяти, т.к. размер адреса не зависит от типа, а зависит от вычислительной системы. В таком случае, зачем при объявлении указателя следует указывать тип данных, на который он может ссылаться? Дело в том, что по типу данных определяется, сколько ячеек памяти занимает значение, на которое ссылается указатель, и через сколько ячеек начнется следующее значение.
Если указатель объявлен, но не определен, то он ссылается на произвольный участок памяти с неизвестно каким значением:
int *pa, *pb; float *pc; printf(" %p %p %pn", pa, pc, pb); // может возникнуть ошибка printf(" %d %fn", *pa, *pc);
Результат (в Ubuntu):
0x400410 0x7fff5b729580 (nil) -1991643855 0.000000
Использование неопределенных указателей в программе при вычислениях чревато возникновением серьезных ошибок. Чтобы избежать этого, указателю можно присвоить значение, говорящее, что указатель никуда не ссылается (NULL). Использовать такой указатель в выражениях не получится, пока ему не будет присвоено конкретное значение:
int a = 5; float c = 6.98; int *pa; float *pc; pa = NULL; pc = NULL; printf(" %15p %15pn", pa, pc); // Error // printf(" %15d %15fn", *pa, *pc); pa = &a; pc = &c; printf(" %15p %15pn", pa, pc); printf(" %15d %15fn", *pa, *pc);
Результат (в Ubuntu):
(nil) (nil) 0x7ffd8e77e550 0x7ffd8e77e554 5 6.980000
В данном случае, если попытаться извлечь значение из памяти с помощью указателя, который никуда не ссылается, то возникает “ошибка сегментирования”.
На этом уроке вы должны понять, что такое адрес переменной и как его получить (&var
), что такое переменная-указатель (type *p_var; p_var = &var
) и как получить значение, хранимое в памяти, зная адрес ячейки (*p_var
). Однако у вас может остаться неприятный осадок из-за непонимания, зачем все это надо? Это нормально. Понимание практической значимости указателей придет позже по мере знакомства с новым материалом.
Практически проверьте результат работы всех примеров данного урока, придумайте свои примеры работы с указателями.
Курс с решением части задач:
pdf-версия, android-приложение
Указатели
Что такое указатели
Последнее обновление: 03.01.2023
Все определенные в программе данные, например, переменные, хранятся в памяти по определенному адресу. И указатели позволяют напрямую обращаться к этим адресам и благодаря
этому манипулировать данными. Указатели представляют собой объекты, значением которых служат адреса других объектов (переменных, констант, указателей) или функций. Указатели – это неотъемлемый компонент для управления памятью в языке Си.
Определение указателя
Для определения указателя надо указать тип объекта, на который указывает указатель, и символ звездочки *.
тип_данных* название_указателя;
Сначала идет тип данных, на который указывает указатель, и символ звездочки *. Затем имя указателя.
Например, определим указатель на объект типа int:
int *p;
Пока указатель не ссылается ни на какой объект. Теперь присвоим ему адрес переменной:
int main(void) { int x = 10; // определяем переменную int *p; // определяем указатель p = &x; // указатель получает адрес переменной return 0; }
Получение адреса данных
Указатель хранит адрес объекта в памяти компьютера. И для получения адреса к переменной применяется операция &.
Эта операция применяется только к таким объектам, которые хранятся в памяти компьютера, то есть к переменным и элементам массива.
Что важно, переменная x имеет тип int, и указатель, который указывает на ее адрес тоже имеет тип int. То есть должно быть соответствие по типу.
Какой именно адрес имеет переменная x? Для вывода значения указателя можно использовать специальный спецификатор %p:
#include <stdio.h> int main(void) { int x = 10; int *p; p = &x; printf("%p n", p); // 0060FEA8 return 0; }
В моем случае машинный адрес переменной x – 0x0060FEA8. (Для адресов в памяти применяется шестнадцатеричная система.) Но в каждом отдельном случае адрес может быть иным. Фактически адрес представляет целочисленное значение, выраженное в шестнадцатеричном формате.
То есть в памяти компьютера есть адрес 0x0060FEA8, по которому располагается переменная x.
Так как переменная x представляет тип int,
то на большинстве архитектур она будет занимать следующие 4 байта (на конкретных архитектурах размер памяти для типа int может отличаться). Таким образом,
переменная типа int последовательно займет ячейки памяти с адресами 0x0060FEA8, 0x0060FEA9, 0x0060FEAA, 0x0060FEAB.
И указатель p будет ссылаться на адрес, по которому располагается переменная x, то есть на адрес 0x0060FEA8.
Стоит отметить, что при выводе адреса указателя функция printf()
ожидает, что указатель будет представлять void*
, то есть указатель на значение типа
void
. Поэтому некоторые компиляторы при некоторых настройках могут при компиляции отображать предупреждения. И чтобы было все канонически правильно, то
переданный указатель нужно преобразовать в указатель типа void *
:
printf("%p n", (void *)p);
Получение значения по адресу
Но так как указатель хранит адрес, то мы можем по этому адресу получить хранящееся там значение, то есть значение переменной x. Для этого применяется
операция * или операция разыменования (dereference operator). Результатом этой
операции всегда является объект, на который указывает указатель. Применим данную операцию и получим значение переменной x:
#include <stdio.h> int main(void) { int x = 10; int *p; p = &x; printf("Address = %p n", (void*) p); printf("x = %d n", *p); return 0; }
Консольный вывод:
Address = 0060FEA8 x = 10
Используя полученное значение в результате операции разыменования мы можем присвоить его другой переменной:
int x = 10; int *p = &x; int y = *p; // присваиваем переменной y значение по адресу из указателя p printf("x = %d n", y); // 10
Здесь присваиваем переменной y
значение по адресу из указателя p
, то есть значение переменной x
.
И также используя указатель, мы можем менять значение по адресу, который хранится в указателе:
int x = 10; int *p = &x; *p = 45; printf("x = %d n", x); // 45
Так как по адресу, на который указывает указатель, располагается переменная x, то соответственно ее значение изменится.
Создадим еще несколько указателей:
#include <stdio.h> int main(void) { char c = 'N'; int d = 10; short s = 2; char *pc = &c; // получаем адрес переменной с типа char int *pd = &d; // получаем адрес переменной d типа int short *ps = &s; // получаем адрес переменной s типа short printf("Variable c: address=%p t value=%c n", (void*) pc, *pc); printf("Variable d: address=%p t value=%d n", (void*) pd, *pd); printf("Variable s: address=%p t value=%hd n", (void*) ps, *ps); return 0; }
В моем случае я получу следующий консольный вывод:
Variable c: address=0060FEA3 value=N Variable d: address=0060FE9C value=10 Variable s: address=0060FE9A value=2
По адресам можно увидеть, что переменные часто расположены в памяти рядом, но не обязательно в том порядке, в котором они определены в тексте программы:
# 4. Указатели
Содержание
- Указатели и адреса
- Указатели и аргументы функций
- Адресная арифметика
- Указатели и массивы
Указатель — это переменная, содержащая адрес переменной. Указатели широко применяются в Си — отчасти потому, что в некоторых случаях без них просто не обойтись, а отчасти потому, что программы с ними обычно короче и эффективнее.
Наряду с goto
, указатели когда-то были объявлены лучшим средством для написания малопонятных программ.
## Указатели и адреса
Рассмотрим упрощенную схему организации памяти. Память типичной машины представляет собой массив последовательно пронумерованных или проадресованных ячеек, с которыми можно работать по отдельности или связными кусками.
A) Унарный оператор &
выдает адрес объекта
переменной p присваивается адрес ячейки c (говорят, что р
указывает на с
). Оператор &
применяется только к объектам, расположенным в памяти: к переменным и элементам массивов. Его операндом не может быть ни выражение, ни константа, ни регистровая переменная.
B) Унарный оператор *
есть оператор косвенного доступа. Примененный к указателю, он выдает объект, на который данный указатель указывает.
пример:
int х = 1, у = 2, z[10]; int *ip; //ip - указатель на int ip = &x; //теперь ip указывает на х y = *ip; //у теперь равен 1 *ip = 0; //х теперь равен 0 ip = &z[0]; //ip теперь указывает на z[0]
Если указатель *ip
указывает на x
, то *ip
можно использовать в любом месте, где допустимо применение х
.
пример:
Унарные операторы *
и &
имеют более высокий приоритет, чем арифметические операторы, так что присваивание. Но унарные операторы *
и ++
имеют одинаковый приоритет и порядок выполнения — справа налево.
пример:
у = *ip + 1 *ip += 1 (*ip)++ *ip++ //увеличится значение самого указателя
И наконец, так как указатели сами являются переменными, в тексте они могут встречаться и без оператора косвенного доступа.
пример:
## Указатели и аргументы функций
Функции в Си в качестве своих аргументов получают значения параметров, нет прямой возможности, находясь в вызванной функции, изменить переменную вызывающей функции. Аргументы-указатели позволяют функции осуществлять доступ к объектам вызвавшей ее программы и дают возможность изменить эти объекты.
пример:
void swap (int x, int y) { //неверно int temp; temp = x; x = y; y = temp; } void swap (int *px, int *py) { //верно int temp; temp = *рх; *рх = *py; *рy = temp; } swap(&a, &b); //использование
Так как оператор &
получает адрес переменной, &a
есть указатель на a
. В самой же функции swap
параметры должны быть объявлены как указатели, при этом доступ к значениям параметров будет осуществляться косвенно.
## Адресная арифметика
Под адресной арифметикой понимаются действия над указателями, связанные с использованием адресов памяти. Рассмотрим операции, которые можно применять к указателям, попутно заметив, что некоторые из них уже рассматривались, однако здесь мы повторимся, для систематизации изложения материала.
Соединение указателей, массивов и адресной арифметики — одна из сильных сторон Си. Для операций над указателями их нужно сначала инициализировать осмысленными значениями.
Инициализация.
Первый способы:
Описать переменные программы. Поскольку компилятор отводит память под переменную в момент ее описания, то присваивание указателю адреса переменной гарантирует, что нужная память отведена и там будут находиться значения переменных.
пример:
float a; float *p = &a; char buf[10]; char *p; p = buf; p = &buf[4];
Второй:
присвоить указателю значение другого указателя, к этому моменту уже правильно инициализированного.
пример:
Третий:
использовать одну из встроенных функции распределения памяти.
пример:
#include <stdio.h> #include <stdlib.h> void main() { int *b = malloc (10*sizeof(int)); if (b == 0){ //error, память не выделилась }else{ int i; for (i=0; i< 10; i++) printf("0x%xn", &b[i]); } }
пример:
char *p, *pp; char buf[10]; int array[8]; p = buf; p = &buf[4]; p = 0x0; p = 0xE040; //error p = (char*) 0x4100; p = array; //error p = pp;
Операции.
-
Присваивание. Указателю можно присвоить значение адреса. Любое число, присвоенное указателю, трактуется как адрес памяти:
пример:
int *u, *adr; int N; u = &N; //указателю присвоен адрес переменной N adr = (int *) 0х00FD; //указателю присвоен 16-теричный адрес
-
Взятие адреса. Так как указатель является переменной, то для получения адреса памяти, где расположен указатель, можно использовать операцию взятия адреса
&
пример:
int *a,*b; a = &b; //указателю a присвоен адрес указателя b
-
Косвенная адресация. Для того, чтобы получить значение, хранящееся по адресу, на который ссылается указатель, или послать данное по адресу, используется операция косвенной адресации
*
пример:
int *p, n, m = 5; p = &m; //адрес переменной m n = *p; //переменная n принимает значение m *p = -13; //переменная m принимает значение -13
-
Преобразование типа. Указатель на объект одного типа может быть преобразован в указатель на другой тип. При этом следует учитывать, что объект, адресуемый преобразованным указателем, будет интерпретироваться по-другому. Операция преобразования типа указателя применяется в виде
пример:
int i, a, *ptr; i = 0x8e41; ptr = &i; a = *ptr; //a = 0x8e41 a = *((char *)ptr); //a = 0x41
Преобразование типа указателя чаще всего применяется для приведения указателя на неопределенный тип данных
void
к типу объекта, доступ к которому будет осуществляться через этот указатель. -
Определение размера. Для определения размера указателя можно использовать операцию размер в виде
sizeof(<указатель>)
. Размер памяти, отводимой компилятором под указатель, зависит от модели памяти. Для близких указателей операцияsizeof
дает значение 2(байт), для дальних 4(байт).пример:
type *p sizeof(*p) = sizeof(type) //размер типа переменной sizeof(p) = sizeof(address) //разрядности шины адреса системы
-
Сравнение. Сравнение двух указателей любой из операций отношения имеет смысл только в том случае, если оба указателя адресуют общий для них объект, например, строку или массив.
пример:
pbuf = buf[4]; ptr = 0xE450; if (pbuf > ptr) ... //результат сравнения непредсказуем
-
Индексация. Указатель может индексироваться применением к нему операции индексации, обозначаемой в Си квадратными скобками
[ ]
. Индексация указателя имеет вид<указатель>[<индекс>]
, где <индекс> записывается целочисленным выражением.Возвращаемым значением операции индексации является данное, находящееся по адресу, смещенному в большую или меньшую сторону относительно адреса, содержащегося в указателе в момент применения операции. Этот адрес определяется так:
(адрес в указателе) + (значение <индекс>) * sizeof(<тип>)
пример:
char *ptext; ptext[2] = ' '; //ptext + 2*sizeof(char) = ptext + 2*1
-
СложениеВычитание. Одним из операндов операции может быть указатель, а другим операндом обязательно должно быть выражение целого типа. Операция вырабатывает адрес, который определяется следующим образом:
(адрес в указателе) + (значение int_выражения) * sizeof (<тип>)
где
<тип>
это тип данных, на которые ссылается указатель.пример:
double *pd; pd += 4; //адрес увеличится на 4*sizeof(double) = 4*8 = 32, не на 4!
-
Увеличение/уменьшение. Если к указателю применяется операция увеличения
++
или уменьшения--
, то значение указателя увеличивается или уменьшается на размер объекта, который он адресует. Упрощенный вариант предыдущего пункта.пример:
long long int ax, *p; p = &ax; //адрес переменной ax p++; //адрес увеличится на sizeof(ax), т.е. на 8, не на 1! p--; char text[] = "hello"; //текстовый массив char *s = text; //адрес текстового массива s++; //адрес увеличится на sizeof(char), т.е. на 1
пример сознательного обхода адресной арифметики.
int n, *p; p = (long)(p) + n; //преобразуем указатель в обычное число и делаем что хотим
## Доп материал
- Символьные указатели функции
- Массивы указателей, указатели на указатели
- Многомерные массивы
- Инициализация массивов указателей
- Указатели против многомерных массивов
- Аргументы командной строки
- Указатели на функции
- Сложные объявления
[[след. лекция](5. структуры, битовые поля, смеси.md)]
[в начало]
В предыдущих главах переменные были описаны как место в памяти компьютера, к которому можно получить доступ через идентификатор (имя переменной). Таким образом, программе не нужно заботиться о физическом адресе данных в памяти; она просто использует идентификатор каждый раз, когда необходимо обратиться к переменной.
Для программы на C++ память компьютера представляется как последовательность ячеек памяти, каждая из которых имеет размер в один байт, и каждая имеет свой уникальный адрес. Эти однобайтовые ячейки памяти упорядочены определенным образом, позволяющим интерпретировать данные размером более одного байта, занимая ячейки памяти, имеющие последовательные адреса.
Таким образом, каждая ячейка может быть легко расположена в памяти посредством своего уникального адреса. Например, ячейка памяти с адресом 1776 всегда следует непосредственно за ячейкой с адресом 1775 и предшествует ячейке с адресом 1777, а также находится точно на тысячу ячеек после 776 и точно на тысячу перед 2776.
Когда переменная объявлена, памяти, необходимой для хранения ее значения, назначается определенное место в памяти (ее адрес памяти). Обычно, программы на C++ не принимают активного участия в определении точного адреса памяти, в которой хранится переменная. К счастью, эта задача оставлена для среды, в которой запускается программа, — обычно это операционная система, которая определяет конкретные области памяти во время выполнения. Тем не менее, программе может быть полезно иметь возможность получить адрес переменной во время исполнения, для доступа к ячейкам данных, которые находятся в определенной позиции относительно этой переменной.
Оператор взятия адреса (&)
Адрес переменной можно получить, поставив перед именем переменной знак амперсанда (&), известный как оператор взятия адреса. Например:
foo = &myvar;
Это присвоит адрес переменной myvar переменной foo; ставя оператор взятия адреса (&) перед именем переменной myvar, мы присваиваем переменной foo не содержимое переменной myvar, а её адрес.
Фактический адрес переменной в памяти неизвестен до выполнения, однако, для выяснения некоторых понятий, давайте предположим, что myvar во время выполнения расположена в памяти по адресу 1776.
В этом случае, рассмотрим следующий фрагмент кода:
myvar = 25;
foo = &myvar;
bar = myvar;
Значения, содержащиеся в каждой переменной после выполнения этого кода, показаны на следующей диаграмме:
Сначала мы присвоили значение 25 переменной myvar (переменная, адрес которой мы предположили равным 1776).
Второе выражение присваивает переменной foo адрес переменной myvar, равный 1776.
Наконец, третье выражение присваивает значение, содержащееся в переменной myvar переменной bar. Это обычная операция присваивания, которая выполнялась множество раз в предыдущих главах.
Главное отличие между вторым и третьим выражениями — появление оператора взятия адреса (&).
Переменная, хранящая адрес другой переменной (как foo в предыдущем примере), это то, что в C++ называется указателем. Указателя являются очень мощным средством языка, которое имеет множество применений в низкоуровневом программировании. Немного позже мы увидим, как объявлять и использовать указатели.
Оператор разыменования (*)
Как только что сказано, переменная, хранящая адрес другой переменной, называется указателем. Говорят, что указатели «указывают на» переменную, адрес которой они хранят.
Интересным свойством указателей является то, что они могут быть использованы для доступа к переменной, на которую они указывают, напрямую. Это делается путём добавления перед именем указателя оператора разыменования (*). Сам оператор может быть прочтен как «значение, на которое указывает».
Следовательно, следуя значениям предыдущего примера, получим выражение:
baz = *foo;
Это может быть прочтено как «baz равен значению, на которое указывает foo», а выражение, в действительности, присвоит значение 25 переменной baz, т.к. foo равна 1776, а значение по адресу 1776 равно 25.
Важно четко различать, что foo ссылается на значение 1776, тогда как *foo (со звездочкой *, предшествующей идентификатору) ссылается на значение, хранящееся по адресу 1776, в данном случае это 25. Обратите внимание на разницу включения или не включения оператора разыменования (Я добавил пояснительный комментарий о том, как можно прочитать каждое из этих двух выражений).
baz = foo; // baz равен foo (1776)
baz = *foo; // baz равен значению, на которое указывает foo (25)
Операторы взятия адреса и разыменования, таким образом, дополняют друг друга:
& это оператор взятия адреса, и может быть прочтен просто как «адрес»
* это оператор разыменования, и может быть прочтен как «значение, на которое указывает»
Таким образом, они имеют своего рода противоположные значения: адрес, полученный с помощью &, может быть разыменован с помощью *.
Ранее мы выполняли следующие две операции присваивания:
myvar = 25;
foo = &myvar;
Сразу после этих двух выражений, все следующие выражения будут иметь значение true в качестве результата:
myvar == 25
&myvar == 1776
foo == 1776
*foo == 25
Первое выражение вполне понятно, учитывая, что была произведена операция присваивания myvar=25. Второе выражение использует оператор взятия адреса (&), который возвращает адрес myvar, который мы предположили равным 1776. Третье выражение очевидно, т.к. второе выражение было истинным, и была произведена операция присваивания foo=&myvar. Четвертое выражение использует оператор разыменования (*), который может быть прочтен как «значение, на которое указывает». а значение, на которое указывает foo в действительности 25.
Таким образом, после всего этого вы также можете сделать вывод, что до тех пор, пока адрес, на который указывает foo, остается неизменным, следующее выражение также будет истинным:
*foo == myvar
Объявление указателей
Благодаря способности указателя напрямую ссылаться на значение, на которое он указывает, указатель имеет разные свойства, когда он указывает на char или когда он указывает на int или float. После разыменования тип должен быть известен. И для этого в объявлении указателя должен быть указан тип данных, на который будет указывать указатель.
Объявление указателя имеет следующий синтаксис:
type * name;
где type — это тип данных, на которые будет ссылаться указатель. Этот тип является типом не самого указателя, а типом данных, на которые он ссылается. Например:
int * number;
char * character;
double * decimals;
Здесь объявлено три указателя. Каждый из них предназначен для указания на различные типы данных, но, в действительности, каждый из них является указателем и все они, вероятно, будут занимать одинаковое количество памяти (размер указателя в памяти зависит от платформы, на которой работает программа). Тем не менее, данные, на которые они указывают, не занимают одинаковое количество памяти и не имеют одинаковый тип: первый указывает на int, второй на char, а третий на double. Следовательно, хотя все эти три переменных являются указателями, в действительности, они имеют различные типы: int*, char* и double* соответственно, в зависимости от типа, на который они указывают.
Обратите внимание, что звездочка (*), используемая при объявлении указателя, означает только то, что это указатель (она является частью составного спецификатора его типа), и её не нужно путать с оператором разыменования, описанным немного ранее, который также записывается как звездочка (*). Это просто две разные вещи, представленные одним и тем же знаком.
Давайте посмотрим на пример с указателями:
// больше указателей
#include <iostream>
using namespace std;
int main ()
{
int firstvalue = 5, secondvalue = 15;
int * p1, * p2;
p1 = &firstvalue; // p1 = адрес firstvalue
p2 = &secondvalue; // p2 = адрес secondvalue
*p1 = 10; // значение, адресуемое p1 = 10
*p2 = *p1; // значение, адресуемое p2 = значению, адресуемому p1
p1 = p2; // p1 = p2 (копируется значение указателя)
*p1 = 20; // значение, адресуемое p1 = 20
cout << "firstvalue is " << firstvalue << 'n';
cout << "secondvalue is " << secondvalue << 'n';
return 0;
}
firstvalue is 10
secondvalue is 20
Каждая операция присваивания включает комментарий о том, как можно прочитать каждую строку: т.е. амперсанды (&) заменены на «адрес», а звездочки (*) на «значение, адресуемое».
Обратите внимание, что существуют выражения с указателями p1 и p2, как с оператором разыменования (*), так и без него. Значение выражения, использующего оператор разыменования (*), очень отличается от выражения, не использующего его. Когда этот оператор предшествует имени указателя, выражение ссылается на значение по адресу указателя, тогда как без него, выражение ссылается на значение самого указателя (т.е. адрес, на который указывает указатель).
Ещё одна строка, которая может привлечь ваше внимание:
int * p1, * p2;
Она объявляет два указателя, использованные в предыдущем примере. Но обратите внимание, что для каждого указателя есть звездочка (*), чтобы оба указателя имели тип int * (указатель на int). Это требование обусловлено правилами приоритета. Обратите внимание, что если вместо этого код был таким:
int * p1, p2;
p1 действительно будет иметь тип int *, но p2 будет иметь тип int. Пробелы не имеют значения в данном контексте. Но в любом случае, просто помнить о том, чтобы поставить одну звездочку на указатель, достаточно при объявлении множества указателей в одном выражении. Или даже лучше: используйте разные выражения для каждой переменной.
Указатели и массивы
Концепция массивов связана с концепцией указателей. На самом деле, массивы очень похожи на указатели на свои первые элементы, и, фактически, массив всегда может быть неявно преобразован в указатель соответствующего типа. Например, рассмотрим эти два объявления:
int myarray [20];
int * mypointer;
Следующая операция присваивания будет корректной:
myarray = mypointer;
Давайте посмотрим на пример, который смешивает массивы и указатели:
// больше указателей
#include <iostream>
using namespace std;
int main ()
{
int numbers[5];
int * p;
p = numbers; *p = 10;
p++; *p = 20;
p = &numbers[2]; *p = 30;
p = numbers + 3; *p = 40;
p = numbers; *(p+4) = 50;
for (int n=0; n<5; n++)
cout << numbers[n] << ", ";
return 0;
}
10, 20, 30, 40, 50,
Указатели и массивы поддерживают одинаковый набор операций с одинаковым смыслом для обоих. Основное отличие состоит в том, что указателям могут быть назначены новые адреса, а массивам — нет.
В главе о массивах скобки ( [ ] ) были объяснены как указание на индекс элемента массива. На самом деле эти скобки — это оператор разыменования, известный как оператор смещения. Они разыменовывают переменную, за которой следуют, так же, как и *, но они также добавляют число в скобках к адресу, на который делается разыменование. Например:
a[5] = 0; // a [смещение на 5] = 0
*(a+5) = 0; // адресуемый через (a+5) = 0
Эти два выражения эквивалентны и допустимы не только если a является указателем, но также и если a является массивом. Помните, имя массива можно использовать так же, как указатель на его первый элемент.
Инициализация указателей
Указатели могут быть инициализированы так, чтобы указывать на определенное место в момент определения:
int myvar;
int * myptr = &myvar;
Результирующее состояние переменных после этого кода такое же, как после:
int myvar;
int * myptr;
myptr = &myvar;
Когда указатели инициализируются, инициализируется адрес, на который они указывают (то есть myptr), а не указываемое значение (то есть * myptr). Поэтому приведенный выше код не следует путать с:
int myvar;
int * myptr;
*myptr = &myvar;
Что в любом случае не имеет большого смысла (и не является корректным кодом).
Звездочка ( * ) в объявлении указателя (строка 2) показывает только то, что это указатель, а не оператор разыменования (как в строке 3). В обоих случаях просто используется один и то же символ: *. Как всегда, пробелы не имеют значения и никогда не изменяют смысла выражения.
Указатели могут быть инициализированы либо по адресу переменной (например, в приведенном выше случае), либо по значению другого указателя (или массива):
int myvar;
int *foo = &myvar;
int *bar = foo;
Адресная арифметика
Выполнение арифметических операций с указателями немного отличается от выполнения их с обычными целочисленными типами. Для начала, разрешены только операции сложения и вычитания; другие не имеют смысла в мире указателей. Но и сложение, и вычитание ведут себя немного иначе с указателями в зависимости от размера типа данных, на который они указывают.
Когда были представлены основные типы данных, мы увидели, что типы имеют разные размеры. Например: char всегда имеет размер 1 байт, short обычно больше него, а int и long еще больше; точный размер зависит от системы. Например, давайте представим, что в данной системе char занимает 1 байт, short занимает 2 байта, а long 4.
Предположим теперь, что мы определили три указателя в этом компиляторе:
char *mychar;
short *myshort;
long *mylong;
и что мы знаем, что они указывают на ячейки памяти 1000, 2000 и 3000 соответственно.
Следовательно, если мы напишем:
++mychar;
++myshort;
++mylong;
mychar, как и следовало ожидать, будет содержать значение 1001. Но не так очевидно, что myshort будет содержать значение 2002, а mylong будет содержать 3004, хотя каждый из них был увеличен только один раз. Причина заключается в том, что при добавлении единицы к указателю, он будет указывать на следующий элемент того же типа, и поэтому к указателю добавляется размер в байтах того типа, на который он указывает.
Это применимо как при сложении, так и при вычитании любого числа в указателе. Это произошло бы точно так же, если бы мы написали:
mychar = mychar + 1;
myshort = myshort + 1;
mylong = mylong + 1;
Что касается операторов инкремента (++) и декремента (—), они оба могут использоваться как префикс или суффикс выражения, с небольшим различием в поведении: в качестве префикса инкремент происходит до того, как вычислено выражение, а в качестве суффикса инкремент происходит после того, как вычислено выражение. Это также относится к выражениям, увеличивающим и уменьшающим указатели, которые могут стать частью более сложных выражений, которые также включают операторы разыменования (*). Вспоминая правила приоритета операторов, мы можем вспомнить, что постфиксные операторы, такие как инкремент и декремент, имеют более высокий приоритет, чем префиксные операторы, такие как оператор разыменования (*). Следовательно, следующее выражение:
*p++
эквивалентно *(p++). Это увеличивает значение p (так что теперь оно указывает на следующий элемент), но так как используется постфиксная запись оператора ++, все выражение вычисляется как значение, на которое указывал указатель изначально (адрес, на который он указывал до инкремента).
По сути, это четыре возможные комбинации оператора разыменования с префиксной и суффиксной версиями оператора инкремента (то же самое применимо и к оператору декремента):
*p++ // same as *(p++): инкремент указателя, разыменование изначального адреса
*++p // same as *(++p): инкремент указателя, разыменование увеличенного адреса
++*p // same as ++(*p): разыменование указателя, инкремент значения, на которое он указывает
(*p)++ // разыменование указателя, пост-инкремент значения, на которое он указывает
Типичное, но не очень простое выражение, включающее эти операторы:
*p++ = *q++;
Поскольку ++ имеет более высокий приоритет, чем *, оба значения p и q увеличиваются, но поскольку оба оператора инкремента (++) используются в качестве постфикса, а не префикса, значение, присваиваемое *p, равно *q до увеличения как p, так и q. А затем оба увеличиваются. Это было бы примерно эквивалентно:
*p = *q;
++p;
++q;
Как всегда, круглые скобки уменьшают путаницу, добавляя читаемость в выражения.
Указатели и const
Указатели могут использоваться для доступа к переменной по ее адресу, и этот доступ может включать изменение значения, на которое они указывают. Но также возможно объявить указатели, которые могут получить доступ к указанному значению, чтобы прочитать его, но не изменить его. Для этого достаточно обозначить тип, на который указывает указатель, как const. Например:
int x;
int y = 10;
const int * p = &y;
x = *p; // ok: чтение p
*p = x; // ошибка: изменение p, объявленного с квалификатором const
Здесь p указывает на переменную, но указывает на нее константно, что означает, что он может прочитать указанное значение, но не может изменить его. Также обратите внимание, что выражение &y имеет тип int*, но оно присваивается указателю типа const int*. Это разрешено: указатель на не-константу может быть неявно преобразован в указатель на константу. Но не наоборот! Из соображений безопасности указатели на const неявно не конвертируются в non-const указатели.
Один из вариантов использования указателей на константные элементы — это параметры функции: функция, которая принимает указатель на non-const в качестве параметра, может изменить значение, переданное в качестве аргумента, а функция, которая принимает указатель на const в качестве параметра, не может.
// указатели как аргументы:
#include <iostream>
using namespace std;
void increment_all (int* start, int* stop)
{
int * current = start;
while (current != stop) {
++(*current); // инкремент адресуемого значения
++current; // инкремент указателя
}
}
void print_all (const int* start, const int* stop)
{
const int * current = start;
while (current != stop) {
cout << *current << 'n';
++current; // инкремент указателя
}
}
int main ()
{
int numbers[] = {10,20,30};
increment_all (numbers,numbers+3);
print_all (numbers,numbers+3);
return 0;
}
11
21
31
Обратите внимание, что print_all использует указатели, которые указывают на константные элементы. Эти указатели указывают на константное содержимое, которое они не могут изменить, но сами они не являются константами: то есть указатели все еще могут увеличиваться или назначаться другим адресам, хотя они не могут изменять содержимое, на которое они указывают.
И здесь к указателям добавляется второе измерение константности: указатели также могут быть константами. И это указывается добавлением const к указанному типу (после звездочки):
int x;
int * p1 = &x; // неконстантный указатель на int
const int * p2 = &x; // неконстантный указатель на const int
int * const p3 = &x; // константный указатель на int
const int * const p4 = &x; // константный указатель на const int
Синтаксис с const и указателями определенно сложен, и для распознавания случаев, которые лучше всего подходят для каждого случая использования, требуется некоторый опыт. В любом случае, важно увидеть константность с указателями (и ссылками) как можно скорее, но вам не следует слишком беспокоиться о том, чтобы понять все, если вы впервые сталкиваетесь с сочетанием констант и указателей. Другие варианты использования будут показаны в следующих главах.
Чтобы еще больше запутать синтаксис const с указателями, квалификатор const может либо предшествовать, либо следовать за указанным типом с одним и тем же смыслом:
const int * p2a = &x; // неконстантный указатель на const int
int const * p2b = &x; // также неконстантный указатель на const int
Как и в случае пробелов, окружающих звездочку, порядок const в этом случае просто вопрос стиля. В этой главе используется префикс const, но оба они в точности эквивалентны. Достоинства каждого стиля до сих пор активно обсуждаются в Интернете.
Указатели и строковые литералы
Как указывалось ранее, строковые литералы — это массивы, содержащие символьные последовательности, заканчивающиеся нулем. В предыдущих разделах строковые литералы использовались для непосредственной вставки в cout, для инициализации строк и для инициализации массивов символов.
Но они также могут быть доступны напрямую. Строковые литералы — это массивы соответствующего типа, которые содержат все его символы плюс завершающий нулевой символ, причем каждый из элементов имеет тип const char (как литералы, они никогда не могут быть изменены). Например:
const char * foo = "hello";
Это объявляет массив с литеральным представлением для «hello», а затем указатель на его первый элемент присваивается foo. Если мы представим, что «hello» хранится в ячейках памяти, которые начинаются с адреса 1702, мы можем представить предыдущее объявление как:
Обратите внимание, что здесь foo является указателем и содержит значение 1702, а не «h» или «hello», хотя 1702 действительно является адресом обоих.
Указатель foo указывает на последовательность символов. И поскольку указатели и массивы ведут себя по существу одинаково в выражениях, foo может использоваться для доступа к символам таким же образом, как массивы символов с нулем на конце. Например:
*(foo+4)
foo[4]
Оба выражения имеют значение «o» (пятый элемент массива).
Указатели на указатели
C++ позволяет использовать указатели, которые указывают на указатели, которые, в свою очередь, указывают на данные (или даже на другие указатели). Синтаксис просто требует звездочки (*) для каждого уровня косвенности в объявлении указателя:
char a;
char * b;
char ** c;
a = 'z';
b = &a;
c = &b;
Предполагая, что ячейки памяти для каждой переменной 7230, 8092 и 10502 выбраны случайно, это может быть представлено как:
При этом значение каждой переменной представлено внутри соответствующей ей ячейки, а их соответствующие адреса в памяти представлены значением под ними.
Новым в этом примере является переменная c, которая является указателем на указатель и может использоваться на трех разных уровнях косвенности, каждый из которых будет соответствовать своему значению:
- c типа char** и значением 8092
- *c типа char* и значением 7230
- **c типа char и значением ’z’
Указатели на void
Указатель типа void — это специальный тип указателя. В C ++ void представляет отсутствие типа. Следовательно, указатели void — это указатели, которые указывают на значение, которое не имеет типа (и, следовательно, также имеет неопределенную длину и неопределенные свойства разыменования).
Это дает указателям void большую гибкость, поскольку позволяет указывать на любой тип данных, от целочисленного значения или с плавающей точкой до строки символов. В обмен на это у них есть большое ограничение: данные, на которые они указывают, не могут быть напрямую разыменованы (что логично, поскольку у нас нет типа для разыменования), и по этой причине, любой адрес в указателе на void должен быть преобразован в некоторый другой тип указателя, который указывает на конкретный тип данных перед разыменованием.
Одним из возможных применений может быть передача обобщенных параметров функции. Например:
#include <iostream>
using namespace std;
void increase (void* data, int psize)
{
if ( psize == sizeof(char) )
{ char* pchar; pchar=(char*)data; ++(*pchar); }
else if (psize == sizeof(int) )
{ int* pint; pint=(int*)data; ++(*pint); }
}
int main ()
{
char a = 'x';
int b = 1602;
increase (&a,sizeof(a));
increase (&b,sizeof(b));
cout << a << ", " << b << 'n';
return 0;
}
y, 1603
sizeof — это оператор, интегрированный в язык C ++, который возвращает размер в байтах своего аргумента. Для нединамических типов данных это значение является константой. Поэтому, например, sizeof (char) равен 1, потому что char всегда имеет размер в один байт.
Недействительные и нулевые указатели
В принципе, указатели предназначены для указания на действительные адреса, такие как адрес переменной или адрес элемента в массиве. Но указатели могут фактически указывать на любой адрес, включая адреса, которые не ссылаются ни на один допустимый элемент. Типичными примерами этого являются неинициализированные указатели и указатели на несуществующие элементы массива:
int * p; // неинициализированный указатель (локальная переменная)
int myarray[10];
int * q = myarray+20; // элемент за границами массива
Ни p, ни q не указывают на адреса, о которых известно, что они содержат значение, но ни одно из приведенных выше выражений не приводит к ошибке. В C++ указатели могут принимать любое значение адреса, независимо от того, есть ли что-то по этому адресу или нет. Что может вызвать ошибку при разыменовании такого указателя (т.е. фактически при получении доступа к значению, на которое он указывает). Доступ к такому указателю вызывает неопределенное поведение — от ошибки во время выполнения до доступа к некоторому случайному значению.
Но иногда указатель действительно должен явно никуда не указывать, а не просто указывать на неверный адрес. Для таких случаев существует специальное значение, которое может принимать любой тип указателя: нулевое значение указателя. Это значение может быть выражено в C++ двумя способами: либо с целочисленным значением, равным нулю, либо с ключевым словом nullptr:
int * p = 0;
int * q = nullptr;
Здесь и p, и q являются нулевыми указателями, это означает, что они явно никуда не указывают, и они фактически равны между собой: все нулевые указатели равны любым другим нулевыми указателям. Также привычно видеть, что константа NULL используется в старом коде для ссылки на значение нулевого указателя:
int * r = NULL;
NULL объявлен в нескольких заголовочных файлах стандартной библиотеки и определяется как псевдоним некоторого значения константы нулевого указателя (например, 0 или nullptr).
Не путайте нулевые указатели с указателями на void! Нулевой указатель — это значение, которое любой указатель может принять для представления того, что он указывает на «никуда», в то время как указатель на void — это тип указателя, который может указывать куда-то без определенного типа. Первый относится к значению, хранящемуся в указателе, а второй — к типу данных, на которые он указывает.
Указатели на функции
C++ позволяет работать с указателями на функции. Типичное использование — передача функции в качестве аргумента другой функции. Указатели на функции объявляются с тем же синтаксисом, что и обычное объявление функции, за исключением того, что имя функции заключено в круглые скобки (), а перед именем стоит звездочка (*):
// указатели на функции
#include <iostream>
using namespace std;
int addition (int a, int b)
{ return (a+b); }
int subtraction (int a, int b)
{ return (a-b); }
int operation (int x, int y, int (*functocall)(int,int))
{
int g;
g = (*functocall)(x,y);
return (g);
}
int main ()
{
int m,n;
int (*minus)(int,int) = subtraction;
m = operation (7, 5, addition);
n = operation (20, m, minus);
cout <<n;
return 0;
}
8
В приведенном выше примере minus — указатель на функцию, имеющую два параметра типа int. Он непосредственно инициализируется для указания на функцию subtraction:
int (* minus)(int,int) = subtraction;