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.
- O que é UTF-8 e por que
"ç"ocupa 2 bytes (mostrando os bytes reais, em hexadecimal). - Por que
.size()conta bytes es[2]pode pegar meio caractere. - Por que
touppernã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:
| Caracteres | Bytes em UTF-8 | Exemplos |
|---|---|---|
| Os 128 da ASCII | 1 | a, Z, 7, !, espaço |
| Letras com acento das línguas europeias | 2 | ç, ã, é, Ó |
| Outros símbolos e escritas | 3 | €, ✓, letras chinesas |
| Emojis e caracteres raros | 4 | 😀 |
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
chartem sinal: os bytes acima de 127 aparecem como números negativos (o primeiro byte doçsairia-61). Convertendo paraunsigned 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::coutmostraria o byte como caractere. Comoint, ele mostra o número. - std::hex
- Mostra os números em hexadecimal a partir daqui (
std::decvolta 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.
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:
a3: uma peça de continuação sem começo. Lixo:�.c3 a7: por coincidência, as peças doçficaram na ordem certa!ç.c3seguido de61: um começo de caractere cuja continuação não veio.�.61 6d:aem.
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
0xna frente avisa).C0é11000000em binário: dois bits1e seis0. - 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. ComC0, 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ão10, 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
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:
- O arquivo
.cpp: o editor precisa salvá-lo em UTF-8. Todos os editores modernos fazem isso por padrão (veja Editores e IDEs). - 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. - 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 mostrama├º├úno lugar demaçã: cada peça é desenhada como uma letra separada. (Esse texto foi obtido decodificando os 6 bytes demaçã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 acentos | Cuidado 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 find | std::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
� 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).
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 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.
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 / conceito | O que é |
|---|---|
| UTF-8 | Formato 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) != 0x80 | Verdadeiro se o byte começa um caractere (não é continuação) |
od -c, od -An -tx1 | No terminal: mostra os bytes exatos que um programa escreveu |
chcp 65001 | Windows: põe o terminal em UTF-8 |
| ICU | Biblioteca 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:
3. O byte sem sinal
Complete a conversão que transforma o char c num número de 0 a 255:
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). toupperignora as letras acentuadas (MAçã);std::reversedesmonta 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.