Урок 8. Указатели
Привет! На этом уроке ты освоишь одну из основных базовых концепций С++ - указатели.
Указатель - это переменная, которая хранит в себе адрес другого объекта.
Что значит “адрес переменной”? Когда ты запускаешь свою программу на выполнение, операционная система загружает код твоей программы в память компьютера. Код твоей программы состоит из объектов - переменных и функций. Каждый объект, который размещается в памяти компьютера, имеет адрес. Имя переменной - это абстракция для программиста, чтобы ему было удобнее писать код. Машина работает не с именами переменных, а с их адресами в памяти компьютера.
Т.е. указатель позволяет обратиться к переменной не по её имени, а по её адресу.
Зачем? Представь, что у нас была бы возможность передать в функцию division адрес переменной c:
int division(int a, int b, адрес переменной c)
{
if (b == 0)
{
return -1;
}
Записать по адресу переменной c = a / b;
return 0;
}
В результате мы смогли бы записать рассчитанное значение прямо по адресу переменной с! Спешу тебя обрадовать, что такой функционал есть в С++. Самое время изучить синтаксис для работы с указателями.
Для того, чтобы объявить указатель, необходимо между типом переменной и именем добавить знак умножения (или символ звездочку):
тип_переменной * имя;
Пример
int* p0 = nullptr;
int *p1 = nullptr;
Положение символа * не имеет значения, как и в случае положения символа & для ссылки.
Указатель - это переменная, поэтому указатель необходимо инициализировать. Для инициализации указателя, который не хранит в себе адрес никакого объекта, необходимо использовать специальный литерал nullptr. Напоминаю тебе, что переменные необходимо инициализировать при создании!
Как записать адрес в указатель? Для того, чтобы записать адрес, его сначала нужно получить. Для того, чтобы получить адрес, необходимо использовать специальный оператор - оператор взятия адреса. Оператор взятия адреса унарный. Это значит, что для работы этого оператора необходим только один операнд (одна переменная, у которой берется адрес). К нашему великому неудобству оператор взятия адреса - это амперсанд:
int c = 0;
int* pC = &c; // &c - операция взятия адреса у переменной с
Важно: тип указателя должен совпадать с типом переменной, адрес которой хранит указатель.
char c = 'c';
int* p = &c; // Ошибка компиляции
Хорошо, у нас есть адрес объекта. Но как нам работать с переменной, зная её адрес? Для этого существует операция “разыменования указателя” - т.е. операция обращения к адресу памяти, который хранит указатель. Выведем в консоль значение, которое хранится по адресу, который записан в указатель pC:
std::cout << "c: " << *pC << '\n';
Для того, чтобы разыменовать указатель, необходимо записать символ умножения перед указателем. Да, этот синтаксис путает новичков:
- символ * используется, чтобы объявить указатель
- символ * используется, чтобы разыменовать указатель
С помощью операции разыменования можно не только получить значение по адресу, но и изменить значение по адресу:
*pC = 1; // присвоили переменной c значение 1
std::cout << "c: " << *pC << '\n'; // выведет 1
Начинающие программисты постоянно путают ссылку и операцию взятия адреса. Я предлагаю тебе запомнить следующий факт: для ссылок & используется только при объявлении ссылки и находится в левой части выражения объявления ссылки
int c = 0;
int& rC = c;
Во всех остальных случаях & - это операция взятия адреса у объекта.
А теперь применим полученные знания к функции division:
int division(int a, int b, int* c)
{
if (b == 0)
{
return -1;
}
*c = a / b;
return 0;
}
int main()
{
int a = 4;
int b = 2;
int c = 0;
int status = 0;
status = division(a, b, &c);
// Остальной код остался без изменений
}
Теперь для того, чтобы вызвать функцию division нужно передать в неё адрес переменной c, поэтому мы используем операцию взятия адреса при вызове функции division(a, b, &c);
Что же такое ссылка? Я думаю, что ты уже начал подозревать, что между ссылкой и указателем есть какая-то связь, потому как это синтаксически разные вещи, которые выполняют один и тот же логический функционал - позволяют работать с переменной напрямую.
Для того, чтобы до конца разобраться в этой истории, нам необходимо познакомиться с ключевым словом const
const Переменные бывают мутабельные (mutable) и иммутабельные (immutable). Мутабельные переменные мы можем изменять, иммутабельные - нет. По умолчанию все переменные в С++ создаются мутабельными, чтобы сделать переменную иммутабельной необходимо использовать ключевое слово const
const int i = 0;
i = 1; // Ошибка компиляции, пытаемся изменить константную переменную
Константный объект обязательно должен быть проинициализирован:
const int c; // Ошибка компиляции
С константными переменными можно работать используя указатель на константную переменную, что вполне логично: значение константной переменной нельзя менять через указатель
const int i = 0;
const int* pI = &i;
*pI = 1; // Ошибка компиляции, мы пытаемся изменить значение через указатель на константу
С другой стороны, ничего не мешает нам запретить менять значение через указатель для неконстантной переменной:
int i = 0;
const int* pI = &i;
*pI = 1; // Ошибка компиляции, мы пытаемся изменить значение через указатель на константу
А теперь, внимание, тонкий нюанс. Указатель - это переменная, а значит мы можем менять значение адреса, который хранится в указателе:
int i0 = 0;
int* pI = &i0;
int i1 = 1;
pI = &i1; // да, мы можем так сделать
Получается, что мы можем сделать так, чтобы значение указателя (именно адреса, который хранит указатель) нельзя было изменить:
int i = 0;
int* const pI = &i;
int i1 = 1;
pI = &i1; // Ошибка компиляции, это константный указатель
Поздравляю, int* const pI - это возможная реализация ссылки на переменную i, потому как
- pI должен быть проинициализирован при создании
- pI “навечно” связан с переменной, адресом которой он был проинициализирован
Единственное отличие - ссылка не может ссылаться на несуществующий объект, константный указатель может:
int& r; // Ошибка компиляции
int* const i = nullptr; // Допустимо
Когда же нужно использовать ссылки, а когда указатели? Я уверен, что к этому моменту этот вопрос уже возник в твоей голове Вот что пишет создатель языка по этому поводу:
Используй ссылки, когда можешь, и указатели, когда должен https://isocpp.org/wiki/faq/references#refs-vs-ptrs
И эта фраза отлично описывает то, как начинающий разработчик на С++ должен делать выбор: используй ссылку до тех пор, пока не поймешь, что ты не можешь написать код без использования указателя. Есть выбор - ссылка, нет выбора - указатель. С примерами того, когда тебе придется использовать указатели, ты будешь знакомиться по мере прохождения курса. Например, уже в следующем уроке!
Задание для самостоятельного выполнения Замени во всех остальных функциях ссылки на указатели. Не забудь внести изменения в main:)
На этом на сегодня всё. До встречи на следующем уроке!