Passagem por valor vs. por referência — quando copiar e quando apontar
Nas aulas de funções você aprendeu que C passa argumentos por valor — a função recebe uma cópia independente de cada argumento. Isso é seguro, mas insuficiente para dois cenários importantes: quando a função precisa modificar a variável original, ou quando passar a cópia de um dado grande (como uma struct) seria caro.
Esta aula aprofunda os dois mecanismos, agora com o conhecimento completo de ponteiros das aulas 20 a 22:
| Mecanismo | O que é passado | Pode modificar o original? | Custo | Sintaxe na chamada |
|---|---|---|---|---|
| Por valor | Cópia do dado | ❌ Não | Proporcional ao tamanho do tipo | f(x) |
| Por referência | Endereço de memória | ✅ Sim | Sempre 8 bytes (o ponteiro) | f(&x) |
Quando você chama f(x), o compilador cria um novo frame na pilha para f e copia o valor de x nesse frame. As duas variáveis são independentes a partir daí.
#include <stdio.h>
void tentarZerar(int n) {
printf(" [dentro] n antes = %d endereço = %p\n", n, &n);
n = 0;
printf(" [dentro] n depois = %d endereço = %p\n", n, &n);
}
int main() {
int x = 42;
printf("[main] x antes = %d endereço = %p\n", x, &x);
tentarZerar(x);
printf("[main] x depois = %d endereço = %p\n", x, &x);
return 0;
}
x e n estão em endereços diferentes — são variáveis distintas. Zerar n não afeta x. Ao retornar de tentarZerar, o frame de n é destruído e x permanece intacto com 42.
| Situação | Justificativa |
|---|---|
Tipos primitivos pequenos (int, float, char) |
Custo de cópia é mínimo (4–8 bytes); evita efeitos colaterais acidentais |
| A função calcula e retorna o resultado | int quadrado(int n) { return n*n; } — limpo e sem efeitos colaterais |
| Você quer garantir que o dado original não será alterado | Isolamento é desejável — a cópia age como proteção |
Ao chamar f(&x), você passa o endereço de x. A função recebe um ponteiro que aponta para a mesma posição de memória que x ocupa no chamador. Tudo que a função escrever via *p modifica diretamente x.
#include <stdio.h>
void zerar(int *p) {
printf(" [dentro] *p antes = %d endereço de p = %p *p em = %p\n",
*p, &p, p);
*p = 0;
printf(" [dentro] *p depois = %d\n", *p);
}
int main() {
int x = 42;
printf("[main] x antes = %d endereço de x = %p\n", x, &x);
zerar(&x);
printf("[main] x depois = %d\n", x);
return 0;
}
p dentro de zerar está em um endereço diferente (na stack de zerar), mas o valor que ele guarda (*p em) é o mesmo endereço de x. Eles compartilham a mesma posição de memória de dado — como dois controles remotos apontando para a mesma TV.
| Situação | Exemplo |
|---|---|
| A função precisa modificar a variável do chamador | void incrementar(int *n) { (*n)++; } |
| A função precisa retornar mais de um resultado | void minMax(int[], int, int*, int*) |
| Passar struct grande para evitar cópia cara | void exibir(const Aluno *a) |
| Alocar memória e devolver o ponteiro ao chamador | void criar(int **pp, int n) |
Em C profissional, a combinação de * e const em parâmetros comunica com precisão a intenção da função — o que ela faz e o que não faz com os dados:
| Declaração do parâmetro | Pode mudar *p? |
Pode mudar p? |
Uso típico |
|---|---|---|---|
int *p |
✅ Sim | ✅ Sim | Parâmetro de saída — função escreve resultado via ponteiro |
const int *p |
❌ Não | ✅ Sim | Parâmetro de entrada — função só lê, não modifica o dado |
int * const p |
✅ Sim | ❌ Não | Raro — o ponteiro em si não pode ser redirecionado |
const int * const p |
❌ Não | ❌ Não | Ponteiro completamente imutável — leitura pura |
#include <stdio.h> /* Só lê — const int* protege o dado */ float calcularMedia(const float *v, int n) { float soma = 0; for (int i = 0; i < n; i++) soma += v[i]; // OK: leitura // v[0] = 0; // ❌ ERRO: const proíbe escrita return soma / n; } /* Escreve resultado via ponteiro de saída */ void minMax(const int *v, int n, int *minimo, int *maximo) { *minimo = *maximo = v[0]; for (int i = 1; i < n; i++) { if (v[i] < *minimo) *minimo = v[i]; if (v[i] > *maximo) *maximo = v[i]; } } int main() { float notas[] = {7.5f, 8.0f, 6.0f, 9.5f, 5.5f}; int valores[] = {3, 7, 1, 9, 4}; printf("Média: %.1f\n", calcularMedia(notas, 5)); int mn, mx; minMax(valores, 5, &mn, &mx); printf("Min: %d Max: %d\n", mn, mx); return 0; }
const tipo *p. Isso é documentação executável — o compilador garante que a função não modifica o dado acidentalmente, e o chamador sabe que seus dados estão protegidos.
Uma função só pode devolver um único valor com return. Quando você precisa de dois ou mais resultados, use ponteiros de saída — é o padrão idiomático em C:
#include <stdio.h> #include <math.h> /* Converte coordenadas cartesianas para polares */ void cartesianParaPolar(double x, double y, double *raio, double *angulo) { *raio = sqrt(x*x + y*y); *angulo = atan2(y, x) * 180.0 / 3.14159265; // em graus } /* Divide e retorna quociente + resto */ int dividir(int dividendo, int divisor, int *quociente, int *resto) { if (divisor == 0) return 0; // erro: retorna 0 (falso) *quociente = dividendo / divisor; *resto = dividendo % divisor; return 1; // sucesso: retorna 1 (verdadeiro) } /* Estatísticas completas de um vetor */ void estatisticas(const float *v, int n, float *media, float *maior, float *menor, float *desvio) { float soma = 0; *maior = *menor = v[0]; for (int i = 0; i < n; i++) { soma += v[i]; if (v[i] > *maior) *maior = v[i]; if (v[i] < *menor) *menor = v[i]; } *media = soma / n; float varSoma = 0; for (int i = 0; i < n; i++) varSoma += (v[i] - *media) * (v[i] - *media); *desvio = sqrt(varSoma / n); } int main() { double r, ang; cartesianParaPolar(3.0, 4.0, &r, &ang); printf("(3,4) → raio=%.2f ângulo=%.1f°\n", r, ang); int q, res; if (dividir(17, 5, &q, &res)) printf("17 ÷ 5 = %d resto %d\n", q, res); if (!dividir(10, 0, &q, &res)) printf("Divisão por zero!\n"); float notas[] = {6.0f, 8.5f, 7.0f, 9.0f, 5.5f}; float med, mx, mn, desv; estatisticas(notas, 5, &med, &mx, &mn, &desv); printf("\nMédia: %.2f Maior: %.1f Menor: %.1f Desvio: %.2f\n", med, mx, mn, desv); return 0; }
dividir usa o return para indicar sucesso/falha (0 = erro, 1 = ok) e ponteiros para os resultados. Esse padrão é onipresente na biblioteca padrão de C — scanf, fgets, malloc e dezenas de outras funções usam exatamente essa convenção.
Passar uma struct por valor copia todos os seus bytes. Para uma struct de 200 bytes chamada 100 mil vezes, isso é 20 MB de cópia desnecessária. Ponteiros resolvem os dois problemas:
#include <stdio.h>
#include <string.h>
typedef struct {
char nome[50];
float salario;
int nivel;
} Funcionario;
/* Só lê — const evita modificação acidental, custo = 8 bytes */
void exibir(const Funcionario *f) {
printf("%-20s | R$%8.2f | Nível %d\n",
f->nome, f->salario, f->nivel);
}
/* Modifica — aplica reajuste direto na struct original */
void aplicarReajuste(Funcionario *f, float percentual) {
f->salario *= (1.0f + percentual / 100.0f);
if (f->salario > 10000.0f && f->nivel < 3)
f->nivel++; // promoção automática acima de R$10k
}
/* Encontra o de maior salário — retorna ponteiro para o elemento */
Funcionario *melhorPago(Funcionario *equipe, int n) {
Funcionario *melhor = &equipe[0];
for (int i = 1; i < n; i++)
if (equipe[i].salario > melhor->salario)
melhor = &equipe[i];
return melhor;
}
int main() {
Funcionario equipe[] = {
{"Ana Oliveira", 8500.0f, 2},
{"Bruno Santos", 6200.0f, 1},
{"Carla Mendes", 11000.0f, 3},
{"Diego Nunes", 7800.0f, 2}
};
int n = 4;
printf("=== Antes do reajuste ===\n");
for (int i = 0; i < n; i++) exibir(&equipe[i]);
for (int i = 0; i < n; i++) aplicarReajuste(&equipe[i], 10.0f);
printf("\n=== Após reajuste de 10%% ===\n");
for (int i = 0; i < n; i++) exibir(&equipe[i]);
Funcionario *star = melhorPago(equipe, n);
printf("\nMelhor pago: %s (R$%.2f)\n", star->nome, star->salario);
return 0;
}
Quando uma função precisa alocar memória e devolver o ponteiro ao chamador, ela recebe um ** (ponteiro para ponteiro). Isso é necessário porque o ponteiro em si precisa ser modificado:
#include <stdio.h> #include <stdlib.h> #include <string.h> /* Cria e inicializa um vetor de zeros — modifica o ponteiro do chamador */ int criarVetor(int **pp, int n) { *pp = (int *) calloc(n, sizeof(int)); // escreve no ponteiro original if (*pp == NULL) return 0; // falhou return 1; // sucesso } /* Duplica uma string em memória dinâmica */ char *duplicarString(const char *src) { int len = strlen(src); char *copia = (char *) malloc((len + 1) * sizeof(char)); if (copia == NULL) return NULL; strcpy(copia, src); return copia; // chamador é responsável por fazer free } int main() { int *v = NULL; if (criarVetor(&v, 5)) { // passa &v — função recebe int ** for (int i = 0; i < 5; i++) v[i] = (i + 1) * 10; printf("Vetor: "); for (int i = 0; i < 5; i++) printf("%d ", v[i]); printf("\n"); free(v); } char *s = duplicarString("Ponteiros em C"); if (s) { printf("Cópia: %s\n", s); free(s); // chamador libera o que a função alocou } return 0; }
duplicarString), o chamador fica responsável por chamar free. Isso deve ser documentado no cabeçalho ou num comentário — é um "contrato" entre quem cria e quem usa a função.
/* ❌ ERRADO — variável local é destruída ao retornar */ int *funcaoPerigosa() { int resultado = 42; return &resultado; // endereço de variável que não existe mais! } /* ✅ CORRETO — aloca no heap: sobrevive ao retorno da função */ int *funcaoCorreta() { int *p = (int *) malloc(sizeof(int)); if (p) *p = 42; return p; // chamador deve fazer free(p) } /* ✅ TAMBÉM CORRETO — ponteiro para static: persiste entre chamadas */ int *funcaoComStatic() { static int resultado = 42; return &resultado; // static vive durante todo o programa }
/* ❌ ERRADO — se p for NULL, *p causa segfault */ void processar_errado(int *p) { *p = *p * 2; // crash se p == NULL } /* ✅ CORRETO — verificar sempre */ int processar_correto(int *p) { if (p == NULL) return 0; // guard clause *p = *p * 2; return 1; }
& na chamada
void dobrar(int *n) { *n *= 2; }
int main() {
int x = 5;
dobrar(x); // ❌ passa o VALOR 5 como se fosse endereço — segfault!
dobrar(&x); // ✅ passa o endereço de x
return 0;
}
/* ❌ Função "leitora" que acidentalmente modifica dados */ float somar_errado(float *v, int n) { float soma = 0; while (n--) soma += *v++; // modifica v — pode ser bug se ponteiro for reutilizado return soma; } /* ✅ Correto: const garante que o compilador avisa qualquer modificação */ float somar_correto(const float *v, int n) { float soma = 0; for (int i = 0; i < n; i++) soma += v[i]; return soma; }
| Erro | Sintoma | Solução |
|---|---|---|
| Retornar ponteiro para local | Comportamento indefinido — dados corrompidos | Usar malloc ou static |
| Desreferenciar sem verificar NULL | Segmentation fault | Sempre verificar if (p == NULL) |
Esquecer & na chamada |
Segmentation fault ou valor errado | Usar f(&x) para passar por referência |
| Modificar dados de entrada inadvertidamente | Bug sutil e difícil de encontrar | Usar const tipo * para parâmetros somente leitura |
Um programa que usa todos os padrões desta aula: leitura por ponteiro, cálculo com const, retorno múltiplo e alocação dinâmica:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
typedef struct {
char nome[40];
float *notas; // array dinâmico — tamanho definido em execução
int qtdNotas;
} Aluno;
/* Cria aluno com array dinâmico de notas */
int inicializarAluno(Aluno *a, const char *nome, int qtd) {
if (a == NULL || qtd <= 0) return 0;
strncpy(a->nome, nome, 39);
a->nome[39] = '\0';
a->notas = (float *) malloc(qtd * sizeof(float));
if (a->notas == NULL) return 0;
a->qtdNotas = qtd;
return 1;
}
/* Calcula estatísticas — const: só lê */
void calcularEstatisticas(const Aluno *a,
float *media, float *maior, float *menor) {
*media = *maior = *menor = a->notas[0];
for (int i = 1; i < a->qtdNotas; i++) {
*media += a->notas[i];
if (a->notas[i] > *maior) *maior = a->notas[i];
if (a->notas[i] < *menor) *menor = a->notas[i];
}
*media /= a->qtdNotas;
}
/* Exibe relatório — const: só lê */
void exibirAluno(const Aluno *a) {
float med, mx, mn;
calcularEstatisticas(a, &med, &mx, &mn);
printf("%-15s | Notas: ", a->nome);
for (int i = 0; i < a->qtdNotas; i++)
printf("%.1f ", a->notas[i]);
printf("| Média: %.1f | %s\n",
med, med >= 7.0f ? "Aprovado" : "Reprovado");
}
/* Libera memória dinâmica do aluno */
void liberarAluno(Aluno *a) {
if (a && a->notas) {
free(a->notas);
a->notas = NULL;
a->qtdNotas = 0;
}
}
int main() {
int qtd = 3;
Aluno turma[3];
const char *nomes[] = {"Ana Silva", "Bruno Lima", "Carla Matos"};
float notasDados[][3] = {
{8.0f, 9.0f, 7.5f},
{5.0f, 6.0f, 4.5f},
{9.5f, 10.f, 9.0f}
};
/* Inicializa cada aluno com array dinâmico */
for (int i = 0; i < qtd; i++) {
if (!inicializarAluno(&turma[i], nomes[i], 3)) {
fprintf(stderr, "Erro ao alocar aluno %d\n", i);
for (int j = 0; j < i; j++) liberarAluno(&turma[j]);
return 1;
}
for (int j = 0; j < 3; j++)
turma[i].notas[j] = notasDados[i][j];
}
printf("=== Boletim da Turma ===\n");
for (int i = 0; i < qtd; i++)
exibirAluno(&turma[i]);
/* Libera toda a memória dinâmica */
for (int i = 0; i < qtd; i++)
liberarAluno(&turma[i]);
return 0;
}
| Aspecto | C | Java |
|---|---|---|
Primitivos (int, float…) |
Por valor — cópia | Por valor — cópia (idêntico) |
| Modificar primitivo no chamador | Ponteiro explícito: void f(int *p) |
Impossível diretamente — primitivos sempre por valor |
| Structs / objetos | Por valor (cópia de todos os bytes) — use ponteiro para evitar | Referência por valor — objetos sempre por referência implícita |
| Múltiplos retornos | Ponteiros de saída: void f(int *a, int *b) |
Retornar array, classe ou usar wrapper |
| Proteção de leitura | const tipo *p |
Sem equivalente direto para primitivos; final para referências |
| Swap de dois inteiros | ✅ trocar(&a, &b) |
❌ Impossível para primitivos |
void trocar(int *a, int *b) {
int t = *a;
*a = *b;
*b = t;
}
int x = 10, y = 20;
trocar(&x, &y);
// x = 20, y = 10 ✅
static void trocar(int a, int b) {
int t = a;
a = b;
b = t;
// modifica cópias locais
}
int x = 10, y = 20;
trocar(x, y);
// x = 10, y = 20 ❌ (inalterados)