Пожалуй, именно на указателях у большинства появляются трудности с пониманием Си. Поэтому настоятельно советую вчитываться в приложенные примеры кода.
В предыдущей теме мы рассмотрели, как работать итеративно с данными с помощью циклов. При этом совершаются одни и те же действия. Логично проводить подобные действия над массивами данных, а не над отдельными данными.
Однако, перед тем, как познакомиться с массивами, стоит обратить внимание на указатели.
Указатель – это переменная, которая хранит адрес области памяти. Указатель, как и переменная, имеет тип.
+--------+
| Данные | <--+
+--------+ | +-----------+
+---| Указатель |
+-----------+
Указатель – это тоже данные, которые интерпретируются как адрес памяти, на который вы ссылаетесь.
Синтаксис объявления указателей:
<тип> *<имя>;
float *a; long long *b;
Два основных оператора для работы с указателями – это оператор & взятия адреса, и оператор * разыменования.
Однако, при объявлении переменной указателя, * вместе с типом используется именно как часть объявления типа, а не для получения данных по адресу.
#include <stdio.h> int main(void) { int A = 100; // Значение int *p; // Неинициализированный указатель printf("%p\n", p); p = &A; // Инициализация printf("%p\n", p); printf("%d\n", sizeof(p)); printf("%d\n", *p); // p = 200; // Кладём адрес в указатель. // Проблем не будет, пока не обратимся по этому адресу. *p = 200; // По указателю кладём по указателю 200 printf("%d\n", A); printf("%d", *p); int **double_ptr = &p; // Указатель на указатель **double_ptr = 300; printf("%d", **double_ptr); printf("%d\n", A); return 0; }
Стоит сказать, что в Си у нас нет "ссылок" – хорошеньких безобидных ссылок. У нас адреса / указатели. Они могут быть не инициализированы, невалидны, их можно даже складывать! "Всё есть байт" – ну, вы помните.
Однако, арифметика указателей – это не глупость и случайность. Это инструмент работы с данными.
#include <stdio.h> int main(void) { int A = 2; int B = 3; int *p; p = &A; printf("%d\n", *p); // Всё нормально p++; printf("%d\n", *p); // Какая-то фигня puts("================"); // Массив из 6-ти int значений int C[8] = {1, 2, 3, 4, 5, 6, 7, 8}; p = &C[0]; // Эквивалентно p = C // И да, на самом деле массивы - это указатели! printf("%d\n", *p); p++; printf("%d\n", *p); // C[1] p = p + 3; printf("%d\n", *p); // C[4] p = p + 333333; printf("%d\n", *p); // ? return 0; }
int C[8]
– объявление массива. Элементы массива хранятся одним "куском"
– один за другим. Благодаря этому адресация в массиве быстрая (O(1)).
И благодаря этому можно провернуть этот "трюк" (на самом деле, это
как раз правда, а A[i] – "синтаксический сахар").
А сколько у нас размер int
? Арифметика указателей зависит от типа указателя,
не байтовая. То есть
int C[8] = {1, 2, 3, 4, 5, 6, 7, 8}; p = C p = p + 3;
– здесь указатель p
сдвинется на 12 байт (при sizeof(int) == 4).
Равно как мы можем преобразовывать одни типы к другим
float b = 2.12; int a = (int)b;
Также мы можем преобразовывать один тип указателей в другой:
#include <stdio.h> int main(void) { int a = 0x45464748; // Шестнадцатиричный формат записи int *p = &a; char *q = (char *)p; printf( "%c %c %c %c\n", *q, *(q + 1), *(q + 2), *(q + 3) ); return 0; }
И получим вывод в представлении символов:
→ clang int-to-chars.c
→ ./a.out
H G F E
"H" = 0x48 и т.д. Здесь наша арифметика уже работает в байтах - по размеру типа char.
Порядок вывода зависит от способа хранения int - от младших байтов к старшим.