Este artigo descreve alguns pontos obscuros da linguagem C e que muitas vezes não são oferecidos ao programador iniciante da linguagem.
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);
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)
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]
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]))
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! */
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.
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)
.
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.
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”.
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
.
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);
Para classificar em ordem inversa (ou decrescente) basta alterar a condição de teste da função de comparação.
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.
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.