Miscelâneas sobre a linguagem C

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

Sobre a função free

free de NULL

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

char *buf = NULL;
free(buf);

Duplo free

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

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

Apresenta a seguinte mensagem de erro no Mac OS X:

doublefree(5193) malloc: *** error for object 0x7f9b0a000000: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
Abort trap: 6

No Linux, a mensagem de erro também agrega backtrace e dump de memória, o que facilita para achar aonde está o problema:

*** glibc detected *** ./doublefree: double free or corruption (top): 0x0000000000db8010 ***
======= Backtrace: =========
/lib/x86_64-linux-gnu/libc.so.6(+0x7e626)[0x7f3716b0d626]
./doublefree[0x400572]
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xed)[0x7f3716ab076d]
./doublefree[0x400489]
======= Memory map: ========
...
Aborted (core dumped)

free em memória heap

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 (ou seja, no heap). O trecho de código abaixo:

char buf[BUFSIZE];
free(buf);

Exibe a seguinte mensagem de erro no Mac OS X:

vectfree(5295) malloc: *** error for object 0x7fff65a587b0: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
Abort trap: 6

E no Linux ocorre uma falha de segmentação (segmentation fault), porém, durante a compilação, é exibida uma mensagem de aviso importante que sinaliza o erro em execução:

vectfree.c: In function ‘main’:
vectfree.c:7:6: warning: attempt to free a non-heap object ‘buf’ [enabled by default]

Sobre o operador sizeof

Número de elementos de um vetor

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 que seguem:

char buf[1024];
char *boolean[] = {"True", "False", NULL};

O número de elementos de buf é 1024, pois a expressão sizeof(buf)/sizeof(char) ou 1024/1 que é igual a 1024.

O número de elementos de boolean é 3, conforme o resultado da expressão sizeof(boolean)/sizeof(char*) ou seja, 12/4 = 3.

A macro abaixo é comumente utilizada para obter o número de elementos de um vetor:

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

Tamanho de uma string em vetor

O operador sizeof pode ser utilizado para retornar o tamanho de uma string guardada em um vetor. Por exemplo, no código abaixo:

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

A variável tam vai conter exatamente o tamanho da string (em bytes). Note que este funcionamento não é portável, pois em codificações que um caracter pode variar em número de bytes (UTF-8, por exemplo), o retorno é o número de bytes totais, e não o número de caracteres.

E cuidado que esta dica só funciona para vetores, quando se usa em ponteiros, o resultado vai ser igual ao tamanho do ponteiro!

char *s = "Hello World!";
int tam = sizeof(s);    /* tam = 8 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 buf[], porém uma declaração de ponteiro pode ou não estar 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'}; /* contém */
char *p = octal; /* aponta */

Lembrando que o nome do vetor é uma constante e que octal é um endereço de memória 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_da_funcao).

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 a função qsort

A função de biblioteca qsort permite classificar (como em ordenar) um vetor genérico e possui o seguinte protótipo de chamada:

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”.

qsort com string

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 vetor 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 (ocorre in place), de forma que o resultado de qsort é o vetor ordenado conforme a regra de compar.

qsort com inteiro

Classificar um vetor de inteiros é muito semelhante, o detalhe está na função de comparação:

int icmp(const void *p1, const void *p2)
{
	int n1 = *(int *)p1;
	int n2 = *(int *)p2;
	return n1 - n2;
}

Infelizmente o código acima pode causar overflow e comparar errado, a solução é utilizar a comparação conforme sugere a macro abaixo:

#define COMPARE(a, b) (((a) > (b)) - ((a) < (b)))

O retorno correto da função seria então definido como:

return (n1>n2) - (n1<n2);

Ordem inversa

Para classificar em ordem inversa (ou decrescente) basta alterar a condição de teste da função de comparação.

Sobre o modelo ILP32

O modelo de programação 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 ponteiro 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.