Указатели и адресная арифметика

⏱ 30 мин · 🧩 заданий: 6

Память — это массив байтов

Для процессора память — длинный ряд пронумерованных байтов. Номер байта — его адрес. Каждая переменная занимает несколько подряд идущих байтов, и её адрес — адрес первого из них.

адрес: 0x1000 0x1001 0x1002 0x1003 0x1004 0x1005 0x1006 0x1007 ┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐ │ int x = 10 │ int y = 7 │ └──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘ &x == 0x1000 &y == 0x1004

Указатель — переменная, значение которой — адрес другой переменной. Тип указателя (int *, double *) говорит, данные какого типа лежат по этому адресу.

Операторы & и *

int x = 10;
int *p = &x;      /* & — взять адрес: p хранит адрес x */

printf("%d\n", *p);   /* * — разыменовать: прочитать значение по адресу → 10 */
*p = 20;              /* записать по адресу → x стало 20 */

printf("%p %p\n", (void *)p, (void *)&x);  /* один и тот же адрес */

Аналогия

Переменная — квартира, адрес — номер квартиры, указатель — бумажка с номером. &x — узнать номер квартиры, *p — пойти по номеру с бумажки и заглянуть внутрь.

Звёздочка в объявлении int *p и звёздочка в выражении *p — разные вещи: первая говорит «p — указатель», вторая — «значение по адресу из p».

NULL

NULL — специальное значение «указатель никуда не указывает» (адрес 0). Функции возвращают его как признак ошибки (malloc), но не все: mmap при ошибке возвращает MAP_FAILED, то есть (void *)-1.

int *p = NULL;
if (p != NULL)
    *p = 1;       /* без проверки — Segmentation fault */

Адресная арифметика

Прибавление целого к указателю сдвигает его на столько элементов, а не байтов:

double arr[4] = {1.0, 2.0, 3.0, 4.0};
double *a = arr;        /* имя массива — адрес первого элемента */

a + 1   /* адрес на 8 байт дальше (sizeof(double)) */
*(a + 2) /* 3.0 — то же, что a[2] */
Выражение Значение при a == 0x1000, тип double *
a 0x1000
a + 1 0x1008
a + i 0x1000 + 8·i
&a[69] 0x1000 + 552 = 0x1228

Разность указателей одного массива — число элементов между ними.

Массивы и указатели

a[i] по определению означает *(a + i). Поэтому код sum += a[i] * b[i]; одинаково работает, объявлены ли a и b массивами или указателями. Но в памяти это разные вещи:

double arr[70]; double *a; (указатель, 8 байт) ┌──────┬──────┬───┬──────┐ ┌──────────┐ ┌──────┬──────┬───┐ │arr[0]│arr[1]│...│arr[69]│ │ 0x50000 │ ─────▶ │ a[0] │ a[1] │...│ └──────┴──────┴───┴──────┘ └──────────┘ └──────┴──────┴───┘ адрес arr известен заранее чтобы найти a[i], сначала читаем сам указатель из памяти

Задача 7

extern double *a; — указатель. Для каждого a[i] процессор читает: (1) значение указателя a из памяти; (2) элемент a[i]. Первое обращение — служебное, оно не связано с ячейкой массива. Если компилятор не держит a в регистре, это 2 лишних обращения на итерацию (для a и b).

Указатели на структуры

typedef struct { int state; unsigned counter; pid_t pid; pid_t ppid; } Mem;

Mem *mem = ...;
(*mem).counter = 0;   /* разыменовать и взять поле */
mem->counter = 0;     /* то же самое, короче */

void * и указатель на указатель

void * — «указатель на что угодно». Его нельзя разыменовать напрямую, но можно присвоить любому указателю. Так устроены malloc и mmap:

Mem *mem = mmap(NULL, sizeof(Mem), ...);   /* mmap возвращает void * */

Указатель на указатель хранит адрес указателя: char **argv — массив строк аргументов командной строки.

int main(int argc, char **argv)
{
    for (int i = 0; i < argc; i++)
        printf("argv[%d] = %s\n", i, argv[i]);
}

Итог

  • Указатель хранит адрес; & берёт адрес, * разыменовывает, -> обращается к полю структуры.
  • p + i сдвигает на i * sizeof(*p) байт; a[i] ≡ *(a + i).
  • Указатель — отдельная переменная в памяти: чтобы прочитать a[i] через глобальный указатель, нужно сначала прочитать сам указатель.
  • Разыменование NULL или «висячего» указателя → SIGSEGV.

Практика решено 0 из 6

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Ввод ответа

Что напечатает фрагмент?

int x = 10;
int *p = &x;
*p = 20;
printf("%d\n", x);
Задание 2 Ввод ответа

Указатель double *a содержит адрес 0x1000. Чему равен адрес a + 3? Ответ в hex, например 0x1003.

Задание 3 Один вариант ответа

Что эквивалентно выражению a[i]?

Задание 4 Ввод ответа

Сколько обращений к памяти нужно процессору, чтобы прочитать a[i], если a — глобальная переменная-указатель (double *a;, лежит в памяти), а i — в регистре? Считайте только обращения к данным.

Задание 5 Написать код

Через указатель Mem *mem запишите в поле pid результат getpid(), а в поле ppid — getppid(). Используйте оператор ->.

Задание 6 Несколько вариантов

Какие из этих операций с указателем — ошибка?

Войдите, чтобы отмечать пройденные уроки.