Указатель — это переменная, которая указывает на некоторый участок памяти. Обычно указатель содержит адрес другой переменной, объявленной в коде ранее. Посмотрим на примере:
void main() { // Объявляем переменную типа int // И инициализируем её int var = 10; }
При выполнении программы в ОЗУ будет выделен участок памяти такого размера, чтобы там свободно помещалось значение нашей переменной var. Размер выделенного участка памяти зависит от типа переменной (посмотреть ‘размер’ каждого типа можно тут); Поэтому инициализировать указатель будем адресом, где хранится значение другой переменной. Итак:
void main() { int var = 10; // & - операция взятия адреса // Результатом операции взятия адреса является адрес ячейки памяти, // которая была выделена компилятором под соответствующую переменную. // Например, если для переменной выделена область памяти, // начиная с адреса 5022FE38, тогда &var будет иметь значение 5022FE38 int* ptr = &var; }
Здесь стоит обратить внимание на то, что тип указателя должен соответствовать типу переменной, адрес которой мы присваиваем указателю. Т.е. в нашем случае переменная имеет тип int. Поэтому тип указателя тоже должен быть int.
Теперь, когда мы имеем указатель на переменную var мы можем оперировать ее значением не только непосредственно с помощью самой переменной, но и с помощью указателя на нее. Посмотрим, как это работает на простом примере:
#include <iostream> void main() { int var = 10; int* ptr = &var; std::cout << "Значение переменной: " << var << std::endl; // 1. std::cout << "Значение указателя: " << ptr << std::endl; // 2. std::cout << "Значение переменной, на которую ссылается указатель:" << *ptr << std::endl; // 3. }
- Здесь все просто — используем саму переменную.
- Во втором случае выведется адрес в оперативной памяти, где расположена переменная var. На этот же адрес ссылается указатель ptr. Например 5022FE38
- А в третьем случае мы обращаемся к значению переменной var через указатель ptr. Но, мы не просто используем имя указателя — здесь используется операция разыменования: она позволяет перейти от адреса к значению.
В предыдущем примере был организован только вывод значения переменной на экран. Можем ли мы используя указатель изменять значение переменной, на которую он указывает? Да, конечно! Все что нужно — сделать разыменование указателя:
void main() { int var = 10; int* ptr = &var; // Увеличиваем значение переменной на единицу (*ptr)++; // Теперь в переменной var будет храниться число 11 }
А сейчас поговорим о том, где можно использовать указатели.
1. Массивы
Как вы уже знаете, массив это структура данных, представленная в виде набора ячеек одного типа, объединенных под одним единым именем. Имя массива является указателем на первый элемент массива (не стоит забывать, что индекс у массива начинается с нуля). Поэтому мы можем обращаться к произвольному элементу массива зная его имя и номер элемента.
void main() { int size = 10; // Создаем массив из десяти элементов int array[10]; // Инициализируем массив нулями for (int i = 0; i < size; i++) { array[i] = 0; } // Так как имя массива - указатель на первый элемент // То изменение третьего элемента можно осуществить так *(array + 2) = 1; // Или же получим "явно" адрес первого элемента массива *(&array[0] + 2) = 1; }
На заметку: компилятор автоматически изменяет a[b] на *(a + b), и иногда можно встретить следующий код 1[a+1]. Не пугайтесь, это всего лишь обращение к третьему элементу массива: 1[a+1] == *(1 + a + 1) == a[2]
2. Динамическое выделение памяти
Очень часто при решении какой-то задачи мы заранее не знаем сколько должно быть элементов в массиве. В этом случае нам помогают динамические массивы. Они позволяют выделить память для всех элементов в процессе выполнения программы. Пример:
#include <iostream> void main() { // Получаем кол-во элементов в динамическом массиве int size = 0; std::cout >> size; // Создаем динамический массив int* array_ptr = new int[size]; }
Что тут произошло? Мы считали целое число, после чего оператор new выделил память для size элементов и возвратил адрес в памяти. Теперь указатель array_ptr хранит в себе адрес первого элемента в памяти. С этим указателем можно работать так же как и с обычным статическим массивом:
*(array_ptr + 3) = 5; // Установили значение четвертого элемента массива
3. Указатель на указатель
Это та же переменная, которая хранит адрес другого указателя. Зачем он нужен? Например для инициализации двумерного динамического массива (матрицы):
#include <iostream> void main() { // Получаем кол-во строк и столбцов в матрице int n, m; std::cout >> n >> m; // Строки матрицы будут хранить в себе указатели на столбцы матрицы int** matrix = new int*[n]; for (int i = 0; i < n; i++) { matrix[i] = new int[m]; } // Не забываем про то, что все элементы матрицы нежно проинициализировать // Но это задание оставлю для вас }
Так же существуют и тройные указатели, и, даже четверные! Но в реальных задачах они практически не встречаются.
5. Указатель в качестве аргумента функции
Задача: написать функцию replace, которая на вход принимает два массива и заменяет элементы первого массива соответствующими элементами из второго массива.
// Функция меняет элементы двух матриц местами void replace(int** a, int** b, int size) { // TODO } void main() { int size = 5; // Создаем две матрицы int** a = new int*[size]; int** b = new int*[size]; for (int i = 0; i < size; i++) { a[i] = new int[size]; b[i] = new int[size]; } // Здесь инициализация всех элементов матрицы // Её вы можете написать сами replace(a, b, size); }
Самый простой вариант — просто заменить элементы в цикле:
void replace(int** a, int** b, int size) { for (int row = 0; row < size; row++) { for (int column = 0; column < size; column++) { // Создаем временную переменную int tmp = a[row][column]; a[row][column] = b[row][column]; b[row][column] = tmp; } } }
Но просто — не значит хорошо. Вспомним, что указатели указывают на какой-то участок в памяти. Почему бы нам просто не поменять указатели местами? Например так:
void replace(int** a, int** b) { // Временная переменная int** tmp = a; a = b; b = a; }
Сработает ли это? Если внутри функции replace вывести две матрицы на экран — то увидим, что значения действительно поменялись. НО: если вывести эти же матрицы в функции main, то значения матриц не изменилось! Почему так? Все предельно просто — в функции repalce мы работали не с самим указателем, а с его локальной копией! Все изменения, которые произошли в функции replace — затронули только локальную копию указателей, но никак не сам указатель. Давайте посмотрим на следующий пример:
void replace(int a, int b) { int tmp = a; a = b; b = a; } int main() { int a = 1; int b = 2; replace(a, b); // Изменились ли здесь значения переменных a и b }
Думаю вы поняли к чему я клоню. Передавая переменные по значению, а не по указателю мы не можем заменить обе переменные в функции. То же самое было и с нашими указателями — используя их в качестве аргумента мы лишали их возможности изменяться. Поэтому нужно создать указатель на наш указатель, как бы запутанно это не звучало. Посмотрим:
void replace(int*** a, int*** b) { int*** tmp = a; a = b; b = a; } void main() { int size = 5; // Создаем две матрицы int** a = new int*[size]; int** b = new int*[size]; for (int i = 0; i < size; i++) { a[i] = new int[size]; b[i] = new int[size]; } // Здесь инициализация всех элементов матрицы // Её вы можете написать сами // Создаем указатели на переменные, которые хотим заменять в функции // В нашем случае переменные - int** int*** first_ptr = &a; int*** second_ptr = &b; replace(a, b); }
Вуаля! Все заработало! Без лишних операций по замене каждого элемента. Просто и элегантно поменяли адреса матриц.
Важное замечание
При работе с данными в динамической памяти (в нашем случае с динамическими массивами) не забываем отчищать память, после того как они перестали быть нам нужны. Добавляем:
delete[] a; delete[] b;
Ну вот и все. Надеюсь вы поняли основные принципы использования указателей и где их можно использовать. Если что-то осталось непонятным — добро пожаловать в комментарии.