
O que é um ponteiro em C?
Na memória do computador cada posição é referenciada por um endereço, atribuído de forma sequencial e, portanto, as posições adjacentes têm endereços consecutivos. Um apontador é uma variável que contém um endereço de outra variável.
Exemplo:
int x = 10; int *px = &x;
Neste caso temos uma variável inteira x e um ponteiro px. O valor do ponteiro px é o endereço na memória da variável x, obtido com o operador &, i.e., o endereço de x é dado por &x. Notar que o tipo do ponteiro px é int * pois é um ponteiro (*) para uma variável inteira (int).
O que é um ponteiro em C? Um ponteiro em C é um endereço de memória!
Declaração de ponteiros
Na declaração de ponteiros temos de indicar ao compilador para que tipo de variável estamos a apontar. Esta informação é indispensável em várias situações, como por exemplo nas operações aritméticas sobre ponteiros como vamos ver mais tarde.
A declaração de um ponteiro para o tipo <tipo> faz-se da seguinte forma:
<tipo> *<variável>;
Notar que o <tipo> pode ser qualquer, em particular pode ser já o tipo de um ponteiro.
Exemplos:
char *cptr; /* ponteiro para caracter */ int *iptr; /* ponteiro para inteiro */ double *dptr; /* ponteiro para double */ /* apenas b e' um ponteiro*/ int a, *b; /* c e d sao ponteiros para float */ float *c, *d;
Operador &
O endereço de uma variável é obtido através do operador &.
Exemplo:
int a = 43; /* um inteiro inicializado a 43 */ int *iptr; /* ponteiro para inteiro */ iptr = &a; /* iptr passa a guardar o endereco de a */ /* podemos tambem inicializar iptr na mesma linha*/ int *iptr = &a; /* declara um ponteiro para inteiro e passa a guardar o endereco de a */
Operador *
O operador * permite aceder ao conteúdo de uma posição memória para onde um ponteiro “aponta”.
Exemplo:
int b; int a = 43; /* um inteiro inicializado a 43 */ int *iptr; /* ponteiro para inteiro */ iptr = &a; /* iptr passa a guardar o endereço de a */ b = *iptr; /* b passa a guardar o valor 43*/ */
Exemplo com ambos os operadores & e *:
#include <stdio.h>
int main() {
/* declara dois inteiros, um com o valor 1 */
int y, x = 1;
/* px e' ponteiro para int, mas nao guarda qualquer endereco */
int *px;
/* px fica a guardar o endereço de x */
px = &x;
/* y toma o valor guardado no endereco de memoria apontado por px*/
y = *px;
/* o conteudo da posicao de memoria para onde px aponta, a variavel x, fica a 0 */
*px = 0;
/* output: 0 1 */
printf("%d %d\n", x, y);
return 0;
}Utilização do *
Declaração do ponteiro:
int *x;
O x é um ponteiro para int.
Conteúdo da posição de memória apontada pelo ponteiro:
*x = 4;
O valor 4 é atribuído ao conteúdo da posição de memória apontada por x.
Utilização de ponteiros
O valor de retorno de uma função pode ser um ponteiro:
int *xpto();
O argumento de uma função pode ser um ponteiro:
int abcd(char *a, int *b);
A prioridade de & e * é superior à dos operadores aritméticos.
y = *px + 1funciona como esperado;++*pxincrementa o valor dex, em quexé a variável apontada porpx;(*px)++(os parênteses são necessários).
Como discutido em aulas anteriores, em C os parâmetros são passados por valor. Por exemplo, no seguinte caso, o comportamento não é o esperado, a troca de valores apenas ocorre no contexto da função, não se manifestando fora da mesma.
void swap(int a, int b) {
int aux;
aux = a;
a = b;
b = aux;
}Podemos no entanto passar os parâmetros por referência utilizando ponteiros:
void swap(int *a, int *b) {
int aux;
aux = *a;
*a = *b;
*b = aux;
}Neste caso, esta função deverá ser chamada da seguinte forma:
int x = 5, y = 6; swap(&x, &y); /* Os valores de x e y estao agora trocados. */
Ponteiro nulo / Endereço zero
O ponteiro nulo representa ou referencia o endereço 0.
int *prt = NULL;
O NULL está definido em stdlib.h, pelo que é necessário incluir este ficheiro se quisermos utilizar o NULL. Na realidade NULL é em geral definido como 0 e, portanto, temos NULL == 0.
Ponteiros e tabelas
Em C existe uma relação estreita entre ponteiros e tabelas. Exemplo:
#include <stdio.h>
int main() {
int a[6] = {1, 2, 3, 4, 5, 6};
int *pa = a;
printf("%d %d %d\n", a[2], *(a+2), *(pa+2));
return 0;
}O a é em particular um ponteiro para a primeira posição do vector. Notar que os ponteiros têm uma aritmética própria, quando fazemos pa+2 estamos a avançar na realidade 2*sozeof(int bytes, ou seja, pa+2 aponta para uma posição de memória 2*sozeof(int bytes depois da posição apontada por pa.
Podemos efectuar quer a operação + quer a operação - sobre ponteiros.
Ainda que exista uma relação estreita entre ponteiros e tabelas, devemos ter em conta que:
- ainda que a declaração
int *p1;declare o mesmo queint p2[];, temos que p1 pode ser alterado, mas p2 não pode ser alterado eint p2[];só pode ser utilizado em certos casos; - a declaração
int p3[100];declara uma tabela com 100 inteiros e aloca memória na quantidade necessária; - a declaração
char *text;não aloca qualquer memória, no entantochar *text = "ola";aloca.
Qual a diferença entre as duas declarações seguintes?
char t1[] = "ola"; char *t2 = "ola";
- Ambas alocam 4 bytes e copiam para essa posição de memória a sequência de caracteres
'o','l','a','\0' - Em ambos os casos é possível modificar o conteúdo da memória alocada.
- Não é possível alterar o valor de
t1, ou seja não é possível port1a apontar para outra posição de memória. - É possível alterar o valor de
t2.
Exercícios e exemplos
Exercício 1: Seja
float a[100]; float *p=a;
Indique para cada uma das seguintes afirmações é verdadeira ou falsa:
a[i]é equivalente a*(a+i)&a[i]é equivalente aa+ia[i]é equivalente ap[i]p[i]é equivalente a*(p+i)*a,a[0]easão equivalentes ap[0]
Exemplo 1: Cópia de sequências de caracteres.
void strcpy(char *s, char *t)
{
int i = 0
while ((s[i] = t[i]) != ´\0´)
i++;
}ou
void strcpy(char *s, char *t)
{
while ((*s = *t) != ´\0´) {
s++;
t++;
}
}ou
void strcpy(char *s, char *t)
{
while ((*s++ = *t++));
}Exercício 2: Quando fazemos
int a;
scanf("%d",&a);o que estamos a passar ao scanf? E Porque não precisamos do & no seguinte caso?
char s[100];
scanf("%s",s);Exemplo 2: A passagem por referência consegue-se enviando ponteiros:
void leVector(int *v, int tamanho) {
int i;
for (i=0 ; i < tamanho ; i++)
scanf("%d", &v[i]);
}Podemos escrever o argumento como int *v ou int v[]. Como v já é um endereço, podemos alterar o v dentro da função.
Exemplo 3: É possível declarar um ponteiro para um ponteiro que pode guardar, portanto, o endereço de outro ponteiro:
#include <stdio.h>
int main() {
int x = 10;
int *px = &x;
int **ppx = &px;
printf("%d %d %d\n", x, *px, **ppx);
return 0;
}Exemplo 4: Ao fazer int *a; apenas estamos a reservar memória para 1 endereço de memória e não para um inteiro. Por esta razão, não devemos inicializar o conteúdo de um ponteiro sem que saibamos exactamente onde ele está a escrever.
int *a; *a = 12; /* A evitar!!! */
Argumentos da linha de comandos
int main(int argc, char *argv[]) {
int i;
for(i=1; i < argc; i++)
printf("%s ", argv[i]);
printf("\n");
return 0;
}O argumento argc guarda o número de argumentos passados ao programa, argv[0] é o nome com que o programa fui invocado e argv[i] é i-ésimo argumento. Se o código acima for compilado e produzir o binário escreve, temos:
[aplf@darkstar ~]$ escreve hello world hello world
Ponteiros para funções
É também possível ter ponteiros para funções e, em particular, o nome de uma função é já um ponteiro para essa função.
Exemplo 1:
int soma(int a, int b) { return a+b; }
int main() {
int (*ptr)(int, int);
ptr = soma;
printf("%d\n", (*ptr)(3,4));
return 0;
}Exemplo 2:
int modulo(int a) {return a<0?-a:a;}
int dobro(int a) {return a*2;}
void escreve(int (*func)(int), int valor){
printf(“%d\n”,(*func)(valor));
}
int main() {
int x=-10;
int (*f)(int);
f=modulo;
escreve(f,x);
return 0;
}Alocação dinâmica de memória
Até ao momento utilizámos sempre alocação estática:
int tab[100];
Neste caso a memória é alocada durante o scope da variável, não é possível libertar a mesma quando já não é necessária, e não é possível utilizar a mesma fora do scope. A solução para ultrapassar estas limitações passa por utilizar alocação dinâmica.
Função malloc
Esta função recebe como argumento o número de bytes (o tipo size_t representa uma dimensão em bytes) e devolve um ponteiro (endereço) para o primeiro byte do bloco de memória contígua alocada. O tipo de retorno void * indica um ponteiro para um tipo não especificado, o que permite a utilização desta função com qualquer tipo de dados. Posteriormente faz-se conversão para o tipo correcto por type cast.
Exemplo:
int *vec; vec = (int*) malloc(sizeof(int)*37);
Função realloc
void *realloc(void *ptr, size_t size);
A função realloc recebe como argumentos um ponteiro ptr para bloco de memória já existente e a dimensão size que o novo bloco de memória deverá ter. Esta função devolve um ponteiro para novo bloco de memória e copia os valores do bloco antigo para o novo, em que se novo bloco for mais pequeno, só copia até caber, se o novo bloco for maior, copia tudo e deixa o resto sem ser inicializado.
Se o argumento ptr for NULL, então a função realloc tem um comportamento idêntico ao da função malloc.
Função free
Para libertar memória podemos utilizar a função:
O argumento é um ponteiro para a primeira posição do bloco de memória contígua a libertar. Esta função não retorna nada.
Exemplo: Como libertar a memória reservada com o malloc anterior?
free(vec);
Todas estas função estão definidas em stdlib.h, pelo que é necessário incluir este ficheiro (#include <stdlib.h>).
Outras funções úteis
Existem outras funções úteis para, por exemplo, inicializar a memória alocada ou para copiar segmentos de memória. De facto, a função void *calloc(size_t nmemb, size_t size) permite tal como a função malloc alocar memória, neste caso para um vector com nmemb elementos em que cada elemento tem size bytes, mas em que a memória é inicializada a zero. Em geral podemos inicializar qualquer segmento de memória com a função void *memset(void *s, int c, size_t n), definida em string.h, que inicializa os primeiros n bytes de memória apontada por s com o valor c. Qual deve ser o valor de c se quisermos inicializar um vector de inteiros com valor -1 em todas as entradas?
A função void *memcpy(void *dest, const void *src, size_t n) é também útil para copiar segmentos de memória. Dados dois ponteiros src e dest para dois segmentos de memória sem sobreposição, esta função copia os primeiros n bytes apontados por src para os primeiros n bytes apontados por dest. É do programador a responsabilidade de garantir que não existe sobreposição entre os segmentos de memória e que os acessos de leitura e escrita ocorrem dentro dos limites da memória alocada.