C++ Acentos e UTF-8

Nas últimas aulas, os acentos apareceram várias vezes como "o problema": "maçã" tem tamanho 6, toupper não transforma o ç, "Olá, " ocupa 7 caixinhas. Nesta aula você vai entender por quê, olhando os bytes de verdade, e vai aprender o que é seguro e o que é perigoso fazer com textos em português. Não é um defeito do C++: é o jeito como o computador guarda letras, e todo programador que trabalha com português precisa conhecê-lo.

Você vai aprender
  • O que é UTF-8 e por que "ç" ocupa 2 bytes (mostrando os bytes reais, em hexadecimal).
  • Por que .size() conta bytes e s[2] pode pegar meio caractere.
  • Por que toupper não converte ç e á, e por que inverter "maçã" vira lixo.
  • Como contar as letras de verdade, pulando os bytes de continuação.
  • O que fazer para o terminal mostrar os acentos (inclusive no Windows) e quais bibliotecas resolvem o problema por completo.

Todos os programas desta aula foram compilados com o g++ 15.2 na máquina fictícia da Ana (com o sistema em pt_BR.UTF-8), na pasta ~/cpp, com o comando padrão g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. Quando a tela mostra um símbolo estranho, a aula também mostra os bytes exatos com od, para você ver o que o programa realmente escreveu.


A ideia: letras são números, e alguns números são grandes

O computador não guarda letras: guarda números. Existe uma tabela combinada que diz "o número 97 é a letra a, o 65 é o A". A primeira tabela famosa, a ASCII (dos anos 1960, americana), tem só 128 posições: letras sem acento, dígitos, pontuação. Cada uma cabe num byte, a "caixinha" de memória que um char ocupa (veja Caracteres).

Só que o mundo tem ç, ã, €, letras gregas, japonesas, emojis… Mais de cem mil caracteres. A tabela que dá um número a todos eles se chama Unicode. Um número tão grande não cabe num byte. O UTF-8 é o jeito de guardar esses números usando de 1 a 4 bytes por caractere:

CaracteresBytes em UTF-8Exemplos
Os 128 da ASCII1a, Z, 7, !, espaço
Letras com acento das línguas europeias2ç, ã, é, Ó
Outros símbolos e escritas3€, ✓, letras chinesas
Emojis e caracteres raros4😀

Pense em peças de montar: a maioria das letras é uma peça só, mas o ç é feito de duas peças encaixadas. O std::string é um colar de peças (bytes), não de letras. Ele não sabe quais peças formam uma letra só. Daí vêm todas as surpresas desta aula.


1. Vendo os bytes de verdade

Vamos imprimir cada byte de "maçã" como número, em hexadecimal (base 16, com dígitos de 0 a f, o jeito tradicional de escrever bytes; cada byte vira exatamente dois dígitos):

#include <iostream>
#include <string>

int main() {
    std::string palavra = "maçã";
    std::cout << palavra << " tem " << palavra.size() << " bytes:\n";
    for (char c : palavra) {
        unsigned char byte = static_cast<unsigned char>(c);
        std::cout << std::hex << static_cast<int>(byte) << ' ';
    }
    std::cout << '\n';
}
$ g++ -std=c++23 -Wall -Wextra bytes.cpp -o bytes
$ ./bytes
maçã tem 6 bytes:
6d 61 c3 a7 c3 a3 
for (char c : palavra)
Passa por cada byte da string (aula Acessar Caracteres). São 6 voltas, não 4.
static_cast<unsigned char>(c)
No g++ do Linux, o char tem sinal: os bytes acima de 127 aparecem como números negativos (o primeiro byte do ç sairia -61). Convertendo para unsigned char, ele aparece como o valor "de verdade" do byte, de 0 a 255 (veja Conversão de Tipos).
static_cast<int>(byte)
Sem isso, o std::cout mostraria o byte como caractere. Como int, ele mostra o número.
std::hex
Mostra os números em hexadecimal a partir daqui (std::dec volta para decimal).

Lendo a saída: 6d é o m, 61 é o a (os mesmos números da ASCII). Depois, c3 a7 são as duas peças do ç, e c3 a3 as duas peças do ã. Quatro letras, seis bytes. Veja mais caracteres:

#include <iostream>
#include <string>

int main() {
    for (std::string s : {"a", "ç", "ã", "€", "😀"}) {
        std::cout << s << ": " << s.size() << " byte(s):";
        for (char c : s) {
            std::cout << ' ' << std::hex << static_cast<int>(static_cast<unsigned char>(c));
        }
        std::cout << std::dec << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra bytes2.cpp -o bytes2
$ ./bytes2
a: 1 byte(s): 61
ç: 2 byte(s): c3 a7
ã: 2 byte(s): c3 a3
€: 3 byte(s): e2 82 ac
😀: 4 byte(s): f0 9f 98 80

Repare num padrão que vai ser útil na seção 5: o primeiro byte de um caractere de várias peças começa com c, e ou f; os bytes seguintes (as "peças de continuação") ficam sempre entre 80 e bf.

Experimente: troque "maçã" pelo seu nome, pelo nome da sua cidade ou por "Ação!" e veja quantos bytes cada um tem. Quais letras ocupam dois bytes?

#include <iostream>
#include <string>

int main() {
    std::string palavra = "maçã";
    std::cout << palavra << " tem " << palavra.size() << " bytes:\n";
    for (char c : palavra) {
        unsigned char byte = static_cast<unsigned char>(c);
        std::cout << std::hex << static_cast<int>(byte) << ' ';
    }
    std::cout << '\n';
}

2. .size() conta bytes, e os índices também

Agora o "problema" da aula Strings fica claro: .size() devolve quantas peças há no colar, e não quantas letras uma pessoa contaria. O mesmo vale para os índices: s[2] é a terceira peça, não a terceira letra. Em "maçã", a terceira peça é a primeira metade do ç:

#include <iostream>
#include <string>

int main() {
    std::string palavra = "maçã";
    std::cout << "[" << palavra[0] << "]\n";
    std::cout << "[" << palavra[2] << "]\n";
    std::cout << "[" << palavra[2] << palavra[3] << "]\n";
    std::cout << "[" << palavra.substr(0, 3) << "]\n";
}
$ g++ -std=c++23 -Wall -Wextra meio.cpp -o meio
$ ./meio
[m]
[�]                « meio ç: o terminal não sabe desenhar
[ç]                « as duas metades juntas formam o ç
[ma�]              « substr(0, 3) cortou o ç ao meio
$ ./meio | od -c
0000000   [   m   ]  \n   [ 303   ]  \n   [ 303 247   ]  \n   [   m   a
0000020 303   ]  \n
0000023

O � (um losango com interrogação) é o que o terminal desenha quando recebe um pedaço de caractere que não forma nada: o "caractere de substituição". O od -c mostra o que o programa escreveu de fato: o byte 303 (em octal; é o c3 em hexadecimal) sozinho. Junto com o 247 (a7), vira ç.

O substr(0, 3) é o caso mais traiçoeiro: "os 3 primeiros caracteres" de maçã deveria ser maç, mas são 3 bytes, e o ç ficou partido. Truncar nomes para caber numa etiqueta, "os 10 primeiros caracteres do produto", é um lugar clássico desse erro.


3. toupper não converte ç e á

A receita de maiúsculas da aula Funções de String trabalha byte a byte. Com acentos:

#include <cctype>
#include <iostream>
#include <string>

int main() {
    std::string palavra = "maçã é ótima";
    for (char& c : palavra) {
        c = std::toupper(static_cast<unsigned char>(c));
    }
    std::cout << palavra << '\n';
}
$ g++ -std=c++23 -Wall -Wextra upper.cpp -o upper
$ ./upper
MAçã é óTIMA
$ ./upper | od -c
0000000   M   A 303 247 303 243     303 251     303 263   T   I   M   A
0000020  \n
0000021

As letras sem acento viraram maiúsculas; ç, ã, é e ó ficaram como estavam. O toupper recebe um byte de cada vez. Ele vê c3, depois a7, e nenhum dos dois, sozinho, é uma letra que ele conheça. Para virar Ç, seria preciso trocar o par c3 a7 pelo par c3 87, e o toupper não enxerga pares.

Ainda bem que ele não mexeu

Repare que o resultado continua sendo UTF-8 válido: os acentos só não mudaram. É por causa do static_cast<unsigned char> e do locale "C" (o padrão de todo programa): nessa configuração, o toupper só mexe nas letras de a a z. Sem o static_cast, os bytes negativos dariam comportamento indefinido.


4. Inverter uma palavra com acento vira lixo

#include <algorithm>
#include <iostream>
#include <string>

int main() {
    std::string palavra = "maçã";
    std::reverse(palavra.begin(), palavra.end());
    std::cout << palavra << '\n';
}
$ g++ -std=c++23 -Wall -Wextra inverter.cpp -o inverter
$ ./inverter
�ç�am
$ ./inverter | od -An -tx1
 a3 c3 a7 c3 61 6d 0a

O std::reverse inverteu as peças: 6d 61 c3 a7 c3 a3 virou a3 c3 a7 c3 61 6d. Leia da esquerda para a direita, como o terminal leu:

  1. a3: uma peça de continuação sem começo. Lixo: �.
  2. c3 a7: por coincidência, as peças do ç ficaram na ordem certa! ç.
  3. c3 seguido de 61: um começo de caractere cuja continuação não veio. �.
  4. 61 6d: a e m.

O resultado, �ç�am, deveria ser ãçam. Com outras palavras, o lixo muda, mas a causa é sempre a mesma: algoritmos que trabalham peça por peça desmontam as letras de várias peças.


5. Contar as letras de verdade

Lembra do padrão da seção 1? No UTF-8, toda peça de continuação tem a forma 10xxxxxx em binário: os dois primeiros bits são 1 e 0 (por isso elas ficam entre 80 e bf em hexadecimal). Então, para contar letras, basta contar os bytes que não são continuação:

#include <iostream>
#include <string>

int main() {
    std::string palavra = "maçã";
    int letras = 0;
    for (char c : palavra) {
        if ((static_cast<unsigned char>(c) & 0xC0) != 0x80) {
            letras++;
        }
    }
    std::cout << palavra << ": " << palavra.size() << " bytes, " << letras << " letras\n";
}
$ g++ -std=c++23 -Wall -Wextra contar.cpp -o contar
$ ./contar
maçã: 6 bytes, 4 letras
0xC0
Um número escrito em hexadecimal (o 0x na frente avisa). C0 é 11000000 em binário: dois bits 1 e seis 0.
byte & 0xC0
O & sozinho é o "E bit a bit": ele compara os dois números bit por bit e só mantém os bits que são 1 nos dois. Com C0, funciona como uma máscara que esconde os 6 bits da direita e deixa só os 2 primeiros do byte. (Não confunda com o && da aula Operadores Lógicos.)
!= 0x80
80 é 10000000: os 2 primeiros bits são 10, a marca da continuação. Se o resultado da máscara é diferente disso, o byte começa um caractere, e conta como uma letra.

Veja a máscara em ação nos três tipos de byte de maçã: o c3 (começo do ç), o a7 (continuação) e o 61 (o a):

$ cat mascara.cpp
#include <iostream>
#include <string>

int main() {
    int c1 = 0xC3, c2 = 0xA7, c3 = 0x61;
    std::cout << (c1 & 0xC0) << ' ' << (c2 & 0xC0) << ' ' << (c3 & 0xC0) << '\n';
    std::cout << std::hex << (c1 & 0xC0) << ' ' << (c2 & 0xC0) << ' ' << (c3 & 0xC0) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra mascara.cpp -o mascara
$ ./mascara
192 128 64
c0 80 40           « só o a7 (continuação) dá 80
"Letra" ainda é uma simplificação

Esse laço conta caracteres Unicode (os "pontos de código"), e isso resolve o português do dia a dia. Mas existem casos em que o que uma pessoa vê como uma letra é formado por dois caracteres Unicode: um e seguido de um "acento solto" que se encaixa nele, ou emojis compostos (uma família, uma bandeira). Para esses, só uma biblioteca especializada conta certo (seção 7).


6. O terminal também precisa falar UTF-8

O programa só escreve bytes. Quem transforma c3 a7 no desenho do ç é o terminal. Para os acentos aparecerem certos, três coisas precisam "falar" UTF-8:

  1. O arquivo .cpp: o editor precisa salvá-lo em UTF-8. Todos os editores modernos fazem isso por padrão (veja Editores e IDEs).
  2. O compilador: o g++ e o Clang leem e gravam os textos em UTF-8 por padrão. O compilador da Microsoft (MSVC) precisa da opção /utf-8.
  3. O terminal: no Linux e no macOS, já é UTF-8. No Windows, o terminal antigo (o cmd) usa outra tabela (no Brasil, a "página de código 850"), e o mesmo programa mostra ma├º├ú no lugar de maçã: cada peça é desenhada como uma letra separada. (Esse texto foi obtido decodificando os 6 bytes de maçã com a tabela 850; não é uma captura de tela do Windows.)

No Windows, o comando chcp 65001 ("change code page", trocar a tabela de caracteres) muda o terminal para UTF-8 (65001 é o número que o Windows dá ao UTF-8). A sessão abaixo é ilustrativa: ela não pôde ser conferida no laboratório, que é um Linux.

C:\Users\ana\cpp> chcp 65001
Página de código ativa: 65001       « ilustrativo: mensagem do Windows em português
C:\Users\ana\cpp> maca.exe          « o programa maca.cpp da aula Strings
maca: 4
maçã: 6

O Windows Terminal (o novo, que vem no Windows 11) já costuma vir em UTF-8. A instalação no Windows é explicada em Instalar no Windows e macOS.


7. A solução completa: bibliotecas

A biblioteca padrão do C++ ainda não tem ferramentas completas para texto Unicode: maiúsculas de qualquer língua, contagem de "letras visíveis", ordem alfabética correta com acentos. Quando o seu programa precisa disso para valer, use uma biblioteca especializada:

  • ICU (International Components for Unicode): a referência mundial, usada por navegadores e sistemas operacionais. Faz maiúsculas e minúsculas em qualquer língua, conta letras visíveis, ordena alfabeticamente conforme o idioma, formata datas e números. A aula Usar Bibliotecas Externas mostra como instalar e usar bibliotecas.

Para a maioria dos programas de quem está aprendendo, as regras da próxima seção bastam.


A regra prática para texto em português

Cuidado com tamanho, índices e maiúsculas. Todo o resto funciona.

Funciona normalmente com acentosCuidado com acentos
Guardar, mostrar, ler com getline.size() como "número de letras": conta bytes
Juntar com + e +=s[i] e .at(i) com índice fixo: podem pegar meio caractere
Comparar com ==substr(0, n) com n fixo: pode cortar uma letra ao meio
find, replace, contains, starts_with com textos inteiros (find("reação") acha certo)toupper/tolower: ignoram as letras acentuadas
substr com posições vindas do findstd::reverse e outros algoritmos byte a byte: desmontam as letras
Laços que só copiam ou procuram bytes ASCII (' ', ',')Ordem com <: "água" < "zebra" dá false, porque o c3 do á vem depois de todas as letras sem acento

Quando dá errado

Aparece � no meio do texto

O que significa: o programa escreveu um pedaço de caractere: um índice (s[i]) ou um substr com tamanho fixo cortou uma letra acentuada ao meio, ou um std::reverse desmontou as peças.

Como consertar: não corte textos com acento em posições fixas; use posições vindas do find, ou ande pelo texto pulando os bytes de continuação (seção 5).

Aparece maçã ou ma├º├ú no lugar de maçã

O que significa: os bytes estão certos, mas o terminal (ou o programa que abriu o arquivo) não está em UTF-8, e desenha cada peça como uma letra separada. ma├º├ú é o que a tabela 850 do cmd brasileiro faz com esses bytes; maçã é o que a tabela "Latin-1", usada por planilhas que abrem arquivos CSV, faz.

Como consertar: ponha o terminal em UTF-8 (chcp 65001 no Windows) ou escolha "UTF-8" ao abrir o arquivo.

O tamanho não bate com o que você contou

O que significa: .size() conta bytes. Cada letra acentuada soma 2.

Como consertar: se você precisa do número de letras (para validar "nome com no máximo 20 letras", por exemplo), use o laço da seção 5.

As maiúsculas ficaram pela metade: MAçã

O que significa: std::toupper trabalha byte a byte e não conhece letras de dois bytes.

Como consertar: para poucos casos, troque os pares à mão com find/replace ("ç" → "Ç"); para valer, use a ICU.


Referência rápida

Escrita / conceitoO que é
UTF-8Formato que guarda cada caractere Unicode em 1 a 4 bytes. ASCII = 1 byte; acentos do português = 2.
s.size()Número de bytes
static_cast<unsigned char>(c)O byte como número de 0 a 255 (sem isso, os bytes de acento são negativos)
std::cout << std::hex << static_cast<int>(byte)Mostra o byte em hexadecimal
(byte & 0xC0) != 0x80Verdadeiro se o byte começa um caractere (não é continuação)
od -c, od -An -tx1No terminal: mostra os bytes exatos que um programa escreveu
chcp 65001Windows: põe o terminal em UTF-8
ICUBiblioteca para Unicode completo

Exercícios

1. Quantos bytes?

Quanto vale std::string("pé").size()?

2. Continuação

Complete a condição que é verdadeira quando o byte b (um unsigned char) começa um caractere:

if ((b & 0xC0) != )

3. O byte sem sinal

Complete a conversão que transforma o char c num número de 0 a 255:

static_cast<>(c)

4. Com std::string s = "ação";, o que s[1] contém?

5. O que a receita com std::toupper faz com "pão"?

6. Qual destas operações é segura com textos acentuados?

Missão final: contar letras de verdade

Missão: para cada palavra, o programa já mostra os bytes, mas as letras estão sempre 0. Escreva o laço que conta as letras de verdade, pulando os bytes de continuação com & 0xC0, para a saída ficar:

João: 5 bytes, 4 letras
coração: 9 bytes, 7 letras
pão: 4 bytes, 3 letras
#include <iostream>
#include <string>

int main() {
    for (std::string p : {"João", "coração", "pão"}) {
        int letras = 0;
        // conte as letras de p aqui

        std::cout << p << ": " << p.size() << " bytes, " << letras << " letras\n";
    }
}
#include <iostream>
#include <string>

int main() {
    for (std::string p : {"João", "coração", "pão"}) {
        int letras = 0;
        for (char c : p) {
            if ((static_cast<unsigned char>(c) & 0xC0) != 0x80) {
                letras++;
            }
        }
        std::cout << p << ": " << p.size() << " bytes, " << letras << " letras\n";
    }
}

(O for (std::string p : {"João", "coração", "pão"}) repete o bloco uma vez para cada palavra da lista entre chaves; veja for de Intervalo.)


Resumo

  • O computador guarda números. Unicode dá um número a cada caractere; UTF-8 guarda cada um em 1 a 4 bytes. Letras sem acento: 1 byte. ç, ã, é: 2 bytes (c3 a7, c3 a3…).
  • O std::string é um colar de bytes: .size() conta bytes, s[i] pega um byte, e com acento isso pode ser meio caractere (� na tela).
  • toupper ignora as letras acentuadas (MAçã); std::reverse desmonta as letras de várias peças (�ç�am).
  • Para contar letras: conte os bytes em que (byte & 0xC0) != 0x80.
  • O terminal precisa estar em UTF-8 (no Windows, chcp 65001). Para Unicode completo, use a ICU.
  • Regra para português: cuidado com tamanho, índices e maiúsculas.