Se você programa em C, vai se sentir em casa com Zig — a sintaxe é familiar, a performance é equivalente e ambas usam o backend LLVM para otimização. Mas Zig resolve dezenas de problemas clássicos que todo programador C conhece: memory leaks, buffer overflows, undefined behavior e toolchains de compilação cruzada impossíveis de configurar.
Neste artigo, vamos comparar as duas linguagens lado a lado com exemplos práticos. Se você quer um guia completo de migração, confira também o Zig para programadores C.
Alocação de memória
A gestão de memória é onde o Zig realmente brilha em relação ao C.
Em C — problemas clássicos:
#include <stdlib.h>
#include <string.h>
char* criar_mensagem(const char* nome) {
// Fácil de calcular errado o tamanho
char* msg = malloc(strlen(nome) + 20);
if (msg == NULL) return NULL; // Fácil de esquecer essa verificação
sprintf(msg, "Olá, %s!", nome);
return msg;
// Quem vai chamar free()? O chamador precisa lembrar!
}
void funcao_com_leak() {
int *dados = malloc(sizeof(int) * 100);
if (alguma_condicao) return; // LEAK! malloc sem free
// ... processamento
free(dados);
}
Em Zig — segurança por design:
const std = @import("std");
fn criarMensagem(allocator: std.mem.Allocator, nome: []const u8) ![]u8 {
// Allocator explícito — sempre claro quem gerencia a memória
return std.fmt.allocPrint(allocator, "Olá, {s}!", .{nome});
}
fn funcaoSemLeak(allocator: std.mem.Allocator) !void {
const dados = try allocator.alloc(i32, 100);
defer allocator.free(dados); // GARANTIDO — executa ao sair do escopo
if (alguma_condicao) return; // defer executa mesmo aqui!
// ... processamento
// free é automático graças ao defer
}
Diferenças chave:
- Allocators explícitos — em Zig, toda alocação de memória recebe um allocator como parâmetro, tornando claro quem é responsável pela memória
defer— garante que a limpeza ocorre mesmo em retornos antecipados, eliminando memory leaks- Sem
NULL— Zig usa optionals e error unions ao invés de retornar NULL
Para entender allocators em profundidade, veja nosso tutorial de gerenciamento de memória em Zig.
Tratamento de erros
Em C, erros são tratados de formas inconsistentes: códigos de retorno, errno, ponteiros nulos. O Zig tem um sistema unificado.
Em C — caos de convenções:
// Convenção 1: retornar código de erro
int abrir_arquivo(const char* caminho, FILE** out) {
*out = fopen(caminho, "r");
if (*out == NULL) return -1;
return 0;
}
// Convenção 2: retornar NULL
char* ler_config(const char* path) {
FILE* f = fopen(path, "r");
if (!f) return NULL; // O que deu errado? Permissão? Arquivo não existe?
// ... leitura
return buffer;
}
// Convenção 3: errno global
ssize_t n = read(fd, buf, sizeof(buf));
if (n < 0) {
perror("read failed"); // Qual erro? Race condition com errno?
}
Em Zig — erros são cidadãos de primeira classe:
const std = @import("std");
fn lerArquivo(caminho: []const u8) ![]u8 {
const file = std.fs.cwd().openFile(caminho, .{}) catch |err| {
// err é tipado: error.FileNotFound, error.AccessDenied, etc.
std.debug.print("Erro ao abrir: {}\n", .{err});
return err;
};
defer file.close();
return file.readToEndAlloc(std.heap.page_allocator, 1024 * 1024);
}
pub fn main() !void {
const conteudo = lerArquivo("config.txt") catch |err| switch (err) {
error.FileNotFound => {
std.debug.print("Arquivo não encontrado\n", .{});
return;
},
error.AccessDenied => {
std.debug.print("Sem permissão\n", .{});
return;
},
else => return err,
};
defer std.heap.page_allocator.free(conteudo);
// Usar conteúdo...
}
Vantagens do Zig:
- Erros são tipados e documentados na assinatura da função
trypropaga erros automaticamente (sem boilerplate)catchpermite tratamento granular comswitch- Sem variáveis globais como
errno - O compilador garante que todos os erros são tratados
Strings e buffers
Em C — território de buffer overflows:
#include <string.h>
void exemplo_perigoso() {
char buffer[10];
strcpy(buffer, "String muito longa para o buffer!"); // BUFFER OVERFLOW
// Comportamento indefinido! Pode crashar, pode parecer funcionar...
char nome[50];
scanf("%s", nome); // Sem limite de tamanho — outra vulnerabilidade
}
Em Zig — segurança em tempo de compilação e runtime:
fn exemploSeguro() void {
var buffer: [10]u8 = undefined;
const texto = "String muito longa";
// Cópia segura com tamanho verificado
if (texto.len > buffer.len) {
std.debug.print("String não cabe no buffer!\n", .{});
return;
}
@memcpy(buffer[0..texto.len], texto);
// Slices sempre carregam o tamanho
const slice: []const u8 = buffer[0..texto.len];
std.debug.print("Tamanho: {}\n", .{slice.len});
}
Diferenças:
- Strings em Zig são slices (
[]const u8) com tamanho conhecido - Acesso fora dos limites é detectado em runtime (safety checks)
@memcpyverifica tamanhos em tempo de compilação quando possível- Sem terminador nulo obrigatório — interop com C usa
[:0]const u8
Undefined Behavior
C é famoso pelo undefined behavior — código que parece correto mas pode fazer qualquer coisa:
Em C — armadilhas silenciosas:
int overflow() {
int x = INT_MAX;
x += 1; // UNDEFINED BEHAVIOR! Pode retornar qualquer coisa
return x;
}
int null_deref(int* ptr) {
return *ptr; // Se ptr é NULL: UNDEFINED BEHAVIOR
}
int array_oob() {
int arr[5] = {0};
return arr[10]; // UNDEFINED BEHAVIOR — acesso fora dos limites
}
Em Zig — comportamento sempre definido:
fn overflow() u32 {
var x: u32 = std.math.maxInt(u32);
x +%= 1; // Operador de wrap explícito — resultado: 0
// x += 1; // Isso geraria panic em modo debug (overflow detectado)
return x;
}
fn acessoSeguro(ptr: ?*i32) i32 {
// Optionals forçam verificação de null
if (ptr) |valor| {
return valor.*;
}
return 0; // Caso null tratado explicitamente
}
fn arraySeguro() i32 {
const arr = [5]i32{ 1, 2, 3, 4, 5 };
// arr[10]; // ERRO DE COMPILAÇÃO — índice fora dos limites
return arr[4]; // OK
}
O Zig elimina undefined behavior com:
- Overflow aritmético detectado em modo debug
- Operadores explícitos para wrap (
+%,-%,*%) - Optionals ao invés de ponteiros nulos
- Verificação de limites em arrays e slices
Compilação cruzada
Uma das maiores dores de C é configurar toolchains para compilação cruzada. Em Zig, é trivial.
Em C — complexidade extrema:
# Compilar para ARM Linux a partir de x86_64:
# 1. Instalar cross-compiler
sudo apt install gcc-aarch64-linux-gnu
# 2. Encontrar as sysroot headers corretas
# 3. Configurar variáveis de ambiente
export CC=aarch64-linux-gnu-gcc
export CROSS_COMPILE=aarch64-linux-gnu-
# 4. Rezar para que as dependências compilem
make ARCH=arm64 CROSS_COMPILE=aarch64-linux-gnu-
# (geralmente falha na primeira tentativa)
Em Zig — um comando:
# Compilar para ARM Linux
zig build-exe main.zig -target aarch64-linux-gnu
# Compilar para Windows a partir de Linux
zig build-exe main.zig -target x86_64-windows-msvc
# Compilar para WebAssembly
zig build-exe main.zig -target wasm32-wasi
# Compilar para macOS a partir de Linux
zig build-exe main.zig -target x86_64-macos
O Zig suporta mais de 30 targets diferentes sem instalar nenhuma ferramenta adicional. Para mais detalhes, veja nosso tutorial de cross-compilation com Zig.
Sistema de build
Em C — múltiplas ferramentas:
# Makefile (ou CMakeLists.txt, ou Meson, ou Autotools...)
CC = gcc
CFLAGS = -Wall -O2
LDFLAGS = -lm -lpthread
all: programa
programa: main.o utils.o
$(CC) $(LDFLAGS) -o $@ $^
%.o: %.c
$(CC) $(CFLAGS) -c $<
clean:
rm -f *.o programa
Em Zig — build system integrado:
// build.zig — tudo em Zig, com type safety
const std = @import("std");
pub fn build(b: *std.Build) void {
const target = b.standardTargetOptions(.{});
const optimize = b.standardOptimizeOption(.{});
const exe = b.addExecutable(.{
.name = "programa",
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
});
// Adicionar biblioteca C? Simples:
exe.linkSystemLibrary("pthread");
exe.linkLibC();
b.installArtifact(exe);
}
Para dominar o build system, confira nosso guia completo do build.zig.
Testes integrados
Em C — frameworks externos necessários:
// Precisa de um framework: CUnit, Check, Unity, cmocka...
#include <CUnit/CUnit.h>
void test_soma() {
CU_ASSERT_EQUAL(soma(2, 3), 5);
}
// + dezenas de linhas de boilerplate para registrar testes
Em Zig — testes embutidos na linguagem:
fn somar(a: i32, b: i32) i32 {
return a + b;
}
test "somar dois números" {
const resultado = somar(2, 3);
try std.testing.expectEqual(@as(i32, 5), resultado);
}
test "somar números negativos" {
const resultado = somar(-1, -1);
try std.testing.expectEqual(@as(i32, -2), resultado);
}
zig test arquivo.zig # Executa todos os testes
Para mais sobre testes, veja nosso tutorial de testes em Zig.
Interoperabilidade: Zig fala C nativamente
Uma das maiores vantagens do Zig para quem vem de C é a interoperabilidade direta:
// Importar headers C diretamente — sem bindings manuais!
const c = @cImport({
@cInclude("stdio.h");
@cInclude("math.h");
});
pub fn main() void {
_ = c.printf("Raiz de 2: %f\n", c.sqrt(2.0));
}
O Zig pode compilar código C diretamente e linkar com bibliotecas C existentes. Isso permite migração gradual — você pode começar a usar Zig em um projeto C existente arquivo por arquivo.
Confira nosso guia completo de interoperabilidade Zig-C.
Tabela comparativa
| Aspecto | C | Zig |
|---|---|---|
| Performance | Excelente | Excelente (mesmo backend LLVM) |
| Segurança de memória | Manual, propenso a erros | Allocators explícitos + defer |
| Undefined behavior | Comum e silencioso | Detectado em runtime |
| Tratamento de erros | Inconsistente | Sistema unificado com error unions |
| Strings | Ponteiros nulos terminados | Slices com tamanho |
| Compilação cruzada | Complexa | Nativa (30+ targets) |
| Sistema de build | Externo (Make/CMake) | Integrado (build.zig) |
| Testes | Framework externo | Embutido na linguagem |
| Interop com C | — | Importação direta de headers |
| Curva de aprendizado | Longa (armadilhas ocultas) | Mais curta (explícito e previsível) |
Quando usar cada linguagem?
Continue com C se:
- Você tem um codebase C maduro e estável
- Precisa de compatibilidade com compiladores específicos (não-LLVM)
- O ecossistema de bibliotecas do seu domínio é exclusivamente C
Escolha Zig se:
- Está começando um projeto novo de sistemas
- Precisa de compilação cruzada
- Quer a performance de C com mais segurança
- Quer migrar gradualmente de C (interop nativa)
- Precisa de um build system moderno e integrado
Conclusão
Zig não é apenas uma “alternativa ao C” — é uma evolução pensada para resolver os problemas reais que desenvolvedores C enfrentam diariamente: memory leaks, buffer overflows, undefined behavior, toolchains de compilação cruzada impossíveis de configurar. E faz tudo isso sem sacrificar a performance que torna C indispensável.
A capacidade de importar headers C diretamente e interoperar com código C existente significa que você não precisa reescrever tudo — pode migrar gradualmente, arquivo por arquivo.
Próximos passos
- Zig para programadores C — Guia detalhado de migração
- Interoperabilidade Zig-C — Como usar C dentro de Zig
- Introdução ao Zig — Tutorial para iniciantes
- Zig vs Rust — Comparação com outra alternativa moderna
- Zig vs Go — Quando usar cada uma