Указатели в C без паники: адрес, * и &

Указатели в 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

Оцените статью
0 0 голоса
Рейтинг статьи
Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
0
Оставьте комментарий! Напишите, что думаете по поводу статьи.x