C Acentos e UTF-8
Em várias aulas, os acentos apareceram como um pequeno mistério: "feijão" tem 6 letras, mas o strlen diz 7; a tabela do printf sai torta na linha do "Café"; o toupper não transforma ç em Ç; e char c = 'ç'; nem funciona. Tudo tem a mesma causa: em C, uma string é uma fileira de bytes, e uma letra acentuada ocupa mais de um byte. Esta aula explica como o texto é guardado (o padrão UTF-8), o que isso quebra nos seus programas e como trabalhar com acentos sem susto.
- O que são o Unicode (o catálogo de caracteres) e o UTF-8 (o jeito de guardá-los em bytes).
- Por que
"maçã"tem 4 letras e 6 bytes, e como ver esses bytes. - A regra do UTF-8, que diz quantos bytes cada caractere ocupa.
- Como contar os caracteres de um texto, e não os bytes.
- As armadilhas:
strlen, a largura doprintf, cortar uma letra ao meio,toupperechar c = 'ç'. - Como escrever acentos por código (
ç) e o que são os caracteres largos (wchar_t) e osetlocale. - De onde vem o
Oláque aparece no Windows, e como evitar.
Todos os programas foram compilados com o gcc 15.2 na máquina fictícia da Ana (usuária ana, computador notebook, Linux em português, pt_BR.UTF-8), na pasta ~/c, com o comando padrão do guia: gcc -std=c23 -Wall -Wextra arquivo.c -o programa. Antes desta aula, veja Caracteres, Strings e Classificar Caracteres.
A ideia: o catálogo e a embalagem
A tabela ASCII da aula Caracteres tem só 128 posições: letras sem acento, algarismos, pontuação. Não cabem ç, ã, o €, o alfabeto grego, o japonês nem os emojis. Para isso existem duas peças, e vale separar bem uma da outra:
- Unicode é um catálogo: uma lista enorme (mais de 150 mil caracteres) em que cada caractere do mundo tem um número fixo, o ponto de código. O
Aé o 65 (igual ao ASCII), oçé o 231, o€é o 8364. Escreve-seU+00E7(o 231 em hexadecimal). - UTF-8 é a embalagem: a regra que diz como guardar esses números em bytes. Número pequeno vai numa caixa de 1 byte; número maior, em 2, 3 ou 4 bytes.
Pense nos Correios: o catálogo diz o que é cada produto; a embalagem decide quantas caixas ele ocupa no caminhão. O C só enxerga as caixas, um byte de cada vez. Hoje, quase todo arquivo, página da web e terminal (o Linux, o macOS, o editor do guia) usa UTF-8.
1. Os bytes de "maçã"
#include <stdio.h>
#include <string.h>
int main(void) {
char palavra[] = "maçã";
printf("\"%s\": strlen = %zu, sizeof = %zu\n", palavra, strlen(palavra), sizeof palavra);
for (int i = 0; palavra[i] != '\0'; i++) {
unsigned char b = palavra[i];
printf("palavra[%d] = %3d = 0x%02X\n", i, b, b);
}
return 0;
}
- char palavra[] = "maçã";
- O arquivo
bytes.cfoi salvo em UTF-8, e o gcc copia os bytes do texto do jeito que estão no arquivo. - strlen / sizeof
- O
strlenconta os bytes até o'\0'; osizeofmede o array inteiro, com o'\0'(aulas Funções de String e Tamanho dos Tipos). - unsigned char b = palavra[i];
- Lê cada byte como um número de 0 a 255. Num
charcomum, os bytes acima de 127 apareceriam negativos (aula Classificar Caracteres). - 0x%02X
- Mostra o byte em hexadecimal com 2 dígitos (aula Formatos do printf). Em hexadecimal, os padrões do UTF-8 ficam fáceis de ver.
$ gcc -std=c23 -Wall -Wextra bytes.c -o bytes $ ./bytes "maçã": strlen = 6, sizeof = 7 palavra[0] = 109 = 0x6D « m palavra[1] = 97 = 0x61 « a palavra[2] = 195 = 0xC3 « ç (1º byte) palavra[3] = 167 = 0xA7 « ç (2º byte) palavra[4] = 195 = 0xC3 « ã (1º byte) palavra[5] = 163 = 0xA3 « ã (2º byte)
4 letras, 6 bytes, e 7 no array com o '\0'. O m e o a são os mesmos números do ASCII. O ç virou o par C3 A7 e o ã, o par C3 A3. Repare: o printf("%s") mostrou maçã certinho. Ele não sabe nada de acentos; só entregou os 6 bytes ao terminal, e o terminal, que também fala UTF-8, juntou os pares e desenhou as letras.
Experimente: troque "maçã" pelo seu nome, por "Ação", "€5" ou por um emoji ("😀"), e conte os bytes. Quais caracteres ocupam 1 byte? E 2, 3, 4?
#include <stdio.h>
#include <string.h>
int main(void) {
char palavra[] = "maçã";
printf("\"%s\": %zu bytes\n", palavra, strlen(palavra));
for (int i = 0; palavra[i] != '\0'; i++) {
printf("%02X ", (unsigned char) palavra[i]);
}
printf("\n");
return 0;
}
2. A regra do UTF-8
Quantos bytes cada caractere ocupa depende só do seu número no catálogo:
#include <stdio.h>
#include <string.h>
int main(void) {
const char *textos[] = {"a", "ç", "€", "😀"};
for (int t = 0; t < 4; t++) {
printf("%s: %zu byte(s):", textos[t], strlen(textos[t]));
for (size_t i = 0; textos[t][i] != '\0'; i++) {
printf(" %02X", (unsigned char) textos[t][i]);
}
printf("\n");
}
return 0;
}
$ gcc -std=c23 -Wall -Wextra tamanhos.c -o tamanhos $ ./tamanhos a: 1 byte(s): 61 ç: 2 byte(s): C3 A7 €: 3 byte(s): E2 82 AC 😀: 4 byte(s): F0 9F 98 80
(O const char *textos[] é uma lista de textos fixos, um array de ponteiros; ele é explicado em Ponteiros e Strings. Aqui basta saber que textos[t] é o texto número t.)
O segredo está nos primeiros bits de cada byte (bits e hexadecimal: aula Operadores de Bits):
| Caracteres | Bytes | 1º byte | Bytes seguintes | Exemplos |
|---|---|---|---|---|
| ASCII (0 a 127) | 1 | 0xxxxxxx (00 a 7F) | nenhum | a, 7, !, espaço |
| Até 2047 | 2 | 110xxxxx (C2 a DF) | 10xxxxxx (80 a BF) | ç ã é Ç, grego, árabe |
| Até 65535 | 3 | 1110xxxx (E0 a EF) | 2 × 10xxxxxx | €, chinês, japonês |
| Até 1114111 | 4 | 11110xxx (F0 a F4) | 3 × 10xxxxxx | emojis, símbolos raros |
Três consequências que deixam o UTF-8 ótimo para o C:
- Todo texto ASCII já é UTF-8. Um programa antigo, que só usa ASCII, continua igual.
- Os bytes de um caractere acentuado são todos 128 ou mais. Nunca aparece um
'\0', um'\n'ou um espaço "escondido" no meio de umç. Por issoprintf,strcpy,strcat,fgetsestrchr(s, ' ')funcionam com acentos sem nenhum cuidado especial. - O começo de cada caractere é reconhecível: os bytes de continuação sempre começam com os bits
10. Isso permite contar caracteres, como na próxima seção.
3. Contar caracteres, e não bytes
Se os bytes de continuação começam com 10, basta contar os bytes que não começam assim: cada um deles é o começo de um caractere.
#include <stdio.h>
#include <string.h>
int main(void) {
char texto[] = "Atenção: café não é chá!";
int caracteres = 0;
for (int i = 0; texto[i] != '\0'; i++) {
unsigned char b = texto[i];
if ((b & 0xC0) != 0x80) {
caracteres++;
}
}
printf("%s\n", texto);
printf("bytes: %zu, caracteres: %d\n", strlen(texto), caracteres);
return 0;
}
- b & 0xC0
- O
&bit a bit (aula Máscaras de Bits) com0xC0(binário11000000) apaga tudo menos os 2 primeiros bits do byte. - != 0x80
0x80é o binário10000000. Se os 2 primeiros bits não são10, o byte não é continuação: é o começo de um caractere (ASCII ou o 1º byte de um acentuado). Conta mais um.
$ gcc -std=c23 -Wall -Wextra contar.c -o contar $ ./contar Atenção: café não é chá! bytes: 30, caracteres: 24
São 6 letras acentuadas (ç, ã, é, ã, é, á), cada uma com 1 byte a mais: 24 + 6 = 30. Esse laço conta pontos de código, o que, para português, é o mesmo que contar letras.
Missão: o cadastro da lojinha aceita nomes com até 20 caracteres, mas o programa está contando bytes. Mude a conta para contar só os bytes que começam um caractere (os que não são de continuação). A saída deve ser caracteres: 12.
#include <stdio.h>
int main(void) {
char nome[] = "Conceição Sá";
int caracteres = 0;
for (int i = 0; nome[i] != '\0'; i++) {
unsigned char b = nome[i];
caracteres++; // conta todos os bytes: corrija
(void) b;
}
printf("%s\n", nome);
printf("caracteres: %d\n", caracteres);
return 0;
}
#include <stdio.h>
int main(void) {
char nome[] = "Conceição Sá";
int caracteres = 0;
for (int i = 0; nome[i] != '\0'; i++) {
unsigned char b = nome[i];
if ((b & 0xC0) != 0x80) {
caracteres++;
}
}
printf("%s\n", nome);
printf("caracteres: %d\n", caracteres);
return 0;
}
(O (void) b; do código inicial só existe para o compilador não avisar que b não é usado. Na sua solução, você usa o b e pode apagar essa linha.)
Para português, um ponto de código é uma letra. Mas o Unicode permite escrever o é de dois jeitos: um ponto de código só (U+00E9) ou um e seguido de um acento "solto" (U+0301), que o terminal desenha em cima do e. No segundo jeito, o laço acima conta 2. E alguns emojis (como as famílias e as bandeiras) são vários pontos de código colados. Contar "o que a pessoa vê" exige bibliotecas especializadas, como a ICU; para nomes e textos comuns digitados em português, o laço basta.
4. As armadilhas
Agora que você sabe que ç são dois bytes, cada estranheza das outras aulas se explica.
strlen e o tamanho do array contam bytes
O strlen("João") é 5, e o array precisa de 6. Ao escolher o tamanho de um array para texto com acentos, conte bytes: em português, reserve folga (o dobro do número de letras é seguro, porque toda letra do português ocupa no máximo 2 bytes). Os limites de fgets e snprintf também são em bytes, e é isso que os mantém seguros.
A largura do printf conta bytes
Na aula Formatos do printf, a tabela saiu torta nas linhas com acento. Veja o porquê:
#include <stdio.h>
int main(void) {
printf("[%-8s] [%-8s] [%-8s]\n", "Arroz", "Feijão", "Maçã");
return 0;
}
$ gcc -std=c23 -Wall -Wextra alinhar.c -o alinhar $ ./alinhar [Arroz ] [Feijão ] [Maçã ]
O %-8s completa com espaços até 8 bytes. "Arroz" tem 5 bytes e ganhou 3 espaços: certo. "Feijão" tem 7 bytes (6 letras) e ganhou 1 espaço; "Maçã" tem 6 bytes (4 letras) e ganhou 2. Na tela, as duas colunas ficaram 1 e 2 posições mais curtas. O conserto é contar as letras com o laço da seção 3 e completar com espaços à mão: é a missão final desta aula.
Cortar no meio de uma letra
#include <stdio.h>
int main(void) {
char nome[] = "João";
nome[3] = '\0';
printf("[%s]\n", nome);
return 0;
}
$ gcc -std=c23 -Wall -Wextra cortar.c -o cortar $ ./cortar [Jo�] « sobrou só o 1º byte do ã
A Ana queria as 3 primeiras letras ("Joã"), mas cortou no byte 3, que é o meio do ã (J o C3 A3 o). Sobrou o C3 sozinho, que não é um caractere completo, e o terminal desenhou o caractere de substituição �, o "não sei o que é isto" do Unicode. O mesmo acontece com strncpy, snprintf ou %.3s cortando um texto acentuado: eles cortam bytes. Para cortar letras, ande pelo texto com a regra da seção 3 e corte no começo de um caractere.
toupper e o ctype.h olham um byte
#include <ctype.h>
#include <stdio.h>
int main(void) {
char texto[] = "ação";
for (int i = 0; texto[i] != '\0'; i++) {
texto[i] = (char) toupper((unsigned char) texto[i]);
}
printf("%s\n", texto);
return 0;
}
$ gcc -std=c23 -Wall -Wextra maiuscula.c -o maiuscula $ ./maiuscula AçãO
O a e o o viraram maiúsculas; o ç e o ã não. Para o toupper, os bytes C3, A7 e A3 não são letras (aula Classificar Caracteres, seção 6), então voltam iguais. Pelo menos ele não estraga nada. Para converter acentos, a seção 6 mostra os caracteres largos.
char c = 'ç' não cabe
Um char guarda 1 byte e o ç são 2. A aula Caracteres mostrou o resultado: o gcc avisa multi-character character constant e guarda só o último byte; o Clang do editor recusa com error: character too large for enclosing character literal type. Guarde um caractere acentuado sozinho como string, char c[] = "ç"; (3 bytes, com o '\0'), ou como caractere largo, wchar_t c = L'ç'; (seção 6).
Comparar e ordenar
O strcmp compara bytes. Como o 1º byte das letras acentuadas do português (o C3) é maior que qualquer letra ASCII (o z é 7A), "Ábaco" fica depois de "zebra". A aula Funções de String mostra esse efeito; para listas em ordem alfabética "de dicionário", existe o strcoll, que segue as regras do idioma: depois de setlocale(LC_COLLATE, "") (seção 7), no notebook da Ana, strcoll("Ábaco", "zebra") já põe o "Ábaco" antes.
5. Escrever acentos por código: \u e U'ç' C99 C11
Se você não consegue digitar um caractere (ou quer deixar claro qual é), escreva o ponto de código. Dentro de uma string, \u seguido de 4 dígitos hexadecimais (ou \U com 8) vira o caractere, já em UTF-8. E um literal com U na frente, U'ç', dá o número do catálogo do caractere:
#include <stdio.h>
#include <string.h>
int main(void) {
char a[] = "maçã";
char b[] = "maçã";
printf("%s %s, iguais: %s\n", a, b, strcmp(a, b) == 0 ? "sim" : "não");
printf("U'ç' = %u, U'€' = %u\n", (unsigned) U'ç', (unsigned) U'€');
return 0;
}
$ gcc -std=c23 -Wall -Wextra escape.c -o escape $ ./escape maçã maçã, iguais: sim U'ç' = 231, U'€' = 8364
- "maçã"
çé o U+00E7 (ç) eãé o U+00E3 (ã). O compilador grava os mesmos 6 bytes de"maçã": as duas strings são iguais. Esses "nomes universais de caractere" são do C99.- U'ç'
- Um literal do tipo
char32_t(C11, de<uchar.h>), que guarda o ponto de código inteiro: 231. Diferente de'ç', ele cabe, porque ochar32_ttem 4 bytes.
O \u é útil quando o arquivo pode ser aberto num editor que não salva em UTF-8: o código só tem ASCII, e o texto sai certo do mesmo jeito.
6. Caracteres largos: wchar_t e setlocale
O C tem um segundo jeito de guardar texto: um array de caracteres largos, wchar_t ("wide character"), em que cada elemento guarda um caractere inteiro, seja ASCII ou acentuado. As funções têm um w no nome: wcslen (o strlen largo), towupper (o toupper largo), iswalpha (o isalpha largo).
#include <locale.h>
#include <stdio.h>
#include <wchar.h>
#include <wctype.h>
int main(void) {
setlocale(LC_CTYPE, "");
wchar_t texto[] = L"Atenção: café";
printf("caracteres: %zu\n", wcslen(texto));
for (size_t i = 0; texto[i] != L'\0'; i++) {
texto[i] = towupper(texto[i]);
}
printf("%ls\n", texto);
printf("iswalpha(L'ç') = %d\n", iswalpha(L'ç') != 0);
printf("sizeof(wchar_t) = %zu\n", sizeof(wchar_t));
return 0;
}
- setlocale(LC_CTYPE, "")
- Liga as configurações regionais do sistema (o locale) para a parte de caracteres (
LC_CTYPE, "character type"). O""quer dizer "a do sistema": no notebook da Ana,pt_BR.UTF-8. Sem isso, o programa fica no locale"C", que só conhece ASCII. - wchar_t texto[] = L"…";
- O
Lna frente das aspas cria um texto largo: umwchar_tpor caractere, mais oL'\0'no fim. - wcslen(texto)
- Conta caracteres: 13, e não 15 bytes.
- towupper(texto[i])
- Converte um caractere inteiro: agora o
çviraÇe oéviraÉ. - %ls
- O formato do
printfpara texto largo. Ele converte de volta para os bytes UTF-8 que o terminal entende, e para isso precisa dosetlocale.
$ gcc -std=c23 -Wall -Wextra largo.c -o largo $ ./largo caracteres: 13 ATENÇÃO: CAFÉ iswalpha(L'ç') = 1 sizeof(wchar_t) = 4
Converter de bytes para largo: mbstowcs
Textos que chegam do teclado ou de um arquivo vêm em bytes (UTF-8). A função mbstowcs ("multibyte string to wide character string") converte para largo e devolve quantos caracteres achou:
#include <locale.h>
#include <stdio.h>
#include <stdlib.h>
#include <wchar.h>
int main(void) {
if (setlocale(LC_CTYPE, "") == NULL) {
printf("locale não disponível\n");
return 1;
}
char texto[] = "pão de queijo";
wchar_t largo[50];
size_t n = mbstowcs(largo, texto, 50);
if (n == (size_t) -1) {
printf("texto com bytes inválidos\n");
return 1;
}
printf("\"%s\": %zu bytes, %zu caracteres\n", texto, sizeof texto - 1, n);
return 0;
}
$ gcc -std=c23 -Wall -Wextra converter.c -o converter $ ./converter "pão de queijo": 14 bytes, 13 caracteres
- setlocale(…) == NULL
- O
setlocaledevolveNULLse o locale pedido não existe na máquina. Confira, como todo retorno. - mbstowcs(largo, texto, 50)
- Converte
textoparalargo, escrevendo no máximo 50 caracteres largos (o tamanho do array). - (size_t) -1
- O sinal de erro: o texto tinha bytes que não formam UTF-8 válido (como o
C3sozinho docortar.c).
Sem o setlocale, o %ls falha
Esquecer o setlocale é o erro clássico com caracteres largos:
$ cat largo-sem.c
#include <stdio.h>
#include <wchar.h>
#include <wctype.h>
int main(void) {
wchar_t texto[] = L"café";
printf("caracteres: %zu\n", wcslen(texto));
texto[3] = towupper(texto[3]);
int r = printf("[%ls]\n", texto);
printf("printf devolveu %d\n", r);
return 0;
}
$ gcc -std=c23 -Wall -Wextra largo-sem.c -o largo-sem
$ ./largo-sem
caracteres: 4
printf devolveu -1 « a linha do café não apareceu
O wcslen funcionou (contar não precisa de locale), mas o printf não soube transformar o é em bytes no locale "C", desistiu e devolveu −1, o sinal de erro do printf. A linha inteira sumiu, sem mensagem.
O editor usa outra biblioteca C (a wasi-libc), e os caracteres largos se comportam um pouco diferente. O largo.c dá a mesma saída. Sem o setlocale, o printf("%ls") também falha, mas mostra o [ antes de desistir ([printf devolveu -1), e o iswalpha(L'ç') responde 1 mesmo sem setlocale, enquanto o gcc do laboratório responde 0. Programe como no Linux: sempre chame o setlocale antes de usar caracteres largos.
Bytes ou largos: qual usar
| Tarefa | Use |
|---|---|
| Guardar, copiar, juntar, mostrar, ler e gravar texto | char com UTF-8, como sempre. Funciona com acentos sem nada especial. |
Procurar um caractere ASCII (' ', ',', '\n') | char: strchr, strtok, strcspn funcionam. |
| Contar letras de um nome | O laço da seção 3, ou mbstowcs |
| Maiúscula, minúscula ou "é letra?" com acentos | wchar_t + setlocale(LC_CTYPE, "") + towupper/iswalpha |
| Ordenar em ordem de dicionário | strcoll com o setlocale |
| Texto "de verdade" em vários idiomas (um editor de texto, por exemplo) | Uma biblioteca especializada, como a ICU |
O wchar_t tem 4 bytes no Linux e no editor, mas 2 bytes no Windows, onde emojis precisam de dois wchar_t. Por isso, muitos programas guardam tudo em UTF-8 (char) e só convertem para largo no momento de classificar ou converter letras.
7. setlocale muda mais do que os acentos
Muitos tutoriais mandam começar o programa com setlocale(LC_ALL, ""). Cuidado: LC_ALL liga todas as configurações regionais, inclusive o formato dos números. Em português, o separador decimal é a vírgula:
#include <locale.h>
#include <stdio.h>
int main(void) {
printf("antes: %.2f\n", 3.14159);
setlocale(LC_ALL, "");
printf("LC_ALL: %.2f\n", 3.14159);
setlocale(LC_ALL, "C");
setlocale(LC_CTYPE, "");
printf("LC_CTYPE: %.2f\n", 3.14159);
return 0;
}
$ gcc -std=c23 -Wall -Wextra virgula.c -o virgula $ ./virgula antes: 3.14 LC_ALL: 3,14 « vírgula: o locale pt_BR mudou o printf LC_CTYPE: 3.14
Para a tela, a vírgula pode até ser bem-vinda. Mas o mesmo vale para o scanf("%lf") e o strtod: depois do LC_ALL, eles esperam 3,14, e um arquivo CSV com 3.14 passa a ser lido errado. O setlocale(LC_ALL, "C") volta ao padrão. A recomendação do guia: quando só precisa de acentos, use setlocale(LC_CTYPE, ""), que muda só a parte de caracteres. (No editor do guia, o setlocale não muda a vírgula: as três linhas saem com ponto.)
8. Outras codificações e o Olá
Antes do UTF-8, cada região usava a sua tabela de 1 byte. No Brasil, a mais comum era a Latin-1 (ISO-8859-1) e a sua prima do Windows, a Windows-1252: nelas, o á é um byte só, o E1. O mesmo texto, em duas embalagens:
#include <stdio.h>
int main(void) {
int c;
while ((c = getchar()) != EOF && c != '\n') {
printf("%02X ", c);
}
printf("\n");
return 0;
}
$ gcc -std=c23 -Wall -Wextra hexa.c -o hexa $ echo 'Olá' | ./hexa 4F 6C C3 A1 « UTF-8: o á são 2 bytes $ echo 'Olá' | iconv -f UTF-8 -t ISO-8859-1 | ./hexa 4F 6C E1 « Latin-1: o á é 1 byte $ echo 'Olá' | iconv -f ISO-8859-1 -t UTF-8 Olá « os bytes UTF-8 lidos como Latin-1
O iconv (programa do Linux que converte codificações) mostra o fenômeno do Olá: pegou os bytes UTF-8 de "Olá" e os leu como se fossem Latin-1. O C3, em Latin-1, é o Ã; o A1 é o ¡. É exatamente o que acontece quando um programa escreve UTF-8 num console que espera outra codificação, como o Prompt de Comando do Windows (aula Instalar no Windows e no Mac). Os bytes estão certos; quem lê é que usa a tabela errada.
Como evitar:
- Salve os arquivos
.cem UTF-8. Todo editor moderno faz isso por padrão (aula Editores). - No Windows, rode
chcp 65001no console antes do programa (65001 é o número do UTF-8 no Windows), ou use o terminal do MSYS2. - Para um sistema antigo que só entende Latin-1, o gcc pode gravar os textos do programa nessa codificação com
-fexec-charset=ISO-8859-1. Com ele, obytes.cmostra oçcomo um byte só,0xE7, e oãcomo0xE3. Só use se precisar mesmo: num terminal UTF-8, esses bytes aparecem como�.
O gcc 15 e o Clang aceitam int ação = 3; (o C23 definiu quais letras do Unicode valem em nomes, aula Nomes de Variáveis). Funciona, mas quase ninguém usa: outros programadores, ferramentas e compiladores antigos podem tropeçar. Acentos, só dentro das aspas.
Quando dá errado
Olá, ç, ã
O que significa: bytes UTF-8 lidos como Latin-1/Windows-1252. Quase sempre é o console do Windows, ou um arquivo de texto aberto com a codificação errada.
Como consertar: chcp 65001 no console do Windows (ou o terminal do MSYS2); abra o arquivo informando a codificação UTF-8.
� no lugar de uma letra
O que significa: o terminal recebeu bytes que não formam um caractere UTF-8 válido: um texto cortado no meio de uma letra (seção 4), um arquivo em Latin-1 mostrado num terminal UTF-8, ou um programa compilado com -fexec-charset=ISO-8859-1.
Como consertar: corte textos no começo de um caractere; converta arquivos antigos com iconv -f ISO-8859-1 -t UTF-8 velho.txt > novo.txt.
printf sai torta nas linhas com acento
O que significa: %-10s conta bytes, e cada letra acentuada tem 1 byte a mais.
Como consertar: conte as letras (seção 3) e complete com espaços à mão, como na missão final.
warning: multi-character character constant [-Wmultichar] com 'ç'
O que significa: um caractere acentuado entre aspas simples são 2 bytes, que não cabem num char. No editor, é erro: character too large for enclosing character literal type.
Como consertar: use uma string ("ç"), um wchar_t (L'ç') ou o número do catálogo (U'ç').
%ls não aparece, e o printf devolve −1
O que significa: o programa usou caracteres largos sem chamar o setlocale, e o printf não conseguiu converter os acentos.
Como consertar: setlocale(LC_CTYPE, ""); no começo do main, com #include <locale.h>.
setlocale, os números saem com vírgula, ou o scanf não lê 3.14
O que significa: o setlocale(LC_ALL, "") ligou também o formato de números do português.
Como consertar: troque por setlocale(LC_CTYPE, "").
Referência rápida
| Escrita | O que faz |
|---|---|
strlen("maçã") | 6: conta bytes, não letras |
(b & 0xC0) != 0x80 | O byte b começa um caractere (não é continuação) |
"ç" | O ç escrito pelo ponto de código (C99) |
U'ç' | O número do catálogo: 231 (char32_t, C11) |
setlocale(LC_CTYPE, "") | Liga o locale do sistema só para caracteres (<locale.h>) |
setlocale(LC_ALL, "") | Liga tudo, inclusive a vírgula nos números |
wchar_t s[] = L"Ação"; | Texto largo: um elemento por caractere (<wchar.h>) |
wcslen(s) | Número de caracteres de um texto largo |
towupper(c) / towlower(c) | Maiúscula / minúscula, com acentos (<wctype.h>) |
iswalpha(c) | "É letra?", com acentos (<wctype.h>) |
printf("%ls", s) | Mostra um texto largo (precisa do setlocale) |
mbstowcs(largo, s, n) | Converte UTF-8 para largo; devolve o número de caracteres ou (size_t) -1 (<stdlib.h>) |
chcp 65001 | Põe o console do Windows em UTF-8 |
Exercícios
1. Quantos bytes?
Em UTF-8, quantos bytes tem o texto "pé" (sem contar o '\0')?
2. O tamanho do array
Quantos bytes ocupa char cidade[] = "Goiânia";, com o '\0'?
3. Só caracteres
Complete a função que liga os acentos sem trocar o ponto dos números pela vírgula:
4. O que é o Unicode?
5. O byte 0xA7 (binário 10100111) aparece no meio de um texto UTF-8. O que ele é?
6. Por que o strchr(texto, ' ') encontra o espaço certo mesmo num texto cheio de acentos?
7. A Ana vê Olá no lugar de Olá no console do Windows. O que aconteceu?
Missão final: a etiqueta de preços alinhada
Missão: a lista de preços da lojinha sai torta, porque o %-12s conta bytes. Troque o %-12s por %s e, depois do nome, mostre você mesmo os espaços que faltam para completar 12 caracteres: conte as letras de cada nome com a regra da seção 3 (os bytes que não são de continuação) e imprima 12 - letras espaços. Os preços têm de ficar alinhados.
Arroz R$ 22.90 Feijão R$ 8.50 Maçã R$ 1.25 Pão de mel R$ 3.00
#include <stdio.h>
int main(void) {
const char *produtos[] = {"Arroz", "Feijão", "Maçã", "Pão de mel"};
double precos[] = {22.90, 8.50, 1.25, 3.00};
for (int p = 0; p < 4; p++) {
printf("%-12sR$ %6.2f\n", produtos[p], precos[p]);
}
return 0;
}
#include <stdio.h>
int main(void) {
const char *produtos[] = {"Arroz", "Feijão", "Maçã", "Pão de mel"};
double precos[] = {22.90, 8.50, 1.25, 3.00};
for (int p = 0; p < 4; p++) {
int letras = 0;
for (int i = 0; produtos[p][i] != '\0'; i++) {
unsigned char b = produtos[p][i];
if ((b & 0xC0) != 0x80) {
letras++;
}
}
printf("%s", produtos[p]);
for (int k = letras; k < 12; k++) {
printf(" ");
}
printf("R$ %6.2f\n", precos[p]);
}
return 0;
}
Na solução, produtos[p][i] é o byte i do produto p: dois índices, como numa tabela (aula Arrays Multidimensionais). O laço dos espaços começa no número de letras e vai até 12: "Feijão" (6 letras) ganha 6 espaços, mesmo tendo 7 bytes. É assim que programas de verdade alinham colunas com acentos.
Resumo
- Unicode é o catálogo (cada caractere tem um número, como U+00E7 para o
ç); UTF-8 é a embalagem: de 1 a 4 bytes por caractere. Letras sem acento ocupam 1 byte; as acentuadas do português, 2. - Uma string em C é uma fileira de bytes:
strlen("maçã")é 6.printf,strcpy,fgetsestrchrcom caracteres ASCII funcionam com acentos sem cuidado especial. - Os bytes de continuação começam com
10: conte os bytes com(b & 0xC0) != 0x80para contar caracteres. - Armadilhas: tamanho de array e largura do
printfem bytes; cortar no meio de uma letra dá�;touppere octype.hnão conhecem acentos;'ç'não cabe numchar;strcmpordena por bytes. - Para letras com acento uma a uma:
setlocale(LC_CTYPE, ""),wchar_t,L"…",wcslen,towupper,iswalpha,%ls. Semsetlocale, o%lsfalha. setlocale(LC_ALL, "")também troca o ponto pela vírgula nos números: prefiraLC_CTYPE.Oláé UTF-8 lido como Latin-1 (o console do Windows:chcp 65001);�é um byte que não forma caractere.