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 valor | por puntero | por referencia | |
|---|---|---|---|
| Sintaxis | f(int x) | f(int* x), llamada f(&a) | f(int& x), llamada f(a) |
| Qué llega | Una copia | La dirección del objeto | Un alias del objeto |
| ¿El llamador ve cambios? | No | Sí | Sí |
| ¿Puede ser «nada»? | No | Sí — puntero nulo | No — debe atarse |
| Coste de copia | Copia completa en cada llamada | Solo una dirección | Solo 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?
- Valores pequeños de solo lectura (
int,double,char): por valor. Las copias son gratis y la función no puede romper tus variables. - Objetos grandes de solo lectura (una cadena larga, un vector grande): referencia
const—const std::string& texto. Sin copia, y el objeto del llamador queda protegido. - La función debe cambiar la variable del llamador: referencia (o puntero). La referencia se lee mejor; el puntero es la vía de la era C.
- «Puede que no haya objeto» es un caso real: puntero, y comprueba si es nulo antes de usarlo. Una referencia no puede ser nula — ese es su superpoder y su límite.
- Arrays C: decaen a puntero te guste o no — pasa siempre la longitud al lado.
Seguimientos que conviene tener listos
- «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
*. - «¿Por qué pasar objetos grandes por referencia const?» — evitar la copia manda;
constgarantiza solo-lectura. - «¿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. - «¿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
- Escribe
addOne(int* p)que incremente la variable del llamador; llámalo conxe imprime. - Cambia
mySwappara recibir los punteros por valor — es decir, intercambiar solo los punteros dentro — y explícate por qué el llamador no nota nada. - 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.