Powinienem zacząć od stwierdzenia, że C i C ++ były pierwszymi językami programowania, których się nauczyłem. Zacząłem od C, potem dużo C ++ w szkole, a potem wróciłem do C, aby nabrać w nim biegłości.
Pierwszą rzeczą, która zdezorientowała mnie we wskaźnikach podczas nauki C, była prosta:
char ch;
char str[100];
scanf("%c %s", &ch, str);
To zamieszanie wynikało głównie z tego, że wprowadzono mnie w używanie odwołań do zmiennej w argumentach OUT, zanim wskaźniki zostały mi prawidłowo wprowadzone. Pamiętam, że pominąłem napisanie pierwszych kilku przykładów w C for Dummies, ponieważ były one zbyt proste tylko, aby nigdy nie uzyskać do działania pierwszego programu, który napisałem (najprawdopodobniej z tego powodu).
Mylące w tym było to, co &ch
tak naprawdę oznaczało i dlaczego tego str
nie potrzebowaliśmy.
Kiedy się z tym zapoznałem, później pamiętam, jak pomyliłem się co do alokacji dynamicznej. W pewnym momencie zdałem sobie sprawę, że posiadanie wskaźników do danych nie jest zbyt przydatne bez pewnego typu dynamicznej alokacji, więc napisałem coś takiego:
char * x = NULL;
if (y) {
char z[100];
x = z;
}
spróbować dynamicznie przydzielić trochę miejsca. To nie zadziałało. Nie byłem pewien, czy to zadziała, ale nie wiedziałem, jak inaczej mogłoby działać.
Później dowiedziałem się o malloc
i new
, ale naprawdę wydawały mi się magicznymi generatorami pamięci. Nie wiedziałem nic o tym, jak mogą działać.
Jakiś czas później ponownie uczono mnie rekurencji (wcześniej nauczyłem się jej samodzielnie, ale teraz byłem na zajęciach) i zapytałem, jak to działa pod maską - gdzie są przechowywane oddzielne zmienne. Mój profesor powiedział „na stosie” i wiele rzeczy stało się dla mnie jasnych. Słyszałem ten termin już wcześniej i wcześniej wdrażałem stosy oprogramowania. Słyszałem, jak inni mówili o „stosie” już dawno, ale zapomniałem o tym.
W tym czasie zdałem sobie również sprawę, że używanie tablic wielowymiarowych w C może być bardzo zagmatwane. Wiedziałem, jak działały, ale tak łatwo je zaplątać, że postanowiłem spróbować obejść ich użycie, kiedy tylko będę mógł. Myślę, że tutaj problem dotyczył głównie składni (zwłaszcza przekazywania lub zwracania ich z funkcji).
Odkąd pisałem C ++ dla szkoły przez następny rok lub dwa, zdobyłem duże doświadczenie w używaniu wskaźników do struktur danych. Tutaj miałem nowy zestaw kłopotów - pomieszanie wskaźników. Miałbym wiele poziomów wskaźników (takich jak node ***ptr;
) potykanie mnie. Wyłuskałem wskaźnik niewłaściwą liczbę razy i ostatecznie uciekłem się do ustalenia, ile *
potrzebuję, metodą prób i błędów.
W pewnym momencie dowiedziałem się, jak działa sterta programu (w pewnym sensie, ale na tyle dobrze, że nie utrzymuje mnie już w nocy). Pamiętam, że czytałem, że jeśli spojrzysz na kilka bajtów przed wskaźnikiem, który malloc
w pewnym systemie zwraca, możesz zobaczyć, ile danych zostało faktycznie przydzielonych. Zdałem sobie sprawę, że kod w programie malloc
może poprosić o więcej pamięci z systemu operacyjnego i ta pamięć nie była częścią moich plików wykonywalnych. Posiadanie przyzwoitego pomysłu na to, jak malloc
działa, jest naprawdę przydatne.
Niedługo potem wziąłem udział w zajęciach z asemblera, które nie nauczyły mnie tyle wskazówek, jak prawdopodobnie większość programistów myśli. Zmusiło mnie to do zastanowienia się, na jaki asembler może zostać przetłumaczony mój kod. Zawsze starałem się pisać wydajny kod, ale teraz miałem lepszy pomysł, jak to zrobić.
Wziąłem też kilka zajęć, na których musiałem napisać trochę seplenienia . Pisząc lisp, nie przejmowałem się tak wydajnością, jak byłem w C.Miałem bardzo mało pojęcia, na co ten kod może zostać przetłumaczony po skompilowaniu, ale wiedziałem, że wydawało się, że używa się wielu lokalnych nazwanych symboli (zmiennych) utworzonych o wiele łatwiejsze. W pewnym momencie napisałem trochę kodu rotacji drzewa AVL w odrobinie seplenienia, że miałem bardzo ciężko pisać w C ++ z powodu problemów ze wskaźnikami. Zdałem sobie sprawę, że moja niechęć do tego, co uważałem za nadmiar zmiennych lokalnych, utrudniła mi napisanie tego i kilku innych programów w C ++.
Wziąłem również zajęcia z kompilatorów. Podczas tych zajęć przerzuciłem się do zaawansowanego materiału i nauczyłem się statycznego przypisania pojedynczego (SSA) i martwych zmiennych, co nie jest takie ważne, z wyjątkiem tego, że nauczył mnie, że każdy przyzwoity kompilator poradzi sobie dobrze ze zmiennymi, które są nieużywany. Wiedziałem już, że więcej zmiennych (w tym wskaźników) z poprawnymi typami i dobrymi nazwami pomoże mi zachować porządek w głowie, ale teraz wiedziałem również, że unikanie ich ze względów wydajnościowych jest jeszcze głupsze niż mówili moi mniej nastawieni na mikro-optymalizację profesorowie mnie.
Więc dla mnie bardzo pomogła wiedza o układzie pamięci programu. Pomaga mi myślenie o tym, co oznacza mój kod, zarówno symbolicznie, jak i na sprzęcie. Używanie lokalnych wskaźników, które mają właściwy typ, bardzo pomaga. Często piszę kod, który wygląda następująco:
int foo(struct frog * f, int x, int y) {
struct leg * g = f->left_leg;
struct toe * t = g->big_toe;
process(t);
więc jeśli schrzanię typ wskaźnika, błąd kompilatora będzie bardzo jasny, na czym polega problem. Gdybym to zrobił:
int foo(struct frog * f, int x, int y) {
process(f->left_leg->big_toe);
i dostanie tam nieprawidłowy typ wskaźnika, błąd kompilatora byłby znacznie trudniejszy do ustalenia. Kusiłoby mnie, aby uciekać się do prób i błędów w mojej frustracji i prawdopodobnie pogorszyć sytuację.