Cuidado com as expressões…

Uma “linguagem de programação” geralmente é composta de sequências de operações, que são divididas em expressões aritméticas e operações de controle de fluxo (if..then..else, while, do…while, …). Humpf… E já ouvi gente dizendo que “programação nada tem a ver com matemática!”…

Como na aritmética e na álgebra, expressões devem ser avaliadas de acordo com a “importância” do operador. Por exemplo, ao fazermos x+y\cdot z, devemos multiplicar y e z antes de adicionarmos x. A isso damos o nome de precedência (no sentido de “o que vem antes”). Mas, como não temos apenas as 4 operações fundamentais da álgebra (+, -, × e ÷), todos os operadores têm que seguir alguma regra de precedência… Na linguagem C temos a seguinte tabela:

Ordem de precedência Operador Associatividade
1 ++, — (pós-incremento e pós-decremento)
() (chamada de função)
[]
.
->
(T){list} (composição)
E→D
2 ++, — (pré-incremento e pré-decremento)
+, – (unários)
!
~
(T) (casting)
* (indireção)
& (“endereço de”)
sizeof
_Alignof
D→E
3 *, /, % E→D
4 +, –
5 <<, >>
6 <, <=, >, >=
7 ==, !=
8 &
9 ^
10 |
11 &&
12 ||
13 ?: D→E
14 =, +=, -=, *=, /=, %=
&=, |=, ^=
<<=, >>=
15 , E→D

Quanto menor for a ordem de precedência, maior é o grau de precedência do operador, ou seja, ele será avaliado antes. Mas, como o leitor pode observar, isso não é suficiente. O termo “associatividade” significa “de que lado” devemos começar. Operadores como + e -, por exemplo, têm associatividade da esquerda para direita (E→D), significando que a sub expressão do lado esquerdo do operador deve ser avaliada antes da do lado direito.

Pôxa! Temos que decorar esses trecos de associatividade também? Não! Note que, além dos operadores de assinalamento, somente os operadores unários (que só têm um único argumento à direita) têm associatividade D→E, e isso faz todo sentido! As únicas exceções são os pós-incrementos e pós-decrementos, chamadas de função, arrays, e o pseudo-operador condicional ?:, que é problemático (às vezes) e recomendo (pela primeira vez neste texto) que seja evitado. Todos os outros operadores têm associatividade da esquerda para direita.

Note também que os operadores de assinalamento (= e seus primos) têm a mais baixa precedência de todos, exceto pelo operador vírgula e, portanto, serão avaliados por último. Por exemplo, numa expressão como:

*p = x + 1;

Temos 3 operadores: O de indireção (*), o de atribuição (=) e a adição (+). Nessa expressão o operador de indireção tem maior precedência (menor ordem), a adição tem precedência menor, mas a atribuição tem a mais baixa (maior ordem). Podemos entender a expressão acima como (*p)=(x+1). De acordo com a associatividade das operações e sabendo que o assinalamento é feito por último, a sub expressão da direita de = é resolvida primeiro e depois a expressão do lado esquerdo. Só então a atribuição é feita.

Existe, é claro, outra forma de entender isso: Uma expressão pode ser organizada numa árvore binária que pode ser percorrida de forma pós-ordenada (visita esquerda, visita direita, visita raiz). É um pouco mais complexo do que isso devido a associatividade, mas em essência, para compreensão, podemos manter essa analogia… Nessa árvore, os operadores com menor precedência são colocados no topo da árvore e os com maior, mais perto dos nós folha…

Isso parece simples, mas note o problema no fragmento de código abaixo:

int x;

x = 1 << 3 + 1

Qual será o resultado? 9 [de (1 << 3)+1] ou 16 [de 1 << (3+1)]? Vejamos: Vimos que = tem precedência mais baixa e associatividade da direita para esquerda, então a sub expressão à sua direita será resolvida primeiro, ou seja, 1 << 3 + 1. Aqui, tanto o operador << quanto + têm a mesma regra de associatividade (da esquerda para direita), mas + precede <<. Assim, a sub expressão fica (1 << (3 + 1)). O resultado é 16.

À primeira vista, pode parecer que o shift lógico tem maior prioridade que a adição ou, pelo menos, a mesma. Parece que o programador queria fazer um shift e depois somar 1. Entender como funciona o esquema precedência e associatividade é importante para evitar esse tipo de problema, senão você terá que usar parenteses em todas as suas expressões (o que não é má ideia, em caso de dúvida).

Outro exemplo está nas comparações. Note que os operadores lógicos (tanto binários [&. | e ^] quando os “lógicos” [&& e ||]) tem precedência menor que comparações (==, !=, <, , >=). Daí, ambas as construções abaixo são idênticas:

if (x == 0 && y == 0) ...
if ((x == 0) && (y == 0)) ...

Substitua == por = e temos uma inversão de precedências, ou seja, o = é feito por último… A expressão x = 0 && y = 0 fica x=((0 && y)=0), o que gerará um erro de LVALUE. Lembre-se, se && tem maior precedência que =, então ele é executado primeiro…

O que diabos é um LVALUE? Bem… operadores de atribuição esperam que o que esteja do lado esquerdo da expressão seja um objeto onde um valor possa ser armazenado. A sub expressão do lado direito criará uma atribuição do tipo 0=0 e não é possível armazenar o valor zero dentro de outro valor zero…

Ainda outro ponto de nota é que cada operador espera um conjunto de argumentos (ou operandos). Falo conjunto porque alguns operadores precisam de apenas um argumento (todos os operadores de ordem de precedência 1 e 2, na tabela acima). O restante, precisa de dois argumentos, exceto pelo operador condicional (?:), que precisa de 3… mas esse, tecnicamente, não é bem um operador e tem lá seus problemas com precedência e associatividade (recomendo evitá-lo, pela segunda vez neste texto).

Ainda falando de precedência, vale relembrar sobre os operadores de maior precedência . e ->, bem como citar o operador de indireção (*), com precedẽncia imediatamente inferior. O problema de misturar o operador de resolução de membros de estruturas (.) com o indireção (quando usamos ponteiros) é que a coisa não funciona bem como pode ser sua intenção. A expressão *p.x = 1;, sendo x um membro de uma estrutura qualquer apontada por p não faz o que se espera dela. Por ‘.’ ter precedência maior que ‘*’, a expressão pode ser entendida como *(p.x) = 1;, ou seja, estamos dizendo que o membro x é que é o ponteiro, não p. Para fazermos o que queríamos, temos que resolver esse problema de precedência com o uso de parenteses: (*p).x = 1;.

Acontece que ficará meio chato ter que fazer isso toda vez que usarmos o ponteiro p para essa (e outras) estruturas. Daí o operador ‘->’ foi criado e ele espera ter, no seu lado esquerdo, um ponteiro. O operador dereferencia o ponteiro sem a necessidade do uso do operador ‘*’. Daí, a expressão p->x = 1; é a mesma coisa que (*p).x = 1;.

Outro problema para sua consideração: Note que casting (promoção de tipo) também é um operador e tem precedência menor, por exemplo, do que um pós-incremento. Isso quer dizer que uma expressão do tipo (char *)p++ pode não fazer exatamente o que você espera… Suponha que p seja do tipo int *. A expressão ao lado vai adicionar sizeof(int) ao ponteiro e depois converter a expressão para o tipo char *. Se você quisesse adicionar sizeof(char) ao ponteiro, terá que, necessariamente, fazer: ((char *)p)++. Isso é fácil demonstrar:

/* test.c */
#include <stdio.h>

void main(void)
{
  static int x[2] = { 1, 2 };
  int *p, *q;
  char c;

  p = q = x;
  c = *(char *)p++;

  printf("(%p) %p -> 0x%02hhx\n", q, p, c);
}

Ao compilar e executar:

$ cc -o test test.c
$ ./test
(0x601038) 0x60103c -> 0x01

E, por último, nem todos os operadores lógicos tem precedência baixa. A exceção são os operadores de negação (! e ~). Isso garante que expressões como !a == b realizem a negação lógica do argumento a apenas, ao invés de toda a sub expressão à direita de !. Compare isso com expressões como a == b && b == c

Anúncios

Ponteiro para um array…

Eu evito algumas construções mais “esotéricas” da linguagem C, não porque não saiba o que elas signifiquem, mas em virtude da legibilidade… Recentemente me perguntaram o que significa a declaração:

int (*p)[4];

Bem… isso ai é um ponteiro para um array de 4 inteiros. O que é bem diferente de:

int *p[4];

Que é um array de 4 ponteiros para o tipo int.

O detalhe é que deixei meio de lado a utilidade da primeira declaração. Considere isso:

int a[4][4] =
  { { 1, 2, 3, 4 },
    { 5, 6, 7, 8 },
    { 9, 0, 1, 2 },
    { 3, 4, 5, 6 } };

void showarray(int a[][4])
{
  int i, j;

  for (i = 0; i < 4; i++)
  {
    for (j = 0; j < 4; j++)
      printf("%d ", a[i][j];
    putchar('\n');
  }
  putchar('\n');
}

A rotina showarray, como declarada acima, é um jeito de mostrar todos os itens de um array. Podemos chamá-la com showarray(a); e tudo funcionará perfeitamente bem… Mas, e se quiséssemos usar ponteiros ao invés de 2 índices (i e j)?

Note que, se você tem um array simples, pode usar um ponteiro para apontar para o primeiro item e incrementar o endereço contido no ponteiro para obter o próximo item, como em:

int a[] = { 1, 2, 3, 4, 5, 0 };

void showarray(int *p)
{
  for (; *p; p++)
    printf("%d ", *p);
  putchar('\n');
}

Aqui, o compilador criará código para somar 4 ao conteúdo de p a cada vez que ele for incrementado. O valor 4 é adicionado porque sizeof(int)==4. A pergunta original é: “Dá pra fazer algo semelhante com arrays bidimensionais?”. A resposta, obviamente, é: SIM!

Ao declarar um ponteiro p como int (*p)[4];, toda vez que você incrementar o ponteiro, a ele será adicionado o valor 16 (4*sizeof(int)) e podemos escrever a rotina showarray original, assim:

void showarray(int (*p)[4])
{
  int i;

  // Não é bem a rotina original, aqui considero que
  // se o primeiro item da "linha" for zero, devemos
  // encerrar o loop.
  //
  // p++ fará o ponteiro p apontar para o início da
  // próxima linha...
  for (; (*p)[0]; p++)
  {
    for (i = 0; i < 4; i++)
      printf("%d ", (*p)[i]);
    putchar('\n');
  }
  putchar('\n');
}

O array original, é claro, deverá ser definido assim, para a rotina funcionar:

int a[][4] =
  { { 1, 2, 3, 4 },
    { 5, 6, 7, 8 },
    { 9, 0, 1, 2 },
    { 3, 4, 5, 6 },
    {} };  // note: 4 zeros aqui!

Mais um exemplo: Nem sempre assembly é a resposta!

Já escrevi sobre isso aqui: Criar suas próprias rotinas em assembly não é uma panaceia para a obtenção de alta performance. É possível acabar com uma rotina pior do que a gerada pela linguagem de alto nível. Especialmente se estivermos falando de compiladores que oferecem recursos de otimização.

Para demonstrar, consideremos a simples rotina de selection sort, abaixo:

#define swap(a,b) \
    { int _t; _t = (a); (a) = (b); (b) = _t; }

// Para ser justo, não vou permitir inlining...
__attribute__((noinline))
static void selection_sort(int *p, unsigned int size)
{
  unsigned int i, j;

  for (i = 0; i < size - 1; i++)
    for (j = i+1; j < size; j++)
      if (p[i] > p[j])
        swap(p[i], p[j]);
}

É legítimo pensar que essa rotina seja menos que ótima, já que usa o macro swap(), que lança mão de uma variável temporária que talvez seja alocada em memória. Assim, o programador pode criar uma rotina equivalente, em assembly, assim:

bits 64

section .text

; void selection_sort_asm(int *, unsigned int);
; -- Para ser justo, alinho em DWORD o ponto de entrada
;    da rotina!
  global selection_sort_asm
  align 4
selection_sort_asm:
  lea r8d,[rsi-1] ; r8d == size - 1;

  xor ecx,ecx     ; i = 0;

.loop2:
  lea edx,[rcx+1] ; j = i + 1;

.loop1:
  mov eax,[rdi+rcx*4]   ; if (ptr[i] > ptr[j]) ...
  cmp eax,[rdi+rdx*4]
  jle .skip

  xchg eax,[rdi+rdx*4]  ; ... swap(p[i], p[j]);
  mov [rdi+rcx*4],eax

.skip:
  add edx,1     ; j++
  cmp edx,esi   ; if (j < size) goto loop1;
  jb  .loop1

  add ecx,1     ; i++;
  cmp ecx,r8d   ; if (i < size - 1) goto loop2;
  jb  .loop2

  ret

Claro que essa rotina pode ser melhorada, especialmente entre os labels .loop1 e .skip, mas, em arquiteturas mais modernas (Ivy Bridge, SandyBridge e Haswell, sem contar com as mais recentes Broadwell e Skylake) os cálculo de endereços efetivos podem ser cacheados, numa espécie de common subexpression elimination, ou seja, isso não tem grande influência… Além do que, quero mostrar como uma abordagem “ingênua” se parece…

Bem… a rotina acima não é tão “ingênua” assim porque levei em conta algumas possíveis otimizações: Note que usei a instrução XCHG, usada para aproveitar o conteúdo de EAX na “troca” e evitar o uso de um outro registrador temporário. Repare também que não uso a instrução INC para evitar a penalidade de 1 ciclo de clock pelo fato dessa instrução não afetar o flag de carry (e precisar preservá-lo). É preferível usar ADD, mesmo tendo tamanho maior, em seu microcódigo… Ainda, os saltos condicionais são feitos “para trás”, para aproveitar o algoritmo estático de branch prediction (exceto para o label .skip). Uso também as instruções LEA para adições rápidas, carryless

Mas, se você medir a performance de ambas as rotinas contra um array de 10 elementos aleatórios obterá (num i5-3570 @ 3.4 GHz, arquitetura Ivy Bridge) algo em torno de 1800 ciclos para a rotina em C e 2200 ciclos para a rotina em assembly! Ou seja, a rotina em C é 18% mais rápida!!! Num i7, com arquitetura Haswell poderá obter menos ciclos…

A rotina em Assembly equivalente para o selection sort, gerada pelo GCC com as opções de compilação “-O2 -mtune=native“, é esta:

bits 64

section .text

  global selection_sort
  align 4
selection_sort:
  cmp esi, 1
  jbe .exit
  lea r11d, [rsi-1]
  mov r9, rdi
  xor r10d, r10d

  align 4
.loop2:
  add r10d, 1
  mov eax, r10d
  cmp esi, r10d
  jbe .skip2

  align 4
.loop1:
  mov edx, eax
  mov ecx, [r9]
  lea rdx, [rdi+rdx*4]
  mov r8d, [rdx]
  cmp ecx, r8d
  jle .skip1
  mov [r9], r8d
  mov [rdx], ecx

.skip1:
  add eax, 1
  cmp esi, eax
  jne .loop1

.skip2:
  add r9, 4
  cmp r10d, r11d
  jne .loop2
  ret

.exit:
  ret

Essencialmente, ela é a mesma coisa da rotina em assembly que mostrei acima. A diferença está na organização das instruções e no aproveitamento do paralelismo das unidades de execução do processador (não confundir com os “cores” ou “núcleos”!).

O GCC também evita instruções problemáticas como XCHG (que, quando faz acesso à memória, “trava” [lock] o barramento de dados). O compilador também entende que pontos de retorno de loops podem precisar estar alinhados para que eles não cruzem a fronteira de uma linha do cache L1I e, por isso, coloca aquele “align 4” lá… Ele também tenta evitar problemas de “interlocking” de registradores (quando modificamos um registrador e tentamos usá-lo, na instrução seguinte, como operando fonte. Isso evita que duas instruções sejam executadas em paralelo).

Ou seja, o compilador tenta levar em conta a grande quantidade de regras de otimização (que podem ser estudadas no manual de otimização de software da Intel [baixe aqui] – prepare-se: são quase 700 páginas!) que você, ou eu, poderíamos deixar “passar batido”. Isso não significa que não seja possível para você ou eu elaborarmos uma rotina que seja melhor da gerada pelo compilador. Isso significa, porém, que na maioria das vezes, o compilador faz um trabalho melhor do que faríamos!

Um kbhit() para Windows

Num artigo anterior (este aqui) mostrei como codificar um kbhit() para Linux. Ficou faltando a mesma função para Windows.

Algumas pessoas insistem em usar o header conio.h, que existe para o MinGW e o Visual Studio… Se esses são seus alvos para compilação e seu projeto não precisa ser portável, vá em frente, use o header. Mas, saiba que você pode codificar essas funções de forma mais “portável”, juntando as definições dos outros artigos (para Linux) com essas aqui.

No caso do Windows, podemos usar as funções da Console API, que é mais ou menos equivalente à termios, do Linux. Eis as rotinas _kbhit() e _getch():

#include <windows.h>

// Ok... essa função só tem um problema.
// Por causa do loop, a thread consumirá
// muito tempo de CPU...
int _kbhit(void)
{
  HANDLE hConsole;
  INPUT_RECORD ir;
  DWORD n;

  hConsole = GetStdHandle(STD_INPUT_HANDLE);
  while (PeekConsoleInput(hConsole, &ir, 1, &n))
    if (ir.EventType == KEY_EVENT)
      return 1;

  return 0;
}

int _getch(void)
{
  HANDLE hConsole;
  DWORD cm, n;
  char buffer[4];   // Para ter certeza que um caracter
                    // UNICODE cabe aqui, alocamos 4 bytes.

  hConsole = GetStdHandle(STD_INPUT_HANDLE);
  GetConsoleMode(hConsole, &cm);
  SetConsoleMode(hConsole, cm &
             ~(ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT));
  ReadConsole(hConsole, buffer, 1, &n, NULL);
  SetConsoleMode(hConsole, cm);

  // Só preciso de 1 char...
  return buffer[0];
}

Repare como, em _getch() desabilito “LINE INPUT” (mais ou menos como o método “canônico” do termios) e o “ECHO INPUT”. Fiz algo semelhante no getch() e kbhit() lá do Linux…

A função _kbhit() fica um pouco mais simples por causa de PeekConsoleInput, que nos permite obter os eventos de entrada sem retirá-los da fila. Essencialmente, percorremos a fila de eventos à procura de um KEY_INPUT. Se algum for achado, retornamos 1 (true)…

Outra vez: Arrays e ponteiros…

Recentemente topei com a seguinte dúvida de um leitor: “Se um item de um array é referenciado por seu ponteiro-base e um offset, uma matriz é um array de ponteiros?”. Infelizmente, não! Para exemplificar, eis o que eu disse, até agora sobre arrays:

Um array e um ponteiro

No caso acima, o ponteiro p aponta para o primeiro item do array a. Note que a variável p contém, em seu interior, o endereço deste primeiro item. Isso significa que se derreferenciarmos o ponteiro p (com o operador de indireção *), obteremos o conteúdo de a[0]. Ou seja: a[0] = *(p+0) = *(a+0). Mas, note o que acontece com um array de ponteiros:

array de ponteiros para char

Cada ítem de a é, em si, um ponteiro para um bloco de chars. Agora, eis a diferença entre isso e um array bidimensional:

Array bidimensional

O símbolo a continua sendo um ponteiro para o primeiro item do array, mas a estrutura, na memória, é a de um array unidimensional simples, onde cada linha tem 9 chars. Os caracteres adicionais, em cada linhas, que não foram preenchidos são automaticamente preenchidos com zeros.

Repare que, na notação da linguagem C, podemos especificar um array “incompleto”, mas somente na última dimensão (a mais a esquerda). Isso é assim porque o compilador precisa saber qual é o tamanho de cada “linha” da “matriz” para calcular a posição inicial da linha desejada… No caso, a especificação a[1][4] é calculada como a+9\cdot1+3, ou, de forma mais genérica:

\displaystyle a[l][c] = *(a + l\cdot C+c)

Onde C é a quantidade de colunas que uma linha possui e as variáveis minúsculas l (letra “éle”) e c correspondem a linha e coluna desejadas. Precisamos multiplicar a linha desejada pelo número de colunas que o array tem e depois adicionar a coluna desejada para obter o offset, em relação ao ponteiro-base a. Note que, em ambos os casos, a especificação a[1][4] nos dará o caracter ‘a’, que está na 13ª posição do array unidimensional equivalente.

Mas, como isso fica se tivermos um array de 3 ou mais dimensões?  Para tornar o cálculo mais simples, especificarei um array “tridimensional” assim: T a[Z][Y][X], onde T é um tipo qualquer e (X,Y,Z) são os tamanhos de cada dimensão. De novo, usarei (x,y,z), minúsculos, para especificar uma posição desejada…

Em primeiro lugar, o tamanho desse array é de X\cdot Y\cdot Z\cdot sizeof(T) bytes e, para encontrarmos o início do array bidimensional especificado por z, precisamos calcular o offset assim: z\cdot X\cdot Y, porque cada uma dessas “camadas” têm X\cdot Y de tamanho. De maneira similar ao array bidimensional, para achar uma coluna nessa “camada”, precisamos fazer y\cdot X+x. Assim, a posição (x,y,z) do array a é derreferenciada como a[z][y][x] = *(a + X\cdot Y\cdot z + X\cdot y + x).

Na notação de arrays, o compilador faz esses cálculos pra você… mas, é bom lembrar que isso é completamente diferente de usar três indireções.

Ahhh. Uma dica: Se for criar arrays multidimensionais, tenha certeza de que as dimensões inferiores tenham tamanho múltiplo de 2^n. Isso porque, já que o compilador terá que usar multiplicações para calcular o offset, dessa maneira ele usará apenas shifts lógicos. Acelera um bocado… Já a dimensão de alta ordem, que não é usada no cálculo do offset como fator multiplicativo, pode ser de qualquer tamanho… Assim, criar um array int a[10][10] é bem menos performático do que criar um int a[10][16]. Você gasta um pouco mais de espaço, mas a performance melhora um bocado.

Localização em C

Alguém me perguntou sobre locale em C. Pra você que não sabe o que locale significa, é somente a maneira como algumas strings relacionadas a formatação se apresentam de acordo com a localidade onde o usuário se encontra. Por exemplo, no Brasil usamos ‘,’ (vírgula) ao invés de ‘.’ (ponto) decimal, quando queremos expressar valores fracionários, como em 3,141593. Mas, parece, que as rotinas das bibliotecas lidam apenas com o formato americano, usando o ‘.’. Bem… não é bem assim.

O padrão ISO 9899, que rege a linguagem C e a biblioteca padrão, nos fornece o header locale.h com estruturas e funções para lidarmos com localização. A localização default é chamada, simplesmente, de “C” e segue o padrão americano. Mas, podemos ajustar a localidade para a nossa língua, nosso país e o charset que usamos, desde que o sistema operacional o tenha acessível. Para isso usamos a função setlocale(), que tem o protótipo:

char *setlocale(int category, const char *locale);

A “categoria” é uma das constantes LC_ALL, LC_ADDRESS, LC_COLLATE, LC_CTYPE, LC_IDENTIFICATION, LC_MEASUREMENT, LC_MESSAGES, LC_MONETARY, LC_NAME, LC_NUMERIC, LC_PAPER, LC_TELEPHONE ou LC_TIME. Cada uma dessas características endereça um aspecto da sua localidade… LC_NUMERIC, por exemplo, é usada para ajustar como o “ponto decimal” e o “separador de milhar” serão usados. LC_MONETARY, a mesma coisa, mas também onde a string da moeda será colocada e se como valores negativos são expressos… A categoria LC_CTYPE tem a ver com as macros contidas no header ctype.h. E por ai vai…

O argumento “locale” é uma string com um formato específico. Pro Brasil, por exemplo, pode-se usar “pt_BR” ou “pt_BR.utf8” (no caso do Linux/BSD/MacOS). O formato segue o padrão “língua[_país][.charset]”, onde “língua” segue a abreviação especificada na ISO 639, enquanto “país” segue a especificação ISO 3166… Mas, é importante notar que apenas os locales disponíveis no sistema operacional são aceitáveis e eles podem ser obtidos assim:

$ locale -a
C
C.UTF-8
en_AG
en_AG.utf8
en_AU.utf8
en_BW.utf8
en_CA.utf8
en_DK.utf8
en_GB.utf8
en_HK.utf8
en_IE.utf8
en_IN
en_IN.utf8
en_NG
en_NG.utf8
en_NZ.utf8
en_PH.utf8
en_SG.utf8
en_US.utf8
en_ZA.utf8
en_ZM
en_ZM.utf8
en_ZW.utf8
POSIX
pt_BR.utf8

Note que, no meu caso, “pt_BR” não está disponível, apenas “pt_BR.utf8”.

O programinha abaixo mostra locale em funcionamento:

#include <stdio.h>
#include <locale.h>

void main(void)
{
  float f;

  setlocale(LC_ALL, "pt_BR.utf8");
  fputs("Entre com um valor float (usando ',' como separador decimal): ", stdout);
  fflush(stdout);
  scanf("%f", &f);
  printf("Valor: %f\n", f);
}

Compilando e executando:

$ cc -o test test.c
$ ./test
Entre com um valor float (usando ',' como separador decimal): 3,14
Valor: 3,140000

Tanto o scanf() quanto o printf() obedeceram a localização ajustada por setlocale()! Mas, note, não é que essas funções respeitem a localização, mas as funções que elas usam o fazem (atof(), por exemplo). Outro ponto interessante é sobre os “separadores de milhar”. Eles não são respeitados nessas funções! Faça float f = atof("32.768,9"); com o locale pt_BR.utf8 ativo e você, provavelmente, obterá 0.0f como resposta, indicando falha da conversão. Não espere que scanf() e printf() vão entender valores monetários, por exemplo… O locale LC_MONETARY existe somente para que você possa pegar as regras correspondentes na estrutura lconv.

Para saber quais são as regras do locale selecionado, use a função localeconv() que retorna um ponteiro para uma estrutura lconv contendo um monte de informações (disponiveis na especificação da linguagem C, aqui). Você não deve modificar os valores da estrutura…

Uma maneira simples de livrar-se dos separadores de milhar, levando-se em conta o indicado em lconv, é usando uma rotina como essa aqui:

#include <string.h>
#include <locale.h>

char *strip_thousands_sep(char *s)
{
  struct lconv *pl;

  pl = localeconv();
  if (*pl->mon_thousands_sep)
  {
    char *p = s;

    while (p = strchr(p, *pl->mon_thousands_sep))
      strcpy(p, p + 1);  // Não há problema em fazer
                         // essa cópia. Não há overlapping
                         // aqui e ambos os ponteiros
                         // são válidos.
  }

  return s;
}

É claro, a localidade terá que ser ajustada antes de chamar essa rotina. E, como já dito, para retornar a localidade default, basta usar “C”, ao invés de “pt_BR.utf8”.

A localização também é útil para comparação de strings. A função strcoll() leva em consideração a localidade para comparar duas strings. O exemplo contido no site cppreference (exemplo, aqui) ilustra bem. Note que strcmp() e derivadas não lidarão com localização… Outra função para lidar com strings e localização útil é strxfrm(). Ela transforma (daí a abreviação xfrm) uma string numa representação que pode ser usada em strcmp(), ou derivadas, para ter o mesmo efeito de strcoll(). Isso pode ser útil em línguas como hebraico e árabe, que são escritas “de trás para frente”…

Instalando novas localidades no Linux:

Nada mais simples. Linux disponibiliza pacotes para linguagens específicas. Para listá-las todas (Debian/Ubuntu):

$ apt-cache search language-pack | grep 'pack-.. '
language-pack-af - translation updates for language Afrikaans
language-pack-am - translation updates for language Amharic
language-pack-an - translation updates for language Aragonese
language-pack-ar - translation updates for language Arabic
language-pack-as - translation updates for language Assamese
language-pack-az - translation updates for language Azerbaijani
language-pack-be - translation updates for language Belarusian
language-pack-bg - translation updates for language Bulgarian
language-pack-bn - translation updates for language Bengali
language-pack-bo - translation updates for language Tibetan
language-pack-br - translation updates for language Breton
language-pack-bs - translation updates for language Bosnian
language-pack-ca - translation updates for language Catalan; Valencian
language-pack-cs - translation updates for language Czech
language-pack-cy - translation updates for language Welsh
language-pack-da - translation updates for language Danish
language-pack-de - translation updates for language German
language-pack-dz - translation updates for language Dzongkha
language-pack-el - translation updates for language Greek, Modern (1453-)
language-pack-en - translation updates for language English
language-pack-eo - translation updates for language Esperanto
language-pack-es - translation updates for language Spanish; Castilian
language-pack-et - translation updates for language Estonian
language-pack-eu - translation updates for language Basque
language-pack-fa - translation updates for language Persian
language-pack-fi - translation updates for language Finnish
language-pack-fr - translation updates for language French
language-pack-ga - translation updates for language Irish
language-pack-gd - translation updates for language Gaelic; Scottish Gaelic
language-pack-gl - translation updates for language Galician
language-pack-gu - translation updates for language Gujarati
language-pack-he - translation updates for language Hebrew
language-pack-hi - translation updates for language Hindi
language-pack-hr - translation updates for language Croatian
language-pack-hu - translation updates for language Hungarian
language-pack-ia - translation updates for language Interlingua (International Auxiliary Language Association)
language-pack-id - translation updates for language Indonesian
language-pack-is - translation updates for language Icelandic
language-pack-it - translation updates for language Italian
language-pack-ja - translation updates for language Japanese
language-pack-ka - translation updates for language Georgian
language-pack-kk - translation updates for language Kazakh
language-pack-km - translation updates for language Central Khmer
language-pack-kn - translation updates for language Kannada
language-pack-ko - translation updates for language Korean
language-pack-ku - translation updates for language Kurdish
language-pack-lt - translation updates for language Lithuanian
language-pack-lv - translation updates for language Latvian
language-pack-mk - translation updates for language Macedonian
language-pack-ml - translation updates for language Malayalam
language-pack-mn - translation updates for language Mongolian
language-pack-mr - translation updates for language Marathi
language-pack-ms - translation updates for language Malay
language-pack-my - translation updates for language Burmese
language-pack-nb - translation updates for language Bokmål, Norwegian; Norwegian Bokmål
language-pack-ne - translation updates for language Nepali
language-pack-nl - translation updates for language Dutch; Flemish
language-pack-nn - translation updates for language Norwegian Nynorsk; Nynorsk, Norwegian
language-pack-oc - translation updates for language Occitan (post 1500)
language-pack-or - translation updates for language Oriya
language-pack-pa - translation updates for language Panjabi; Punjabi
language-pack-pl - translation updates for language Polish
language-pack-pt - translation updates for language Portuguese
language-pack-ro - translation updates for language Romanian
language-pack-ru - translation updates for language Russian
language-pack-si - translation updates for language Sinhala; Sinhalese
language-pack-sk - translation updates for language Slovak
language-pack-sl - translation updates for language Slovenian
language-pack-sq - translation updates for language Albanian
language-pack-sr - translation updates for language Serbian
language-pack-sv - translation updates for language Swedish
language-pack-ta - translation updates for language Tamil
language-pack-te - translation updates for language Telugu
language-pack-tg - translation updates for language Tajik
language-pack-th - translation updates for language Thai
language-pack-tr - translation updates for language Turkish
language-pack-ug - translation updates for language Uighur; Uyghur
language-pack-uk - translation updates for language Ukrainian
language-pack-uz - translation updates for language Uzbek
language-pack-vi - translation updates for language Vietnamese
language-pack-xh - translation updates for language Xhosa

Se adicionar language-pack-ja, por exemplo, a localidade jp_JP.utf8 será adicionada nas tabelas de localidade do sistema e ficará disponível para uso de seus programas.

Provavelmente seu professor não explicou isso direito pra você! (parte 2)

Neste outro artigo, aqui, eu te expliquei o funcionamento do stream stdout e do uso de fflush(). Mas, deixei alguns detalhes de fora, especialmente quanto ao stream stdin.

Em um grupo do Facebook tenho visto uma galera usando fflush(stdin) em resoluções de exercícios, usando linguagem C. Essa construção é inútil porque, por padrão, fflush() funciona apenas para streams que podem ser escritas. Este é o caso de stdout.

Outro detalhe é sobre o uso da função scanf(). Ela tem uns problemas que podem ser desesperadores para o novato.

Também têm um valor de retorno. A função devolve um valor do tipo int que é o número de itens que puderam ser convertidos na “varredura” (scan) ou -1, em caso de erro catastrófico. Se você fizer:

int a, b, n;

n = scanf("%d %d", &a, &b);

E entrar com duas strings não numéricas, vai obter n diferente de 2. Se entrar com “xpto xpto”, obterá n=0. Se entrar com “1 xpto”, obterá n=1… A função também interpreta ‘\n’ como “espaço” a ser ignorado. Se você entrar com apenas “1”, a função continuará esperando pelo segundo argumento a ser convertido!

O termo “varredura” é importante! A função procura pelos itens a serem convertidos, de acordo com o formato especificado, varrendo o stream stdin, até conseguir convertê-los ou falhar, ignorando os “espaços”. No exemplo acima, se entrássemos com:

"1          1" ou "       1 1"

Teríamos a e b convertidos corretamente.

No caso de itens que não podem ser convertidos, aparece um problema: Eles permanecem no buffer de stdin! Para demonstrar isso, eis um exemplo:

#include <stdio.h>

void main(void)
{
  int a, b, n;

  for (;;) {
    printf("Entre com dois números: "); fflush(stdout);
    n = scanf("%d %d", &a, &b);
    printf("%d valores convertidos: %d e %d\n", n, a, b);
  }
}

Compile e execute esse programinha e entre com “abc abc”… Observe os printf‘s sendo executados um monte de vezes! Isso acontece porque scanf() deixará no buffer de stdin os caracteres não convertidos da string lida anteriormente (como expliquei ai em cima), bem como qualquer caracter “não pulado”. Assim, scanf() tentará reconvertê-los, falhando diversas vezes até limpar todo o buffer de stdin.

Infelizmente não há jeito portável de fazer um flushing no stream stdin… Mas, usando uma extensão da glibc podemos corrigir esse problema facilmente:

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

int main(void)
{
  char *line;
  size_t line_size;
  int n;

  unsigned char b;
  char str[21];

  do {
    printf("Entre com um valor de 8 bits e uma string separados por espaço:\n");

    // força alocação da linha em getline().
    line_size = 0;
    line = NULL;

    // Lê uma linha de stdin.
    // getline() é uma extensão da glibc!
    if (getline(&line, &line_size, stdin) == -1)
    {
      fputs("ERRO alocando espaço para conter a linha!\n", stderr);
      exit(EXIT_FAILURE);
    }

    // Para termos um valor definido, em caso de falha.
    str[0] = b = 0;

    // Usa sscanf() porque quando scanf falha, deixa o stream stdin com lixo!
    n = sscanf(line, "%hhu %20s", &b, str);

    // livra-se da linha alocada.
    free(line);

    printf("%d itens convertidos: %hhu, \"%s\"\n", n, b, str);
  } while (1);
}

Aqui, sscanf uão usa um stream para obter os valores convertidos, ela usa uma string! A função getline() lerá uma linha da entrada (stdin) — ou seja, até char um ‘\n’, que será colocado no final do buffer alocado, precedendo o caracter NUL — e retornará com buffer apontado por line dinamicamente alocado (ou retorna -1 em caso de falha). Note que depois usar o sscanf fiz uma chamada a free(), livrando-se do buffer alocado por getline.

Se você digitar ENTER e obter uma linha vazia, ela terá 1 único caracter (‘\n’) o o NUL. Assim, sscanf falhará, mas não há nenhum buffer mantido por um stream para bagunçar o coreto.

Já que getline() é uma extensão, você pode optar por usar fgets(), mas perderá o recurso de alocação dinâmica automática, tendo de codificar isso por si mesmo… Ahhh… não use gets() para ler o stream stdin. Essa função é obsoleta desde a especificação C99 e não deve ser mais usada.