Dovrei iniziare dicendo che C e C ++ sono stati i primi linguaggi di programmazione che ho imparato. Ho iniziato con C, poi ho fatto C ++ a scuola, e poi sono tornato a C per diventare fluente in esso.
La prima cosa che mi ha confuso riguardo ai puntatori durante l'apprendimento del C è stata la semplice:
char ch;
char str[100];
scanf("%c %s", &ch, str);
Questa confusione era principalmente radicata nell'essere stata introdotta all'utilizzo del riferimento a una variabile per gli argomenti OUT prima che i puntatori mi fossero introdotti correttamente. Ricordo di aver saltato la scrittura dei primi esempi in C for Dummies perché erano troppo semplici solo per non far funzionare il primo programma che avevo scritto (molto probabilmente per questo).
Ciò che confondeva al riguardo era ciò che &ch
realmente significava e perchéstr
non ne aveva bisogno.
Dopo aver acquisito familiarità, ricordo di essere stato confuso riguardo all'allocazione dinamica. A un certo punto mi sono reso conto che avere puntatori ai dati non era estremamente utile senza un'allocazione dinamica di qualche tipo, quindi ho scritto qualcosa del tipo:
char * x = NULL;
if (y) {
char z[100];
x = z;
}
per tentare di allocare dinamicamente un po 'di spazio. Non ha funzionato Non ero sicuro che avrebbe funzionato, ma non sapevo come altrimenti avrebbe funzionato.
Più tardi ho imparato a conoscere malloc
enew
, ma mi sembravano davvero dei generatori di memoria magici. Non sapevo nulla di come avrebbero potuto funzionare.
Qualche tempo dopo mi è stato insegnato di nuovo la ricorsione (l'avevo imparato da solo, ma ora ero in classe) e ho chiesto come funzionava sotto il cofano - dove erano memorizzate le variabili separate. Il mio professore ha detto "in pila" e molte cose mi sono diventate chiare. Avevo già sentito il termine in precedenza e prima avevo implementato stack di software. Avevo sentito altri fare riferimento alla "pila" molto tempo prima, ma me ne ero dimenticato.
In questo periodo ho anche capito che l'uso di array multidimensionali in C può diventare molto confuso. Sapevo come funzionavano, ma erano così facili da aggrovigliare, che decisi di provare a aggirare usandoli ogni volta che potevo. Penso che il problema qui sia stato principalmente sintattico (soprattutto passando o restituendoli da funzioni).
Da quando ho scritto C ++ per la scuola per il prossimo anno o due ho avuto molta esperienza nell'uso di puntatori per strutture di dati. Qui ho avuto una nuova serie di problemi: confondere i puntatori. Avrei più livelli di puntatori (cose del genere node ***ptr;
) che mi fanno inciampare. Dereggerei un puntatore il numero sbagliato di volte e alla fine ricorrerei per capire quanti ne *
avevo bisogno per tentativi ed errori.
Ad un certo punto ho imparato come funzionava l'heap di un programma (in qualche modo, ma abbastanza buono da non farmi più stare sveglio la notte). Ricordo di aver letto che se guardi qualche byte prima del puntatore che malloc
su un certo sistema ritorna, puoi vedere quanti dati sono stati effettivamente allocati. Mi sono reso conto che il codice in malloc
potrebbe richiedere più memoria dal sistema operativo e questa memoria non faceva parte dei miei file eseguibili. Avere un'idea di lavoro decente su come malloc
funziona è davvero utile.
Poco dopo ho seguito un corso di assemblaggio, che non mi ha insegnato tanto sui puntatori come probabilmente pensano i programmatori. Mi ha fatto pensare di più a quale assembly il mio codice potrebbe essere tradotto. Avevo sempre cercato di scrivere un codice efficiente, ma ora avevo un'idea migliore su come farlo.
Ho anche preso un paio di lezioni in cui ho dovuto scrivere un po 'di musica . Quando scrivevo lisp non mi preoccupavo tanto dell'efficienza come in C. Avevo ben poca idea in cosa potesse essere tradotto questo codice se compilato, ma sapevo che sembrava usare molti simboli locali (variabili) creati le cose sono molto più facili. Ad un certo punto ho scritto un po 'di codice di rotazione dell'albero AVL in un po' di lisp, che mi è stato molto difficile scrivere in C ++ a causa di problemi con i puntatori. Mi sono reso conto che la mia avversione a ciò che pensavo fosse un eccesso di variabili locali aveva ostacolato la mia capacità di scrivere questo e molti altri programmi in C ++.
Ho anche preso una classe di compilatori. Mentre in questa classe sono passato al materiale avanzato e ho appreso l' assegnazione singola statica (SSA) e le variabili morte, il che non è così importante tranne che mi ha insegnato che qualsiasi compilatore decente farà un lavoro decente nel trattare con le variabili che sono non più utilizzato. Sapevo già che più variabili (compresi i puntatori) con tipi corretti e buoni nomi mi avrebbero aiutato a mantenere le cose dritte nella mia testa, ma ora sapevo anche che evitarle per motivi di efficienza era ancora più stupido di quanto dicessero i miei professori meno micro-ottimizzati me.
Quindi, per me, sapere molto del layout di memoria di un programma mi ha aiutato molto. Pensare al significato del mio codice, sia simbolicamente che sull'hardware, mi aiuta. L'uso di puntatori locali che hanno il tipo corretto aiuta molto. Scrivo spesso un codice simile a:
int foo(struct frog * f, int x, int y) {
struct leg * g = f->left_leg;
struct toe * t = g->big_toe;
process(t);
così che se sbaglio un tipo di puntatore è molto chiaro dall'errore del compilatore quale sia il problema. Se avessi fatto:
int foo(struct frog * f, int x, int y) {
process(f->left_leg->big_toe);
e ho sbagliato qualsiasi tipo di puntatore, l'errore del compilatore sarebbe molto più difficile da capire. Sarei tentato di ricorrere a cambiamenti di prova ed errore nella mia frustrazione e probabilmente peggiorare le cose.