Material Extra - C Avançado
Esta aula é EXTRA. O conteúdo abaixo não é cobrado nas avaliações da disciplina e não é pré-requisito para as próximas aulas. Ele existe para quem quiser aprofundar um pouco mais em C: matrizes dinâmicas, ponteiros genéricos, ponteiros para função e compilação com Makefile. Uma introdução (obrigatória) a C++ para quem já sabe C será vista mais adiante, na Aula 15b.
|
Matrizes dinâmicas (vetores de ponteiros)
Diferentemente de um vetor unidimensional, uma matriz dinamicamente alocada normalmente é construída como um vetor de ponteiros, onde cada ponteiro aponta para um vetor (uma linha da matriz), alocado separadamente.
#include <stdio.h> #include <stdlib.h> int main() { int linhas = 3, colunas = 4; // 1. Aloca o vetor de ponteiros (uma posição por linha) int **matriz = (int **) malloc(linhas * sizeof(int *)); // 2. Aloca cada linha individualmente for (int i = 0; i < linhas; i++) { matriz[i] = (int *) malloc(colunas * sizeof(int)); } // 3. A matriz pode ser utilizada normalmente, com dois índices for (int i = 0; i < linhas; i++) { for (int j = 0; j < colunas; j++) { matriz[i][j] = i + j; } } // 4. Libera cada linha e, por último, o vetor de ponteiros for (int i = 0; i < linhas; i++) { free(matriz[i]); } free(matriz); return 0; }
| Observação. A liberação de uma matriz dinâmica deve seguir a ordem inversa da alocação: primeiro liberamos cada linha individualmente e, somente depois, o vetor de ponteiros que as armazenava. Liberar o vetor de ponteiros antes das linhas faria perdermos o único acesso a elas, causando vazamento de memória. |
Ponteiros genéricos (void *)
Um ponteiro do tipo void * é chamado de ponteiro genérico: ele armazena um endereço de memória, mas não possui um tipo associado, não podendo ser diretamente desreferenciado. É esse o tipo de ponteiro retornado por malloc(), justamente para que a função possa ser utilizada com qualquer tipo de dado.
void *ponteiroGenerico; int x = 10; ponteiroGenerico = &x; // Para utilizar o valor, é necessário convertê-lo (cast) para o tipo correto printf("%d\n", *((int *) ponteiroGenerico));
Esse recurso será revisitado mais adiante na disciplina, ao implementarmos estruturas de dados genéricas, capazes de armazenar diferentes tipos de dados através de ponteiros void *.
Ponteiros para função
Assim como podemos ter um ponteiro que armazena o endereço de uma variável, também é possível ter um ponteiro que armazena o endereço de uma função. Esse recurso é bastante utilizado para tornar o comportamento de uma função configurável, permitindo, por exemplo, indicar qual critério de comparação deve ser utilizado em um algoritmo de ordenação.
#include <stdio.h> int soma(int a, int b) { return a + b; } int subtracao(int a, int b) { return a - b; } int main() { // 'operacao' é um ponteiro para uma função que recebe (int, int) e retorna int int (*operacao)(int, int); operacao = soma; printf("%d\n", operacao(4, 3)); // Imprime: 7 operacao = subtracao; printf("%d\n", operacao(4, 3)); // Imprime: 1 return 0; }
Este é apenas um primeiro contato com o conceito; ponteiros para função voltarão a ser úteis mais adiante na disciplina, especialmente ao implementarmos estruturas de dados genéricas.
Compilação e execução em linha de comando
Um programa em C passa por duas etapas até virar algo executável: a compilação (o código-fonte é traduzido para um arquivo executável) e a execução (o sistema operacional roda esse executável):
gcc -Wall main.c -o programa # compila main.c e gera o executável "programa" ./programa # executa o programa (Linux/macOS) programa.exe # executa o programa (Windows)
Quando o programa está dividido em vários arquivos-fonte (como vimos ao separar interface e implementação de um TAD, na Aula 2b), todos precisam ser informados ao compilador de uma só vez:
gcc -Wall main.c aluno.c turma.c -o programa
Digitar esse comando à mão, listando manualmente todo arquivo .c do projeto, é cansativo e cresce junto com o projeto — e é exatamente esse problema que o Makefile resolve.
Automatizando com Makefile
Um Makefile é um arquivo de configuração lido pelo programa make, que descreve como construir um projeto: quais arquivos dependem de quais, e qual comando executar para gerar cada um. Em vez de lembrar (e digitar) o comando completo do gcc, basta rodar:
make
Um Makefile simples para o exemplo com main.c, aluno.c e turma.c poderia ser:
programa: main.c aluno.c turma.c gcc -Wall main.c aluno.c turma.c -o programa clean: rm -f programa
Estrutura de uma regra. Cada regra do Makefile segue o formato alvo: dependências, seguido de uma linha de comando (que deve começar com um caractere de tabulação, não espaços). A regra programa: main.c aluno.c turma.c diz que o alvo programa depende desses três arquivos; se qualquer um deles for modificado, rodar make novamente recompila o projeto. A regra clean é uma convenção comum para remover os arquivos gerados, executada com make clean.
|
Para projetos maiores, o make também evita recompilar arquivos que não mudaram, tornando a compilação mais rápida — mas essa otimização (usando arquivos .o intermediários) foge do escopo desta disciplina. Por ora, o importante é reconhecer que um Makefile existe para automatizar exatamente o comando de compilação que já conhecemos.
Exercícios
- Explique por que a liberação de uma matriz dinâmica (vetor de ponteiros) precisa seguir a ordem inversa da alocação.
-
Por que um
Makefileé útil mesmo sendo possível compilar um projeto digitando o comandogcccompleto manualmente? O que aconteceria se a linha de comando de uma regra doMakefilecomeçasse com espaços em vez de uma tabulação?
Sugestões de Respostas dos Exercícios
Exercício 1
Porque, para acessar qualquer linha da matriz, é preciso primeiro passar pelo vetor de ponteiros (matriz) para depois chegar a cada linha (matriz[i]). Se liberássemos matriz antes de liberar cada matriz[i], perderíamos a única referência que tínhamos para essas linhas, tornando-as inacessíveis e impossíveis de liberar — um vazamento de memória. Por isso, é preciso liberar primeiro as linhas, e só depois o vetor de ponteiros.
Exercício 2
Um Makefile é útil porque evita ter que lembrar e redigitar o comando de compilação completo (com todos os arquivos .c do projeto) toda vez que algo precisar ser recompilado — especialmente à medida que o projeto cresce. Com o Makefile, basta digitar make.
Se a linha de comando de uma regra começasse com espaços em vez de uma tabulação, o programa make não reconheceria aquela linha como o comando da regra, e o processo de compilação falharia com um erro — o caractere de tabulação faz parte da sintaxe exigida pelo formato do Makefile.