Miscelâneas sobre a linguagem C

Este artigo descreve alguns pontos pouco comuns sobre a linguagem C e que muitas vezes não são oferecidos ao programador iniciante da linguagem.

Sobre a função de biblioteca free()

Segundo o padrão ANSI C, é seguramente válido passar um ponteiro NULL a função free(), portando o trecho de código abaixo não apresenta nenhum erro de execução:

char *buffer = NULL;
free(buffer);

Entretanto, chamar mais de uma vez a função free() em um mesmo ponteiro (double free) é um erro, ou pelo menos é uma condição anormal que é devidamente sinalizada pelo sistema operacional. Por exemplo, o código abaixo:

char *buffer = malloc(BUFSIZ);
free(buffer);
free(buffer);

Apresenta a seguinte mensagem de erro no Mac OS X:

doublefree(3744) malloc: *** error for object 0x800000: double free
*** set a breakpoint in malloc_error_break to debug

No Linux a mensagem de erro também segue um backtrace e um dump de memória, o que facilita em achar aonde está o problema.

*** glibc detected *** ./free: double free or corruption (top): 0x0804a008 ***

Por último, a região de memória que se passa a função free() deve ter sido previamente criada através da função malloc() ou relacionada. O trecho de código abaixo:

char buf[1024];
free(buf);

Exibe a seguinte mensagem de erro no Mac OS X:

notallocated(48169) malloc: *** error for object 0xbffff6bc: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug

Já no Linux ocorre uma falha de segmentação (Segmentation fault).

Sobre o operador sizeof

O operador sizeof aplicado a um vetor retorna o número de elementos desse vetor, multiplicado pelo tamanho (em bytes) do tipo base desse elemento. Nos exemplos abaixos:

char buffer[80];
char *boolean[] = {"True","False",NULL};

O número de elementos de buffer é 80, pois a expressão sizeof(buffer)/sizeof(char) ou 80/1 é igual a 80. O número de elementos de boolean é 3, conforme o resultado da expressão sizeof(boolean)/sizeof(char*) ou 12/4 = 3.

#define NELEM(v) (sizeof(v)/sizeof(v[0]))

O operador sizeof pode ser utilizado para retornar o tamanho de uma string em um vetor.

char str[] = "Hello World!";
int tam = sizeof(str);  /* tam = 13  (12+1 do \0) */

Cuidado que a dica só funciona para vetores, quando se usa em ponteiros o resultado não vai ser o que se espera, o tamanho vai ser igual ao tamanho de um ponteiro!

char *s = "Hello World!";
int tam = sizeof(s);    /* tam = 4 pois é um ponteiro! */

Sobre ponteiros & vetores

Não se pode levar ao pé da letra que um ponteiro em C é estritamente igual a um vetor do mesmo tipo. Um ponteiro char *p pode fazer referência a um vetor char buffer[], porém uma declaração de ponteiro nunca está associado a criação de conteúdo, enquanto que um vetor está associado a criação do conteúdo.

char octal[8] = {'0','1','2','3','4','5','6','7'}; /* contem */
char *p = octal; /* aponta */

Lembrando que o nome do vetor é uma constante e que octal é um endereço equivalente à expressão &octal[0]. O valor da expressão *p é o caracter 0, o valor de *(p+2) é o caracter 2, o valor de p[7] é o caracter 7 e o valor de *(p++) é o caracter 1. A regra para ponteiro e vetores *(p+n) == p[n] == octal[n] é sempre válida.

Sobre ponteiros de função

Um dos tópicos que as vezes parece magia negra na linguagem C é a declaração de ponteiros de função. De forma simples, um ponteiro de função é declarado com um asterisco * como prefixo do nome da função, por motivos de precedência entre operadores, deve-se utilizar parênteses para delimitar o nome do ponteiro, por exemplo (*nome).

Alguns exemplos de ponteiros de funções.
Declaração de função Declaração de ponteiro de função
void sync(void); void (*ptr_sync)(void) = sync;
void exit(int); void (*ptr_exit)(int) = exit;
size_t strlen(char *); size_t (*ptr_strlen)(char *) = strlen;
char *strstr(char *, char *); char* (*ptr_strstr)(char *, char*) = strstr;

Com estas definições, é possível evocar as funções originais através de ponteiros, como em ptr_sync() ou ptr_exit(1) e o mesmo para as demais funções.

Sobre função de biblioteca qsort()

A função qsort permite ordenar um vetor e possui o seguinte protótipo de chamada no Mac OS X:

qsort(void *base,
      size_t nel,
      size_t width,
      int (*compar)(const void *, const void *));

Aonde base é o endereço base do vetor (o nome do vetor), nel é o número de elementos, width é o tamanho de um elemento e compar é uma função de comparação que retorna os inteiros -1, 0 ou +1 conforme a expressão “A-B”.

Exemplo de implementação para a função compar que utiliza comparação de strings.

int scmp(const void *p1, const void *p2)
{
  const char *s1 = *(char **)p1;
  const char *s2 = *(char **)p2;
  return strcmp(s1, s2);
}

Vamos classificar o vertor char *v[] = {"John", "Paul", "George", "Ringo"} com qsort e a função scmp.

qsort(v, sizeof(v)/sizeof(v[0]), sizeof(v[0]), scmp);

A função qsort modifica o vetor v, de forma que o resultado de qsort é o vetor ordenado conforme a regra de compar.

Sobre o modelo ILP32

O modelo de dados ILP32 estabelece para os tipos inteiros (int), inteiros longos (long) e ponteiros (void *) que ambos possuem um tamanho fixo de 32 bits, conforme a sigla indica.

É considerado seguro guardar um inteiro em um pointeiro e vice-versa. São exemplos de sistemas que utilizam o modelo ILP32, o Mac OS X (em 32 bits) e o Linux (em 32 bits).

Para forçar uma compilação em 32 bits, deve-se passar a opção -m32 ao compilador GCC.

Sobre o modelo LP64

Uma boa parte dos sistemas Unix de 64 bits, como o Linux e o Mac OS X, adotam o modelo de programação LP64. Neste modelo, os inteiros (int) possuem 32 bits e os inteiros longos (long) e ponteiros (void*) possuem 64 bits, conforme a sigla indica.

Guardar um ponteiro em um tipo inteiro em um ambiente de 64 bits é dar um tiro no pé, e o compilador vai gerar um aviso, mas é considerado seguro guardar um ponteiro em um inteiro longo.

Para forçar uma compilação em 64 bits, deve-se passar a opção -m64 ao compilador GCC.