C ++ Zwracanie referencji do zmiennej lokalnej

117

Czy poniższy kod (func1 ()) jest poprawny, jeśli ma zwrócić i? Pamiętam, że czytałem gdzieś, że jest problem podczas zwracania odwołania do zmiennej lokalnej. Czym się różni od func2 ()?

int& func1()
{
    int i;
    i = 1;
    return i;
}

int* func2()
{
    int* p;
    p = new int;
    *p = 1;
    return p;
}
blitzkriegz
źródło
1
Jeśli zmienisz func1 (), aby używać dynamicznie przydzielanej pamięci, to będą takie same :-)int& i = * new int;
Martin York,
1
Powiązane dla const locals: stackoverflow.com/questions/2784262/…
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功

Odpowiedzi:

193

Ten fragment kodu:

int& func1()
{
    int i;
    i = 1;
    return i;
}

nie zadziała, ponieważ zwracasz alias (odwołanie) do obiektu z okresem istnienia ograniczonym do zakresu wywołania funkcji. Oznacza to, że po func1()powrocie int iumiera, sprawiając, że odwołanie zwrócone z funkcji jest bezwartościowe, ponieważ teraz odwołuje się do obiektu, który nie istnieje.

int main()
{
    int& p = func1();
    /* p is garbage */
}

Druga wersja działa, ponieważ zmienna jest przydzielona do wolnego magazynu, który nie jest powiązany z okresem istnienia wywołania funkcji. Jednak jesteś odpowiedzialny za deleteprzydzielone int.

int* func2()
{
    int* p;
    p = new int;
    *p = 1;
    return p;
}

int main()
{
    int* p = func2();
    /* pointee still exists */
    delete p; // get rid of it
}

Zwykle owinąłbyś wskaźnik w jakąś klasę RAII i / lub funkcję fabryczną, więc sam nie musisz tego robić delete.

W obu przypadkach możesz po prostu zwrócić samą wartość (chociaż zdaję sobie sprawę, że przykład, który podałeś, został prawdopodobnie wymyślony):

int func3()
{
    return 1;
}

int main()
{
    int v = func3();
    // do whatever you want with the returned value
}

Zwróć uwagę, że zwracanie dużych obiektów w ten sam sposób, w jaki func3()zwraca się wartości pierwotne, jest całkowicie w porządku, ponieważ obecnie prawie każdy kompilator implementuje jakąś formę optymalizacji wartości zwracanych :

class big_object 
{ 
public:
    big_object(/* constructor arguments */);
    ~big_object();
    big_object(const big_object& rhs);
    big_object& operator=(const big_object& rhs);
    /* public methods */
private:
    /* data members */
};

big_object func4()
{
    return big_object(/* constructor arguments */);
}

int main()
{
     // no copy is actually made, if your compiler supports RVO
    big_object o = func4();    
}

Co ciekawe, wiązanie tymczasowe z odwołaniem do const jest całkowicie legalnym C ++ .

int main()
{
    // This works! The returned temporary will last as long as the reference exists
    const big_object& o = func4();    
    // This does *not* work! It's not legal C++ because reference is not const.
    // big_object& o = func4();  
}
In silico
źródło
2
Piękne wyjaśnienie. : hattip: W trzecim fragmencie kodu, który usuwasz int* p = func2(); delete p;Teraz, kiedy usuwałeś 'p', czy to oznacza, że ​​pamięć przydzielona "wewnątrz" func2()definicji funkcji również została usunięta?
Aquarius_Girl
2
@Anisha Kaul: Tak. Pamięć została przydzielona wewnątrz func2()i zwolniona na zewnątrz w następnej linii. Jest to jednak raczej podatny na błędy sposób obsługi pamięci, tak jak powiedziałem, zamiast tego używałbyś jakiegoś wariantu RAII. Nawiasem mówiąc, brzmisz, jakbyś się uczył C ++. Polecam skorzystać z dobrej książki wprowadzającej do języka C ++, z której można się uczyć. Ponadto, jeśli masz jakieś pytania, możesz je zawsze opublikować na stronie Stack Overflow. Komentarze nie służą do zadawania zupełnie nowych pytań.
In silico
Teraz zrozumiałem, zrobiłeś to dobrze! Funkcja zwracała wskaźnik, a poza tą funkcją usunąłeś pamięć, na którą wskazywała. Teraz jest już jasne i dziękuję za link.
Aquarius_Girl
i zredagowałeś odpowiedź? : mad: Mogłem to łatwo przeoczyć. ;);)
Aquarius_Girl
@Anisha Kaul: Nie, nie zrobiłam tego. Ostatni raz redagowałem odpowiedź 10 stycznia, zgodnie z datownikiem pod moim postem.
In silico
18

Zmienna lokalna to pamięć na stosie, która nie jest automatycznie unieważniana, gdy wyjdziesz poza zakres. Z funkcji zagnieżdżonej głębiej (wyżej na stosie w pamięci), dostęp do tej pamięci jest całkowicie bezpieczny.

Gdy funkcja powróci i zakończy się, sytuacja stanie się niebezpieczna. Zwykle pamięć nie jest usuwana ani nadpisywana po powrocie, co oznacza, że ​​pamięć pod tymi adresami nadal zawiera dane - wskaźnik wydaje się prawidłowy.

Dopóki inna funkcja nie utworzy stosu i nie nadpisuje go. Dlatego może to działać przez chwilę - a potem nagle przestaje działać po tym, jak jeden szczególnie głęboko zagnieżdżony zestaw funkcji lub funkcja o naprawdę dużych rozmiarach lub wielu lokalnych obiektach ponownie osiągnie tę pamięć stosu.

Może się nawet zdarzyć, że ponownie dotrzesz do tej samej części programu i nadpiszesz swoją starą lokalną zmienną funkcji nową zmienną funkcji. Wszystko to jest bardzo niebezpieczne i należy go mocno odradzać. Nie używaj wskaźników do lokalnych obiektów!

Cycero
źródło
2

Warto zapamiętać te proste zasady, które dotyczą zarówno parametrów, jak i typów zwracanych ...

  • Wartość - tworzy kopię danego elementu.
  • Wskaźnik - odnosi się do adresu danej pozycji.
  • Odniesienie - to dosłownie przedmiot, o którym mowa.

Dla każdego jest czas i miejsce, więc upewnij się, że je znasz. Zmienne lokalne, jak pokazałeś tutaj, są po prostu ograniczone do czasu, w którym żyją lokalnie w zakresie funkcji. W twoim przykładzie zwracanie int*i zwracanie typu &ibyłoby równie niepoprawne. W takim przypadku byłoby lepiej, gdybyś zrobił to ...

void func1(int& oValue)
{
    oValue = 1;
}

Spowoduje to bezpośrednią zmianę wartości przekazanego parametru. Podczas gdy ten kod ...

void func1(int oValue)
{
    oValue = 1;
}

nie. Po prostu zmieniłby wartość oValuelokalnego wywołania funkcji. Powodem tego jest fakt, że w rzeczywistości zmieniasz tylko „lokalną” kopię oValue, a nie oValuesiebie.

David Sumich
źródło