Home RecentChanges

Lesson12

https://web.ist.utl.pt/~aplf/aed/ponteiros.png

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.

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:

Qual a diferença entre as duas declarações seguintes?

char t1[] = "ola";
char *t2 = "ola";

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:

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

void *malloc(size_t size);

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:

void free(void *ptr);

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.