Программы
Указатели, Массивы, Строки

Указатели, Массивы, Строки

Пожалуй, именно на указателях у большинства появляются трудности с пониманием Си. Поэтому настоятельно советую вчитываться в приложенные примеры кода.

В предыдущей теме мы рассмотрели, как работать итеративно с данными с помощью циклов. При этом совершаются одни и те же действия. Логично проводить подобные действия над массивами данных, а не над отдельными данными.

Однако, перед тем, как познакомиться с массивами, стоит обратить внимание на указатели.

Изображение Изучаем язык программирования Си

Указатели

Указатель – это переменная, которая хранит адрес области памяти. Указатель, как и переменная, имеет тип.

+--------+
| Данные  | <--+
+--------+    |   +-----------+
              +---| Указатель   |
                  +-----------+

Указатель – это тоже данные, которые интерпретируются как адрес памяти, на который вы ссылаетесь.

Синтаксис объявления указателей:

<тип> *<имя>;
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 - от младших байтов к старшим.