Para criar um array de strings em Zig, use [_][]const u8{ "Ana", "Bruno", "Carla" }. O tipo inferido será [3][]const u8: um array de três elementos, cada um contendo uma string somente para leitura. Quando uma função deve aceitar listas de qualquer tamanho, receba []const []const u8. Para adicionar e remover nomes em runtime, use std.ArrayList([]const u8) e defina explicitamente quem é dono da memória de cada string.
A aparente quantidade de colchetes assusta no começo, mas cada camada tem um significado. Este guia explica a notação, mostra exemplos de iteração, busca, ordenação, cópia, argumentos de CLI e listas dinâmicas, além de destacar os erros de memória mais comuns.
Resposta rápida
| Necessidade | Tipo ou estrutura |
|---|---|
| Uma string somente para leitura | []const u8 |
| Uma string mutável | []u8 |
| Três strings fixas | [3][]const u8 |
| Array com tamanho inferido | [_][]const u8 |
| Parâmetro com lista somente para leitura | []const []const u8 |
| Lista dinâmica | std.ArrayList([]const u8) |
| Cópia própria de uma string | try allocator.dupe(u8, texto) |
| Comparar conteúdo | std.mem.eql(u8, a, b) |
| Ordenar lexicograficamente | std.mem.order(u8, a, b) |
Se os conceitos de array e slice ainda forem novos, leia primeiro a sintaxe básica de Zig e o guia de alocação de memória em Zig.
Por que uma string é []const u8?
Zig não tem uma classe String embutida. Um literal como "Brasil" representa bytes terminados por sentinela, mas normalmente é usado por meio de uma slice constante de bytes:
const std = @import("std");
pub fn main() void {
const linguagem: []const u8 = "Zig";
std.debug.print("{s} tem {d} bytes\n", .{
linguagem,
linguagem.len,
});
}
O especificador {s} pede ao formatador que imprima os bytes como texto. O campo .len informa bytes, não necessariamente caracteres Unicode percebidos pelo usuário. Em UTF-8, "ação" tem mais bytes que letras. Para armazenamento, I/O, comparação exata e protocolos, trabalhar com bytes é uma decisão explícita e eficiente.
Leia a notação de dentro para fora:
u8: um byte;[]u8: uma slice mutável de bytes;[]const u8: uma slice de bytes que essa referência não pode alterar;[]const []const u8: uma slice somente para leitura cujos elementos são strings somente para leitura.
O primeiro const protege os elementos da lista contra substituição pela função. O segundo protege os bytes de cada string.
Criando um array fixo de strings
Quando a quantidade de itens é conhecida em compile time, um array é a opção mais simples:
const std = @import("std");
pub fn main() void {
const nomes = [_][]const u8{
"Ana",
"Bruno",
"Carla",
};
for (nomes, 0..) |nome, indice| {
std.debug.print("{d}: {s}\n", .{ indice, nome });
}
}
[_] manda o compilador inferir o tamanho. Nesse exemplo, o tipo de nomes é [3][]const u8. Você também poderia escrevê-lo por extenso:
const nomes: [3][]const u8 = .{ "Ana", "Bruno", "Carla" };
O tamanho faz parte do tipo. Portanto, [3][]const u8 e [4][]const u8 são tipos diferentes. Essa característica é útil quando o tamanho é uma garantia do programa, mas inconveniente para uma função genérica que deve receber qualquer quantidade de itens. Para essa fronteira, converta o array em slice.
Passando a lista para uma função
Uma função que só lê a lista deve receber []const []const u8:
const std = @import("std");
fn imprimirTodos(nomes: []const []const u8) void {
for (nomes) |nome| {
std.debug.print("- {s}\n", .{nome});
}
}
pub fn main() void {
const nomes = [_][]const u8{ "Ana", "Bruno", "Carla" };
imprimirTodos(&nomes);
}
O endereço do array, &nomes, pode ser convertido para uma slice que carrega ponteiro e tamanho. Em muitos contextos, nomes[0..] deixa essa intenção ainda mais visível:
imprimirTodos(nomes[0..]);
Prefira receber slices em APIs comuns. Assim, a mesma função aceita arrays fixos, partes de arrays, memória alocada e conteúdo de uma ArrayList.
Acessando uma parte do array
Uma slice pode representar apenas um intervalo:
const nomes = [_][]const u8{
"Ana",
"Bruno",
"Carla",
"Diego",
};
const intermediarios: []const []const u8 = nomes[1..3];
// Contém "Bruno" e "Carla".
O limite final não é incluído. nomes[1..3] pega os índices 1 e 2. Não há cópia dos bytes nem das referências para as strings; a slice aponta para a região do array original. Por isso, o array precisa continuar vivo enquanto a slice for usada.
Lista mutável não significa texto mutável
É possível permitir a troca dos elementos sem permitir alteração nos bytes apontados por eles:
const std = @import("std");
pub fn main() void {
var nomes = [_][]const u8{ "Ana", "Bruno", "Carla" };
nomes[1] = "Bia";
for (nomes) |nome| {
std.debug.print("{s}\n", .{nome});
}
}
A variável nomes é mutável, então nomes[1] pode passar a apontar para outra string. Entretanto, cada elemento continua sendo []const u8; você não pode mudar um byte de um literal.
Se precisa editar os bytes, use um buffer mutável:
var nome = [_]u8{ 'z', 'i', 'g' };
const texto: []u8 = nome[0..];
texto[0] = 'Z';
Essa distinção evita escrever acidentalmente em memória somente para leitura e deixa a propriedade dos dados clara.
Buscando e comparando strings
Slices não devem ser comparadas com == para verificar conteúdo. Use std.mem.eql:
const std = @import("std");
fn contem(lista: []const []const u8, procurado: []const u8) bool {
for (lista) |item| {
if (std.mem.eql(u8, item, procurado)) return true;
}
return false;
}
pub fn main() void {
const linguagens = [_][]const u8{ "Zig", "C", "Rust" };
std.debug.print("Tem Zig? {}\n", .{
contem(&linguagens, "Zig"),
});
}
A comparação é byte a byte e diferencia maiúsculas de minúsculas. Para busca ASCII sem diferenciar caixa, você pode normalizar os dados ou usar uma função apropriada da biblioteca padrão para o release instalado. Não aplique regras ASCII como se fossem regras Unicode completas.
Ordenando um array de strings
Para ordenar as referências no próprio array, forneça uma função de comparação:
const std = @import("std");
fn vemAntes(_: void, a: []const u8, b: []const u8) bool {
return std.mem.order(u8, a, b) == .lt;
}
pub fn main() void {
var nomes = [_][]const u8{ "Diego", "Ana", "Carla", "Bruno" };
std.mem.sort([]const u8, &nomes, {}, vemAntes);
for (nomes) |nome| {
std.debug.print("{s}\n", .{nome});
}
}
A ordenação troca as slices armazenadas no array; não reescreve os bytes das strings. A ordem é lexicográfica por bytes UTF-8, adequada para identificadores e dados técnicos, mas não equivale à ordenação linguística do português. Para nomes exibidos a usuários, regras de acento, caixa e locale exigem uma solução específica.
A API exata de sort pode evoluir enquanto Zig está pré-1.0. Confira a documentação da sua versão caso o compilador sinalize mudança de assinatura.
Criando uma lista dinâmica com ArrayList
Quando a quantidade de itens só é conhecida em runtime, use std.ArrayList([]const u8). O ponto mais importante não é apenas criar a lista: é decidir quem possui os bytes das strings.
Guardando strings emprestadas
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var nomes: std.ArrayList([]const u8) = .empty;
defer nomes.deinit(allocator);
try nomes.append(allocator, "Ana");
try nomes.append(allocator, "Bruno");
try nomes.append(allocator, "Carla");
for (nomes.items) |nome| {
std.debug.print("{s}\n", .{nome});
}
}
Aqui, a ArrayList é dona apenas do buffer que armazena as slices. Os bytes pertencem aos literais, que vivem durante todo o programa. nomes.deinit(allocator) libera o buffer interno da lista, não as strings.
O exemplo segue a API das versões recentes de Zig, com .empty e o allocator passado para operações que podem alocar. Em versões anteriores, você encontrará o formato ArrayList(T).init(allocator), append(item) e deinit(). Confira a documentação do release instalado, mas preserve o mesmo modelo de propriedade: um allocator sustenta o armazenamento da lista, e os bytes de cada string têm um dono separado.
Guardando cópias próprias
Se as strings vêm de um buffer temporário, duplique cada uma:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
var nomes: std.ArrayList([]const u8) = .empty;
defer {
for (nomes.items) |nome| allocator.free(nome);
nomes.deinit(allocator);
}
const entradas = [_][]const u8{ "Ana", "Bruno", "Carla" };
for (entradas) |entrada| {
const copia = try allocator.dupe(u8, entrada);
errdefer allocator.free(copia);
try nomes.append(allocator, copia);
}
std.debug.print("Total: {d}\n", .{nomes.items.len});
}
Agora existem duas camadas de alocação:
- o buffer interno da
ArrayList; - uma alocação para cada string duplicada.
A ordem de limpeza reflete isso: libere cada string e depois desinicialize a lista. O errdefer evita vazamento caso append falhe depois que a cópia já foi criada. Veja também as boas práticas de tratamento de erros em Zig.
O bug clássico: guardar referência para buffer reutilizado
Este padrão é perigoso:
var buffer: [128]u8 = undefined;
while (lerProximaLinha(&buffer)) |linha| {
try nomes.append(linha);
}
Se linha aponta para buffer, todas as entradas guardadas podem terminar apontando para a mesma região, sobrescrita a cada iteração. A lista preserva slices, não cria cópias automaticamente.
A correção é duplicar os bytes antes de reutilizar o buffer:
const copia = try allocator.dupe(u8, linha);
errdefer allocator.free(copia);
try nomes.append(allocator, copia);
Essa regra aparece em parsers, leitura de CSV, processamento de logs e protocolos de rede. Se o produtor reutiliza o buffer, o consumidor que precisa reter o valor deve copiá-lo ou transferir formalmente a propriedade. O guia de parsing e serialização em Zig aprofunda esse tipo de fronteira.
Arrays de strings e argumentos de linha de comando
Argumentos de CLI são um caso real de lista de strings. A biblioteca padrão fornece uma coleção alocada com o executável e seus argumentos:
const std = @import("std");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const args = try std.process.argsAlloc(allocator);
defer std.process.argsFree(allocator, args);
for (args, 0..) |arg, indice| {
std.debug.print("args[{d}] = {s}\n", .{ indice, arg });
}
}
O tipo retornado é conceitualmente uma lista de strings alocadas. Use a função de liberação correspondente em vez de tentar liberar partes manualmente. Para parsing de flags, subcomandos e mensagens de ajuda, consulte o guia de aplicações de linha de comando em Zig.
Quando usar um buffer único
Muitas alocações pequenas podem ser desnecessárias. Para dados carregados em lote, uma estratégia eficiente é manter:
- um buffer grande com todos os bytes;
- uma lista de slices apontando para intervalos desse buffer.
Por exemplo, um arquivo lido inteiro pode ser dividido por linhas sem copiar cada linha. Nesse desenho, todas as slices ficam válidas enquanto o buffer principal estiver vivo. A limpeza também fica simples: libere a lista de slices e o buffer uma vez cada.
Outra opção é um arena allocator: várias strings são copiadas para a arena e liberadas juntas no fim da operação. Isso funciona bem para um parser, request ou job com ciclo de vida único. Não use arena como desculpa para manter dados indefinidamente; associe-a a uma fronteira clara.
Interoperabilidade com strings C
Uma lista de []const u8 não é automaticamente um char ** de C. Strings C precisam de terminador nulo, e o array de ponteiros costuma exigir outra representação. Ao chamar uma API C, você pode precisar de strings com sentinela, como [:0]const u8, e de um array de ponteiros compatível com a assinatura externa.
Não faça cast cego entre as representações. Converta na fronteira, garanta o byte nulo e preserve a vida útil de cada buffer até a função C terminar. Para exemplos completos, veja interoperabilidade entre Zig e C e o guia do Zig como compilador C/C++.
Erros comuns
Confundir uma string com uma lista de strings
const texto: []const u8 = "Zig";
const lista: []const []const u8 = // várias strings
Cada [] adiciona uma camada. Nomear tipos intermediários pode melhorar a leitura:
const String = []const u8;
const StringList = []const String;
Retornar slice de um array local
Não retorne uma slice que aponta para uma variável local que deixará de existir. Retorne um array por valor quando o tamanho for fixo, receba um buffer do chamador ou aloque memória com um contrato claro de liberação.
Duplicar sem liberar
Cada allocator.dupe bem-sucedido cria uma obrigação de free, a menos que a memória pertença a uma arena liberada em bloco.
Liberar memória emprestada
Literais, argumentos gerenciados por outra API e slices de um buffer externo não devem ser liberados individualmente. Só o dono libera.
Usar .len como quantidade de caracteres
.len conta bytes. Isso importa para truncamento de texto UTF-8: cortar em qualquer byte pode produzir uma sequência inválida.
Checklist de decisão
Antes de escolher a representação, responda:
- a quantidade de strings é conhecida em compile time?
- a lista precisa crescer ou diminuir?
- as strings são literais, slices emprestadas ou cópias próprias?
- qual objeto precisa continuar vivo para as slices permanecerem válidas?
- quem libera o buffer da lista?
- quem libera os bytes de cada string?
- a comparação deve ser por bytes, ASCII sem caixa ou regras linguísticas?
- a fronteira exige strings terminadas em zero para C?
Se essas respostas estiverem explícitas, os tipos de Zig deixam de parecer ruído e passam a documentar o contrato de memória.
Perguntas frequentes
Qual é o tipo de um array de strings em Zig?
Um array fixo pode ter o tipo [N][]const u8, em que N é a quantidade de strings. Com tamanho inferido, escreva [_][]const u8{ "A", "B" }. Para parâmetros que aceitam qualquer quantidade, prefira []const []const u8.
Qual é a diferença entre []const u8 e []const []const u8?
[]const u8 representa uma sequência de bytes, normalmente uma string UTF-8. []const []const u8 representa uma sequência de elementos que, individualmente, são strings. Em outras palavras: uma string versus uma lista de strings.
Como criar uma lista dinâmica de strings em Zig?
Use std.ArrayList([]const u8) com um allocator compatível com sua versão de Zig. Se os textos tiverem vida útil menor que a lista, copie-os com allocator.dupe. Se forem literais ou slices de um buffer que continuará vivo, a lista pode guardar referências emprestadas.
Como comparar duas strings em Zig?
Use std.mem.eql(u8, a, b) para igualdade de conteúdo. Para decidir ordem lexicográfica, use std.mem.order(u8, a, b). Essas operações comparam bytes e diferenciam maiúsculas de minúsculas.
Preciso liberar cada string de um ArrayList?
Somente se a lista for dona de alocações individuais. Se cada item veio de allocator.dupe, libere cada item e depois a lista. Se os itens são literais ou slices emprestadas, libere apenas o armazenamento interno da ArrayList.
Próximos passos
Depois de dominar arrays de strings, pratique com três exercícios: leia argumentos da CLI, filtre os que começam com um prefixo e ordene o resultado; carregue linhas de um arquivo mantendo um buffer único; depois implemente uma versão que duplica cada linha e compare os contratos de limpeza. Esses exemplos conectam slices, allocators, I/O e tratamento de erros — exatamente os fundamentos que tornam Zig previsível em programas de sistemas.