10

Bibliotecas e Modularização em C

Bibliotecas e Modularização em C10.1

Até este ponto, a maior parte dos programas foi escrita em um único arquivo .c. Essa abordagem funciona bem em exercícios pequenos, mas começa a se tornar ruim quando o código cresce, quando precisamos reaproveitar funções em outros projetos ou quando queremos organizar melhor as responsabilidades do sistema.

Na linguagem C, essa organização aparece de forma muito clara no uso de bibliotecas. Algumas bibliotecas já vêm prontas, como stdio.h, string.h e math.h. Outras podem ser criadas pelo próprio programador para agrupar funções relacionadas em um mesmo módulo.

Este capítulo tem como objetivo explicar não apenas como usar bibliotecas prontas, mas também como construir as suas próprias. Além disso, vamos diferenciar três ideias que costumam ser confundidas: incluir um cabeçalho com #include, declarar funções em um arquivo .h e implementar essas funções em um arquivo .c, e finalmente compilar tudo corretamente com o GCC.

Objetivos
  • Compreender o que é uma biblioteca na linguagem C.
  • Diferenciar bibliotecas padrão de bibliotecas próprias.
  • Entender a função da diretiva #include.
  • Reconhecer a diferença entre arquivos .h e .c.
  • Utilizar corretamente header guards.
  • Compilar programas com múltiplos arquivos usando o GCC.
  • Compreender o uso das opções -lm e -I no processo de compilação.

Por que modularizar um programa10.2

Quando um programa é pequeno, colocar tudo dentro de main.c parece suficiente. O problema é que, com o tempo, essa escolha gera várias dificuldades:

  • O arquivo fica grande demais e difícil de manter.
  • O reaproveitamento de código passa a depender de copiar e colar funções.
  • Pequenas alterações ficam espalhadas por várias partes do programa.
  • Torna-se mais difícil separar o que cada parte do sistema faz.

Uma forma clássica de resolver isso é dividir o programa em módulos. Cada módulo fica responsável por uma tarefa específica, como cálculos matemáticos, manipulação de texto, validação de dados ou operações com vetores.

Vantagem da Modularização

Ao dividir um programa em módulos, você deixa o código mais organizado, facilita a manutenção e aumenta muito a chance de reutilizar funções em outros projetos.

O que é uma biblioteca em C10.3

Uma biblioteca é um conjunto de recursos prontos para uso, como funções, tipos, constantes e macros, que podem ser aproveitados em diferentes programas.

Quando dizemos que um programa está usando uma biblioteca, isso pode significar duas situações:

  • Ele está usando uma biblioteca padrão da linguagem, como stdio.h.
  • Ele está usando uma biblioteca criada pelo próprio desenvolvedor.

Em ambos os casos, a ideia é a mesma: evitar reescrever código que já pode ser organizado e reutilizado.

Biblioteca padrão e biblioteca própria10.3.1

As bibliotecas padrão são fornecidas pelo ecossistema da linguagem e normalmente já estão disponíveis com o compilador. Já as bibliotecas próprias são criadas dentro do projeto para reunir funções específicas do sistema que estamos desenvolvendo.

Por exemplo:

  • stdio.h é usada para entrada e saída.
  • string.h é usada para manipulação de textos.
  • math.h é usada para cálculos matemáticos.
  • calc.h pode ser criada por você para reunir funções como soma, media e maximo.

Bibliotecas padrão importantes10.4

Ao estudar bibliotecas em C, vale conhecer pelo menos algumas das bibliotecas padrão mais importantes e as funções mais comuns que elas oferecem.

Biblioteca <stdio.h>10.4.1

A biblioteca <stdio.h> reúne funções ligadas à entrada e saída padrão, como leitura do teclado e impressão na tela.

Algumas funções importantes:

  • printf(): imprime valores formatados na tela.

Exemplo de uso:

printf("Idade: %d\n", idade);
  • scanf(): lê valores digitados pelo usuário.

Exemplo de uso:

int idade;
scanf("%d", &idade);
  • fgets(): lê uma linha de texto com limite de tamanho.

Exemplo de uso:

char nome[30];
fgets(nome, 30, stdin);

A função fgets() lê até atingir o limite informado, encontrar uma quebra de linha \n ou chegar ao fim da entrada. Diferentemente de scanf("%s", ...), ela aceita espaços no texto digitado. Por isso, costuma ser a melhor escolha quando queremos ler nomes completos, frases ou linhas inteiras.

  • puts(): imprime uma string e salta para a próxima linha.

Exemplo de uso:

puts("Mensagem escrita com puts()");

Se scanf() for usado antes de fgets(), pode ser necessário consumir a quebra de linha deixada no teclado. Uma forma simples de fazer isso em exemplos introdutórios é usar getchar() antes do fgets(). A função getchar() lê um único caractere da entrada, então ela pode ser usada nesse caso para consumir o \n que ficou pendente no teclado.

Biblioteca <string.h>10.4.2

A biblioteca <string.h> oferece funções específicas para manipulação de strings.

Algumas funções importantes:

  • strlen(): calcula quantos caracteres existem na string.

Exemplo de uso:

char texto[] = "Programacao";
printf("Tamanho: %zu\n", strlen(texto));
  • strcpy(): copia o conteúdo de uma string para outra.

Exemplo de uso:

char origem[] = "Casa";
char destino[20];
strcpy(destino, origem);
  • strcat(): adiciona uma string ao final de outra.

Exemplo de uso:

char texto[20] = "Linguagem ";
strcat(texto, "C");
  • strcmp(): compara duas strings e retorna 0 se elas forem iguais, um valor menor que 0 se a primeira vier antes da segunda na ordem lexicografica, e um valor maior que 0 se a primeira vier depois da segunda.

Exemplo de uso:

if(strcmp("abc", "abc") == 0)
    printf("Strings iguais\n");
  • strchr(): procura a primeira ocorrência de um caractere em uma string.

Exemplo de uso:

char palavra[] = "Computador";
char *posicao = strchr(palavra, 'p');
if(posicao != NULL)
    printf("Caractere encontrado: %c\n", *posicao);
Segurança de Memória

Funções como strcpy() e strcat() exigem cuidado com o tamanho do array de destino. Se não houver espaço suficiente, o programa pode sobrescrever memória indevidamente.

Biblioteca <math.h>10.4.3

A biblioteca <math.h> contém funções matemáticas prontas.

Algumas funções importantes:

  • sqrt(): calcula a raiz quadrada de um número.

Exemplo de uso:

printf("%.1f\n", sqrt(25.0));
  • pow(): calcula uma potência.

Exemplo de uso:

printf("%.1f\n", pow(2, 3));
  • sin(): calcula o seno de um ângulo em radianos.

Exemplo de uso:

printf("%.1f\n", sin(0));
  • cos(): calcula o cosseno de um ângulo em radianos.

Exemplo de uso:

printf("%.1f\n", cos(0));
  • fabs(): retorna o valor absoluto de um número real.

Exemplo de uso:

printf("%.1f\n", fabs(-4.5));

Além das funções, ela também é muito usada quando queremos trabalhar com constantes matemáticas importantes, como o valor de pi e o número de Euler. Em muitos compiladores, aparecem macros como M_PI e M_E, mas isso não é garantido em todos os ambientes. Uma forma mais portátil de obter esses valores é:

  • acos(-1.0) para obter pi;
  • exp(1.0) para obter o número de Euler.

Assim, em vez de depender de constantes que podem não existir em todos os compiladores, podemos gerar esses valores com funções que fazem parte da própria biblioteca matemática.

Exemplo de uso:

double pi = acos(-1.0);
printf("PI = %.6f\n", pi);
double euler = exp(1.0);
printf("Euler = %.6f\n", euler);
Compilação com math.h

Em muitos sistemas Linux e Unix, usar #include <math.h> não basta. Também é necessário passar a opção -lm no GCC para ligar a biblioteca matemática.

Biblioteca <stdlib.h>10.4.4

A biblioteca <stdlib.h> oferece funções utilitárias para várias tarefas gerais do programa.

Algumas funções importantes:

  • atoi(): converte uma string numerica para inteiro.

Exemplo de uso:

char numero[] = "123";
int valor = atoi(numero);
printf("%d\n", valor);
  • rand(): gera um numero pseudoaleatorio inteiro entre 0 e RAND_MAX.

Exemplo de uso:

printf("%d\n", rand() % 100);

Como rand() gera valores entre 0 e RAND_MAX, normalmente usamos o operador % para limitar o resultado a um intervalo menor. Por exemplo, rand() % 100 gera valores entre 0 e 99.

Se quisermos gerar numeros entre um minimo min e um maximo max, um truque comum e usar:

min + rand() % (max - min + 1)

Por exemplo, para gerar um valor entre 5 e 10:

int valor = 5 + rand() % (10 - 5 + 1);

Nesse caso, os valores possiveis sao 5, 6, 7, 8, 9 e 10.

  • srand(): inicializa a semente do gerador pseudoaleatorio.

Exemplo de uso:

srand(time(NULL));

A função rand() gera valores pseudoaleatorios, isto e, numeros que parecem aleatorios, mas sao produzidos por uma sequencia interna. Se voce nao usar srand() antes, o programa tende a repetir a mesma sequencia a cada execucao. Por isso, normalmente usamos srand(time(NULL)) antes da primeira chamada de rand().

Biblioteca <ctype.h>10.4.5

A biblioteca <ctype.h> trabalha com testes e transformações em caracteres individuais.

Algumas funções importantes:

  • toupper(): converte um caractere para maiusculo.

Exemplo de uso:

printf("%c\n", toupper('a'));
  • tolower(): converte um caractere para minusculo.

Exemplo de uso:

printf("%c\n", tolower('Z'));
  • isdigit(): verifica se o caractere e um digito.

Exemplo de uso:

printf("%d\n", isdigit('7'));
  • isalpha(): verifica se o caractere e uma letra.

Exemplo de uso:

printf("%d\n", isalpha('A'));
  • isspace(): verifica se o caractere e um espaco em branco.

Exemplo de uso:

printf("%d\n", isspace(' '));

Biblioteca <time.h>10.4.6

A biblioteca <time.h> fornece funções relacionadas a tempo e data.

Algumas funções importantes:

  • time(): obtém o horario atual do sistema.

Exemplo de uso:

time_t agora = time(NULL);
printf("%ld\n", (long)agora);

O valor retornado por time() representa, em geral, a quantidade de segundos passados desde 01/01/1970 00:00:00, referencia conhecida como Unix Epoch.

  • localtime(): converte um valor de tempo para uma estrutura com data e hora local.

Exemplo de uso:

time_t agora = time(NULL);
struct tm *info = localtime(&agora);
printf("%02d/%02d/%04d\n",
       info->tm_mday,
       info->tm_mon + 1,
       info->tm_year + 1900);

A diretiva #include10.5

Para usar uma biblioteca em C, utilizamos a diretiva #include. Essa diretiva é tratada antes da compilação, em uma etapa chamada pré-processamento.

Quando o pré-processador encontra uma linha como esta:

#include "calc.h"

ele substitui essa linha pelo conteúdo do arquivo indicado. Em outras palavras, o #include funciona como uma inserção textual do cabeçalho naquele ponto do código.

Se tivermos:

// main.c
#include "calc.h"

e dentro de calc.h existir:

int soma(int a, int b);
float media(int a, int b);

o pré-processador age como se main.c passasse a enxergar essas declarações diretamente no próprio arquivo.

Isso significa que o #include não compila o programa sozinho e não produz o executável. Ele apenas torna declarações visíveis ao arquivo que está sendo compilado.

Diferença Crucial

Incluir um arquivo com #include não é a mesma coisa que compilar esse arquivo. O #include apenas insere texto. A compilação e a ligação continuam sendo etapas separadas.

Diferença entre < > e " "10.5.1

Existem duas formas comuns de usar #include:

  • #include <stdio.h>: normalmente usado para bibliotecas padrão. O compilador procura o arquivo nos diretórios padrão do sistema.
  • #include "calc.h": normalmente usado para bibliotecas do próprio projeto. O compilador procura primeiro no diretório local e em outros diretórios informados ao compilador.
#include <stdio.h>
#include "calc.h"

Criando sua própria biblioteca10.6

Em projetos próprios, o padrão mais comum é dividir o módulo em dois arquivos:

  • um arquivo .h, com a interface;
  • um arquivo .c, com a implementação.

Arquivo de cabeçalho .h10.6.1

O arquivo .h informa ao restante do programa quais funções, tipos ou constantes aquele módulo oferece. Exemplo:

#ifndef CALC_H
#define CALC_H

int soma(int a, int b);
float media(int a, int b);
int maximo(int a, int b);

#endif

Nesse exemplo, o cabeçalho informa que o módulo oferece três funções: soma, media e maximo.

Repare que o .h não contém o corpo das funções. Ele apenas anuncia o que existe, para que outros arquivos possam usar essas funções corretamente.

Arquivo de implementação .c10.6.2

O arquivo .c contém o corpo real das funções.

#include "calc.h"

int soma(int a, int b){
    return a + b;
}

float media(int a, int b){
    return (a + b) / 2.0;
}

int maximo(int a, int b){
    if(a > b)
        return a;
    return b;
}
Verificação de Consistência

É uma boa prática que o próprio arquivo .c inclua o seu cabeçalho correspondente. Assim, o compilador verifica se a implementação está coerente com os protótipos declarados.

Nesse caso, calc.c inclui calc.h justamente para garantir que as funções implementadas em calc.c sejam as mesmas que foram anunciadas ao restante do programa.

Uso do módulo no main10.6.3

Depois de criar o módulo, o programa principal pode usar as funções declaradas no cabeçalho.

#include <stdio.h>
#include "calc.h"

int main(){
    printf("Soma: %d\n", soma(4, 7));
    printf("Media: %.2f\n", media(4, 7));
    printf("Maximo: %d\n", maximo(4, 7));
    return 0;
}

Nesse ponto, o fluxo completo do módulo fica assim:

  • calc.h anuncia as funções disponíveis.
  • calc.c implementa essas funções.
  • main.c usa essas funções.
  • o comando do GCC junta tudo no executável final.

Expandindo o módulo10.6.4

Uma das vantagens da modularização é que a biblioteca pode crescer sem desorganizar o programa principal. Se quisermos adicionar uma nova função, basta atualizar o cabeçalho e a implementação do módulo.

Por exemplo, para acrescentar a função minimo, poderíamos fazer:

No arquivo calc.h:

int minimo(int a, int b);

No arquivo calc.c:

int minimo(int a, int b){
    if(a < b)
        return a;
    return b;
}

Depois disso, main.c já pode usar a nova função normalmente, desde que o projeto continue sendo compilado com todos os arquivos necessários.

Header guards10.7

Como o #include faz inserção textual, o mesmo cabeçalho pode acabar sendo incluído mais de uma vez durante a compilação. Para evitar esse problema, usamos os chamados header guards.

#ifndef CALC_H
#define CALC_H

int soma(int a, int b);

#endif

O funcionamento é simples:

  • #ifndef CALC_H verifica se esse identificador ainda não foi definido.
  • #define CALC_H marca que o arquivo já foi incluído.
  • #endif encerra a proteção.
Inclusão Múltipla

Sem header guards, o mesmo cabeçalho pode ser processado mais de uma vez na mesma compilação, gerando erros de redefinição ou conflitos entre declarações.

Compilando com o GCC10.8

Depois de escrever os arquivos do projeto, precisamos compilá-los corretamente. É nesse ponto que muitos alunos confundem #include com o comando de compilação.

Compilando vários arquivos10.8.1

Se o projeto tiver os arquivos main.c, calc.c e calc.h, um comando correto é:

gcc main.c calc.c -o programa

Nesse comando:

  • main.c contém a função main.
  • calc.c contém as implementações.
  • -o programa define o nome do executável final.

Se você compilar apenas main.c, o compilador pode conhecer os protótipos através de calc.h, mas o linker não encontrará as implementações que estão em calc.c.

Erro de Ligação

Quando a função foi declarada corretamente, mas a implementação não entrou na compilação, o erro mais comum é undefined reference.

Compilando programas que usam math.h10.8.2

Em muitos sistemas, a biblioteca matemática precisa ser ligada explicitamente com a opção -lm.

gcc programa.c -o programa -lm

Se o projeto tiver mais de um arquivo:

gcc main.c calc.c -o programa -lm

Nesse caso, -lm significa que o programa deve ser ligado contra a biblioteca matemática padrão.

Informando diretórios extras de cabeçalho com -I10.8.3

A opção -I do GCC serve para informar ao compilador diretórios adicionais onde ele deve procurar arquivos de cabeçalho .h.

Isso é útil quando os cabeçalhos não estão na mesma pasta do main.c.

Considere esta organização:

projeto/
|-- main.c
|-- src/
|   `-- calc.c
`-- include/
    `-- calc.h

Nesse caso, um comando possível é:

gcc main.c src/calc.c -Iinclude -o programa

Com isso, o compilador passa a procurar cabeçalhos também dentro da pasta include. Assim, em main.c, podemos escrever:

#include "calc.h"

sem precisar escrever o caminho completo do arquivo.

Também é possível usar mais de uma opção -I, se o projeto tiver mais de um diretório de cabeçalhos.

gcc main.c src/calc.c -Iinclude -Ilibs/include -o programa
Organização de Projeto

Em projetos maiores, é muito comum separar os arquivos .h em uma pasta include/ e os arquivos .c em uma pasta src/. A opção -I ajuda justamente a manter essa organização.

Exemplo completo de biblioteca própria10.9

O exemplo abaixo mostra a estrutura de um módulo simples de texto.

Arquivo texto_util.h:

#ifndef TEXTO_UTIL_H
#define TEXTO_UTIL_H

int contar_vogais(char str[]);
void inverter_string(char str[]);
int eh_palindromo(char str[]);

#endif

Arquivo texto_util.c:

#include "texto_util.h"
#include <ctype.h>
#include <string.h>

int contar_vogais(char str[]){
    int cont = 0;
    int i;

    for(i = 0; str[i] != '\0'; i++){
        char c = tolower(str[i]);
        if(c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u')
            cont++;
    }

    return cont;
}

void inverter_string(char str[]){
    int tam = strlen(str);
    int i;

    for(i = 0; i < tam / 2; i++){
        char aux = str[i];
        str[i] = str[tam - 1 - i];
        str[tam - 1 - i] = aux;
    }
}

int eh_palindromo(char str[]){
    int inicio = 0;
    int fim = strlen(str) - 1;

    while(inicio < fim){
        if(tolower(str[inicio]) != tolower(str[fim]))
            return 0;
        inicio++;
        fim--;
    }

    return 1;
}

Arquivo main.c:

#include <stdio.h>
#include "texto_util.h"

int main(){
    char palavra1[] = "arara";
    char palavra2[] = "computador";

    printf("Vogais em %s: %d\n", palavra2, contar_vogais(palavra2));
    printf("%s eh palindromo? %d\n", palavra1, eh_palindromo(palavra1));

    inverter_string(palavra2);
    printf("Invertida: %s\n", palavra2);

    return 0;
}

Compilação:

gcc main.c texto_util.c -o programa

Questões10.10

1. Escreva um programa que utilize a biblioteca <stdio.h> para ler o nome de um usuário e imprimir uma mensagem de boas-vindas.

  • Entrada: Maria.
  • Saída Esperada: Ola, Maria.

2. Crie um programa que utilize a biblioteca <string.h> para ler uma palavra e imprimir seu tamanho usando strlen().

  • Entrada: Computador.
  • Saída Esperada: Tamanho: 10.

3. Desenvolva um programa que utilize <math.h> para calcular a raiz quadrada de um número digitado pelo usuário. Escreva também o comando do GCC necessário para compilar o programa corretamente.

  • Entrada: 49.
  • Saída Esperada: Raiz: 7.0.

4. Escreva um arquivo calc.h contendo os protótipos das funções soma, media e maximo.

  • Entrada: Nenhuma.
  • Saída Esperada: Arquivo de cabeçalho criado corretamente.

5. Implemente o arquivo calc.c com as funções declaradas na questão anterior.

  • Entrada: Nenhuma.
  • Saída Esperada: Arquivo de implementação criado corretamente.

6. Crie um main.c que utilize as funções soma, media e maximo da sua própria biblioteca e mostre os resultados na tela.

  • Entrada: 10 e 6.
  • Saída Esperada: Soma: 16, Media: 8.0, Maximo: 10.

7. Escreva o comando do GCC para compilar corretamente um projeto composto por main.c, calc.c e calc.h.

  • Entrada: Nenhuma.
  • Saída Esperada: gcc main.c calc.c -o programa.

8. Considere a estrutura abaixo:

projeto/
|-- main.c
|-- src/
|   `-- calc.c
`-- include/
    `-- calc.h

Escreva o comando do GCC para compilar esse projeto usando a opção -I.

  • Entrada: Nenhuma.
  • Saída Esperada: gcc main.c src/calc.c -Iinclude -o programa.

9. Desenvolva uma biblioteca própria de vetores contendo pelo menos três funções, como soma_vetor, maior_vetor e media_vetor. Depois, crie um main que utilize essas funções.
Use a seguinte sugestao de arquivos:

  • vetor_util.h
  • vetor_util.c
  • main.c

Protótipos sugeridos:

int soma_vetor(int v[], int n);
int maior_vetor(int v[], int n);
float media_vetor(int v[], int n);

No main, teste com o vetor int v[5] = {1, 2, 3, 4, 5}; e escreva tambem o comando do GCC para compilar o projeto.

  • Entrada: Vetor {1, 2, 3, 4, 5}.
  • Saída Esperada: Valores calculados corretamente.

10. Crie uma biblioteca de strings contendo três funções: contar_vogais, inverter_string e eh_palindromo. Em seguida, escreva um programa principal que teste as três funções.
Use a seguinte sugestao de arquivos:

  • texto_util.h
  • texto_util.c
  • main.c

Protótipos sugeridos:

int contar_vogais(char str[]);
void inverter_string(char str[]);
int eh_palindromo(char str[]);

No main, teste com as strings "arara" e "computador" e escreva tambem o comando do GCC para compilar o projeto.

  • Entrada: arara e computador.
  • Saída Esperada: Número de vogais, verificação de palíndromo e string invertida.

11. Explique com suas palavras a diferença entre:

  • incluir um arquivo com #include;
  • declarar uma função em um .h;
  • implementar uma função em um .c;
  • compilar um programa com o GCC.

12. Explique por que um programa pode compilar parcialmente ao incluir o cabeçalho correto, mas ainda assim falhar na etapa final com a mensagem undefined reference.

Próximos passos10.11

Agora que entendemos como organizar o código em múltiplos arquivos e como criar módulos reutilizáveis, o próximo passo natural é modelar melhor os dados que esses módulos manipulam. Na próxima aula, Tipos Definidos pelo Programador, veremos como struct, enum, union e typedef ajudam a organizar informações e a construir bibliotecas mais claras em C.