Указатели в C кажутся страшными, пока их объясняют как мистику. На самом деле первая модель простая: переменная лежит где-то в памяти, у этой ячейки есть адрес, а указатель хранит этот адрес. Символ & берет адрес, символ * идет по адресу и получает значение
В этом уроке мы не полезем в сложные структуры и ручное управление памятью. Сначала сделаем один честный пример: изменим переменную через указатель и увидим, почему это нужно функциям
- Что получится в конце
- Адрес переменной: что делает &
- Указатель: переменная, которая хранит адрес
- Разыменование: что делает *
- Зачем указатели нужны функциям
- Как печатать адрес
- NULL: указатель, который никуда не указывает
- Мини-практика: обмен значениями через указатели
- Указатель на массив и массив указателей
- Почему адреса нельзя сравнивать как обычные числа
- Время жизни объекта
- Частые ошибки и порядок проверки
- Что может быть еще интересно по этой теме
- Что почитать дальше по C
Что получится в конце
Файл pointers_intro.c:
#include <stdio.h>
void add_bonus(int *score)
{
*score = *score + 10;
}
int main(void)
{
int points = 40;
int *points_ptr = &points;
printf("points before: %d\n", points);
printf("address: %p\n", (void *)points_ptr);
add_bonus(&points);
printf("points after: %d\n", points);
return 0;
}
Сборка:
gcc -Wall -Wextra -std=c17 pointers_intro.c -o pointers_intro
./pointers_intro
Адрес в выводе у вас будет другим. Это нормально: адрес зависит от запуска, системы и расположения данных в памяти
Адрес переменной: что делает &
Строка:
int points = 40;
создает переменную. У нее есть значение 40, но есть и место в памяти. Чтобы получить адрес этого места, используется &:
&points
Можно читать это как «адрес points». В уроке про scanf вы уже видели:
scanf("%d", &age);
Теперь смысл становится яснее: scanf должен положить введенное число внутрь переменной, поэтому получает адрес, куда можно записать результат
Указатель: переменная, которая хранит адрес
Строка:
int *points_ptr = &points;
создает указатель на int. Здесь важно не испугаться звездочки. int * означает: это переменная, которая хранит адрес int
Можно представить так:
points -> значение 40
points_ptr -> адрес переменной points
Сам указатель тоже переменная. Он хранит не число баллов, а адрес места, где лежит число баллов
Разыменование: что делает *
Если points_ptr хранит адрес, то выражение:
*points_ptr
означает «значение по этому адресу». Это называется разыменование указателя
Пример:
*points_ptr = 50;
Эта строка меняет не сам указатель, а значение переменной, на которую он указывает. Если points_ptr указывает на points, то после такой строки points станет равен 50
Одна и та же звездочка в C может выглядеть по-разному в зависимости от места:
int *p = &points; /* объявление указателя */
*p = 50; /* изменение значения по адресу */
В первом случае мы объявляем тип указателя. Во втором — идем по адресу
Зачем указатели нужны функциям
В C аргументы функции обычно передаются по значению. Если написать:
void add_bonus_wrong(int score)
{
score = score + 10;
}
и вызвать:
add_bonus_wrong(points);
изменится только копия внутри функции. Переменная points в main останется прежней
Чтобы функция изменила исходную переменную, передаем адрес:
void add_bonus(int *score)
{
*score = *score + 10;
}
И вызываем:
add_bonus(&points);
Теперь функция получает адрес points и может изменить значение по этому адресу
Как печатать адрес
Для печати адреса используется %p:
printf("address: %p\n", (void *)points_ptr);
Приведение к (void *) выглядит странно, но это нормальная форма для %p. Вывод может быть похож на:
address: 0x16d4a2f18
Не пытайтесь запоминать конкретный адрес. Он нужен только как доказательство, что указатель хранит не «магический объект», а адрес в памяти
NULL: указатель, который никуда не указывает
Иногда указатель специально делают пустым:
int *p = NULL;
NULL означает, что указатель не указывает на допустимый объект. Разыменовывать такой указатель нельзя:
*p = 10; /* ошибка времени выполнения */
Перед работой с указателем часто проверяют:
if (p != NULL) {
printf("%d\n", *p);
}
Эта привычка особенно важна в уроке про malloc, где память может не выделиться
Мини-практика: обмен значениями через указатели
Классическая задача для указателей — поменять местами значения двух переменных. Если функция получает обычные int, она меняет только копии. Если получает адреса, она может изменить исходные переменные
#include <stdio.h>
void swap(int *left, int *right)
{
int temp = *left;
*left = *right;
*right = temp;
}
int main(void)
{
int a = 10;
int b = 20;
printf("Before: a=%d b=%d\n", a, b);
swap(&a, &b);
printf("After: a=%d b=%d\n", a, b);
return 0;
}
Внутри swap переменные left и right хранят адреса. Строка int temp = left; читает значение по адресу left. Строка left = *right; записывает в первый адрес значение из второго адреса
Это тот же принцип, что в scanf: функция не может изменить вашу переменную, если получила только копию значения. Ей нужен адрес
Указатель на массив и массив указателей
Две записи выглядят похоже, но означают разные вещи:
int numbers[3] = {1, 2, 3};
int *p = numbers;
p указывает на первый элемент массива. Можно прочитать *p или p[0], можно перейти к следующему элементу через p + 1
А вот это уже массив указателей:
int a = 1;
int b = 2;
int *items[2] = {&a, &b};
items хранит два адреса. Каждый элемент массива — указатель на int. Для первых уроков это не нужно использовать часто, но полезно знать, что звездочка рядом с именем и квадратные скобки меняют смысл записи
Почему адреса нельзя сравнивать как обычные числа
Адрес выглядит как число в шестнадцатеричном формате, но обращаться с ним как с обычным числом не стоит. Вы не должны пытаться угадать соседний адрес или сохранить адрес между запусками программы. Адрес действителен только в рамках текущего процесса и только пока объект существует
Если переменная была локальной внутри функции, после выхода из функции ее адрес больше нельзя безопасно использовать. Это частая ошибка: вернуть указатель на локальную переменную и потом удивляться странному поведению
Правило для новичка: указатель полезен, когда он указывает на живой объект, область памяти malloc до free или элемент массива в допустимых границах
Время жизни объекта
Самая полезная проверка для указателя — спросить, жив ли объект, на который он смотрит. Локальная переменная живет до выхода из блока или функции. Память из malloc живет до free. Элемент массива живет, пока жив массив
Опасный пример:
int *bad_pointer(void)
{
int value = 10;
return &value;
}
Функция возвращает адрес локальной переменной. Но после выхода из функции value больше не существует как безопасный объект. Указатель внешне содержит адрес, но пользоваться им нельзя
Правильная учебная альтернатива — передать адрес переменной снаружи или выделить память через malloc и явно договориться, кто потом вызовет free. Так указатели становятся не страшной темой, а вопросом владения и времени жизни данных
Частые ошибки и порядок проверки
Перепутали адрес и значение points — значение переменной. &points — адрес переменной. points_ptr — указатель с адресом. *points_ptr — значение по адресу
Разыменовали неинициализированный указатель Не пишите int p; p = 10;. Такой указатель содержит неизвестный адрес. Сначала присвойте ему адрес настоящей переменной или NULL
Забыли & при вызове функции Если функция ждет int *, вызывайте add_bonus(&points), а не add_bonus(points)
Пытаетесь понять все указатели за один вечер Не надо. Сначала адрес переменной и изменение через функцию. Потом массивы, строки, malloc, структуры и только затем более сложные схемы
Что может быть еще интересно по этой теме
Почему массивы связаны с указателями? Имя массива во многих выражениях превращается в адрес первого элемента. Поэтому arr[i] и работа через адреса тесно связаны, хотя это не одно и то же
Указатель и ссылка из C++ — это одно и то же? Нет. В C есть указатели. В C++ есть и указатели, и ссылки, и более высокоуровневые механизмы владения памятью. Для C достаточно ясно понимать адрес и разыменование
Можно ли обойтись без указателей? В первых маленьких программах почти можно. Но scanf, массивы, строки, динамическая память и работа с файлами быстро приводят к адресам
Что дальше после указателей? Следующий логичный урок — malloc и free, потому что там указатель начинает хранить адрес памяти, выделенной во время работы программы
Что почитать дальше по C
- [Массивы и строки в C: индексы, char[] и нулевой символ](https://aglamov.biz/jazyki-programmirovanija/si/massivy-i-stroki-v-c)
- malloc и free в C: память без утечек в первом примере
- struct в C: свои типы данных без классов
- Файлы в C: fopen, fgets, fprintf и проверка ошибок



