A linguagem C evolui ao longo do tempo atualizando sua versão, até a atual C23. As ferramentas do ambiente C também evoluíram, e isso que quero mostrar.
Criando projeto
Não vamos começar com algo complexo de início. Vamos apenas criar um código C simples com um src/main.c e ir incrementando.
#include <stdio.h>
#include <stdlib.h>
int main() {
printf("Iniciando projeto!\n");
return EXIT_SUCCESS;
}
Agora vamos compilar usando o GCC e executar
gcc src/main.c -o main
./main
Agora vamos criar um struct para representar um usuário. Essa struct terá os campos id, nome, email e created_at. Vamos criar também funções para criar a struct, printar os valores dela e para liberar a memória.
#include <stdint.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
typedef struct {
uint64_t id;
char *name;
char *email;
time_t created_at;
} User;
User *User_new(uint64_t id, const char *name, const char *email) {
User *user = malloc(sizeof(User));
if (!user) {
fprintf(stderr, "Falha ao alocar memória para o usuário\n");
exit(EXIT_FAILURE);
}
bool is_email_valid = strstr(email, "@") != NULL;
if (!is_email_valid) {
fprintf(stderr, "Email inválido: %s\n", email);
free(user);
exit(EXIT_FAILURE);
}
user->id = id;
user->name = strdup(name);
user->email = strdup(email);
user->created_at = time(NULL);
return user;
}
void User_free(User *user) {
if (user) {
free(user->name);
free(user->email);
free(user);
}
}
void User_print(const User *user) {
struct tm *info_time = gmtime(&user->created_at);
char timestamp_str[20];
strftime(timestamp_str, sizeof(timestamp_str), "%Y-%m-%dT%H:%M:%S", info_time);
printf(
"User{id: %" PRIu64 ", name: %s, email: %s, created_at: %s}\n",
user->id,
user->name,
user->email,
timestamp_str
);
}
int main() {
printf("Iniciando projeto!\n");
User *user = User_new(1, "Fulano", "fulano@mail.com");
User_print(user);
User_free(user);
return EXIT_SUCCESS;
}
Agora vamos compilar e vamos adicionar explicitamente para usar a versão C23 da linguagem C.
gcc src/main.c -o main -std=c23
./main
Separando arquivos
Fazer tudo dentro do arquivo main.c deixa tudo muito poluído. Uma boa prática é separar o código em outros arquivos. Em C podemos criar arquivo de cabeçalho (.h) com as definições das funções e structs e um arquivo fonte (.c) para implementar o cabeçalho.
Vamos criar o include/user.h e mover a definições da estrutura e das funções para lá. Vamos usar o #pragma once no arquivo para impedir que o cabeçalho seja importado duas vezes.
// Garante que só importe o cabeçalho apenas uma vez,
// caso use o #include duas vezes ou mais
#pragma once
#include <stdint.h>
#include <time.h>
typedef struct {
uint64_t id;
char *name;
char *email;
time_t created_at;
} User;
User *User_new(uint64_t id, const char *name, const char *email);
void User_free(User *user);
void User_print(const User *user);
E criaremos um arquivo fonte src/user.c para implementar essas funções.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <inttypes.h>
#include "user.h" // importa o cabeçalho
User *User_new(uint64_t id, const char *name, const char *email) {
User *user = malloc(sizeof(User));
if (!user) {
fprintf(stderr, "Falha ao alocar memória para o usuário\n");
exit(EXIT_FAILURE);
}
bool is_email_valid = strstr(email, "@") != NULL;
if (!is_email_valid) {
fprintf(stderr, "Email inválido: %s\n", email);
free(user);
exit(EXIT_FAILURE);
}
user->id = id;
user->name = strdup(name);
user->email = strdup(email);
user->created_at = time(NULL);
return user;
}
void User_free(User *user) {
if (user) {
free(user->name);
free(user->email);
free(user);
}
}
void User_print(const User *user) {
struct tm *info_time = gmtime(&user->created_at);
char timestamp_str[20];
strftime(timestamp_str, sizeof(timestamp_str), "%Y-%m-%dT%H:%M:%S", info_time);
printf(
"User{id: %" PRIu64 ", name: %s, email: %s, created_at: %s}\n",
user->id,
user->name,
user->email,
timestamp_str
);
}
E nosso src/main.c irá importar nosso arquivo de cabeçalho user.h e isso deixará nosso código na main mais limpo.
#include <stdio.h>
#include <stdlib.h>
#include "user.h"
int main() {
printf("Iniciando projeto!\n");
User *user = User_new(1, "Fulano", "fulano@mail.com");
User_print(user);
User_free(user);
return EXIT_SUCCESS;
}
Agora para compilar vamos ter que passar os dois arquivos fonte main.c e user.c.
Além disso como importamos o cabeçalho como #include "user.h" vamos precisar passar onde esse cabeçalho está usando a flag -I<path>. Poderiamos passar o caminho completo ou relativo do arquivo no include, eliminando a necessidade da flag, #include "../include/user.h, mas o padrão é passar a flag ao compilar.
Também vou adicionar outras flag para tornar o compilador mais rigoroso como -Wall e -Wextra para ativar alguns warnings e -Werror para transformar os warnings em erros.
gcc -o main -std=c23 -Wall -Wextra -Werror -Iinclude src/main.c src/user.c
./main
Automatizando o build
À medida que o projeto cresce, novos arquivos de cabeçalho e fonte são criados. Com isso, o nosso comando de compilação vai crescendo e se torna cada vez mais difícil de lembrar. Você pode acabar esquecendo de incluir um arquivo ou um diretório e falhar na compilação.
Para resolver isso, temos ferramentas de build como o Make e o Ninja, que é uma opção mais rápida e moderna que iremos usar. Geralmente, o Ninja não é escrito diretamente à mão, ele costuma ser usado junto de outra ferramenta: o CMake. O CMake é um gerador de sistema de build. Ele lê um arquivo de configuração simples e gera a estrutura complexa necessária para o Ninja realizar o trabalho pesado da compilação.
Vamos criar um arquivo chamado CMakeLists.txt na raiz do projeto para descrever como nosso código deve ser compilado:
cmake_minimum_required(VERSION 3.20) # versão mínima do CMake necessária para o projeto
project(projectUsers LANGUAGES C) # Nome do projeto e a linguagem como C
set(CMAKE_C_STANDARD 23) # definindo a versão padrão do C como C23
set(CMAKE_C_STANDARD_REQUIRED ON) # garantindo que a versão especificada seja obrigatória
set(CMAKE_C_EXTENSIONS OFF) # desativando extensões específicas do compilador
# Nome do arquivo de saída e os arquivos fonte que serão compilados para gerar o executável
# Usarei o nome do projeto como nome do executável
add_executable(${PROJECT_NAME}
src/main.c
src/user.c
)
# Informa ao CMake onde procurar pelos arquivos de cabeçalho (.h)
# O equivalente à flag -Iinclude do GCC
target_include_directories(${PROJECT_NAME} PRIVATE include)
# Adiciona opções de compilação específicas para o alvo (target) do projeto
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Werror)
Agora podemos gerar o sistema de build, ninja, usando o CMake. Usaremos a flag -B <path> para dizer o direto de saída e -G <generator> para dizer qual o sistema de build será gerado.
cmake -B build -G Ninja
Com isso o cmake criará um diretório build com o Ninja como sistema de build. Agora para compilar o código e gerar um executável usamos o seguinte comando:
cmake --build build
Isso irá gerar o executável build/projectUsers que é o nome que demos no nosso CMakeLists.txt no add_executable().
./build/projectUsers
Iniciando projeto!
User{id: 1, name: Fulano, email: fulano@mail.com, created_at: 2026-10-02T12:10:42}
Com isso se eu modificar algum arquivo já existente eu só preciso recompilar usando o cmake --build build.
void User_print(const User *user) {
struct tm *info_time = gmtime(&user->created_at);
char timestamp_str[20];
strftime(timestamp_str, sizeof(timestamp_str), "%Y-%m-%dT%H:%M:%S", info_time);
// Colocando as string entre aspas no print
printf(
"User{id: %" PRIu64 ", name: \"%s\", email: \"%s\", created_at: %s}\n",
user->id,
user->name,
user->email,
timestamp_str
);
}
cmake --build build
./build/projectUsers
Iniciando projeto!
User{id: 1, name: "Fulano", email: "fulano@mail.com", created_at: 2026-10-02T12:11:38}
Mas se eu adicionar novos arquivos ao projeto eu tenho que listar esses arquivos dentro do CMakeLists.txt e recriar o sistema de build com o cmake -B build -G Ninja e depois compilar com cmake --build build.
Dependências Externas
Para usar bibliotecas externas no C é um pouco trabalhoso, pois você precisa manualmente baixar e compilar cada biblioteca. Em linguagens como Java, NodeJS, Python, temos um gerenciador de dependências. No C temos o vcpkg, um gerenciador de dependências da Microsoft.
O vcpkg pode ser integrado com o CMake para buscar as dependências externa ao criar o sistema de build.
Na raiz do projeto vamos usar o comando vcpkg new --aplication. Isso irá gerar os arquivos vcpkg-configuration.json e vcpkg.json.
No vcpkg.json será onde ficará listados as dependências do projeto. Vamos usar a biblioteca cJSON. Para adicionar ela ao projeto usamos o comando vcpkg add port cjson. Isso atualizará o vcpkg.json.
{
"dependencies": [
"cjson"
]
}
Se quiser achar outra dependência pode olhar no site do vcpkg.
Integrando vcpkg e CMake
Agora no comando para gerar o sistema de build passamos a flag --toolchain <diretorio_vcpkg>/scripts/buildsystems/vcpkg.cmake para injetar o script do vcpkg no CMake que fica dentro do diretório de instalação do vcpkg.
rm -rf build
cmake -B build -G Ninja --toolchain $VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
Na saída do terminal você verá que mostra o que deve ser adicionado ao CMakeLists.txt para compilar com essa dependência. No caso do cJSON temos essa saída
cjson provides CMake targets:
# this is heuristically generated, and may not be correct
find_package(cJSON CONFIG REQUIRED)
target_link_libraries(main PRIVATE cjson)
O CMakeLists.txt ficou assim:
cmake_minimum_required(VERSION 3.20)
project(projectUsers LANGUAGES C)
set(CMAKE_C_STANDARD 23)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS OFF)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # Para intelisense da IDE usando o clangd
find_package(cJSON CONFIG REQUIRED)
add_executable(${PROJECT_NAME}
src/main.c
src/user.c
)
target_link_libraries(${PROJECT_NAME} PRIVATE cjson)
target_include_directories(${PROJECT_NAME} PRIVATE include)
target_compile_options(${PROJECT_NAME} PRIVATE -Wall -Wextra -Werror)
rf -rf build
cmake -B build -G Ninja --toolchain $VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
Com isso temos o cJSON configurado para usar no projeto. Vamos criar um nova função no include/user.h para usar a biblioteca.
#pragma once
#include <stdint.h>
#include <time.h>
typedef struct {
uint64_t id;
char *name;
char *email;
time_t created_at;
} User;
User *User_new(uint64_t id, const char *name, const char *email);
void User_free(User *user);
void User_print(const User *user);
// Nova função para converter um User em JSON
char *User_to_json(const User *user);
E vamos implementar no src/user.c a nova função usando o cJSON.
#include <cjson/cJSON.h>
// resto do código omitido
char *User_to_json(const User *user) {
if (!user) {
return nullptr;
}
cJSON *user_json = cJSON_CreateObject();
struct tm *info_time = gmtime(&user->created_at);
char timestamp_str[20];
strftime(timestamp_str, sizeof(timestamp_str), "%Y-%m-%dT%H:%M:%S", info_time);
cJSON_AddNumberToObject(user_json, "id", user->id);
cJSON_AddStringToObject(user_json, "name", user->name);
cJSON_AddStringToObject(user_json, "email", user->email);
cJSON_AddStringToObject(user_json, "created_at", timestamp_str);
char *json_string = cJSON_Print(user_json);
cJSON_Delete(user_json);
return json_string;
}
E agora vamos usar a função no src/main.c
#include <stdio.h>
#include <stdlib.h>
#include "user.h"
int main() {
printf("Iniciando projeto!\n");
User *user = User_new(1, "Fulano", "fulano@mail.com");
User_print(user);
char *json = User_to_json(user);
if (!json) {
fprintf(stderr, "Falha ao converter o usuário para JSON\n");
User_free(user);
return EXIT_FAILURE;
}
printf("JSON: %s\n", json);
User_free(user);
free(json);
return EXIT_SUCCESS;
}
cmake --build build
./build/projectUsers
Iniciando projeto!
User{id: 1, name: "Fulano", email: "fulano@mail.com", created_at: 2026-10-02T16:06:28}
JSON: {
"id": 1,
"name": "Fulano",
"email": "fulano@mail.com",
"created_at": "2026-10-02T16:06:28"
}
Simplificando comandos com CMake Presets
Se você reparou, nosso comando do CMake ainda está muito grande e pode ser que acabe sendo digitado de forma errada ou faltando algo.
cmake -B build -G Ninja --toolchain $VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
E além disso, se rodar o mesmo comando no Windows irá falhar, pois a sintaxe para variáveis de ambiente é diferente.
Para resolver isso o CMake possui os Presets. Podemos criar um arquivo CMakePresets.json na raiz do projeto.
{
"version": 3,
"cmakeMinimumRequired": {
"major": 3,
"minor": 20,
"patch": 0
},
"configurePresets": [
{
"name": "default",
"displayName": "Configuração padrão (Ninja + vcpkg)",
"generator": "Ninja",
"binaryDir": "${sourceDir}/build",
"cacheVariables": {
"CMAKE_TOOLCHAIN_FILE": "$env{VCPKG_ROOT}/scripts/buildsystems/vcpkg.cmake"
}
}
],
"buildPresets": [
{
"name": "default",
"configurePreset": "default"
}
]
}
Repare que transferimos a variável do vcpkg para o arquivo de preset, usando $env{VCPKG_ROOT} para ler a variável de ambiente do sistema operacional, seja ele Windows ou Linux.
Agora podemos gerar nosso sistema de build usando um comando mais simples
rm -rf build
cmake --preset default
O CMake vai ler o CMakePresets.json para usar o preset default que criamos para gerar o sistema de build. Agora basta compilar o projeto e executar:
cmake --build build
./build/projectUsers
Iniciando projeto!
User{id: 1, name: Fulano, email: fulano@mail.com, created_at: 2026-10-02T21:38:56}
JSON: {
"id": 1,
"name": "Fulano",
"email": "fulano@mail.com",
"created_at": "2026-10-02T21:38:56"
}
Conclusão
O C tem a fama de ser uma linguagem velha e intimidadora e ter um ecossistema difícil de configurar. No entanto, como vimos ao longo deste artigo, isso não é verdade hoje em dia.
A versão C23 trouxe muita modernidade e as ferramentas do ecossistema C está evoluindo.
Espero ter te ajudado a ver como algo simples com 1 arquivo e compilando usando o gcc pode evoluir para arquivos separados e um build automatizado com bibliotecas externas.
Tente fazer um projeto C e adicionar alguma biblioteca e deixe o link do repositório nos comentários e diga o que você achou da leitura.
Top comments (0)