Перевод с небольшими дополнениями, уточнениями и примерами.
Оригинал в посте C Pointers Explained, Really.
>>><<<
Когда я учился в колледже – мой друг пожаловался, что ему сложно понять синтаксис и использование указателей при программировании на С.
Для примера он привёл код вида “*x=**p++
“, говоря что он уродливый и сложный для чтения, в нём слишком много операторов, наложенных друг на друга, что усложняет понимает того, что код должен выполнить.
Я написал небольшое пояснение для наших студентов и получил много хороших отзывов. Некоторые люди говорили, что они программируют на С много лет – но только прочтя мой пост они поняли указатели в С.
Мы знаем про то, что представляет собой карта памяти: RAM – это длинный массив байт. Держите это в уме, что было проще понять указатели. Почему-то книги и учителя забывают об этом, когда говорят об указателях.
Допустим, у нас имеется код:
... main() { int n; int *p; ...
Тогда – в памяти имеется участок, который будет выглядеть примерно так:
: Address: : |-----| 0x5100| | n is an integer, one machine word big |-----| 0x5104| | p is a pointer, also one word big |-----| 0x5108| | other unused memory |-----| : :
Давайте зададим значения этим переменным.
Например, n
будет иметь значение 151:
n = 151;
И создадим указатель p
, который указывает на целочисленное n
:
p = &n;
Т.е., мы говорим, что “значение переменной p
– это адрес переменной n
“:
: Address: : Value at that address: |----| 0x5100 | 151| n |----| 0x5104 |5100| p |----| 0x5108 | ?| |----| : :
Теперь – я могу напечатать двумя способами:
printf("n is %d.\n", n); printf("n is %d.\n", *p);
Весь код будет выглядеть так:
#include <stdio.h> main() { int n; int *p; n = 151; p = &n; printf("n is %d.\n", n); printf("n is %d.\n", *p); return 0; }
Результат его работы:
$ ./pointers_explained n is 151. n is 151.
Оператор “*
” в нашем коде говорит: “Дайте мне объект, расположенный по такому-то адресу“. Тип объекта – такой же, как же тип указателя, который мы определили при его объявлении (int *p;
). Так как мы определили тип указателя как целочисленный (integer) – то С будет предполагать, что объект, на который он указывает так же является целочисленным.
Теперь – я хочу вывести адрес памяти переменной n
:
... printf("n is located at $%x.\n", &n); printf("n is located at $%x.\n", p); ...
$ ./pointers_explained n is located at $6ef29314. n is located at $6ef29314.
Оператор “&
” говорит “Дайте мне адрес, с которого начинается такой-то объект“. В данном случае это шеснадцатиричное значение 6ef29314 (0x5104 в примере карты памяти выше).
Обратите внимание, что значение переменной p
– это адрес.
А как на счёт адреса самой переменной p
? Конечно, как и любая другая переменная – у неё есть свой адрес, и получить его можно так:
printf("p is located at $%x.\n", &p);
$ ./pointers_explained n is located at $81b22104. n is located at $81b22104. p is located at $81b22108.
Мы получаем адрес переменной с помощью оператора &
.
Усложним пример.
main() { char name[] = "Bill"; char *p; int *q; return 0; }
Теперь у нас имеется массив name[]
, с которым можно поиграть. Вот как будет выглядеть память теперь:
|---| 0x5100 |'B'| "name" is an address constant that has value hex 5100 |---| 0x5101 |'i'| char: 1 byte |---| 0x5102 |'l'| char: 1 byte |---| 0x5103 |'l'| char: 1 byte |---| 0x5104 |\0 | char: 1 byte |---| 0x5105 | | p is a pointer: 1 word |---| 0x5109 | | q is a pointer: 1 word |---|
Установим p
как указатель на массив name[]
:
p = name;
Тепреь значение p
– 0х5100. Мы можем использовать оператор разыменовывания “*
” и получить символ B, который находится по адресу 0х5100:
main() { char name[] = "Bill"; char *p; int *q; p = name; printf("%c\n", *p); return 0; }
$ ./pointers_explained B
А что произойдёт, если мы выполним инкремент p
?
... p = name; printf("%c\n", *p); ++p; printf("%c\n", *p); ...
Значение указателя p
будет увеличено на 1, а его значение станет равным 0х5101. Очень просто, правда?
$ ./pointers_explained B i
А теперь – давайте сделаем что-то безответственное:
q = name;
Но ведь q
имеет целочисленный (int *q;
) тип!
Если выполним разыменование q
– мы получим слово в 4 байта, т.к. тип int
как правило равен 4 байтам:
... int integerType; printf("Size of int: %ld bytes\n",sizeof(integerType)); ...
$ ./pointers_explained Size of int: 4 bytes
Которое начинается с адреса “name” (0x5100), которое будет транслировано в челочисленное значение. ‘B‘, ‘i‘, ‘l‘, ‘l‘ в целочисленном представлении вернёт нам некое длинное число, в зависимости от машины:
... q = name; printf("%d\n", *q); ...
$ ./pointers_explained 1819044162
Число 1819044162 тут – это десятичное представление значения 0x6c6c6942, которое получено из шеснадцатиричного представления символов:
B i l l name 42 69 6c 6c
Которое можно увидеть, если добавить printf %x
(hexadecimal представление):
... q = name; printf("%d\n", *q); printf("%x\n", *q); ...
Вернёт нам:
$ ./pointers_explained 1819044162 6c6c6942
Тут мы видим ключевую ошибку, которую можно допустить при использовании указателей, о которой я упоминал выше: С предполагает, что раз указатель имеет тип integer
– то и объект, на который он указывает – тоже имеет целочисленный тип данных.
Что бы лучше продемонстрировать это – возьмём ещё один пример, и выполним инкремент указателя c
:
... char long_name[] = "BillLongName"; int *c; c = long_name; printf("%x\n", *c); ++c; printf("%x\n", *c); ...
$ ./pointers_explained 6c6c6942 676e6f4c
После инкремента целочисленного указателя c
– он указывает на следующее слово в памяти:
g n o L long_name 67 6e 6f 4c
Всё должно выглядеть достаточно просто, если вспомнить предыдущий параграф с указателем p
. Значение указателя c
было увеличено на размер объекта, на который направлен наш указатель. Так как и указатель имеет тип integer
, следовательно – предполагается что объект так же явлется типом int
. Таким образом инкремент “передвигает” указатель на следующие 4 байта (рамер int
).
Возьмём следущий пример:
int main() { int n; n = 151; f(n); } int f(int x) { printf("%d\n", x); }
Это простая программа, в которой происходит передача integer объекта “по значению” (by value) – n
передаётся в функцию f()
.
Т.е. – значение переменной n
копируется в новую переменную x
:
|---| 0x5100 |151| n is an integer |---| 0x5104 |151| x is another integer |---|
Когда мы говорим о переменной x
– мы используем значение из адреса 0x5104, и мы можем делать с ним что угодно, при этом не затрагивая число по адресу 0x5100.
Но что, если мы хотим в фукнции f()
изменить значение переменной, а потом использовать его в функции main()
? В C это можно сделать, передав переменную “по ссылке” (by reference):
int main() { int n; n = 151; f(&n); printf("After F(): %d\n", n); } int f(int *x) { printf("%d\n", *x); *x = 451; }
Мы передаём адрес переменной n
(в виде &n
), а в функции f()
x
объявляется как указатель на n
.
Мы всё ещё передаём некоторое значение, но теперь это значение – адрес, а не число:
|----| 0x5100 | 151| n is an integer |----| 0x5104 |5100| x is a pointer to int |----|
x
всё ещё указывает на адрес 0x5100, но мы изменили значение в этом участке памяти (*x = 451;
).
А теперь – вернёмся к тому, с чего начинали: уродливому и нечитабельному выражению “*x=**p++
“.
Рассмотрим карту памяти:
|----| слово в памяти со значением 0, без переменной 0x5100 | 0| |----| 0x5104 | 12| тут хранится значение, слово в памяти, тоже без переменной |----| 0x5108 |5104| целочисленный указатель на предыдущее слово |----| 0x511c |5108| тут переменная p, указатель на целочисленный указатель |----| 0x5120 |5100| тут переменная x, указатель, которая указывает на адрес со значением 0 |----|
Для начала – давайте посмотрим на x
и p
:
int *x;
– указатель на integer;int **p;
– указатель на указатель на указатель; “подчинённый” указатель указывает на int.
Вы знаете, что такое *x
– это значит “тут значение из 0x5100“.
И вы знаете, что *p
– это указатель на значение по адресу 0x5108.
Теперь выражение “x = **p
” можно прочитать как “Целочисленное по адресу 0х5100, которое принимает значение из адреса 0x5104“.
Расмотрим “указатель на указатель” в таком примере:
int main () { char a; char *b; char **c; a = 'A'; b = &a; c = &b; printf("A value: %c\n", a); printf("A adress: %p\n", (void*) &a); printf("B value: %p\n", b); printf("B adress: %p\n", (void*) &b); printf("C value: %p\n", c); printf("C adress: %p\n", (void*) &c); }
$ ./pointers_explained A value: A A adress: 0x7ffccd4ea9cf B value: 0x7ffccd4ea9cf B adress: 0x7ffccd4ea9d0 C value: 0x7ffccd4ea9d0 C adress: 0x7ffccd4ea9d8
Но возвращаясь к нашему коду, "*x=**p++
” – что тут значит “**p++
“? Он будет эквивалентен коду “*( *( p++ ) )
“, т.е. – “Указатель на указатель на ineteger, а после выполнения действия – увеличить его значение на 1“.
Дополним пример выше:
... printf("\nAfter *b=**c++;:\n\n"); *b=**c++; printf("B value: %p\n", c); printf("B adress: %p\n", (void*) &c); printf("C value: %p\n", c); printf("C adress: %p\n", (void*) &c); ...
Результат:
$ ./pointers_explained A value: A A adress: 0x7ffd01a1c08f B value: 0x7ffd01a1c08f B adress: 0x7ffd01a1c090 C value: 0x7ffd01a1c090 C adress: 0x7ffd01a1c098 After *b=**c++;: B value: 0x7ffd01a1c098 B adress: 0x7ffd01a1c098 C value: 0x7ffd01a1c098 C adress: 0x7ffd01a1c098
Оригинал тут>>>.