Call by Value, Call by Reference, und dann... ?



  • Hallo zusmamen,

    wie der Name des Titels schon aussagt, habe ich gerade ein kleines Problem beim Verständnis bzw. bei der Syntax. Vorab - mir sind beide Dinge klar. Ein Beispiel sollte es verdeutlichen was ich meine.

    Was ich mich gerade frage ist folgendes Konstrukt:

    //- call by value
    void functions1(int b)
    {
        std::cout << "function1 int b:   "<< b << "\n";
        ++b;
    };
    //- call by reference
    void functions2(int* b)
    {
        std::cout << "function2 int* b:  "<< *b << "\n";
        ++*b;
    };
    //- call by 
    void functions3(int& b)
    {
        std::cout << "function3 int& b:  "<< b << "\n";
        ++b;
    };
    
    int main()
    {
    
        int a{23};
    
        std::cout << "a: " << a << "\n";
        functions1(a);                       // kopieren der Variablen a
        std::cout << "a: " << a << "\n"; 
        functions2(&a);                      // Adresse von a wird übergeben
        std::cout << "a: " << a << "\n";
        functions3(a);                       // was passiert hier?
        std::cout << "a: " << a << "\n";
    
        std::cout << "\n\nProgram end\n";
    
        return 0;
    }
    

    Die Ausgabe:

    a: 23
    function1 int b:   23
    a: 23
    function2 int* b:  23
    a: 24
    function3 int& b:  24
    a: 25
    
    Program end
    

    function3 leuchtet mir gerade noch nicht wirklich ein. Wieso kann ich die Variable ändern, wenn ich doch den Wert der Variablen übergebe und nicht die Adresse? Außer das Programm übersetzt das irgendwie so, dass es ähnlich ist wie call-by-reference?

    In function3 übergebe ich eigentlich den Wert der Variable und wäre dann wieder bei call-by-value aber das kann ja nicht stimmen, da ich in der Funktion den Wert von a manipulieren kann.

    Viele Grüße und danke fürs Durchlesen.
    Tobi



  • dein "call by reference" nicht "by reference" sondern "by pointer". 3 ist "by reference"



  • KN4CK3R schrieb:

    dein "call by reference" nicht "by reference" sondern "by pointer". 3 ist "by reference"

    "by pointer" gibt es nicht. "by reference" bedeutet, dass man mit dem Ursprungsobjekt arbeitet. "call by reference" kann in C++ sowohl mit Pointern als auch mit Referenzen umgesetzt werden.


  • Mod

    dein "call by reference" nicht "by reference" sondern "by pointer".

    Doch, man nennt es call by reference. reference bzeichnet in diesem Kontext einen Verweis, nicht eine eigentliche Referenz im Sinne der Sprache.

    Wieso kann ich die Variable ändern, wenn ich doch den Wert der Variablen übergebe und nicht die Adresse?

    Du übergibst die Variable, jedoch wird sie nicht kopiert sondern ein Verweis auf sie "gespeichert". Eine Referenz

    int& t = a;
    

    ist, grob gesagt, ein anderer Name ( t ) für das Objekt auf welches sie verweist ( a ).



  • Hallo,

    danke für deine Erklärung... dann ist alles in meinem Buch richtig 🙂
    Wenn man aber sagt, dass man mit Referenzen arbeiten soll (Funktionsübergabe), ist dann function3 gemeint.

    Grüße
    Tobi



  • Hi,

    das ist eben der große Unterschied zw. Referenzen und Zeigern. Während Du bei Zeigern explizit den Adress- (&) und den Dereferenzoperator (*) nutzen mußt. Darfst Du das bei Referenzen eben nicht, sie werden implizit eingefügt.

    Aus diesem Grund verwende ich bei Funktionsdefinitionen, wenn ich call by reference brauche eher selten Referenzen sondern lieber Zeiger. In diesem Fall sieht man nämlich die Aufrufart auch beim Aufrufer. Wenn ich aber call bei referece nur aus Performanzgründen nutze und ich den Wert eh nicht verändern will, dann nehme ich auch schon mal 'ne const reference:

    void function ( const type &reference )
    {
        ...
    }
    

    manchmal ist es aber angenehmer mit Refernzen zu arbeiten:

    void function ( std::vector<int>  &myVector )
    {
       int first = myVector[0];
       ...
    }
    

    vs

    void function ( std::vector<int>  *myVector )
    {
       int first = (*myVector)[0];
       ...
    }
    

    mfg Martin



  • //- call by reference
    void functions2(int* b)
    

    ist nicht Call by Reference. Der Pointer (den man als Argument übergibt) wird kopiert, d.h. es handelt sich um Call by Value.

    C und Java unterstützen kein Call by Reference. Man kann zwar einen Pointer (in C) oder eine Referenz (in Java) übergeben aber diese werden stets kopiert.



  • mgaeckler schrieb:

    Aus diesem Grund verwende ich bei Funktionsdefinitionen, wenn ich call by reference brauche eher selten Referenzen sondern lieber Zeiger.

    Und dann hast du das Problem, dass Zeiger nullptr sein können. Ist eine gute Quelle für UB... 🙄



  • Shor-ty schrieb:

    Hallo,

    danke für deine Erklärung... dann ist alles in meinem Buch richtig 🙂
    Wenn man aber sagt, dass man mit Referenzen arbeiten soll (Funktionsübergabe), ist dann function3 gemeint.

    Grüße
    Tobi

    Zeiger haben ein hohes Fehlerpotential. Referenzen zwar auch, aber einige Fehler lassen sich damit vermeiden. Der größte Vorteil von Referenzen ist, daß diese grundsätzlich bei der Definition auch initialisiert werden MÜSSEN. Nicht initialisierte Zeiger mit den entsprechenden Nebenwirkungen sind bei schlampigen Programmieren leider oft ein Problem.

    mfg Martin



  • icarus2 schrieb:

    mgaeckler schrieb:

    Aus diesem Grund verwende ich bei Funktionsdefinitionen, wenn ich call by reference brauche eher selten Referenzen sondern lieber Zeiger.

    Und dann hast du das Problem, dass Zeiger nullptr sein können. Ist eine gute Quelle für UB... 🙄

    Bei welcher Gelegenheit liefert der Adressoperator eine NULL? Nicht initialisierte Zeiger sind ein Problem, das hat aber erst mal nix mit Zeigern als Funktionsparameter zu tun. Außerdem manchmal will man ja eine NULL zurückliefern oder übergeben, um mitzuteilen "Sorry hab gerade nix". Das geht mit Referenzen gar nicht.

    mfg Martin


  • Mod

    Bei welcher Gelegenheit liefert der Adressoperator eine NULL?

    *patsch*
    Du bist zu gutgläubig.

    Außerdem manchmal will man ja eine NULL zurückliefern oder übergeben, um mitzuteilen "Sorry hab gerade nix".

    Genau, und wenn nicht nehmen wir trotzdem Zeiger. Einfach so. Weil wir fehleranfällige Programme fördern, sonst hat dieses Forum ja keinen Daseinszweck.

    Dein Argument mit der & -Syntax, dass auf der call-site klarer wird was mit dem Argument geschieht, ist relativ schwach. Der Programmierer muss wissen was eine Funktion tut um einen Aufruf derer zu verstehen. Dementsprechend muss er auch die Funktionssignatur kennen. Und wenn er sie kennt, braucht er keinen Hinweis darauf dass der Parameter eine Referenz ist.

    Rohe Zeiger als Funktionsparameter sind in C++ IMO fast nur für Arrays sinnvoll. Für optionale Parameter gibt es Überladung. Und optionale Rückgabetypen sind std::optional (bald im TR) oder unique_ptr .

    Referenzen zwar auch, aber einige Fehler lassen sich damit vermeiden.

    Nein, Referenzen haben ein sehr kleines Fehlerpotential. Nur hängende Referenzen stellen ein Problem dar.



  • icarus2 schrieb:

    //- call by reference
    void functions2(int* b)
    

    ist nicht Call by Reference. Der Pointer (den man als Argument übergibt) wird kopiert, d.h. es handelt sich um Call by Value.

    C und Java unterstützen kein Call by Reference. Man kann zwar einen Pointer (in C) oder eine Referenz (in Java) übergeben aber diese werden stets kopiert.

    Das ist aber sehr spitzfindig und für die Praxis völlig ohne Bedeutung. K & R haben übrigens über diesen Umstand ein eigenes kleines Kapitel. Wer in C von call by reference sprach, hat immer POINTER als Formalparameter gemeint, in C++ können es halt AUCH REFERENZEN sein.

    mfg Martin



  • Arcoth schrieb:

    Dein Argument mit der & -Syntax, dass auf der call-site klarer wird was mit dem Argument geschieht, ist relativ schwach.

    ne

    Arcoth schrieb:

    Der Programmierer muss wissen was eine Funktion tut um einen Aufruf derer zu verstehen.

    wieder ne

    Arcoth schrieb:

    Dementsprechend muss er auch die Funktionssignatur kennen. Und wenn er sie kennt, braucht er keinen Hinweis darauf dass der Parameter eine Referenz ist.

    blah

    Arcoth schrieb:

    Rohe Zeiger als Funktionsparameter sind in C++ IMO fast nur für Arrays sinnvoll.

    schon wieder bullshit

    Arcoth schrieb:

    Für optionale Parameter gibt es Überladung.

    quatsch

    Arcoth schrieb:

    Und optionale Rückgabetypen sind std::optional (bald im TR) oder unique_ptr .

    nope, auch nich

    Arcoth schrieb:

    Nein, Referenzen haben ein sehr kleines Fehlerpotential.

    und das ganz sicher nich

    Arcoth schrieb:

    Nur hängende Referenzen stellen ein Problem dar.

    ok



  • Arcoth schrieb:

    Bei welcher Gelegenheit liefert der Adressoperator eine NULL?

    *patsch*
    Du bist zu gutgläubig.

    Das beantwortet nicht meine Frage.

    Arcoth schrieb:

    Außerdem manchmal will man ja eine NULL zurückliefern oder übergeben, um mitzuteilen "Sorry hab gerade nix".

    Genau, und wenn nicht nehmen wir trotzdem Zeiger. Einfach so. Weil wir fehleranfällige Programme fördern, sonst hat dieses Forum ja keinen Daseinszweck.

    Also in meiner knapp 30 jährigen Praxis kamen mir nicht initialisierte Zeiger eher selten vor. "Vagabundierene" Zeiger sind eher ein Problem, das geht aber eben auch mit Referenzen.

    Jetzt aber mal ein Beispiel, das ich mit Referenzen nicht (oder langsamer) lösen kann:
    Ich habe ein Baum und brauche eine Funktion, die mir einen Knoten zurückliefert, der eine bestimmte Bedingung erfüllt. Was soll die Funktion zurückgeben, wenn es einen solchen Knoten nicht gibt? NULLREFERENCE gibt es ja nicht. Eine Exception zu werfen ist ja auch nicht das gelbe vom Ei, weil das auf die Performanz geht. Dem Aufrufer ist es aber ein leichtes zu überprüfen, ob er eine NULL zurückbekommen hat.

    Arcoth schrieb:

    Dein Argument mit der & -Syntax, dass auf der call-site klarer wird was mit dem Argument geschieht, ist relativ schwach. Der Programmierer muss wissen was eine Funktion tut um einen Aufruf derer zu verstehen. Dementsprechend muss er auch die Funktionssignatur kennen. Und wenn er sie kennt, braucht er keinen Hinweis darauf dass der Parameter eine Referenz ist.

    Grundätzlich gebe ich Dir recht, aber wenn man sich neu in ein Projekt einarbeiten muß, ist das schon sehr hilfreich.

    Arcoth schrieb:

    Rohe Zeiger als Funktionsparameter sind in C++ IMO fast nur für Arrays sinnvoll. Für optionale Parameter gibt es Überladung. Und optionale Rückgabetypen sind std::optional (bald im TR) oder unique_ptr .

    Überladung ist war manchmal (vieleicht auch oft) auch eine sinnvolle Lösung, aber leider nicht immer die beste. Beispiel:

    Eine Funktion macht einen Abgleich des lokalen Dateisysters mit der Datenbank. Die Funktion kann vom Anwender direkt über ein Menüpunkt aufgerufen werden oder aber von einem Hintergrundprozess. Im letzteren Fall übergebe ich der Funktion ein Dateihandle mit, in der sie ein Protokoll schreiben kann im ersten Fall ist das nicht notwendig:

    bool THE_FOLDER_REF::refresh( bool recursive, ostream *stream )
    {
    	....
    	if( stream )
    		*stream << "Folder Created: " << newFolder->getPath() << '\n';
    	....
    }
    

    Nur wegen zwei Zeilen zwei Version einer 240 Zeilen langen Funktion?

    Merke: Zeiger sind gut beherschbar, wenn man die möglichen Probleme kennt und diese bei der Implementierung im Hinterkopf behält. Für komplexere Angelegenheiten sollte man natürlich irgendwelche Smart-Pointer benutzen.

    Arcoth schrieb:

    Referenzen zwar auch, aber einige Fehler lassen sich damit vermeiden.

    Nein, Referenzen haben ein sehr kleines Fehlerpotential. Nur hängende Referenzen stellen ein Problem dar.

    Das würde ich eben nicht als kleines Fehlerpotential betrachten. Das kann ziemlich gemein sein. Das geht schneller als man denkt.

    mfg Martin



  • Kellerautomat schrieb:

    Arcoth schrieb:

    Dein Argument mit der & -Syntax, dass auf der call-site klarer wird was mit dem Argument geschieht, ist relativ schwach.

    ne

    doch

    Arcoth schrieb:

    Der Programmierer muss wissen was eine Funktion tut um einen Aufruf derer zu verstehen.

    wieder ne

    wieder doch

    Arcoth schrieb:

    Dementsprechend muss er auch die Funktionssignatur kennen. Und wenn er sie kennt, braucht er keinen Hinweis darauf dass der Parameter eine Referenz ist.

    blah

    quatsch

    Arcoth schrieb:

    Rohe Zeiger als Funktionsparameter sind in C++ IMO fast nur für Arrays sinnvoll.

    schon wieder bullshit

    schon wieder quatsch

    Arcoth schrieb:

    Für optionale Parameter gibt es Überladung.

    quatsch

    blah

    Arcoth schrieb:

    Und optionale Rückgabetypen sind std::optional (bald im TR) oder unique_ptr .

    nope, auch nich

    ganz sicher ja

    Arcoth schrieb:

    Nein, Referenzen haben ein sehr kleines Fehlerpotential.

    und das ganz sicher nich

    jup, ham se

    Arcoth schrieb:

    Nur hängende Referenzen stellen ein Problem dar.

    ok

    ja



  • mgaeckler schrieb:

    Arcoth schrieb:

    Bei welcher Gelegenheit liefert der Adressoperator eine NULL?

    *patsch*
    Du bist zu gutgläubig.

    Das beantwortet nicht meine Frage.

    Bei einem Pointer bei einer Funktion, wo man immer etwas gültiges haben will, muss man das dokumentieren, dass die Funktion keinen nullptr haben will.
    Bei einer Funktion mit Referenz, ist das klar, dass da immer ein gültiges Objekt hin soll.
    Die beste Dokumentation ist die, die man im Code sieht.
    Aus denselben Gründen verwende ich auch unsigned Typen.

    Jetzt aber mal ein Beispiel, das ich mit Referenzen nicht (oder langsamer) lösen kann:
    [...]

    Brauchste Pointer, nimm Pointer. Pointer sind nicht per sé schlecht.

    Arcoth schrieb:

    Dein Argument mit der & -Syntax, dass auf der call-site klarer wird was mit dem Argument geschieht, ist relativ schwach. Der Programmierer muss wissen was eine Funktion tut um einen Aufruf derer zu verstehen. Dementsprechend muss er auch die Funktionssignatur kennen. Und wenn er sie kennt, braucht er keinen Hinweis darauf dass der Parameter eine Referenz ist.

    Grundätzlich gebe ich Dir recht, aber wenn man sich neu in ein Projekt einarbeiten muß, ist das schon sehr hilfreich.

    Gut, das kann sein. Da man aber eh nicht immer weiß, was alle Parameter bedeuten sollte man sich sowieso die Funktionssignatur anzeigen lassen (geht bei IDEs ja schon automatisch.

    Merke: Zeiger sind gut beherschbar, wenn man die möglichen Probleme kennt und diese bei der Implementierung im Hinterkopf behält. Für komplexere Angelegenheiten sollte man natürlich irgendwelche Smart-Pointer benutzen.

    +1, aber komplexere Angelegenheiten sind besitzende Pointer. Kein rohe Pointer sollte eine Resource besitzen.


  • Mod

    Das beantwortet nicht meine Frage.

    Das mit der Gutgläubigkeit war darauf bezogen dass das Funktionsargument kein &Objekt -Ausdruck sein muss.

    Das geht schneller als man denkt.

    Referenzen sollten stets lokal sein. Nur als Funktionsparameter oder lokale Variablen auftreten.
    Hängende Referenzen können nur entstehen indem

    • Ein funktionslokales Objekt zurückgegeben wird, wobei der Rückgabetyp eine Referenz ist. Wird vom Compiler per Warnung angemeckert, die man natürlich stets aktiviert hat.
    • Allgemeinere Variante des oberen Stickpunkts: Das Objekt auf das eine Referenz verweist wird zerstört. Das dürfte niemals passieren da Referenzen keine Klassenmember sein sollten. Sie sollten auch nicht mit etwas initialisiert werden was zerstört wird bevor die Referenz out-of-scope geht.
    • die Referenz mit einem ungültigen lvalue initialisiert wird - bspw. Dereferenzierung eines Zeigers der auf kein Objekt zeigt. Hier ist das Problem auf einen hängenden Zeiger oder falsche Array-Indizen zurückzuführen. Ist der Zeiger uninitialisiert (und damit singulär) oder ein Null-Zeiger erzeugt die Dereferenzierung sowieso UB ohne die Referenz auch nur einzubeziehen.

    Schuld für die Situationen im letzten Stichpunkt haben sicherlich nicht Referenzen.

    Zeiger sind gut beherschbar, wenn man die möglichen Probleme kennt und diese bei der Implementierung im Hinterkopf behält.

    Ja, das ist richtig. 👍

    Nur wegen zwei Zeilen zwei Version einer 240 Zeilen langen Funktion?

    😮 Eine Funktion sollte i.d.R. nicht länger als 10-15 Zeilen lang werden!



  • Arcoth schrieb:

    Das beantwortet nicht meine Frage.

    Das mit der Gutgläubigkeit war darauf bezogen dass das Funktionsargument kein &Objekt -Ausdruck sein muss.

    Das hab ich mir schon fast gedacht.

    Arcoth schrieb:

    Das geht schneller als man denkt.

    Referenzen sollten stets lokal sein. Nur als Funktionsparameter oder lokale Variablen auftreten.
    Hängende Referenzen können nur entstehen indem

    • Ein funktionslokales Objekt zurückgegeben wird, wobei der Rückgabetyp eine Referenz ist. Wird vom Compiler per Warnung angemeckert, die man natürlich stets aktiviert hat.
    • Allgemeinere Variante des oberen Stickpunkts: Das Objekt auf das eine Referenz verweist wird zerstört. Das dürfte niemals passieren da Referenzen keine Klassenmember sein sollten. Sie sollten auch nicht mit etwas initialisiert werden was zerstört wird bevor die Referenz out-of-scope geht.
    • die Referenz mit einem ungültigen lvalue initialisiert wird - bspw. Dereferenzierung eines Zeigers der auf kein Objekt zeigt. Hier ist das Problem auf einen hängenden Zeiger oder falsche Array-Indizen zurückzuführen. Ist der Zeiger uninitialisiert (und damit singulär) oder ein Null-Zeiger erzeugt die Dereferenzierung sowieso U ohne die Referenz auch nur einzubeziehen.

    Schuld für die Situationen im letzten Stichpunkt hat sicherlich nicht die Referenz.

    Es gibt bestimmt noch mehr Möglichkeiten. sowas z.B.:

    std::vector< anyType > myVector;
    
    ....
    
    anyType &myType = myVector[i];
    
    .... // any thing that "could" change the vector
    
    myType.member = 1;
    

    Das Gemeine daran ist, das kann mal funktionieren aber muß nicht.

    Arcoth schrieb:

    Nur wegen zwei Zeilen zwei Version einer 240 Zeilen langen Funktion?

    😮 Eine Funktion sollte i.d.R. nicht länger als 10-15 Zeilen lang werden.

    Ja, wünschenswert ist das sicherlich. Aber leider nicht immer ohne Klimmzüge realisierbar.

    mfg Martin



  • icarus2 schrieb:

    //- call by reference
    void functions2(int* b)
    

    ist nicht Call by Reference. Der Pointer (den man als Argument übergibt) wird kopiert, d.h. es handelt sich um Call by Value.

    C und Java unterstützen kein Call by Reference. Man kann zwar einen Pointer (in C) oder eine Referenz (in Java) übergeben aber diese werden stets kopiert.

    Also so wie ich das sehe handelt es sich nicht um call-by-value weil du ja nicht den Wert (value) sondern die Adresse übergibst. Zwar wird ja trotzdem ein Pointer angelegt der im Speicher ist, allerdings hat dieser die Adresse der übergebenen Variablen. Somit würde ich nicht von "call-by-value" sprechen.

    Kritik ist erwünscht.


  • Mod

    anyType &myType = myVector[i];
     
    .... // any thing that "could" change the vector
     
    myType.member = 1;
    

    ➡

    Arcoth schrieb:

    Allgemeinere Variante des oberen Stickpunkts: Das Objekt auf das eine Referenz verweist wird zerstört. [...] Sie sollten auch nicht mit etwas initialisiert werden was zerstört wird bevor die Referenz out-of-scope geht.

    Ja, wünschenswert ist das sicherlich. Aber leider nicht immer ohne Klimmzüge realisierbar.

    Kompensieren die Vorteile nicht die Klimmzüge?

    Also so wie ich das sehe handelt es sich nicht um call-by-value weil du ja nicht den Wert (value) sondern die Adresse übergibst.

    Eine Adresse ist auch ein Wert. Ein Zeiger hat einen Wert, nämlich eine Adresse.


Log in to reply