Introdução aos ponteiros
Nos exercícios e exemplos mais simples envolvendo estruturas de seleção, laços de repetição, vetores, matrizes e funções. Um ponteiro não era necessário para a resolução do problema. Em alguns momentos um ponteiro apareceu como no caso da função scanf(), mas os detalhes do seu funcionamento estavam omitidos para fins de simplificação. É possível uma função ter mais de um resultado e devolvê-los? Mais de um resultado sim, mas devolver mais de um resultado não. Para isso entram em cena os ponteiros.
Ponteiros são um conceito bastante abstrato. A recomendação usual é que se resolvam muitos exercícios que dependam dos ponteiros, caso contrário fica muito difícil de entender o que são e como usar ponteiros. Mentalmente nós guardamos, recuperamos e alteramos memórias sem executar nenhuma operação mental que diga onde uma memória está (pelo menos não conscientemente). No computador isso é exposto na forma de ponteiros. De outra forma como um computador funcionaria?
Não é possível fazer uma analogia entre função do tipo 'void' e a matemática, pois não há função na matemática que não calcule um valor final. Funções 'void' são melhor entendidas como "receitas", operações para fazer algo que não é representado por um valor final ou um resultado só. Por exemplo: ordenar números em ordem crescente. Para diferentes entradas há diferentes saídas, apenas a "receita" de como organizar em ordem crescente é o que há de fixo na função.
Dependendo do caso, ponteiros são vistos muito rapidamente numa única aula.
A lógica básica é: ponteiros são um tipo de variável que guarda endereços de memória. São uma variável que "intermedia" certas operações. Ponteiros estão intimamente ligados a funções.
Erros de lógica:
- Ponteiros que apontam para endereços desconhecidos ou errados;
- Uma boa parte dos erros de sintaxe não permite a compilação do programa.
- Função que troca os valores de duas variáveis:
/* recebe a e b e troca o valor de ambas */ void troca (int a, int b) { int aux; aux = a; a = b; b = aux; }
Trocar valor a função trocou, mas ela trocou o valor das variáveis parâmetro. Percebeu também é que impossível a função devolver dois valores, os valores de 'a' e de 'b'? Este exemplo é bem prático para entender um conceito muito importante: a função não recebe variáveis, assim como não devolve variáveis. Ela recebe ou devolve apenas valores.
Analogia: você tem 2 garrafas, uma com vinho branco a outra com vinho tinto. Você quer trocar os vinhos de garrafa. Quando você chama a função, o que ela faz é receber o vinho, não a garrafa. Meio estranho mas...
A mesma função que troca valor de duas variáveis, mas agora com ponteiros:
void troca_var(int *pa, int *pb) { int temp; temp = *pa; *pa = *pb; *pb = temp; } /* a chamada da função é feita assim */ troca_var(&a, &b);
Os parâmetros da função são agora ponteiros e ponteiros apenas podem receber endereços de memória. Quando a função é chamada a operação de troca ocorre nas posições de memória onde estes valores estão. Daí que o algoritmo anterior, com variáveis, não trocava os valores como esperado. Isso explica por que a função é do tipo 'void' e não devolve nada. A função não tem o que devolver. Ela não recebeu valores. Não recebeu variáveis. Não tem nada para devolver então.
Uma analogia: imagine uma adega com muitas garrafas, cada garrafa é única e armazena uma coisa só por vez. No exemplo sem ponteiros eram dois vinhos e duas garrafas, A e B. Quando a função recebeu os vinhos. Ela trocou, mas com duas garrafas diferentes, C e D. Nada aconteceu com A e B. Com ponteiros o que a função recebe não é o vinho, mas a localização da garrafa na adega. Quando a operação de troca for feita, ela será feita com as localizações das duas garrafas. Ou seja, o lugar onde A e B estão no meio da adega. Com o uso dos ponteiros não há garrafas C e D na história. Por enquanto não interessa como as garrafas estão organizadas na adega e nem o lugar exato de cada uma lá.
- Função que recebe dois números e testa se é par ou ímpar:
/* recebe dois valores, x e y, e dois ponteiros, xx e yy xx "envia" o valor 1 para um endereço de memória se x é par, 0 se for ímpar yy "envia" o valor 1 para um endereço de memória se y é par, 0 se for ímpar */ void testar (int x, int y, int *xx, int *yy) { if (x % 2 == 0) *xx = 1; else *xx = 0; if (y % 2 == 0) *yy = 1; else *yy = 0; } /* a chamada da função é feita assim */ testar(var1, var1, &a, &b);
Exemplo bastante simples que ilustra a necessidade dos ponteiros. Cuidado! O valor 1 ou 0 não foi atribuído ao ponteiro, mas sim ao lugar da memória apontado por ele. Um ponteiro guarda endereços, não valores (endereço de memória também é valor, mas continuemos usando o termo endereço para não confundir). Agora pense no seguinte problema: e se forem 500 números? Uma função receber 500 valores e 500 ponteiros é impraticável, aí entra a combinação de ponteiros e vetores.
- Calcular a área de um triângulo onde os vértices são o ponto máximo / mínimo de uma parábola e os pontos de intersecção da parábola com o eixo x
/* recebe os coeficientes a, b e c e e os endereços de memória das variáveis que guardam os valores da raiz1, raiz2 e vértice. Os resultados são atribuídos nos endereços de memória das respectivas variáveis */ void parabola (double a, double b, double c, double *raiz1, double *raiz2, double *vertice) { *raiz1 = (-b + sqrt(b*b - 4 * a * c)) / (2 * a); *raiz2 = (-b - sqrt(b*b - 4 * a * c)) / (2 * a); *vertice = (-(b*b - 4 * a * c)) / (4 * a); } /* recebe os endereços de memória das variáveis que guardam os valores da raiz1, raiz2 e vértice para calcular a área do triângulo */ double area_tri (double *r1, double *r2, double *vert) { return (fabs(*r2 - *r1) * fabs(*vert)) / 2; } /* a chamada das funções é feita assim */ parabola(a, b, c, &r1, &r2, &vert); area_tri(&r1, &r2, &vert);
Sem ponteiros o programa teria quatro chamadas de função, uma para cada raiz, uma para o vértice e a última para calcular a área. A vantagem dos ponteiros foi fundir três funções em uma só.
Obs.: para usar a função fabs() (módulo de um número), não esqueça de incluir a biblioteca math.h no seu programa!
Veja agora como usar ponteiros de função:
double raiz1 (double a, double b, double c) { return (-b + sqrt(b*b - 4 * a * c)) / (2 * a); } double raiz2 (double a, double b, double c) { return (-b - sqrt(b*b - 4 * a * c)) / (2 * a); } double vertice (double a, double b, double c) { return (-(b*b - 4 * a * c)) / (4 * a); } /* recebe os endereços de memória das três funções e tambem os valores dos coeficientes a, b e c */ double area_tri (double (*raiz1)(double, double, double), double (*raiz2)(double, double, double), double (*vertice)(double, double, double), double a, double b, double c) { return (fabs((*raiz2)(a, b, c) - (*raiz1)(a, b, c)) * fabs((*vertice)(a, b, c))) / 2; }
Fundamentalmente ponteiros de função são iguais a ponteiros de variáveis, apenas a sintaxe pode ser difícil de entender num primeiro momento. Agora é apenas uma chamada para calcular a área. As funções que calculam as raízes e o vértice não são mais chamadas diretamente pela função principal, mas indiretamente pela própria função que calcula a área. Porém, em troca, a chamada da função 'area_tri' terá que ser chamada com os coeficientes a, b e c e também com o endereço da memória de cada uma das funções, num total de seis argumentos.
Neste tipo de situação é bom resolver os problemas um por um. Primeiro uma função, depois outra. Assim fica mais fácil acompanhar os ponteiros e não se confundir com variáveis e endereços de variáveis.
Veja o último exemplo combinando tanto ponteiros de variáveis quanto ponteiros de funções:
void parabola (double a, double b, double c, double *raiz1, double *raiz2, double *vertice) { *raiz1 = (-b + sqrt(b*b - 4 * a * c)) / (2 * a); *raiz2 = (-b - sqrt(b*b - 4 * a * c)) / (2 * a); *vertice = (-(b*b - 4 * a * c)) / (4 * a); } /* recebe um endereço de memória da função, os coeficientes a, b e c e os endereços de memória das variáveis que guardam os valores calculados para raiz1, raiz2 e o vértice da parábola */ double area_tri (void (*p)(double, double, double, double *, double *, double *), double a, double b, double c, double *r1, double *r2, double *vert) { /* esta chamada precisa ser feita, do contrário, a sintaxe estará correta, mas os valores das raízes e do vértice estarão indeterminados para o cálculo da área do triângulo */ (*p)(a, b, c, r1, r2, vert); return (fabs(*r2 - *r1) * fabs(*vert)) / 2; } /* a chamada da função é feita assim */ area_tri(parabola, a, b, c, &r1, &r2, &vert);
Serve apenas para ilustrar que há sempre uma troca. Por um lado há um ganho. Por outro uma complexidade maior para acompanhar o código. Veja que no caso anterior a função 'area_tri' tinha dentro de si três chamadas de função para calcular as raízes e o vértice. Agora é apenas uma. A necessidade de 'area_tri' receber os valores dos coeficientes a, b e c continua inalterada, pois é 'area_tri' quem chama a função 'parábola' e 'parábola' precisa saber os coeficientes para calcular alguma coisa.