Можно ли получить доступ к памяти локальной переменной вне ее области?

У меня есть следующий код.

#include <iostream>

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

И код просто выполняется без исключений во время выполнения!

Результат был 58

Как это может быть? Разве память локальной переменной не недоступна вне ее функции?


person Community    schedule 22.06.2011    source источник
comment
это даже не компилируется как есть; если вы исправите несоответствующий бизнес, gcc все равно предупредит address of local variable ‘a’ returned; valgrind показывает Invalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr   -  person sehe    schedule 22.06.2011
comment
В некоторых платформах / компиляторах (особенно старых компиляторах для DOS) вы даже можете писать через указатель NULL, и все кажется нормальным, пока вы не перезапишете что-то важное (например, выполняемый код). :)   -  person Serge Dundich    schedule 22.06.2011
comment
@Serge, это потому, что большинство операционных систем в наши дни имеют нулевую страницу с защитой от записи, но не все из них!   -  person Jasper Bekkers    schedule 22.06.2011
comment
@Serge: Еще в юности я однажды работал над каким-то хитрым кодом с нулевым кольцом, который работал в операционной системе Netware, который включал умное перемещение указателя стека способом, не совсем санкционированным операционной системой. Я знал, когда делал ошибку, потому что часто стек в конечном итоге перекрывал экранную память, и я мог просто наблюдать, как байты записывались прямо на дисплей. В наши дни такие вещи не сойдут с рук.   -  person Eric Lippert    schedule 23.06.2011
comment
Ах, чувак, из-за этого я скучаю по дням работы с C ++ / DCOM / VB. У нас было доморощенное красно-черное дерево, у которого были проблемы с доступом к неверным указателям. Я имел явное удовольствие отлаживать его.   -  person xanadont    schedule 23.06.2011
comment
@Jasper Bekkers: это потому, что большинство операционных систем в наши дни имеют нулевую страницу с защитой от записи, но не все они это делают! Да. Я знаю.   -  person Serge Dundich    schedule 23.06.2011
comment
@Xeo - Я думаю, вы меня неправильно поняли ... Я знаю, что это небезопасно, это точно! Я думал, что это будет невозможно. Думаю, мне следует привыкнуть к свободе, которую C ++ дает разработчику.   -  person Avi Shukron    schedule 23.06.2011
comment
ржу не могу. Мне нужно было прочитать вопрос и несколько ответов, прежде чем я даже понял, в чем проблема. Это действительно вопрос об области доступа к переменной? Вы даже не используете «а» вне своей функции. Вот и все. Обращение к некоторым ссылкам на память - это совершенно другая тема, нежели область видимости переменных.   -  person erikbwork    schedule 23.06.2011
comment
@Tomalak, пожалуйста, предоставьте дублирующую ссылку, и я рад проголосовать за закрытие. Мы можем попросить модератора объединить с вопросом, что этот - надувательство.   -  person Johannes Schaub - litb    schedule 23.06.2011
comment
Ответ на обман не означает вопрос об обмане. Многие ложные вопросы, которые здесь предлагали, - это совершенно разные вопросы, которые относятся к одному и тому же основополагающему симптому ... но задающий вопрос знает способ узнать это, поэтому они должны оставаться открытыми. Я закрыл старый обман и объединил его в этот вопрос, который должен оставаться открытым, потому что на него есть очень хороший ответ.   -  person Joel Spolsky    schedule 23.06.2011
comment
@Joel: Если ответ здесь хороший, его следует объединить с более старыми вопросами, из которых это обман, а не наоборот. И этот вопрос действительно представляет собой обман других вопросов, предложенных здесь, а затем некоторых (хотя некоторые из предложенных подходят лучше, чем другие). Обратите внимание: я думаю, что ответ Эрика хорош. (Фактически, я пометил этот вопрос для объединения ответов в один из старых вопросов, чтобы спасти старые вопросы.)   -  person sbi    schedule 23.06.2011
comment
@Joel dupe означает (цитата) Этот вопрос охватывает точно такую ​​же основу, что и предыдущие вопросы по этой теме ;, а не Этот вопрос охватывает точно такую ​​же основу, что и новый вопрос по этой теме ;. Либо у вашего слияния, либо у всплывающего окна закрытия оно перевернуто.   -  person Johannes Schaub - litb    schedule 23.06.2011
comment
Но в этом случае людям не нужно вручную щелкать прямую ссылку ... так что, возможно, это было хорошей идеей. Но все же слияние было обратным. Попытки оправдать, говоря, что это был правильный путь, не сработают.   -  person Johannes Schaub - litb    schedule 23.06.2011
comment
Странный вопрос с такой любовью, и я раньше думал, что разработчики C ДОЛЖНЫ понимать, как работает оборудование, распределение стека всегда было неизменным.   -  person bestsss    schedule 24.06.2011
comment
@Maxpm, нулевая страница на 8086 (и 0000: 0000 тоже) имеет свои применения - векторы прерываний и т. Д., Поэтому адресация была вполне нормальной. Раньше вирусы (и антивирусы) перезаписывали довольно много файлов.   -  person bestsss    schedule 24.06.2011
comment
Значит память перезаписана. В противном случае вы получили бы "55".   -  person Martin York    schedule 25.06.2011
comment
я имею в виду, что он не перезаписывается после выхода из функции foo. И я могу вывести его, даже если локальная переменная была уничтожена.   -  person Stals    schedule 25.06.2011
comment
@Stals Неопределенное поведение не определено. Вы не должны его использовать, и рассуждать об этом непродуктивно. Конечно, компилятор не тратит циклы на обнуление памяти, принадлежащей чему-то, что выходит за рамки. Вы по-прежнему не можете писать код, который использует что-то за пределами своей области, определенной языком. Если вы этого не сделаете, ваш код недействителен, независимо от того, дает ли он «ожидаемый» результат.   -  person underscore_d    schedule 07.07.2020


Ответы (21)


Как это может быть? Разве память локальной переменной не недоступна вне ее функции?

Вы снимаете номер в отеле. Вы кладете книгу в верхний ящик тумбочки и ложитесь спать. Вы выезжаете на следующее утро, но «забываете» вернуть ключ. Вы украли ключ!

Через неделю вы возвращаетесь в отель, не регистрируетесь, пробираетесь в свою старую комнату с украденным ключом и заглядываете в ящик. Ваша книга все еще там. Удивительный!

Как такое может быть? Разве содержимое ящика гостиничного номера недоступно, если вы не сняли номер?

Что ж, очевидно, что такой сценарий может произойти в реальном мире без проблем. Нет никакой таинственной силы, которая заставляет вашу книгу исчезать, когда вам больше не разрешено находиться в комнате. Нет и таинственной силы, которая мешает вам войти в комнату с украденным ключом.

Администрация отеля не обязана удалять вашу книгу. Вы не заключали с ними договор, в котором говорилось, что если вы оставите вещи, они их для вас разорвут. Если вы незаконно войдете в номер с украденным ключом, чтобы вернуть его, сотрудники службы безопасности отеля не обязаны задержать вас крадущимся. Вы не заключали с ними договор, в котором говорилось "если Я пытаюсь прокрасться в свою комнату позже, ты должен меня остановить ». Скорее, вы подписали с ними контракт, в котором говорилось: «Я обещаю не пробираться обратно в мою комнату позже», контракт, который вы разорвали.

В этой ситуации может случиться все, что угодно. Книга может быть - тебе повезло. Чужая книга может быть там, а ваша может быть в топке отеля. Кто-то мог быть там, когда вы входили, разрывая вашу книгу на части. В отеле можно было полностью убрать стол и забронировать номер и заменить его шкафом. Весь отель может быть снесен и заменен футбольным стадионом, и вы умрете от взрыва, пока будете красться.

Вы не знаете, что произойдет; когда вы выехали из отеля и украли ключ, чтобы использовать его незаконно, вы отказались от права жить в предсказуемом и безопасном мире, потому что вы решили нарушить правила системы.

C ++ - небезопасный язык. Это с радостью позволит вам нарушить правила системы. Если вы попытаетесь сделать что-то незаконное и глупое, например, вернуться в комнату, в которой вам не разрешено находиться, и рыться в столе, которого, возможно, даже нет, C ++ не остановит вас. Более безопасные языки, чем C ++, решают эту проблему, ограничивая ваши возможности - например, за счет более строгого контроля над ключами.

ОБНОВИТЬ

Боже мой, этот ответ привлекает много внимания. (Я не уверен, почему - я считал это просто забавной аналогией, но неважно.)

Я подумал, что было бы уместно немного обновить это, добавив еще несколько технических мыслей.

Компиляторы создают код, который управляет хранением данных, обрабатываемых этой программой. Существует множество различных способов создания кода для управления памятью, но со временем закрепились два основных метода.

Первый - иметь своего рода "долгоживущую" область хранения, где "время жизни" каждого байта в памяти - то есть период времени, когда он корректно связан с некоторой программной переменной - нельзя легко предсказать заранее. времени. Компилятор генерирует вызовы «диспетчера кучи», который знает, как динамически выделять память, когда она нужна, и возвращать ее, когда она больше не нужна.

Второй метод - иметь «недолговечную» область хранения, в которой время жизни каждого байта хорошо известно. Здесь воплощения следуют шаблону «вложенности». Самые долгоживущие из этих короткоживущих переменных будут выделены перед любыми другими короткоживущими переменными и будут освобождены в последнюю очередь. Короткоживущие переменные будут размещены после самых долгоживущих и будут освобождены до них. Время жизни этих короткоживущих переменных «вложено» в срок жизни более долгоживущих.

Локальные переменные следуют последнему шаблону; когда вводится метод, оживают его локальные переменные. Когда этот метод вызывает другой метод, оживают локальные переменные нового метода. Они умрут раньше, чем умрут локальные переменные первого метода. Относительный порядок начала и окончания срока службы хранилищ, связанных с локальными переменными, может быть определен заранее.

По этой причине локальные переменные обычно генерируются как хранилище в структуре данных «стек», потому что стек обладает тем свойством, что первое, что в него помещено, будет последним, что выскочит.

Это похоже на то, что отель решает сдавать комнаты только последовательно, и вы не можете выписаться, пока все, у кого номер комнаты выше, чем вы выселили.

Итак, давайте подумаем о стеке. Во многих операционных системах вы получаете один стек на поток, и стек выделяется определенного фиксированного размера. Когда вы вызываете метод, все содержимое помещается в стек. Если вы затем передадите указатель на стек обратно из вашего метода, как это делает исходный плакат, это будет просто указатель на середину некоторого полностью допустимого блока памяти размером в миллион байт. В нашей аналогии вы выезжаете из отеля; когда вы это сделаете, вы только что выехали из занятой комнаты с самым большим номером. Если после вас никто не зарегистрируется, и вы вернетесь в свой номер незаконно, все ваши вещи гарантированно останутся там в этом конкретном отеле.

Мы используем стеки для временных складов, потому что они действительно дешевы и просты. Реализация C ++ не требуется для использования стека для хранения локальных переменных; он мог использовать кучу. Это не так, потому что это замедлит работу программы.

Реализация C ++ не обязана оставлять мусор, который вы оставили в стеке, нетронутым, чтобы вы могли вернуться за ним позже незаконно; компилятор совершенно законно генерирует код, который обнуляет все в «комнате», которую вы только что освободили. Это не так, потому что, опять же, это было бы дорого.

Реализация C ++ не требуется, чтобы гарантировать, что когда стек логически сжимается, адреса, которые раньше были действительными, все еще отображались в памяти. Реализации разрешено сообщать операционной системе: «Мы закончили использовать эту страницу стека. Пока я не скажу иначе, создайте исключение, которое уничтожит процесс, если кто-либо коснется ранее действующей страницы стека». Опять же, реализации на самом деле этого не делают, потому что это медленно и ненужно.

Вместо этого реализации позволяют совершать ошибки и избегать наказания за них. Большую часть времени. Пока однажды что-то действительно ужасное не пойдет не так, и процесс взорвется.

Это проблематично. Правил очень много, и их очень легко случайно нарушить. Я, конечно, много раз. И что еще хуже, проблема часто возникает только тогда, когда обнаруживается, что память повреждена через миллиарды наносекунд после того, как произошло повреждение, когда очень трудно выяснить, кто это испортил.

Более безопасные для памяти языки решают эту проблему, ограничивая ваши возможности. В «обычном» C # просто нет возможности взять адрес локального и вернуть его или сохранить на потом. Вы можете взять адрес локального, но язык продуман так, что его невозможно использовать после окончания срока жизни локального. Чтобы взять адрес локального компьютера и передать его обратно, вы должны перевести компилятор в специальный «небезопасный» режим, и добавить слово «небезопасно» в вашу программу, чтобы привлечь внимание к факт, что вы, вероятно, делаете что-то опасное, что может нарушать правила.

Для дальнейшего чтения:

  • Что, если бы C # позволял возвращать ссылки? По совпадению, это тема сегодняшнего сообщения в блоге:

    https://ericlippert.com/2011/06/23/ref-returns-and-ref-locals/

  • Почему мы используем стеки для управления памятью? Всегда ли типы значений в C # хранятся в стеке? Как работает виртуальная память? И многие другие темы о том, как работает диспетчер памяти C #. Многие из этих статей также актуальны для программистов на C ++:

    https://ericlippert.com/tag/memory-management/

person Eric Lippert    schedule 22.06.2011
comment
Если бы отель собирался заменить футбольным стадионом, разве вы не заметили бы недостатка людей? Или чудовищная армия гигантских бульдозеров снаружи? - person Mateen Ulhaq; 23.06.2011
comment
@muntoo: К сожалению, операционная система не издает предупреждающую сирену перед тем, как списать или освободить страницу виртуальной памяти. Если вы возитесь с этой памятью, когда она больше не принадлежит вам, операционная система полностью вправе прекратить весь процесс, когда вы касаетесь освобожденной страницы. Бум! - person Eric Lippert; 23.06.2011
comment
Мне нравится эта аналогия, но почти все отели используют программируемые карточки-ключи, которые блокируются в определенное время или когда для этого номера выдается новый ключ, в зависимости от того, что наступит раньше. И я предполагаю, что очень немногие отели, которые не используют такую ​​систему, будут очень настойчиво требовать, чтобы вы вернули ключ при оформлении заказа. - person Kyle Cronin; 23.06.2011
comment
Это отличная аналогия, но кричать о C ++ в конце нельзя. C ++ не налагает слишком много ограничений, но это отсутствие ограничений обычно окупается ощутимым приростом производительности. - person cyberguijarro; 23.06.2011
comment
@Kyle: Это делают только безопасные отели. Небезопасные отели получают ощутимую прибыль от того, что им не нужно тратить время на программирование ключей. - person Alexander Torstling; 23.06.2011
comment
@cyberguijarro Я не думаю, что в конце он критикует C ++. C ++ небезопасен, и, как вы говорите, во многих ситуациях это хорошо. Точно так же более безопасные языки менее эффективны, но могут быть проще в использовании. Они просто разные. - person Edd; 23.06.2011
comment
@cyberguijarro: То, что C ++ небезопасен для памяти, - это просто факт. Это ни к чему не приставляет. Если бы я сказал, например, что C ++ - это ужасная мешанина недоопределенных, чрезмерно сложных функций, нагроможденных поверх хрупкой и опасной модели памяти, и я благодарен каждый день, что я больше не работаю над ней из соображений собственного здравомыслия, что ругать C ++. Указание на то, что это небезопасно для памяти, объясняет, почему исходный плакат видит эту проблему; это ответ на вопрос, а не редакционная статья. - person Eric Lippert; 23.06.2011
comment
@Eric: C # (на самом деле, .NET) тоже небезопасен в этом отношении. Я могу комбинировать Math.Random, IntPtr и Marshal.Copy и вызывать полный хаос (не требуется ни ключевое слово unsafe, ни переключатель компилятора /unsafe). Безопасность проистекает из соблюдения контракта, а не из языкового дизайна (хотя язык может и должен максимально упростить кодирование в стиле, который придерживается контракта, и предупреждать, когда контракт максимально нарушается). - person Ben Voigt; 23.06.2011
comment
Хорошее объяснение, Эрик. Быстрый вопрос! Какой язык вы бы назвали более безопасным ?! - person Bitmap; 23.06.2011
comment
@Bitmap: LOGO вполне безопасен. - person Ben Voigt; 23.06.2011
comment
@Ben ну да, очевидно, есть способы стать небезопасными, которые включают в себя библиотечные функции, отмеченные как таковые (так что разрешения включаются, если требуются). Если бы кто-то реализовал LOGO с библиотечной функцией, допускающей моральные эквиваленты intptr, тогда он перестал бы быть безопасным и для вашей метрики. - person ShuggyCoUk; 23.06.2011
comment
Строго говоря, аналогия должна указывать на то, что администратор в отеле был очень рад, что вы взяли ключ с собой. О, не возражаешь, если я возьму с собой этот ключ? Вперед, продолжать. Зачем мне все равно? Я только здесь работаю. Это не станет незаконным, пока вы не попытаетесь его использовать. - person philsquared; 23.06.2011
comment
@PhilNash: Он там немного ломается, так как ключ обычно является собственностью отеля. - person Lightness Races in Orbit; 23.06.2011
comment
@Ben: @ShuggyCoUk прав; то, что есть библиотечные функции, которые делают ужасные вещи, если вы неправильно их используете, - это свойство этих библиотечных функций, а не языка C #. C # язык безопасен для памяти и безопасен по типу при условии, что в нем нет небезопасных блоков кода. Если да, то он так же небезопасен для памяти, как C ++. Дело в том, чтобы изолировать небезопасные области памяти от областей, которые можно легко идентифицировать и тщательно изучить. - person Eric Lippert; 23.06.2011
comment
@ Кайл Кронин, твоя точка зрения лишь усиливает аналогию. Когда был изобретен C ++, программируемые карточные ключи для отелей были менее распространены или вообще отсутствовали. Новые отели, естественно, приняли более безопасные методы, как и более новые языки. Даже старые отели были модернизированы новыми замками, как и C ++ (кто-нибудь умные указатели?) - person Mark Ransom; 23.06.2011
comment
С ++, небезопасный для памяти, делает его прагматичным. Можно использовать некоторые уловки, и их бы не было, если бы C ++ был слишком безопасным. - person Thaddee Tyl; 23.06.2011
comment
@Thaddee: Во-первых, существует множество прагматических языков, безопасных для памяти. Однако проблема C ++ не в том, что он небезопасен. Проблема в том, что так легко случайно сделать что-то в высшей степени небезопасное и не осознавать этого, пока не выйдет из строя машина конечного пользователя. Я согласен с тем, что языки с небезопасной памятью часто бывают весьма полезны, но должен быть способ изолировать эту небезопасность именно от тех хитроумных фрагментов кода, которые действительно в ней нуждаются. - person Eric Lippert; 23.06.2011
comment
Эрик: Этот вопрос может привлекать трафик, потому что это была самая популярная публикация в Hacker News: news.ycombinator. ru / item? id = 2686580. Тем не менее, 1100 голосов за 24 часа ?! Это, должно быть, рекорд. - person Steve Tjoa; 23.06.2011
comment
Пожалуйста, пожалуйста, хотя бы подумайте о написании книги в один прекрасный день. Я бы купил его, даже если бы это был просто сборник отредактированных и расширенных сообщений в блоге, и я уверен, что многие люди тоже. Но было бы неплохо прочитать книгу с вашими оригинальными мыслями по различным вопросам, связанным с программированием. Я знаю, что найти на это время невероятно сложно, но, пожалуйста, подумайте о том, чтобы написать его. - person Dyppl; 24.06.2011
comment
@Dyppl: Спасибо за добрые слова. Написав уже пару книг, я прекрасно понимаю, сколько это стоит работы! Я подумал о том, чтобы превратить блог в книгу, и возможно, когда-нибудь, если найду и время, и желающего издателя. - person Eric Lippert; 24.06.2011
comment
На самом деле существует три основных метода управления памятью в C и C ++. Есть два, о которых вы упомянули, плюс static память, в которой переменные имеют время жизни процесса. И если вы не против углубиться в технические подробности, есть также хранилище регистровых файлов, но, по-видимому, это игнорируется текущими компиляторами. - person ThomasMcLeod; 06.06.2013
comment
Так как же часто я нахожу эту книгу каждый раз, когда пробираюсь в одну и ту же комнату? Кроме того, от каких факторов зависит эта частота? - person chosentorture; 27.06.2013
comment
@chosentorture ответит на ваш вопрос наукой. Получите несколько сотен компиляторов c и попробуйте несколько сотен различных конфигураций каждого из них, и вскоре вы получите отличные эмпирические данные. Все остальное - догадки. - person Eric Lippert; 27.06.2013
comment
Я написал много кода на разных языках. Моим наименее любимым языком был C ++. У меня нет проблем с небезопасными языками - у меня проблемы с языком, который настолько плохо спроектирован (а затем взломан, чтобы скрыть эти недостатки дизайна), что СЛИШКОМ ЛЕГКО создать код, который дает сбой. Я сейчас имею дело с проблемой, которая ПРЕДНАЗНАЧЕНА для устранения утечек памяти. Теперь он неисправен. Веселье. -_- - person Lloyd Sargent; 01.10.2013
comment
Применимо ли то же самое к локальным переменным внутри одной и той же функции, объявленным в другой области? Я спрашиваю, поскольку, по моему опыту, и GCC, и MSVC: а) не предупреждают (даже с -Wextra) об использовании указателя на переменную в другой области и б) создают сборку, которая предлагает отслеживать использование каждой переменной с помощью указателей. даже за пределами области действия переменной. пример: void foo (void) {int i, * px; для (i = 0; i ‹10; i + = * px) {int x = i + 1; px =} printf (* px =% i \ n, * px); } - person Timo; 04.11.2013
comment
@timo вы обязаны никогда не использовать адрес для локального пользователя, чье время жизни закончилось. Если вы это сделаете, и это сработает, ну, опять же, среда выполнения не обязана давать сбой, когда вы нарушаете правила. Небезопасный код помечен как небезопасный не просто так. - person Eric Lippert; 04.11.2013
comment
@EricLippert Я думаю, что это лучшая аналогия, которую я когда-либо видел по этой теме, но у меня есть одна путаница, которую вы написали, что ваша может быть в топке отеля, это означает, что моя ценность может быть там в другом месте в системе или что-то еще, что вы попробуете объяснять ? - person Vikas Verma; 25.01.2014
comment
@VikasVerma: Некоторые менеджеры памяти намеренно уничтожают память, когда она больше не используется. Отладочная версия среды выполнения Microsoft C, например, устанавливает для неиспользуемой памяти значение 0xCC, потому что (1) в окне памяти отладчика очень легко увидеть, что конкретный блок памяти больше не действителен, и (2) это взлом кода инструкции отладчика; если измельченная память когда-либо будет выполнена, то отладчик будет активирован. - person Eric Lippert; 26.01.2014
comment
Я на самом деле не согласен с этим как с «ответом» после того, как мне прислали ссылку, поскольку он не отвечал на явную причину путаницы для плаката. Он явно считает, что, подобно объекту, функция содержит собственное локальное хранилище и, следовательно, не существует после уничтожения функции. Только на самом деле он не разрушен. В отличие от классов, они не являются контейнером для используемых в них переменных, это элементы, хранящиеся в стеке или в регистре. Однако этот ответ - отличная аналогия того, как управляемый доступ к стеку может (и не может) работать. Однако важнее технический ответ. - person Deji; 22.04.2014
comment
@Deji: Ваши экстрасенсорные способности намного сильнее моих; Понятия не имею, о чем думал оригинальный плакат. - person Eric Lippert; 22.04.2014
comment
@ErricLippert Это не экстрасенсорные способности, а более глубокое знакомство с путаницей, примером, который он использует, и фактическим вопросом, который он задал. Он спросил, была ли память недоступна, что означает, что он, вероятно, думает, что памяти больше не существует. И то, и другое неверно, память доступна и существует. Причина этого заключается в различии способов их хранения, поэтому я думаю, что «ответ» должен сосредоточиться на этом на техническом уровне. - person Deji; 22.04.2014
comment
@VikasVerma Это означает, что ваш ящик реконструируется. - person joey rohan; 13.08.2014
comment
@EricLippert: Спасибо за отличный ответ. Что вы думаете о способах бесплатного управления магазином в C ++ 11 и C ++ 14 с помощью интеллектуальных указателей? Могу ли я теперь сказать, что современный C ++ - безопасный язык, потому что нет необходимости использовать оператор удаления - person Destructor; 02.03.2015
comment
@meet: Я ни в коем случае не являюсь экспертом в том, что было добавлено в C ++ 11 и 14, хотя, когда я разговариваю с людьми, которые являются экспертами, мне кажется, что там много хороших вещей. В целом, я рад видеть, что комитет C ++ готов быть смелым и активным, поскольку они продвигают язык к чему-то более современному и менее подверженному ошибкам. - person Eric Lippert; 02.03.2015
comment
@EricLippert: Хорошо. Но вопрос в том, почему C ++ не остановит меня, если я сделаю глупость? Разве не было бы очень хорошо, если бы компилятор выдает ошибку, когда я пытаюсь получить адрес локальной переменной? Почему C ++ предоставляет программисту столько свободы? Или это проблемы C ++ унаследованные от C? Ваша помощь будет оценена по достоинству. - person Destructor; 02.03.2015
comment
@meet: вы должны задать эти вопросы тому, кто хорошо разбирается в дизайне C ++; Я бы не стал рассуждать о мотивах разработчиков языка C ++. Я хотел бы отметить, что запретить пользователю делать что-то глупое, похоже, не было слишком высоко в списке черт, которые дизайнеры C. - person Eric Lippert; 02.03.2015
comment
@Meet Помните, что C ++ - это общий язык поддержки. Полный контроль памяти необходим для широкого спектра приложений (инструменты взлома). - person Edwin Rodríguez; 27.08.2015
comment
@EricLippert: C ++ не определяет такое поведение, правда. Но я думаю, что если вы работаете на x86, архитектура гарантирует, что вы можете безопасно писать и читать до 128 байт выше указателя стека (esp), не рискуя изменением памяти. Теперь, если компилятор не компилирует никаких инструкций, которые активно изменяют эту память (что, вероятно, так, поскольку он просто увеличит esp и вернется назад при выходе из функции), я думаю, вы могли бы технически сказать, что на x86 это определенное поведение. Это правда? Есть мысли по этому поводу? - person Martijn Courteaux; 28.08.2015
comment
@MartijnCourteaux: Кто сказал, что компилятор должен использовать esp для определения местоположения локальных переменных? Если конкретный поставщик компилятора определяет поведение для конкретной реализации, то это поведение определяется реализацией. - person Eric Lippert; 28.08.2015
comment
@EricLippert: Я не понимаю, куда вы идете. Может ли быть правдой, что в определенной комбинации архитектура / компилятор это приводит к согласованному поведению, даже если локальные переменные теперь находятся над указателем стека (&var < esp)? Например: я пробовал x86_64 с gcc -O0, и он создает код, который, как мне кажется, даст согласованные результаты. - person Martijn Courteaux; 28.08.2015
comment
@MartijnCourteaux: Неопределенное поведение может делать все, что угодно. Последовательное поведение - это подмножество чего угодно, так что да, это возможно. Куда я пойду: вы спрашиваете, является ли поведение, определяемое конкретной реализацией, разновидностью поведения, определяемого реализацией. Да, это так. - person Eric Lippert; 28.08.2015
comment
В последнем абзаце перед обновлением вы говорите что-то о том, что C ++ не является безопасным языком [...] Более безопасные языки, такие как C ++ [....] Вы хотели сказать, что C # безопаснее? ? - person canon; 29.01.2016
comment
Было бы неплохо, если бы в ответе хоть раз упоминалось слово undefined behavior - person Giorgi Moniava; 29.01.2016
comment
@GiorgiMoniava: Комментарий отмечен. Подумайте о том, чтобы написать ответ, который вам больше нравится; таким образом улучшается весь сайт. - person Eric Lippert; 30.01.2016
comment
@EricLippert Ваш ответ уже довольно хорош, я не пытаюсь его критиковать, просто написал свое мнение. Спасибо. Будет непросто / реалистично написать здесь что-то лучше, чем текущие ответы. - person Giorgi Moniava; 30.01.2016
comment
Я должен согласиться с @Dyppl, что я хотел бы прочитать книгу, написанную вами. Наряду с некоторыми сообщениями / ответами в блоге, написанными командой jOOQ или Джошем Блохом / Гетцем, ваши ответы содержат действительно подробный и легко понятный материал о закулисных / скрытых деталях, касающихся языков программирования. - person Abdul; 15.12.2016
comment
C ++ - небезопасный язык. И бензопилы тоже небезопасны, но пока вы используете их правильно, они настолько намного лучше, чем альтернатива :-) Если, конечно, альтернативой не является Python. - person paxdiablo; 04.09.2020
comment
@Destructor Но вопрос в том, почему C ++ не остановит меня, если я сделаю глупость? Разве не было бы очень хорошо, если бы компилятор выдает ошибку, когда я пытаюсь получить адрес локальной переменной? Почему C ++ предоставляет программисту столько свободы? Пусть разработчик C ++ ответит на это: stroustrup.com/bs_faq.html#unsafe - person Jerry Jeremiah; 23.11.2020

Здесь вы просто читаете и записываете в память, которая раньше была адресом a. Теперь, когда вы находитесь за пределами foo, это просто указатель на какую-то случайную область памяти. Так уж получилось, что в вашем примере эта область памяти действительно существует, и в данный момент больше ее не использует. Вы ничего не сломаете, продолжая его использовать, и ничто другое еще не перезаписало его. Следовательно, 5 все еще там. В реальной программе эта память будет повторно использована почти сразу, и вы что-нибудь сломаете, сделав это (хотя симптомы могут появиться гораздо позже!)

Когда вы возвращаетесь из foo, вы сообщаете ОС, что больше не используете эту память, и ее можно переназначить на что-то другое. Если вам повезет, и он никогда не будет переназначен, и ОС не поймает вас на его повторном использовании, то ложь вам сойдет с рук. Скорее всего, вы в конечном итоге переписываете все, что заканчивается этим адресом.

Теперь, если вам интересно, почему компилятор не жалуется, возможно, это потому, что foo был исключен оптимизацией. Обычно он предупреждает вас об этом. C предполагает, что вы знаете, что делаете, и технически вы здесь не нарушили область видимости (нет ссылки на сам a за пределами foo), только правила доступа к памяти, которые вызывают только предупреждение, а не ошибку.

Вкратце: обычно это не срабатывает, но иногда случается.

person Rena    schedule 23.06.2011

Потому что пространство для хранения еще не было задействовано. Не рассчитывай на такое поведение.

person msw    schedule 19.05.2010
comment
Чувак, это было самое долгое ожидание комментария с тех пор, что такое правда? - сказал шутливо Пилат. Может быть, это была Библия Гидеона в ящике отеля. И что вообще с ними случилось? Обратите внимание, что их больше нет, по крайней мере, в Лондоне. Я предполагаю, что в соответствии с законодательством о равенстве вам понадобится библиотека религиозных трактатов. - person Rob Kent; 16.07.2015
comment
Я мог бы поклясться, что написал это давно, но недавно он всплыл и обнаружил, что моего ответа там нет. Теперь мне нужно выяснить ваши намеки выше, поскольку я ожидаю, что меня позабавит, когда я это сделаю ›.‹ - person msw; 17.07.2015
comment
Ха-ха. Фрэнсис Бэкон, один из величайших эссеистов Великобритании, который, как некоторые подозревают, написал пьесы Шекспира, потому что они не могут согласиться с тем, что школьник из сельской школы, сын Гловера, мог быть гением. Такова система классов английского языка. Иисус сказал: «Я есмь Истина». oregonstate.edu/instruct/phl302/texts/bacon/bacon_essays.html - person Rob Kent; 17.07.2015

Небольшое дополнение ко всем ответам:

если вы сделаете что-то подобное:

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

вывод, вероятно, будет: 7

Это потому, что после возврата из foo () стек освобождается и затем повторно используется boo (). Если вы разберете исполняемый файл, вы увидите его отчетливо.

person Community    schedule 25.06.2011
comment
Простой, но отличный пример для понимания лежащей в основе теории стека. Всего одно тестовое дополнение, объявляющее int a = 5; в foo () как static int a = 5; может использоваться для понимания объема и времени жизни статической переменной. - person control; 07.05.2013
comment
-1 для вероятно будет 7. Компилятор может зарегистрировать a в boo. Он может удалить его, потому что в нем нет необходимости. Велика вероятность, что * p не будет 5, но это не значит, что есть какая-то особо веская причина, по которой вероятно, будет 7. - person Matt; 09.10.2013
comment
Это называется неопределенным поведением! - person Francis Cugler; 27.03.2015
comment
почему и как boo повторно использует foo стек? не отделены друг от друга стеки функций, также я получаю мусор при запуске этого кода в Visual Studio 2015 - person ampawd; 22.08.2016
comment
@ampawd ему почти год, но нет, стеки функций не отделены друг от друга. КОНТЕКСТ имеет стек. Этот контекст использует свой стек для входа в main, затем спускается в foo(), существует, затем спускается в boo(). Foo() и Boo() оба входят с указателем стека в одном и том же месте. Однако это не то поведение, на которое следует полагаться. Другие «вещи» (например, прерывания или ОС) могут использовать стек между вызовами boo() и foo(), изменяя его содержимое ... - person Russ Schultz; 16.07.2017

В C ++ вы можете получить доступ к любому адресу, но это не значит, что вы должны. Адрес, к которому вы обращаетесь, больше не действителен. Это работает, потому что больше ничего не скремблирует память после возврата foo, но при многих обстоятельствах может произойти сбой. Попробуйте проанализировать свою программу с помощью Valgrind или просто скомпилировать ее с оптимизацией и посмотреть ...

person Charles Brunet    schedule 22.06.2011
comment
Вы, вероятно, имеете в виду, что можете попытаться получить доступ к любому адресу. Потому что большинство современных операционных систем не позволяют какой-либо программе получить доступ к любому адресу; существует масса мер предосторожности для защиты адресного пространства. Вот почему не будет другого LOADLIN.EXE. - person v010dya; 12.05.2015

Вы никогда не генерируете исключение C ++, обращаясь к недействительной памяти. Вы просто приводите пример общей идеи ссылки на произвольную ячейку памяти. Я мог бы сделать то же самое:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Здесь я просто обрабатываю 123456 как адрес двойника и пишу ему. Может случиться любое количество вещей:

  1. q на самом деле может быть действительным адресом двойника, например double p; q = &p;.
  2. q может указывать где-то внутри выделенной памяти, и я просто перезаписываю там 8 байтов.
  3. q указывает за пределы выделенной памяти, и диспетчер памяти операционной системы отправляет моей программе сигнал об ошибке сегментации, в результате чего среда выполнения завершает ее.
  4. Вы выигрываете в лотерею.

То, как вы его настроили, немного более разумно, чтобы возвращаемый адрес указывал на допустимую область памяти, поскольку он, вероятно, будет немного дальше по стеку, но это все еще недопустимое место, к которому вы не можете получить доступ в детерминированная мода.

Никто не будет автоматически проверять семантическую валидность таких адресов памяти для вас при нормальном выполнении программы. Однако отладчик памяти, такой как valgrind, с радостью сделает это, поэтому вам следует запустить свою программу через него и увидеть ошибки.

person Kerrek SB    schedule 22.06.2011
comment
Я просто собираюсь написать программу, которая будет продолжать ее выполнение, чтобы 4) I win the lottery - person Aidiakapi; 11.03.2015

Вы скомпилировали свою программу с включенным оптимизатором? Функция foo() довольно проста и могла быть встроена или заменена в результирующем коде.

Но я согласен с Марком Б в том, что результирующее поведение не определено.

person gastush    schedule 22.06.2011
comment
Это моя ставка. Оптимизатор сбросил вызов функции. - person Erik Aronesty; 22.06.2011
comment
В этом нет необходимости. Поскольку после foo () новая функция не вызывается, локальный стековый фрейм функций просто еще не перезаписан. Добавьте еще один вызов функции после foo (), и 5 будет изменен ... - person Tomas; 23.06.2011
comment
Я запустил программу с GCC 4.8, заменив cout на printf (включая stdio). Правильно предупреждает предупреждение: возвращен адрес локальной переменной «a» [-Wreturn-local-addr]. Выводит 58 без оптимизации и 08 с -O3. Как ни странно, у P есть адрес, хотя его значение равно 0. Я ожидал NULL (0) в качестве адреса. - person Kevin; 04.05.2017

Ваша проблема не имеет ничего общего с областью действия. В показанном вами коде функция main не видит имен в функции foo, поэтому вы не можете получить доступ к a в foo напрямую с этим именем за пределами foo.

Проблема, с которой вы столкнулись, заключается в том, почему программа не сигнализирует об ошибке при обращении к недопустимой памяти. Это связано с тем, что стандарты C ++ не определяют четкой границы между незаконной памятью и допустимой памятью. Ссылка на что-то в раскрывающемся стеке иногда вызывает ошибку, а иногда - нет. Это зависит. Не рассчитывайте на такое поведение. Предположим, что это всегда будет приводить к ошибке при программировании, но предположим, что он никогда не будет сигнализировать об ошибке при отладке.

person Chang Peng    schedule 23.06.2011
comment
Я вспоминаю старую копию Turbo C Programming для IBM, которую я использовал для экспериментов в прошлом, когда, как напрямую управлять графической памятью и компоновкой видеопамяти IBM в текстовом режиме , было описано очень подробно. Конечно, тогда система, в которой работал код, четко определила, что означает запись по этим адресам, так что пока вы не беспокоились о переносимости на другие системы, все было в порядке. IIRC, указатели на void были общей темой в этой книге. - person user; 23.06.2011
comment
@ Майкл Кьёрлинг: Конечно! Людям нравится время от времени делать какую-нибудь грязную работу;) - person Chang Peng; 23.06.2011

Вы просто возвращаете адрес памяти, это разрешено, но, вероятно, это ошибка.

Да, если вы попытаетесь разыменовать этот адрес памяти, у вас будет неопределенное поведение.

int * ref () {

 int tmp = 100;
 return &tmp;
}

int main () {

 int * a = ref();
 //Up until this point there is defined results
 //You can even print the address returned
 // but yes probably a bug

 cout << *a << endl;//Undefined results
}
person Brian R. Bondy    schedule 19.05.2010
comment
Я не согласен: есть проблема перед cout. *a указывает на нераспределенную (освобожденную) память. Даже если вы не снимете защиту, это все равно опасно (и, скорее всего, подделка). - person ereOn; 19.05.2010
comment
@ereOn: Я пояснил, что имел в виду под проблемой, но нет, это не опасно с точки зрения правильного кода на C ++. Но это опасно с точки зрения вероятности того, что пользователь ошибся и сделает что-то плохое. Возможно, например, вы пытаетесь увидеть, как растет стек, и заботитесь только о значении адреса и никогда не будете его разыменовывать. - person Brian R. Bondy; 19.05.2010

Это классическое неопределенное поведение, которое обсуждалось здесь не более двух дней назад - немного поищите по сайту. Короче говоря, вам повезло, но могло случиться что угодно, и ваш код делает недопустимый доступ к памяти.

person Kerrek SB    schedule 24.06.2011

Как отметил Алекс, это поведение не определено - на самом деле, большинство компиляторов предупреждают об этом, потому что это простой способ получить сбои.

В качестве примера жуткого поведения, которое вы вероятно столкнетесь, попробуйте этот пример:

int *a()
{
   int x = 5;
   return &x;
}

void b( int *c )
{
   int y = 29;
   *c = 123;
   cout << "y=" << y << endl;
}

int main()
{
   b( a() );
   return 0;
}

Это напечатает «y = 123», но ваши результаты могут отличаться (действительно!). Ваш указатель затирает другие, не связанные локальные переменные.

person AHelps    schedule 24.06.2011

Обратите внимание на все предупреждения. Не только устраняйте ошибки.
GCC показывает это предупреждение

предупреждение: возвращен адрес локальной переменной 'a'

В этом сила C ++. Вам следует заботиться о памяти. С флагом -Werror это предупреждение становится ошибкой, и теперь вам нужно его отладить.

person Community    schedule 17.08.2015

Это работает, потому что стек не изменялся (пока) с тех пор, как туда был помещен. Вызовите несколько других функций (которые также вызывают другие функции), прежде чем снова получить доступ к a, и вам, вероятно, больше не повезет ... ;-)

person Adrian Grigore    schedule 23.06.2011

Фактически вы вызвали неопределенное поведение.

Возвращение адреса временных работ, но поскольку временные объекты уничтожаются в конце функции, результаты доступа к ним будут неопределенными.

Таким образом, вы не изменили a, а изменили место в памяти, где когда-то находился a. Эта разница очень похожа на разницу между сбоями и отсутствием сбоев.

person Alexander Gessler    schedule 24.06.2011

В типичных реализациях компилятора код можно представить как «распечатать значение блока памяти с адресом, который раньше был занят». Кроме того, если вы добавляете вызов новой функции к функции, которая содержит локальный int, велика вероятность того, что значение a (или адрес памяти, на который указывает a) изменится. Это происходит потому, что стек будет перезаписан новым фреймом, содержащим другие данные.

Однако это неопределенное поведение, и вам не следует полагаться на его работу!

person larsmoa    schedule 22.06.2011
comment
выводить значение блока памяти с адресом, который раньше был занят, не совсем правильно. Это заставляет думать, что его код имеет какое-то четко определенное значение, что не так. Вы правы в том, что, вероятно, большинство компиляторов реализовали бы это именно так. - person Brennan Vincent; 23.06.2011
comment
@BrennanVincent: Пока память была занята a, указатель содержал адрес a. Хотя Стандарт не требует, чтобы реализации определяли поведение адресов после того, как время жизни их цели закончилось, он также признает, что на некоторых платформах UB обрабатывается документированным способом, характерным для среды. Хотя адрес локальной переменной обычно не будет иметь особого смысла после того, как он вышел за пределы области видимости, некоторые другие типы адресов могут по-прежнему иметь смысл после времени жизни их соответствующих целей. - person supercat; 27.06.2018
comment
@BrennanVincent: Например, хотя Стандарт может не требовать, чтобы реализации позволяли сравнивать указатель, переданный на realloc, с возвращаемым значением, а также не разрешали изменять указатели на адреса в старом блоке так, чтобы они указывали на новый, некоторые реализации делают поэтому код, использующий такую ​​возможность, может быть более эффективным, чем код, который должен избегать любых действий - даже сравнений - с указателями на выделение, которое было дано realloc. - person supercat; 27.06.2018

Может, потому что a - это переменная, временно выделенная на время существования своей области (foo функция). После возврата из foo память свободна и может быть перезаписана.

То, что вы делаете, описывается как неопределенное поведение. Результат невозможно предугадать.

person littleadv    schedule 24.06.2011

Вещи с правильным (?) Выводом в консоль могут кардинально измениться, если вы используете :: printf, но не cout. Вы можете поиграть с отладчиком в приведенном ниже коде (протестировано на x86, 32-разрядной версии, MSVisual Studio):

char* foo() 
{
  char buf[10];
  ::strcpy(buf, "TEST”);
  return buf;
}

int main() 
{
  char* s = foo();    //place breakpoint & check 's' varialbe here
  ::printf("%s\n", s); 
}
person Community    schedule 24.06.2011

После возврата из функции все идентификаторы уничтожаются вместо сохраненных значений в ячейке памяти, и мы не можем найти значения, не имея идентификатора, но это место по-прежнему содержит значение, сохраненное предыдущей функцией.

Итак, здесь функция foo() возвращает адрес a, а a уничтожается после возврата его адреса. И вы можете получить доступ к измененному значению через этот возвращенный адрес.

Рассмотрим пример из реальной жизни:

Предположим, человек прячет деньги в каком-то месте и сообщает вам это место. Через какое-то время человек, сказавший вам местонахождение денег, умирает. Но все же у вас есть доступ к этим скрытым деньгам.

person Community    schedule 19.07.2017

Это «грязный» способ использования адресов памяти. Когда вы возвращаете адрес (указатель), вы не знаете, принадлежит ли он локальной области видимости функции. Это просто адрес. Теперь, когда вы вызвали функцию 'foo', этот адрес (ячейка памяти) 'a' уже был выделен там в (по крайней мере, пока безопасно) адресуемой памяти вашего приложения (процесса). После возврата функции 'foo' адрес 'a' можно считать 'грязным', но он есть, не очищен, не нарушен / не изменен выражениями в другой части программы (по крайней мере, в этом конкретном случае). Компилятор C / C ++ не останавливает вас от такого «грязного» доступа (хотя может вас предупредить, если вам не все равно). Вы можете безопасно использовать (обновлять) любую ячейку памяти, которая находится в сегменте данных вашего экземпляра программы (процесса), если вы не защитите адрес каким-либо образом.

person Community    schedule 08.03.2017

Ваш код очень рискованный. Вы создаете локальную переменную (которая считается уничтоженной после завершения функции), и вы возвращаете адрес памяти этой переменной после ее уничтожения.

Это означает, что адрес памяти может быть действительным или нет, и ваш код будет уязвим для возможных проблем с адресом памяти (например, ошибка сегментации).

Это означает, что вы делаете очень плохой поступок, потому что вы передаете адрес памяти указателю, которому вообще нельзя доверять.

Вместо этого рассмотрим этот пример и протестируем его:

int * foo()
{
   int *x = new int;
   *x = 5;
   return x;
}

int main()
{
    int* p = foo();
    std::cout << *p << "\n"; //better to put a new-line in the output, IMO
    *p = 8;
    std::cout << *p;
    delete p;
    return 0;
}

В отличие от вашего примера, в этом примере вы:

  • выделение памяти для int в локальную функцию
  • этот адрес памяти все еще действителен, когда функция истекает (он никем не удаляется)
  • адрес памяти является надежным (этот блок памяти не считается свободным, поэтому он не будет отменен, пока он не будет удален)
  • адрес памяти следует удалять, если он не используется. (см. удаление в конце программы)
person Community    schedule 02.05.2019
comment
Вы добавили что-то, что еще не охвачено существующими ответами? И, пожалуйста, не используйте необработанные указатели / new. - person Lightness Races in Orbit; 02.05.2019
comment
Спрашивающий использовал необработанные указатели. Я сделал пример, который в точности отражает тот пример, который он сделал, чтобы позволить ему увидеть разницу между ненадежным указателем и надежным. На самом деле есть еще один ответ, похожий на мой, но он использует strcpy, который, IMHO, может быть менее понятен начинающему кодеру, чем мой пример, использующий new. - person Nobun; 02.05.2019
comment
Они не использовали new. Вы учите их использовать new. Но вы не должны использовать new. - person Lightness Races in Orbit; 02.05.2019
comment
Так что, по вашему мнению, лучше передать адрес локальной переменной, которая уничтожается в функции, чем фактически выделять память? Это не имеет никакого смысла. Понимание концепции выделения и освобождения памяти важно, imho, в основном, если вы спрашиваете об указателях (запрашивающий не использовал новые, а использовал указатели). - person Nobun; 02.05.2019
comment
Когда я это сказал? Нет, лучше использовать интеллектуальные указатели, чтобы правильно указывать владение ресурсом, на который имеется ссылка. Не используйте new в 2019 году (если вы не пишете код библиотеки) и не учите этому новичков! Ваше здоровье. - person Lightness Races in Orbit; 02.05.2019
comment
умные указатели, безусловно, лучше new, я с вами согласен. Но я использовал new, так как это проще, чем интеллектуальные указатели, в использовании и понимании для новичка. Я считаю, что эту сложность нужно масштабировать. Прежде всего, нужно понять, что означает распределение, прежде чем перейти к следующему шагу (что может быть ... как мне лучше использовать распределение? - ›умные указатели и другие инструменты в std: :) - Однако я признаю, что я очень олдскульный автоучитель C ++: D так что - ›моя вина: P - person Nobun; 02.05.2019
comment
Управление объектами с помощью интеллектуальных указателей - вот чему следует учить. new и delete - это сложная тема, которую можно преподать позже. :) - person Lightness Races in Orbit; 02.05.2019

Это зависит от языка. В C & C ++ / Cpp, ДА, вы технически могли бы, потому что он имеет очень слабые проверки того, действительно ли какой-либо указанный указатель указывает на действительный или нет. Компилятор сообщит об ошибке, если вы попытаетесь получить доступ к самой переменной, когда она находится вне области видимости, но он вряд ли будет достаточно умен, чтобы знать, намеренно ли вы копируете указатель на местоположение этой переменной в какой-либо другая переменная, которая останется в области видимости позже.

Однако изменение этой памяти после того, как переменная выходит за пределы области видимости, будет иметь совершенно неопределенный эффект. Вы, вероятно, повредите стек, который мог повторно использовать это пространство для новых переменных.

Более современные языки, такие как Java или C #, часто делают все возможное, чтобы программисту в первую очередь не требовался доступ к фактическим адресам переменных, а также доступ к массивам с проверкой границ и сохранением ссылок. количество переменных, которые указывают на объекты в куче, чтобы они не освобождались преждевременно, и так далее. Все это предназначено для того, чтобы помочь программисту не сделать что-то непреднамеренно небезопасное и / или выходящее за пределы переменных в области видимости.

person Community    schedule 02.05.2021