Указатели в С++
Для любого типа T тип T* означает «указатель на объект типа Т».
Указатель является объектом (переменной), хранящим адрес другого объекта. Указатель – это переменная, которая хранит адрес области памяти. Указатель, как и переменная, имеет тип. Синтаксис определения указателей:
<тип> *<имя>; |
Например float *a;
long long *b;
Для обращения к объекту, на который указывает указатель (адрес которого хранится в указателе), используется оператор * (звёздочка), называемый оператором разыменования или косвенным обращением.
Для получения адреса объекта используется унарный оператор & взятия адреса. Т.о., для работы с указателями существует два основных оператора - оператор & взятия адреса и оператор * разыменования.
Таким образом, для объекта:
int value;
запись &value означает «адрес объекта value». Воспользовавшись этим можно сохранить адрес объекта value следующим образом:
int value;
int *p_value = &value;
что означает, что переменная p_value, имеющая тип «указатель на объект типа int» инициализируется адресом переменной value.
С точки зрения синтаксиса языка безразлично, где будет находиться пробел – справа или слева от символа *. Его вообще может не быть. Однако, рекомендуется всегда символ * присоединять к имени переменной, а не к имени типа, что кажется более логичным. Причина этого следующая. В приведённой записи:
int* p,q;
int *r,s;
переменные p и r имеют тип «указатель на тип int», а переменные q и s имеют тип int. Хотя по записи (особенно в первой строке) может показаться, что все четыре переменные должны иметь тип «указатель на тип int». Для того, чтобы все четыре переменные имели тип «указатель на тип int» необходимо написать:
int* p, *q;
int *r, *s;
это синтаксическая проблема языка и проще всего обойти её, привыкнув присоединять символ * к объекту, а не к типу.
Теперь указатель p_value может быть использован для доступа к объекту, на который он указывает:
*p_value = 1;
Запись означает: поместить единицу в объект типа int, адрес которого хранится в объекте p_value. Поскольку в p_value хранится адрес объекта value, то будет изменено значение именно этой переменной – в неё будет помещена единица.
Точно также можно считать значение переменной по указателю:
cout << “Value=” << *p_value;
Указатели придуманы с целью непосредственного отражения механизмов адресации компьютеров, на которых исполняются программы.
Гарантируется, что нет объектов с нулевым адресом. Следовательно, указатель, равный нулю, можно интерпретировать как указатель, который ни на что не ссылается.
Пример.
#include <conio.h>
#include <stdio.h>
void main()
{
int A = 100;
int *p;
//Получаем адрес переменной A
p = &A;
//Выводим адрес переменной A
printf("%p\n", p);
//Выводим содержимое переменной A
printf("%d\n", *p);
//Меняем содержимое переменной A
*p = 200;
printf("%d\n", A);
printf("%d", *p);
getch();
}
Рассмотрим код внимательно, ещё раз
?
5 |
int A = 100; |
Была объявлена переменная с именем A. Она располагается по какому-то адресу в памяти. По этому адресу хранится значение 100.
?
6 |
int *p; |
Создали указатель типа int.
?
9 |
p = &A; |
Теперь переменная p хранит адрес переменной A. Используя оператор * мы получаем доступ до содержимого переменной A. Чтобы изменить содержимое, пишем
?
18 |
*p = 200; |
После этого значение A также изменено, так как она указывает на ту же область памяти. Ничего сложного.
Теперь другой важный пример
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <conio.h> #include <stdio.h>
void main() { int A = 100; int *a = &A; double B = 2.3; double *b = &B;
printf("%d\n", sizeof(A)); printf("%d\n", sizeof(a)); printf("%d\n", sizeof(B)); printf("%d\n", sizeof(b));
getch(); } |
Будет выведено :
4
4
8
4
Несмотря на то, что переменные имеют разный тип и размер, указатели на них имеют один размер. Действительно, если указатели хранят адреса, то они должны быть целочисленного типа. Так и есть, указатель сам по себе хранится в переменной типа size_t (а также ptrdiff_t), это тип, который ведёт себя как целочисленный, однако его размер зависит от разрядности системы. В большинстве случаев разницы между ними нет. Зачем тогда указателю нужен тип?
Арифметика указателей
Во-первых, указателю нужен тип для того, чтобы корректно работала операция разыменования (получения содержимого по адресу). Если указатель хранит адрес переменной, необходимо знать, сколько байт нужно взять, начиная от этого адреса, чтобы получить всю переменную. Во-вторых, указатели поддерживают арифметические операции. Для их выполнения необходимо знать размер. операция +N сдвигает указатель вперёд на N*sizeof(тип) байт.
Например, если указатель int *p; хранит адрес CC02, то после p += 10; он будет хранить адрес СС02 + sizeof(int)*10 = CC02 + 28 = CC2A (Все операции выполняются в шестнадцатиричном формате). Пусть мы создали указатель на начало массива. После этого мы можем "двигаться" по этому массиву, получая доступ до отдельных элементов.
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <conio.h> #include <stdio.h>
void main() { int A[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *p;
printf("%d\n", *p); p++; //p=p+1; printf p = A; ("%d\n", *p); p = p + 4; printf("%d\n", *p);
getch(); } |
Заметьте, каким образом мы получили адрес первого элемента массива
?
8 |
p = A; |
Массив, по сути, сам является указателем, поэтому не нужно использовать оператор &. Можно переписать по-другому
8 |
p = &A[0]; |
Получить адрес первого элемента и относительно него двигаться по массиву. Кроме операторов + и - указатели поддерживают операции сравнения. Если у нас есть два указателя a и b, то a > b, если адрес, который хранит a, больше адреса, который хранит b.
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <conio.h> #include <stdio.h>
void main() { int A[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int *a, *b;
a = &A[0]; //a=A; b = &A[9];
printf("&A[0] == %p\n", a); printf("&A[9] == %p\n", b);
if (a < b) { printf("a < b"); } else { printf("a > b"); }
getch(); } |
Если же указатели равны, то они указывают на одну и ту же область памяти.
Указатели на массивы
Имя массива можно рассматривать как указатель на его первый элемент.
Пример:
int v[] = { 1,2,3,7 };
int *p1 = v; // Указатель на первый элемент
int *p2 = &v[0]; // Указатель на первый элемент
int *p3 = &v[4]; // Указатель на элемент, следующий за последним
В противоположность указателю на массив, определение массива указателей выглядит следующим образом:
int *ap[15]; // Массив из 15 указателей на int
Результат применения операторов -, +, --, ++ к указателю зависит от типа объекта, на который ссылается указатель. Если к указателю p типа T* применяется арифметическая операция, предполагается, что он указывает на элемент массива объектов типа Т; p+1 указывает на следующий элемент массива, а p-1 на предыдущий. То есть целое значение p+1 будет на sizeof(T) больше, чем целое значение р.
Рассмотрим пример обнуления элементов массива с использованием индексов
int arr[ArraySize];
for (int i=0; i < ArraySize; ++i)
arr[i] = 0;
и с использованием указателей:
int arr[ArraySize];
int *p=arr;
for (int i=0; i < ArraySize; ++i)
*p++ = 0; //{*p=0; p++;}
Интересна запись *p++. Унарные операторы * и ++ имеют одинаковый приоритет, однако они правоассоциативны. То есть в данном случае первым будет выполняться оператор ++, увеличивающий значение указателя. Указатель будет сдвинут на следующий элемент массива. Но поскольку это оператор постинкремента, то для разыменования будет использовано старое значение указателя. Таким образом, в одном выражении записано сразу два действия: передвинуть указатель и разыменовать указатель. В объект, на который указывает указатель, помещается ноль.
