Arrays de ponteiros
Nesta aula, você aprenderá sobre arrays de ponteiros em C, com foco em arrays de strings, a manipulação de argv, técnicas de iteração e casos de uso práticos. O conteúdo inclui explicações detalhadas, exemplos de código e exercícios para fixação.
Arrays de ponteiros são uma das construções mais versáteis e poderosas da linguagem C. Eles permitem armazenar e manipular coleções de ponteiros, sendo essenciais para trabalhar com strings, listas de argumentos de linha de comando, tabelas de funções e estruturas de dados dinâmicas. Nesta aula, vamos explorar em profundidade o conceito de arrays de ponteiros, com ênfase em arrays de strings e no parâmetro argv da função main.
Compreender arrays de ponteiros é fundamental para dominar a gestão de memória e a manipulação de dados em C. Ao final desta aula, você será capaz de declarar, inicializar e percorrer arrays de ponteiros, bem como aplicá-los em situações práticas como processamento de argumentos e tabelas de lookup.
Array de strings (char*[])
Em C, uma string é tradicionalmente representada como um array de caracteres terminado por '\0'. Um array de strings é, portanto, um array de ponteiros para caracteres, ou seja, char *[]. Cada elemento do array é um ponteiro que aponta para o primeiro caractere de uma string. Essa estrutura permite armazenar múltiplas strings de comprimentos variados sem desperdiçar memória, pois cada string ocupa apenas o espaço necessário.
Por exemplo, podemos declarar um array de strings da seguinte forma:
const char *frutas[] = {"maçã", "banana", "laranja"};Aqui, frutas é um array de três ponteiros, cada um apontando para uma string literal. O modificador const indica que as strings não devem ser modificadas, pois literais de string ficam em região de memória somente leitura. Se precisarmos modificar as strings, devemos usar arrays de caracteres ou alocar memória dinamicamente.
Outra forma de inicializar um array de strings é usando arrays de caracteres separados e depois atribuindo-os a um array de ponteiros:
char nome1[] = "Alice";
char nome2[] = "Bob";
char *nomes[2];
nomes[0] = nome1;
nomes[1] = nome2;Nesse caso, os ponteiros apontam para arrays locais (na pilha) que podem ser modificados. É importante entender a diferença entre apontar para literais (constantes) e apontar para áreas editáveis.
O tamanho do array de ponteiros pode ser omitido se inicializarmos na declaração, e o compilador calculará o número de elementos automaticamente. Podemos também acessar e percorrer os elementos normalmente com índices ou aritmética de ponteiros.
argv
Na função main da linguagem C, os parâmetros argc e argv permitem que o programa receba argumentos da linha de comando. argc é o número de argumentos (incluindo o nome do programa), e argv é um array de ponteiros para strings, onde cada elemento é um argumento. A assinatura típica é:
int main(int argc, char *argv[])O array argv é um array de ponteiros para caracteres. argv[0] é o nome do programa (ou caminho usado para invocá-lo), e argv[1] até argv[argc-1] são os argumentos passados pelo usuário. O último elemento, argv[argc], é garantidamente NULL pela padrão C, o que pode ser usado como sentinela em loops.
Vamos ver um exemplo simples que imprime todos os argumentos:
#include <stdio.h>
int main(int argc, char *argv[]) {
for (int i = 0; i < argc; i++) {
printf("Argumento %d: %s\n", i, argv[i]);
}
return 0;
}Se compilarmos e executarmos com ./programa alfa beta, a saída será:
Argumento 0: ./programa
Argumento 1: alfa
Argumento 2: betaObserve que argv é um array de ponteiros, portanto argv[i] é um ponteiro para o primeiro caractere de cada argumento. Podemos também acessar caracteres individuais com argv[i][j].
É importante notar que os argumentos são passados como strings, mesmo que representem números. Para convertê-los para inteiros, usamos funções como atoi ou strtol.
Iteração
Iterar sobre um array de ponteiros pode ser feito de várias maneiras. A forma mais comum é usando um índice, como vimos no exemplo de argv. Também podemos usar um ponteiro para percorrer o array, aproveitando a aritmética de ponteiros. Por exemplo, para percorrer o array de frutas:
const char *frutas[] = {"maçã", "banana", "laranja"};
int n = sizeof(frutas) / sizeof(frutas[0]);
for (int i = 0; i < n; i++) {
printf("%s\n", frutas[i]);
}Alternativamente, podemos usar um ponteiro para ponteiro (char **p) para percorrer o array:
const char **p = frutas;
for (int i = 0; i < n; i++) {
printf("%s\n", *(p + i));
}Ou, de forma mais elegante, incrementando o ponteiro:
const char **p = frutas;
while (*p != NULL) {
printf("%s\n", *p);
p++;
}Nesse caso, o array precisa ter um elemento NULL no final para servir de sentinela. Muitas funções da biblioteca padrão, como execvp, esperam arrays terminados em NULL.
Quando usamos sizeof para obter o número de elementos, devemos ter cuidado: sizeof(frutas) retorna o tamanho total do array em bytes, e sizeof(frutas[0]) retorna o tamanho de um ponteiro. A divisão nos dá a quantidade de elementos. Isso só funciona se o array for definido na mesma função (não decaído para ponteiro).
Se o array for passado para uma função, ele decai para um ponteiro para o primeiro elemento, e sizeof não funcionará. Nesse caso, é necessário passar o número de elementos como parâmetro separado.
Casos de uso
Arrays de ponteiros são amplamente utilizados em programação C. Alguns casos comuns incluem:
- Manipulação de listas de strings: como listas de nomes, opções de menu, palavras-chave, etc.
- Argumentos de linha de comando: como vimos,
argvé um array de ponteiros. - Tabelas de lookup: mapear strings para valores ou funções, por exemplo, uma tabela de comandos para funções.
- Arrays de ponteiros para funções: permitem implementar máquinas de estado ou despacho de eventos.
- Estruturas de dados dinâmicas: como listas encadeadas de strings, onde cada nó contém um ponteiro para a string e outro para o próximo nó.
Vejamos um exemplo de tabela de lookup simples que associa comandos a funções:
#include <stdio.h>
#include <string.h>
void comando_ajuda() {
printf("Comandos disponíveis: ajuda, versao, sair\n");
}
void comando_versao() {
printf("Versão 1.0\n");
}
void comando_sair() {
printf("Saindo...\n");
}
int main() {
const char *comandos[] = {"ajuda", "versao", "sair"};
void (*funcoes[])() = {comando_ajuda, comando_versao, comando_sair};
char entrada[100];
while (1) {
printf("Digite um comando: ");
fgets(entrada, sizeof(entrada), stdin);
entrada[strcspn(entrada, "\n")] = 0; // remove newline
int i;
for (i = 0; i < 3; i++) {
if (strcmp(entrada, comandos[i]) == 0) {
funcoes[i]();
break;
}
}
if (i == 3) {
printf("Comando desconhecido. Use 'ajuda' para listar.\n");
}
if (strcmp(entrada, "sair") == 0) break;
}
return 0;
}Nesse exemplo, usamos dois arrays paralelos: um de strings (nomes dos comandos) e outro de ponteiros para funções. Isso permite associar cada comando a uma ação de forma eficiente.
Outro caso de uso importante é a criação de uma lista de strings dinâmica, onde o tamanho pode crescer conforme necessário usando realloc. Por exemplo:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
char **lista = NULL;
int capacidade = 0;
int tamanho = 0;
char buffer[100];
while (1) {
printf("Digite uma string (ou 'fim' para encerrar): ");
fgets(buffer, sizeof(buffer), stdin);
buffer[strcspn(buffer, "\n")] = 0;
if (strcmp(buffer, "fim") == 0) break;
if (tamanho == capacidade) {
capacidade = capacidade ? capacidade * 2 : 4;
lista = realloc(lista, capacidade * sizeof(char *));
if (!lista) { perror("realloc"); exit(1); }
}
lista[tamanho] = malloc(strlen(buffer) + 1);
if (!lista[tamanho]) { perror("malloc"); exit(1); }
strcpy(lista[tamanho], buffer);
tamanho++;
}
for (int i = 0; i < tamanho; i++) {
printf("%s\n", lista[i]);
free(lista[i]);
}
free(lista);
return 0;
}Esse trecho demonstra como gerenciar dinamicamente um array de ponteiros para strings, alocando memória conforme necessário e liberando-a ao final.
Boas práticas e observações finais
Ao trabalhar com arrays de ponteiros, é crucial estar atento à gestão de memória: sempre libere a memória alocada com malloc ou realloc usando free. Além disso, prefira usar const em ponteiros para strings literais para evitar modificações acidentais. Quando passar arrays para funções, lembre-se de que eles decaem para ponteiros, então o tamanho deve ser passado explicitamente ou o array deve ser terminado com NULL.
Outra boa prática é usar funções como strlcpy (se disponível) ou snprintf para evitar estouro de buffer. E, ao percorrer arrays, considere usar o padrão de sentinela NULL para simplificar os loops.
Dominar arrays de ponteiros abrirá portas para técnicas avançadas, como implementação de interpretadores, compiladores e sistemas de plugins. Pratique os exercícios para consolidar o conhecimento.
Referências
- GNU libc: Argument Syntax
- CppReference: Array
- CppReference: Pointer
- CppReference: malloc
- CppReference: free
- IBM Docs: argc/argv
- CProgramming: Arrays and Pointers
Exercícios
Escreva um programa que receba argumentos de linha de comando e os imprima na ordem inversa (do último para o primeiro).
✓ Resposta:#include <stdio.h> int main(int argc, char *argv[]) { for (int i = argc - 1; i >= 0; i--) { printf("%s\n", argv[i]); } return 0; }Dado o array de strings
const char *palavras[] = {"casa", "carro", "moto"};, escreva um programa que imprima cada string e o número de caracteres dela.✓ Resposta:#include <stdio.h> #include <string.h> int main() { const char *palavras[] = {"casa", "carro", "moto"}; int n = sizeof(palavras) / sizeof(palavras[0]); for (int i = 0; i < n; i++) { printf("%s: %zu caracteres\n", palavras[i], strlen(palavras[i])); } return 0; }Crie uma função que receba um array de ponteiros para strings e seu tamanho, e imprima todas as strings que começam com a letra 'a' (maiúscula ou minúscula).
✓ Resposta:#include <stdio.h> #include <ctype.h> void imprimir_com_a(const char *arr[], int n) { for (int i = 0; i < n; i++) { if (arr[i][0] == 'a' || arr[i][0] == 'A') { printf("%s\n", arr[i]); } } } int main() { const char *palavras[] = {"abacaxi", "banana", "Amora", "uva"}; int n = sizeof(palavras) / sizeof(palavras[0]); imprimir_com_a(palavras, n); return 0; }Implemente um programa que use um array de ponteiros para funções para simular uma calculadora simples (soma, subtração, multiplicação, divisão). O usuário deve digitar a operação e dois números. Use a tabela de lookup para selecionar a função.
✓ Resposta:#include <stdio.h> #include <string.h> double soma(double a, double b) { return a + b; } double subtrai(double a, double b) { return a - b; } double multiplica(double a, double b) { return a * b; } double divide(double a, double b) { return a / b; } int main() { const char *ops[] = {"+", "-", "*", "/"}; double (*funcs[])(double, double) = {soma, subtrai, multiplica, divide}; char op[10]; double a, b; printf("Digite a operação (+, -, *, /): "); scanf("%s", op); printf("Digite dois números: "); scanf("%lf %lf", &a, &b); int i; for (i = 0; i < 4; i++) { if (strcmp(op, ops[i]) == 0) { printf("Resultado: %.2lf\n", funcs[i](a, b)); break; } } if (i == 4) printf("Operação inválida.\n"); return 0; }Escreva um programa que leia várias strings do usuário até que ele digite "fim". Armazene-as em um array de ponteiros alocado dinamicamente e depois imprima todas. Não se esqueça de liberar a memória.
✓ Resposta:#include <stdio.h> #include <stdlib.h> #include <string.h> int main() { char **lista = NULL; int capacidade = 0; int tamanho = 0; char buffer[100]; while (1) { printf("Digite uma string (ou 'fim'): "); fgets(buffer, sizeof(buffer), stdin); buffer[strcspn(buffer, "\n")] = 0; if (strcmp(buffer, "fim") == 0) break; if (tamanho == capacidade) { capacidade = capacidade ? capacidade * 2 : 4; lista = realloc(lista, capacidade * sizeof(char *)); if (!lista) { perror("realloc"); exit(1); } } lista[tamanho] = malloc(strlen(buffer) + 1); if (!lista[tamanho]) { perror("malloc"); exit(1); } strcpy(lista[tamanho], buffer); tamanho++; } for (int i = 0; i < tamanho; i++) { printf("%s\n", lista[i]); free(lista[i]); } free(lista); return 0; }