В предыдущей теме мы рассмотрели, как работать итеративно с данными с помощью циклов. При этом совершаются одни и те же действия. Логично проводить подобные действия над массивами данных, а не над отдельными данными.
Однако, перед тем, как познакомиться с массивами, стоит обратить внимание на указатели.
Указатели
Указатель – это переменная, которая хранит адрес области памяти. Указатель, как и переменная, имеет тип.
+--------+
| Данные | <--+
+--------+ | +-----------+
+---| Указатель |
+-----------+
Указатель – это тоже данные, которые интерпретируются как адрес памяти, на который вы ссылаетесь.
Синтаксис объявления указателей:
<тип> *<имя>;
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 - от младших байтов к старшим.