Ponteiro para Ponteiro e Argumentos de Linha de Comando

[352] Ponteiro para Ponteiro e Argumentos de Linha de Comando

Cada asterisco a mais é um passo a mais na cadeia — e são dois deles que o sistema operacional usa para entregar a linha de comando ao seu programa. Aqui, argc e argv deixam de ser fórmula decorada: por que a contagem nunca é zero, por que o 42 digitado chega como texto, e como uma função altera o ponteiro do chamador.
Linguagem C

9 min de leitura

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

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.

Comentários

Mais em Linguagem C

Automação de Build com Make
Automação de Build com Make

Indentar o comando com espaço em vez de TAB gera um erro enigmático — é o…

Comunicação em Rede com Sockets
Comunicação em Rede com Sockets

Depois de conectado, um socket se lê e se escreve como arquivo — e é essa…

Variáveis, Tipos e a Memória por Trás Deles
Variáveis, Tipos e a Memória por Trás Deles

Um int não tem 4 bytes garantidos: o padrão fixa apenas mínimos e uma ordem, e…