Чи можна отримати доступ до пам'яті локальної змінної за її межами?


1028

У мене є такий код.

#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; шоу Invalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
вальгринд

76
@Serge: Ще в юності я колись працював над деяким хитромудрим кодом нульового дзвінка, який працював на операційній системі Netware, яка передбачала вміле переміщення по вказівнику стека способом, не точно санкціонованим операційною системою. Я б знав, коли я помилився, тому що часто стек в кінцевому підсумку перекриває пам'ять екрану, і я міг просто спостерігати, як байти записуються прямо на дисплей. Ти не можеш уникнути подібних речей сьогодні.
Ерік Ліпперт

23
Лол. Мені потрібно було прочитати питання та деякі відповіді, перш ніж я навіть зрозумів, де проблема. Це насправді питання про область доступу змінної? Ви навіть не використовуєте "a" поза вашою функцією. І це все є. Прикидання деяких посилань на пам'ять - абсолютно інша тема від змінної області.
erikbwork

10
Відповідь Дупа не означає дурного питання. Дуже багато питань, що пропонуються тут, є абсолютно різними питаннями, які, як правило, посилаються на той самий основний симптом ... але запитуючий має знати, як знати, що тому вони повинні залишатися відкритими. Я закрив старішу дупу і об'єднав її в це питання, яке повинно залишатися відкритим, оскільки воно має дуже гарну відповідь.
Джоель Спольський

16
@Joel: Якщо відповідь тут хороша, її слід об'єднати в більш старі питання , серед яких це дур, а не навпаки. І це запитання справді є спільним з інших питань, запропонованих тут, а потім і з деякими (хоча деякі із запропонованих краще підходять, ніж інші). Зауважте, що я вважаю, що відповідь Еріка хороша. (Насправді я позначив це питання для об'єднання відповідей в одне із старих питань, щоб врятувати старіші запитання.)
sbi

Відповіді:


4798

Як це може бути? Хіба пам'ять локальної змінної недоступна поза її функцією?

Ви орендуєте номер в готелі. Ви кладете книгу у верхню шухляду тумбочки і лягаєте спати. Ви перевіряєте наступного ранку, але "забуваєте" повернути свій ключ. Ви вкрали ключ!

Через тиждень ви повертаєтесь у готель, не заїжджаєте, забираєтесь у свою стару кімнату зі своїм викраденим ключем і зазираєте у шухляду. Ваша книга все ще є. Дивовижно!

Як це може бути? Чи не є вміст ящика готельного номера недоступним, якщо ви не взяли в оренду номер?

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

Для видалення вашої книги керівництву готелю не потрібно . Ви не укладали з ними контракту, в якому говорилося, що якщо ви залишите речі позаду, вони поріжуть це за вас. Якщо ви незаконно зайшли в свою кімнату з викраденим ключем, щоб повернути її назад, персонал служби готелю не зобов’язаний ловити вас, як ви прокрадаєтеся. Ви не уклали з ними контракт, який сказав "якщо я спробую прокрастися назад до свого кімната пізніше, ви зобов’язані мене зупинити ". Швидше за все, ви підписали з ними контракт, в якому сказано: "Я обіцяю, щоб пізніше не проникнути в свою кімнату", контракт, який ви розірвали .

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

Ви не знаєте, що буде; коли ви виписалися з готелю і вкрали ключ незаконно використовувати пізніше, ви відмовилися від права жити в передбачуваному, безпечному світі , тому що ви вирішили порушити правила системи.

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/


55
@muntoo: На жаль, це не так, як операційна система звучить попереджувальною сиреною, перш ніж вона розкладе або розместить сторінку віртуальної пам'яті. Якщо ви спілкуєтесь із цією пам'яттю, коли не володієте нею, операційна система цілком в межах своїх прав знімати весь процес, коли ви торкаєтесь розміщеної сторінки. Бум!
Ерік Ліпперт

82
@Kyle: Тільки безпечні готелі роблять це. Небезпечні готелі отримують вагомі прибутки від того, що не потрібно витрачати час на клавіші програмування.
Олександр Торстлінг

497
@cyberguijarro: Те, що C ++ не є безпечним для пам’яті, просто факт. Це ніщо не "б'є". Якби я сказав, наприклад, "C ++ - це жахливий мешанець недостатньо визначених, надмірно складних функцій, накопичених на крихкій, небезпечній моделі пам'яті, і я вдячний щодня, що більше не працюю в ній заради власної гідності", це буде бити C ++. Вказуючи, що це не безпечно для пам’яті, це пояснення, чому оригінальний плакат бачить це питання; це відповідь на питання, а не редагування.
Ерік Ліпперт

49
Власне кажучи, в аналогії слід згадати, що портьє в готелі були дуже раді вам взяти ключ із собою. "О, ви не проти, якщо я візьму з собою цей ключ?" "Вперед. Навіщо мені байдуже? Я працюю лише тут". Це не стає незаконним, поки ви не спробуєте ним скористатися.
фільтрований

139
Будь ласка, будь-ласка, принаймні подумайте, як написати книгу одного дня Я б придбав його, навіть якби це була лише колекція переглянутих та розширених дописів у блогах, і я впевнений, що так багато людей. Але книжка з вашими оригінальними думками з різних питань, пов'язаних з програмуванням, була б чудовою для читання. Я знаю, що знайти час для цього надзвичайно важко, але, будь ласка, подумайте про те, щоб написати його.
Dyppl

275

Тут ви просто читаєте і записуєте в пам'ять, яка раніше була адресою a. Тепер, коли ви знаходитесь поза межами foo, це лише вказівник на якусь область випадкової пам'яті. Просто так трапляється, що у вашому прикладі ця область пам’яті існує, і зараз її ніхто іншим не використовує. Ви нічого не ламаєте, продовжуючи користуватися ним, і нічого іншого ще не перезаписали. Тому 5ще є. У реальній програмі ця пам’ять буде використана майже негайно, і ви щось зламаєте, зробивши це (хоча симптоми можуть проявитися набагато пізніше!)

Повернувшись foo, ви повідомляєте ОС, що більше не використовуєте цю пам'ять, і її можна перепризначити на щось інше. Якщо вам пощастить, і це ніколи не стане перерозподіленим, і ОС не застане вас використовувати його знову, тоді ви втечете від брехні. Швидше за все, ви все-таки напишіть все, що ще закінчиться цією адресою.

Тепер, якщо вам цікаво, чому компілятор не скаржиться, це, мабуть, тому, що fooйого усунули оптимізацією. Зазвичай це попереджатиме вас про подібні речі. C припускає, що ви знаєте, чим займаєтесь, і технічно ви тут не порушили сфери застосування (немає посилання на aсебе поза foo), лише правила доступу до пам'яті, що викликає лише попередження, а не помилку.

Коротше кажучи: зазвичай це не спрацює, але іноді випадково.


151

Тому що місце для зберігання ще не було затоптане. Не розраховуйте на таку поведінку.


1
Людина, це було найдовше очікування коментаря, оскільки: "Що таке правда? Сказав пихаючий Пілат". Можливо, це була біблія Гедеона в тій же шухляді готелю. І що з ними все-таки сталося? Зауважте, їх більше немає, принаймні, у Лондоні. Я здогадуюсь, що відповідно до законодавства про рівності вам знадобиться бібліотека релігійних урочищ.
Роб Кент

Я міг би присягнути, що писав це давно, але він з’явився недавно і виявив, що моя відповідь не була там. Тепер я мушу розібратися у ваших натяках вище, оскільки я очікую, що мене розвеселять, коли я це роблю>. <
msw

1
Ха-ха. Френсіс Бекон, один з найбільших есеїстів Великобританії, якого деякі люди підозрюють, писав п'єси Шекспіра, тому що вони не можуть прийняти, що дитина з гімназії з країни, син гловерса, може бути генієм. Така система англійської класу. Ісус сказав: "Я - Істина". oregonstate.edu/instruct/phl302/texts/bacon/bacon_essays.html
Роб Кент

83

Невелике доповнення до всіх відповідей:

якщо ви робите щось подібне:

#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 () як "статичний int a = 5;" може бути використаний для розуміння сфери та часу життя статичної змінної.
контроль

15
-1 "для, ймовірно, буде 7 ". Компілятор може зареєструвати boo. Це може видалити, оскільки це непотрібно. Є хороший шанс, що * p не буде 5 , але це не означає, що є якась особливо вагома причина, чому вона, ймовірно, буде 7 .
Метт

2
Це називається невизначеною поведінкою!
Френсіс Куглер

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

1
@ampawd це майже рік, але ні, "стеки функцій" не відокремлені один від одного. У CONTEXT є стек. Цей контекст використовує свій стек для введення основного, потім спускається foo(), існує, потім спускається в boo(). Foo()і Boo()обидва входять за допомогою вказівника стека на одне місце. Однак це не поведінка, на яку слід покладатися. Інші "речі" (наприклад, переривання або ОС) можуть використовувати стек між викликом boo()та foo(), змінюючи його вміст ...
Russ Schultz

71

У C ++ ви можете отримати доступ до будь-якої адреси, але це не означає, що вам слід . Адреса, до якої ви отримуєте доступ, більше не дійсна. Це працює тому, що більше нічого не зміцнювало пам'ять після повернення foo, але воно може розбитися за багатьох обставин. Спробуйте проаналізувати свою програму за допомогою Valgrind або навіть просто оптимізуючи її компіляцію, і подивіться ...


5
Ви, мабуть, маєте на увазі, що можете спробувати отримати доступ до будь-якої адреси. Тому що більшість операційних систем сьогодні не дозволить жодній програмі отримати доступ до будь-якої адреси; Є багато гарантій захисту адресного простору. Ось чому там не буде іншого LOADLIN.EXE.
v010дя

66

Ви ніколи не кидаєте виняток 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

28

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

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


Це моя ставка. Оптимізатор скинув виклик функції.
Ерік Аронесті

9
Це не обов'язково. Оскільки після foo () жодна нова функція не викликається, локальний фрейм стека функцій просто ще не перезаписаний. Додайте ще виклик функції після foo (), і 5заповіт буде змінено ...
Томаш,

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

22

Ваша проблема не має нічого спільного з масштабом . У коді, який ви показуєте, функція mainне бачить імен у функції foo, тому ви не можете отримати доступ aу foo безпосередньо з цим ім'ям зовні foo.

Проблема, яка виникає, полягає в тому, що програма не сигналізує про помилку при посиланні на незаконну пам'ять. Це пояснюється тим, що стандарти C ++ не визначають дуже чітку межу між незаконною пам'яттю та законною пам'яттю. Посилання на щось у вискочившому стеку іноді викликає помилки, а іноді - ні. Це залежить. Не розраховуйте на таку поведінку. Припустимо, що це завжди призведе до помилки під час програмування, але припустимо, що вона ніколи не подасть помилку під час налагодження.


Я пригадую зі старої копії програмування Turbo C для IBM , якою я займався деяким часом назад, коли, як безпосередньо маніпулювати графічною пам'яттю та компонуванням відеопам'яті в текстовому режимі IBM, було описано дуже докладно. Зрозуміло, тоді в системі, в коді якої працював чітко визначений, що означає написання на ці адреси, до тих пір, поки ви не турбувались про переносимість на інші системи, все було добре. IIRC, покажчики недійсності були поширеною темою в цій книзі.
CVn

@Michael Kjörling: Звичайно! Людям подобається робити поодинокі брудні роботи;)
Чанг Пен

17

Ви просто повертаєте адресу пам'яті, це дозволено, але, ймовірно, помилка.

Так, якщо ви спробуєте знеструмити цю адресу пам'яті, у вас буде не визначена поведінка.

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вказує на нерозподілену (звільнену) пам’ять. Навіть якщо ви цього не зневажаєте, це все-таки небезпечно (і, швидше за все, хитрістю).
ereOn

@ereOn: Я уточнив більше, що я мав на увазі під проблемою, але ні, це не небезпечно з точки зору дійсного коду c ++. Але це небезпечно з точки зору ймовірності, що користувач допустив помилку і зробить щось погане. Можливо, ви, наприклад, намагаєтесь побачити, як зростає стек, і вам цікаво лише значення адреси і ніколи його не відкинете.
Брайан Р. Бонді

17

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


17

Як зазначив Алекс, така поведінка не визначена - насправді більшість компіляторів застерігає від цього, оскільки це простий спосіб отримати збої.

Як приклад виду привиди поведінки ви , ймовірно , щоб отримати, спробуйте цей приклад:

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", але результати можуть бути різними (справді!). Ваш покажчик клобує інші, не пов’язані між собою локальні змінні.


17

Зверніть увагу на всі попередження. Не вирішуйте лише помилки.
GCC показує це Попередження

попередження: повернута адреса локальної змінної 'a'

Це потужність C ++. Вам слід дбати про пам’ять. З -Werrorпрапором це попередження позначає помилку, і тепер вам доведеться налагодити його.


16

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


15

Ви фактично посилалися на невизначену поведінку.

Повернення адреси тимчасової роботи, але оскільки часові знищуються в кінці функції, результати доступу до них будуть визначені.

Тож ви не змінювали, aа скоріше місце розташування пам'яті, де aколись було. Ця різниця дуже схожа на різницю між аварійними та не збійними.


13

У типових реалізаціях компілятора ви можете вважати код як "роздрукувати значення блоку пам'яті з адресою, яка раніше була зайнята знаком". Крім того, якщо ви додасте нове виклик функції до функції, яка містить локальну, intце хороший шанс, що значення a(або адреса пам'яті, aна яку вказувались) змінюється. Це відбувається тому, що стек буде перезаписаний новим кадром, що містить різні дані.

Однак це невизначена поведінка, і ви не повинні покладатися на це, щоб вона працювала!


3
"роздрукувати значення блоку пам'яті з адресою, яку раніше займала", не зовсім правильно. Це звучить так, що його код має певне чітко визначене значення, що не так. Ви маєте рацію, що, мабуть, саме так реалізували б більшість компіляторів.
Бренан Вінсент

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

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

13

Це може, тому що aце змінна, виділена тимчасово протягом життя її сфери ( fooфункції). Після повернення з fooпам'яті вільний і його можна перезаписати.

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


11

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

4

Після повернення з функції всі ідентифікатори знищуються замість збережених значень у пам'яті, і ми не можемо знайти значення без ідентифікатора. Але це місце все ще містить значення, збережені попередньою функцією.

Отже, тут функція foo()повертає адресу aі aзнищується після повернення її адреси. І ви можете отримати доступ до зміненого значення через ту повернуту адресу.

Дозвольте взяти приклад із реального світу:

Припустимо, чоловік приховує гроші в місці розташування та повідомляє вам місцезнаходження. Через деякий час людина, яка сказала вам місце знаходження грошей, помирає. Але все ж у вас є доступ до цих прихованих грошей.


3

Це "брудний" спосіб використання адрес пам'яті. Коли ви повертаєте адресу (покажчик), ви не знаєте, чи належить вона до локальної області функції. Це просто адреса. Тепер, коли ви посилалися на функцію '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в 2019 році (якщо ви не пишете код бібліотеки) і не вчіть новачків робити це також! Ура.
Гонки легкості по орбіті
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.