Денис Злобин

Урок 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:)

На этом на сегодня всё. До встречи на следующем уроке!