Pular para conteúdo

2. Funções: valor, ponteiro e referência

Este é o capítulo mais importante da Unidade 1. Em C você já domina duas formas de passar parâmetro: por valor e por ponteiro. O C++ acrescenta uma terceira — referência — que resolve o motivo mais comum pelo qual você usava ponteiro em C: "quero que a função altere a variável do chamador" ou "quero evitar copiar um valor grande, como um texto".

Por valor: o padrão que você já conhece

void dobra(int x) {
    x = x * 2;   // altera so a copia local
}

int main() {
    int n = 5;
    dobra(n);
    // n continua 5 aqui
}

Igualzinho a C: x é uma cópia. Alterar x dentro da função não afeta n no chamador.

Por ponteiro: como você resolvia isso em C

void dobra_ponteiro(int* x) {
    *x = *x * 2;
}

int main() {
    int n = 5;
    dobra_ponteiro(&n);
    // agora n e 10
}

Funciona, mas tem um custo de legibilidade: no ponto de chamada (dobra_ponteiro(&n)) fica visível que é passagem por endereço, mas dentro da função você precisa desreferenciar (*x) toda hora, e o ponteiro pode ser nulo — a função precisaria decidir o que fazer com dobra_ponteiro(nullptr).

Por referência: a novidade do C++

Uma referência é um "apelido" para uma variável já existente. Sintaxe: & no tipo do parâmetro, não na chamada.

void dobra_referencia(int& x) {
    x = x * 2;   // x AQUI e a variavel do chamador, nao uma copia
}

int main() {
    int n = 5;
    dobra_referencia(n);   // sem & na chamada
    // agora n e 10
}

Compare as três chamadas:

dobra(n);              // por valor: n nao muda
dobra_ponteiro(&n);     // por ponteiro: precisa de &, dentro usa *
dobra_referencia(n);    // por referencia: chamada limpa, sem * dentro

Diferenças que importam na prática:

  • Referência não pode ser nula. Uma vez ligada a uma variável, int& x = n; sempre aponta pra n — não existe "referência vazia" (ao contrário de ponteiro, que pode ser nullptr). Isso elimina uma categoria inteira de bug.
  • Referência não pode ser "religada". Depois de inicializada, x continua sendo um apelido para a mesma variável.
  • Por isso, sempre que a intenção é "alterar o valor do chamador, e ele com certeza vai existir", referência é a ferramenta certa. Ponteiro continua fazendo sentido quando o "nada" é uma resposta válida (por exemplo, "procure X e retorne o endereço, ou nulo se não achar" — você vai ver uma alternativa mais moderna para esse caso, o padrão do capítulo 3: bool de retorno e o resultado por referência).

const T&: a combinação que você vai usar o tempo todo

Agora o motivo pelo qual referência importa mesmo quando você não quer alterar nada.

Passar um std::string (ou, mais adiante, um std::vector) por valor faz uma cópia inteira toda vez que a função é chamada:

double calcula_total(std::string produto, double preco) {
    // "produto" foi copiado inteiro so para a funcao ler o valor
    return preco * 1.1;
}

Passar por referência evita a cópia — mas uma referência comum (std::string&) permite que a função altere o argumento, o que aqui não é a intenção. A solução é referência constante:

double calcula_total(const std::string& produto, double preco) {
    // sem copia, e o compilador GARANTE que "produto" nao e alterado aqui
    return preco * 1.1;
}

const T& virou o padrão em C++ para "quero ler um valor potencialmente grande sem copiar e sem correr risco de alterá-lo sem querer". Você já viu isso nos exercícios da Lista 01 (const std::string& nome) — agora sabe por quê. No capítulo 3 você vai criar seus próprios tipos com struct, e a mesma regra vale para eles: const MinhaStruct& é o padrão para recebê-los.

Regra prática para decidir a assinatura de um parâmetro:

Situação Assinatura
Tipo pequeno (int, double, char, bool), só leitura por valor: int n
Tipo qualquer, só leitura, quer evitar cópia const T&
Precisa alterar o argumento do chamador T&
"Pode não existir" bool de retorno + parâmetro por referência (capítulo 3)

Retorno por referência (cuidado)

Você pode retornar uma referência, mas nunca para uma variável local — ela deixa de existir quando a função termina:

int& perigoso() {
    int local = 10;
    return local;   // ERRO logico: referencia para memoria que nao existe mais
}

O compilador frequentemente avisa (warning: reference to local variable returned). Leve esse warning a sério — é um dos poucos casos em que C++ tenta te salvar de um bug sério e você não deve ignorar.

Sobrecarga de funções (overload)

Em C, cada função precisa de um nome único — por isso bibliotecas C têm abs, labs, fabs para int, long, double. C++ permite várias funções com o mesmo nome, desde que os parâmetros sejam diferentes:

int soma(int a, int b) { return a + b; }
double soma(double a, double b) { return a + b; }
std::string soma(const std::string& a, const std::string& b) { return a + b; }

O compilador escolhe a versão certa pelo tipo dos argumentos na chamada:

soma(2, 3);          // chama a versao int
soma(2.5, 1.5);       // chama a versao double
soma("a", "b");       // erro: const char* nao converte direto para a versao string sem ambiguidade em alguns casos — prefira soma(std::string("a"), "b")

Argumentos padrão (default arguments)

Um parâmetro pode ter um valor usado quando o chamador não o fornece:

double calcula_total(double preco, double taxa = 1.1) {
    return preco * taxa;
}

calcula_total(100.0);       // usa taxa = 1.1
calcula_total(100.0, 1.05); // usa taxa = 1.05

Argumentos com valor padrão precisam vir depois dos que não têm (senão o compilador não sabe qual argumento você está pulando).

Erros comuns

"error: invalid initialization of non-const reference from an rvalue" — você tentou passar um valor temporário (ex.: 2 + 3) para um parâmetro int& (referência não-const). Referência não-const precisa de uma variável de verdade para se ligar; use const int& se só for ler.

"error: call of overloaded ‘soma(const char [2], const char [2])’ is ambiguous" — literais de string ("a") são const char* em C++, não std::string. Quando isso causa ambiguidade entre sobrecargas, converta explicitamente: soma(std::string("a"), "b").

warning "reference to local variable ‘x’ returned" — veja a seção acima. Não ignore.

Resumo

  • Por valor: cópia, seguro, mas caro para tipos grandes.
  • Por ponteiro: como em C, permite alterar o chamador, mas pode ser nulo e exige */& manuais.
  • Por referência (T&): "apelido" da variável do chamador, nunca nula, sem * no corpo da função.
  • const T&: o padrão para parâmetros grandes que só serão lidos — sem cópia, sem risco de alteração.
  • Nunca retorne referência para variável local.
  • Sobrecarga: mesmo nome, parâmetros diferentes, o compilador escolhe pela chamada.
  • Argumento padrão: valor usado quando o chamador omite o argumento.