Этот материал является продолжением урока №15.
Область видимости и продолжительность жизни
Прежде чем мы начнем, нам нужно сначала разобраться с двумя терминами: область видимости и продолжительность жизни. Область видимости определяет, где можно использовать переменную. Продолжительность жизни (или «время жизни») определяет, где переменная создается и где уничтожается. Эти две концепции связаны между собой.
Переменные, определенные внутри блока, называются локальными переменными. Локальные переменные имеют автоматическую продолжительность жизни: они создаются (и инициализируются, если необходимо) в точке определения и уничтожаются при выходе из блока. Локальные переменные имеют локальную область видимости (или «блочную»), т.е. они входят в область видимости с точки объявления и выходят в самом конце блока, в котором определены.
Например, рассмотрим следующую программу:
1 2 3 4 5 6 7 8 9 10 |
#include <iostream> int main() { int x(4); // переменная x создается и инициализируется здесь double y(5.0); // переменная y создается и инициализируется здесь return 0; } // x и y выходят из области видимости и уничтожаются здесь |
Поскольку переменные x
и y
определены внутри блока, который является главной функцией, то они обе уничтожаются, когда main() завершает свое выполнение.
Переменные, определенные внутри вложенных блоков, уничтожаются, как только заканчивается вложенный блок:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
#include <iostream> int main() // внешний блок { int m(4); // переменная m создается и инициализируется здесь { // начало вложенного блока double k(5.0); // переменная k создается и инициализируется здесь } // k выходит из области видимости и уничтожается здесь // Переменная k не может быть использована здесь, так как она уже уничтожена! return 0; } // переменная m выходит из области видимости и уничтожается здесь |
Такие переменные можно использовать только внутри блоков, в которых они определены. Поскольку каждая функция имеет свой собственный блок, то переменные из одной функции никак не соприкасаются и не влияют на переменные из другой функции:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> void someFunction() { int value(5); // value определяется здесь // value можно использовать здесь } // value выходит из области видимости и уничтожается здесь int main() { // value нельзя использовать внутри этой функции someFunction(); // value здесь также нельзя использовать return 0; } |
В разных функциях могут находиться переменные или параметры с одинаковыми именами. Это хорошо, потому что не нужно беспокоиться о возможности возникновения конфликтов имен между двумя независимыми функциями. В примере, приведенном ниже, в обеих функциях есть переменные x
и y
. Они даже не подозревают о существовании друг друга:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> // Параметр x можно использовать только внутри функции add() int add(int x, int y) // параметр x функции add() создается здесь { return x + y; } // параметр x функции add() уничтожается здесь // Переменную x функции main() можно использовать только внутри функции main() int main() { int x = 5; // переменная x функции main() создается здесь int y = 6; std::cout << add(x, y) << std::endl; // значение x функции main() копируется в переменную x функции add() return 0; } // переменная x функции main() уничтожается здесь |
Вложенные блоки считаются частью внешнего блока, в котором они определены. Следовательно, переменные, определенные во внешнем блоке, могут быть видны и внутри вложенного блока:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
#include <iostream> int main() { // начало внешнего блока int x(5); { // начало вложенного блока int y(7); // Мы можем использовать x и y здесь std::cout << x << " + " << y << " = " << x + y; } // переменная y уничтожается здесь // Переменную y здесь нельзя использовать, поскольку она уже уничтожена! return 0; } // переменная x уничтожается здесь |
Сокрытие имен
Переменная внутри вложенного блока может иметь то же имя, что и переменная внутри внешнего блока. Когда подобное случается, то переменная во вложенном (внутреннем) блоке «скрывает» внешнюю переменную. Это называется сокрытием имен:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
#include <iostream> int main() { // внешний блок int oranges(5); // внешняя переменная oranges if (oranges >= 5) // относится к внешней oranges { // вложенный блок int oranges; // скрывается внешняя переменная oranges // Идентификатор oranges теперь относится к вложенной переменной oranges. // Внешняя переменная oranges временно скрыта oranges = 10; // здесь мы присваиваем значение 10 вложенной переменной oranges, не внешней! std::cout << oranges << std::endl; // выводим значение вложенной переменной oranges } // вложенная переменная oranges уничтожается // Идентификатор oranges опять относится к внешней переменной oranges std::cout << oranges << std::endl; // выводим значение внешней переменной oranges return 0; } // внешняя переменная oranges уничтожается |
Результат выполнения программы:
10
5
Здесь мы сначала объявляем переменную oranges
во внешнем блоке. Затем объявляем вторую переменную oranges
, но уже во вложенном (внутреннем) блоке. Когда мы присваиваем oranges
значение 10
, то оно относится к переменной во вложенном блоке. После вывода этого значения (и окончания внутреннего блока), внутренняя переменная oranges
уничтожается, оставляя внешнюю oranges
с исходным значением (5
), которое затем выводится. Результат выполнения программы был бы тот же, даже если бы мы назвали вложенную переменную по-другому (например, nbOranges
).
Обратите внимание, если бы мы не определили вложенную переменную oranges
, то идентификатор oranges
относился бы к внешней переменной и значение 10
было бы присвоено внешней переменной:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> int main() { // внешний блок int oranges(5); // внешняя переменная oranges if (oranges >= 5) // относится к внешней переменной oranges { // вложенный блок // Никакого определения внутренней переменной oranges здесь нет oranges = 10; // это применяется к внешней переменной oranges, хоть мы и находимся во вложенном блоке std::cout << oranges << std::endl; // выводим значение внешней переменной oranges } // значением переменной oranges будет 10 даже после того, как мы выйдем из вложенного блока std::cout << oranges << std::endl; // выводим значение переменной oranges return 0; } // переменная oranges уничтожается |
Результат выполнения программы:
10
10
В обоих примерах на внешнюю переменную oranges
никак не влияет то, что происходит с вложенной переменной oranges
. Единственное различие между двумя программами — это то, к чему применяется выражение oranges = 10
.
Сокрытие имен — это то, чего, как правило, следует избегать, поскольку оно может быть довольно запутанным!
Правило: Избегайте использования вложенных переменных с именами, идентичными именам внешних переменных.
Область видимости переменных
Переменные должны определяться в максимально ограниченной области видимости. Например, если переменная используется только внутри вложенного блока, то она и должна быть определена в нем:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> int main() { // Не определяйте x здесь { // Переменная x используется только внутри этого блока, поэтому определяем её здесь int x(7); std::cout << x; } // В противном случае, переменная x может быть использована и здесь return 0; } |
Ограничивая область видимости, мы уменьшаем сложность программы, поскольку число активных переменных уменьшается. Таким образом, легче увидеть, где какие переменные используются. Переменная, определенная внутри блока, может использоваться только внутри этого же блока (или вложенных в него подблоков). Этим мы упрощаем понимание и логику программы.
Если во внешнем блоке нужна переменная, то её необходимо объявлять во внешнем блоке:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> int main() { int y(5); // мы объявляем переменную y здесь, потому что она нам будет нужна как во внутреннем, так и во внешнем блоке чуть позже { int x; std::cin >> x; // Если бы мы объявили y здесь, непосредственно перед её первым фактическим использованием, if (x == 4) y = 4; } // то она бы уничтожилась здесь std::cout << y; // а переменная y нам нужна еще здесь return 0; } |
Это один из тех редких случаев, когда вам может понадобиться объявить переменную до её первого использования.
Правило: Определяйте переменные в наиболее ограниченной области видимости.
Параметры функций
Хотя параметры функций не определяются внутри основного блока (тела) функции, в большинстве случаев они имеют локальную область видимости:
1 2 3 4 5 6 |
int max(int x, int y) // x и y определяются здесь { // Присваиваем большее из значений (x или y) переменной max int max = (x > y) ? x : y; // max определяется здесь return max; } // x, y и max уничтожаются здесь |
Заключение
Переменные, определенные внутри блоков, называются локальными переменными. Они доступны только внутри блока, в котором определены (включая вложенные блоки) и уничтожаются при завершении этого же блока.
Определяйте переменные в наиболее ограниченной области видимости. Если переменная используется только внутри вложенного блока, то и определять её следует внутри этого же вложенного блока.
Тест
Задание №1
Напишите программу, которая просит пользователя ввести два целых числа: второе должно быть больше первого. Если пользователь введет второе число меньше первого, то используйте блок и временную переменную, чтобы поменять местами пользовательские числа. Затем выведите значения этих переменных. Добавьте в свой код комментарии, объясняющие, где и какая переменная уничтожается.
Результат выполнения программы должен быть примерно следующим:
Введите число: 4
Введите большее число: 2
Меняем значения местами
Меньшее число: 2
Большее число: 4
Подсказка: Чтобы использовать кириллицу, добавьте следующую строчку кода в самое начало функции main():
1 |
setlocale(LC_ALL, "rus"); |
Ответ №1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
#include <iostream> int main() { // Используем кириллицу setlocale(LC_ALL, "rus"); std::cout << "Введите число: "; int smaller; std::cin >> smaller; std::cout << "Введите большее число: "; int larger; std::cin >> larger; // Если пользователь ввел числа не так, как нужно if (smaller > larger) { // То меняем местами эти значения std::cout << "Меняем значения местами\n"; int temp = larger; larger = smaller; smaller = temp; } // temp уничтожается здесь std::cout << "Меньшее число: " << smaller << "\n"; std::cout << "Большее число: " << larger << "\n"; return 0; } // smaller и larger уничтожаются здесь |
Задание №2
В чём разница между областью видимости и продолжительностью жизни переменной? Какую область видимости и продолжительность жизни по умолчанию имеют локальные переменные (и что это значит)?
Ответ №2
Область видимости определяет, где переменная доступна для использования. Продолжительность жизни переменной определяет, когда переменная создается и когда уничтожается.
Локальные переменные имеют локальную (блочную) область видимости, доступ к ним осуществляется только внутри блока, в котором они определены.
Локальные переменные имеют автоматическую продолжительность жизни, что означает, что они создаются в точке определения и уничтожаются в конце блока, в котором определены.
Конечно это не по теме урока, но просто тянется рука использовать условный тернарный оператор, который мы уже прошли в уроке 41. Короткий вариант программы:
main.ccp:
Добрый день!
В уроке 12
https://ravesli.com/urok-12-funktsii-i-return/#toc-4
указано —
"процессор встречает в функции оператор return, он немедленно выполняет возврат значения обратно в caller и точка выполнения также переходит в caller. Любой код, который находится за return-ом в функции — игнорируется."
В ответе №1 на Задание №1 Вы указали в комментарии
, что временные переменные уничтожаются ЗА ОПЕРАТОРОМ return
Вопрос- действительно ли это так? ведь за return уже ничего нет…
Т.е. уничтожение ДОЛЖНО произойти в момент return
В источнике https://docs.microsoft.com/ru-ru/cpp/cpp/functions-cpp?view=msvc-160
"Переменные, объявленные в теле функции, называются локальными. Они исчезают из области видимости при выходе из функции, поэтому функция никогда не должна возвращать ссылку на локальную переменную."
Есть также интересный эксперимент — с уничтожением временных объектов.
https://ru.stackoverflow.com/questions/430567/%D0%9F%D0%BE%D1%80%D1%8F%D0%B4%D0%BE%D0%BA-%D1%83%D0%BD%D0%B8%D1%87%D1%82%D0%BE%D0%B6%D0%B5%D0%BD%D0%B8%D1%8F-%D0%B2%D1%80%D0%B5%D0%BC%D0%B5%D0%BD%D0%BD%D1%8B%D1%85-%D0%BE%D0%B1%D1%8A%D0%B5%D0%BA%D1%82%D0%BE%D0%B2
Вот по этому и стоит учить ассемблер. Для понимания, что, как и когда делается. На самом деле ничего не уничтожается.
Молодец, Богдан. "На самом деле ничего не уничтожается."
При объявлении переменной в функции часть оперативной памяти выделяется под этот объект (урок 10) https://ravesli.com/urok-10-peremennye-initsializatsiya-i-prisvaivanie-v-s/
После возвращения из функции область памяти, где хранились переменные помечается 'как свободная' (для программистов там мусор). Значения в этой области или от прежних событий (т. е. ваши бывшие локальные переменный ), или перезаписаны сервисами операционной системы.
В любом случае доступ к этой области памяти через "псевдоимя" типа x или подобное ("объект с именем") — не доступен.
Блин, ни как не получается перевести иероглифы в кирилицу, я пробовал:
———————-
———————-
———————-
Может кто сталкивался, че подскажите?
компилирую через g++
Инклуде виндовх пишешь в самом верху.
Сет консоле сп 1251 пишешь сразу после инт мэйн в теле.
Так же файл блокнота тоде должен быть в кодировке 1251
В самом начале функции main нужно написать setlocale (LC_ALL, "Russian") и по идее всегда будет работать.
Интересно, а к каких нибудь компиляторах существует функция размена?
К примеру если мне нужно разменять переменные по идее достаточно изменить им выделенные адреса памяти (кстати тоже разменять).
Я вот не помню точно, в ассемблере вроде так можно было одной командой.
В задаче требуется использовать только 1 доп переменную
Думаю, так даже проще должно быть
Еще вариант:
хм, сделал немного наоборот, но вроде смысл тот же
У меня получился такой вариант — ibb.co/jJLATc. Сделал пару функций. Перед выполнением думал, что получится поменять без временной переменной (в моем случае Z).
Вы правильно сделали, разбив программу на функции. А насчет временной переменной, то без неё здесь не обойтись 🙂
Нашел вариант без временной переменной — onlinegdb.com/B1E-4RKpM.
Хоть и работает, но у вас здесь вместо одной временной переменной используются две временные переменные: xFunc и yFunc. + еще глобальные переменные, которые нежелательно лишний раз использовать.
Но в общем, реализация интересная однозначно.
В каком смысле "без дополнительной переменной не обойтись"? А как же классика?
PS: имена переменным поставил простейшие, чтобы было понятно, что происходит.
Юрий, доброго дня.
При решении теста №1, как сюда ввести вариант, если ввели равные числа?
Спасибо.
Если ввести правильные числа сразу, то они и выведутся после, без выполнения условия if. Дополнительно ничего прописывать не нужно. В условии if ведь и указывается, что менять значение нужно только при условии что значение smaller, которое ввел пользователь — больше значения large. Если же это не так, то ничего менять и не нужно.