Objetos em C não existem nativamente
O C é uma linguagem procedural. Não há classes, herança, polimorfismo ou construtores no padrão da linguagem. Mas isso não impede que você simule comportamento orientado a objetos usando structs e funções. É um exercício comum para quem está começando a migrar do C para C++ ou precisa manter codebases legacy.
introdução a objetos no c
A base é simples: structs substituem classes, e funções separadas substituem métodos. Cada struct pode ter ponteiros para funções que atuam sobre ela, ou funções externas podem receber a struct como primeiro parâmetro. A convenção mais usada é passar um ponteiro para a struct como o primeiro argumento de qualquer função que a manipule. Veja um exemplo mínimo de uma struct que representa um retângulo:
```c typedef struct { float x; float y; float largura; float altura; } Retangulo; ```Para criar métodos, você escreve funções normais:
```c float area_retangulo(Retangulo *r) { return r->largura * r->altura; } void mover_retangulo(Retangulo *r, float dx, float dy) { r->x += dx; r->y += dy; } ```Chamar essas funções parece quase orientação a objetos quando você organiza bem:
```c Retangulo ret = {0.0f, 0.0f, 10.0f, 5.0f}; area_retangulo(&ret); mover_retangulo(&ret, 2.0f, 3.0f); ```Isso é praticamente tudo que existe. structs, ponteiros, funções. Sem magic.
Padrões avançados: vtable e encapsulamento
Se você precisa de algo mais parecido com herança e polimorfismo, o caminho é usar tabelas de funções (vtable), inspirado no que o GCC faz por trás das cortinas no C++. A ideia é colocar um ponteiro de função dentro da struct e usar herança de struct via composição. Por exemplo, para criar uma hierarquia simples de formas geométricas:
```c typedef struct { float (*area)(void *); void (*mover)(void *, float, float); char *nome; } Forma; typedef struct { Forma base; float lado; } Quadrado; float area_quadrado(void *q) { return ((Quadrado*)q)->lado * ((Quadrado*)q)->lado; } ```Aqui, a struct Forma age como uma interface. Cada tipo concreto preenche esses ponteiros de função durante a inicialização. Você chama através do ponteiro genérico, e o código correto roda. Esse padrão é útil em sistemas embarcados onde você precisa trocar implementações em tempo de execução sem depender de dynamic dispatch do C++. Mas traz um custo: cada chamada indireta via ponteiro de função adiciona overhead, e o debugger fica mais difícil de seguir. Já perdi meia hora rastreando um bug porque o ponteiro de função estava apontando para um stub não implementado em uma struct aninhada.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Encapsulamento real em C
Uma coisa que muitos iniciantes ignoram: você pode esconder definições de structs usando arquivos de cabeçalho separados. Se você declara apenas um tipo incompleto no cabeçalho público e define a struct completa no .c, ninguém fora daquele arquivo consegue acessar os campos diretamente.
```c // figura.h typedef struct Figura Figura; Figura* figura_criar(int lados); void figura_destruir(Figura *f); float figura_area(Figura *f); ``` ```c // figura.c #include "figura.h" #includeIsso é encapsulamento de verdade. A API pública só expõe funções, nunca dados. É como uma classe com members privados, mas escrito manualmente. O trade-off é que todo acesso aos dados deve passar por getters/setters, o que aumenta a verbosidade do código consideravelmente. Em projetos pequenos, isso costuma ser overkill. Em bibliotecas destinadas a múltiplos times, economiza horas de debugging.
Construtores e destrutores
Como o C não tem construtores, você cria funções que alocam e inicializam a struct. O padrão típico é uma função que retorna um ponteiro:
```c Lista* lista_criar(void) { Lista *l = malloc(sizeof(Lista)); if (!l) return NULL; l->tamanho = 0; l->capacidade = 4; l->itens = malloc(sizeof(void*) * l->capacidade); if (!l->itens) { free(l); return NULL; } return l; } ```Repare que o erro de alocação é tratado de forma explícita. Isso é crucial em C. Um NULL mal tratado aqui causaria um segfault em qualquer chamada subsequente. Já vi códigos que puxavam o ponteiro sem verificar NULL depois de criar uma lista, e o bug só aparecia em produção sob carga alta. O destrutor segue o mesmo princípio:
```c void lista_destruir(Lista *l) { if (l) { free(l->itens); free(l); } } ```Simples. Nada de RAII. Se você esquecer de chamar o destrutor, o memory leak persiste até o processo morrer. Em programas de longa execução, isso se acumula rapidamente.
Pegadinhas comuns
Um problema frequente é confundir cópia de struct com compartilhamento de estado. Em C, atribuir uma struct copia todos os campos por valor. Se a struct contém ponteiros, o ponteiro é copiado, não o dado apontado. Duas variáveis apontarão para o mesmo buffer.
```c Retangulo a = {0, 0, 10, 5}; Retangulo b = a; // b é uma cópia independente b.x = 100; // a.x continua 0 ```Isso funciona bem para structs pequenas e POD. Mas se você estiver gerenciando recursos dinâmicos (memória, arquivos, locks), cópias superficiais vão gerar duplo-free ou use-after-free. Nesse caso, implemente uma função de cópia profunda explicitamente. Outro problema: ordenação de campos em structs afeta o layout em memória. Campos de tipos diferentes podem ter padding inserido pelo compilador. Se você precisar de um layout fixo para comunicação com hardware ou protocolos de rede, use `#pragma pack` ou attributes específicos, mas saiba que isso desabilita otimizações de alinhamento e pode degradar performance em certas arquiteturas.
Quando isso não vale a pena
Simular OOP em C funciona bem para bibliotecas pequenas e middleware. Mas se o projeto cresce para centenas de tipos com hierarquias complexas, a verbosidade explode. Cada novo tipo exige funções de criação, destruição, cópia e métodos manualmente. Em C++, o mesmo código seria 70% menor com templates e classes. Se você está entrando em um projeto novo e não tem restrição de linguagem, C++ ou Rust oferecem tudo que a simulação em C oferece, mas com segurança de memória e menos boilerplate. Use a simulação apenas quando obrigado por restrições de plataforma ou legacy code.