Zig vs C Moderno (C23)

Introdução

C23 é a mais recente versão do padrão ISO C, trazendo melhorias como constexpr, typeof, nullptr, _BitInt, atributos padronizados e mais. A pergunta que muitos desenvolvedores C fazem é: “C23 resolve os problemas que me fariam considerar Zig?”

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.

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.

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

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 todas as 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.

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.