Tabela de conteúdos
Guia de estudo: C
Aprender uma nova linguagem de programação e suas peculiaridades tende a ser uma atividade árdua e desafiadora, especialmente quando não se tem referências de estudo adequadas.
O objetivo deste documento é apresentar algumas sugestões para você que está interessado em aprender C, dando prioridade, mas não exclusividade a materiais em português.
Sugiro ler o guia até o fim antes de iniciar os estudos, propriamente, para que possa ter uma visão geral dessa proposta de estudo. Talvez você considere que alguns assuntos possuem maior prioridade do que outros, ou que alguns temas são supérfluos para os seus objetivos.
O guia usa apenas fontes de estudo abertas, que você pode acessar, já que estamos em uma wiki e a ideia aqui é remover barreiras, mas evidentemente eu não poderia deixar de citar o clássico The C Programming Language, de Kernighan e Ritchie.
História
Antes de se aprofundar no estudo de C, você talvez queira entender em que contexto essa linguagem surgiu, e para que ela serviu inicialmente, além de como se popularizou e ganhou a notoriedade que possui hoje.
Isto dará uma melhor noção dos pontos fortes e fracos dessa linguagem, e ajudar a melhorar sua compreensão sobre quando pode fazer sentido recorrer a esse ambiente de desenvolvimento.
O artigo a seguir foi publicado por ninguém menos que Dennis Ritchie, autor da linguagem C, relatando o cenário em que ela foi criada, nos laboratórios Bell:
Este documento é uma versão traduzida para português, o original, em inglês, se chama The Development of the C Language.
É curioso notar que Ritchie se refere a C como uma linguagem de alto nível (o que especialmente para aquela época não é nenhum exagero), e frisa sua importância por ser uma linguagem portável, característica que por vezes é deixada de lado ou mal compreendida quando o assunto é C.
Explico: quando a linguagem C ainda estava sendo criada, era comum usar linguagem de montagem (Assembly) para escrever os programas que executariam nos minicomputadores da época (como o PDP-7 onde o UNIX foi criado).
Isso significa que para cada modelo de minicomputador, era preciso reescrever o programa, já que Assembly não é uma linguagem portável, e varia conforme a arquitetura utilizada. Com C, esse problema foi gradualmente superado, na medida em que os compiladores C eram disponibilizados para diferentes arquiteturas.
E ainda dentro de uma mesma arquitetura a linguagem C é também portável entre sistemas operacionais, desde que para cada sistema alvo exista uma biblioteca C e um compilador aderente ao padrão estabelecido para C.
A fama de C como uma linguagem não portável, deve-se ao amplo uso de bibliotecas que são específicas de algum sistema operacional. Se você usa, por exemplo, a biblioteca POSIX, então é esperado que o programa funcione apenas em sistemas que possuem essa biblioteca. Mas isso não quer dizer que o programa não seja portável por causa da linguagem C.
O artigo da Wikipédia também é útil para obter uma visão geral sobreo tema.
Familiarize-se com C
Sempre friso que programar e compreender uma linguagem de programação são habilidades diferentes. Portanto, se você já tiver algum conhecimento prévio dos conceitos, terá mais facilidade para aprender a usar a linguagem C.
Se você acredita que ainda precisa reforçar seu conhecimento sobre programação de um modo geral, talvez queira conferir o seguinte livro:
- Computer Science I (inglês)
Os primeiros passos com C envolvem uma familiarização com a sua sintaxe.
A seguir você encontra dois materiais muito bem elaborados, e em português, que auxiliam nesse primeiro contato:
O segundo possui uma ênfase maior na compreensão de algoritmos, mas também possui seções dedicadas a tópicos mais básicos da linguagem C.
O artigo a seguir funciona bem como um resumo e ao mesmo tempo mostra exemplos práticos com os conceitos mais importantes e corriqueiros:
- C Primer (inglês)
Alguns livros que você pode também usar como base de estudo são:
- Beej's Guide to C Programming (inglês)
- Modern C (inglês)
Adianto que o terceiro possui uma leitura mais densa e menos amigável a iniciantes, porém o rigor com que trata o assunto é importante para escrever programas robustos, especialmente em uma linguagem como C.
Portanto, aconselho a leitura desse livro depois que já tiver adquirido alguma vivência e maturidade com a linguagem.
Você também pode se interessar pelo seguinte livro:
Ele é inspirado no material “Projeto de Algoritmos” que citei anteriormente, porém de versões mais antigas desse material (publicação de 2009). Ainda assim, o fundamento permanece igualmente válido.
Além de materiais com finalidade mais didática, você pode também querer consultar algumas referências mais formais sobre como essa linguagem funciona e/ou está implementada.
- C - Project status and milestones (inglês)
- The GNU C Reference Manual (inglês)
O primeiro link acima apresenta os documentos formais de cada versão de C. Os documentos em si são fechados, porém os rascunhos (entenda-se, documento que ainda estava passando por revisão, então oficialmente não é o padrão) estão disponíveis em PDF para baixar.
Já o segundo documenta como o compilador do GNU (o GCC) implementa a linguagem, e portanto como se espera que o padrão seja tratado nele.
Como você pode perceber, existem diferentes padrões de C (conhecidos como C89, C99, C11, dentre outros), e talvez esteja se perguntando qual deles deveria usar como referência. Isso depende muito da abrangência e do público-alvo do programa.
Especificações mais antigas possuem uma maior abrangência e suporte, pelo fato de já existirem muitos compiladores aderentes a eles. Quanto mais novo um padrão, menos compiladores estarão totalmente aderentes e com pleno suporte.
Por outro lado, especificações mais novas estabelecem novos recursos, que podem ser importantes para um projeto, algumas vezes evitando a dependência sobre uma biblioteca externa, o que tende a aumentar a portabilidade do programa, especialmente no longo prazo.
A especificação do padrão C, como eu disse, não é um documento didático, mas pode contribuir para seu estudo como material de consulta, quando estiver em dúvida sobre como C trata algum tópico em específico.
Ferramentas essenciais
Bem, só aprender como a linguagem funciona rapidamente pode ficar entediante se você não puder praticar. E para praticar com C, você vai precisar basicamente de:
- Um editor de texto simples (qualquer um, da sua preferência);
- Um compilador (ou coleção de ferramentas de compilação, para ser mais específico).
Agora repita consigo mesmo as seguintes palavras: “Eu não preciso de IDE!”
Repita até entender. É importante frisar que um ambiente de desenvolvimento integrado (IDE, na sigla em inglês), embora possa sim ser uma ferramenta útil para aumentar a sua produtividade, não é um item essencial para programar em C, e especialmente, pode até dificultar o seu processo de aprendizado.
Portanto, concentre-se primeiro em entender os fundamentos e use apenas o básico. Você não precisa de um kit de ferramentas com trocentas funções que ainda não entende, enquanto está ainda nos primeiros passos. Isso só vai servir para tirar o seu foco.
Se ainda assim você quer uma ferramenta que integre a edição do texto com o acionamento de alguns comandos e execuções, talvez se interesse pelo editor Geany. Ele consegue um bom equilíbrio entre funcionalidade e simplicidade.
Compiladores
Muito bem, vamos detalhar mais sobre os compiladores. A rigor, o processo que chamamos de “compilação” envolve um conjunto de etapas, notadamente:
- Compilação propriamente
As ferramentas que desempenham essas funções podem ou não estar agrupadas em uma mesma coleção (compiladores mais conhecidos, como GCC e Clang, por exemplo, fazem esse agrupamento de funções).
Os artigos a seguir explicam por alto como funciona o processo de compilação:
- Compiler Basics (inglês)
Existem vários compiladores C, alguns mais conhecidos e populares, outros menos. Você não precisa conhecer todos eles agora (e aliás, dificilmente em algum momento vai precisar conhecer todos), mas é importante compreender ao menos um pouco essa variedade e quais são os pontos fortes e fracos de ao menos alguns deles.
A seguir você encontra uma lista contemplando e categorizando alguns compiladores C:
- C compilers (inglês)
Dois compiladores de grande destaque são o GCC, do projeto GNU, e o Clang, que faz parte da infraestrutura de compilação LLVM. Conhecer ao menos esses dois já será suficiente para compreender as funções centrais desse tipo de ferramenta.
Pré-processamento
Ao ler arquivos-fonte C, você talvez tenha reparado que algumas
linhas iniciam pelo caractere de jogo-da-velha (#
) e pensado
que essa notação também é parte da linguagem C. Ledo engano.
Essas instruções são conhecidas como diretivas de pré-processamento, e são usadas como um complemento da linguagem C para automatizar a geração de partes do código C, antes que ele efetivamente seja compilado (daí o nome “pré-processamento”).
Por exemplo, as diretivas de inclusão (#include
) incluem o código
de um outro arquivo-fonte C (por convenção, os cabeçalhos, que possuem
extensão .h
), como se fosse parte do próprio arquivo. Diretivas de
definição (#define
) definem constantes ou macro-processadores, que
serão substituídos pelo valor definido em cada ocorrência.
Essas substituições são feitas pelo pré-processador. Além disso, ele também substitui trigrafos (ignore se não sabe o que são) e substitui comentários por espaços em branco. Tudo isso é feito antes da compilação. O arquivo gerado pelo pré-processamento terá código C, exclusivamente.
Você pode saber mais sobre as funções do pré-processador a partir dos documentos a seguir:
- C preprocessor (inglês)
Existem diferentes pré-processadores, alguns deles, como foi dito, fazem parte de uma coleção de ferramentas para compilação, como GCC, Clang, TCC e outros, porém também existem pré-processadores avulsos. Alguns exemplos:
- GNU CPP (parte do GCC)
Compilação
Montagem
Ligação
Biblioteca C
Ainda no âmbito de ferramentas, é importante que você entenda o papel das bibliotecas C nesse processo. Um compilador, como você já deve ter visto nos materiais anteriores, é responsável por manipular o código-fonte do programa e gerar como saída os programas executáveis em código de máquina.
Porém, para fazer isso, ele também precisará usar como insumo alguma biblioteca C, que é a implementação de funcionalidades que fazem parte do padrão da linguagem, conforme especificado nos padrões ISO/IEC, que mencionei antes.
Bibliotecas C são um componente central em sistemas operacionais implementados em C, pois serão a principal interface entre o núcleo do sistema e suas aplicações. Cada sistema operacional pode optar por uma implementação diferente.
Você pode compreender melhor para que serve a biblioteca C a partir dos seguintes documentos:
- The C Library Reference Guide (inglês)
Algumas implementações de biblioteca C são:
Como se pode ver, algumas dessas implementações contemplam não apenas a biblioteca C em si, mas também a biblioteca POSIX.
O comparativo a seguir também é interessante para compreender os recursos implementados e características dessas bibliotecas.
Mais bibliotecas
A biblioteca C é conhecida por ser pequena. Diferente de outros ambientes de desenvolvimento, C não fornece estruturas de dados sofisticadas ou abstrações de protocolos. Em C, partimos da premissa de que você que está programando é quem deve decidir qual implementação utilizar para necessidades mais especializadas.
Em alguns casos você pode decidir implementar por conta própria (menos comum), e em outros, pode reutilizar alguma biblioteca existente. Se por um lado, essa característica de C pode parecer pouco prática para um iniciante, por outro lado, isso aumenta drasticamente a portabilidade da linguagem, pois amplia a quantidade de dispositivos capazes de aderir ao padrão da linguagem, além de facilitar a manutenção de implementações da bibliotecas C.
Saber quando implementar algo e quando buscar uma implementação pronta, bem como escolher quais bibliotecas você deseja utilizar é uma arte. Não apenas podem existir impeditivos técnicos para usar uma biblioteca em um projeto, como também impeditivos legais, a depender das licenças usadas pelas bibliotecas.
Como regra geral, uma grande quantidade de dependências (isto é, bibliotecas que um programa depende para funcionar) não é algo positivo. Quanto menos, melhor. Por outro lado, isso não significa que você deve então implementar do zero tudo o que quiser fazer, pois na prática o resultado pode ser muito pior, se você não souber o que está fazendo.
É importante entender que a escolha de bibliotecas pode limitar os dispositivos ou sistemas operacionais compatíveis com o programa. No entanto, isso pode ser uma característica aceitável em muitos casos, dependendo do objetivo do programa. Por exemplo, programas voltados para sistemas aderentes ao padrão POSIX podem fazer uso da biblioteca POSIX, pois é esperado que ela esteja presente nesses sistemas.
Em relação a C, o padrão POSIX especifica uma biblioteca que inclui as funcionalidades básicas de C (isto é, da biblioteca padrão) e de extensões próprias (redes, interação com o sistema de arquivos, threads, etc.). Você pode saber mais sobre o padrão POSIX nos documentos a seguir:
- C POSIX Library (inglês)
- Headers (ingês)
Não por acaso, algumas bibliotecas C voltadas para esses sistemas (vide seção anterior) implementam também a biblioteca POSIX.
Gerenciadores / Construtores
Quando se trabalha com programas pequenos, de um único arquivo, talvez não seja tão tedioso ou trabalhoso usar o compilador diretamente. Mas para programas maiores, isso começa a ficar pouco prático. Nesses casos, usar ferramentas de construção ajuda a organizar o projeto e evitar a necessidade de realizar manualmente tarefas repetitivas.
O mais comum para programas em C, nesse quesito, é o utilitário make.
- Make (POSIX, em inglês)
- PMake - A Tutorial (inglês)
Embora seja parte do padrão POSIX, existem diferenças significativas entre diferentes implementações dessa ferramenta.
A implementação do FreeBSD por acaso é a mesma utilizada no NetBSD, conforme descrito na página a seguir:
- bmake (inglês)
É desejável, sempre que possível, criar arquivos Makefile
portáveis, isto é, que funcionem em diferentes implementações.
Isto facilita significativamente a manutenção quando há
possibilidade de usar uma implementação diferente.
Por exemplo, em distribuições GNU/Linux, é mais comum o uso do
GNU Make, muitas vezes já instalado por padrão no sistema. Nos
BSDs, o mais comum é a implementação conhecida como bmake
.
Ao criar scripts Makefile
que podem ser usados em sistemas
diferentes, considere aderir totalmente ao padrão POSIX.
Isso pode ser um pouco difícil se você já tiver se acostumado a uma implementação específica. O artigo a seguir oferece algumas dicas nesse sentido.
- A Tutorial on Portable Makefiles (inglês)
Estilos de programação
Não raro, as convenções de código e estilos de programação são um tema tratado como secundário ou de menor importância. Porém esse assunto é mais relevante do que pode parecer.
Estilos podem dizer respeito a vários tipos de convenção, alguns mais
frequentes, outros menos. Dentre os mais frequentes, temos convenções de
nomes (de variáveis, macros, funções, tipos, etc.), estilos de comentários,
regras de indentação e espaçamento, posições de abertura e fechamento de
chaves ({}
), e separação de linhas por exemplo.
Alguns fatores que podem influenciar nas escolhas de um estilo proposto são:
- Gosto pessoal prevalente entre desenvolvedores de um projeto;
- Gosto pessoal de desenvolvedores-chave ou “autoridades” em um projeto;
- Economia de espaço (vertical ou horizontal);
- Facilitação de compreensão do código;
- Facilitação de pesquisa de texto;
- Facilitação de geração de documentação do código;
- Redução de margem para o cometimento de erros.
A melhor maneira de aprender sobre regras de estilo é conhecendo algumas delas, e comparando. Alguns exemplos de estilo de código C:
Não se preocupe em decorar essas regras (ao menos não enquanto não estiver estudando ou contribuindo código para esses projetos), a ideia é que você obtenha algumas dicas de estilo ou reflita sobre essas proposições. Algumas delas podem ser mais detalhadas, então se não entender algum ponto ou conceito abordado, anote para rever quando tiver uma base mais sólida.
Caso essas não sejam do seu agrado, existem muitas outras que pode consultar. Mas independente de gosto, o que realmente importa é entender quais são as decisões que podem ser tomadas sobre um estilo, e por que elas são escolhidas para cada projeto.
Algumas escolhas podem fazer sentido para um projeto mas serem inadequadas em outro. Compreender essas escolhas é o que realmente importa, e comparar estilos diferentes, especialmente estilos incompatíveis entre si, é uma forma de ampliar essa compreensão.
Em seus projetos, você pode adotar um estilo próprio, mas é importante manter a consistência. Começar por um estilo e depois no mesmo projeto adotar um outro diferente, certamente vai ficar confuso até mesmo para você, quando precisar reler o código.
Para trabalhos em equipe, é importante que haja consenso sobre o estilo a ser usado, e que uma vez decidido, ele efetivamente seja seguido em todo o código do projeto.
Ferramentas para formatação automática
Existem ferramentas que facilitam a adequação de programas a um estilo em particular. Geralmente elas possuem uma série de opções que você pode definir para aderir ao estilo proposto, e uma vez aplicadas sobre o código, ele é automaticamente formatado.
Porém é importante saber que essas ferramentas são apenas um instrumento auxiliar. Elas não eximem o programador de compreender o estilo do projeto em que estão trabalhando, e nem de verificar, após o uso da ferramenta, que de fato o resultado ficou aderente ao estilo.
Existem limites para o que pode ser automatizado nesse sentido, e enquanto desenvolvedor de um projeto, não pense que colocar a culpa na ferramenta depois de enviar código fora do padrão funcionará como desculpa.
Isso dito, você pode se interessar por algumas das ferramentas a seguir:
- GNU Indent (
indent
) - Artistic Style (
astyle
) - ClangFormat (
clang-format
)
Inspiração
Parte do estudo de C envolve ler código C já produzido, de programas reais, pois assim você consegue visualizar a aplicação desse conhecimento na prática. Alguns critérios que você pode considerar para decidir quais programas quer estudar:
- Programas pequenos (menos conceitos para compreender e memorizar);
- Programas que você já utiliza (familiaridade com as funções do programa);
- Programas bem documentados;
- Programas com um estilo de código que você se identifica;
- Programas portáveis (para evitar vícios que comprometem a portabilidade).
Alguns exemplos que considero particularmente inspiradores:
Para bibliotecas, você pode querer começar o estudo pela API, já que será um ponto de partida para o uso delas. A biblioteca do SQLite, por exemplo possui uma excelente documentação, não apenas no próprio código-fonte, mas também alguns manuais de estudo.
- SQLite in 5 Minutes Or Less (inglês)
E possui ainda uma descrição em alto nível da sua arquitetura (em inglês).
A biblioteca Lua, por sua vez, além de ser reconhecida pelo rigor com a portabilidade, é também bastante enxuta e pequena. O livro Programming in Lua é uma referência de facto para o estudo da linguagem e da API C.
A primeira edição, embora antiga, permanece válida em muitos pontos e está disponível online. Nela, você consegue uma descrição bem didática de como funciona a API C (em inglês). Detalhes sobre cada função também são apresentados na documentação oficial. A versão 5.2, inclusive, possui uma tradução para português.
Uma vez que tenha entendido a API de Lua, você pode em seguida querer ler os arquivos que implementam a biblioteca padrão de Lua, que faz uso dessa API, e é relativamente fácil de entender (especialmente, mas não exclusivamente para quem já usou a linguagem Lua).
Já o compilador TCC, embora não possua uma documentação tão detalhada, possui o essencial para compreender como o código está organizado, especialmente na seção 8.
A biblioteca musl libc ainda não possui uma documentação completa, mas é interessante por ser uma biblioteca pequena e ainda assim com um bom custo-benefício. Sua documentação oficial ainda está em construção.
- musl (inglês)
De qualquer modo, conhecendo a estrutura da biblioteca padrão de C (vide especificação de C, que mencionei antes), já é possível explorar o seu repositório com alguma confiança.
As ferramentas suckless são conhecidas pelo minimalismo e código limpo. Apesar de não haver muita documentação sobre o código, isso acaba não sendo tão necessário assim, se você souber o que essas ferramentas fazem (o que não é difícil de descobrir, porque elas tendem a fazer apenas uma coisa e bem, seguindo a filosofia UNIX).
Uma característica comum em ferramentas desse projeto, como dmenu ou st é a presença do arquivo config.def.h
que possui uma documentação das opções do programa na forma de comentários. Ao copiar esse arquivo e renomear para config.h
, e então editar essas opções, você pode compilar o programa com as opções desejadas.
Ainda outro projeto interessante é o signify. Você pode saber mais sobre ele neste artigo, do próprio autor:
- signify - sign and verify (inglês).
Possui uma base de código bem pequena, e o cerne da funcionalidade está no arquivo signify.c.
Depuração
Quando se percebe que um programa possui comportamento diferente do esperado, é necessário realizar uma depuração, tarefa que consiste em reproduzir o erro e identificar a sua causa para poder corrigir o problema.
Em alguns casos, esse processo pode ser não apenas tedioso, mas propenso a falhas, caso haja uma cadeia de causas e efeitos complexa demais para se analisar manualmente.
Isso pode ser um indicador importante de que o programa está mais complexo do que deveria, mas ainda assim, você talvez não tenha a opção de reescrever o programa, ou grande parte dele, e precisa de um instrumento de “precisão cirúrgica” para identificar o que está acontecendo na estrutura atual.
Nesses casos, os depuradores podem ser ferramentas bastante úteis, pois eles conseguem ler as entranhas do programa enquanto ele é executado, e com eles você consegue simular uma execução, avançando ou retrocedendo no tempo para observar como ele se comporta internamente.
Para entender melhor o que é um processo de depuração e como depuradores auxiliam nesse processo, você pode conferir os seguintes artigos:
Depuradores
Dois depuradores bastante difundidos, em grande medida por causa das infraestruturas do GCC e do LLVM, são o GDB (também do projeto GNU) e o LLDB (do LLVM).
Para se familiarizar com o uso desses depuradores, você pode conferir os tutoriais a seguir.
- Beej's Quick Guide to GDB (inglês)
- LLDB - Tutorial (inglês)
Análise estática e dinâmica
Em geral, o processo de depuração é orientado por problemas que já foram detectados em um programa. Porém, apenas esperar que problemas sejam detectados para então resolvê-los não é uma boa prática, especialmente quando existem meios de detecção automatizada para alguns tipos de falha.
A detecção automatizada não quer dizer que você não precisará de qualquer modo ler o código e apurar alguns problemas manualmente. Mas o uso de algumas ferramentas pode agilizar essa busca e reduzir o tempo que gastaria com algumas falhas mais comuns e mais fáceis de detectar.
Existem diferentes técnicas para analisar o código de um programa e automaticamente detectar potenciais falhas e então emitir alertas para correção. Em geral, agrupamos essas técnicas como análise estática ou análise dinâmica.
Como se pode deduzir, a análise estática é aquela que fazemos sem executar o programa. Confira os artigos a seguir para saber um pouco mais sobre esse assunto.
- Static Code Analysis (inglês)
A análise estática é prática e tende a consumir pouco tempo. Mas não é capaz de detectar tudo, especialmente os problemas mais insidiosos. As ferramentas de análise dinâmica avaliam o programa durante sua execução, e com isso conseguem traçar um perfil de execução.
Isso pode servir para detectar o uso indevido de memória, partes do programa que nunca são executadas, ou possibilidades de otimização para o desempenho.
Evidentemente, a análise dinâmica não se resume ao uso de ferramentas específicas, mas também sobre uma disciplina de avaliação e leitura dos resultados desses testes.
Para saber mais sobre a análise dinâmica, você pode conferir os seguintes artigos:
- Dynamic program Analysis (inglês)
- Profiling (computer programming) (inglês)
Ferramentas de análise estática
A seguir, você pode conferir algumas ferramentas de análise estática:
Você pode encontrar uma lista mais extensa na Wikipédia:
Ferramentas de análise dinâmica
A seguir, você pode conferir algumas ferramentas de análise dinâmica:
Teste de cobertura de código:
Perfiladores:
- GNU Profiler (
gprof
) - Linux Perf (
perf
)