Dziwne zachowanie przy zwracaniu wartości z funkcji

0

Cześć. Mamy taką sytuację:

#include <iostream>
using namespace std;

int & foo(int k){ //funkcja zwracająca referencje
	int c = 12;
	return c; //return k;
}

int main() {
	int k = 666;
	int & b = foo(k); //przypisanie do referencji innej referencji
	
	cout<<b;
	// your code goes here
	return 0;
}

Przy zwracaniu c, program działa logicznie, stos jest czyszczony po wyjściu z funkcji foo i zwracana jest pusta referencja (albo jakaś śmieciowa). Natomiast gdy zwrócę k, wyświetla mi 666, mimo że też powinno zwrócić pustą referencję ponieważ k jest przesyłane przez wartość i podobnie jak ze zmienną c jest tworzona nowa kopia k. Na czym polega różnica?

1

Nie możesz bezkarnie zwracać referencje/wskaźnik na zmienną lokalną lub parametr przekazany przez wartość, po zakończeniu funkcji te zmienne znikają więc masz referencje/wskaźnik donikąd.

0

Przecież to właśnie napisałem :D Znam ten efekt. Wiem o co z tym chodzi. Natomiast zastanawia mnie dlaczego gdy zwracam 'k' ten efekt nie zachodzi. Przecież też jest tworzona na stosie nowa kopia, która jest dostępna tylko w ciele funkcji.

0

Na tym kompilatorze i tym środowisku z tym kodem dookoła z c - działa, z k - nie. Na innym kompilatorze lub innym środowisku lub innym kodem dookoła będzie inaczej.

0

Czyli jest to tzw. UB ? Wszystko zależy od kompilatora? Na logikę nie powinno, bo zachowanie w takich sytuacjach jest jasno zdefiniowane - zmienne lokalne są usuwane przez wyjście z bloku i referencje do nich są puste. W przypadku kompilatora używanego przez ideone.com zachodzi coś sprzecznego z logiką.

1

Czyli jest to tzw. UB ? Wszystko zależy od kompilatora? Na logikę nie powinno, bo zachowanie w takich sytuacjach jest jasno zdefiniowane - zmienne lokalne są usuwane przez wyjście z bloku i referencje do nich są puste. W przypadku kompilatora używanego przez ideone.com zachodzi coś sprzecznego z logiką.

Dokładnie, to UB. UB to znaczy że może działać, może nie działać, może zwracać (pseudo)losową wartość (i do tego za każdym razem inną), może zwracać zawsze zero, może spowodować segrault, może spowodować uruchomienie calc.exe na Twoim komputerze (albo nethacka jak o gcc mowa), może spowodować odpalenie rakiet nuklearnych, etc.

W skrócie, standard nie daje po prostu /żadnych/ gwarancji jak to działa. W tym przypadku działa bo tak przypadkowo spowodował kompilator (ale drugim razem niekoniecznie zadziała), w innym może nie działać.

(jakby co to było to samo co napisał @_13th_Dragon tylko dłuższe).

edit: Przeczytałem jeszcze raz to co cytowałem. Jeśli pytasz /dlaczego/ to jest UB to w krótka odpowiedź brzmi "bo tak mówi standard".
Długa odpowiedź brzmi: referencja jest implementowana najczęściej jako wskaźnik. Dla twórców kompilatorów problemem by było robienie jakichś gwarancji odnośnie tego jak zachowuje się wskaźnik wskazujący na nieistniejącą zmienną (dodatkowo w niektórych przypadkach takie gwarancje są niemożliwe, poza tym że są niepraktyczne). Dlatego referencja kiedy jest nieprawidłowa sobie po prostu gdzieś wskazuje, a Ty otrzymujesz to co przypadkowo tam siedzi.

1

Zmienne nie są "usuwane" zostają na stosie z tą samą wartością zaś referencji do nich nikt nie opróżnia aby stali się "pustymi". Referencje nadal masz na ten sam obszar pamięci w którym kiedyś była ta zmienna.

0

@msm dzięki za obszerne wyjaśnienie :)
@_13th_Dragon, skoro nie są usuwane to dlaczego nie można z nich korzystać? Tzn, że gdy wyjdę z bloku funkcji, te zmienne i ich wartości nadal są w pamięci? Wiem że tak jest w przypadku pamięci alokowanej na stercie (poprzez malloc/new) ale nie na stosie. Trochę się to kłóci z tym co przeczytałem w "mądrych książkach", tj. że gdy wychodzimy z bloku np. funkcji, wszystkie zmienne lokalne tam są usuwane.

3

Tzn, że gdy wyjdę z bloku funkcji, te zmienne i ich wartości nadal są w pamięci? Wiem że tak jest w przypadku pamięci alokowanej na stercie (poprzez malloc/new) ale nie na stosie. Trochę się to kłóci z tym co przeczytałem w "mądrych książkach", tj. że gdy wychodzimy z bloku np. funkcji, wszystkie zmienne lokalne tam są usuwane.

Mądre książki upraszczają mocno. Z drugiej strony, prawdopodobnie nie potrzebujesz tego wiedzieć ;)

Jeśli czujesz się na siłach, obszerne wyjaśnienia są tutaj: http://en.wikipedia.org/wiki/X86_calling_conventions (w szczególności akapit o cdecl)

Skrócona wersja, wymaga trochę wiedzy niekopoziomowej, ale mam nadzieję że pomoże. No więc tak naprawdę, to wygląda to tak:

Przekazując parametry do funkcji, tak naprawdę są one wrzucane na pewien stos, z którego funkcja docelowa je czyta. Na szczyt stosu (dzięki czemu wiemy skąd czytać) wskazuje wskaźnik stosu, nazwijmy go ESP.

Teraz załóżmy że masz funkcję

int foo(int a, int b, int c) {
}

I wywołujesz ją jako:

foo(1, 2, 3)

Wygeneruje to taki kod maszynowy:

push	3 ; trzeci argument na stos
push	2 ; drugi argument na stos
push	1 ; pierwszy argument na stos
call	foo ; wywołanie funkcji
add 	esp, 12 ; poprawienie wskaźnika stosu (cofnięcie o 3 parametry do góry: każdy po 4 bajty, razem 3*4=12 bajtów)

Przed wywołaniem foo stos wygląda tak:

[to co było wcześniej na stosie] <- ESP

W momencie wywołania funkcji foo, na stosie będzie coś takeigo:

[to co było wcześniej na stosie]
3 (trzeci argument)
2 (drugi argument)
1 (pierwszy argument)
adres_powrotu (szczegół techniczny) <- ESP

A po tym jak foo skończy się wykonywać, będzie prawdopodobnie wyglądać tak:

[to co było wcześniej na stosie] <- ESP
(trzeci argument)
(drugi argument)
(pierwszy argument)
adres_powrotu (szczegół techniczny)

Argumenty leżą poniżej wskaźnika stosu, czyli są już nieaktualne, ale to nie znaczy że zmienne są niszczone w jakiś sposób - po prostu siedzą tam aż coś je nadpisze.

Jeśli zwrócisz referencję na trzeci argument, to będzie pokazywać na miejsce w pamięci gdzie siedzi trzeci argument. Nie ma znaczenia że ten argument jest już nieaktualny.

[to co było wcześniej na stosie] <- ESP
(trzeci argument) <- referencja na to miejsce w pamięci
(drugi argument)
(pierwszy argument)
adres_powrotu (szczegół techniczny)

Swoją drogą możesz to łatwo zobaczyć wywołując po zwróceniu referencji inną funkcję - prawdopodobnie wtedy pamięc zostanie już nadpisana.

edit: przy okazji widać na czym polega kopiowanie zmiennych przy wywoływaniu funkcji - na stos wrzucane są po prostu ich obecne wartości

0

Super, teraz wszystko czaję - o to mi chodziło. Dzięki!

1 użytkowników online, w tym zalogowanych: 0, gości: 1