Code Problem mit konstantem Pointer



  • Hi Leute. Hab seit diesem Semester C++ und mir fällt der Umstieg von Java nicht so leicht. Am meisten Probleme machen mir wie wohl den meisten Anfängern und Quereinsteigern die Pointer. Bis jetzt bin ich noch immer selbst auf die Lösung gekommen. Aber bei meiner aktuellen Aufgabe komm ich nicht weiter. Ich poste jetzt einfach mal meinen Code und gehe danach aufs Problem ein.

    #include <iostream>
    #include <cassert>
    #include <cstdlib>
    
    using namespace std;
    
    bool isNumber(char);
    bool isNumber(const char*);
    bool isAlpha(char);
    int getNumbers(const char* const*, size_t, double*, size_t);
    int getStrings(const char* const*, int, const char**, size_t);
    
    int main(int argc, char** argv) {
    
    	size_t length = 80;
    	double* aNumbers = new double[length];
    	char* aAlphas = new char[length];
    
    	const char** aPtr;
    	aPtr = (const char**)aAlphas;
    
    	cout << getNumbers(argv, argc, aNumbers, length) << " Zahlen in den Parametern gefunden!" << endl;
    	cout << "Schreibe gefundene Zahlen in Array aNumbers[]\n" << endl;
    
    	for(int i=0; i < getNumbers(argv, argc, aNumbers, length); i++) {
    		cout << "aNumbers[" << i << "]: " << aNumbers[i] << endl;
    	}
    
    	cout << "\n" << getStrings(argv, argc, aPtr, length) << " Strings in den Parametern gefunden!" << endl;
    	cout << "Schreibe gefundene Strings in Array aAlphas[]\n" << endl;
    
    	for(int i=0; i < getStrings(argv, argc, aPtr, length); i++) {
    		cout << "aAlphas[" << i << "]: " << aAlphas[i] << endl;
    	}
    
    return EXIT_SUCCESS;
    }
    
    bool isNumber(char c) {
    
    	if(c >= 48 && c <= 57)
    		return true;
    
    return false;
    }
    
    bool isNumber(const char* string) {
    
    	int laenge = strlen(string);
    	bool isNum = false;
    
    	for(int i=0; i < laenge; i++) {
    
    		if(isNumber(string[i]))
    			isNum = true;
    		else
    			isNum = false;
    	}
    return isNum;
    }
    
    bool isAlpha(char c) {
    
    	if(c >= 65 && c <= 90 || c >= 97 && c <= 122)
    		return true;
    
    return false;
    }
    
    int getNumbers(const char* const* params, size_t nparams, double* numbers, size_t maxNumbers) {
    
    	int nNumbers = 0;
    	char asChar = '\0';
    	double asDouble;
    	double* numbersPtr = numbers;
    
    	for(size_t i=1; i < nparams; i++) {
    
    		int paramLength = strlen(params[i]);
    
    		for(size_t j=0; j < paramLength; j++) {
    
    			if(isNumber(params[i][j])) {
    				nNumbers++;
    				asChar = params[i][j];
    				asDouble = atof(&asChar);
    
    				// Kontrollausgaben:
    				// cout << "Zahl in Param " << i << " an Index " << j;
    				// cout << " ist Zahl " << asDouble << endl;
    
    				*numbersPtr++ = asDouble;
    			}
    
    		}
    
    	}
    return nNumbers;
    }
    
    int getStrings(const char* const* params, int nparams, const char** strings, size_t maxStrings) {
    
    	int nStrings = 0;
    	char asChar;
    	const char** alphasPtr = strings;
    
    	for(size_t i=1; i < nparams; i++) {
    
    		int paramLength = strlen(params[i]);
    
    		for(size_t j=0; j < paramLength; j++) {
    
    			if(isAlpha(params[i][j])) {
    				nStrings++;
    				asChar = params[i][j];
    
    				// Kontrollausgaben:
    				// cout << "String in Param " << i << " an Index " << j;
    				// cout << " ist String " << asChar << endl;
    
    				*alphasPtr++ = asChar; // hier steckt der Fehler
    
    			}
    
    		}
    
    	}
    return nStrings;
    }
    

    In der Aufgabe ging es darum mit den vorgegebenen Funktionssignaturen ein Programm zu schreiben, dass die Kommandozeilenparameter auf Zahlen und Zeichen prüft, diese raussucht und jeweils in zwei Arrays schreibt. Mit den Zahlen funktioniert das auch alles. Allerdings macht mir die Funktion getStrings etwas Probleme, da ich die Zeichen am Schluss nicht ins Array bekomme, ansonsten funktioniert sie aber.

    Ich hab echt keinen Schimmer wo das Problem ist. Ich hab auch schon mit Casts versucht, hat aber auch nicht geholfen, war aber wahrscheinlich sowieso totaler Schwachsinn 😃



  • Warum lässt dich dein Prof mit C-Strings arbeiten? Mit std::string wäre das ganze so viel leichter.

    bool isNumber(const char* string) {
    
        int laenge = strlen(string);
        bool isNum = false;
    
        for(int i=0; i < laenge; i++) {
    
            if(isNumber(string[i]))
                isNum = true;
            else
                isNum = false;
        }
    return isNum;
    }
    

    Hier machst du folgendes: Du nimmst ein Array und überprüfst nacheinander, ob die Zeichen Zahlen sind. Wenn jetzt aber das 3. Zeichen ein Buchstabe ist, wird dein isNum auf false gesetzt. Wenn das nächste jetzt wieder eine Zahl ist, wird es auf true gesetzt. Beabsichtigt? Vorschlag: Du gibst, wenn isNumber() false zurückgibt direkt false zurück, ansonsten überprüfst du nur, ob das letzte Zeichen eine Zahl oder nicht ist.

    Den Rest guck ich mir jetzt nicht an, da ich von diesem elenden Pointergefrickel nach C-Style nichts halte.


  • Mod

    Abgesehen davon, dass ihr ganz ganz ganz grauenhaftes C++ lernt (man könnte das ganze Programm in etwa 5-10 Zeilen wesentlich sicherer, besser und lesbarer schreiben), sind folgende Dinge ganz offensichtlich falsch:

    Zeile 17: Wozu?

    Zeile 20: Wenn du einen reinterpret_cast benötigst (nicht anderes ist dein Cast im C-Stil, er versteckt es bloß, daher besser nicht benutzen) ist das ein fast sicheres Zeichen, dass etwas falsch ist. so auch hier:

    Ich hab auch schon mit Casts versucht, hat aber auch nicht geholfen, war aber wahrscheinlich sowieso totaler Schwachsinn

    Da hast du mit deiner Analyse recht. Casts lösen keine Probleme, sie verhindern bloß, dass der Compiler dich vor Schwachsinn warnt. Casts brauchst du nur, wenn du es besser weißt als der Compiler, was eigentlich nur der Fall ist, wenn du eine schrottige Vorlage bekommst, die man irgendwie richtig biegen muss. (Hmm, bei der Vorlage hier eigentlich gar nicht so unrealistisch 😃 )

    Jedenfalls musst du irgendwie dein Array mit Zeichenketten befüllen. Das heißt, du musst erst einmal im Stil von Zeilen 16-17 deinem aPtr ein Array von char-Pointern zuweisen und dann musst du alle diese Pointer durchgehen (Schleife) und ihnen noch einmal char-Arrays zuweisen. Und die kannst du dann in deiner Funktion befüllen.

    Was uns zum nächsten Punkt bringt:
    Zeile 116: Arrays und Zeichenketten kann man in C (ja, in C, denn du lernst ein C-Java Gemisch, mit C++ hat dies leider nicht im entferntesten zu tun) nicht durch Zuweisung kopieren. Damit verbiegst du bloß ein paar Pointer. Du brauchst die Funktionen strcpy() oder memcpy(). Oder eine eigene Schleife, die das Array elementweise kopiert (nicht anderes machen effektiv strcpy() und memcpy(), bloß vielleicht ein bisschen cleverer).

    Das sollte deine Probleme lösen.

    [rant]Du wirst aber in Zukunft sicherlich noch viele Probleme bekommen, wenn du weiter diesen Kurs besuchst. Dein Code ist wirklich sehr merkwürdig. Einerseits sieht alles stilistisch sehr sauber aus: Aussagekräftige Bezeichner, saubere Einrückung, durchaus gut durchdachte Struktur. Man sieht, hier kann jemand programmieren. Aber das was du da schreibst ist technisch auf dem Niveau von 1979. Nein, nicht einmal das. Eigentlich ist der ganze Zweck von C++, dieses Low-Level Gefrickel unnötig zu machen. Dein Lehrer scheint C++ als C mit cout aufzufassen. Tatsächlich ist wirklich das cout und das new das einzige was es bei deinem Programm so nicht in C gibt. Und das cout wird hier auch nur für Trivialausgaben benutzt, die auch die C-stdio erledigen könnte und das new ist so in modernem C++ praktisch ausgestorben und ist wohl der "C nach C++"-Übersetzungstabelle deines Lehrers entnommen, wo er noch malloc() benutzt hat. (Wobei ein malloc an der Stelle in modernem C auch nichts zu suchen hätte. Wie gesagt: Technischer Stand von vor 1979)
    [/rant]



  • @Sepp
    Mhhh naja. Wir hatten vorher Java gemacht. In seinem Script steht auch irgendwas mit C++ für Java Programmierer. Leider will mein Prof genau diesen Stil mit den C-Strings, da man mit denen seiner Meinung nach den Umgang mit Pointern besser versteht... Bei mir bewirkt das allerdings das Gegenteil, mich verwirren die ganzen Pointer, die dann manchmal noch konstant sind, und auf andere konstante Pointer zeigen, nur 😮

    Ich weiss dass das Ziel dieser Aufgabe wesentlich kürzer und leichter zu erreichen wäre. Nur leider müssten wir die Funktionen mit genau diesen Signaturen wie sie in meinem Code sind nutzen und selbst schreiben, es soll ja zur Übung dienen.

    @Der Tobi
    (a)Schreiben Sie eine Funktion bool isNumber(char c), die pr ¨ uft, ob das übergebene Zeichen eine Ziffer ist.
    (b) Schreiben Sie eine ¨uberladene Variante bool isNumber(const char* string), die prüft, ob der ¨ubergebene C-String ausschließlich aus Ziffern besteht.

    Deshalb die überladene Funktion isNumber, die meiner Meinung nach auch sinnlos ist, und in dem Code bei mir auch eigentlich keine Verwendung hat.

    Eigentlich ist mein Problem, dass der Compiler immer meckert wenn ich einem const char** ein char zuweisen will. 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.


  • Mod

    Nur mal so zur Info, was dir entgeht: So sähe das in halbwegs "normalem" C++ aus:

    #include <iostream>
    #include <vector>
    #include <sstream>
    #include <string>
    
    // Ich bin ja eigentlich kein Fan davon, zuerst zu deklarieren, sondern definiere
    // lieber gleich. Aber ich halte mich mal an deinen Stil:
    template<typename ValueType, typename Iterator> std::vector<ValueType> convert_to_list(Iterator begin, Iterator end);
    
    int main(int argc, char* argv[])
    {
      // Die Schwierigkeit ist eigentlich, die olle C-Schnittstelle der main-Funktion
      // zu benutzen. Machen wir es uns einfach und wandeln alles in Strings um:
      std::vector<std::string> args_as_string(argv, argv + argc);
      // Ok, war doch nicht so schwer :-)
    
      // Der Rest ist auch nicht viel schwieriger:
      auto args_as_double = convert_to_list<double>(argv, argv+argc);
      auto args_as_int = convert_to_list<int>(argv, argv+argc);
    
      //  Meinetwegen noch ein paar Kontrollausgaben:
      for (unsigned i = 0; i < args_as_string.size(); ++i)
        std::cout << "Das " << i+1 << ". Kommandozeilenargument als String: " 
                  << args_as_string[i] << '\n';
      std::cout << "Folgende double-Werte wurden in den Kommandozeilenargumenten erkannt:\n";
      for (auto value : args_as_double)
        std::cout << value << '\t';
      std::cout << "\nFolgende int-Werte wurden in den Kommandozeilenargumenten erkannt:\n";
      for (auto value : args_as_int)
        std::cout << value << '\t';
      std::cout << '\n';
    }
    
    // Nun aber die Definition von convert_to_list:
    // Dieser Code ersetzt praktisch dein gesamtes Programm
    template<typename ValueType, typename Iterator> std::vector<ValueType> convert_to_list(Iterator begin, Iterator end)
    {
      std::vector<ValueType> results;
      for (; begin != end; ++begin)
        {
          std::stringstream parser(*begin);
          ValueType value;
          if (parser >> value)
            results.push_back(value);
        }
      return results;
    }
    

    Wie angedeutet, kann man dein ganzes Programm durch 10-15 Zeilen ersetzen. Hier: Zeilen 14, 18, 19 und 36-47. Der Rest ist nur zur Demonstration.

    Wohlgemerkt: Ohne Speicherlöcher (die hast du momentan bei dir drin!), ohne Fehler bei falscher Eingabe (die auch!). Ohne dass ich beim Programmieren überhaupt auf diese Dinge achten musste, denn das wird alles automatisch für mich erledigt. Sieht für dich vielleicht etwas kryptisch aus, weil du noch nichts davon gelernt hast, aber wenn du C++ könntest, dann könntest du das flüssig lesen, easy peasy.

    Aber was soll man machen? Wenn du dir den Lehrer nicht aussuchen kannst, dann lernst du eben C mit Klassen. (Sogar nur mäßig gutes C, denn auch in C kann man schön programmieren) 😞



  • 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