Przekazywanie przez wartość a przekazywanie przez referencję/wskaźnik

0

Mamy taki kod:

#include <iostream>
using namespace std;
class SimpleCar
{
        public:
                SimpleCar();                 // konstruktor bezargumentowy
                SimpleCar(SimpleCar&);  // konstruktor kopiujący
                ~SimpleCar();              // destruktor
};
SimpleCar::SimpleCar(){cout << "Wywolanie kostruktora bezargumentowego...\n";}
SimpleCar::SimpleCar(SimpleCar&){cout << "Wywolanie kostruktora kopiujacego...\n";}
SimpleCar::~SimpleCar(){cout << "Wywolanie destruktora...\n";}
SimpleCar FunctionOne(SimpleCar theCar);          // parametr przekazywany poprzez wartość
SimpleCar* FunctionTwo(SimpleCar *theCar);      // parametr przekazywany poprzez wskaźnik
 int main()
{
        cout << "Tworze obiekt SimpleCar...\n";
        SimpleCar Mercedes;
        
        cout << "\n***Uzycie 'FunctionOne'***\n\n";
        cout << "Wywoluje FunctionOne...\n";
        FunctionOne(Mercedes);
        
 
        cout << "\n***Uzycie 'FunctionTwo'***\n\n";
        cout << "Wywoluje FunctionTwo...\n";
        FunctionTwo(&Mercedes);
        cout << "\n";
 
        return 0;
}
SimpleCar FunctionOne(SimpleCar theCar)
{
        cout << "FunctionOne. Wracam...\n";
        return theCar;
}
SimpleCar* FunctionTwo(SimpleCar *theCar)
{
        cout << "FunctionTwo. Wracam...\n";
        return theCar;
}

Wynik jego działania jest następujący:

 
Tworze obiekt SimpleCar...
Wywolanie kostruktora bezargumentowego...

***Uzycie 'FunctionOne'***

Wywoluje FunctionOne...
Wywolanie kostruktora kopiujacego...
FunctionOne. Wracam...
Wywolanie kostruktora kopiujacego...
Wywolanie destruktora...
Wywolanie destruktora...

***Uzycie 'FunctionTwo'***

Wywoluje FunctionTwo...
FunctionTwo. Wracam...

Wywolanie destruktora...

Teraz chcę rozwiać moje wątpliwości czy dobrze to wszystko rozumiem :)

Gdy tworzę obiekt, w odniesieniu do powyższego kodu, wywoływany jest konstruktor bezargumentowy (domyślny). Jeśli klasa miałaby jakieś pola, a także posiadała odpowiedni konstruktor tworzenie obiektu mogłoby wyglądać następująco: SimpleCar Mercedes(1994,lpg). Oczywiście mógłbym pozostawić deklarację SimpleCar Mercedes i wówczas wywołany byłby również konstruktor domyślny. Dodatkowo, jeśli sam nie zadeklaruję w klasie konstruktora bezargumentowego, to zostanie on stworzony podczas kompilacji. To chyba dobrze rozumiem.

Dalej mam wywołania dwóch funkcji. Pierwsza, FunctionOne, otrzymuje parametr poprzez wartość, zwraca również wartość. Druga, FunctionTwo, otrzymuje parametr poprzez wskaźnik do obiektu SimpleCar, zwraca również wskaźnik do obiektu.

Przejdę teraz do opisu działania obu funkcji w kodzie. Zacznę od FunctionOne. Najpierw wywołuję funkcję, co wiąże się z uprzednim wyświetleniem komunikatu zawartego w ciele funkcji main. Funkcja ta otrzymuje obiekt poprzez wartość, więc na stosie tworzona jest kopia przekazywanego obiektu, dlatego wywoływany jest konstruktor kopiujący. Następnie wykonanie przechodzi do ciała funkcji FunctioOne, o czym świadczy stosowny komunikat. Następnie wykonanie powraca do ciała funkcji main, jednocześnie FunctionOne zwraca poprzez wartość obiekt typu SimpleCar, co powoduje wywołanie konstruktora kopiującego (ponowne), gdyż na stosie tworzona jest kolejna kopia obiektu (tym razem jest to obiekt zwracany). Wartość ta nie jest przypisywana niczemu, obiekt na stosie jest niszczony, co powoduje wywołanie destruktora. Działanie FunctionOne jest zakończone, utworzona wcześniej kopia przekazanego jej obiektu jest niszczona, więc znowu wywoływany jest destruktor. Sterowanie wraca do ciała funkcji main.

Następnie wywoływana jest funkcja FunctionOne. Funkcja ta otrzymuje parametr przekazywany przez... (!) no i tu mam wątpliwości, nieco zgłupiałem. Wydaje mi się, że jest to przekazywanie parametru przez wskaźnik. Niezależnie czy używam wskaźników czy referencji do przekazywania parametrów, nie jest tworzona kopia obiektu, dlatego nie jest wywoływany konstruktor. Jest przekazywany jego (obiektu) adres. Obiekt zwracany jest również przez wskaźnik, więc nic nie jest niszczone. Sterowanie jest przekazywane do ciała funkcji main.

Na koniec wywoływany jest destruktor, gdyż program kończy działanie, a obiekt Mercedes jest niszczony.

Użycie funkcji, która otrzymuje parametry i zwraca wynik poprzez wartość wiązało się z dwukrotnym wywołaniem konstruktora kopiującego i dwukrotnym wywołaniem destruktora. Natomiast wykorzystanie wskaźników zredukowało ilość wywołań, ani konstruktor, ani destruktor nie musiał być wywołany. Wniosek jest zatem taki, że użycie wskaźników pozwala na większą wydajność. Czy tak jest zawsze ?

Czy dobrze rozumiem działanie kodu ? Czy można coś dodać ?

1

Skompiluj ten kod z włączoną optymalizacją (w GCC -O2) i zobacz co się stanie. Kompilatory starają się unikać niepotrzebnego kopiowania. Możesz zapoznać się z tym artykułem: http://cpp-next.com/archive/2009/08/want-speed-pass-by-value/

Oczywiście wszystkie te techniki są dla kompilatorów nieobowiązkowe, więc w ogólnym przypadku tak, jeżeli obiekt jest większy niż rozmiar wskaźnika to przekazywanie przez wartość będzie wolniejsze.

1

Oczywiście wszystkie te techniki są dla kompilatorów nieobowiązkowe, więc w ogólnym przypadku tak, jeżeli obiekt jest większy niż rozmiar wskaźnika to przekazywanie przez wartość będzie wolniejsze.

I dlatego na przykład w C++11 pojawiły się referencje do r-wartości (r-value references). Najkrócej i praktycznie mówiąc, tworząc metodę / konstruktor / operator z takim argumentem, zostanie on wywołany dla obiektów tymczasowych. Wtedy można sobie jego zawartość "ukraść". W praktyce unikając kosztownego kopiowania.

W zasadzie to na tej stronie, którą podał Endrju (i w kolejnej części, do której jest tam link) jest opisane wszystko, co można o tym powiedzieć.

0

Dzięki za odpowiedzi.

Endrju napisał(a):

Skompiluj ten kod z włączoną optymalizacją (w GCC -O2)

W Code::Blocks: "Build options... -> Compiler settings -> Compiler Flags -> Categories:Optimization" ustawiłem -O2 i różnicy nie widać.

Swoją drogą, chyba bezpieczniejszym rozwiązaniem byłoby użycie const, tzn. coś takiego:

const SimpleCar *const FunctionTwo(const SimpleCar *const theCar);
0

Dlaczego byłoby bezpieczniejsze? Const używasz tam, gdzie ma to sens, a nie walisz nim gdzie popadnie ;).

1

const powinno być wszędzie tam gdzie powinno, i nigdzie więcej ;-)

konstruktor kopiujący powinien wyglądać tak:

SimpleCar::SimpleCar(const SimpleCar&)

bo chyba nie zamierzasz modyfikować oryginalnego obiektu podczas kopiowania.

w przypadku twojej funkcji

const SimpleCar *const FunctionTwo(const SimpleCar *const theCar);

co prawda to zależy co ta funkcja ma robić, ale tak na oko to
pierwszy const jest raczej zbędny,
drugi bezsensowny,
trzeci może nawet wskazany (co może ale nie musi uzasadniać pierwszego consta),
a czwarty bez sensu.

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