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 + 1
funciona como esperado;++*px
incrementa 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 port1
a 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+i
a[i]
é equivalente ap[i]
p[i]
é equivalente a*(p+i)
*a
,a[0]
ea
sã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.