Указатели: адреса, отсылки и арифметика
Поймите указатели как адреса памяти, изучите адресные и дереферативные операторы, арифметику указателей в массивах, точность с указателями и когда необработанные указатели все еще уместны в современном C++.
Что такое указатель?
pointer — переменная, сохраняющая адрес memory. Каждый объект в C++ живет по определенному адресу в памяти, и указатель позволяет ссылаться на этот объект косвенно. Указатели имеют основополагающее значение для C++ — они обеспечивают динамические структуры данных, полиморфизм и эффективную передачу параметров.
Тип указателя объявляется с *: int* p; объявляет указатель на int. Сам указатель занимает память (обычно 8 байт на 64-битных системах) и его значение является адресом, а не данными, на которые он указывает.
Обсуждение Address-Of & Dereference
Двумя основными операторами указателей являются & (адрес) и * (указатель). Оператор & получает адрес переменной. Оператор * следует за адресом для доступа к заостренному объекту.
#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 теряют информацию о размере при передаче функции.
#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, если у вас еще нет цели. Компилятор НЕ будет предупреждать обо всех неинициализированных использованиях указателей.
Пойнтер против ссылки: когда использовать
Ссылки и указатели обеспечивают опосредование, но они различаются по важным параметрам. Ссылки не могут быть нулевыми, не могут быть повторно установлены и не требуют явной отсылки. Предпочитают ссылки для параметров функции; используйте указатели, когда требуется аннулирование или повторное размещение.
#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++?