Перейти к содержимому

Указатели: адреса, отсылки и арифметика

Поймите указатели как адреса памяти, изучите адресные и дереферативные операторы, арифметику указателей в массивах, точность с указателями и когда необработанные указатели все еще уместны в современном C++.

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

pointer — переменная, сохраняющая адрес memory. Каждый объект в C++ живет по определенному адресу в памяти, и указатель позволяет ссылаться на этот объект косвенно. Указатели имеют основополагающее значение для C++ — они обеспечивают динамические структуры данных, полиморфизм и эффективную передачу параметров.

Тип указателя объявляется с *: int* p; объявляет указатель на int. Сам указатель занимает память (обычно 8 байт на 64-битных системах) и его значение является адресом, а не данными, на которые он указывает.

Обсуждение Address-Of & Dereference

Двумя основными операторами указателей являются & (адрес) и * (указатель). Оператор & получает адрес переменной. Оператор * следует за адресом для доступа к заостренному объекту.

address_deref.cpp
#include <iostream>

int main() {
    int value = 42;
    int* ptr = &value;  // ptr holds the address of value

    std::cout << "value:  " << value << '\n';        // 42
    std::cout << "&value: " << &value << '\n';       // e.g. 0x7ffd5a3c
    std::cout << "ptr:    " << ptr << '\n';           // same address
    std::cout << "*ptr:   " << *ptr << '\n';          // 42 (dereference)

    *ptr = 100;  // modify value through the pointer
    std::cout << "value after *ptr = 100: " << value << '\n'; // 100

    int* null_ptr = nullptr;  // points to nothing — safe sentinel
    // *null_ptr = 5;  // UNDEFINED BEHAVIOR — crash on most systems
}

Оригинальное название: Pointer Arithmetic & Array Decay

Арифметика указателей действительна только в массиве (или в одном конце). Компилятор масштабирует арифметику по sizeof(T). Имя массива распадает до указателя на его первый элемент в большинстве контекстов — вот почему массивы в стиле C теряют информацию о размере при передаче функции.

pointer_arithmetic.cpp
#include <iostream>
#include <cstddef>  // std::ptrdiff_t

int main() {
    int arr[5] = {10, 20, 30, 40, 50};
    int* p = arr;  // array decays to pointer to first element

    std::cout << *p << '\n';        // 10
    std::cout << *(p + 2) << '\n';  // 30  (moves 2*sizeof(int) bytes)
    std::cout << p[3] << '\n';      // 40  (p[i] is *(p + i))

    // Iterating via pointer
    for (int* it = arr; it != arr + 5; ++it) {
        std::cout << *it << ' ';
    }
    std::cout << '\n';  // 10 20 30 40 50

    // Pointer difference
    std::ptrdiff_t diff = (arr + 4) - arr;  // 4 (elements, not bytes)
    std::cout << "diff: " << diff << '\n';

    // WARNING: arithmetic outside the array is UNDEFINED BEHAVIOR
    // int* bad = arr + 10;  // UB — don't do this
}

Const Pointers против Pointer-to-const

Размещение const относительно * определяет, что является неизменным:

- const int* p (или int const* p) - указатель на const: вы не можете изменить заостренное значение через p, но вы можете повторно установить p, чтобы указать в другом месте.
- int* const p - const pointer: сам указатель неизменен (не может быть повторно размещен), но вы можете изменить значение, на которое он указывает.
const int* const p — как указатель, так и заостренное значение неизменны.

Прочитайте декларации справа налево : int* const p читается как «p является указателем на int».

Пустые указатели (void*) могут содержать любой адрес, но не могут быть удалены без литья. Они в основном используются в API-интерфейсах C-стиля и реализациях распределителей.

Лучшая практика

В современном C++ необработанные указатели должны использоваться исключительно как не имеющие права собственности, указатели наблюдателей. Они говорят: «Я вижу этот объект, но не несу ответственности за его жизнь». Общее законное использование:

- Внедрение структур данных (дерево узлов, указывающих на родителя)
Функциональные параметры, которые наблюдают, но не владеют (хотя ссылки часто лучше)
- Взаимодействие с C API
Итераторы в пользовательских контейнерах

Если указатель владеет ресурсом, заверните его в std::unique_ptr или std::shared_ptr. Если вы обнаружили, что пишете delete, вы почти наверняка делаете это неправильно в современном C++.

Подводный камень

Dangling pointers — это ошибка #1. Указатель становится болтающимся, когда объект, на который он указывает, разрушается:

Возвращение указателя к локальной переменной
Удаление объекта, в то время как другой указатель все еще ссылается на него
- Укажите в std::vector, который перераспределяется после push_back

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

Пойнтер против ссылки: когда использовать

Ссылки и указатели обеспечивают опосредование, но они различаются по важным параметрам. Ссылки не могут быть нулевыми, не могут быть повторно установлены и не требуют явной отсылки. Предпочитают ссылки для параметров функции; используйте указатели, когда требуется аннулирование или повторное размещение.

ptr_vs_ref.cpp
#include <iostream>

void increment_ref(int& val) { ++val; }         // cannot be null
void increment_ptr(int* val) { if (val) ++(*val); } // must null-check

int main() {
    int x = 10;
    increment_ref(x);        // clean call syntax
    increment_ptr(&x);       // caller must take address
    std::cout << x << '\n';  // 12

    int* p = &x;
    p = nullptr;  // OK — pointers can be re-seated and nulled

    int& r = x;
    // r = ???;    // references cannot be re-seated
    // int& bad;   // ERROR — references must be initialized
}
Ключевые выводы
  • & получает адрес, * следует за ним — это обратные операции
  • Арифметика указателей действительна только в массивах и масштабах sizeof(T)
  • Имена Array распадаются на указатели, теряя информацию о размере
  • Размещение const справа налево: const int* против int* const
  • В современном C++ необработанные указатели должны быть только не принадлежащими наблюдателям.
  • Предпочитаете ссылки вместо указателей, если вам не нужна отмена или повторное размещение

Тест — проверьте свои знания

(15 XP)

1. Что означает `const int* p`?

2. Что происходит, когда массив передается функции, ожидающей указательного параметра?

3. Когда арифметика указателей действительна в C++?