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


1031

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

#include <iostream>

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

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

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

Выход был 58

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


14
это даже не скомпилируется как есть; если вы исправите неформальный бизнес, gcc все равно предупредит address of local variable ‘a’ returned; Valgrind показываетInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
сехе

76
@Serge: В юности я когда-то работал над каким-то хитрым нулевым кодом, работавшим в операционной системе Netware, который предусматривал умное перемещение по указателю стека способом, не совсем санкционированным операционной системой. Я знал бы, когда совершил ошибку, потому что часто стек заканчивал тем, что перекрывал экранную память, и я мог просто наблюдать, как байты записываются прямо на дисплей. Вы не можете сойти с рук такого рода в эти дни.
Эрик Липперт

23
лол. Мне нужно было прочитать вопрос и несколько ответов, прежде чем я понял, в чем проблема. Это на самом деле вопрос о области доступа переменной? Вы даже не используете «а» вне своей функции. И это все, что нужно сделать. Бросать некоторые ссылки на память - это совершенно другая тема из области видимости переменных.
erikbwork

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

16
@Joel: Если ответ здесь хороший, его следует объединить со старыми вопросами , из которых это обман, а не наоборот. И этот вопрос действительно является дублированием других вопросов, предложенных здесь, а затем и некоторых (хотя некоторые из предложенных лучше подходят, чем другие). Обратите внимание, что я думаю, что ответ Эрика хорош. (На самом деле, я пометил этот вопрос для объединения ответов в один из более старых вопросов, чтобы спасти более старые вопросы.)
sbi

Ответы:


4802

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

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

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

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

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

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

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

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

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/


56
@muntoo: К сожалению, операционная система не выдает предупреждающую сирену перед тем, как отключить или освободить страницу виртуальной памяти. Если вы возитесь с этой памятью, когда у вас ее больше нет, то операционная система вполне может отменить весь процесс, когда вы касаетесь освобожденной страницы. Boom!
Эрик Липперт

83
@ Кайл: Только безопасные отели делают это. Небезопасные отели получают ощутимую прибыль, не тратя время на программирование ключей.
Александр Торстлинг

498
@cyberguijarro: То, что C ++ не безопасен для памяти, это просто факт. Это ничего не "избивает". Если бы я сказал, например, «C ++ - это ужасная путаница недоопределенных, чрезмерно сложных функций, сложенных поверх хрупкой, опасной модели памяти, и я благодарен каждый день, что больше не работаю в ней ради собственного здравого смысла», это будет бить C ++. Указание, что это не безопасно для памяти, объясняет, почему оригинальный постер видит эту проблему; он отвечает на вопрос, а не редактирует.
Эрик Липперт

50
Строго говоря, по аналогии следует отметить, что администратор отеля был очень рад, что вы взяли с собой ключ. "О, вы не возражаете, если я возьму этот ключ со мной?" «Вперед. Почему мне все равно? Я работаю только здесь». Это не становится незаконным, пока вы не попытаетесь его использовать.
philsquared

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

276

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

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

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

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


152

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


1
Чувак, это было самое долгое ожидание комментария с тех пор: «Что есть истина?» - сказал шутя Пилат. Может быть, это была Библия Гедеона в ящике отеля. И что с ними случилось? Обратите внимание, что их больше нет, по крайней мере, в Лондоне. Я предполагаю, что в соответствии с законодательством о равенстве вам понадобится библиотека религиозных трактатов.
Роб Кент

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

1
Ха - ха. Фрэнсис Бэкон, один из величайших эссеистов Британии, которого, как подозревают некоторые люди, написал пьесы Шекспира, потому что они не могут смириться с тем, что ребенок из гимназии, сын Гловера, может быть гением. Такова английская система классов. Иисус сказал: «Я Истина». oregonstate.edu/instruct/phl302/texts/bacon/bacon_essays.html
Роб Кент

84

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

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

#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 (). Если вы разберете исполняемый файл, вы увидите это ясно.


2
Простой, но отличный пример, чтобы понять основную теорию стека. Просто одно тестовое дополнение, объявляющее "int a = 5;" в foo () как "static int a = 5;" может использоваться для понимания объема и времени жизни статической переменной.
контроль

15
-1 "для, вероятно, будет 7 ". Компилятор может зарегистрировать в boo. Это может удалить его, потому что это не нужно. Есть большая вероятность, что * p не будет 5 , но это не значит, что есть какая-то особенно веская причина, почему это, вероятно, будет 7 .
Мэтт

2
Это называется неопределенным поведением!
Фрэнсис Куглер

почему и как booповторно использует fooстек? не являются стеками функций, отделенными друг от друга, также я получаю мусор при запуске этого кода в Visual Studio 2015
ampawd

1
@ampawd ему почти год, но нет, «функциональные стеки» не отделены друг от друга. КОНТЕКСТ имеет стек. Этот контекст использует свой стек для входа в main, затем спускается в foo(), существует, затем спускается в boo(). Foo()и Boo()оба входят с указателем стека в том же месте. Однако это не то поведение, на которое следует полагаться. Другие «вещи» (например, прерывания или ОС) могут использовать стек между вызовом boo()и foo(), изменяя его содержимое ...
Расс Шульц

72

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


5
Вы, вероятно, имеете в виду, что вы можете попытаться получить доступ к любому адресу. Потому что большинство операционных систем сегодня не позволяют какой-либо программе получить доступ к любому адресу; Есть множество способов защиты адресного пространства. Вот почему там не будет другого LOADLIN.EXE.
v010дя

67

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

unsigned int q = 123456;

*(double*)(q) = 1.2;

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

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

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

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


9
Я просто собираюсь написать программу, которая будет продолжать запускать эту программу, так что4) I win the lottery
Aidiakapi

29

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

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


Это моя ставка. Оптимизатор сбросил вызов функции.
Эрик Аронесты

9
Это не обязательно. Поскольку после foo () новая функция не вызывается, кадр локального стека функций просто еще не перезаписан. Добавьте еще один вызов функции после foo (), и 5переменная будет изменена ...
Томас

Я запустил программу с GCC 4.8, заменив cout на printf (включая stdio). Справедливо предупреждает "предупреждение: адрес локальной переменной 'a' вернул [-Wreturn-local-addr]". Выходы 58 без оптимизации и 08 с -O3. Странно, что у P есть адрес, хотя его значение равно 0. Я ожидал NULL (0) в качестве адреса.
Кевинф

23

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

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


Я вспоминаю из старой копии Turbo C Programming для IBM , с которой я когда-то играл, когда детально описывалось, как напрямую манипулировать графической памятью и расположением видеопамяти IBM в текстовом режиме. Конечно, система, в которой выполнялся код, четко определяла, что означает запись по этим адресам, поэтому, пока вы не беспокоились о переносимости в другие системы, все было в порядке. IIRC, указатели на аннулирование были общей темой в этой книге.
CVn

@ Майкл Кьёрлинг: Конечно! Людям нравится время от времени выполнять какую-то грязную работу;)
Чанг Пенг

18

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

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

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
}

Я не согласен: есть проблема до cout. *aуказывает на нераспределенную (освобожденную) память. Даже если вы не разыграете его, это все равно опасно (и, вероятно, фальшиво).
2010 года

@ereOn: Я пояснил больше, что имел в виду под проблемой, но нет, это не опасно с точки зрения правильного кода на C ++. Но это опасно с точки зрения вероятности того, что пользователь допустил ошибку и сделает что-то плохое. Может быть, например, вы пытаетесь увидеть, как растет стек, и вы заботитесь только о значении адреса и никогда не разыменовываете его.
Брайан Р. Бонди

18

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


18

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

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

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», но ваши результаты могут отличаться (действительно!). Ваш указатель забивает другие, не связанные локальные переменные.


18

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

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

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


17

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


16

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

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

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


14

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

Тем не менее, это неопределенное поведение, и вы не должны полагаться на него, чтобы работать!


3
«распечатать значение блока памяти с адресом, который раньше был занят» не совсем верно. Это звучит так, как будто его код имеет какое-то четкое значение, а это не так. Вы правы, что, вероятно, именно так большинство компиляторов будет реализовывать это.
Бреннан Винсент

@BrennanVincent: пока хранилище было занято a, указатель содержал адрес a. Хотя в стандарте не требуется, чтобы реализации определяли поведение адресов после окончания времени жизни их цели, он также признает, что на некоторых платформах UB обрабатывается документированным образом, характерным для среды. Хотя адрес локальной переменной, как правило, не будет иметь большого значения после того, как он вышел из области видимости, некоторые другие виды адресов могут все еще иметь смысл после времени жизни их соответствующих целей.
суперкат

@BrennanVincent: Например, хотя стандарт может не требовать, чтобы реализации позволяли reallocсравнивать переданный указатель с возвращаемым значением, а также не позволяли указателям на адреса в старом блоке корректироваться для указания на новый, некоторые реализации делают это и код, использующий такую ​​функцию, может быть более эффективным, чем код, который должен избегать каких-либо действий - даже сравнений - с указателями на присвоение, которое было дано realloc.
Суперкат

14

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

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


12

Вещи с правильным (?) Выводом на консоль могут сильно измениться, если вы используете :: 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); 
}

5

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

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

Позвольте мне привести пример из реального мира:

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


4

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


1

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

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

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

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

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 в локальную функцию
  • этот адрес памяти остается в силе и после истечения срока действия функции (никто не удаляется)
  • адрес памяти является надежным (этот блок памяти не считается свободным, поэтому он не будет переопределен, пока не будет удален)
  • адрес памяти должен быть удален, когда он не используется. (см. удаление в конце программы)

Вы добавили что-то, что еще не охватывалось существующими ответами? И, пожалуйста, не используйте сырые указатели / new.
Гонки

1
Аскер использовал сырые указатели. Я сделал пример, который точно отражал пример, который он сделал, чтобы позволить ему увидеть разницу между ненадежным указателем и надежным указателем. На самом деле есть другой ответ, похожий на мой, но он использует strcpy, который, IMHO, может быть менее понятен начинающему программисту, чем мой пример, использующий new.
Нобун

Они не использовали new. Вы учите их использовать new. Но вы не должны использовать new.
Гонки

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

Когда я это сказал? Нет, лучше использовать умные указатели, чтобы правильно указывать право собственности на указанный ресурс. Не используйте newв 2019 году (если вы не пишете код библиотеки) и не учите новичков делать это! Приветствия.
Гонки
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.