Paso por valor vs puntero vs referencia en C++

Respuesta corta: por valor la función recibe una copia (los cambios no salen). Punteros y referencias le dan el objeto real (los cambios salen). Un puntero puede ser nulo y moverse; una referencia es un alias atado de por vida. Por defecto: valor para cosas pequeñas, referencia const para objetos grandes de solo lectura.

Mira pasar la diferencia en directo — ambas funciones intentan poner su argumento a cero y solo una lo logra:

#include <iostream>
using namespace std;

void tryToZero(int x)    { x = 0; }    // una copia — el llamador no la ve
void actuallyZero(int* x) { *x = 0; }  // la dirección del objeto real

int main() {
    int a = 5, b = 5;
    tryToZero(a);
    cout << "after tryToZero:   " << a << endl;
    actuallyZero(&b);
    cout << "after actuallyZero: " << b << endl;
    return 0;
}

Las demos de esta página usan punteros porque nuestro motor del navegador aún no soporta parámetros por referencia (int&) — pero todas las reglas de abajo valen también para referencias, y su sintaxis va en bloques de código.

Las tres opciones, lado a lado

por valorpor punteropor referencia
Sintaxisf(int x)f(int* x), llamada f(&a)f(int& x), llamada f(a)
Qué llegaUna copiaLa dirección del objetoUn alias del objeto
¿El llamador ve cambios?NoSíSí
¿Puede ser «nada»?NoSí — puntero nuloNo — debe atarse
Coste de copiaCopia completa en cada llamadaSolo una direcciónSolo un alias

El caso clásico: intercambiar dos variables

Un swap debe modificar las variables del llamador, así que las copias no sirven — hacen falta direcciones:

#include <iostream>
using namespace std;

void mySwap(int* a, int* b) {
    int t = *a;   // guardar el valor apuntado por a
    *a = *b;
    *b = t;
}

int main() {
    int x = 1, y = 2;
    mySwap(&x, &y);
    cout << x << " " << y << endl;   // 2 1
    return 0;
}

La versión con referencia se lee más limpia en la llamada — misma mecánica, sin el ruido de &/*:

void mySwap(int& a, int& b) {
    int t = a;
    a = b;
    b = t;
}
// llamada: mySwap(x, y);   // parece por valor, actúa como puntero

Sorpresa: los arrays se pasan siempre como puntero

Al pasar un array C, lo que llega de verdad es un puntero a su primer elemento — el tamaño nunca viaja con él. Por eso toda función que recibe un array recibe también una longitud:

#include <iostream>
using namespace std;

int sum(int arr[], int n) {   // arr en realidad es int*
    int s = 0;
    for (int i = 0; i < n; i++) s += arr[i];
    return s;
}

int main() {
    int a[3] = {10, 20, 30};
    cout << sum(a, 3) << endl;
    return 0;
}

¿Entonces cuál uso?

Seguimientos que conviene tener listos

  1. «Puntero vs referencia — una razón concreta de cada uno.» Puntero: puede ser nulo y reasignarse. Referencia: siempre refiere a algo y jamás necesita comprobaciones de *.
  2. «¿Por qué pasar objetos grandes por referencia const?» — evitar la copia manda; const garantiza solo-lectura.
  3. «¿Qué falla en int& f() que devuelve una referencia a una local?» — la local muere al volver la función; la referencia le sobrevive. Comportamiento indefinido y pregunta-trampa favorita.
  4. «¿Pasar por valor cuesta siempre una copia?» — desde C++11, la semántica de movimiento convierte copias caras de temporales en movimientos baratos.

Práctica en el editor

  1. Escribe addOne(int* p) que incremente la variable del llamador; llámalo con x e imprime.
  2. Cambia mySwap para recibir los punteros por valor — es decir, intercambiar solo los punteros dentro — y explícate por qué el llamador no nota nada.
  3. Escribe maxOf(int a, int b, int c) por valor y comprueba que valor es el default correcto para valores pequeños.

Relacionado: las 15 preguntas para juniors (puntero vs referencia es la nº 1), ++i vs i++ y la división entera.