Указатель — это переменная, которая указывает на некоторый участок памяти. Обычно указатель содержит адрес другой переменной, объявленной в коде ранее. Посмотрим на примере:
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;
Ну вот и все. Надеюсь вы поняли основные принципы использования указателей и где их можно использовать. Если что-то осталось непонятным — добро пожаловать в комментарии.