paulo1205
(usa Ubuntu)
Enviado em 19/08/2016 - 14:50h
A razão de ter argumentos de funções do tipo ponteiro é que o C sempre passa
cópias dos valores dos argumentos para a função.
Algumas vezes, no entanto, você gostaria que a função alterasse o valor do argumento. Imagine que você queira fazer uma função que troque o valor de duas variáveis inteiras, passadas a ela como parâmetros. Uma tentativa inicial de fazer a função poderia se parecer com isso.
// Tentativa 1 (não funciona)
void swap(int a, int b){
int temp=a;
a=b;
b=temp;
}
Se você testar essa função com o seguinte programa, vai ver que os valores não serão alterados.
int main(void){
int a='a', b='b';
printf("Antes de swap(a, b): a=%c, b=%c\n", a, b);
swap(a, b); /* <-- copia os valores de “a” e de “b”, em vez de referir-se aos originais. */
printf("Após swap(a, b): a=%c, b=%c\n", a, b);
return 0;
}
Por que não funcionou? Porque, como eu disse lá no início, o compilador C sempre coloca cópias dos valores dos argumentos como parâmetros das funções chamadas. Assim, no código acima, a função não recebeu as variáveis
a e
b , mas cópias dos valores dessas variáveis.
Uma forma de
contornar essa característica é fazer com que a função passe a receber os endereços de onde residem os dados, em lugar de receber os valores dos dados. Veja que interessante: se eu tiver várias cópias do mesmo endereço de memória, qualquer uma das cópias vai me levar à mesma posição de memória. O compilador vai continuar colocando para a função cópias dos argumentos, mas agora serão cópias dos endereços, então tais endereços vão permitir chegar aos mesmos dados originais.
// Segunda tentativa: agora vai funcionar.
void swap(int *p_a, int *p_b){
int temp=*p_a;
*p_a=*p_b;
*p_b=temp;
}
int main(void){
int a='a', b='b';
printf("Antes de swap(&a, &b): a=%c, b=%c\n", a, b);
swap(&a, &b); /* <-- copia os endereços de “a” e de “b”, cópias de endereços apontam para os mesmos lugares. */
printf("Após swap(&a, &b): a=%c, b=%c\n", a, b);
return 0;
}
Agora funciona. No entanto, como você pode ver, existe um preço a se pagar: você tem explicitamente de indicar onde quer fornecer endereços dos dados (com o operador
& , como usado na hora de chamar a função), e também dizer que quer fazer acesso aos dados apontados pelos endereços (através do operador
* , no corpo da função).
---
Nos exemplos acima, o tipo dos dados que você queria manipular era
int . Mas e se fosse outro tipo?
Imagine o seguinte dado:
double **Matriz;
E imagine duas funções que vão manipular essa matriz:
double **alocaMatriz(size_t n_linhas, size_t n_colunas);
void liberaMatriz(double **Matriz, size_t n_linhas);
Tipicamente, você usaria essas funções junto sobre a variável da seguinte maneira.
Matriz=alocaMatriz(N, M);
/ * ... Manipula os elementos de “Matriz” ... */
liberaMatriz(Matriz, N);
Acima eu mostrei a forma que normalmente se usa. Note que a alocação geralmente acontece dentro da função, e ela retorna para você um valor de ponteiro que você atribui a uma variável de um tipo compatível. Já a liberação passa
uma cópia do valor do ponteiro do dado alocado, e realiza a liberação da memória usando a cópia do endereço.
Como nós vimos acima, quando você tem cópias de endereços, todas as operações feitas com os dados apontados por esses endereços vão valer também para o valor original do endereço. O “problema” é o seguinte: a operação que foi feita foi justamente tornar inválido qualquer acesso futuro ao conteúdo do endereço, já que aquela região de memória está sendo devolvida ao sistema pela função de liberação; no entanto, como você enviou apenas uma cópia do endereço, você ainda dispõe da referência original, e poderia, POR ERRO DE PROGRAMAÇÃO (mas um erro que é relativamente fácil de cometer!!), acabar usando essa cópia para tentar chegar a um dado que não é mais válido.
Por conta disso, algumas pessoas (eu, inclusive) acham melhor que a função de liberação altere o valor da referência original, atribuindo-lhe um ponteiro nulo (
NULL ). Isso tem o benefício e permitir usando valor nulo como indicador de se a memória para o dado está alocada ou não
Para tanto, porém, a função de desalocação tem de ser alterada, pois a forma corriqueira dela trabalha com uma cópia da matriz, e não altera o valor original. Para que passe a trabalhar com endereço, você precisa de um nível a mais de ponteiro. Como o tipo da matriz já incorpora dois níveis de ponteiros, eis aí o motivo do terceiro nível.
void liberaMatriz(double ***p_Matriz, size_t n_linhas){
while(n_linhas--)
free((*p_Matriz)[n_linhas]);
free(*p_Matriz);
*p_Matriz=NULL;
}
int main(void){
double **Matriz=alocaMatriz(N, M);
/ * ... Manipula os elementos de “Matriz” ... */
liberaMatriz(&Matriz, N); /* <-- Note que eu pego o endereço da variável Matriz. */
}
---
Esse uso que eu defendo não é padronizado. A biblioteca padrão do C e outras bibliotecas locais do sistema operacional têm vários casos em que a alocação de recurso entrega um meio de acesso ao recurso através do valor de retorno da função de alocação, e a função de liberação correspondente trabalha com uma cópia do valor de acesso ao recurso, permitindo a quem a chamou manter o valor original, mesmo depois de o recurso já ter sido oficialmente liberado. É assim com
malloc ()/
free () (recurso alocado/liberado é a memória, e o manipulador do recurso é um ponteiro para região de memória alocada), com
fopen ()/
fclose () (alocação de
stream de acesso a arquivo, e o manipulador do recurso é um ponteiro para uma representação interna de controle de acesso ao arquivo), com
open ()/
close () (acesso direto a arquivos no mundo Unix, e o manipulador do recurso é um valor inteiro, que funciona com índice de uma tabela de arquivos abertos pelo processo),
CreateFile ()/
CloseHandle () (acesso direto a arquivos no mundo Windows, e o manipulador do recurso é um dado de tipo interno, não revelado para o usuário), e muitas outras coisas.
A razão para não existir a etapa de limpeza da variável usada para acesso ao recurso é desempenho. Limpar o recurso seria uma etapa a mais a ser executada pela biblioteca. E se o programador não cometer nenhum erro de programação (especificamente o erro de usar um recurso que ele mesmo disse que podia ser liberado), tal etapa será redundante de todo modo. Se eu ou você quisermos usar uma valor inválido de recurso para sinalizar que o recurso não está disponível, nós que implementemos essa sinalização como etapa adicional nos nossos programas, sem impor esse custo adicional a outros programadores.
Faz sentido.