Texto, endianness e alinhamento

TL;DR

Texto é número disfarçado: ASCII mapeou 128 caracteres em 7 bits, Unicode mapeou 1,1 milhão de pontos de código — mas mapa não é codificação. UTF-8 venceu por ser ASCII-compatível e compacto. Endianness decide qual byte de um inteiro vai para o endereço mais baixo: x86/ARM são little-endian, a rede é big-endian. Alinhamento de memória obriga o compilador a inserir padding em structs; reordenar campos na ordem “maior para menor” elimina bytes desperdiçados e melhora uso de cache.


1. Por que texto é número

O hardware só entende bits. Para armazenar a letra A, alguém precisou decidir: “vou usar o número 65 para representar A.” Essa convenção — tabela que mapeia caracteres a números — é chamada codificação de caracteres.

Sem um acordo sobre essa tabela, dois computadores trocando bytes lêem coisas diferentes. “Plain text” não existe; todo arquivo de texto traz implícita uma codificação.


2. ASCII — o ponto de partida

O ASCII (American Standard Code for Information Interchange, 1963) usa 7 bits por caractere → 128 posições possíveis (0127).

Faixa decimalConteúdo
0–31 e 127Controle (tab, newline, null…)
32–47Pontuação e espaço
48–57Dígitos 09
65–90Letras maiúsculas AZ
97–122Letras minúsculas az

128 caracteres bastam para o inglês. Mas e o ç, o é, o ñ, o Ö?

A solução provisória foi usar o 8º bit extra: Latin-1 / ISO 8859-1 mapeia 256 posições, cobrindo boa parte das línguas ocidentais. Mas 256 posições não comportam árabe, chinês, japonês, emojis — e cada região inventou sua própria extensão de 8 bits. Resultado: mojibake — caracteres corrompidos porque dois sistemas usam tabelas incompatíveis.


3. Unicode — o mapa universal

O Unicode é um mapa de caracteres, não uma codificação. Ele define um espaço de 1,1 milhão de code points, escritos como U+XXXX (hex).

Alguns exemplos:

Code pointCaractereNome
U+0041ALATIN CAPITAL LETTER A
U+00E9éLATIN SMALL LETTER E WITH ACUTE
U+4E2DCJK UNIFIED IDEOGRAPH
U+1F600😀GRINNING FACE

O Unicode diz o que é cada caractere. Não diz como armazená-lo em bytes — isso é função da codificação (UTF-8, UTF-16, UTF-32).

Code point ≠ caractere visível (grapheme)

O é pode ser U+00E9 (1 code point) ou e (U+0065) + combining acute (U+0301) = 2 code points. Ambos produzem o mesmo glyph. Isso se chama normalização Unicode (NFC vs NFD).


4. UTF-8, UTF-16 e UTF-32

A tabela abaixo resume as três principais codificações:

CodificaçãoTamanho por charASCII-compat?Uso típico
UTF-81–4 bytesWeb, Linux, maioria do mundo
UTF-162 ou 4 bytesWindows interno, Java, JS
UTF-324 bytes fixosProcessamento interno raro

Como UTF-8 codifica um code point

UTF-8 usa prefixos de bits para indicar quantos bytes o caractere ocupa.

1 byte  → 0xxxxxxx           (U+0000..U+007F, ASCII puro)
2 bytes → 110xxxxx 10xxxxxx  (U+0080..U+07FF)
3 bytes → 1110xxxx 10xxxxxx 10xxxxxx       (U+0800..U+FFFF)
4 bytes → 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx  (U+10000..U+10FFFF)

Os bytes de continuação sempre começam com 10, o que torna qualquer byte isolado auto-identificável: você nunca confunde o início de um caractere com o meio de outro.

Leitura do diagrama: os x marcam onde os bits do code point são encaixados. Para U+00E9 (é), o valor decimal é 233 = 11101001b. Cabe em 2 bytes: 11000011 10101001 = 0xC3 0xA9.

Bytes na memória (hex)Significado
41A (U+0041, ASCII)
C3 A9é (U+00E9, 2 bytes UTF-8)
E4 B8 AD中 (U+4E2D, 3 bytes)
F0 9F 98 80😀 (U+1F600, 4 bytes)

Por que UTF-8 ganhou

  1. ASCII-compatível: qualquer texto ASCII puro é UTF-8 válido sem alteração.
  2. Compacto: texto em inglês/código fonte usa 1 byte por caractere.
  3. Auto-sincronizável: se você perder o início de um caractere, pode reposicionar sem ambiguidade.
  4. Sem BOM obrigatório: evita a confusão do Byte Order Mark (que o UTF-16 exige).

UTF-16 domina ambientes Windows e Java por razões históricas — foi adotado quando se acreditava que 2 bytes seriam suficientes para todo o Unicode. Quando o Unicode expandiu além de U+FFFF, UTF-16 precisou de surrogate pairs (dois u16 encadeados para representar um code point acima de U+FFFF), complexificando o processamento.

Surrogate pairs em detalhe

Code points de U+10000 a U+10FFFF (Supplementary Planes, onde ficam muitos emojis) não cabem em 16 bits. UTF-16 usa dois valores especiais:

  • High surrogate: U+D800 a U+DBFF (1.024 valores)
  • Low surrogate: U+DC00 a U+DFFF (1.024 valores)

Juntos, representam 1.024 × 1.024 = 1.048.576 code points suplementares. O emoji 😀 (U+1F600) vira o par 0xD83D 0xDE00 em UTF-16.

O problema: se você indexar uma string UTF-16 pelo índice (str[i]), pode cair no meio de um surrogate pair e extrair lixo. É a mesma armadilha do byte de continuação no UTF-8, só que em unidades de 16 bits.

UTF-32 resolve esse problema: cada code point ocupa exatamente 4 bytes, então str[i] sempre aponta para um code point válido — mas o custo de memória é 4× o do UTF-8 para texto ASCII.

NFC vs NFD — normalização importa em comparações

O é pode chegar como:

  • NFC (Canonical Decomposition followed by Canonical Composition): U+00E9 — 1 code point.
  • NFD (Canonical Decomposition): U+0065 + U+0301 — 2 code points.

Ambos parecem idênticos na tela, mas "é" == "é" retorna False em Python quando um é NFC e o outro é NFD. Bancos de dados, índices de busca e comparações de senha podem falhar silenciosamente por isso.

Fix: normalize para NFC antes de armazenar ou comparar.

import unicodedata
s_nfc = unicodedata.normalize("NFC", s)

5. Camadas: byte → code point → grapheme → string

Uma confusão clássica: "😀".length retorna 2 em JavaScript, não 1. Por quê?

Camada        Significado                         Exemplo: "é😀"
-------       --------------------------------    ------------------
Byte          Unidade de armazenamento físico     C3 A9 F0 9F 98 80
Code point    Posição no mapa Unicode             U+00E9, U+1F600
Grapheme      Caractere visível (cluster)         é, 😀

Leitura da tabela: a string "é😀" tem 6 bytes em UTF-8, 2 code points, e 2 graphemes. Em JavaScript (UTF-16 interno), length conta unidades UTF-16: é = 1 unidade, 😀 = 2 unidades (surrogate pair) → length = 3. Para contar graphemes corretamente em JS, use Intl.Segmenter.

Armadilha do .length com emojis

Em Python 3, len("😀") = 1 (conta code points). Em JavaScript, "😀".length = 2 (conta unidades UTF-16). Em Ruby, depende do encoding. Nunca assuma que .length = número de caracteres visíveis.


6. Endianness — qual byte vem primeiro?

Um inteiro de 32 bits como 0x12345678 ocupa 4 bytes na memória. Mas em qual ordem?

Depende da arquitetura:

  • Big-endian: o byte mais significativo (MSB) vai para o endereço mais baixo.
  • Little-endian: o byte menos significativo (LSB) vai para o endereço mais baixo.

0x12345678 em memória (4 bytes, endereços crescendo →)

EndereçoBig-endianLittle-endian
0x10000x120x78
0x10010x340x56
0x10020x560x34
0x10030x780x12

Leitura da tabela: em big-endian, leia da esquerda para a direita e você vê o número “natural” (12 34 56 78). Em little-endian, o byte de menor peso fica primeiro, então você lê “de trás para frente” (78 56 34 12). O valor inteiro é o mesmo; só a ordem dos bytes em memória difere.

x86 e ARM (modo padrão) são little-endian. Processadores MIPS/SPARC tradicionais e a rede TCP/IP usam big-endian (chamado de network byte order).

graph LR
    A["Inteiro: 0x12345678"] --> B{"Arquitetura?"}
    B -->|"Big-endian"| C["mem[0]: 0x12\nmem[1]: 0x34\nmem[2]: 0x56\nmem[3]: 0x78"]
    B -->|"Little-endian x86"| D["mem[0]: 0x78\nmem[1]: 0x56\nmem[2]: 0x34\nmem[3]: 0x12"]

Leitura do diagrama: o mesmo inteiro produz layouts de bytes opostos. Quando você serializa dados binários e lê em outra máquina sem conversão, o valor inteiro aparece completamente diferente.

Network byte order e as funções hton/ntoh

Protocolos de rede (IP, TCP, UDP) adotaram big-endian como padrão. Em C, as funções htonl/htons (host to network long/short) convertem um valor do endian da sua máquina para big-endian antes de enviar; ntohl/ntohs fazem o caminho inverso ao receber.

uint32_t valor = 0x12345678;
uint32_t para_rede = htonl(valor); // big-endian, pronto pra send()

Em plataformas little-endian, htonl inverte os bytes. Em big-endian, é um no-op.

Endianness em arquivos binários

Formatos como PNG, TIFF, WAV e BMP especificam explicitamente o byte order no cabeçalho. PNG usa big-endian. WAV usa little-endian. Se você ler um uint32_t de um arquivo WAV num processador big-endian sem converter, o valor será nonsense.


7. Alinhamento de memória

O hardware lê memória em blocos do tamanho da word (4 bytes em 32-bit, 8 bytes em 64-bit). Para que isso funcione com eficiência máxima, o compilador alinha cada variável no endereço múltiplo de seu tamanho.

TipoTamanhoAlinhamento típico
char1 byte1 byte (qualquer endereço)
short2 bytesmúltiplo de 2
int4 bytesmúltiplo de 4
double8 bytesmúltiplo de 8
ponteiro 64-bit8 bytesmúltiplo de 8

Se um int estiver no endereço 0x1001 (desalinhado), o hardware precisa de duas leituras de 4 bytes e uma operação de recombinação. Algumas arquiteturas (ARM v6 e anteriores, MIPS) simplesmente lançam uma exceção em acesso desalinhado.

Padding em structs

O compilador insere bytes de padding entre (e depois dos) campos para manter o alinhamento de cada membro.

Struct “descuidada” — ordem char, int, char:

struct Ruim {
    char  a;      // 1 byte  → endereço 0
    // 3 bytes padding
    int   b;      // 4 bytes → endereço 4
    char  c;      // 1 byte  → endereço 8
    // 3 bytes padding (para alinhar próxima instância)
};
// sizeof = 12 bytes

Struct “otimizada” — maior para menor:

struct Boa {
    int   b;      // 4 bytes → endereço 0
    char  a;      // 1 byte  → endereço 4
    char  c;      // 1 byte  → endereço 5
    // 2 bytes padding (alinha o final)
};
// sizeof = 8 bytes

A tabela abaixo ilustra o layout byte a byte das duas structs:

Offsetstruct Ruimstruct Boa
0a (char)b (int byte 0)
1paddingb (int byte 1)
2paddingb (int byte 2)
3paddingb (int byte 3)
4b (int byte 0)a (char)
5b (int byte 1)c (char)
6b (int byte 2)padding
7b (int byte 3)padding
8c (char)
9–11padding

Leitura da tabela: a struct Ruim desperdiça 6 bytes em padding; a struct Boa apenas 2. A regra prática: ordene campos do maior para o menor tipo e agrupe campos do mesmo tamanho.

graph TD
    A["struct Ruim: 12 bytes"] --> B["char a — 1 byte"]
    A --> C["3 bytes PADDING"]
    A --> D["int b — 4 bytes"]
    A --> E["char c — 1 byte"]
    A --> F["3 bytes PADDING"]
    G["struct Boa: 8 bytes"] --> H["int b — 4 bytes"]
    G --> I["char a — 1 byte"]
    G --> J["char c — 1 byte"]
    G --> K["2 bytes PADDING"]

Leitura do diagrama: os blocos de padding aparecem onde o compilador precisou “pular” bytes para alinhar o próximo campo. A versão otimizada empilha os chars menores depois do int, reduzindo o desperdício.

Verificando alinhamento em código real

Em C/C++, use offsetof(struct, campo) para inspecionar onde cada campo começa:

#include <stddef.h>
#include <stdio.h>
 
struct Ruim { char a; int b; char c; };
struct Boa  { int b; char a; char c; };
 
int main(void) {
    printf("Ruim: sizeof=%zu, offsetof b=%zu\n",
           sizeof(struct Ruim), offsetof(struct Ruim, b));
    printf("Boa:  sizeof=%zu, offsetof b=%zu\n",
           sizeof(struct Boa),  offsetof(struct Boa,  b));
}
// Saída típica (x86-64):
// Ruim: sizeof=12, offsetof b=4
// Boa:  sizeof=8,  offsetof b=0

Em Rust, o compilador reordena campos automaticamente (por padrão) para minimizar padding. Para layout exato compatível com C, use #[repr(C)].

Em Go, o unsafe.Sizeof e unsafe.Offsetof expõem o mesmo. A ferramenta fieldalignment do golang.org/x/tools aponta structs subótimas automaticamente.

Packing forçado

#pragma pack(1) (GCC: __attribute__((packed))) elimina padding. O sizeof diminui, mas acessos a campos desalinhados ficam lentos (ou geram exceção em algumas arquiteturas). Use apenas quando o layout binário precisa ser exato (protocolos, arquivos de formato fixo).

Packing e undefined behavior

Em C, acessar um campo desalinhado por um ponteiro (int *p = &packed.b) é undefined behavior na maioria das arquiteturas. Use memcpy para mover os bytes explicitamente quando precisar de structs packed em código crítico.


8. Conexão com cache e localidade

Structs compactas cabem em menos cache lines. Se você tem um array de struct Ruim (12 bytes cada), 5 elementos ocupam 60 bytes → 1 cache line de 64 bytes quase cheia. Com struct Boa (8 bytes), 8 elementos cabem em 64 bytes → 37% mais elementos por cache line.

No data-oriented design, quando precisa iterar apenas sobre o campo b, cria um array separado de int b[] em vez de um array de structs. Assim, 16 valores de b cabem em uma única cache line de 64 bytes — versus 5 com a struct original.

Veja 11 - Hierarquia de memória e localidade para a análise completa de cache lines e localidade espacial.


9. Prática: bugs comuns e como evitar

flowchart TD
    A["Bug de texto/encoding"] --> B{"Diagnóstico"}
    B --> C["Mojibake: lixo ao exibir"]
    B --> D["length errado com emoji"]
    B --> E["Strings NFD != NFC"]
    C --> F["Causa: encoding errado na leitura\nFix: declare charset explícito\nex: open(f, encoding=utf-8)"]
    D --> G["Causa: length conta unidades\nnão graphemes\nFix: Intl.Segmenter / len em Python"]
    E --> H["Causa: normalização diferente\nFix: unicodedata.normalize NFC"]

Leitura do diagrama: cada caminho representa uma classe de bug de encoding, sua causa raiz e o fix canônico. Todos têm uma causa comum: confundir as camadas (byte, code point, grapheme).

A tabela abaixo consolida as armadilhas mais frequentes em produção:

ArmadilhaCausaFix
Mojibake ao ler arquivoEncoding errado (ex: Latin-1 em vez de UTF-8)Declare encoding='utf-8' explicitamente
"é".length == 2 em JSJS usa UTF-16; é composto = 2 code unitsUse [...str].length ou Intl.Segmenter
BOM inesperado no inícioUTF-8 com BOM gerado pelo WindowsAbra como utf-8-sig no Python; strip 
Campo de BD truncadoVARCHAR(10) conta bytes, não caracteres em algumas DBsUse NVARCHAR ou charset utf8mb4
Ordenação estranhaNormalização mista NFC/NFDNormalize antes de comparar/indexar
Inteiro corrompido via redeEndian do host ≠ network byte orderUse htonl/ntohl ou protocolo com byte order explícito
sizeof(struct) inesperadoPadding implícito do compiladorReordene campos; use static_assert
Arquivo binário lixoLer WAV com lógica PNG (byte order invertido)Leia a spec do formato; converta se necessário

10. Base64 — texto ASCII-safe para dados binários

Às vezes você precisa transportar bytes arbitrários por um canal que só aceita ASCII (email, JSON, URLs). Base64 codifica 3 bytes de entrada em 4 caracteres ASCII (A–Z, a–z, 0–9, +, /), expandindo o tamanho em ~33%.

Não é criptografia — é apenas transporte. Veja 02 - Representação binária de inteiros para entender a aritmética de bits por trás.


11. IEEE 754 e o paralelo com texto

Assim como texto precisou de um padrão universal (Unicode), ponto flutuante precisou do 03 - Ponto flutuante - IEEE 754. Em ambos os casos, a ausência de padrão gera resultados diferentes para os mesmos bytes em máquinas diferentes.


Resumo em uma linha

Texto é número (UTF-8 ganhou); inteiros têm ordem de bytes (x86 = little-endian, rede = big-endian); structs têm padding que você pode eliminar reordenando campos do maior para o menor.


Em entrevista

Esses três tópicos aparecem regularmente em telas de sistemas, entrevistas de backend e qualquer posição que lide com protocolos binários, internacionalização ou otimização de memória.

Reforce que você sabe a diferença entre Unicode (mapa) e UTF-8 (codificação). Se mencionar endianness, conecte com htonl/ntohl e com serialização binária. Para alinhamento, mostre que sabe reordenar structs e cite sizeof.

Termo PTTermo EN
Codificação de caracteresCharacter encoding
Ponto de códigoCode point
Grafema / cluster de grafemaGrapheme / grapheme cluster
Sequência de escape UnicodeUnicode escape sequence
Byte mais significativoMost Significant Byte (MSB)
Byte menos significativoLeast Significant Byte (LSB)
Ordem de bytes da redeNetwork byte order
Byte de preenchimento / paddingPadding byte
Estrutura empacotadaPacked struct
Normalização UnicodeUnicode normalization
Par substitutoSurrogate pair
Indicador de ordem de bytesByte Order Mark (BOM)
Codificação compatível com ASCIIASCII-compatible encoding
Alinhamento de memóriaMemory alignment
MojibakeMojibake (termo universal)
Ordem de bytesByte order / Endianness
Formato de transformaçãoTransformation format (UTF)
Tamanho de estruturasizeof

“UTF-8 is ASCII-compatible because any byte below 0x80 is a valid ASCII character and a complete UTF-8 sequence.”

“In a little-endian system, the least significant byte is stored at the lowest memory address.”

“Network byte order is big-endian; use htonl before sending integers over a socket.”

“Unicode is a character map, not an encoding. UTF-8 is the encoding that stores Unicode code points as 1 to 4 bytes.”

“Struct padding is inserted by the compiler to align each field to its natural alignment requirement.”

“Reordering struct fields from largest to smallest type minimizes padding and reduces sizeof.”

“A grapheme cluster is what a user perceives as a single character; it may span multiple code points.”

“BOM in UTF-8 is unnecessary and can cause parsing issues; prefer UTF-8 without BOM.”

“Base64 encodes arbitrary binary data as ASCII-safe text, at a cost of ~33% size increase.”


Lastro

  • Bryant, R. E. & O’Hallaron, D. R.Computer Systems: A Programmer’s Perspective (CS:APP), 3rd ed. Pearson, 2015. Cap. 2 (representação de dados, endianness, alinhamento). csapp.cs.cmu.edu
  • Spolsky, J. — “The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)”, Joel on Software, 2003. joelonsoftware.com
  • Yergeau, F. — RFC 3629: UTF-8, a Transformation Format of ISO 10646. IETF, novembro 2003. rfc-editor.org/rfc/rfc3629
  • The Unicode ConsortiumThe Unicode Standard, versão 15.0+. unicode.org — referência canônica para code points, normalização e grapheme clusters.
  • Patterson, D. A. & Hennessy, J. L.Computer Organization and Design, 5th ed. Morgan Kaufmann, 2014. Apêndice B (representação de números, alinhamento e byte order em RISC/x86).