O tipo void

Em 6 de abril de 2012, em Linguagem C, por ronaldo

Existe um tipo de dados na linguagem C que causa muita confusão em quem está aprendendo a usar a linguagem: o tipo void. Como o próprio nome diz, o tipo de dados void é um tipo nulo, ou seja, ele pode ser qualquer tipo. O seu principal uso é para a criação de algoritmos que tratam com informações arbitrárias para as quais o tipo de dados não é conhecido de antemão.

Como a linguagem C não permite a programação genérica tipada, como ocorre com os templates em C++, a forma de realizar a programação genérica em C é através de ponteiros para tipos genéricos, ou seja, ponteiros para void.  A principal desvantagem de usar este tipo de dados é o fato que está na responsabilidade do programador controlar os tipos de dados dentro do programa, não sendo delegada esta função ao compilador como ocorre com a linguagem C++.

No entanto, a falta da checagem de tipos pode ser muito vantajosa pois permite que exatamente a mesma rotina trate diversos tipos de dados diferentes. No caso dos templates em C++, efetivamente teremos código gerado pelo compilador para cada tipo de dado utilizado, o que torna o programa final um pouco mais “gordo”.

Para entender por que o tipo void é vantajoso, aqui está um pequeno exemplo de como tirar vantagem dele. Seja uma lista encadeada simples na qual você queira colecionar informações de forma a acessá-las de maneira linear. Uma sugestão de implementação é algo como segue:

typedef struct linked_list {
    struct linked_list *next;
    void *data;
} linked_list;

O campo data é um ponteiro para void. Isso significa dizer que pode-se referenciar ali qualquer tipo de dado válido em linguagem C, seja integral ou estruturado. As funções que manipulam a lista ligada não precisam ter conhecimento do tipo de dado colocado dentro da lista. Afinal, quem for manipular a lista será o responsável por lidar com as informações.

Dica do dia: diretiva Warning

Em 20 de março de 2012, em Linguagem C, por ronaldo

Para quem programa em C há uma diretiva de pré-processamento muito interessante: a diretiva warning. Esta diretiva, quando encontrada no código, gera uma advertência que pode ser rastreada facilmente. Mas por que forçar uma advertência no código?

Há vários usos para isso. Você pode usar isso para informar a um usuário que determinado símbolo necessário para uma API que você escreveu não foi definido ou para algo mais simples, como deixar marcas no código para verificação posterior.

Particularmente uso advertências para me lembrar de algo que eu deveria ter feito no código e que não fiz: melhoramentos, tratamentos de erro, por aí vai. Uma forma interessante de criar uma marca no código é esta:

#warning TODO: Melhorar este código visando eficiência

Este pequeno exemplo irá cuspir uma advertência indicando que algo ficou para trás. A principal vantagem disso é que você nunca vai se esquecer de algo no código pois o compilador sempre vai te avisar.

Marcado com:  

Um pouco sobre ponteiros

Em 18 de novembro de 2011, em Codexis, por ronaldo

Aqui vai uma implementação simples da função strlen, que só fica compacta devido ao uso de ponteiros.

ptrdiff_t strlen (const char * str) {
    const char * p = str;
    while (*p) ++p;
    return p - str;
}

Observe que p é incrementado até atingir o fim da cadeia de caracteres. Como p é do tipo char, fazer pstr retorna a quantidade de caracteres entre a posição de p, que agora está deslocado da quantidade de caracteres, e a posição de str que é o início da cadeia de caracteres.

O tipo de dados ptrdiff_t é, na verdade, do tipo inteiro sem sinal. Segundo a especificação ANSI C, a diferença entre ponteiros deve ser expressa nesse tipo de dado. Porém, para esta rotina em particular, é totalmente cabível expressar o resultado da diferença pstr em termos do tipo size_t, o que daria, inclusive, uma semântica mais clara à função. Portanto, o cabeçalho da função ficaria:

size_t strlen (const char * str);

A semântica fica mais clara, pois strlen calcula o tamanho de uma cadeia de caracteres e nada mais claro do que ter esta função retornando um tamanho, expresso em termos de um tipo size_t.

Não confunda API com linguagem

Em 18 de novembro de 2011, em Codexis, por ronaldo

Não é difícil achar programadores que confundem API com a definição de uma linguagem de programação. Há pessoas que acham que clearscr é uma função da linguagem C e que <conio.h> é um arquivo cabeçalho padrão. A linguagem C provavelmente foi a primeira linguagem a causar confusão sobre o que é a API para um determinado sistema operacional ou sub-sistema e as bibliotecas definidas para a própria linguagem.

Não somente isso, mas há ainda confusão entre o que é definição da linguagem e o que é biblioteca padrão da linguagem. Por exemplo, print não faz parte da definição da linguagem C, justamente por ser uma função definida da biblioteca stdio, cujo cabeçalho é definido em <stdio.h>.

Muitos programadores acham que isso é conhecimento desnecessário e assim permanecem fazendo confusão. E essa confusão se torna evidente quando precisam portar código de uma plataforma para outra. É comum ver programadores escrevendo wrappers para arquivos de cabeçalho como conio.h em ambientes onde este arquivo não está disponível.

Essa confusão tornou-se ainda maior depois que linguagens independentes de plataforma, como o Java, Python e TCL, surgiram com uma ampla API disponível para qualquer sistema no qual exista um intérprete. Por exemplo, há programadores que acham que JFrame faz parte da definição da linguagem Java, sendo que esse objeto faz parte da definição da API gráfica que acompanha a linguagem.

 

Marcado com:  

Tópicos de C: dados do parser de arquivos .ini

Em 7 de novembro de 2011, em Codexis, por ronaldo

Antes de falarmos sobre qual estrutura de dados criar é importante ter em mente que não existe resposta certa ou errada para o que estou propondo. O fato é que a solução para a descrição de um arquivo ini vai depender da experiência do programador, do seu conhecimento e da sua prática. A estrutura de dados pode ficar complexa, dependendo do tipo de operação que se deseje executar. É importante ter isso em mente pois cada pessoa tem um ponto-de-vista significativamente distinto quando avaliando um problema e várias soluções podem surgir.

Se a solução é certa ou errada vai depender de fatores como tempo de desenvolvimento, recursos necessários para a implementação, ambiente de execução e outras restrições de projeto que podem surgir ao longo do caminho. O que quero chamar a atenção é que deve-se ouvir todas as proposições de solução antes de decidir qual será adotada.

Como critério, tenho a tendência de usar a mais fácil de ser implementada. Normalmente, soluções simples levam a código simples, compacto e eficiente. Bem, chega de conversa. Vamos à definição das estruturas de dados.

Para o usuário da biblioteca, é importante que ele tenha em mãos um descritor do arquivo de configurações. Este descritor permitirá que a biblioteca seja utilizada com diversos arquivos distintos. No entanto, queremos que o usuário da biblioteca use somente nossas funções para manipular os dados em memória. Assim, o descritor deverá ser algo opaco, declarado dessa forma:

typedef struct ini_cfg_file_t ini_cfg_file_t;

Esta declaração deverá ser criada dentro do arquivo de cabeçalho público da aplicação. Note que não definimos o conteúdo da estrutura. Isso, na verdade, é um truque para forçar o compilador a verificar os tipos sem, no entanto, revelar o conteúdo da estrutura. Obviamente que quando escrevermos as funções que manipulam esta estrutura,  definiremos os campos da estrutura. Afinal, o compilador não é adivinho.

O conteúdo de um arquivo ini é claramente hierárquico, ou seja, há pares chave-valor dentro de uma seção. Assim, poder-se-ia descrever a estrutura de um arquivo ini como sendo chaves e valores e, por fim, seções:

typedef struct {
  const char *key;
  const char *value;
} ini_cfg_keypair_t;

typedef struct {
  const char *section;
  ini_cfg_keypair_t *pairs;
} ini_cfg_section_t;

Aqui definimos duas estruturas: uma para descrever os pares chave-valor e outra para descrever uma seção. A seção é composta por um nome e depois por um vetor de pares. Dessa forma temos uma maneira extremamente simples de descrever as informações. A única coisa que nos falta descrever é o arquivo em si, o descritor ini_cfg_file_t:

typedef struct {
  ini_cfg_section_t *sections;
} ini_cfg_file_t;

A descrição do arquivo deixa clara a principal intenção do código: manter todo o arquivo ini em memória. Esta abordagem pode não ser boa se o arquivo ini for muito grande. No entanto, considerando que um arquivo de configurações não ultrapassa mais do que alguns quilobytes, esta abordagem é válida.

Hospedado nos servidores da Saibre Tecnologia da Informação Limitada