2

No entiendo como manejar un array con doble puntero (no es una matriz, es un arreglo lineal). Me hice algunas pruebas con el siguiente código:

 int *num = new int;

 int** algo = #//acá se pasa por referencia (no por copia), el puntero apuntará a la dirección de num
 int k = 6;
 int p = 5;

 algo[7] = &k; //el arreglo en esa posición (que no inicialicé ¿?) toma la dirección de k
 algo[500] = &p;

 cout << *algo[500]; //se imprime los valores de esos arrays en esas posiciónes, que nunca inicialicé
 cout << *algo[7];

El código funciona y a pesar de que lo hice yo, no lo entiendo bien, ya que hasta ahora le puse un tamaño fijo a los arrays y al momento de inicializarlos me surgen las siguientes preguntas:

  • ¿realmente quedó inicializado?
  • ¿Está bien pensado esto o puede dar problemas?
  • ¿Cómo puedo saber el tamaño del arreglo?

Esto forma parte de un ejercicio que me mandaron a hacer en la facultad.

Eduardo Javier Maldonado
  • 2,455
  • 7
  • 20
  • 41
Mathias
  • 69
  • 9

2 Answers2

5

Formaciones en C++.

Tanto C como C++1 no realizan comprobaciones de límites sobre las formaciones2, en el caso de C y C++1 no se hacen estas comprobaciones para favorecer el rendimiento del código.

En C++ existe el tipo de datos formación, que imbuye en el tipo el tamaño de la formación, pero sólo si se conoce dicho tamaño en tiempo de compilación.

using cien_int = int[100];

cien_int a; // Tanto 'a' como 'b' tienen el mismo tipo...
int b[100]; // ... el tipo es 'formación de 100 int'.

Pero el tipo formación de decae en puntero al tipo subyacente de la formación al ser usados en varios contextos, por ejemplo:

template <typename T>
void f(T) { ... }

using cien_int = int[100];

cien_int a; // Tanto 'a' como 'b' tienen el mismo tipo...
int b[100]; // ... el tipo es 'formación de 100 int'.

f(a);
f(b);

En el código anterior, en ambos casos el tipo T de la función plantilla f se deduce como int * en lugar de como int[100]. Esto es importante porque nos demuestra que podemos recibir un puntero a una formación con memoria reservada pero habiendo perdido información del tamaño asociado a la formación.

Una vez introducidos los conceptos, vamos a abordar las preguntas.

¿Realmente quedó inicializado?

Todas estas instrucciones son inicializaciones:

int *num = new int; // Reserva un 'int' en memoria dinámica y lo almacena en 'num'.
int** algo = &num;  // Almacena la dirección de meoria de 'num' en 'algo'.
int k = 6;          // Almacena 6 en 'k'.
int p = 5;          // Almacena 5 en 'p'.

Estas líneas no son inicializaciones, son asignaciones:

algo[7] = &k;   // Asigna la dirección de memoria de 'k' en la octava posición de 'algo'.
algo[500] = &p; // Asigna la dirección de memoria de 'p' en la quingentésima primera posición de 'algo'

El problema de las asignaciones anteriores es que acceden a memoria que no pertenece al programa:

Dado que no se hacen comprobaciones de límites, es posible acceder y escribir en esas zonas de memoria, incluso aunque no se hayan asignado a nuestro programa. Esto podría derivar en una violación de acceso, o en un comportamiento errático del programa3.

¿Está bien pensado esto o puede dar problemas?

Es difícil saber si está bien pensado con el poco código que compartes, pero sin duda, como se ha comentado en el punto anterior, puede dar problemas... problemas serios.

¿Cómo puedo saber el tamaño del arreglo?

No puedes. Los punteros no almacenan información de tamaño; pero tienes alternativas:

  • Marca de final: Puedes reservar una posición adicional en la que guardarías un valor que marca el final de la colección de datos.

    int **algo = new int*[501];
    algo[500] = 0xf10a1;
    
  • Variable adicional: En el momento de reserva de memoria, guardando en una variable adicional el tamaño.

    int tamanyo = 500;
    int **algo = new int*[tamanyo];
    
  • La stl: Usando un contenedor de la librería estándar de plantillas se puede tener control sobre el tamaño y la gestión de memoria, lee este hilo para averiguar cuál se ajusta a tus necesidades.

  1. Y otros lenguajes.
  2. También conocidas como arreglos (o en inglés arrays).
  3. Por ejemplo, la invocación de infraseres de la novena esfera del infierno, que saldrían disparados por tus fosas nasales.
PaperBirdMaster
  • 44,474
  • 6
  • 44
  • 82
3

Tienes razón en todo lo que dices y en todos tus comentarios.

int *num = new int;

Eso, ciertamente, crea un int. Ni mas ni menos que 1 y solo 1 int.

Así pués, la gran pregunta es ... ¿ porqué funciona ?

Esto se va a extender un poco. A ver si con una imagen (sacada de aquí) ...

introducir la descripción de la imagen aquí

Cada vez que realizas llamadas a funciones, los argumentos se van colocando en la pila ( stack ). La tienes en la parte superior de la imagen.

Los datos creados con new van ocupando memoria en la zona marcada como heap.

Pero toda ella, desde la parte marcada como .text hasta el último byte del stack pertenece a tu programa. El sistema controla ciertas cosas, como por ejemplo las escrituras en zonas de solo-lectura. Pero, por lo demás, es tuya.

Eso quiere decir que, en realidad, puedes leer y escribir en toda tu memoria; otra cosa es que lo que te encuentres tenga valores correctos.

El mecanismo de reserva dinámica de memoria lo que hace es organizar unas estructuras internas, de forma que no pisotees tus propios datos; te garantiza que usaras direcciones de memoria distintas para cosas distintas.

La inicialización, por otra parte, es el mecanismo por el que asignas valores iniciales conocidos a variables.

Pero todo eso son cosas extra, facilidades que obtenemos del sistema, de las librerías, ... el acceso a la memoria raw, saltándonos cualquier organización lógica nuestra, está siempre permitido.

Por eso tu código funciona. Lees y escribes en tu memoria; que pisotees estructuras de código (tuyas o de las librerias), que leas valores inicializados o no ... eso es problema del programador.

Eso es la gran ventaja y el gran problema de C y C++. Te permiten utilizar por completo la memoria de tu proceso. Puedes optimizar tu código, de ser necesario, para situaciones concretas, saltándote las funciones de las librerias para gestionar la memoria como tu quieras.

Y, por eso mismo, si no tienes cuidado, puedes corromper la memoria, escribiendo valores donde no debes, o leer cosas que no se han escrito correctamente.

Pero, en general, acceder a posiciones de memoria aleatorias DENTRO de tu programa no es un error en si mismo; de hecho, es perfectamente válido ( tu código lo prueba ).

Otra cosa muy distinta es que sepamos lo que estamos haciendo ...

Trauma
  • 25,297
  • 4
  • 37
  • 60
  • Huy. Creo que me he desviado de la pregunta orginal. Que me he emocionado ... ahora cuando pueda lo arreglo ... – Trauma Mar 23 '18 at 05:18
  • No tiene razón "en todo lo que dice en los comentarios". En la segunda instrucción de su código de ejemplo ya dice algo incorrecto. No hay ni copias ni referencias implicadas en esa línea. – PaperBirdMaster Mar 23 '18 at 07:11
  • 3
    @Paula_plus_plus Mira que llegas a ser ... *puntillosa* O_O – Trauma Mar 23 '18 at 07:14