Указатели

Указатель — это переменная, которая предназначена для хранения адреса. В языке C++ указатели часто используются в следующих случаях:

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

Объявление указателя имеет следующий формат:

<Тип> *<Переменная>;

Пример объявления указателя на тип int:

int *p = nullptr;
std::printf("%p\n", p);              // Вывод адреса
// Значение в проекте Test32: 00000000
// Значение в проекте Test64: 0000000000000000
std::cout << sizeof(p) << std::endl; // Вывод размера
// Значение в проекте Test32: 4
// Значение в проекте Test64: 8

Очень часто символ * указывается после типа, а не перед именем переменной:

int* p;

С точки зрения компилятора эти два объявления ничем не отличаются. Однако следует учитывать, что при объявлении нескольких переменных в одной инструкции, символ * относится к переменной, перед которой он указан, а не к типу данных. Например, следующая инструкция объявляет указатель и переменную, а не два указателя:

int* p, x; // Переменная x указателем не является!!!

Поэтому более логично указывать символ * перед именем переменной:

int *p, x;

Чтобы указателю присвоить адрес переменной, необходимо при присваивании значения перед названием переменной указать оператор &. Типы данных переменной и указателя должны совпадать. Это нужно, чтобы при адресной арифметике был известен размер данных. Пример присвоения адреса:

int *p = nullptr, x = 10;
p = &x;
std::printf("%p\n", p);  // Значение в проекте Test64: 000000000023fe44
std::printf("%p\n", &x); // Значение в проекте Test64: 000000000023fe44

Чтобы получить или изменить значение, расположенное по адресу на который ссылается указатель, необходимо выполнить операцию разыменования указателя. Для этого перед названием переменной указывается оператор *. Пример:

int *p = nullptr, x = 10;
p = &x;
std::cout << *p << std::endl; // 10
*p = *p + 20;
std::cout << *p << std::endl; // 30

Основные операции с указателями приведены в листинге 3.9.

Листинг 3.9. Указатели

#include <iostream>
#include <cstdio>

int x = 10;
int *p = nullptr; // Нулевой указатель

int main() {
   // Присваивание указателю адреса переменной x
   p = &x;
   // Вывод адреса
   std::cout << p << std::endl;  // Например: 0x408010
   std::printf("%p\n", p);       // Например: 0000000000408010
   // Вывод значения
   std::cout << *p << std::endl; // 10
   return 0;
}

В этом примере при объявлении указателя ему присваивается значение nullptr. Указатель, которому присвоено значение nullptr или NULL или 0, называется нулевым указателем. Определение макроса NULL зависит от используемого компилятора и языка. Определение в языке C выглядит так:

#define NULL ((void *)0)

В языке C++ в проекте Test64 так:

#define NULL __null

В других компиляторах может быть определен так:

#define NULL 0LL

В языке C++ вместо NULL и 0 лучше использовать значение nullptr.

В данном случае можно было и не присваивать указателю значение, так как глобальные и статические локальные указатели автоматически получают значение 0. Однако указатели, которые объявлены в локальной области видимости, будут иметь произвольное значение. Если попытаться записать какое-либо значение через такой указатель, то можно повредить операционную систему. Поэтому согласно соглашению, указатели, которые не на что не указывают, должны иметь значение nullptr.

Значение одного указателя можно присвоить другому указателю. При этом важно учитывать, что типы указателей должны совпадать. Пример:

int *p1 = nullptr, *p2 = nullptr, x = 10;
p1 = &x;
p2 = p1;  // Копирование адреса переменной x
std::cout << *p2 << std::endl; // 10
*p2 = 40; // Изменение значения в переменной x
std::cout << *p1 << std::endl; // 40
std::cout << x << std::endl;   // 40

В этом примере мы просто скопировали адрес переменной x из одного указателя в другой. Помимо копирования адреса можно создать указатель на указатель. Для этого при объявлении перед названием переменной указываются два оператора *:

int **p = nullptr;

Чтобы получить адрес указателя используется оператор &, а для получения значения переменной, на которую ссылается указатель, применяются два оператора *. Пример:

int *p1 = nullptr, **p2 = nullptr, x = 10;
p1 = &x;
p2 = &p1;  // Указатель на указатель
std::cout << **p2 << std::endl; // 10
**p2 = 40; // Изменение значения в переменной x
std::cout << *p1 << std::endl; // 40
std::cout << x << std::endl;   // 40

При каждом вложении указывается дополнительная звездочка:

int *p1 = nullptr, **p2 = nullptr, ***p3 = nullptr, x = 10;
p1 = &x;
p2 = &p1;
p3 = &p2;
std::cout << ***p3 << std::endl; // 10
***p3 = 40;                      // Изменение значения в переменной x
std::cout << **p2 << std::endl;  // 40
std::cout << *p1 << std::endl;   // 40
std::cout << x << std::endl;     // 40

Подобный синтаксис трудно понять и очень просто сделать ошибку, поэтому обычно ограничиваются использованием указателя на указатель.

При инициализации указателя ему можно присвоить не только числовое значение, но и строку. При этом дополнительно перед типом следует указать ключевое слово const, т. к. изменять такую строку нельзя. Пример:

const char *str = "String";
std::cout << str << std::endl; // String

Указатели можно сохранять в массиве. При объявлении массива указателей используется следующий синтаксис:

<Тип> *<Название массива>[<Количество элементов>];

Пример использования массива указателей:

int *p[3]; // Массив указателей из трех элементов
int x = 10, y = 20, z = 30;
p[0] = &x;
p[1] = &y;
p[2] = &z;
std::cout << *p[0] << std::endl; // 10
std::cout << *p[1] << std::endl; // 20
std::cout << *p[2] << std::endl; // 30

Объявление массива указателей на строки и вывод значений выглядит так:

const char *str[] = {"String1", "String2", "String3"};
std::cout << str[0] << std::endl; // String1
std::cout << str[1] << std::endl; // String2
std::cout << str[2] << std::endl; // String3

Указатели очень часто используются для обращения к элементам массива, так как адресная арифметика выполняется эффективнее, чем доступ по индексу. В качестве примера создадим массив из трех элементов, а затем выведем значения (листинг 3.10).

Листинг 3.10. Перебор элементов массива

#include <iostream>

int main() {
   const int ARR_SIZE = 3;
   int *p = nullptr, arr[ARR_SIZE] = {10, 20, 30};
   // Устанавливаем указатель на первый элемент массива
   p = arr; // Оператор & не указывается!!!
   for (int i = 0; i < ARR_SIZE; ++i) {
      std::cout << *p << std::endl;
      ++p; // Перемещаем указатель на следующий элемент
   }
   p = arr; // Восстанавливаем положение указателя
   // Выполняем какие-либо инструкции
   return 0;
}

В первой строке внутри функции main() объявляется константа ARR_SIZE, в которой сохраняется количество элементов в массиве. Если массив используется часто, то лучше сохранить его размер как константу, так как количество элементов нужно будет указывать при каждом переборе массива. Если в каждом цикле указывать конкретное число, то при изменении размера массива придется вручную вносить изменения во всех циклах. При объявлении константы достаточно будет изменить ее значение один раз при инициализации.

Далее объявляется указатель на тип int и массив. Количество элементов массива задается константой ARR_SIZE. При объявлении массив инициализируется начальными значениями.

После объявления переменных указателю присваивается адрес первого элемента массива. Обратите внимание на то, что перед названием массива не указывается оператор &, так как название переменной содержит адрес первого элемента. Если использовать оператор &, то необходимо дополнительно указать индекс внутри квадратных скобок:

p = &arr[0]; // Эквивалентно: p = arr;

Для перебора элементов массива используется цикл for. В первом параметре цикла задается начальное значение (int i = 0), во втором — условие (i < ARR_SIZE), а в третьем — приращение на единицу (++i) на каждой итерации цикла. Инструкции внутри цикла будут выполняться пока условие является истинным (значение переменной i меньше количества элементов массива).

Внутри цикла выводится значение элемента на который ссылается  указатель, а затем значение указателя увеличивается на единицу (++p). Обратите внимание на то, что изменяется адрес, а не значение элемента массива. При увеличении значения указателя используются правила адресной арифметики, а не правила обычной арифметики. Увеличение значения указателя на единицу означает, что значение будет увеличено на размер типа. Например, если тип int занимает 4 байта, то при увеличении значения на единицу указатель вместо адреса 0x0012FF30 будет содержать адрес 0x0012FF34. Значение увеличилось на 4, а не на 1. В нашем примере вместо двух инструкций внутри цикла можно использовать одну:

std::cout << *p++ << std::endl;

Выражение p++ возвращает текущий адрес, а затем увеличивает его на единицу. Символ * позволяет получить доступ к значению элемента по указанному адресу. Последовательность выполнения соответствует следующей расстановке скобок:

std::cout << *(p++) << std::endl;

Если скобки расставить так:

std::cout << (*p)++ << std::endl;

то, вначале будет получен доступ к элементу массива и выведено его текущее значение, а затем производится увеличение значения элемента массива. Перемещение указателя на следующий элемент не производится.

Получить доступ к элементу массива можно несколькими способами. Первый способ заключается в указании индекса внутри квадратных скобок. Во втором способе используется адресная арифметика совместно с разыменованием указателя. В третьем способе внутри квадратных скобок указывается название массива, а перед квадратными скобками — индекс элемента. Этот способ может показаться странным. Однако, если учесть, что выражение 1[arr] воспринимается компилятором как *(1 + arr), то все встанет на свои места. Таким образом, все эти инструкции являются эквивалентными:

int arr[3] = {10, 20, 30};
std::cout << arr[1] << std::endl;     // 20
std::cout << *(arr + 1) << std::endl; // 20
std::cout << *(1 + arr) << std::endl; // 20
std::cout << 1[arr] << std::endl;     // 20

С указателем можно выполнять следующие арифметические и логические операции:

  • прибавлять целое число. Число умножается на размер базового типа указателя, а затем результат прибавляется к адресу;
  • вычитать целое число. Вывести значения элементов массива в обратном порядке можно так:
const int ARR_SIZE = 3;
int *p = nullptr, arr[ARR_SIZE] = {10, 20, 30};
// Устанавливаем указатель на последний элемент
p = &arr[ARR_SIZE - 1];
for (int i = ARR_SIZE - 1; i >= 0; --i) {
   std::cout << *p-- << std::endl;
}
  • вычитать один указатель из другого. Это позволяет получить количество элементов базового типа между двумя указателями;
  • сравнивать указатели между собой.

При использовании ключевого слова const применительно к указателям важно учитывать местоположение ключевого слова const. Например, следующие объявления не эквивалентны:

const char *p = str;
char const *p = str;
char * const p = str;
const char * const p = str;

Первые два объявления являются эквивалентными. В этом случае изменить значение, на которое ссылается указатель, нельзя, но указателю можно присвоить другой адрес:

char str1[] = "String", str2[] = "New";
const char *p = str1;
p = str2;                            // Нормально
p[0] = 's';                          // Ошибка

При третьем объявлении изменить значение, на которое ссылается указатель, можно, но указателю нельзя присвоить другой адрес:

char str1[] = "String", str2[] = "New";
char * const p = str1;
p = str2;                            // Ошибка
p[0] = 's';                          // Нормально

Четвертое объявление запрещает изменение значения, на которое ссылается указатель, и присвоение другого адреса:

char str1[] = "String", str2[] = "New";
const char * const p = str1;
p = str2;                            // Ошибка
p[0] = 's';                          // Ошибка

Указатели часто используются при передаче параметров в функцию. По умолчанию в функцию передается копия значения переменной. Если мы в этом случае изменим значение внутри функции, то это действие не затронет значения внешней переменной. Чтобы иметь возможность изменять значение внешней переменой, параметр функции объявляется как указатель, а при вызове функции передается адрес переменной (листинг 3.11).

Листинг 3.11. Передача параметров в функцию

#include <iostream>

void func1(int x);
void func2(int *x);

int main() {
   int y = 10;
   func1(y);                    // Передаем копию значения
   std::cout << y << std::endl; // 10 (значение не изменилось!!!)

   func2(&y);                   // Передаем адрес, а не значение
   std::cout << y << std::endl; // 20 (значение изменилось!!!)
   return 0;
}
void func1(int x) {
   x = x * 2;                   // Значение нигде не сохраняется
}
void func2(int *x) {
   *x = *x * 2;
}

При использовании функции func1() передача параметра осуществляется по значению (применяется по умолчанию). При этом создается копия значения и все операции производятся с этой копией. Так как локальные переменные видны только внутри тела функции, после завершения выполнения функции копия удаляется. Любые изменения значения копии не затронут значения оригинала.

При использовании функции func2() мы передаем адрес переменной:

func2(&y);

Внутри функции адрес присваивается указателю. Используя операцию разыменования указателя можно изменить значение самой переменной, а не значение копии. Достигается это с помощью следующей инструкции:

*x = *x * 2;

Учебник C++ (MinGW-W64)
Учебник C++ (MinGW-W64) в формате PDF

Помощь сайту

ЮMoney (Yandex-деньги): 410011140483022

ПАО Сбербанк:
Счет: 40817810855006152256
Реквизиты банка:
Наименование: СЕВЕРО-ЗАПАДНЫЙ БАНК ПАО СБЕРБАНК
Корреспондентский счет: 30101810500000000653
БИК: 044030653
КПП: 784243001
ОКПО: 09171401
ОКОНХ: 96130
Скриншот реквизитов