Урок 7. Ссылки
Привет! Этот урок будет посвящен решению проблемы из урока 6: каким образом сделать функцию division безопасной? Для начала разберемся в терминологии. Что значит безопасная функция? Это значит, что использование функции с любыми наборами входных данных не приведет к аварийному завершению работы программы. В нашей текущей реализации функция не является безопасной: если передать в функцию второй аргумент равный нулю, то программа аварийно завершится:
int division(int a, int b)
{
return a / b;
}
Чтобы не допустить аварийного завершения работы программы, необходимо проверять второй аргумент на равенство нулю перед тем, как вызвать функцию:
if (b != 0)
{
c = division(a, b);
printResult(a, b, c, '/');
}
else
{
std::cout << "Error! Division by zero is not allowed";
}
Проблема кода из Урока 5 заключается в том, что пользователь (т.е. программист, который использует функцию division) может запросто забыть сделать проверку! Как исправить эту ситуацию?
В этой и подобных ситуациях я рекомендую не изобретать велосипед, а взять за пример уже существующее решение из мира программирования. Например, за идею решения нашей проблемы можно взять интерфейс функций из библиотеки языка С.
Псевдокод с интерфейсом функции division в стиле функций из библиотеки языка С выглядит следующим образом:
статус_выполнения division(аргумент1, аргумент2, результат_выполнения);
статус_выполнения - теперь функция возвращает не результат, а целое число (на сленге “код возврата”), по значению которого мы будем определять, закончилось ли выполнение функции успехом или при выполнении функции возникли проблемы. Если функция была выполнена успешно условимся возвращать 0, иначе будем возвращать -1. аргумент1 - переменная a аргумент2 - переменная b результат_выполнения - результат работы функции, т.е. это переменная с
Сначала напишем код с использованием новой версии функции division:
int main()
{
int a = 4;
int b = 2;
int c = 0;
int status = 0;
status = division(a, b, c);
if (status == 0)
{
printResult(a, b, c, '/');
}
else if (status == -1)
{
std::cout << "Error! Division by zero is not allowed";
}
else
{
std::cout << "Unknown error at division operation";
}
}
У нас получилось исключить случай с аварийным завершением работы программы при делении на 0. Заодно мы познакомились с новой конструкцией условий: else if записывается между if и else и позволяет добавить дополнительные проверки, если if вернул ложь.
Увы, полностью решить проблему с некорректным использованием функции division у нас не получилось: пользователь может забыть проверить код возврата, в итоге в программе будет использоваться некорректное значение для операции деления. К сожалению, в рамках С стиля эффективного решения этой проблемы не существует: язык С минималистичен и полностью перекладывает ответственность за корректное использование функций на плечи программиста.
Попробуем написать код на С++, используя имеющиеся у нас на текущий момент знания:
// Внимание! Код содержит ошибку
int division(int a, int b, int c)
{
if (b == 0)
{
return -1;
}
c = a / b;
return 0;
}
Этот код содержит ошибку: если собрать приложение, то в выводе результата деления мы всегда будем получать ноль. Почему? Потому что в С++ функции принимают свои аргументы по копии.
Без шуток, я рекомендую тебе распечатать эту строчку и прикрепить к своему монитору, чтобы она была постоянно перед твоими глазами. Увы, об этой особенности языка забывают не только новички, но и разработчики с опытом, особенно на собеседованиях:(
Что же такое “функция принимает свои аргументы по копии”? Соберем весь код вместе, уберем незначимый для текущего вопроса код и найдем проблему:
// Внимание! Код содержит ошибку
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:
int division(int a, int b, int c)
Функция division принимает все аргументы по копии - это значит, что после вызова функции division создаются аргументы функции и им присваивается значение переменных, которые были переданы в вызов функции. Сложно? Я попробую объяснить на примере аргумента a. В момент вызова функции division(a, b, c); происходит следующее:
- сначала создается аргумент a функции division. Аргумент a функции division - это отдельная сущность, которая существует только в теле функции division
- Аргумент a функции division инициализируется значением аргумента a из функции main. Т.е. значение аргумента a из функции main копируется в аргумент a функции division. Несмотря на то, что эти две сущности называются одинаково, имеют один и тот же тип, одно и тоже значение, они больше никак не связаны: аргумент a из функции main никогда не узнает, что произойдет с аргументом a в функции divison. Абсолютно идентичные рассуждения можно применить к аргументам b и c. Получается, что аргумент c в функции main никогда не узнает, какое значение примет аргумент c в функции division
Может ли функция менять значения своих аргументов? Да.
Ссылки В С++ есть изящный механизм, который позволяет аргументам функции менять своё значение. Это механизм называется ссылкой
Ссылка - это другое имя объекта https://isocpp.org/wiki/faq/references#overview-refs
Пример:
int a = 1; // объект а
int& refA = a; // ссылка на объект a
Важные моменты
- отличительным признаком ссылки является использование символа амперсанда (&) после типа. Амперсанд должен находиться после типа и до имени ссылки, допустимые варианты
int a = 1; // объект а
int& refA0 = a;
int & refA1 = a;
int &refA2 = a;
- ссылка должна иметь тот же тип, что и объект, на который она ссылается
char c = '-';
int& cRef = c; // Error!
- ссылка должна быть проинициализирована при создании объектом, на который она ссылается. Нельзя создать неинициализированную ссылку, что вполне логично: ссылка - это псевдоним, а не самостоятельная сущность.
int& ref; // Error!
- над ссылкой можно выполнять все те же операции, что и над объектом, на который она ссылается
- т.к. ссылка - это псевдоним, то нет ничего удивительного, что, если присвоить ссылке новое значение, то изменится значение объекта; если присвоить объекту новое значение, изменится значение ссылки
int a = 1;
int& refA = a;
refA = 2;
std::cout << "a: " << a << '\n'; // Выведет a: 2
a = 3;
std::cout << "refA: " << refA << '\n'; // Выведет refA: 3
- Ссылка и объект, на который она ссылается, неразделимы после создания ссылки, т.е. ссылке нельзя присвоить другой объект.
int a = 1;
int& refA = a;
int b = 2;
int& refB = b;
// Здесь написано: присвоить объекту a значение объекта b
refA = refB;
std::cout << "refA: " << refA << '\n'; // Выведет refA: 2
// Проверим, что refA не ссылается на b
b = 3;
std::cout << "refA: " << refA << '\n'; // По-прежнему выведет refA: 2
Говоря кратко: ссылка позволяет работать с объектом напрямую.
Для аргументов функции действуют такие же правила объявления, как и для обычных переменных, значит, мы можем использовать ссылки в аргументах функции.
Перепишем функцию 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);
if (status == 0)
{
printResult(a, b, c, '/');
}
else if (status == -1)
{
std::cout << "Error! Division by zero is not allowed";
}
else
{
std::cout << "Unknown error at division operation";
}
}
Я добавил один символ, который позволит функции division работать, как и задумано: если b не равен 0, то в аргумент c теперь записывается результат операции деления, иначе аргумент c остается без изменений. Функция division работает не с копией переменной c из функции main, а непосредственно с самой переменной c из функции main.
В этом уроке мы узнали, что такое ссылка в С++. В следующем уроке ты узнаешь, как устроена ссылка “под капотом”!
На этом на сегодня всё. До встречи на следующем уроке!