Ponteiros, de novo!!!

Acho que essa deve ser a terceira ou quarta vez que falo sobre ponteiros, tentando acabar com o ar “exotérico” que existe sobre esse “conceito”. Eis mais uma tentativa.

A coisa toda deriva do fato de que um ponteiro é um valor inteiro de tamanho fixo e que não tem sinal. Na arquitetura i386 um ponteiro é exatamente um unsigned int (32 bits). Já na arquitetura x86-64 ele é exatamente um unsigned long long (64 bits, ou um unsigned long no padrão I32LP64, usado em POSIX, por exemplo). Ou seja, esse número pode ser usado como um endereço. Ênfase no “pode”.

O problema que o novato enfrenta, em C, é que existe uma maneira especial de declarar variáveis que contém esse valor inteiro que pode ser usado como endereço:

char *p;

O que fizemos ai em cima é declarar uma variável chamada p que conterá um valor inteiro… Note: O nome da variável é p, não *p! O asterisco na declaração nos diz apenas que o conteúdo dessa variável pode ser usado como um endereço para a memória e que a porção da memória que será lida ou gravada tem o tamanho de um char (8 bits) se p for usado como um endereço!

Quando fazemos:

int x = 10;
int *p;

p = &x;

A variável p conterá um valor inteiro correspondente ao “endereço de x” (A operação & significa isso: “obtenha o endereço de”). É claro que podemos abreviar isso ai escrevendo:

int *p = &x;

Mas, ainda assim, a variável continua sendo p, e não *p! E, se você imprimir o valor de p, provavelmente obterá algo como 0x7ffc0d62dfa4. Isso ai é apenas a notação hexadecimal de um valor inteiro.

Já mostrei, antes, que declarações e usos são coisas diferentes, em C. Se fizermos algo assim:

#include <stdio.h>

void main(void)
{
  int x = 10;
  int *p = &x;

  printf("p = 0x%lx\n"
         "*p = %d\n", 
         p, *p);
}

Onde se lê “int *p = …” é uma declaração da variável p. Onde se lê *p, num dos argumentos da chamada da função printf, estamos usando a variável p junto com um operador de indireção. O operador de indireção diz “use o valor de p como se fosse um endereço”. Chama-se operador de indireção, porque o acesso ao dado é indireto, feito através de um endereço contido na variável p.

Ao executar o programinha obteremos algo assim:

$ gcc -o test test.c
$ ./test
p = 0x7ffc1e11d314
*p = 10

Repare que não há avisos na compilação, mesmo que eu não tenha usado “%p” na string de formatação do printf… Eu usei “%lx”, que imprimirá um valor unsigned long em hexadecimal…  Substitua o “%lx” por “%p” e você obterá o mesmo resultado!

E isso é tudo o que você precisa saber sobre ponteiros…

Bem… quase tudo…

Ponteiros válidos:

Para cada processo o seu sistema operacional recorta uma fração da memória e diz “a memória usável por esse processo vai do endereço X até o endereço Y”. Qualquer tentativa de usarmos endereços fora da faixa entre X e Y causará um erro de proteção (você estará tentando acessar memória protegida pelo sistema operacional!)… Nos sistemas POSIX esse erro é conhecido como “segmentation fault”, no Windows isso é chamado de “access violation”.

Se você inicializar a variável p, no exemplo acima, com um valor inteiro que não esteja dentro da faixa estabelecida pelo sistema operacional, vai tomar um erro na fuça! Quer dizer, o ponteiro será inválido para uso, mas não será um “ponteiro inválido”, em termos do conceito de ponteiros!! Você pode inicializar um ponteiro com um valor que o sistema operacional não reconhece como válido para o seu processo, só não poderá usá-lo sem tomar um erro!

Não significa que o endereço “não existe”… Ele só não é acessível para o seu processo! Este segmento da memória está “em falta” para o seu processo (dai o “segmentation fault”) ou, no caso do Windows, você está violando a proteção de acesso imposta pelo OS (dai o “acess violation”).

Ponteiros NULL:

Um desses valores de endereço invalido especial é o valor zero… Um endereço zerado é, por convenção, chamado de nulo. Para facilitar a leitura de um código que lide com ponteiros, existe o símbolo NULL definido em stddef.h. Esse valor inteiro especial é usado por toda a biblioteca padrão de C para exprimir dois significados:

  • Em funções que retornam ponteiros, NULL significa “inválido”;
  • Em funções que tomam ponteiros em um ou mais de seus argumentos, NULL pode significar “opcional” ou “inválido”;

Mas, eis o detalhe interessante: Embora seja bom usar o símbolo NULL para realizar testes, quando lidamos com ponteiros, ele não é obrigatório… Você pode muito bem comparar com zero! Por exemplo, você já deve ter visto códigos assim:

  FILE *f;

  f = fopen("myfile.dat", "r");
  if (!f)
  {
    perror("fopen");
    exit(1);
  }
  ...

Ora… A documentação de fopen nos diz claramente para testarmos o retorno contra NULL, caso a função falhe, mas estamos comparando com zero, no código acima, através do operador booleano unário de negação “!”. Isso é perfeitamente válido, já que NULL é a mesma coisa que zero.

Em stddef.h o símbolo NULL é definido como:

#define NULL (void *)0

casting para um ponteiro void serve para que o compilador tenha chances de fazer checagens entre variáveis declaradas como ponteiros e outras, declaradas como tipos inteiros simples. Isso possibilita ao compilador emitir avisos de problemas em potencial. Mas, a comparação e uso de ponteiros como se fossem inteiros simples não é proibida e, de fato, é a essência da coisa toda!

De novo: derreferenciar NULL não é a única causa de segmentation faults:

Claro que se você tentar usar o operador de indireção “*” com um ponteiro zerado obterá um segmentation fault. A mesma coisa acontece com qualquer outro valor que não foi destinado ao seu processo! Nada nos impede de inicializarmos um ponteiro com o endereço 10, por exemplo:

/* O casting aqui é necessário apenas para evitar avisos do compilador! */
char *p = (char *)10;

Aqui p conterá o valor 10… Se tentarmos acessar o dado, via o ponteiro p, através do operador de indireção (via *p), obteremos também um segmentation fault.

Você pode acessar regiões de memória “não alocadas”

Isso soa estranho… Mas considere o fragmento de programa abaixo:

  ...
  char *p = (char *p)malloc(1000);
  char *q = p + 1010;
  *q = 'a';
  ...

A função malloc alocará 1000 bytes em alguma região da memória, certo? Mas note que estamos inicializando q com um endereço que está 10 bytes além do espaço alocado por malloc

Provavelmente a derreferência de q, logo abaixo, e a escrita do caracter ‘a’ neste endereço não vai causar um segmentation fault. Isso acontece porque o sistema operacional oferece uma faixa de endereços bem maior que apenas 1000 bytes. A função malloc é uma daquelas funções de gerência de memória e ela só pedirá para o OS mais memória para o processo se for realmente necessário… Não faz parte do escopo desse artigo, que já está ficando bem grande, mas os OSs geralmente designam regiões da memória em tamanhos múltiplos de 4 kB para um processo no userspace… Assim mesmo que tenhamos, em teoria, alocado apenas 1000 bytes no heap, temos (talvez!) uns 4096 endereços válidos e acessíveis por ponteiros…

Mas, note que fazer isso não é uma boa idéia… Não temos como saber exatamente onde o malloc separou esse pedaço do heap para nós (ele poderia ter separado no final da faixa de endereços válidos!).

Anúncios

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s