Aula 5 – Ponteiros

Entender a fundo o uso de ponteiros é fundamental para um bom programador C. Eles são muito úteis porém perigosos. Um ponteiro mal referenciado pode causar falha em todo o sistema. Usá-los incorretamente poderá ocasionar erros muito difícieis de encontrar.

O que são Ponteiros?

Um ponteiro é uma variável que contém um endereço de memória. Esse endereço normalmente é a posição de outra variável na memória. Se uma variável contém o endereço de outra, ela aponta para a esta.

Variáveis Ponteiros

Para que uma variável seja um ponteiro, precisa ser declarada com um * na frente no nome da variável. A forma geral de declaração de um ponteiro é:

tipo *nome;

onde tipo é qualquer tipo válido em C e nome é o nome da variável ponteiro. O tipo base do ponteiro define para qual tipo de dado o ponteiro aponta (quantos bits irá ocupar na memória). Isso é fundamental para o uso da aritmética de ponteiros, discutido mais a baixo.

Os Operadores de Ponteiros

Para trabalhar com ponteiros usamos dois operadores especiais. O & é um oeprador unário que devolve o endereço da variável na memória de seu operando. Por exemplo:

m = &count;

o m receberá o endereço da memória que contém a variável count. O endereço não tem qualquer relação com o valor de count. Podemos ler o código acima da seguinte maneira: o ponteiro para inteiro m recebe o endereço de memória da variável inteira count. Digamos que o endereço de memória de count seja 2000, e que o valor desta variável é 100. Após a atribuição acima m terá o valor 2000. O outro operador de ponteiro, *, é o complemento de &. Também é um operador unário, e serve para devolver o valor da variável localizada no endereço que aponta. Supondo que m contém o endereço 2000,

q = *m;

q receberá o valor da variável apontada por m, neste caso count, ou seja, 100. O valor 100 estava armazenada na posição de memória 2000 que estava armazenada em m. Podemos ler o comando acima como: “q recebe o valor apontado pelo ponteiro m”. Atribuição de Ponteiros Assim como outras atribuições, o ponteiro pode ser usado no lado direito de um comando, para passar seu valor para outro ponteiro.

#include <stdio.h>
void main(void)
{
    int x;
    int *p1, *p2;
    p1 = &x;
    p2 = p1;
    // Escreve o endereço de x, não seu valor
    printf("%p", p2);
}

No programa acima, p1 e p2 apontam para x. O endereço de x será mostrado usando o modificador de formato printf() %p, que exibe o endereço da memória, algo como: 0x7fffce57b98c ( na minha máquina de 64bits).

Aritmética de Ponteiros

Apenas adição (++) e subtração (–) podem ser usadas com ponteiros. Assumindo que p1 aponte para um inteiro de 2 Bytes e aponte para o endereço 2000, a operação

p1++;

Irá fazer com que p1 aponte para 2002 e não para 2001, afinal ele aponta para o próximo inteiro (+ 2 Bytes). O mesmo ocorre para o decremento

p1–;

que receberia 1998.

Lembrando: ao incrementar ou decrementar um ponteiro, este sempre irá apontar para uma casa a mais ou a menos de seu tipo de dado:

Aritmética de ponteiros

Além de incrementar, podemos somar valores aos ponteiros:

p1 = p1 + 12;

Não podemos efetuar divisões ou multilicações com ponteiros, nem usar operadores de deslocamente e de mascaramento bit a bit. Não podemos subtrair ou somar tipos float e double com ponteiros.

Comparação de Ponteiros

Podemos comparar ponteiros em uma expressão relacional:

    if (p < q) printf("p aponta para uma memória mais baixa que q\n");

O programa abaixo usa comparação de ponteiros em uma pilha. Pilhas são usadas para armazenamento de informações onde o primeiro a entrar é o último a sair. Assim como quando enchugamos a louça e guardamos os pratos. O primeiro prato guardado será o último a ser utilizado, esse é o conceito de pilha.

Em um programa, precisamos de duas funções básicas:
pop(): Remove o último ítem da pilha
push(): Coloca um novo item na pilha

#include <stdlib.h>
void push(int i);
int pop(void);
int *tos, *p1, stack[50];

void main(void)
{
    int value;
    //  tos conterá o topo da pilha
    tos = stack;
    //  inicia p1
    p1 = stack;

    do {
        printf("Entre com o valor: ");
        scanf("%d", &value);
        if (value != 0) push(value);
        else printf("valor do topo é %d\n", pop());
    } while(value != -1);

}

void push(int i)
{
    p1++;
    if(p1 == (tos + 50)){
        printf("O famovos estouro de pilha ocorreu");
        exit(1);
    }
    *p1 = i;
}

pop(void)
{
    if (p1 == tos){
        printf("pilha vazia");
        exit(1);
    }
    p1--;
    return *(p1 + 1);
}

A memória da pilha é pornecida pela matriz stack que contém 50 campos. O p1 foi ajustado para apontar para o primeiro byte de stack. A pilha é acessada pela variável p1. A variável tos conteḿ o endereço do topo da pilha e ele garante que não se retirem elementos da pilha vazia. Estude a fundo o programa acima para ter certeza de que não tem mais dúvidas a respeito de comparações de ponteiros.

Ponteiros e Matrizes

Em C, há uma estreita relação entre ponteiros e matrizes, por exemplo:

    char str[80], *p1;
    p1 = str;

Acima, p1 contém o endereço do primeiro elemento da matriz str. Para acessar o quinto elemento, faríamos str[4] ou *(p1 + 4). Isso porque o nome da matriz sem o índice (str) retorna o primeiro elemento da matriz. O acesso à matrizes por ponteiros geralmente é mais rápido, por esse motivo a maioria dos programadores em C normalmente os usam para acessar matrizes.

A função puts() pode ser usada para escrever uma string no dispositivo de saída padrão. Abaixo temos duas implementações dela, uma com indexação de matrizes e outra com ponteiros.

// Indexa s como uma matriz
void puts(char *s)
{
    register int t;
    for (t = 0; s[t]; ++t) putchar(s[t]);
}

// Acessa s como um ponteiro
void puts(char *s)
{
    while(*s) putchar(*s++);
}

Matrizes de Ponteiros

Assim como qualquer tipo de dado, ponteiros podem ser organizados em matrizes. Para fazer a declaração usamos int *x[10] que será uma matriz de ponteiros inteiros com tamanho 10. Para atribuir um valor ao quinto elemento usamos x[4] = &var. E por fim, para acessar o valor deste elemento usamos *x[4].

Se quizermos passar uma matriz de ponteiros para uma função, apenas chamamos a função com o nome da matriz, sem qualquer índice. A função que recebe uma matriz x se parece com isso:

void display_array(int *q[])
{
    int t;
    for (t = 0; t < 10; t++)
        printf("%d ", *q[t]);
}

Lembrando que q é um ponteiro para uma matriz de ponteiros inteiros, por este motivo é necessário declarar o parâmetro q como uma matriz de ponteiros inteiros. Matrizes de ponteiros normalmente são usadas como ponteiros para strings. Podemos ter uma função que exibe mensagens de erro de acordo com seu número de código.

void syntax_error(int num)
{
    static char *err[] = {
        "arquivo não pode ser aberto\n",
        "erro de leitura\n",
        "erro de escrita\n",
        "falta da mídia\n"
    };
    printf("%s", err[num]);
}

A matriz err contém ponteiros para cada string e printf é chamada com um ponteiro de caracteres que aponta para uma das várias mensagens. Se fosse passado o valor 1 como parâmetro da função, a mensagem “erro de leitura” seria apresentada. Podemos observar que o argumento da função main argv é uma matriz de ponteiros para caracteres.

Indireção Múltipla

Também conhecida como ponteiros para ponteiros ocorre quando usamos um ponteiro que aponta para o endereço de variável de outro ponteiro que por sua vez aponta para o endereço da variável que contém o valor. Não podemos confundir indireção múltipla com listas encadeadas, pois para um ponteiro apontar para outro, ele deve ser declarado como tal, para isso usamos dois asteriscos na frente do nome da variável.

#include <stdio.h>
void main(void)
{
    int x, *p, **q;
    x = 10;
    p = &x;
    q = &p;
    // Imprime o valor de x
    printf("%d", **q);
}

Inicialização de Ponteiros

Após um ponteiro ser declarado e antes que lhe seja atribuído um valor, ele contém um valor desconhecido. Tentar acessar este valor pode gerar um erro fatal no nosso programa ou até sistema operacional. Por convenção devemo sempre iniciar um ponteiro apontando para nulo que só não será seguro se for usado à esquerda de um comando de atribuição. Abaixo, a função search() contém um laço for que será encerrado quando encontrar a string desejada, ou quando o final da matriz, que é marcado com um ponteiro nulo, for alcançado.

// Procura uma string
search(char *p[], char *name)
{
    register int t;
    for (t = 0; p[t]; ++t)
        if (!strcmp(p[t], name)) return t;
    // Caso não encontre, returna -1
    return -1;
}

Se inicializarmos uma string como: char *p = “alo mundo\n”; veremos que p não é uma matriz. Essa inicialização funciona porque todo compilador C cria uma tabela de string que é usada internamente para armazenar constantes string usadas pelo programa. Assim o comando mostrado coloca o endereço de “alo mundo“, armazenado na tabela de strings, no ponteiro p. O programa abaixo é perfeitamente válido:

#include <stdio.h>
#include <string.h>
char *p = "alo mundo";

void main(void)
{
    register int t;
    // Imprime o conteúdo da string de trás para frente
    printf(p);
    for (t = strlen(p) -1; t > -1; t--) printf("%c", p[t]);
}

Ponteiros para Funções

Apesar de muito confuso, é muito poderoso o uso de ponteiros para função em C. Como toda função contém um endereço físico na memória, este endereço é a entrada da função. Portanto, um ponteiro de função pode ser usado para chamar uma função.

Para entender como isso funciona, precisamos entender que quando cada função é compilada, o código-fonte é transformado em código-objeto e um ponto de entrada é estabelecido. Quando é feita uma chamada à função no programa, um “call” é feito em linguagem de máquina para este ponto de entrada. Portanto, se um ponteiro contém o endereço do ponto de entrada de uma função, ele pode ser usado para chamar a função.

Para obter o endereço de uma função, usamos o nome dela sem parênteses ou arguemntos. Olhe com atenção as declarações do programa abaixo:

#include <stdio.h>
#include <string.h>
void check(char *a, char *b, int(*cmp)());
void main(void)
{
    char s1[80], s2[80];
    int (*p)();
    p = strcmp;
    scanf("%s", s1);
    scanf("%s", s2);
    check(s1, s2, p);
}

void check(char *a, char *b, int (*cmp)())
{
    printf("testando igualdade\n");
    if (!(*cmp)(a, b)) printf("igual");
    else printf("diferente");
}

A função check() recebe dois ponteiros para caracteres e um ponteiro para função. Devemos notar que o ponteiro para função é declarado com dois parênteses, do contrário não funcionaria. o comando (*cmp)(a, b) chama strcmp(), que é apontado por cmp com os argumentos a e b.[

Mas afinal, para que usar um ponteiro para função? Em certos programas onde uma matriz de strings contém nomes de funções, pode ser útil usá-los para fazer a chamada de determinada função ao invés de um grande comando switch. O programa abaixo a função check() testa igualdade numérica ou alfabética, simplesmente chamando uma função de comparação diferente.

#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
#include <string.h>

void check(char *a, char *b, int (*cmp)());
int numcmp(char *a, char *b);

void main(void)
{
    char s1[80], s2[80];
    int (*p)();
    scanf("%s",s1);
    scanf("%s", s2);
    if (isalpha(*s1))
        check(s1, s2, strcmp);
    else
        check(s1, s2, numcmp);
}

void check(char *a, char *b, int (*cmp)())
{
    printf("testando igualdade\n");
    if (!(*cmp)(a, b)) printf("igual\n");
    else printf("diferente\n");
}
x
numcmp(char *a, char *b)
{
    if (atoi(a) == atoi(b)) return 0;
    else return 1;
}

As Funções de Alocação Dinâmica em C

Como sabemos, variáveis globais tem armazenamento alocado em tempo de compilação. Variáveis locais usam a pilha. Ambas não podem ser acrescentadas durante o tempo de execução. Porém programas como processadores de texto ou banco de dados precisam usar muita memória RAM, e como cada sistema pode ser diferente de outro, eles devem fazer isso da maneira que for possível. Os ponteiros fornecem o suporte necessário para o podereoso sistema de alocação dinâmica de C.

A memória alocada por funções de alocação dinâmica de C é obtida do heap, que geralmente tem um grande espaço disponível. As funções mais utilizadas para alocação dinâmica em C são malloc() e free() onde elas alocam e liberam memória respectivamente. Para utilizá-las precisamos incluir o cabeçalho STDLIB.H.

A função malloc() tem o protótipo:

void * malloc(size_t número_de_bytes);

O número_de_bytes é a quantidade de memória que queremos alocar. size_t é definido em STDLI.H como (mais ou menos) um inteiro sem sinal. Após a chamada bem sucedida, malloc() retorna um ponteiro para o primeiro byte da região de memória alocada no heap. Se não conseguiu alocar memória, retorna um nulo. O fragmento de código abaixo aloca 1000 bytes na memória RAM:

char *p;

p = malloc(1000); // obtém 1000 bytes

Após a atribuição, p aponta para o primeiro dos 1000 bytes de memória livre. No próximo exemplo usamos a função sizeof para assegurar portabilidade:

int *p;

p = malloc(50 * sizeof(int));

Assumindo o fato de heap não ser infinito, devemos sempre testar o valor retornado por malloc() antes de usar o ponteiro. Afinal um ponteiro nulo irá, quase que certamente, travar o computador. O programa abaixo tentou alocar 4GB de memória em meu sistema de 3GB por 64bits e não conseguiu:

#include <stdio.h>
#include <stdlib.h>

void main(void)
{
    char *p;

    if (!malloc(4000000000)){
        printf("sem memória para fazer isso\n");
        exit(1);
    }
}

A função free() é o oposto de malloc(), pois serve para devolver ao sistema a memória previamente alocada. Seu protótipo é simples:

void free(void *p);

É muito importante nunca utilizar free() com argumentos inválidos, pois isso pode destruir a lista de memória livre. O uso mais comum das funções malloc() e free() são em programas que fazem o uso de listas encadeadas ou árvores binárias.

Matrizes Dinamicamnete Alocadas

Em alguns caso iremos precisar alocar uma matriz dinamicamente e percorrê-la usando indexação de matrizes. Abaixo um exemplo disto:

/**
 * Aloca espaço para uma string dinamicamente, solicita a entrada do usuário
 * e, em seguida, imprime a string de trás para frente.
 */
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
void main(void)
{
    char *s;
    register int t;
    s = malloc(80);

    if (!s){
        printf("falha na solicitação de memória\n");
        exit(1);
    }

    scanf("%s", s);
    for (t = strlen(s) -1; t >= 0; t--) putchar(s[t]);
    free(s);
}

Acessar a memória alocada como se fosse uma matriz unidimensional é simples. O problema está nas matrizes dinâmicas multidimensionais visto que não podemos indexar um ponteiro diretamente como se ele fosse uma matriz multidimensional. Para conseguir uma matriz multidimencional precisamos recorrer a um truque: passar o ponteiro como parâmetro à uma função. Assim a função pode definir as dimensões do parâmetro que recebe o ponteiro, permitindo a indexação normal da matriz. O exemplo a seguir constrói uma tabela dos números de 1 a 10 elevados a primeira, segunda, terceira e quarta potência:

/**
 * Apresenta as potências dos números de 1 a 10.
 * Nota: muito embora esse programa esteja correto, alguns compiladores
 * apresentarão uma mensagem de advertência com relação aos argumentos para
 * as funções table() e show(). Se ocorrer, ignore.
 */
#include <stdio.h>
#include <stdlib.h>
int pwr(int a, int b);
void table(int p[4][10]);
void show(int p[4][10]);
void main(void)
{
    int *p;
    p = malloc(40 * sizeof(int));
    if (!p){
        printf("falha na solicitação de memória\n");
        exit(1);
    }
    // Aqui, p é simplesmente um ponteiro
    table(p);
    show(p);
}

// Constrói a tabela de potências
void table(int p[4][10])
{
    // Agora o compilador tem uma matriz para trabalhar
    register int i, j;
    for (j = 1; j < 11; j++)
        for (i = 1; i < 5; i++)
            p[i][j] = pwr(j, i);
}

// Exibe a tabela de potências inteiras
void show(int p[4][10])
{
    // Agora o compilador tem um matriz para trabalhar
    register int i, j;
    printf("%10s %10s %10s %10s\n", "N", "N^2", "N^3", "N^4");
    for (j = 1; j < 11; j++){
        for (i = 1; i < 5; i++) printf("%10d ", p[i][j]);
        printf("\n");
    }
}

// Eleva um inteiro a uma potência especificada
pwr(int a, int b)
{
    register int t = 1;
    for (; b; b--) t = t * a;
    return t;
}

O programa acima induz o compilador C a manipular matrizes dinâmicas multidimensionais. Do ponto de vista do compilador, temos uma matriz de 4×10 dentro das funções show() e table(). A diferença é que o armazenamento para a matriz é alocado manualmente usando o comando malloc(), em lugar de usar o comando normal de declaração de matriz. Podemos também notar o uso de sizeof para calcular o número de bytes necessários para uma matriz inteira de 4×10. Isso garante portabilidade ao código para que opere com inteiros de tamanhos diferentes.

Problemas com Ponteiros

Os ponteiros são fantásticos, porém um ponteiro selvagem pode ser o erro mais difícil de descobrir. O problema é que um ponteiro que aponte para um local desconhecido pode apontar para lixo, então ao ler o valor não teremos grandes problemas, porém ao escrever algo no ponteiro podemos estar escrevendo sobre outras partes do nosso código ou dados. Isso pode fazer com que procuremos o problema no lugar errado e pode tirar várias horas de sono de um programador. Discutiremos aqui os erros mais comuns com ponteiros.

O exemplo clássico é um ponteiro não inicializado. Ele atribui o valor 10 a alguma posição de memória desconhecida. O ponteiro p nunca recebeu um valor; portanto, ele contém lixo. Esse tipo de problema sempre passa despercebido, quando seu programa é pequeno, pois é pouco provável que p conhtenha um endereço que esteja em nosso código, area de dados ou sistema operacional. Porém, conforme o programa aumenta, essa probabilidade aumenta.

// Este programa está errado
void main(void)
{
    int x, *p;
    x = 10;
    *p = x;
}

O segundo erro comum ocorre em um simples equívoco sobre como usar um ponteiro. A chamada printf() não imprime o valor de x, que é 10, pois na atribuição p = x; p irá receber o valor 10. Para corrigir o problema escreva p = &x;

// Este programa está errado
void main(void)
{
    int x, *p;
    x = 10;
    p = x;
    printf("%d", *p);
}

Um erro, que as vezes ocorre, é quando comparamos ponteiros que naõ apontam para um objeto em comum. Isso produz resultados inesperados. É um conceito inválido:

    char s[80], y[80];
    char *p1, *p2;
    p1 = s;
    p2 = y;
    if (p1 < p2) ...

Um erro semelhante acontece, da suposição de que duas matrizes adjacentes podem ser indexadas como uma única, simplesmente incrementando um ponteiro através dos limites da matriz. Embora possa funcionar, não é certo pois isso pode não ocorrer sempre.

    int first[10], second[10];
    int *p, t;
    p = first;
    for (t = 0; t < 20; ++t) *p++ = t;

O programa abaixo mostra um tipo de erro muito perigoso. p1 é usado para imprimir valores ASCII associados a cada caractere contido em s. O problema é que o endereço de s é atribuído a p1 somente uma vez. Na primeira iteração do laço, p1 aponta para o primeiro caractere em s. Já na segunda interação, ele continua apontado aonde foi deixado, porque ele não é reinicializado para o começo de s. E este próximo caractere pode ser uma outra string, variável ou pedaço de programa:

#include <string.h>
#include <stdio.h>
void main(void)
{
    char *p1;
    char s[80];
    p1 = s;

    do{
        // Lê uma string
        scanf("%s", s);
        // Imprime o equivalente decimal de cada caractere
        while(*p1) printf(" %d", *p1++);
    } while(strcmp(s, "done"));
}

A forma correta de escrita do programa está abaixo. Cada vez que o laço se repete, p1 é ajustado para o início da string. Em geral devemos sempre nos lembrar de reinicializar um ponteiro se ele for reutilizado.

#include <string.h>
#include <stdio.h>
void main(void)
{
    char *p1;
    char s[80];
    do{
        // Faz a atribuição em cada looping
        p1 = s;
        // Lê uma string
        scanf("%s", s);
        // Imprime o equivalente decimal de cada caractere
        while(*p1) printf(" %d", *p1++);
    } while(strcmp(s, "done"));
}

Minha Reflexão

A primeira vez que li sobre ponteiros (em 2008 no livro C++ como programar) senti um certo receio, afinal o livro afirmava que talvez fosse aquele o assunto mais difícil de se entender em programação. Agora vejo que os ponteiros são simples, porém precisam ser praticados, como todo o resto.

4 Comments

  1. Irei concordar com a Tamires, achei este site procurando conteúdo sobre ponteiros, muito bacana, muito esclarecedor. Obrigada.

Leave a Reply

%d