Базовое разыменование указателя
Разыменование указателя на C++ — это получение доступа к значению, которое хранится в занимаемом указателем адресе памяти.
Указатель обычно разыменовывается оператором-звездочкой *.
Начнем с простого примера:
#include <iostream>
int main() {
int value = 42;
int* ptr = &value;
std::cout << "Value: " << *ptr << std::endl; // Вывод: «Значение: 42»
return 0;
}
В этом примере ptr
является указателем на value
. Выражением *ptr
этот указатель разыменовывается, и мы получаем доступ к значению, на которое указывается указателем.
Разыменование и изменение значений
Разыменование применяется и для изменения значения, на которое указывается указателем:
#include <iostream>
int main() {
int value = 42;
int* ptr = &value;
*ptr = 100; // Изменение значения через указатель
std::cout << "New value: " << value << std::endl; // Вывод: «Новое значение: 100»
return 0;
}
Здесь мы изменили значение value
через указатель ptr
.
Разыменование указателей на объекты
При работе с объектами оператором-стрелочкой ->
осуществляются разыменовывание и доступ к членам объекта:
#include <iostream>
#include <string>
struct Person {
std::string name;
int age;
};
int main() {
Person alice = {"Alice", 30};
Person* ptr = &alice;
std::cout << "Name: " << ptr->name << ", Age: " << ptr->age << std::endl;
// Вывод: «Имя: Элис, возраст: 30»
// Это эквивалентно:
// std::cout << "Name: " << (*ptr).name << ", Age: " << (*ptr).age << std::endl;
return 0;
}
Оператором ->
выполняется вместе разыменовывание и доступ к членам объекта.
Разыменование и массивы
Разыменование массивов и указателей отличается:
#include <iostream>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int* ptr = arr; // «arr» превращается в указатель на свой первый элемент
for (int i = 0; i < 5; ++i) {
std::cout << *ptr << " "; // Разыменование
ptr++; // Переходим к следующему элементу
}
// Вывод: 10 20 30 40 50
return 0;
}
Здесь арифметикой указателей мы перемещаемся по массиву, на каждом шаге выполняя разыменовывание.
Разыменование и указатели функции
Разыменовыванием также вызывается функция, на которую указывается ее указателями:
#include <iostream>
int add(int a, int b) {
return a + b;
}
int subtract(int a, int b) {
return a - b;
}
int main() {
int (*operation)(int, int);
operation = &add;
std::cout << "Result of add: " << (*operation)(5, 3) << std::endl; // Вывод: «Результат добавления: 8»
operation = &subtract;
std::cout << "Result of subtract: " << (*operation)(5, 3) << std::endl; // Вывод: «Результат вычитания: 2»
return 0;
}
В этом случае при разыменовании указателя функции вызывается функция, на которую указателем указывается.
Смарт-указатели и разыменование
В современном C++ используются смарт-указатели. Они разыменовываются точно так же, как и обычные:
#include <iostream>
#include <memory>
int main() {
auto ptr = std::make_unique<int>(42);
std::cout << "Value: " << *ptr << std::endl; // Вывод: «Значение: 42»
*ptr = 100;
std::cout << "New value: " << *ptr << std::endl; // Вывод: «Новое значение: 100»
return 0;
}
Синтаксис разыменования смарт-указателей снабжен дополнительным функционалом безопасности.
Реальный сценарий: проход связного списка
Рассмотрим практическое применение разыменования указателей при обходе связного списка:
#include <iostream>
struct Node {
int data;
Node* next;
Node(int d) : data(d), next(nullptr) {}
};
void printList(Node* head) {
while (head != nullptr) {
std::cout << head->data << " ";
head = head->next;
}
std::cout << std::endl;
}
int main() {
Node* head = new Node(1);
head->next = new Node(2);
head->next->next = new Node(3);
std::cout << "Linked List: ";
printList(head); // Вывод: «Связный список: 1 2 3»
// Очистка
while (head != nullptr) {
Node* temp = head;
head = head->next;
delete temp;
}
return 0;
}
В этом примере демонстрируется важность разыменования указателей при манипулировании связными структурами данных.
Константные указатели и разыменование
При работе с константными указателями поведение разыменования меняется в зависимости от того, куда помещено ключевое слово const
:
#include <iostream>
int main() {
int value = 42;
int anotherValue = 100;
const int* ptr1 = &value; // Указатель на «const int»
// *ptr1 = 50; // Ошибка: нельзя изменить через «ptr1»
ptr1 = &anotherValue; // То, на что указывается «ptr1», можно изменить
int* const ptr2 = &value; // Константный указатель на «int»
*ptr2 = 50; // Можно изменить через «ptr2»
// ptr2 = &anotherValue; // Ошибка: нельзя изменить то, на что указывается «ptr2»
const int* const ptr3 = &value; // Константный указатель на «const int»
// *ptr3 = 50; // Ошибка: нельзя изменить через «ptr3»
// ptr3 = &anotherValue; // Ошибка: нельзя изменить то, на что указывается «ptr3»
std::cout << "Value: " << value << std::endl; // Вывод: «Значение: 50»
return 0;
}
Чтобы писать корректный и безопасный код, важно разбираться в этих нюансах.
Указатели void и разыменование
Нюансы имеются и при разыменовании указателей void
:
#include <iostream>
int main() {
int value = 42;
void* voidPtr = &value;
// *voidPtr; // Ошибка: указатель «void» не разыменовывается напрямую
int* intPtr = static_cast<int*>(voidPtr);
std::cout << "Value: " << *intPtr << std::endl; // Ошибка: «Значение: 42»
return 0;
}
Перед разыменованием указатели void
приводятся к приемлемому типу.
Производительность
Разыменование обычно выполняется очень быстро, но чрезмерное разыменование в сплошных циклах сказывается на производительности.
Вот пример:
#include <iostream>
#include <chrono>
#include <vector>
const int SIZE = 100000000;
void directAccess(std::vector<int>& vec) {
for (int i = 0; i < SIZE; ++i) {
vec[i] = i;
}
}
void pointerAccess(std::vector<int>& vec) {
int* ptr = vec.data();
for (int i = 0; i < SIZE; ++i) {
*ptr = i;
++ptr;
}
}
int main() {
std::vector<int> vec(SIZE);
auto start = std::chrono::high_resolution_clock::now();
directAccess(vec);
auto end = std::chrono::high_resolution_clock::now();
std::chrono::duration<double> diff = end - start;
std::cout << "Direct access time: " << diff.count() << " s\n";
start = std::chrono::high_resolution_clock::now();
pointerAccess(vec);
end = std::chrono::high_resolution_clock::now();
diff = end - start;
std::cout << "Pointer access time: " << diff.count() << " s\n";
return 0;
}
Запустите этот тест у себя в системе и сравните по эффективности прямой доступ к массиву и разыменование указателя.
Типичные ошибки и как их избежать
Разыменование нулевых указателей. Перед разыменованием всегда проверяйте, не является ли указатель нулевым.
int* ptr = nullptr;
if (ptr != nullptr) {
std::cout << *ptr << std::endl; // Безопасно: разыменовываем, только если не нулевой
}
Разыменование неинициализированных указателей. Перед использованием убедитесь, что указатели корректно инициализированы.
Разыменование «висячих» указателей. Будьте осторожны с указателями, которые становятся недопустимыми, например, после высвобождения памяти.
Ошибки смещения на единицу в массивах. Помните, что индексация массива начинается с 0, и не выходите за пределы массива.
Неудаленная динамически выделенная память. Во избежание утечек памяти new
всегда используйте с delete
.
Заключение
Разыменование указателей на C++ — это фундаментальная операция для работы с памятью напрямую.
Корректное ее выполнение, начиная с простого доступа к значению и заканчивая сложными манипуляциями со структурами данных, важно для эффективного программирования на C++.
Читайте также:
- C++: подробно о реализации двусторонней очереди
- C++: подробное руководство по is_open()
- Как создать CSV-файл с помощью C++
Читайте нас в Telegram, VK и Дзен
Перевод статьи ryan: How to Dereference a Pointer in C++: A Comprehensive Guide