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




