Eu deveria começar dizendo que C e C ++ foram as primeiras linguagens de programação que aprendi. Comecei com C, depois fiz C ++ na escola, muito, e depois voltei para C para me tornar fluente.
A primeira coisa que me confundiu sobre ponteiros ao aprender C foi a simples:
char ch;
char str[100];
scanf("%c %s", &ch, str);
Essa confusão estava principalmente enraizada em ter sido introduzida no uso de referência a uma variável para argumentos OUT antes que os ponteiros fossem corretamente introduzidos para mim. Lembro-me de que deixei de escrever os primeiros exemplos em C para Dummies, porque eram muito simples para nunca conseguir que o primeiro programa que escrevi funcionasse (provavelmente por causa disso).
O que era confuso sobre isso era o que &ch
realmente significava e também porque str
não precisava.
Depois que me familiarizei com isso, lembro-me de estar confuso sobre a alocação dinâmica. Eu percebi em algum momento que ter ponteiros para dados não era extremamente útil sem a alocação dinâmica de algum tipo, então escrevi algo como:
char * x = NULL;
if (y) {
char z[100];
x = z;
}
para tentar alocar dinamicamente algum espaço. Não deu certo. Eu não tinha certeza de que iria funcionar, mas não sabia de que outra forma poderia funcionar.
Mais tarde eu aprendi sobre malloc
e new
, mas eles realmente pareciam geradores de memória mágicos para mim. Eu não sabia nada sobre como eles poderiam funcionar.
Algum tempo depois, eu estava aprendendo a recursão novamente (eu já havia aprendido antes, mas agora estava na aula) e perguntei como funcionava sob o capô - onde estavam armazenadas as variáveis separadas. Meu professor disse "na pilha" e muitas coisas ficaram claras para mim. Eu já tinha ouvido o termo antes e implementado pilhas de software antes. Eu já tinha ouvido outros se referirem à "pilha" muito antes, mas havia esquecido.
Nessa época, também percebi que o uso de matrizes multidimensionais em C pode ficar muito confuso. Eu sabia como eles funcionavam, mas eles eram tão fáceis de se envolver, que eu decidi tentar contorná-los sempre que pudesse. Eu acho que o problema aqui foi principalmente sintático (especialmente passando ou devolvendo-os de funções).
Desde que eu escrevi C ++ para a escola nos próximos dois anos, adquiri muita experiência usando ponteiros para estruturas de dados. Aqui eu tive um novo conjunto de problemas - misturando indicadores. Eu teria vários níveis de indicadores (coisas como node ***ptr;
) me enganando. Desdiferenciaria um ponteiro o número errado de vezes e, eventualmente, recorria a descobrir quantas *
eu precisava por tentativa e erro.
Em algum momento, aprendi como o heap de um programa funcionava (mais ou menos, mas bom o suficiente para não me manter acordado à noite). Lembro-me de ler que, se você olhar alguns bytes antes do ponteiro que malloc
em um determinado sistema retornar, poderá ver quantos dados foram realmente alocados. Percebi que o código malloc
poderia pedir mais memória do sistema operacional e essa memória não fazia parte dos meus arquivos executáveis. Ter uma idéia decente de como malloc
funciona é realmente útil.
Logo depois disso, participei de uma aula de montagem, que não me ensinou tanto sobre ponteiros como a maioria dos programadores provavelmente pensa. Isso me fez pensar mais sobre em qual assembly meu código poderia ser traduzido. Eu sempre tentei escrever código eficiente, mas agora tinha uma idéia melhor de como fazê-lo.
Também participei de algumas aulas nas quais tive que escrever um cocô . Ao escrever lisp, eu não estava tão preocupado com a eficiência quanto em C. Eu tinha muito pouca idéia de como esse código poderia ser traduzido se compilado, mas eu sabia que parecia usar muitos símbolos nomeados locais (variáveis) criados coisas muito mais fáceis. Em algum momento, eu escrevi um pouco de código de rotação de árvore AVL em um pouco de lisp, que tive muita dificuldade em escrever em C ++ por causa de problemas com ponteiros. Percebi que minha aversão ao que eu pensava serem variáveis locais em excesso havia prejudicado minha capacidade de escrever esse e vários outros programas em C ++.
Também participei de uma aula de compiladores. Enquanto nesta aula, avancei para o material avançado e aprendi sobre atribuição única estática (SSA) e variáveis mortas, o que não é tão importante, exceto que me ensinou que qualquer compilador decente fará um trabalho decente ao lidar com variáveis que são não mais usado. Eu já sabia que mais variáveis (incluindo ponteiros) com tipos corretos e bons nomes me ajudariam a manter as coisas em mente, mas agora também sabia que evitá-las por razões de eficiência era ainda mais estúpido do que meus professores menos otimistas disseram mim.
Então, para mim, saber um pouco sobre o layout da memória de um programa ajudou muito. Pensar no que meu código significa, simbolicamente e no hardware, me ajuda. O uso de ponteiros locais com o tipo correto ajuda bastante. Costumo escrever um código que se parece com:
int foo(struct frog * f, int x, int y) {
struct leg * g = f->left_leg;
struct toe * t = g->big_toe;
process(t);
de modo que, se eu estragar um tipo de ponteiro, é muito claro pelo erro do compilador qual é o problema. Se eu fiz:
int foo(struct frog * f, int x, int y) {
process(f->left_leg->big_toe);
e com algum tipo de ponteiro errado, o erro do compilador seria muito mais difícil de descobrir. Eu ficaria tentado a recorrer a mudanças de tentativa e erro na minha frustração e provavelmente pioraria as coisas.