Uso de referencias con memoria dinámica [C++]

Iniciado por K-YreX, 30 Marzo 2019, 14:23 PM

0 Miembros y 2 Visitantes están viendo este tema.

K-YreX

Cuando trabajamos con memoria dinámica y pasamos una variable (ya sea un array, una variable sencilla o un objeto), hasta el punto que yo sé, lo que estamos pasando es el puntero que apunta a la dirección de memoria donde hemos guardado esa variable. Entonces me surgen las siguientes preguntas:
  • ¿Por qué si pasamos un puntero a una función y queremos reservar memoria una vez en la función, hay que pasar ese puntero además por referencia?
  • Tenemos, imaginemos, un array bidimensional (matriz) que ha sido reservada con memoria dinámica, ¿hay diferencia entre pasarla por referencia o por valor?
    En ambos casos estamos pasando la dirección de memoria de su primer elemento, pero viendo que para la primera pregunta que he planteado sí hay diferencia entre usar referencias o no, quería saber si para este segundo caso es más "eficiente" pasarla por referencia.
Código (cpp) [Seleccionar]

cout << "Todos tenemos un defecto, un error en nuestro código" << endl;

CalgaryCorpus

Si pasas por valor, no lograras que lo que asignes a ese puntero se modifique, pues vas a estar asignandole a una copia.

No hay diferencia de otra manera.
Aqui mi perfil en LinkedIn, invitame un cafe aqui

K-YreX

#2
Después de darle unas cuántas vueltas creo que he llegado a una explicación. Si alguien ve algo raro o incorrecto que me lo comente para corregirlo ya que creo que puede servir para gente que tenga dudas con esto :-X
Voy a usar un ejemplo muy sencillo para explicar esto:
Código (cpp) [Seleccionar]

void anular(int *v, int pos){
   v[pos] = 0;
}

int main(){
   int *p;
   p = new int [2];
   p[0] = 1;
   p[1] = 2;
   anular(p,1);
}


En la línea 6, declaramos un puntero (almacena una dirección de memoria) <p> y hacemos que apunte a <NULL>. Vamos a suponer que se guarda en la posición de memoria 0x1 (voy a usar posiciones de memoria consecutivas para simplificar el ejemplo). Entonces quedaría así:

0x1(p) = NULL



En la línea 7, reservamos memoria para guardar dos enteros. Suponemos que sus posiciones son 0x2 y 0x3 respectivamente y en las líneas 8 y 9 asignamos valores a esas posiciones. Entonces tenemos:

0x1(p) = 0x2
0x2(p[0]) = 1
0x3(p[1]) = 2



En la línea 10, llamamos a la función <anular()>, la cual recibe el puntero que almacena la dirección de memoria del primer elemento del array. Al ser pasado por valor, se copia el contenido de <p> en <v> (suponemos <v> en 0x4) y el 1 se copia en <pos> (suponemos 0x5). Entonces tenemos antes de ejecutar la función:

0x1(p) = 0x2
0x2(p[0]) = 1
0x3(p[1]) = 2
0x4(v) = 0x2
0x5(pos) = 1



Al ejecutar la línea 2, el contenido se modifica en el "array original" (que es siempre el mismo porque sólo tenemos uno). Esto es porque <v> almacena la misma posición de memoria que <p> (OJO: <v> no almacena la posición de memoria dónde se guarda <p>, sino la dirección de memoria que se guarda EN <p>).
Por eso para hacer cambios o acciones sobre el array en sí, no es necesario pasarlo por referencia, porque ambos punteros apuntan al mismo sitio.


Sin embargo, si tenemos esta alternativa:
Código (cpp) [Seleccionar]

void reservar(int *&v, int size){
   v = new int [size];
}

Y queremos reservar memoria, esto es una operación que se debe aplicar, siguiendo con el mismo ejemplo, sobre 0x1 (dirección en la que hemos guardado <p>), no sobre 0x4 (dirección en la que hemos guardado <v>). Al pasar por referencia la tabla de direcciones de memoria anterior quedaría de la siguiente manera:

0x1(p) = 0x2
0x2(p[0]) = 1
0x3(p[1]) = 2
0x1(v) = 0x2 (no hemos copiado el valor de <p> en otra direccion de memoria sino que usamos la direccion de memoria de <p>
0x5(pos) = 1

Y entonces ahora sí que podemos aplicar la orden de reserva sobre <v> ya que en realidad la estamos aplicando sobre <p> (realmente se lo estamos aplicando a la dirección 0x1).

Entonces mi conclusión es que el paso por referencia es para operar sobre la dirección de memoria DÓNDE está el puntero, no para operar sobre la dirección de memoria que está EN el puntero. Por lo que supongo que a efectos de "eficiencia", es lo mismo hacer un paso por referencia que por valor (en ambos casos pasamos únicamente una dirección de memoria, en un caso la dirección dónde está el puntero y en el otro, la dirección que guarda el puntero)


Espero que le sirva a alguien. :-X
Código (cpp) [Seleccionar]

cout << "Todos tenemos un defecto, un error en nuestro código" << endl;

Loretz

Creo que no está del todo bien.

Pongo acá tu ejemplo añadiendo algunos textos que pueden ayudar:

Código (cpp) [Seleccionar]
#include <iostream>

void anular(int* v, int pos)
{
    std::cout << "la direccion de v es: " << &v << " pero apunta a " << v << " (igual que p)\n";
    v[pos] = 0;
}

int main()
{
    int* p; // sin inicializar. no es NULL
    // std::cout << p << '\n'; [[error -> no puede accederse a un puntero sin inicializar]]
    std::cout << "p es una variable que esta en algun lado, su direccion es: " << &p << '\n';

    p = nullptr; // se la asigna a p el valor cero (cero de puntero, nullptr no es la macro NULL, es de tipo nullptr_t).
    std::cout << "p apunta a: " << p << '\n';
   

    p = new int[2];
    std::cout << "Ahora p apunta a: " << p << '\n';
    std::cout << "pero su direccion sigue siendo: " << &p << '\n';

    p[0] = 1;
    p[1] = 2;

    anular(p, 1);

}


Yo lo diría así:
Un puntero es una variable como cualquier otra, en este caso, p es de tipo int*. Como toda variable está en algún lado, se puede tomar su dirección; además se le puede asignar un valor, otra dirección de memoria, aunque esa dirección de memoria que se asigna al puntero puede estar en el free store (memoria libre) o en la pila (stack), a p le da igual. Por ejemplo:int i=5; p = &i; // stack

En tu línea 6 tienes declarado el puntero p, pero sin inicializar, no apunta a NULL; probablemente apunte a algo pero no podemos saberlo, intentar acceder a qué apunta un puntero sin inicializar es un error que no debería siquiera compilar. Lo que sí puedes es leer su propia dirección, en dónde está. Entonces, en un puntero tenemos su propia dirección de memoria y la dirección de memoria a la que apunta.

Cuando pasas a p como parámetro de anular(), puedes ver que la dirección de v es diferente a la dirección de p, naturalmente, v está en el el stack frame de anular(), pero apunta a la misma dirección que apunta p. Puedes verlo ejecutando mi ejemplo.

Si observas los valores reales de las direcciones de memoria a donde apuntan p[0] y p[1] podrás ver que
1) p == &p[0];
2) &p[1] == &p[0] + sizeof(int*);

p y &p[0] podrían llegar a ser de tipos distintos, pero los dos guardan la misma dirección de memoria.

Como &p[0] y &p[1] son las direcciones consecutivas en memoria capaces de guardar sendos int(s), deben diferir en sizof(int*), ni más ni menos.

NextByte

#4
Cita de: Loretz en 31 Marzo 2019, 01:20 AM
Creo que no está del todo bien.

Pongo acá tu ejemplo añadiendo algunos textos que pueden ayudar:

Código (cpp) [Seleccionar]
#include <iostream>

void anular(int* v, int pos)
{
   std::cout << "la direccion de v es: " << &v << " pero apunta a " << v << " (igual que p)\n";
   v[pos] = 0;
}

int main()
{
   int* p; // sin inicializar. no es NULL
   // std::cout << p << '\n'; [[error -> no puede accederse a un puntero sin inicializar]]
   std::cout << "p es una variable que esta en algun lado, su direccion es: " << &p << '\n';

   p = nullptr; // se la asigna a p el valor cero (cero de puntero, nullptr no es la macro NULL, es de tipo nullptr_t).
   std::cout << "p apunta a: " << p << '\n';
   

   p = new int[2];
   std::cout << "Ahora p apunta a: " << p << '\n';
   std::cout << "pero su direccion sigue siendo: " << &p << '\n';

   p[0] = 1;
   p[1] = 2;

   anular(p, 1);

}


Yo lo diría así:
Un puntero es una variable como cualquier otra, en este caso, p es de tipo int*. Como toda variable está en algún lado, se puede tomar su dirección; además se le puede asignar un valor, otra dirección de memoria, aunque esa dirección de memoria que se asigna al puntero puede estar en el free store (memoria libre) o en la pila (stack), a p le da igual. Por ejemplo:int i=5; p = &i; // stack

En tu línea 6 tienes declarado el puntero p, pero sin inicializar, no apunta a NULL; probablemente apunte a algo pero no podemos saberlo, intentar acceder a qué apunta un puntero sin inicializar es un error que no debería siquiera compilar. Lo que sí puedes es leer su propia dirección, en dónde está. Entonces, en un puntero tenemos su propia dirección de memoria y la dirección de memoria a la que apunta.

Cuando pasas a p como parámetro de anular(), puedes ver que la dirección de v es diferente a la dirección de p, naturalmente, v está en el el stack frame de anular(), pero apunta a la misma dirección que apunta p. Puedes verlo ejecutando mi ejemplo.

Si observas los valores reales de las direcciones de memoria a donde apuntan p[0] y p[1] podrás ver que
1) p == &p[0];
2) &p[1] == &p[0] + sizeof(int*);

p y &p[0] podrían llegar a ser de tipos distintos, pero los dos guardan la misma dirección de memoria.

Como &p[0] y &p[1] son las direcciones consecutivas en memoria capaces de guardar sendos int(s), deben diferir en sizof(int*), ni más ni menos.



¿ Por qué se dice que la macro NULL no es recomendada en C++ ?, he visto varias fuentes que afirman eso pero no logro entender el por que. Sé que la macro 'NULL' viene desde C y que representa el numero entero 0 y que 'nullptr' fue agregado en C++11 pero ¿ qué problemas puede conllevar el usar la macro 'NULL'. ?

K-YreX

#5
Cita de: Loretz en 31 Marzo 2019, 01:20 AM
En tu línea 6 tienes declarado el puntero p, pero sin inicializar, no apunta a NULL
Tienes razón en esto. Fue fallo mío que pensé en asignarle NULL pero lo olvidé al estar pensando en el resto de la explicación :xD
Además no conocía la diferencia entre <NULL> y <nullptr> así que gracias también por ello.

Cita de: NextByte en 31 Marzo 2019, 05:01 AM
¿ Por qué se dice que la macro NULL no es recomendada en C++ ?, he visto varias fuentes que afirman eso pero no logro entender el por que. Sé que la macro 'NULL' viene desde C y que representa el numero entero 0 y que 'nullptr' fue agregado en C++11 pero ¿ qué problemas puede conllevar el usar la macro 'NULL'. ?
Por lo que he leído yo es porque <NULL> equivale a un 0 de tipo <int>; mientras que <nullptr> equivale a 0 también (si muestras el valor puedes verlo) siempre sigue siendo de tipo puntero, como dice @Loretz es de tipo <nullptr_t>. Entonces se evita el uso de <NULL> porque puede dar errores de ambigüedad al cambiar el tipo de dato.

Cita de: Loretz en 31 Marzo 2019, 01:20 AM
Como &p[0] y &p[1] son las direcciones consecutivas en memoria capaces de guardar sendos int(s), deben diferir en sizof(int*), ni más ni menos.
Es cierto que sus posiciones de memoria deben diferir en el tamaño del tipo de dato que puede almacenar. Pero como comenté, uso posiciones de memoria consecutivas y simples ya que lo que quería que se entendiese era ver de qué posiciones de memoria estamos hablando en cada momento.

Gracias por la respuesta ya que me diste la idea de probar el mismo programa para ver las posiciones de memoria que se utilizan. He añadido otra función para que se vea mejor el paso de un puntero por valor y por referencia.
Código (cpp) [Seleccionar]

int main(){
int *p = nullptr;
cout << "p esta en " << &p << " y su contenido es " << p << endl;
reservarMemoria(p,2);
p[0] = 1;
cout << "p[0] esta en " << &p[0] << " y su contenido es " << p[0] << endl;
p[1] = 2;
cout << "p[1] esta en " << &p[1] << " y su contenido es " << p[1] << endl;
anular(p,1);
// cout << "p[1] esta en " << &p[1] << " y su contenido es " << p[1] << endl;
}

void reservarMemoria(int *&v, int size){
cout << "FUNCION RESERVAR MEMORIA (puntero por referencia)" << endl;
cout << "v esta en " << &v << " y su contenido es " << v << endl;
v = new int [size];
}

void anular(int *v, int pos){
cout << "FUNCION ANULAR (puntero por valor)" << endl;
cout << "v esta en " << &v << " y su contenido es " << v << endl;
v[pos] = 0;
}


Al ejecutarlo obtenemos la siguiente salida (las posiciones de memoria depende de cada ejecución) donde podemos observar que coinciden los valores que comenté en mi anteior explicación:

p esta en 0x7fff4ce5ed50 y su contenido es 0
FUNCION RESERVAR MEMORIA (puntero por referencia)
v esta en 0x7fff4ce5ed50 y su contenido es 0
p[0] esta en 0x5650660fe280 y su contenido es 1
p[1] esta en 0x5650660fe284 y su contenido es 2
FUNCION ANULAR (puntero por valor)
v esta en 0x7fff4ce5ed38 y su contenido es 0x5650660fe280

En la función <reservarMemoria()> como se pasa el puntero por referencia se ve que la dirección de memoria de <v> (&v) es la misma que la de <p> (&p). Sin embargo en la función <anular()> como se pasa el puntero por valor, la dirección de memoria de <v> (&v) es distinta  a la de <p> (&p) aunque el valor que guardan que es la dirección de memoria de <p[0]> (&p[0]) en ambos casos, sí es la misma.
Código (cpp) [Seleccionar]

cout << "Todos tenemos un defecto, un error en nuestro código" << endl;

Loretz

Citar¿ Por qué se dice que la macro NULL no es recomendada en C++ ?

Acá va un ejemplo donde se invocan distintas funciones sobrecargadas:

#include <iostream>

void fun(int i)
{
    std::cout << "int 0\n";
}

void fun(int* p)
{
    std::cout << "puntero nulo\n";
}

int main()
{
    fun(0);       // no hay duda de cuál de las dos se invoca.
    fun(nullptr); // tampoco hay duda
    fun(NULL);    // ¿y ahora?
}



Además la constante nullptr (de tipo nullptr_t):

- Puede convertirse a cualquier tipo de puntero o puntero a miembro y a bool, pero a nada más.
- No puede ser usada en expresiones aritméticas.
- Puede comparase con el int 0 [[ como en if(p == 0) ]]
- Puede usarse en expresiones relacionales (expresiones que se evalúan a true o false) para compararse con punteros (u otro dato de tipo nullptr_t).

De todos modos, por una cuestión de compatibilidad, aún en C++17 es aceptable asignar 0 (o NULL) a un puntero, como en
char* pch = 0;