Zunächst sollte ich sagen, dass C und C ++ die ersten Programmiersprachen waren, die ich gelernt habe. Ich habe mit C angefangen, dann viel C ++ in der Schule gemacht und bin dann zurück zu C gegangen, um fließend zu sprechen.
Das erste, was mich beim Lernen von C über Zeiger verwirrte, war das Einfache:
char ch;
char str[100];
scanf("%c %s", &ch, str);
Diese Verwirrung beruhte hauptsächlich darauf, dass die Verwendung eines Verweises auf eine Variable für OUT-Argumente eingeführt wurde, bevor mir Zeiger ordnungsgemäß eingeführt wurden. Ich erinnere mich, dass ich das Schreiben der ersten Beispiele in C for Dummies übersprungen habe, weil sie zu einfach waren, um nie das erste Programm, das ich geschrieben habe, zum Laufen zu bringen (höchstwahrscheinlich aus diesem Grund).
Was daran verwirrend war, war, was &cheigentlich bedeutete und warum stres nicht gebraucht wurde.
Nachdem ich mich damit vertraut gemacht hatte, erinnere ich mich, dass ich über die dynamische Zuordnung verwirrt war. Irgendwann wurde mir klar, dass Zeiger auf Daten ohne dynamische Zuweisung eines Typs nicht besonders nützlich waren, also schrieb ich etwas wie:
char * x = NULL;
if (y) {
char z[100];
x = z;
}
um zu versuchen, dynamisch etwas Speicherplatz zuzuweisen. Es hat nicht funktioniert. Ich war mir nicht sicher, ob es funktionieren würde, aber ich wusste nicht, wie es sonst funktionieren könnte.
Ich habe später von mallocund erfahren new, aber sie schienen mir wirklich magische Gedächtnisgeneratoren zu sein. Ich wusste nichts darüber, wie sie funktionieren könnten.
Einige Zeit später wurde mir wieder Rekursion beigebracht (ich hatte es vorher selbst gelernt, war aber jetzt im Unterricht) und ich fragte, wie es unter der Haube funktioniert - wo wurden die einzelnen Variablen gespeichert? Mein Professor sagte "auf dem Stapel" und viele Dinge wurden mir klar. Ich hatte den Begriff schon einmal gehört und zuvor Software-Stacks implementiert. Ich hatte schon lange zuvor gehört, dass andere sich auf "den Stapel" bezogen, aber ich hatte es vergessen.
Um diese Zeit wurde mir auch klar, dass die Verwendung mehrdimensionaler Arrays in C sehr verwirrend sein kann. Ich wusste, wie sie funktionierten, aber sie waren so leicht zu verwickeln, dass ich mich entschied, sie zu umgehen, wann immer ich konnte. Ich denke, dass das Problem hier hauptsächlich syntaktisch war (insbesondere das Übergeben oder Zurückgeben von Funktionen).
Seit ich in den nächsten ein oder zwei Jahren C ++ für die Schule geschrieben habe, habe ich viel Erfahrung mit Zeigern für Datenstrukturen. Hier hatte ich eine Reihe neuer Probleme - das Verwechseln von Zeigern. Ich hätte mehrere Ebenen von Zeigern (Dinge wie node ***ptr;), die mich stolpern lassen. Ich würde einen Zeiger falsch oft dereferenzieren und schließlich herausfinden, wie viele* ich durch Ausprobieren brauchte.
Irgendwann habe ich gelernt, wie der Haufen eines Programms funktioniert (irgendwie, aber gut genug, dass es mich nachts nicht mehr wach hält). Ich erinnere mich, dass ich gelesen habe, wenn Sie ein paar Bytes vor dem Zeiger suchen, der mallocauf einem bestimmten System zurückgegeben wird, können Sie sehen, wie viele Daten tatsächlich zugewiesen wurden. Ich erkannte, dass der Code in mallocmehr Speicher vom Betriebssystem verlangen konnte und dieser Speicher nicht Teil meiner ausführbaren Dateien war. Eine anständige Vorstellung davon zu haben, wie es mallocfunktioniert, ist wirklich nützlich.
Bald danach nahm ich an einer Montageklasse teil, die mir nicht so viel über Zeiger beibrachte, wie die meisten Programmierer wahrscheinlich denken. Es brachte mich dazu, mehr darüber nachzudenken, in welche Assembly mein Code übersetzt werden könnte. Ich hatte immer versucht, effizienten Code zu schreiben, aber jetzt hatte ich eine bessere Idee, wie es geht.
Ich nahm auch an einigen Kursen teil, in denen ich etwas Lispeln schreiben musste . Beim Schreiben von lisp ging es mir nicht so sehr um Effizienz wie in C. Ich hatte sehr wenig Ahnung, in was dieser Code übersetzt werden könnte, wenn er kompiliert wird, aber ich wusste, dass es so aussah, als würde man viele lokal benannte Symbole (Variablen) verwenden Dinge viel einfacher. Irgendwann schrieb ich einen AVL-Baumrotationscode in ein wenig Lisp, was mir aufgrund von Zeigerproblemen sehr schwer fiel, in C ++ zu schreiben. Ich erkannte, dass meine Abneigung gegen das, was ich für überschüssige lokale Variablen hielt, meine Fähigkeit, dieses und mehrere andere Programme in C ++ zu schreiben, behindert hatte.
Ich habe auch einen Compiler-Kurs besucht. Während in dieser Klasse blätterte ich in das vorgeschobenen Material voraus und darüber gelernt , statische Einzelbelegung (SSA) und toten Variablen, die außer nicht so wichtig ist , dass es hat mich gelehrt , dass jeder anständiger Compiler einen anständigen Job für den Umgang mit Variablen tun , die sind nicht mehr verwendet. Ich wusste bereits, dass mehr Variablen (einschließlich Zeiger) mit korrekten Typen und guten Namen mir helfen würden, die Dinge im Kopf zu behalten, aber jetzt wusste ich auch, dass es noch dümmer war, sie aus Effizienzgründen zu vermeiden, als meine weniger auf Mikrooptimierung ausgerichteten Professoren sagten mich.
Für mich hat es sehr geholfen, ein gutes Stück über das Speicherlayout eines Programms zu wissen. Das Überlegen, was mein Code symbolisch und auf der Hardware bedeutet, hilft mir dabei. Die Verwendung lokaler Zeiger mit dem richtigen Typ hilft sehr. Ich schreibe oft Code, der aussieht wie:
int foo(struct frog * f, int x, int y) {
struct leg * g = f->left_leg;
struct toe * t = g->big_toe;
process(t);
Wenn ich also einen Zeigertyp vermassle, wird durch den Compilerfehler sehr deutlich, wo das Problem liegt. Wenn ich es tat:
int foo(struct frog * f, int x, int y) {
process(f->left_leg->big_toe);
Wenn ein Zeigertyp falsch ist, ist der Compilerfehler viel schwieriger herauszufinden. Ich wäre versucht, in meiner Frustration auf Versuch und Irrtum-Änderungen zurückzugreifen und die Dinge wahrscheinlich noch schlimmer zu machen.