Linguagem C - Funções Variádicas

Artigo com o intuito de demonstrar o funcionamento de funções com número variável de argumentos. As famosas funções declaradas como func(int, ...).

[ Hits: 14.743 ]

Por: Enzo de Brito Ferber em 20/04/2016 | Blog: http://www.maximasonorizacao.com.br


Expandindo horizontes



As funções scanf() e printf() estão entre as funções mais complexas da libc. Reconstruí-las do zero seria, no mínimo, burrice. Elas já existem a séculos e possuem *inúmeros* fixes/hacks para suportarem quase todos os tipos de esquisitices possíveis, bem como uma dependência da libc que será difícil de implementar em um programa pequeno... Entretanto, podemos implementar wrappers ou outras funções baseados nelas que podem ser bem úteis - como a função debug() da página anterior.

Quando construímos grandes aplicações em C orientadas ao usuário, uma grande parte da aplicação será a impressão de um valor e logo em seguida a leitura da respectiva variável. Para os WebDesigners, a dupla dinâmica <label> + <input>.

printf("Nome: "); fflush(stdout);
fgets(registro->nome, TAM_NOME, stdin);

printf("Telefone: "); fflush(stdout);
fgets(registro->telefone, TAM_TELEFONE, stdin);

Podemos escrever uma simples função que faça isso:

void label(char *l, char *var, size_t tam)
{
   printf(l); fflush(stdout);
   fgets(var, tam, stdin);
}

...

label("Nome: ", registro->nome, TAM_NOME);
label("Telefone: ", registro->telefone, TAM_TELEFONE);

Bom. Mas e se quisermos usar a função label para ler inteiros também? Agora complica um pouco. Podemos alterar a lista de parâmetros e colocar um campo adicional chamado "type", e mudar o tipo de "var" para "void" e mudar o fgets para um scanf.

Pensando bem, agora precisamos também de uma função para construir a string de formato do scanf(). Também temos um parâmetro inútil 50% das vezes. Pense: qual o motivo de ter um parâmetro de tamanho para leitura de números?

Tente escrever a versão que suporta a leitura de strings e inteiros. Depois, tente escrever uma outra versão que inclua caracteres, floats e doubles. Depois short ints, long doubles.... Deu pra entender onde quero chegar?

Essa situação fica pior ainda quando precisarmos alterar o label baseado em uma outra variável. Imagine que você vá ler 10 registros, e queira mostrar ao usuário da forma mais prolixa possível qual o número do registro atual. Algo como:

for(i = 0; i < REGISTROS; i++) {
   printf("Digite o nome do registro %d: "); fflush(stdout);
   fgets(registros[i]->nome, TAM_NOME, stdin);

   ...
}

E agora, como construímos label? Adicionamos outra variável aos parâmetros, que quando for zero, significa que não há nenhuma informação variável no label. Quando for maior que zero, significa que há um número definido de números (no pun intended) no label.

Agora temos algo como:

#define INT   1
#define CHAR   2
#define FLOAT   3
...

void label(char *l, int label_n, int type, void *addr, size_t n)
{
   ...
}

...

label("Digite %d: ", 1, INT, &int_var, 0);

Espera, e se eu quiser alterar meu label de acordo com uma variável float? Ou com outra string? Deu pra entender aquele negócio de arquitetura de interfaces, né? Pra resolver isso, vamos criar uma função que analise uma string de formato e possa ter quantos argumentos quisermos. Precisamos definir uma sintaxe que permita diferenciar entradas de saídas.

Como write() tem 5 letras, utilizamos % para o que for saída, igual ao formato do printf() e scanf(). read() tem 4 letras, então utilizamos o $ (cifrão). A função mostrada aqui irá entender apenas números inteiros, strings e números binários. Decidi colocar o suporte a números binários pois sempre achei que printf() deveria ter isso... E de quebra, dá pra entender como as funções printf() e scanf() implementam mecanismos para ler um número, um hexa, um float etc.

Ou seja, podemos chamar a função assim:

label("Nome %d: $s", i, registro[i]->nome);

Os mais espertinhos notaram que não há especificação do tamanho da string. Há, entretanto, um mecanismo implementado para detectar tamanhos dos parâmetros na função input. Portanto, você pode fazer algo como:

label("Nome %4d: $30s", i, registro[i]->nome);

Na implementação mostrada aqui, isso faz... nada. A função só reconhece e sabe que existe o número 4 antes de 'd' e o número 30 antes do 's'. Porém há uma implementação bem interessante para manipulação de números binários reconhecendo o número de bits(size). Vendo este mecanismo você poderá deduzir e implementar sua própria rotina de tratamento de tamanhos de strings e quantos outros mecanismos quiser copiar de printf(). Trocar o scanf() atual por um fgets() e usar 'size' não é difícil. Fica como exercício.

Você pode escrever algo como:

label("Binario de %d e %8b ", 64, 64);
// Binario de 64 e 01000000

label("Binario de %d e %16b ", 64, 64);
// Binario de 64 e 0000000001000000

label("Binario de %d e %4b ", 4, 4);
// Binario de 4 e 0100

E pode ler dados da seguinte forma:

unsigned long long int bin;

label("Escreva um numero binario: $4b", &bin);
// Escreva um numero binario:

label("Binario de %d e %4b ", bin, bin);
// Binario de 15 e 1111

Sem mais papo furado, o código de label():

int label(const char *fmt, ...)
{
   int i = 0;
   const char *s = fmt;
   char c;
   va_list ap;
   uint64 size = 0;

   va_start(ap, fmt);

   while((c = *s++)) {
      switch(c) {
      case '%':
         if(*s == '%') putchar('%');

         size = 0;
         while(isdigit(*s)) {
            size *= 10;
            size += *s++ - '0';
         }

         if(*s == 'd') printf("%d", va_arg(ap, int));
         else if(*s == 's') printf("%s", va_arg(ap, char *));
         else if(*s == 'b') {
            char *bin;
            uchar bits = size ? (uchar)size : 8;

            bin = dec_to_bin(va_arg(ap, uint64), bits);
            printf("%s", bin);
            free(bin);
         }

         i++;
         s++;
         break;

      case '$':
         /* putchar, printf sao bufferizados */
         fflush(stdout);

         if(*s == '$') putchar('$');

         size = 0;
         while(isdigit(*s)) {
            size *= 10;
            size += *s++ - '0';
         }

         if(*s == 'd') scanf("%d", va_arg(ap, int *));
         else if(*s == 's') scanf("%s", va_arg(ap, char *));
         else if(*s == 'b') {
            uint64 *n = va_arg(ap, uint64 *);
            uint64 bits = size ? size : 8;
            char bin[65];

            read(0, bin, bits + 1);
            bin[bits] = 0x0;

            *n = bin_to_dec(bin, bits);
         }

         i++;
         s++;
         break;

      default:
         putchar(c);
      }
   }

   va_end(ap);

   return i;
}

E as funções para conversão de decimal para binário e vice-versa:

typedef unsigned long long int uint64;
typedef unsigned char uchar;

char *dec_to_bin(uint64 n, uchar bits)
{
   uint64 i;
   char *s = malloc(bits + 1);
   if(!s) return NULL;

   for(i = 1LLU << (bits - 1); i >= 1; i >>= 1)
      *s++ = !!(n & i) + '0';

   *s = 0x0;
   return (s - bits);
}

uint64 bin_to_dec(char *bin, uchar bits)
{
   uint64 mask,r = 0LLU;

   if(strlen(bin) != bits) return 0;

   for(mask = 1LLU << (bits - 1); mask >= 1; mask >>= 1)
      r |= (*bin++ - '0') ? mask : 0;

   return r;
}

Vale a pena notar que a função printf() não usa a função printf() para impressão de números e strings como nossa função (sério, Sherlock?!). Ela usa um buffer(stdout) e só imprime na tela quando encontra um caractere de nova linha (se defindo assim). Ou de acordo com alguma condição definida por setvbuf(3). Vale a pena ler o manual.

Então, como ela escreve? Depende do tipo de bufferização, mas a chamada usada é write(2). A nossa não utiliza write(2) por simplicidade. Como um exemplo rápido, ela serve ao propósito de demonstrar uma boa aplicação de funções variádicas com uma funcionalidade interessante, e sem ser demasiadamente complexa.

A implementação real de printf(3), além de ser bem complexa, depende do ambiente de execução C, e isto é construído através de uma série de chamadas de funções *antes* de main() ser chamada. A função printf() também utiliza um mecanismo similar para analisar a string de formato e determinar argumentos e seus tipos etc. Este mecanismo foi simplificado aqui, mas é uma boa base de como o parsing dessas funções funciona.

Página anterior     Próxima página

Páginas do artigo
   1. Introdução
   2. stdarg.h
   3. Exemplos
   4. Expandindo horizontes
   5. MACROS Variádicas
   6. Conclusão e referências
Outros artigos deste autor

Linguagem C - Árvores Binárias

Linguagem C - Listas Duplamente Encadeadas

Leitura recomendada

A mágica do polimorfismo e seus conceitos na ótica de C/C++

O Produtor e o Consumidor

Bug afeta todas as distros

A poderosa nuvem: Intel® DevCloud com GPU Iris Xe Max!

Projeto Icecream (parte 1)

  
Comentários
[1] Comentário enviado por sacioz em 21/04/2016 - 10:15h

Gostei , com certeza vou dar um controldê na pag.inicial
Obrigado...:-))

[2] Comentário enviado por removido em 21/04/2016 - 18:14h

Muito bom. Vou guardar como referência.

----------------------------------------------------------------------------------------------------------------
# apt-get purge systemd (não é prá digitar isso!)

Encryption works. Properly implemented strong crypto systems are one of the few things that you can rely on. Unfortunately, endpoint security is so terrifically weak that NSA can frequently find ways around it. — Edward Snowden

[3] Comentário enviado por EnzoFerber em 30/04/2016 - 12:49h

Muito obrigado, @sacioz e @listeiro_037.


Contribuir com comentário




Patrocínio

Site hospedado pelo provedor RedeHost.
Linux banner

Destaques

Artigos

Dicas

Tópicos

Top 10 do mês

Scripts