C++ Lernen - Verständnisfragen



  • Hallo Freunde,

    ich bin Thomas (39) und lerne gerade ohne jegliche Vorkenntnisse und nur aus Spaß an der Freude C++.
    Ich arbeite gerade das Buch Einstieg in C++ von Thomas Teis durch.

    Edit:
    "Ich möchte diesen Thread nutzen um Sachen, die ich nicht verstehe, mit euch zu klären. Auch möchte ich vermeiden für jede kleinere Frage einen neuen Thread zu erstellen."

    Vielen Dank für eure Unterstützung vorab!

    1. Frage:

    Im o.g. Buch gibt es die Übung 6A: Ein Programm mit 5 Eingaben, welche in einem Array gespeichert werden sollen und die Ausgabe soll das Array in umgekehrter Reihenfolge ausgeben. Die umgekehrte Ausgabe wurde bisher nicht behandelt. Ich habe dazu ja einiges an Lösungen im Netz gefunden, wo das Problem aber anders abgearbeitet wird (z.B. https://www.techiedelight.com/de/print-contents-array-reverse-order-cpp/). Ich möcht nun nicht blind irgendeinen anderen Code übernehmen, sondern versuchen zu verstehen...

    Ich hoffe mir kann jemand in einfachen Worten erklären warum meine Ausgabe leer bleibt. Mein Gedanke: Bei der Eingabe zähle ich mit einer for-Schleife durch das Array ja jeweils +1 weiter zum nächsten Wert. Warum also nicht am Ende (ganzahliger Wert eingabe.size() würde den letzten Wert ja vorgeben?!) des Array anfangen und rückwärts zählen?! Der auskommentierte Bereich dient hier nur als Kontrolle der Eingabe.

    #include <iostream>
    #include <array>
    using namespace std;
    
    int main ()
    {
    array <double,5> eingabe;
    
    for (int i=0; i<eingabe.size(); i++)
    {
        cout << "Eingabe " << i+1 << ". Zahl: ";
        cin >> eingabe.at(i);
        cout << endl;
    }
    /*
    cout << "Ausgabe des Feldes in richtiger Reihenfolge: ";
    for (int k=0; k<eingabe.size(); k++)
    {
        cout << eingabe.at(k);
    }
    cout << endl;
    cout << endl;
    */
    
    cout << "Ausgabe des Feldes in umgekehrter Reihenfolge: ";
    for (int j=eingabe.size(); j=0; j--)
    {
       cout << eingabe.at(j);
    }
    cout << endl;
    cout << endl;
    }
    


  • Wenn dein Array die Länge 5 hat, dann:

    • ist die Länge 5, d.h. eingabe.size() == 5
    • die gültigen Indizes sind 0, 1, 2, 3, 4 (das sind insgesamt 5, aber die 5 ist nicht dabei)

    Der Code

    for (int j=eingabe.size(); j=0; j--)
    {
       cout << eingabe.at(j);
    }
    

    ist auf mehreren Ebenen falsch:

    a) die Abbruchbedingung j = 0 ist bei dir eine Zuweisung (weise j 0 zu und gib 0, also false, zurück). Du willst aber wohl die Schleife durchlaufen, solange j nicht 0 ist (auf gleich würdest du mit == (2 Gleich-Zeichen!) vergleichen, aber das ist ja die "laufe solange wie"-Bedingung, nicht die "brich ab, wenn"-Bedingung)
    b) Der Startwert j = size() setzt j auf 5, aber der maximal erlaubte Index ist 4. Wenn du a) behebst, wird dir das hier um die Ohren fliegen
    c) size ist ein size_t, kein int. Deine Loop-Variable sollte optimalerweise auch size_t sein. Achtung, das hat aber kein Vorzeichen. So zu loopen, ist also immer problematisch (bzw. erfordert besondere Vorsicht/Sorgsamkeit). Daher würde ich davon abraten.

    Wie sollst du es stattdessen machen?
    Schau dir mal an, wie man mit Iteratoren über ein Array iteriert. Also von begin() bis end(). Für Rückwärts-Iteration könntest du einfach von rbegin() bis rend() iterieren. Dann brauchst du dir über so Dinge wie "was ist 0 minus 1" keine Gedanken zu machen. Siehe auch: https://en.cppreference.com/w/cpp/container/array/rbegin

    (wenn du über die Indzies loopen willst, würde ich mit for (size_t i = arr.size(); i > 0; --i) und dann mit dem Index i - 1 arbeiten - aber generell: das geht oft mit einem Iterator einfacher und vor allem generischer)

    Und zuletzt obligatorisch: dein Compiler sollte für deine for-Schleife eine Warnung ausgeben. Wenn nicht: hast du Warnungen aktiviert? Dein Compiler sollte etwas ausgeben, das ungefähr so aussieht:

    warning: using the result of an assignment as a condition without parentheses [-Wparentheses]
    for (int j=eingabe.size(); j=0; j--)
                               ~^~
    <source>:26:29: note: place parentheses around the assignment to silence this warning
    for (int j=eingabe.size(); j=0; j--)
                                ^
                               (  )
    <source>:26:29: note: use '==' to turn this assignment into an equality comparison
    for (int j=eingabe.size(); j=0; j--)
                                ^
                                ==
    


  • @wob sagte in C++ Lernen - Verständnisfrage (Ausgabe rückwärts - aus Array):

    wenn du über die Indzies loopen willst, würde ich mit for (size_t i = arr.size(); i > 0; --i) und dann mit dem Index i - 1 arbeiten - aber generell: das geht oft mit einem Iterator einfacher und vor allem generischer

    Ja, Interatoren sollten die erste Wahl sein und sind m.E. auch etwas, dass man als Einsteiger möglichst früh lernen sollte (und nicht unter ferner liefen, wie leider in so vielen mäßigen Büchern und Tutorials).

    Ansonsten gibts auch noch nen kleinen Trick, wie man diese unsigned-Rückwärtsschleifen nicht verbockt - einfach immer ne Vorwärtsschleife verwenden:

    for (std::size_t i = 0; i < eingabe.size(); i++)
    {
        cout >> eingabe.at(eingabe.size() - 1 - i);
    }
    

    Gut, bei der Index-Berechnung muss man etwas nachdenken, dafür verhaut man die Schleife selbst nicht. 🙂



  • Danke, sehr verständlich! Stimmt da hatte ich einen Denkfehler...

    Wenn man auf ein Index in einem Array verweist, das nicht existiert, dann stürtzt das Programm ab - so stand das auch im Buch.

    Mich hatte es schon gewundert das mein Ansatz nirgends benutzt wird. An den Bedingungen der for-Schleife hatte ich schon rumprobiert. Das gab dann aber immer einen Programmabsturz. In dem Buch steht das size() einen Ganzzahligen Wert zurück liefert, was nach meinem aktuellem Verständnis ein int war/ist...

    for (size_t i = arr.size(); i > 0; --i) würde dann aber ja den Index "0" des Arrays nicht mit ausgeben?! Also müsste man sicher i>=0 nehmen, richtig?

    So funktioniert die Funktion:

    for (int j=eingabe.size()-1; j>=0; j--)
    {
       cout << eingabe.at(j) << " ";
    }
    

    Nutze ich:

    for (size_t j=eingabe.size()-1; j>=0; j--)
    {
       cout << eingabe.at(j) << " ";
    }
    

    erfolgt die Ausgabe ebenfalls korrekt. Jedoch hängt das Programm einen Fehler an (out of range)... wenn ich den Index "0" des Arrays nicht mit ausgeben lasse also i > 0 funktioniert das Programm mit size_t.

    Iteratoren wurden bisher nicht und werden laut Index des Buches auch auch erst kurz vor Ende behandelt, size_t wird nicht dazu gehören.
    Ich arbeite mit Code::Blocks + Mingw. Der Compiler lieferte keine Fehler. Vielleicht finde ich ja Online ein gutes Tutorial dazu, damit er mir die Funktionen von CodeBlock und Compiler etwas erklärt. Das würde so sehr helfen, wenn ich wieder mal eine Zuweisung anstatt eines Vergleichs mache usw.

    Der Ansatz von Finnegan ist auch intersant. Danke dafür!



  • @Drgreentom sagte in C++ Lernen - Verständnisfrage (Ausgabe rückwärts - aus Array):

    for (size_t i = arr.size(); i > 0; --i) würde dann aber ja den Index "0" des Arrays nicht mit ausgeben?! Also müsste man sicher i>=0 nehmen, richtig?

    Nein, das hat schon so seine Richtigkeit. @wob hat ja geschrieben, dass diese Schleifen mit unsigned-Zähler (wie solche vom Typ size_t) nicht leicht korrekt hinzubekommen sind. i >= 0 ist nämlich für diese vorzeichenlosen Ganzzahlen eine Tautologie, also ein Ausdruck der immer wahr ist, für jeden Wert, den diese Zahl annehmen kann (da es keine unsigned-Zahlen kleiner 0 gibt). Die Schleife würde also nie enden und auf falsche Indizes zugreifen.

    Auch können unsigned-Variablen über- und unterlaufen (siehe Arithmetischer Überlauf), das heisst, dass falls eingabe.size() == 0 gilt, j = eingabe.size() - 1 zu einem Unterlauf führt, nach dem j den maximalen Wert hat, den eine Variable dieses Typs annehmen kann. Auch mit dieser Schleife würdest du auf falsche Indizes zugreifen.

    Mit int passiert das übrigens nicht, da dieser auch negative Werte annehmen kann und in deinem Fall nicht "unterläuft".

    Es gibt mit einem int-Zähler allerdings das Problem, dass dieser (üblicherweise) nur Werte bis 23112^{31}-1 annehmen kann (auf bestimmten Systemen auch nur bis 21512^{15}-1. Damit läuft man dann auf Probleme, wenn der std::vector zu gross wird (auf einem 64-Bit-System geht std::size_t üblicherweise bis 26312^{63}-1). Bei kleinen, per Hand eingegebenen Datenmengen kann man das machen, aber es ist keine saubere Umsetzung.

    Ab C++20 gibt es auch noch std::ssize, das gibt die Größe eines Containers als vorzeichenbehafteten Integer-Typen zurück, z.B. als 64-Bit Integer. Das wäre eine etwas sauberere Lösung:

    for (auto j = std::ssize(eingabe) - 1; j >= 0; j--)
    {
       cout << eingabe.at(j) << " ";
    }
    

    ... auch wenn dieser Typ ebenfalls nicht alle Werte eines std::size_t abdeckt, aber zumindest auf einem 64-bit-System kann man derzeit nicht genug Speicher haben, als dass ein std::vector zu groß dafür würde.

    Wenn du mit std::size_t arbeitest, musst du es schon so machen wie @wob vorgeschlagen hat (oder eben meinen Vorwärtsschleifen-Ansatz). Aber ich würde dir das generell nur als Übung empfehlen und nahelegen, dich möglichst früh in Iteratoren einzuarbeiten. Mit denen lassen sich Probleme dieser Art elegant vermeiden.



  • @Drgreentom sagte in C++ Lernen - Verständnisfrage (Ausgabe rückwärts - aus Array):

    Wenn man auf ein Index in einem Array verweist, das nicht existiert, dann stürtzt das Programm ab - so stand das auch im Buch.

    Das kommt darauf an.

    a) Wenn du, wie in deinem Beispiel, mit eingabe.at(i) zugreifst, dann wird einfach eine Exception (std::out_of_range) geworfen, siehe https://en.cppreference.com/w/cpp/container/array/at. Das Programm stürzt nicht ab. Du musst die Exception natürlich fangen.

    b) Wenn du mit eingabe[i] zugreifst, dann darf das Programm abstürzen. Es muss aber nicht. Der Absturz ist NICHT garantiert. Es darf aber auch ein falsches Ergebnis herauskommen. Oder sogar das richtige (hm, was bedeutet "richtig" hier?). Es darf halt einfach irgendwas rauskommen. Es ist schlicht undefiniertes Verhalten. Siehe https://en.cppreference.com/w/cpp/container/array/operator_at (Zitat aus Notes: "Accessing a nonexistent element through this operator is undefined behavior." Und UB willst du nie haben.)

    Ich arbeite mit Code::Blocks + Mingw. Der Compiler lieferte keine Fehler.

    Du musst Warnungen einschalten. Warnungen sind allerdings keine Fehler, du solltest sie aber wie Fehler behandeln. Ich habe Code::Blocks nicht, aber StackOverflow zeigt hier Screenshots, wie man Warnungen einschaltet: https://stackoverflow.com/questions/43447796/how-to-enable-narrowing-warnings-in-codeblocks (schalte mindestens -Wall und -Wextra ein)



  • Das beantwortet fürs erste meine Fragen. Vielen Dank



  • Ich habe mir jetzt dazu die Lösung im Buch angesehen:

    for(int i=eingabe.size()-1; i>=0; i--)
         cout << eingabe.at(i) << " ";
    

    Das entspricht ja etwa dem Ansatz wie ich es mir überlegt hatte.

    Ich gebe euch aber recht. Die Variante mit rend ist schöner - nur wurden for_each - Schleifen noch nicht behandelt. Kann aber sicher nicht Schaden vorzugreifen und es gleich richtig zu lernen, auch weil man ja mehr als nur simple Ausgabe damit machen kann.

    for_each(eingabe.crbegin(), eingabe.crend(), [](double ausgabe){cout << ausgabe << ' '; });
    

    ---------------------------------------------------------------------------------Ende 1.Frage--------------------------------------------------------------------


  • Mod

    Das ist aber auch ein bisschen eine alte Art und Weise, das zu schreiben. So wie anno 2010, ca. Du siehst ja vielleicht selbst, dass das nicht unbedingt intuitiv verständlich ist.

    Seither sind aber zwei wesentliche Dinge dazu gekommen:

    1. For-Schleifen, die einfach nur über alle Elemente in einen Container iterieren sollen, schreibt man nun als ranged base for:
    for (auto element: container)
    {
        // Tu etwas mit dem Element aus dem Container
    }
    
    1. Es sind allerlei Helferlein dazu gekommen, mit denen man einfacher mit solchen Containersequenzen umgehen kann, unter anderem reverse_view. Das Beispiel hinter dem Link passt ziemlich exakt zu deinem Problem, und zeigt wie man es mit diesen Mitteln auf verschiedene Arten und Weisen lösen kann.

    Letzteres setzt aber einen einigermaßen aktuellen Compiler voraus, weil es "erst" ein paar Jahre alt ist, was für C++-Verhältnisse quasi brandneu ist.



  • @Drgreentom sagte in C++ Lernen - Verständnisfragen:

    Ich habe mir jetzt dazu die Lösung im Buch angesehen:

    for(int i=eingabe.size()-1; i>=0; i--)
         cout << eingabe.at(i) << " ";
    

    Ich hoffe das Buch erwähnt auch, dass int mit Vorzeichen hier wichtig ist. Wenn man das durch ein auto ersetzt (i bekommt dann den Typ, der von eingabe.size() zurückgegeben wird) dann fliegt einem das um die Ohren. Nur um das nochmal zu unterstreichen, da der Großteil der Diskussion genau darum ging 😉



  • Um mal einen anderen Ansatz zu zeigen:

    std::vector<int> foobar( 10, 1 );
    for ( std::size_t i = 0; i < foobar.size(); ++i )
    {
       std::cout << foobar.at( foobar.size() - i - 1 ) << "\n";
    }
    
    

    Man muss eigentlich in fast keinem Fall unbedingt rückwärts iterieren und wenn doch, dann am besten über die von den anderen hier schon genannten Methoden.
    Das rückwärts-durchlaufen führt insbesondere bei Neueinsteigern oft zu Problemen, wenn man die Funktionsweise von C++ ( auch die von positiven ganzzahligen Datentypen ) noch nicht komplett durchdrungen hat.

    Ich verwende eigentlich ausschließlich die Range-Based-Loop und eben die Variante mit Iteratoren, insbesondere dann wenn ich den Iterator z.B. zum Aufruf von "erase()" benötige.



  • @It0101 sagte in C++ Lernen - Verständnisfragen:

    Ich verwende eigentlich ausschließlich die Range-Based-Loop und eben die Variante mit Iteratoren, insbesondere dann wenn ich den Iterator z.B. zum Aufruf von "erase()" benötige.

    Hm. Das würde ich nicht machen, denn z.B. vector.erase invalidiert alle nachfolgenden Iteratoren, auch das vorherige end. In der Range-Loop wurde das aber am Anfang geholt. Also eine range-Loop mit erase darin - das kann doch eigentlich nicht sinnvoll gehen?


  • Mod

    Das ist wahrscheinlich ungünstiger Satzbau und soll heißen, dass die Iterator-Variante dem range-based Loop vorgezogen wird, wenn es um Dinge wie erase geht. Denn anders geht es auch gar nicht, denn vector::erase arbeitet schließlich auf einem Iterator, nicht auf einem Wert. Für so etwas ist dann erase_if gut.



  • @wob sagte in C++ Lernen - Verständnisfragen:

    Hm. Das würde ich nicht machen, denn z.B. vector.erase invalidiert alle nachfolgenden Iteratoren, auch das vorherige end. In der Range-Loop wurde das aber am Anfang geholt. Also eine range-Loop mit erase darin - das kann doch eigentlich nicht sinnvoll gehen?

    Jain. Man muss natürlich trotzdem aufpassen.
    Es sieht in der Regel so aus:

    std::vector<int> foobar;
    for ( auto it = foobar.begin(); it != foobar.end(); )
    {
        if ( hasToBeDeleted( *it ) )
           it = foobar.erase( it );
       else
            ++it;
    }
    

    Das funktioniert, weil erase dann quasi schon den "neuen" iterator zurückliefert. Ich nutze das, wenn die Möglichkeit besteht, dass mehrere Elemente entfernt werden müssen.



  • @It0101 Hier hast du aber auch Iteratoren genutzt und nicht wie von wob vermutet eine range based for loop.



  • @Tyrdal sagte in C++ Lernen - Verständnisfragen:

    @It0101 Hier hast du aber auch Iteratoren genutzt und nicht wie von wob vermutet eine range based for loop.

    Ja natürlich. Ich fummel doch nicht innerhalb einer Range-Based-Loop mit erase rum.... 🤣

    Dann war das Missverständnis ein anderes, als ich dachte.

    Edit: jetzt hab ich's noch mal gelesen. Es verhält sich im Grunde genau so wie @SeppJ es geschrieben hat.



  • Ah, dann hatte ich dich falsch verstanden. Jetzt, wo ich deinen Post nochmal lese, ist es wohl das "und", was ich nicht richtig verstanden hatte.

    Statt

    Ich verwende eigentlich ausschließlich die Range-Based-Loop und eben die Variante mit Iteratoren, insbesondere dann wenn ich den Iterator z.B. zum Aufruf von "erase()" benötige.

    Wäre klarer gewesen:

    Ich verwende eigentlich ausschließlich die Range-Based-Loop. Wenn ich aber den Iterator z.B. zum Aufruf von "erase()" benötige, verwende ich die Variante mit Iteratoren.

    Das meintest du. Dann ist es natürlich richtig. Ich hatte den Text oben zu schnell gelesen.

    Wobei: in deinen Beispiel würde ich eher das erase-remove-Idiom einsetzen:

    Also statt

    for ( auto it = foobar.begin(); it != foobar.end(); )
    {
        if ( hasToBeDeleted( *it ) )
           it = foobar.erase( it );
       else
            ++it;
    }
    

    ...lieber sowas wie:

    foobar.erase(
        std::remove_if(foobar.begin(), foobar.end(), hasToBeDeleted),
        foobar.end()
    );
    

    (da vergesse ich nur sehr, sehr, sehr gerne das 2. Argument von erase. 😕 )



  • @wob Müsste nicht inzwischen sowas gehen:

    auto [begin, end] = std::ranges::remove_if(foobar, hastToBeDeleted);
    foobar.erase(begin, end);
    

    Edit: Code angepasst... erase hat gefehlt. Macht es so nicht viel besser.



  • @Schlangenmensch sagte in C++ Lernen - Verständnisfragen:

    @wob Müsste nicht inzwischen sowas gehen:

    auto [begin, end] = std::ranges::remove_if(foobar, hastToBeDeleted);
    foobar.erase(begin, end);
    

    Edit: Code angepasst... erase hat gefehlt. Macht es so nicht viel besser.

    Nicht in jedem fall da hier eine range erwartet wird.
    Aber im folgenden falle nicht
    vector enthält folgende werte

    1, 9, 5, 10

    hastToBeDeleted hat die Bedingung <value> <= 5
    Dann würde std::ranges::remove_if(foobar, hastToBeDeleted); wohl nach dem es auf den wert 9 trifft abbrechen. Und Dadurch würde der Wert 5 nicht gelöscht werden
    Oder im schlimmsten falle würde die 9 auch mit gelöscht werden.



  • @firefly Nö, warum: https://godbolt.org/z/fv8G9sf7M
    std::ranges::remove_if verschiebt die Elemente so, dass die Elemente, die nicht gelöscht werden am Anfang des Ranges stehen und die, die gelöscht werden am Ende. Und zurück gegeben wird die Subrange der zu löschenden Elemente.


Anmelden zum Antworten