Se um ponteiro guarda o endereço de uma variável, o que guarda o endereço de um ponteiro? Outro ponteiro. Essa ideia — ponteiro para ponteiro — parece um jogo de espelhos gratuito, mas ela resolve dois problemas muito reais: fazer uma função alterar um ponteiro do chamador, e representar uma lista de strings. E é justamente uma lista de strings que o sistema operacional entrega ao seu programa toda vez que ele roda. Hoje, aquele misterioso int main(int argc, char *argv[]) finalmente vai fazer sentido.
Um endereço que aponta para outro endereço
Vamos com calma. Você já sabe que int *p = &x; faz p guardar o endereço de x. Agora, p também é uma variável — logo, ela também tem um endereço. Um ponteiro para ponteiro guarda esse endereço:
#include <stdio.h>
int main(void) {
int x = 42;
int *p = &x; // p aponta para x
int **pp = &p; // pp aponta para p
printf("x = %d\n", x); // 42
printf("*p = %d\n", *p); // 42 — uma seta: p -> x
printf("**pp = %d\n", **pp); // 42 — duas setas: pp -> p -> x
return 0;
}
Leia int **pp como "ponteiro para ponteiro para int". Para chegar ao valor final, você segue duas setas: *pp te dá p (o ponteiro do meio), e **pp te dá x (o valor lá no fim). Cada asterisco é um passo a mais na cadeia.
Por que isso é útil: alterar o ponteiro do chamador
Lembre-se da lição central dos ponteiros: para uma função alterar uma variável do chamador, ela precisa do endereço dessa variável. Isso vale também quando a variável a ser alterada é, ela mesma, um ponteiro. Se uma função precisa mudar para onde um ponteiro aponta, ela precisa receber o endereço do ponteiro — ou seja, um ponteiro para ponteiro:
#include <stdio.h>
void apontar_para_novo(int **destino, int *novo) {
*destino = novo; // altera o ponteiro original do chamador
}
int main(void) {
int a = 10, b = 20;
int *p = &a;
printf("Antes: *p = %d\n", *p); // 10
apontar_para_novo(&p, &b); // passamos o ENDEREÇO de p
printf("Depois: *p = %d\n", *p); // 20 — p agora aponta para b
return 0;
}
Se a função recebesse apenas int *, ela poderia mudar o valor apontado, mas nunca para onde o ponteiro do chamador aponta — porque receberia só uma cópia do ponteiro. Recebendo int **, ela alcança o ponteiro original. Esse padrão é essencial na próxima fase do curso, quando funções de alocação de memória precisarão redirecionar seus ponteiros.
Um vetor de strings é um ponteiro para ponteiro
Aqui a teoria vira algo palpável. Uma string, você já sabe, é um char * (um ponteiro para seu primeiro caractere). Então uma lista de strings é um vetor de char * — e o nome desse vetor, por decaimento, é um char **:
#include <stdio.h>
int main(void) {
char *linguagens[] = {"C", "Rust", "Python", "Go"};
int total = 4;
for (int i = 0; i < total; i++) {
printf("%d: %s\n", i, linguagens[i]);
}
return 0;
}
Cada linguagens[i] é um char * que aponta para uma string. O vetor linguagens inteiro é, na prática, um char **. Segure essa imagem, porque é exatamente ela que o sistema operacional usa para conversar com o seu programa.
Finalmente: argc e argv
Todo programa C pode receber informações da linha de comando — os argumentos que você digita depois do nome do programa no terminal. Para acessá-los, escrevemos main na sua forma completa:
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("Nome do programa: %s\n", argv[0]);
printf("Número de argumentos: %d\n", argc);
for (int i = 0; i < argc; i++) {
printf("argv[%d] = %s\n", i, argv[i]);
}
return 0;
}
Dois parâmetros: argc (argument count) é o número de argumentos, e argv (argument vector) é o vetor de strings com os argumentos em si — um char *argv[], que é o mesmo que char **argv. Uma sutileza importante: argv[0] é sempre o nome do próprio programa, então argc é no mínimo 1. Os argumentos que você digitou começam em argv[1].
Compile e rode assim:
gcc programa.c -o programa
./programa laranja banana uva
Saída:
Nome do programa: ./programa
Número de argumentos: 4
argv[0] = ./programa
argv[1] = laranja
argv[2] = banana
argv[3] = uva
Argumentos chegam sempre como texto
Um detalhe que pega muita gente: todo argumento em argv é uma string, mesmo quando parece número. Se o usuário digita 42, seu programa recebe a string "42", não o inteiro. Para usá-lo como número, é preciso converter — com atoi (para int) ou, melhor, strtol, ambos de <stdlib.h>:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc < 3) {
printf("Uso: %s <numero1> <numero2>\n", argv[0]);
return 1; // encerra sinalizando erro
}
int a = atoi(argv[1]); // converte a string em int
int b = atoi(argv[2]);
printf("%d + %d = %d\n", a, b, a + b);
return 0;
}
Rodando ./soma 15 27, você obtém 15 + 27 = 42. Repare também no cuidado defensivo no início: verificamos argc antes de acessar argv[1] e argv[2]. Sem essa checagem, rodar o programa sem argumentos faria você acessar posições inexistentes do vetor — o mesmo desastre de sempre. Esse return 1 com uma mensagem de uso é o jeito idiomático de um programa C reclamar de argumentos faltando.
Um programa de verdade
Vamos montar uma pequena calculadora de linha de comando que soma todos os números passados:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Uso: %s n1 n2 n3 ...\n", argv[0]);
return 1;
}
long soma = 0;
for (int i = 1; i < argc; i++) { // começa em 1: pula o nome do programa
soma += atoi(argv[i]);
}
printf("Soma de %d números: %ld\n", argc - 1, soma);
return 0;
}
Rodando ./somatorio 10 20 30 40, a saída é Soma de 4 números: 100. O laço começa em i = 1 de propósito, para ignorar argv[0] (o nome do programa) e somar só os argumentos reais.
O que vem a seguir
Hoje fechamos a escada dos ponteiros com o ponteiro para ponteiro, e vimos que ele não é abstração vazia: é o que permite uma função redirecionar o ponteiro do chamador e é a forma como o sistema operacional entrega argumentos ao seu programa via argc/argv. Com isso, encerramos os fundamentos de ponteiros e vetores. Na próxima aula, começa a parte que talvez mais transforme sua compreensão do computador: vamos entender onde a memória vive — a diferença entre a pilha e o heap —, o mapa que prepara você para a alocação dinâmica com malloc.
Fontes e leituras recomendadas
- The C Programming Language (K&R), Kernighan & Ritchie — Cap. 5.6 (ponteiros para ponteiros) e Cap. 5.10 (
argc/argv) - cppreference —
mainfunction e parâmetros de linha de comando — https://en.cppreference.com/w/c/language/main_function - cppreference —
strtole conversão de strings para números — https://en.cppreference.com/w/c/string/byte/strtol - Modern C, Jens Gustedt — seção sobre
maine argumentos de programa — https://gustedt.gitlabpages.inria.fr/modern-c/ - POSIX — convenções de argumentos de linha de comando — https://pubs.opengroup.org/onlinepubs/9699919799/
Exercícios
Exercício 1
Declare int x = 7; int *p = &x; int **pp = &p;. Imprima x, *p e **pp e confirme que os três valem 7. Depois, altere x via **pp = 100; e imprima x para confirmar a mudança.
Ver resposta
✓ Resposta:
#include <stdio.h>
int main(void) {
int x = 7;
int *p = &x;
int **pp = &p;
printf("%d %d %d\n", x, *p, **pp); // 7 7 7
**pp = 100; // duas setas até x
printf("x = %d\n", x); // 100
return 0;
}
**pp = 100 segue pp -> p -> x e escreve no fim da cadeia, que é x.
Exercício 2
Escreva um programa que aceite argumentos de linha de comando e imprima quantos foram passados (sem contar o nome do programa) e cada um deles, numerado.
Ver resposta
✓ Resposta:
#include <stdio.h>
int main(int argc, char *argv[]) {
printf("Foram passados %d argumento(s).\n", argc - 1);
for (int i = 1; i < argc; i++) {
printf(" %d: %s\n", i, argv[i]);
}
return 0;
}
Subtraímos 1 de argc porque argv[0] é o nome do programa, não um argumento do usuário.
Exercício 3
Escreva um programa ./maiuscula palavra que receba uma palavra pela linha de comando e a imprima em maiúsculas. Trate o caso de nenhum argumento ser passado.
Ver resposta
✓ Resposta:
#include <stdio.h>
#include <ctype.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Uso: %s <palavra>\n", argv[0]);
return 1;
}
for (int i = 0; argv[1][i] != '\0'; i++) {
putchar(toupper((unsigned char) argv[1][i]));
}
putchar('\n');
return 0;
}
Note argv[1][i]: argv[1] é uma string (um char *), então indexá-la de novo dá um caractere. Verificamos argc antes de tocar em argv[1].
Exercício 4
Explique, com suas palavras, por que uma função que precisa redirecionar um ponteiro do chamador deve receber um int ** em vez de um int *.
Ver resposta
✓ Resposta: Uma função só recebe cópias dos seus argumentos. Se ela recebe um int * (uma cópia do ponteiro), pode alterar o valor apontado, mas qualquer mudança que faça no próprio ponteiro afeta só a cópia local — o ponteiro do chamador continua apontando para o mesmo lugar. Para mudar para onde o ponteiro do chamador aponta, a função precisa do endereço desse ponteiro, isto é, um int **. Com ele, *destino = novo alcança o ponteiro original e o redireciona. É a mesma lógica de "para alterar algo, receba o endereço dele" — só que aplicada a um ponteiro.
Exercício 5
Escreva um programa ./media n1 n2 n3 ... que calcule a média aritmética de todos os números passados como argumentos (use atof de <stdlib.h> para converter para double). Trate o caso de nenhum número ser passado, evitando divisão por zero.
Ver resposta
✓ Resposta:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc < 2) {
printf("Uso: %s n1 n2 n3 ...\n", argv[0]);
return 1;
}
double soma = 0.0;
int quantidade = argc - 1;
for (int i = 1; i < argc; i++) {
soma += atof(argv[i]);
}
printf("Média de %d números: %.2f\n", quantidade, soma / quantidade);
return 0;
}
A checagem argc < 2 garante que há pelo menos um número, o que impede a divisão por zero em soma / quantidade. O atof converte cada argumento (que chega como texto) em double.