¿Cuál es exactamente la diferencia entre “pasar por referencia” en C y en C++?
terminology pass-by-reference (1)
Hay preguntas que ya tratan con la diferencia entre pasar por referencia y pasar por valor . En esencia, pasar un argumento por valor a una función significa que la función tendrá su propia copia del argumento, su valor se copia. La modificación de esa copia no modificará el objeto original. Sin embargo, al pasar por referencia, el parámetro dentro de la función se refiere al mismo objeto que se pasó: cualquier cambio dentro de la función se verá afuera.
Desafortunadamente, hay dos formas en que se usan las frases "pasar por valor" y "pasar por referencia" que pueden causar confusión. Creo que esto es en parte la razón por la cual los punteros y las referencias pueden ser difíciles de adoptar para los nuevos programadores de C ++, especialmente cuando provienen de un fondo en C.
do
En C, todo se pasa por valor en el sentido técnico . Es decir, todo lo que proporcione como argumento a una función, se copiará en esa función. Por ejemplo, llamar a una función void foo(int)
con foo(x)
copia el valor de x
como el parámetro de foo
. Esto se puede ver en un ejemplo simple:
void foo(int param) { param++; }
int main()
{
int x = 5;
foo(x);
printf("%d/n",x); // x == 5
}
El valor de x
se copia en foo
y esa copia se incrementa. La x
en main
sigue teniendo su valor original.
Como estoy seguro de que saben, los objetos pueden ser de tipo puntero. Por ejemplo, int* p
define p
como un puntero a un int
. Es importante tener en cuenta que el siguiente código introduce dos objetos:
int x = 5;
int* p = &x;
El primero es de tipo int
y tiene el valor 5
. El segundo es de tipo int*
y su valor es la dirección del primer objeto.
Al pasar un puntero a una función, todavía lo está pasando por valor. La dirección que contiene se copia en la función. La modificación de ese puntero dentro de la función no cambiará el puntero fuera de la función; sin embargo, la modificación del objeto al que apunta cambiará el objeto fuera de la función. ¿Pero por qué?
Como dos punteros que tienen el mismo valor siempre apuntan al mismo objeto (contienen la misma dirección), se puede acceder y modificar el objeto al que se está apuntando a través de ambos. Esto da la semántica de haber pasado el apuntado al objeto por referencia, aunque en realidad nunca existió ninguna referencia, simplemente no hay referencias en C. Echa un vistazo al ejemplo cambiado:
void foo(int* param) { (*param)++; }
int main()
{
int x = 5;
foo(&x);
printf("%d/n",x); // x == 6
}
Podemos decir que cuando se pasa el int*
a una función, el int
que apunta se "pasó por referencia", pero en verdad el int
nunca fue pasado a ninguna parte, solo el puntero se copió en la función. Esto nos da el significado coloquial 1 de "pasar por valor" y "pasar por referencia".
El uso de esta terminología está respaldado por términos dentro del estándar. Cuando tiene un tipo de puntero, el tipo al que apunta se conoce como su tipo de referencia . Es decir, el tipo referenciado de int*
es int
.
Un tipo de puntero puede derivarse de un tipo de función, un tipo de objeto o un tipo incompleto, llamado el tipo referenciado .
Mientras que el operador unario *
(como en *p
) se conoce como direccionamiento indirecto en la norma, comúnmente también se conoce como desreferenciación de un puntero. Esto promueve aún más la noción de "pasar por referencia" en C.
C ++
C ++ adoptó muchas de las características del lenguaje original de C. Entre ellas se encuentran los punteros y, por lo tanto, esta forma coloquial de "pasar por referencia" todavía se puede usar - *p
sigue siendo una desreferenciación p
. Sin embargo, usar el término será confuso, porque C ++ introduce una característica que C no tiene: la capacidad de pasar referencias verdaderas.
Un tipo seguido de un signo es un tipo de referencia 2 . Por ejemplo, int&
es una referencia a un int
. cuando se pasa un argumento a una función que toma el tipo de referencia, el objeto se pasa realmente por referencia. No hay punteros involucrados, ni copia de objetos, ni nada. El nombre dentro de la función en realidad se refiere exactamente al mismo objeto que se pasó. Para contrastar con el ejemplo anterior:
void foo(int& param) { param++; }
int main()
{
int x = 5;
foo(x);
std::cout << x << std::endl; // x == 6
}
Ahora la función foo
tiene un parámetro que es una referencia a un int
. Ahora, al pasar x
, param
refiere precisamente al mismo objeto. El param
incremental tiene un cambio visible en el valor de x
y ahora x
tiene el valor 6.
En este ejemplo, nada fue pasado por valor. Nada fue copiado. A diferencia de C, donde pasar por referencia era simplemente pasar un puntero por valor, en C ++ realmente podemos pasar por referencia .
Debido a esta posible ambigüedad en el término "pasar por referencia", es mejor usarlo solo en el contexto de C ++ cuando se usa un tipo de referencia. Si está pasando un puntero, no está pasando por referencia, está pasando un puntero por valor (por supuesto, a menos que esté pasando una referencia a un puntero, por ejemplo, int*&
). Sin embargo, es posible que encuentre usos de "pasar por referencia" cuando se usan los punteros, pero al menos ahora sabe lo que realmente está sucediendo.
Otros idiomas
Otros lenguajes de programación complican aún más las cosas. En algunos, como Java, cada variable que tiene se conoce como una referencia a un objeto (no es lo mismo que una referencia en C ++, más como un puntero), pero esas referencias se pasan por valor. Entonces, aunque parezca que está pasando a una función por referencia, lo que realmente está haciendo es copiar una referencia en la función por valor. Esta diferencia sutil de pasar por referencia en C ++ se nota cuando asigna un nuevo objeto a la referencia pasada en:
public void foo(Bar param) {
param.something();
param = new Bar();
}
Si param.something()
que llamar a esta función en Java, pasando un objeto de tipo Bar
, se llamaría a la llamada a param.something()
en el mismo objeto que pasó. Esto se debe a que pasó una referencia a su objeto. Sin embargo, aunque se asigna una nueva Bar
a param
, el objeto fuera de la función sigue siendo el mismo objeto antiguo. El nuevo nunca se ve desde el exterior. Esto se debe a que la referencia dentro de foo
se está reasignando a un nuevo objeto. Este tipo de referencias de reasignación es imposible con las referencias de C ++.
1 Por "coloquial", no quiero sugerir que el significado de C de "pasar por referencia" sea menos verdadero que el de C ++, solo que C ++ realmente tiene tipos de referencia y, por lo tanto, usted realmente está pasando por referencia . El significado de C es una abstracción sobre lo que realmente pasa por valor.
2 Por supuesto, estas son referencias de valores y ahora también tenemos referencias de valores en C ++ 11.
La frase "pasar por referencia" es utilizada por los desarrolladores de C y C ++ por igual, pero parece que se usan para significar cosas diferentes. ¿Cuál es exactamente la diferencia entre esta frase equívoca en cada idioma?