Menu

Указатели в C: полное руководство с примерами

Указатель — это переменная, хранящая адрес в памяти. Эта страница строит идею с нуля: операторы & и *, объявление и разыменование указателей, зачем нужны типы указателей и функция swap(), которая показывает, ради чего указатели вообще существуют.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Каждая переменная вашей программы живёт где-то в памяти, и у каждого «где-то» есть номер — его адрес. Указатель — это просто переменная, хранящая один из таких номеров. В этом вся идея. Всё остальное на этой странице — синтаксис и следствия.

Указатели так важны в C потому, что C передаёт каждой функции копию её аргументов. Функция, получившая число 7, может менять свою копию сколько угодно; переменная вызывающей стороны не сдвинется. Передайте функции адрес — и она сможет дотянуться до оригинала и изменить его. Массивы, строки, динамическая память и любая структура данных, которую вы когда-либо построите на C, опираются на эту одну возможность.

Память, адреса и почему у всего есть адрес

Когда вы пишете int score = 42;, компилятор резервирует несколько байт памяти и даёт им имя, которым вы можете пользоваться. У этих байтов есть и числовой адрес. Представьте память как очень длинную улицу с пронумерованными домами: score — семья, живущая в доме номер 0x7ffd4c2a; имя нужно вам, номер — машине.

Оператор & спрашивает этот номер:

Запустите. Три адреса различаются, потому что три переменные занимают разные байты, и они будут меняться от запуска к запуску — это нормально, и именно поэтому адреса никогда не прописывают в коде вручную. Обратите внимание на приведение (void *): %p по стандарту принимает void *, и передача любого другого типа указателя формально является неопределённым поведением. Сделайте это приведение привычкой.

Непредсказуемость адресов не проблема, потому что вам почти никогда не важно, чему именно равен адрес. Важно, что он у вас есть.

Объявление указателя

Объявление указателя называет тип, на который он указывает:

int    *p;      // p может хранить адрес int
double *q;      // q может хранить адрес double
char   *name;   // name может хранить адрес char

Читайте int *p; как «*p — это int»: разыменование p даёт int. Такое чтение выдержит любое сложное объявление, которое C подкинет вам позже.

Где ставить * — вопрос стиля, который компилятор игнорирует: int* p, int *p и int * p объявляют одно и то же. Большинство кода на C пишет int *p, и на то есть практическая причина:

int* a, b;   // a - указатель на int; b - ОБЫЧНЫЙ int, а не указатель
int *a, *b;  // оба указатели - * привязывается к имени, а не к типу

* относится к описателю, а не к типу. Запись рядом с именем делает это заметным.

& и *: два оператора

Всю работу делают два оператора, и они — полные противоположности.

  • &xвзятие адреса. Даёт адрес, по которому живёт x.
  • *pразыменование. Идёт по адресу в p и даёт живущий там объект.

Две последние строки — весь смысл указателей. Ничего не присваивалось age напрямую, а age изменилась — потому что *p = 31 означает «сохранить 31 по адресу, который хранит p», а этот адрес и есть адрес age.

Вот картина в памяти:

      p                        age
 +------------+           +----------+
 | 0x7ffd1c40 | --------> |    31    |
 +------------+           +----------+
 по 0x7ffd1c38            по 0x7ffd1c40

 p    -> адрес 0x7ffd1c40
 *p   -> хранящийся там int, 31
 &age -> 0x7ffd1c40, тот же адрес, что хранит p
 &p   -> 0x7ffd1c38, где живёт сам p

Обратите внимание на последнюю строку: указатель сам является переменной где-то в памяти, поэтому у него тоже есть собственный адрес. Это не вопрос с подвохом, это просто последовательность — и именно так работают указатели на указатели (int **).

Поскольку & и * отменяют друг друга, *&age — это просто age, а &*p — просто p.

Зачем нужны типы указателей

Указатель хранит адрес, а адрес — просто число; так почему компилятор настаивает на том, чтобы знать, указывает ли он на int или на double? Две причины:

  1. Сколько байт читать. Разыменование int * читает 4 байта (обычно); разыменование double * читает 8. Без типа *p был бы бессмысленным.
  2. Как интерпретировать эти байты. Один и тот же набор битов — одно число как int и совершенно другое как float.

А вот размер самого указателя не зависит от того, на что он указывает: адрес есть адрес.

На 64-битной машине все три указателя занимают 8 байт, тогда как то, на что они указывают, — 4, 8 и 1. Именно тип, на который указывают, придаёт смысл *p и арифметике указателей.

Присваивание между разными типами указателей — ошибка, на которую компилятор пожалуется, и не зря: pi = &d; подготовил бы почву для чтения 4 байт от double с объявлением результата целым числом.

Каноническая мотивация: swap()

Вот функция, к которой обращается каждый курс по C, потому что она ломается ровно тем способом, который объясняет указатели.

Функция сообщает, что поменяла их местами, а main сообщает, что ничего не произошло. broken_swap получила копии x и y. Она безупречно поменяла местами свои копии, а затем эти копии перестали существовать при возврате из функции.

Передайте ей вместо этого адреса:

Теперь a и b по-прежнему копии — копии двух адресов, — но копия адреса указывает на ту же самую переменную, что и оригинал. *a — это x, откуда бы функция ни была вызвана.

Это шаблон, стоящий за каждой функцией C, которая изменяет данные вызывающей стороны, и за параметрами функций в целом: в C нет передачи по ссылке, поэтому вы передаёте указатель и разыменовываете его. Именно поэтому scanf нужен &:

int n;
scanf("%d", &n);   // scanf должна уметь писать в n, поэтому ей нужен адрес n

Возврат более одного значения

Функция в C возвращает одно значение. Указатели — способ это обойти: передайте адреса для дополнительных результатов.

Возвращаемое значение несёт статус, а указатели несут результаты. Это разделение встречается повсюду в собственной стандартной библиотеке C и в API операционных систем.

Указатели на указатели

Поскольку указатель — это переменная, можно взять его адрес, и тип этого адреса — «указатель на указатель на int»:

  pp              p               value
+------+       +------+        +------+
| &p   | ----> | &val | -----> |   8  |
+------+       +------+        +------+

С ** вы встретитесь по-настоящему, когда функции понадобится изменить указатель, которым владеет вызывающая сторона, — например, в аллокаторе, устанавливающем указатель вызывающей стороны на свежую память, — и в char *argv[] из аргументов командной строки.

Четыре ошибки, о которых стоит знать заранее

Использование указателя, который никуда не направили. Неинициализированный указатель хранит те байты, что там уже лежали. Его разыменование читает или пишет по случайному адресу.

int *p;      // p хранит мусор
*p = 10;     // неопределённое поведение - скорее всего, аварийное завершение

Инициализируйте каждый указатель: настоящим адресом или значением NULL. Про дисциплину вокруг этого — в нулевых указателях.

Путаница между *p и p. p = 5; устанавливает указатель на адрес 5 (бессмыслица); *p = 5; сохраняет 5 туда, куда он указывает. Компилятор предупредит о первом, а предупреждения стоит читать.

Забытый & в scanf. scanf("%d", n) передаёт значение n в качестве адреса. Это компилируется с предупреждением, а затем пишет в ту память, которую называет это число.

Возврат адреса локальной переменной. Локальная переменная исчезает в момент возврата из функции; адрес становится квитанцией на снесённое здание.

int *bad(void) {
    int local = 42;
    return &local;   // вызывающая сторона получает висячий указатель
}

Если вам нужна память, переживающая вызов, выделите её — для этого и существует динамическая память.

Куда двигаться дальше

Указатели не заканчиваются на «хранить один адрес». Прибавление к указателю шагает по памяти в единицах его типа — это тема арифметики указателей; а имя массива в C превращается в указатель на его первый элемент, поэтому указатели и массивы — фактически одна тема, как только вы знаете обе. Прочитайте эти две страницы следующими — и остальной C перестанет выглядеть как магия.

Часто задаваемые вопросы

Что такое указатель в C?

Указатель — это переменная, значением которой является адрес в памяти, то есть расположение какого-то другого объекта. int *p = &age; заставляет p хранить адрес age, и тогда *p читает или изменяет переменную age по этому адресу.

В чём разница между * и & в C?

&x — оператор взятия адреса: он даёт адрес, по которому живёт x. *p — оператор разыменования: он идёт по адресу, хранящемуся в p, и даёт находящийся там объект. Они отменяют друг друга, поэтому *&x — это просто x.

Почему * встречается дважды — в объявлении и при использовании указателя?

Это два разных применения одного символа. В int *p; звёздочка — часть типа, она говорит «p — указатель на int». В *p = 5; звёздочка — оператор разыменования: «сохранить 5 по адресу, который хранит p». Чтение объявления как «*p — это int» делает осмысленным и то, и другое.

Зачем в C нужны указатели?

C передаёт каждый аргумент по значению, поэтому функция не может изменить переменную вызывающей стороны, если ей не передали адрес. Указатели также позволяют эффективно обходить массивы, возвращать больше одного результата, строить связанные структуры и работать с памятью, выделенной во время выполнения через malloc.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ