Code Problem mit konstantem Pointer



  • Mhhhh mich wunder das jetzt alles ein wenig, denn in meinem C++ Einsteiger Buch wird im selben Stil programmiert bzw. in die C++ Materie eingeführt wie in meiner Vorlesung durch den Prof... :-|

    Leider check ich in deinem Code nix, da wir davon noch nichts gemacht hatten. Hab zwar schon sehr häufig im Internet gelesen dass man anstatt statischer arrays nurnoch diese Vektorklasse nutzen soll, da diese dynamisch bleibt... aber wenn wir das noch nicht in der Vorlesung hatten, sieht mein Prof das wohl auch eher ungern in meinem Code 😕

    Wenn ich die Aufgabe hätte lösen können wie ich wollte, hätte ich es vermutlich auch anders gelöst 🕶


  • Mod

    dyingangel666 schrieb:

    Mhhhh mich wunder das jetzt alles ein wenig, denn in meinem C++ Einsteiger Buch wird im selben Stil programmiert bzw. in die C++ Materie eingeführt wie in meiner Vorlesung durch den Prof... :-|

    Das ist leider nicht verwunderlich. Da draußen gibt es viele Programmierbücher (besonders im deutschsprachigen Raum) die von Autoren geschrieben werden, die sich ein halbes Jahr eine Sprache angucken und dann ihr Standardbuch dazu schreiben (sie haben einmal vor 15 Jahren ein gutes Buch geschrieben, dass sie immer wieder ein wenig anpassen). Das Buch bekommt dann für seinen Stil auf Amazon gute Noten und verkauft sich gut. Doch natürlich ist niemand nach so kurzer Zeit qualifiziert, über eine Sprache ein Buch zu schreiben, das gilt nicht nur für C++. Und besonders nicht für beeinflussbare Anfänger. Leider trotzdem sehr verbreitet, da es Geld macht 😞 . Guck also lieber, was ein Autor sonst noch so treibt. Niemand kann jedes Jahr ein neues Buch zu einer neuen Sprache rausbringen, das was taugt.

    Leider check ich in deinem Code nix, da wir davon noch nichts gemacht hatten.

    Ja, das ist kein Wunder. C und C++ haben nicht viel miteinander zu tun.

    Hab zwar schon sehr häufig im Internet gelesen dass man anstatt statischer arrays nurnoch diese Vektorklasse nutzen soll, da diese dynamisch bleibt...

    Das ist an sich eine schlechte Begründung, dynamische Arrays nimmst du natürlich nur wenn du Dynamik brauchst (was jedoch der Regelfall ist). Dann natürlich std::vector. Falls du statische Arrays brauchst, dann nimm sie auch. Oder besser std::array 🙂 . Aber ja: Dein Prof wird vermutlich nur ganz leise Gerüchte über std::vector gehört haben und über std::array vermutlich gar nix.
    Internettutorials sind übrigens erfahrunsgemäß in der Programmierung auch selten gut, da du nicht wissen kannst, ob der Autor qualifiziert ist. Jedenfalls werden sie gerne von absoluten Anfänger geschrieben, die sich selber für die größten halten. Erfahrenere Leute trauen sich nicht, etwas zu schreiben, da sie mit ihrem größeren Überblick erkennen, dass sie keine Gurus sind. Echte Gurus schreiben gleich ein Buch und lassen sich gut bezahlen dafür.



  • SeppJ schrieb:

    Ja, das ist kein Wunder. C und C++ haben nicht viel miteinander zu tun.

    Also uns wird immer erzählt, dass C eine Untermege von C++ ist und man es dann im Prinzip ja kann, wenn man C++ lernt.



  • Zeiger für Java-Programmierer

    oder: Eine Einführung in Werte-Semantik und Gültigkeitsbereiche

    Wenn man von Java kommt, sollten Zeiger einem sehr vertraut sein: in Java werden alle Objekte von Klassen über Zeiger angesprochen. Im Java-Jargon spricht man von Referenzen, wenn man meint, was in C++ Zeiger sind. Hier besteht Verwirrungspotential, weil der Begriff "Referenz" in C++ anders belegt ist als in Java. Neu dazu kommt Zeigerarithmetik (dazu kommen wir später), weg fällt Garbage Collection. Beides hat Auswirkungen darauf, wie man Zeiger in C++ im Gegensatz zu Java verwendet, aber zunächst nicht darauf, was sie sind.

    Achtung: Der folgende C++-Codeschnipsel ist kein Beispiel für guten Code.

    Wenn man beispielsweise in Java schreibt

    // "Referenzen"
    MeineKlasse obj1 = new MeineKlasse();
    MeineKlasse obj2 = obj1;
    
    obj2.foo();
    

    entspricht das grob folgendem C++-Code:

    // "Zeiger"
    MeineKlasse *obj1 = new MeineKlasse();
    MeineKlasse *obj2 = obj1;
    
    obj2->foo();
    

    Jetzt haben wir aber ein organisatorisches Problem: In C++ gibt es keine Garbage-Collection. Das Objekt, auf das unsere Zeiger zeigen, muss irgendwann wieder weggeräumt werden, und wenn wir uns von Hand darum kümmern müssten, würde das bei massig Zeigern ausgesprochen unübersichtlich. Aus diesem Grund macht man das in C++ nicht so! Und damit hängt auch zusammen, warum man es in C++ ausdrücklich angeben muss, wenn man einen Zeiger haben will, und warum man es nach Möglichkeit nicht tut.

    Wertesemantik

    Im obrigen Beispiel zeigen obj1 und obj2 jeweils auf das selbe Objekt. Lasse ich den Zeigerkram weg und schreibe

    MeineKlasse obj1;
    MeineKlasse obj2 = obj1;
    
    obj2.foo();
    

    ...dann handelt es sich bei obj1 und obj2 um zwei verschiedene Objekte; obj2 ist eine Kopie von obj1. Wenn obj2.foo() obj2 verändert, verändert obj1 sich nicht. Auch können obj1 und obj2 nicht auf Null gesetzt werden - es sind Objekte, keine Verweise. Anders als in Java sind Klassen in C++ keine Referenztypen.

    Ich könnte allerdings

    MeineKlasse *zgr1 = &obj1; // &obj1 ist die Adresse von obj1
    zgr1->foo();
    

    schreiben und dann durch zgr1 obj1 verändert. Dann ist obj1 ein Objekt und zgr1 ein Zeiger auf obj1. Oder auch

    MeineKlasse &ref1 = obj1;
    ref1.foo();
    

    ...Dann ist ref1 eine Referenz (C++-Jargon, anders als in Java), d. h. ref1 ist jetzt ein anderer Name für obj1. Merke: Referenzen in C++ dürfen nicht null sein und können nach ihrer Erstellung nicht auf andere Objekte umgehangen werden.

    Dir wird auch auffallen, dass in diesem Codeschnipsel kein "new" auftaucht - das liegt daran, dass der Speicher für diese Objekte nicht vom Heap kommt. In dieser Form verwendet liegen die Objekte lokal, d. h. in diesem Fall auf dem Call-Stack. Sie verhalten sich vergleichbar mit Basisdatentypen wie int und double in Java. "Lokal" kann in anderem Zusammenhang auch direkt in einem umgebenden Objekt oder Array bedeuten. Beispielsweise wird ein Objekt einer Klasse Rechteck der Form

    class Punkt {
    ...
      double x, y;
    };
    
    class Rechteck {
    ...
      Punkt lo; // links oben
      Punkt ru; // rechts unten
    };
    

    in C++ ein Speicherlayout wie dieses erzeugen:

    +--------+--------+--------+--------+
    |  lo.x  |  lo.y  |  ru.x  |  ru.y  |
    +--------+--------+--------+--------+
    

    ...wohingegen das gleiche Objekt in Java etwa so aussähe:

    +--------+--------+
    |  lo    |  ru    |
    +--------+--------+
        |        |              +--------+--------+
        |        +------------->|   x    |   y    |
        |                       +--------+--------+
        |
        |                       +--------+--------+
        +---------------------->|   x    |   y    |
                                +--------+--------+
    

    Verwaltungszeug wie vtables dabei mal außen vor gelassen.

    Gültigkeitsbereiche

    Jetzt liegt unser Objekt also auf dem Call-Stack. Schön, dass wir so billig Speicher kriegen und uns nicht mit anderen Threads prügeln müssen, aber wie läuft das nachher mit dem Aufräumen? Und wo bleibt das Objekt, wenn der Funktionsaufruf, zu dem die Position auf dem Call-Stack gehört, beendet ist?

    Die Antworten auf beide Fragen gehören zusammen. Das Aufräumen passiert in unserem Fall automatisch, und es passiert, wenn der Gültigkeitsbereich des Objektes verlassen wird. Der Gültigkeitsbereich ist:

    1. Bei Objekten mit automatischer Speicherdauer der umgebende Block
    2. Bei Klassenmembern der Gültigkeitsbereich des umgebenden Objekts
    3. Bei dynamisch (mit new) erzeugten Objekten bis zu dem Zeitpunkt, an dem sie wieder gelöscht werden.

    Stellen wir 3. erstmal zurück. Konkret bedeuten 1. und 2., dass

    void foo() {
      MeineKlasse obj1;
    
      if(irgendwas()) {
        Rechteck r;
    
        ...
      } // r wird hier zerstört.
        // Dadurch werden auch r.lo und r.ru zerstört.
    
    } // obj1 wird hier zerstört.
    

    Offensichtlicher Caveat dabei: Nachdem ein Objekt zerstört wurde, kann es nicht mehr benutzt werden. Zeiger und Referenzen, die auf dieses Objekt verwiesen haben, dürfen nach seiner Zerstörung naturgemäß nicht mehr benutzt werden. Es obliegt den Programmierer, dies sicherzustellen.

    Die Zerstörung eines Objektes geschieht durch eine spezielle Methode, den sog. Destruktor. Dieser führt ggf. benutzerdefinierten Code aus und zerstört dann von hinten nach vorne die Datenmember des Objekts (ggf. inklusive der Basisklassenobjekte). Caveat: Wenn ein Zeiger zerstört wird, wird nicht automatisch auch das Objekt zerstört, auf das er zeigt. Ist ein solches Verhalten gewollt, sollte man Smart-Pointer benutzen (siehe unten).

    Dynamischer Speicher

    Es kommt vor, dass automatische Speicherdauer nicht ausreicht. Habe ich eine verkettete Liste und möchte dieser einen neuen Wert hinzufügen, sollte der neue Listenknoten den Aufruf der Hinzufügemethode überleben. Dafür fordert man etwas Speicher vom Heap an und erstellt das neue Objekt statt auf dem Call-Stack in diesem Speicher. Will sagen: Man lässt den new-Operator das für einen machen und erhält so einen Zeiger auf ein neu erstelltes Objekt:

    MeineKlasse *zgr = new MeineKlasse;
    MeineKlasse *arr = new MeineKlasse[n]; // Array dynamischer Länge
    
    ...
    
    delete   zgr; // um wieder aufzuräumen.
    delete[] arr; // new -> delete, new[] -> delete[].
    

    Das Aufräumen passiert dann zum Beispiel in einer Methode, die Elemente aus der Liste löscht, und dem Destruktor der Liste.

    Aber das alles immer von Hand machen zu müssen...so was vergisst man zu leicht. Also...

    RAII, oder: dämlicher Name, aber sehr, sehr wichtige Idee.

    Ich habe in letzter Zeit manchmal den Begriff SBRM (scope-based resource management) für RAII (resource acquisition is initialisation) gesehen, was eigentlich ein treffenderer Name wäre, aber RAII war halt zuerst da. Wie dem auch sei, grundsätzlich geht es um folgende Idee:

    Wenn ich etwas anfordere, was spätere Aufräumarbeiten erfordert, wird dieses sofort in den Besitz eines Objektes mit definiertem Gültigkeitsbereich gegeben, dessen Aufgabe die Verwaltung der Ressource ist. Der Destruktor dieses Objektes übernimmt spätestens die Aufräumarbeit.

    Im Beispiel der verketteten Liste geht der Listenknoten sofort in den Besitz der Liste über, die sich dann um die Verwaltung des Speichers kümmert. In anderen Fällen geht das noch direkter:

    #include <fstream>
    
    void cat(char const *filename) {
      std::ifstream fd(filename); // Eine Datei wird geöffnet. Der Dateideskriptor gehört fd.
      std::cout << fd.rdbuf();
    
    } // Hier endet der Gültigkeitsbereich von fd. Der Destruktor schließt
      // den Dateideskriptor, den fd verwaltet.
    
    int main() {
      cat("foo.txt");
      // An dieser Stelle im Programm sind keine Dateihandles mehr offen.
    }
    

    Alle Container der Standardbibliothek arbeiten nach diesem Muster. Und zuletzt der Hinweis auf

    Smart-Pointer: std::unique_ptr, std::shared_ptr

    std::unique_ptr und std::shared_ptr sind ziemlich schmale Klassenvorlagen, die die gängigsten Fälle von Speichermanagement in RAII-Form kapseln. std::unique_ptr erhebt alleinigen Besitzanspruch auf das ihm anvertraute Objekt, std::shared_ptr teilt sie sich mit anderen Objekten seiner Art über einen Referenzzähler. So könnte ich beispielsweise

    {
      std::unique_ptr<MeineKlasse> ptr(new MeineKindKlasse());
    
      ...
    } // Das Objekt wird hier wieder gelöscht
    

    schreiben und müsste mich nicht um die Aufräumarbeit kümmern. Syntaktisch verhält sich ptr dank Operatorüberladung genau wie ein Zeiger. Analog kann ich

    {
      std::vector<std::shared_ptr<MeineKlasse> > meine_objekte;
    
      meine_objekte.emplace_back(new MeineKindKlasse());       // Objekt 1
      meine_objekte.emplace_back(new MeineAndereKindKlasse()); // Objekt 2
    
      std::shared_ptr<MeineKlasse> ptr = meine_objekte[0];
    
      meine_objekte.clear(); // Hier wird Objekt2 zerstört.
                             // Objekt1 wird zunächst noch von ptr am Leben gehalten
    
    } // Hier wird ptr zerstört, der Referenzzähler für Objekt1 geht auf Null, und
      // Objekt1 wird zerstört.
    

    schreiben.

    Zeigerarithmetik

    Das erwähne ich jetzt nur der Vollständigkeit halber; üblicherweise sollte man von Zeigerarithmetik die Finger lassen, bis man weiß, was man tut. Grundsätzlich läuft Zeigerarithmetik wie folgt: Ich habe ein Array, also einen Speicherbereich, in dem mehrere Objekte direkt hintereinander liegen. Nehmen wir mal ints:

    +---+---+---+---+---+---+---+---+
    | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    +---+---+---+---+---+---+---+---+
    

    Und nehmen wir weiter einen Zeiger p (für "Pointer") in dieses Array:

    +---+---+---+---+---+---+---+---+
    | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    +---+---+---+---+---+---+---+---+
                  ^
                  |
                  p
    

    In diesem Fall ist *p == 4, und

    +---+---+---+---+---+---+---+---+
    | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
    +---+---+---+---+---+---+---+---+
          ^       ^           ^
          |       |           |
        p - 2     p         p + 3
    

    Ferner ist p[n] eine Kurzschreibweise für *(p + n), d. h. das, worauf p + n zeigt. Also ist p[0] == 4, p[3] == 7 und p[-2] == 2.

    Und das ist auch schon alles. Man muss damit allerdings höllisch aufpassen, weil es ein elendes Gefummel ist und man sich verdammt leicht damit vertun und in Speicher landen kann, der einem nicht gehört. Nicht gut.



  • Das kann man gleich mal irgendwo sticky machen...



  • Erstmal danke für den ausführlichen Beitrag, soweit war mir das auch schon klar. Müsste es hier

    MeineKlasse &ref1 = obj1;
    ref1.foo();
    

    auch nicht heissen

    MeineKlasse ref1& = obj1;
    ref1.foo();
    


  • Sehr guter Beitrag, Seldon!

    Nein, das & gehört zum Typ und nicht zum Variablennamen. In diesem Kontext bedeutet es, dass der folgende Variablenname eine Referenz (Alias) auf eine andere Variable ist. Nicht zu verwechseln mit dem Adressoperator &, der die Speicheradresse einer Variablen zurückgibt.



  • dyingangel666 schrieb:

    Sprich ich will meine Zeichen, die in der getStrings Funktion ermittelt werden, am Schluss in das aAlphas Array packen und das ganze irgendwie mit dem Funktionsparametern die so wie bei mir sind, da diese ja vorgegeben wurden vom Prof.

    Das Problem mit der Aufgabe ist, dass sie meines erachtens widersprüchlich ist:
    int getStrings(const char* const* params, int nparams, const char** strings, size_t maxStrings);
    Hier kannst Du in das array strings keine strings legen, weil es ja ein array von const char* ist.
    D.h. mit kopieren ist da nix. Du kannst allerdings - Pointer auf die Original strings aus argv ablegen. Keine Kopie, aber gut genug.
    Das Array, dass Du dann übergibst muesste allerdings dann auch dem Formalparameter entsprechen:

    int getStrings(const char* const*, int, const char**, size_t);  // array von const char* 
    const char** str_array = new const char*[size];
    //...
    str_array[i] = argv[j];  // in getString()
    //....
    delete [] str_array;
    

    Wenn Du allerdings wirklich Kopien anlegen willst (mit strdup() ), sieht einiges anders aus:

    int getStrings(const char* const*, int,  char**, std::size_t);  // dritter Parameter zum fuellen und liebhaben...(ginge auch char** const, ist aber Kokolores!)
    
    char** str_array = new char*[size];
    //...
    
    str_array[i] = strdup(argv[j]);  // in getString()
    //...
    std::free(str_array[i]);
    
    delete []str_array;
    


  • Ah super Furble, das hat mir jetzt geholfen. Werde das heute mittag mal ausprobieren. Hatte jetzt zwischenzeitlich einfach mal bei der getStrings Funktion die Parameter geändert wie bei getNumbers, also keine konstanten Ptr.

    Danke



  • Kleiner Nachtrag noch zu RAII:

    Wenn du mit Java 7 schon gearbeitet hast, kennst du etwas ähnliches wie RAII vom AutoCloseable-Interface. Wenn ich das richtig im Kopf habe, ist die Syntax da

    try( 
      Ressourcenverwalter obj = new Ressourcenverwalter(); // implements AutoCloseable
    ) {
      dieseFunktionKannEineExceptionWerfen();
    } // obj.close() wird hier aufgerufen, auch wenn eine Exception geworfen wird.
    

    Es ist kein Zufall, dass die Java-Leute den Mechanismus in die Exception-Handling-Syntax eingebunden haben; auch in C++ ist RAII der bevorzugte Mechanismus, um Exceptionsicherheit herzustellen. Zum Beispiel

    void foo() {
      std::lock_guard<std::mutex> lock(mein_mutex);
    
      das_hier_kann_exceptions_werfen();
    } // lock wird hier zerstört, mein_mutex dadurch wieder aufgeschlossen.
    

    Vergleiche das mit

    void foo() {
      try {
        mein_mutex.lock();
    
        das_hier_kann_exceptions_werfen();
    
        mein_mutex.unlock();
      } catch(...) {
        mein_mutex.unlock();
        throw;
      }
    }
    

    Eklig, oder? Und damit wäre es auch leichter, sich zu vertun. Den gleichen Effekt hast du mit nacktem new:

    // Falsch:
    
    void foo() {
      int *arr = new int[10];
    
      dies_kann_eine_exception_werfen();
    
      delete[] arr; // Wenn eine Exception geworfen wird, wird das nie aufgerufen.
    }
    
    // Eklig:
    
    void foo() {
      int *arr = new int[10];
    
      try {
        dies_kann_eine_exception_werfen();
    
        delete[] arr;
      } catch(...) {
        delete[] arr;
        throw;
      }
    }
    
    // Richtig:
    
    void foo() {
      std::vector<int> vec(10);
    
      dies_kann_eine_exception_werfen();
    } // vec wird hier zerstört und sein Speicher freigegeben, auch wenn eine Exception geworfen wurde.
    

    In gutem C++-Code sieht man solche Dinge andauernd.


Anmelden zum Antworten