Zig vs C Moderno (C23): Segurança, Build, Performance e Migração

Introdução

C23 é a versão mais recente do padrão ISO C e trouxe melhorias importantes: constexpr, typeof, nullptr, _BitInt, atributos padronizados, #embed e uma semântica mais clara para funções sem parâmetros. A pergunta prática para quem escreve sistemas é outra: C23 resolve os problemas que fazem equipes considerarem Zig?

A resposta curta é: C23 moderniza a sintaxe e reduz algumas dores, mas não muda a natureza da linguagem. C continua sendo excelente quando ABI, portabilidade histórica e ecossistema legado são mais importantes que ergonomia. Zig tenta ocupar o mesmo espaço operacional de C com outra proposta: build integrado, cross-compilation simples, comptime, error unions, slices com tamanho, allocators explícitos e verificações úteis em debug.

Este artigo compara Zig com C23 especificamente — não com o C de 1989, mas com o C mais moderno disponível. Para comparação com C++, veja Zig vs C++. Para migração prática, consulte Guia de Migração: C para Zig e Como Migrar um Projeto C para Zig.

Resposta Rápida: Zig ou C23?

CenárioEscolha mais provávelMotivo
Código legado com ABI C pública e toolchain já aprovadoC23Menor risco organizacional e compatibilidade máxima
Projeto novo de CLI, runtime, ferramenta interna ou biblioteca de sistemasZigBuild/cross-compilation integrados e menos cola de infraestrutura
Firmware/regulatório com MISRA/CERT C obrigatórioC23O ecossistema de certificação ainda gira em torno de C
Migração gradual de uma base C existenteZig + C@cImport, zig cc e wrappers permitem trocar partes aos poucos
Ensino de programação de sistemas com feedback rápidoZigErros explícitos, testes integrados e debug checks ajudam a aprender
Biblioteca que precisa ser consumida por qualquer linguagem via ABIC23 ou Zig expondo ABI CA ABI C continua sendo o contrato universal

A regra prática é simples: C23 é a melhor evolução conservadora de C; Zig é a melhor aposta quando você quer manter controle de baixo nível, mas remover parte do atrito histórico de C.

O Que C23 Trouxe de Novo

C23 é uma evolução significativa:

  • constexpr: Variáveis e funções avaliadas em compilação
  • typeof: Inferência de tipo (finalmente!)
  • nullptr: Constante nula tipada (substitui NULL)
  • _BitInt(N): Inteiros de largura arbitrária
  • Atributos padrão: [[nodiscard]], [[maybe_unused]], [[deprecated]]
  • auto: Inferência de tipo para variáveis locais
  • #embed: Incluir dados binários diretamente
  • Funções sem parâmetros: void f() agora significa zero parâmetros

Essas melhorias são bem-vindas, mas Zig foi projetado desde o início para resolver problemas que C23 apenas mitiga.

Segurança de Memória

C23

C23 não resolve os problemas fundamentais de segurança de C:

// Todos estes bugs AINDA existem em C23:
int* p = malloc(sizeof(int));
free(p);
*p = 42; // use-after-free: undefined behavior

int arr[10];
arr[15] = 0; // buffer overflow: undefined behavior

char* s = NULL;
printf("%s", s); // null dereference: undefined behavior

C23 adicionou nullptr e constexpr, mas a linguagem continua fundamentalmente insegura em relação a memória.

Zig

Zig resolve esses problemas com verificações de segurança em tempo de compilação e runtime:

// Use-after-free: detectado em debug
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
const allocator = gpa.allocator();
const p = try allocator.create(i32);
allocator.destroy(p);
// p.* = 42; // Erro: uso de memória liberada (em debug)

// Buffer overflow: sempre verificado
var arr: [10]i32 = undefined;
// arr[15] = 0; // panic: index out of bounds

// Null dereference: prevenido pelo sistema de tipos
const s: ?[]const u8 = null;
// s.len; // Erro de compilação: precisa unwrap optional
if (s) |valor| {
    std.debug.print("{s}\n", .{valor});
}

Veja Segurança de Memória em Zig e Detectar Vazamentos.

Metaprogramação: constexpr vs Comptime

C23 constexpr

C23 constexpr é limitado — não permite loops, chamadas de função arbitrárias ou alocação em tempo de compilação:

// C23: constexpr básico
constexpr int MAX_TAMANHO = 1024;
constexpr int quadrado(int x) { return x * x; }
constexpr int tabela[5] = {0, 1, 4, 9, 16};

Zig comptime

Zig comptime executa código Zig arbitrário em tempo de compilação — incluindo loops, chamadas de função, e até criação de tipos:

// Zig: comptime completo
fn criarLookupTable(comptime n: usize) [n]u32 {
    var tabela: [n]u32 = undefined;
    for (&tabela, 0..) |*entry, i| {
        entry.* = @intCast(i * i);
    }
    return tabela;
}

const TABELA = criarLookupTable(256); // calculada em compilação

fn HashMap(comptime K: type, comptime V: type) type {
    return struct {
        // tipo inteiro criado em compilação
        entries: []Entry,
        const Entry = struct { key: K, value: V };
    };
}

A diferença é enorme: constexpr de C23 avalia expressões simples; comptime de Zig é uma linguagem de metaprogramação completa usando a mesma sintaxe.

Tratamento de Erros

C23

C23 não tem mecanismo de tratamento de erros integrado. O padrão continua sendo códigos de retorno e errno:

FILE* f = fopen("config.txt", "r");
if (f == NULL) {
    perror("Erro ao abrir arquivo");
    return -1;
}
// fácil esquecer de verificar

Zig

Zig tem error unions integrados ao sistema de tipos:

fn lerConfig(caminho: []const u8) !Config {
    const arquivo = try std.fs.cwd().openFile(caminho, .{});
    defer arquivo.close();
    // ...
}

// O compilador OBRIGA tratamento do erro
const config = lerConfig("config.txt") catch |err| {
    std.debug.print("Erro: {}\n", .{err});
    return;
};

Veja Error Sets Customizados e Padrões Errdefer.

Sistema de Build

C23

C23 não define um sistema de build. Desenvolvedores C usam Make, CMake, Meson, Autotools, ou outros — cada um com sua própria sintaxe e complexidade:

CC=gcc
CFLAGS=-std=c23 -Wall -O2
programa: main.o utils.o
    $(CC) $(CFLAGS) -o $@ $^

Zig

Zig tem um sistema de build integrado escrito em Zig:

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,
    });
    b.installArtifact(exe);
}

Veja Migrar de Makefile para build.zig e Migrar de CMake para build.zig.

Comparação Direta de Features

FeatureC23Zig
Inferência de tipoauto/typeof (limitado)Completa
Null safetynullptr (sintática)Optionals ?T (semântica)
Bounds checkingNenhumDebug mode automático
Error handlingerrno/códigos de retornoError unions integrados
Metaprogramaçãoconstexpr (limitado) + macroscomptime (completo)
Generics_Generic (muito limitado)comptime generics
Sistema de buildExterno (Make/CMake)Integrado (build.zig)
TestesExterno (Unity, cmocka)Integrados (test)
Cross-compilationVia toolchain externoIntegrada
Package managerNenhum oficialIntegrado (zon)
Stringschar* (null-terminated)Slices com tamanho
deferNão existeSim
Undefined behaviorExtensivo (200+ formas)Minimizado, detectado

Preprocessador vs Comptime

O preprocessador C é uma das maiores fontes de problemas. C23 não o substitui:

C23 (preprocessador)

// Problemas clássicos do preprocessador
#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = MAX(i++, j++); // bug: i++ ou j++ avaliado duas vezes

#define TAMANHO_BUFFER 1024
// Não é uma constante real, é substituição textual

Zig (comptime)

fn max(a: anytype, b: @TypeOf(a)) @TypeOf(a) {
    return if (a > b) a else b;
}
// Sem efeitos colaterais duplicados — é uma função real

const TAMANHO_BUFFER: usize = 1024;
// Constante real com tipo

Veja Substituir Macros C por Comptime Zig para um guia completo.

Interoperabilidade C-Zig

Zig pode usar código C23 diretamente:

const c = @cImport({
    @cDefine("_GNU_SOURCE", {});
    @cInclude("minha_lib_c23.h");
});

pub fn main() !void {
    const resultado = c.minha_funcao_c23();
    std.debug.print("Resultado: {}\n", .{resultado});
}

Veja Interoperabilidade C-Zig e Converter Ponteiros C para Zig.

Performance: O Ponto Onde a Comparação Fica Menos Ideológica

Em código nativo bem escrito, Zig e C23 podem chegar a performances muito próximas. Ambos compilam para binários nativos, permitem controle de layout de dados, evitam garbage collector obrigatório e conseguem usar instruções específicas de arquitetura quando necessário. A diferença não é “Zig é sempre mais rápido” ou “C é sempre mais rápido”. A diferença real está no caminho até um binário rápido, reproduzível e fácil de medir.

Em C, performance costuma depender de uma combinação externa: compilador, flags, sistema de build, bibliotecas, sanitizers, profiler e scripts de release. Em Zig, mais dessa configuração fica dentro do próprio projeto:

  • zig build -Doptimize=ReleaseFast documenta o modo de otimização no fluxo oficial do projeto.
  • ReleaseSafe permite uma opção intermediária quando segurança em runtime ainda importa.
  • zig cc pode compilar dependências C com uma toolchain mais previsível.
  • Cross-compilation é uma função do compilador, não uma coleção de pacotes externos por plataforma.
  • Benchmarks e testes podem viver no mesmo fluxo de zig build test.

Para aprender a medir sem cair em microbenchmark enganoso, veja Benchmarking em Zig: medir performance e, para comparação com outras linguagens de sistemas, Zig vs Rust e Zig vs Go.

Migração Gradual: O Caso Mais Realista

Poucas equipes jogam fora uma base C grande para reescrever tudo em uma linguagem nova. O caminho mais seguro costuma ser incremental:

  1. Usar zig cc como compilador C em um projeto existente para simplificar cross-compilation e releases.
  2. Criar testes de borda para partes críticas antes de trocar implementação.
  3. Escrever novos módulos em Zig que exponham uma ABI C simples.
  4. Importar headers C com @cImport enquanto a fronteira entre as linguagens ainda muda.
  5. Mover lógica de build para build.zig quando o projeto já tiver uma fatia Zig relevante.
  6. Substituir macros perigosas por funções ou comptime onde houver bugs recorrentes.

Esse caminho reduz risco porque C e Zig podem conviver no mesmo binário. Você não precisa vender uma reescrita total para ganhar benefícios práticos: pode começar por uma ferramenta CLI, uma biblioteca de parsing, uma rotina de serialização, um worker de alta performance ou uma parte difícil de cross-compilar.

Leia também Interop C em Zig, Migrar projeto C para Zig e Cross-compilation em Zig.

Quando C23 Ainda Faz Sentido

  • Projetos com requisitos de conformidade MISRA/CERT C
  • Ecossistemas que mandatam C (Linux kernel, firmware regulatório)
  • Equipes com décadas de experiência em C e resistência a mudança
  • Bibliotecas que precisam de compatibilidade ABI C máxima
  • Bases legadas onde a maior parte do risco está em processo, certificação e tooling, não em sintaxe

Quando Zig É a Melhor Aposta

Zig faz mais sentido quando a equipe quer continuar perto do metal, mas está cansada de manter uma pilha de ferramentas ao redor de C. Alguns sinais fortes:

  • Você precisa gerar binários para Linux, macOS, Windows, ARM e x86 sem manter múltiplas toolchains.
  • O projeto tem muitos Makefile, scripts de release e flags duplicadas.
  • O time quer remover macros perigosas sem adotar C++ ou Rust.
  • Você quer testes e build reproduzíveis como parte da experiência padrão.
  • Você está escrevendo uma CLI, um runtime, uma lib nativa, um parser, um servidor leve ou uma ferramenta de infraestrutura.
  • Você precisa interoperar com C, mas quer escrever as novas partes com um sistema de tipos mais explícito.

FAQ: Zig vs C Moderno

Zig substitui C23?

Não universalmente. Zig pode substituir C em muitos projetos novos e em partes isoladas de bases existentes, mas C23 continua mais conservador para firmware regulado, bibliotecas com contrato ABI histórico e ambientes onde o compilador C é obrigatório.

C23 ficou seguro como Zig?

Não. C23 melhora a linguagem, mas não adiciona null safety semântica, bounds checking padrão, error unions, slices com tamanho ou um modelo de allocators como parte central da experiência. Bugs clássicos de C continuam possíveis.

Zig é mais rápido que C?

Não há resposta universal. Zig e C podem gerar código nativo extremamente rápido. A vantagem de Zig costuma aparecer mais em ergonomia de build, cross-compilation, testes e controle explícito do projeto do que em uma promessa absoluta de benchmark.

Posso usar C e Zig no mesmo projeto?

Sim. Esse é um dos pontos fortes de Zig. Você pode importar headers C com @cImport, compilar C com zig cc, chamar bibliotecas C existentes e expor uma ABI C para consumidores externos.

Devo aprender C antes de Zig?

Ajuda, mas não é obrigatório. C ensina a história e a ABI que ainda governam sistemas. Zig pode ser uma porta de entrada mais segura para aprender memória, ponteiros, layout e build moderno sem aceitar todas as armadilhas de C como padrão.

Conclusão

C23 é uma melhoria real sobre C17, mas não resolve os problemas fundamentais de C: segurança de memória, undefined behavior, ausência de sistema de build, e preprocessador frágil. Zig foi projetado para resolver exatamente esses problemas mantendo o mesmo nível de controle que C oferece.

Se você está começando um projeto novo de sistemas, Zig oferece muitas das vantagens de C com segurança e ergonomia modernas. Se está mantendo código C existente, considere a migração gradual descrita em Como Migrar um Projeto C para Zig.

Se você está avaliando alternativas modernas ao C, confira também nosso conteúdo sobre Rust, outra linguagem que busca substituir C com foco em segurança de memória.

Para começar, visite Introdução ao Zig e Zig para Programadores C.

Continue aprendendo Zig

Explore mais tutoriais e artigos em português para dominar a linguagem Zig.