Programação genérica
Em resumo, a programação genérica permite definir algoritmos, sem concretizar no momento da sua definição, os tipos de dados sobre os quais operam.
A linguagem C dispõe dos seguintes mecanismos para realizar esse tipo de programação:
Ponteiro
*voidPonteiro para função
Função com número de parâmetros variável
Ponteiro *void
Não existem objetos do tipo void mas existem objetos do tipo void *.
São ponteiros para objetos sem tipo definido.
Utilizam-se para programar algoritmos do modo independente do tipo de dados concreto.
As funcões memcpy e memmove são exemplos destes casos. Servem para copiar
objetos tendo como referência os ponteiros para esses objetos.
Como, para efeitos de cópia, a diferença entre os objetos é apenas a quantidade de memória que ocupam,
o terceiro parâmetro – count – serve para fornecer essa informação.
Em termos algoritmicos estas funções operam como se um array de bytes de tratasse.
O primeiro parâmetro recebe o endereço de destino,
o segundo o endereço fonte e o terceiro a quantidade de bytes a copiar.
A diferença de funcionamento entre estas funções deve-se a razões de eficiencia.
A função memmove acautela a eventual sobreposição das zonas de memória em causa
e por isso é menos eficiente que memcopy.
A função memcpy só deve ser usada se as zonas de memória em causa não tiverem zonas de interseção.
void *memcpy(void *dest, const void *src, size_t count);
void *memmove(void *dest, const void *src, size_t count);
Exemplo
Deslocar cada elemento de um array para a posição seguinte. O elemento da posição 0 é copiado para a posição 1; o elemento da posição 1 é copiado para a posição 2 e assim sucessivamente até à penúltima posição. O último elemento é descartado.
Os elementos do array, desde a posição 1 até à última,
são copiados como um bloco de bytes.
Escolhe-se a função memmove porque as zonas de memória em causa não são disjuntas.
A dimensão deste bloco é calculada pela expressão sizeof array - sizeof array[0]
– a dimensão de array menos a dimensão de um elemento.
1#include <stdlib.h>
2#include <string.h>
3#include <stdio.h>
4
5#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))
6
7int array[] = { 20, 4, 6, 2, 325, 24, 45};
8
9int main() {
10 for (int i = 0; i < ARRAY_SIZE(array); ++i)
11 printf("[%d] - %d\n", i, array[i]);
12 putchar('\n');
13
14 memmove(array + 1, array, sizeof array - sizeof array[0]);
15
16 for (int i = 0; i < ARRAY_SIZE(array); ++i)
17 printf("[%d] - %d\n", i, array[i]);
18}
Ponteiro para função
Um ponteiro para função pode ser afetado a uma variável, passado como argumento ou devolvido como valor de função.
É utilizado para invocar uma operação de forma indireta.
Exemplos de aplicação deste método são as funções de biblioteca qsort e bsearch,
em que se parametriza o critério de comparação dos elementos de um array com vista à ordenação
ou à procura.
Nestas funções é também aplicado um método de definição de array genérico com base em três componentes: ponteiro inicial; número de elementos; dimensão dos elementos.
void qsort( void* ptr, size_t count, size_t size,
int (*comp)(const void*, const void*) );
void* bsearch( const void *key, const void *ptr, size_t count, size_t size,
int (*comp)(const void*, const void*) );
Exemplo
No exemplo da Listagem 73 a função qsort é utilizada para ordenar um array de inteiros.
O critério de comparação é programado na função específica, cmp_int cujo ponteiro é passado como último argumento
na invocação de qsort.
Esta função é invocada em callback sempre que o algoritmo determina a comparação de elementos do array, recebemdo como argumentos os ponteiros para as posições a comparar.
A função de comparação é programada no contexto do conhecimento do tipo de dados em causa.
Os seus parâmetros formais, do tipo void *, são convertidos explicitamente
para ponteiros para o tipo dos elementos do array.
No exemplo, os parâmetros a e b são convertidos para int * nas linhas 10 e 12.
1#include <stdlib.h>
2#include <string.h>
3#include <stdio.h>
4
5#define ARRAY_SIZE(a) (sizeof(a) / sizeof(a[0]))
6
7int array[] = { 20, 4, 6, 2, 325, 24, 45};
8
9int cmp_int(const void *a, const void *b) {
10 if (*(int *)a > *(int*) b)
11 return 1;
12 else if (*(int *)a < *(int*) b)
13 return -1;
14 return 0;
15}
16
17int main() {
18 for (int i = 0; i < ARRAY_SIZE(array); ++i)
19 printf("%d\n", array[i]);
20 putchar('\n');
21 qsort(array, ARRAY_SIZE(array), sizeof(array[0]), cmp_int);
22
23 for (int i = 0; i < ARRAY_SIZE(array); ++i)
24 printf("%d\n", array[i]);
25}
Exercício
Modifique o programa da Listagem 73 de modo a exemplificar a ordenação de um array de ponteiros para strings pelo critério da ordem alfabética do conteúdo das strings.
Função com número de parâmetros variável
A função com número de parâmetros variável mais conhecida é a função printf,
com a seguinte assinatura:
int printf(const char *format, ...);
As reticências significam que a função pode ser invocada com qualquer número e tipo de argumentos,
depois do argumento do parâmetro format.
Como é que a função printf «sabe» quantos e de que tipo são os argumentos
que lhe foram passados numa dada invocação?
É pela análise da string passada no argumento format. As definições de conversão
marcadas com %, caraterizam os retantes argumento a processar, em número e em tipo.
- O acesso aos argumentos da zona variável faz-se segundo o seguinte modelo:
os argumentos constituem uma sequência;
a macro
va_startinicializa um ponteiro para o inìcio da sequência tomando como referência o último parâmetro fixo;o valor dos argumentos é obtido através da macro
va_arg;a macro
va_argalém de extrair o valor do argumento corrente avança para o próximo argumento.
Exemplo
1#include <stdarg.h>
2
3int sum(int n, ...) {
4 if (n <= 0)
5 return 0;
6 va_list ap;
7
8 va_start(ap, n);
9 int result = va_arg(ap, int);
10 for (int i = 1; i < n; ++i)
11 result += va_arg(ap, int);
12 va_end(ap);
13 return result;
14}
15
16int main() {
17 int a = sum(2, 10, 10);
18 int b = sum(3, 10, 20, 30);
19}
A função sum realiza a adição dos valores de uma sequência variável de valores inteiros.
A dimensão da sequência é indicado pelo parâmetro n.
Na linha 8, a variável ap é inicializada por va_start(ap, n) tomando n como referência.
Na linha 11, cada um dos valores a adicionar é obtido por va_arg(ap, int).
ap é avançado para a posição seguinte.
O argumento int indica o tipo do valor a extarir.
A título ilustrativo, apresenta-se uma definição simplificada (e imprecisa) das macros va_start e va_arg:
#define va_start(ap, fixed) ap = &fixed + 1
#define va_arg(ap, type) *((type) *)ap++)
No final, a macro va_end deve ser invocada, após o que, o ponteiro ap fica indefinido.
Exemplo
Outro exemplo bem ilustrativo do funcionamento dos mecanismos de processamento de lista de parâmetros variável é a programação da função printf. Da qual se apresenta uma versão reduzida da variante snprintf.
Destaca-se a invocação da macro va_arg nas linhas 23, 33, 43 e 52.
O tipo do valor a extrair, indicado como segundo argumento, depende do especicicador %.
Este argumento determina também a amplitude do avanço de ap.
1#include <stdarg.h>
2#include <stdlib.h>
3#include <string.h>
4
5static size_t int_to_string(unsigned value, int base, char buffer[], size_t buffer_size);
6
7int mini_snprintf(char *buffer, size_t buffer_size, const char *fmt, ...) {
8 if (buffer_size == 0)
9 return 0;
10 va_list ap;
11 va_start(ap, fmt);
12 char *str = buffer;
13 size_t str_size = 0;
14 for (const char *p = fmt; *p && buffer_size > 1; p++) {
15 if (*p != '%') {
16 *str++ = *p;
17 str_size++;
18 buffer_size--;
19 continue;
20 }
21 switch (*++p) {
22 case 'd': {
23 int ival = va_arg(ap, int);
24 size_t nbytes = int_to_string(ival, 10, str, buffer_size);
25 if (nbytes == 0)
26 goto ret;
27 str_size += nbytes;
28 str += nbytes;
29 buffer_size -= nbytes;
30 break;
31 }
32 case 'x': {
33 int ival = va_arg(ap, int);
34 size_t nbytes = int_to_string(ival, 16, str, buffer_size);
35 if (nbytes == 0)
36 goto ret;
37 str_size += nbytes;
38 str += nbytes;
39 buffer_size -= nbytes;
40 break;
41 }
42 case 'c': {
43 char cval = va_arg(ap, int);
44 if (buffer_size < 2)
45 goto ret;
46 *str++ = cval;
47 str_size++;
48 buffer_size--;
49 break;
50 }
51 case 's': {
52 char *sval = va_arg(ap, char *);
53 size_t str_len = strlen(sval);
54 if (buffer_size < str_len + 1)
55 goto ret;
56 strcpy(str, sval);
57 str += str_len;
58 str_size += str_len;
59 buffer_size -= str_len;
60 break;
61 }
62 default:
63 *str++ = *p;
64 str_size++;
65 buffer_size--;
66 }
67 }
68ret:
69 *str = '\0';
70 va_end(ap);
71 return str_size;
72}
Variante vprintf
A lista de argumentos variável pode ser propagada como argumento de outras funções sem ser processada localmente.
Para que isso aconteça, é necessário que a função invocada receba como parâmetro fixo
um ponteiro do tipo va_list.
As variantes de printf prefixadas com «v» são casos de funções que recebem uma lista de parâmetros variável pertencente a outra função.
int vprintf(const char* format, va_list vlist);
Exemplo
A função trace escreve em stdout uma string de informação etiquetada com uma referência de tempo.
Depois de escrever a indicação de tempo – linha 11 –
invoca vprintf para escrever a informação,
passando a lista de argumentos variável através do parâmetro vlist.
1#include <stdio.h>
2#include <stdarg.h>
3#include <time.h>
4
5void trace(const char *format, ...) {
6 struct timespec time;
7 timespec_get(&time, TIME_UTC);
8
9 va_list ap;
10 va_start(ap, format);
11 printf("[%ld.%ld] ", time.tv_sec, time.tv_nsec / 1000000);
12 vprintf(format, ap);
13 putchar('\n');
14 va_end(ap);
15}
16
17
18int main() {
19 trace("Informação = %d", 123);
20}
Referências
The C Programming Language, secçãos 5.11, secção 7.3