jul 2010 30

A função strtok acompanha todos os compiladores ANSI C, e serve para dividir uma string em diversas cadeias menores. Um exemplo seria separar os campos de um registro no formato “campo a:campo b:campo c”, uma linha de dados tendo colunas divididas pelo caractere “:”.

Existem diversos motivos para evitar esta função a qualquer custo. Tive problemas recentes com o strtok(), e decidí compartilhar aí pelo menos 3 razões para evitar strtok(). São os chamados “efeitos colaterais” de funções “legadas” da biblioteca C.

Lá vão, 3 motivos para você evitar strtok():

1) A função não é reentrante. Você a chama a primeira vez utilizando a cadeia de caracteres que deseja dividir, e as chamadas seguintes passando um ponteiro nulo. Se alguma outra parte de seu programa utilizar a função strtok, a sequência inicial será comprometida.

2) strtok() alter a cadeia de caracteres que está sendo dividida – sem te avisar. Este motivo justifica não utilizar strtok() em hipótese alguma.

3) A string de origem, sendo dividida, não pode ser constante, deve ser um bloco de memória alocado e que comporte as alterações que a função efetua na cadeia original.

Os dois seguintes trechos ilustram na prática estes 3 defeitos. Aliás, talvez este seja um post inútil, visto que você não deve usar strtok(). No entanto é um exemplo de função padrão mal implementada, presente em 100% das bibliotecas C do mundo, o que provavelmente representa praticamente 100% de todos os computadores em funcionamento no mundo hoje*.

Este programa gera um Bus Error ou Segmentation Fault, dependendo de sua implementação:

/*
 ============================================================================
 Name        : strtok.c
 Copyleft  : ZeFonseca.com
 Description : Demonstra efeitos colaterais de strtok()
 ============================================================================
 */

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

int main(void) {

	char *tok = NULL;

        // temp é uma cadeia estatica, e o espaço alocado em memória tem
        // apenas os caracteres necessários para
        // armazenar todos os caracteres, mais um caractere nulo não visível na mesma
	char *temp = "name:address:telephone:email";
       // somamos um para acomodar o caractere nulo ao final da cadeia
	int dll = strlen(temp) + 1; 

	tok = (char*)calloc(dll, 1);

	if ( tok == NULL ) {
		fprintf(stderr, "Unable to allocate %u bytes for token.\n", dll);
		return EXIT_FAILURE;
	}

       // primeira chamada de strtok() gera Bus Error, o programa não passará daqui
	tok = strtok(temp, ":");
        // a linha acima ilustra os problemas descritos nos itens 2 e 3 acima
	printf("%s\n", tok);
	fflush(stdout);

	while ( (tok = strtok(NULL, ":")) !=  NULL ) {
                // strtok é chamada com um ponteiro nulo a partir da 2a chamada
                // assim vemos que, caso qualquer outra função
                // chame strtok() em outra thread
                // a proxima iteracao deste loop retornara um valor sem sentido
                // aqui ilustramos o ponto 1 dos problemas de strtok()
		printf("%s\n", tok);
		fflush(stdout);
	}

	return EXIT_SUCCESS;
}

Ao tentar manipular a string temp, strtok gera um Bus Error ou Segmentation Fault. O motivo da falha pode não ser óbvia de inicio: strtok() está tentando manipular a cadeia original, e não uma cópia dela! strtok() insere caracteres nulos nos locais onde encontra a divisão da cadeia procurada. A cadeia original não é uma área dinamicamente alocada, e sim uma área estática de memória. Quando strtok() tenta alterá-la, o sistema operacional encerra o processo.

A seguinte versão corrige este problema, alocando um espaço muito maior que o necessário e trabalhando com uma cópia da cadeia original. Como podemos perceber, este programa está longe de ser eficiente, terminamos com 3 cópias da mesma informação ao final do processo: a string original, uma cópia para trabalho, e as substrings contendo os trechos buscados, caso existam(somadas formam a cadeia original).

/*
 ============================================================================
 Name        : strtok2.c
 Copyleft  : ZeFonseca.com
 Description : Desvia de efeitos colaterais de strtok() criando um frankenstein
 ============================================================================
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define STR_BUFF_SIZE 1024

int main(void) {
        // copiaremos a string para ca e dividiremos esta copia atraves de strtok()
	char *dataline  = NULL;
        // guardaremos a versao original para comparar posteriormente
	char *dataline2 = NULL;
	char *tok = NULL; // armazenara cada token obtido
	char *temp = "name:address:telephone:email"; // cadeia original para testes
        // comprimento da string + 1 para acomodar o byte nulo \0
	int dll = strlen(temp) + 1; 

	dataline = (char *)calloc(STR_BUFF_SIZE, 1);
	memcpy(dataline, temp, dll);

	dataline2 = (char *)malloc(STR_BUFF_SIZE);
	tok = (char*)calloc(STR_BUFF_SIZE, 1);

	if ( tok == NULL ) {
		fprintf(stderr, "Unable to allocate %u bytes for token.\n", dll);
		return EXIT_FAILURE;
	}

	memset(dataline2,0,dll);
	memcpy(dataline2, dataline, dll);

	printf("string 1 before: %s\n", dataline);
	printf("string 2 before: %s\n", dataline2);
        // strtok() nao causa Bus Error, pois dataline
        // tem espaço suficiente para acomodar
        // os efeitos colaterais de strtok()
	tok = strtok(dataline, ":"); 

	printf("%s\n", tok);
	fflush(stdout);

	while ( (tok = strtok(NULL, ":")) !=  NULL ) {
		printf("%s\n", tok);
		fflush(stdout);
	}

        // como strtok insere nulos na string,
        // este printf mostrara apenas o primeiro campo
	printf("string 1 after: %s\n", dataline);
        // a string original sera totalmente impressa
	printf("string 2 after: %s\n", dataline2); 

	return EXIT_SUCCESS;
}

Qual a alternativa? Diversos frameworks e bibliotecas fornecem alternativas seguras à strtok(). Procure evitar implementações da Microsoft(strtok_s) e outras variações como strtok_r. Em vez disso, use uma biblioteca bem implementada e bem testada como a glib.

* Praticamente todos os sistemas operacionais de ampla utilização são escritos em C e vão ligados à biblioteca padrão.