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.

Você vai aprender
  • 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 do printf, cortar uma letra ao meio, toupper e char c = 'ç'.
  • Como escrever acentos por código (ç) e o que são os caracteres largos (wchar_t) e o setlocale.
  • 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-se U+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.c foi salvo em UTF-8, e o gcc copia os bytes do texto do jeito que estão no arquivo.
strlen / sizeof
O strlen conta os bytes até o '\0'; o sizeof mede 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 char comum, 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):

CaracteresBytes1º byteBytes seguintesExemplos
ASCII (0 a 127)10xxxxxxx (00 a 7F)nenhuma, 7, !, espaço
Até 20472110xxxxx (C2 a DF)10xxxxxx (80 a BF)ç ã é Ç, grego, árabe
Até 6553531110xxxx (E0 a EF)2 × 10xxxxxx€, chinês, japonês
Até 1114111411110xxx (F0 a F4)3 × 10xxxxxxemojis, 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 isso printf, strcpy, strcat, fgets e strchr(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) com 0xC0 (binário 11000000) apaga tudo menos os 2 primeiros bits do byte.
!= 0x80
0x80 é o binário 10000000. Se os 2 primeiros bits não são 10, 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.)

Letra, ponto de código e o que a pessoa vê

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 o char32_t tem 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 L na frente das aspas cria um texto largo: um wchar_t por caractere, mais o L'\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 printf para texto largo. Ele converte de volta para os bytes UTF-8 que o terminal entende, e para isso precisa do setlocale.
$ 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 setlocale devolve NULL se o locale pedido não existe na máquina. Confira, como todo retorno.
mbstowcs(largo, texto, 50)
Converte texto para largo, 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 C3 sozinho do cortar.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.

No editor do guia

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

TarefaUse
Guardar, copiar, juntar, mostrar, ler e gravar textochar 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 nomeO laço da seção 3, ou mbstowcs
Maiúscula, minúscula ou "é letra?" com acentoswchar_t + setlocale(LC_CTYPE, "") + towupper/iswalpha
Ordenar em ordem de dicionáriostrcoll 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 .c em UTF-8. Todo editor moderno faz isso por padrão (aula Editores).
  • No Windows, rode chcp 65001 no 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, o bytes.c mostra o ç como um byte só, 0xE7, e o ã como 0xE3. Só use se precisar mesmo: num terminal UTF-8, esses bytes aparecem como �.
E os nomes de variáveis?

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

O texto aparece como 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.

O texto aparece com � 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.

A tabela do 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'ç').

Uma linha com %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>.

Depois do 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

EscritaO que faz
strlen("maçã")6: conta bytes, não letras
(b & 0xC0) != 0x80O 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 65001Põ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')?

strlen("pé") ==

2. O tamanho do array

Quantos bytes ocupa char cidade[] = "Goiânia";, com o '\0'?

sizeof cidade ==

3. Só caracteres

Complete a função que liga os acentos sem trocar o ponto dos números pela vírgula:

setlocale(, "");

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, fgets e strchr com caracteres ASCII funcionam com acentos sem cuidado especial.
  • Os bytes de continuação começam com 10: conte os bytes com (b & 0xC0) != 0x80 para contar caracteres.
  • Armadilhas: tamanho de array e largura do printf em bytes; cortar no meio de uma letra dá �; toupper e o ctype.h não conhecem acentos; 'ç' não cabe num char; strcmp ordena por bytes.
  • Para letras com acento uma a uma: setlocale(LC_CTYPE, ""), wchar_t, L"…", wcslen, towupper, iswalpha, %ls. Sem setlocale, o %ls falha.
  • setlocale(LC_ALL, "") também troca o ponto pela vírgula nos números: prefira LC_CTYPE.
  • Olá é UTF-8 lido como Latin-1 (o console do Windows: chcp 65001); � é um byte que não forma caractere.