Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.2.1c Puntero a puntero

"I’ve come to realize that understanding pointers in C is not a skill, it’s an aptitude. In first year computer science classes, there are always about 200 kids at the beginning of the semester, all of whom wrote complex adventure games in BASIC for their PCs when they were 4 years old. They are having a good ol’ time learning C or Pascal in college, until one day they professor introduces pointers, and suddenly, they don’t get it. They just don’t understand anything any more. 90% of the class goes off and becomes Political Science majors, then they tell their friends that there weren’t enough good looking members of the appropriate sex in their CompSci classes, that’s why they switched. For some reason most people seem to be born without the part of the brain that understands pointers. Pointers require a complex form of doubly-indirected thinking that some people just can’t do, and it’s pretty crucial to good programming. A lot of the “script jocks” who started programming by copying JavaScript snippets into their web pages and went on to learn Perl never learned about pointers, and they can never quite produce code of the quality you need".  Joel Spolsky. "The Guerrilla Guide to Interviewing (version 3.0)"    www.joelonsoftware.com/ 

§1 Sinopsis

Al presentar los punteros a objeto ( 4.2.1) se señaló que, puesto que los punteros también son objetos y en consecuencia, pueden existir punteros que señalen a otros punteros (así sucesivamente). El lenguaje C++ permite que se puedan declarar punteros con múltiples niveles de indirección mediante un número adecuado de asteriscos. Por ejemplo:

char letra = 'A';

char* cPtr = &letra;             // define puntero-a-carácter

char** cPtr_bis = &cPtr;         // define puntero-a-puntero-a-char

cPtr_bis* cPtr_bix = &cPtr_bis;  // Error!! cPtr_bis no es un tipo definido

cout << "Letra " << *cPtr;       // -> Letra A

cout << "Letra " << **cPtr_bis;  // ->  Letra A


Naturalmente, las expresiones ** solo tienen sentido para apuntar a un puntero, no sirven a ningún otro tipo. Ejemplo:

char letra = 'A';
char** cPtr = &letra;    // Error. letra NO es un puntero-a-char

§2 Ejemplo

Supongamos que en un programa nos interesa presentar el estado de un proceso, caben tres posibilidades: "Bien"; "Regular" y "Mal".

char* aP[] = {"Bien", "Regular", "Mal"};    // aP matriz de punteros a char
char* (*aPp) [] = &aP;           // aPp puntero a matriz de punteros-a-char


Nótese que en la primera sentencia, el compilador crea tres cadenas de caracteres: "Bien/0", "Regular/0" y "Mal/0"; las almacena en tres posiciones de memoria (no necesariamente consecutivas 3.2.3f); crea una matriz de tres punteros-a-carácter (cuyo nemónico es aP), y rellena los tres elementos (consecutivos) de esta matriz, con las direcciones de los primeros caracteres ('B', 'R' y 'M') de las tres cadenas. Hemos señalado que en un compilador de 32 bis el tamaño de un puntero es de 4 bytes ( 4.2), por lo que el tamaño de aP es 3 x 4 = 12 bytes. Nótese también que &aP es lo mismo que &aP[0], por lo que en principio, aPp apunta al primer elemento de la matriz aP.

Las diversas posibilidades pueden ser expresadas como elementos de la matriz aP. Si estos elementos los expresamos mediante notación de subíndices [0] se tiene:

cout << aP[i] << endl;    // i == 0; 1; 2


§2.1  Lo anterior puede ser expresado en forma de ejecutable:

#include <iostream>
using namespace std;

int main() {              // =================
  char* aP[] = {"Bien", "Regular", "Mal"};
  char* (*aPp) [] = &aP;
  int n = sizeof(aP) / sizeof(aP[0]);  // tamaño de la matriz aP
  for(int i=0; i<n; i++) {
    cout << aP[i] << endl;    // M.5
  }
}

Salida:

Bien
Regular
Mal

En este ejemplo, el primer elemento ("Bien") de la matriz aP se ha referenciado mediante el operador de elemento de matriz en la forma aP[0], pero dicho elemento también puede ser referenciado mediante una doble indirección del puntero aPp:

cout << **aPp << endl;        // -> Bien

Sin embargo, el intento de referenciar los otros dos elementos ("Regular" y "Mal") en función del referido puntero conlleva una serie de dificultades teóricas cuya solución es del mayor interés para comprender el mecanismo de punteros C++ y su álgebra.


§2.2 Consideremos un primer intento de mimetizar el ejemplo anterior sustituyendo la expresión aP[i] de M.5 , por una expresión que contenga aPp:

#include <iostream>
using namespace std;

void main() {                // =================
  char* aP[] = {"Bien", "Regular", "Mal"};  // M.1
  char* (*aPp) [] = &aP;     // M.2
  int n = sizeof(aP) / sizeof(aP[0]);
  for(int i=0; i<n; i++) {
    cout << **aPp << endl;   // M.5
    aPp++;                   // M.6
  }
}

El primer problema que se presenta al intentar compilar este módulo, es que se obtiene un error en M.6: Size of the type 'char *[]' is unknown or zero.... La razón es que en la definición del puntero aPp en M.2, se ha dejado abierta la dimensión del objeto señalado, y la aritmética de punteros implícita en M.6 ( 4.2.2), exige que el compilador conozca el tamaño del objeto señalado. En este caso, el objeto señalado por aPp es una matriz de 3 punteros-a-char. El problema se resuelve sustituyendo la sentencia M.2 por una que indique el tamaño:

char* (*aPp) [3] = &aP;    // M.2bis


Observe que el tamaño de aP también se ha dejado abierto en M.1; sin embargo, el compilador puede deducir su dimensión de la propia definición. Desde luego podría argumentarse que en M.2 el compilador también puede conocer por la definición el tamaño del objeto señalado, lo que es cierto; sin embargo la solución M.2bis es necesaria sencillamente por que en el estado actual, los compiladores utilizados [2] no son suficientemente "inteligentes".

Con M.2bis el programa compila sin dificultad, pero solo la primera salida es correcta. Las dos siguientes contienen basura [1]. La razón es que después de la primera salida, la sentencia M.6 suma una unidad al valor actual del puntero, que señala al primer elemento (aP[0]) de la matriz aP, que señala a su vez al primer carácter de la cadena "Bien".

Sumarle una unidad implica desplazar el puntero el tamaño correspondiente al objeto señalado aP; en este caso una matriz de tres punteros a carácter, cuyo tamaño es 4 bytes [3]. El resultado es que el puntero no señala ahora al elemento aP[1] como pretendíamos, ya que este elemento de la matriz está separado solo 4 bytes del anterior -los punteros-a-char (como el resto) ocupan solo 4 bytes ( 4.2)-.


§2.3 La versión modificada que sigue funciona correctamente, proporcionando las salidas esperadas, pero se han utilizado algunos artificios para conseguir expresar las salidas en función del puntero-a-puntero aPp.

#include <iostream>
using namespace std;

void main() {                 // =============
  char* aP[] = {"Bien", "Regular", "Mal"};
  char* (*aPp) [] = &aP;      // M.2
  int n = sizeof(aP) / sizeof(aP[0]);
  int dir;
  for(int i=0; i<n; i++) {
    cout << **aPp << endl;
    dir = reinterpret_cast<int> (aPp);           // M.6bis
    aPp = reinterpret_cast<char* (*)[]> (dir+4); // M.7
  }
}

Comentario:

En esta versión, la línea M.6 del ejemplo anterior , se ha sustituido por M.6bis y M.7.

Mediante un "casting" ( 4.9.9d) adecuado, en M.6 asignamos el valor del puntero aPp a un entero dir, que almacena la misma dirección pero como un valor int. De esta forma, en M.7 podemos sumarle el desplazamiento adecuado, y (mediante el modelado inverso), volverlo a asignar al puntero aPp, con lo que efectivamente, en cada iteración del bucle señala sucesivamente a los miembros aP[0]; aP[1] y aP[2]; de forma que la doble indirección **aPp proporciona en cada caso la salida adecuada.

Observe la expresión utilizada en M.7, para transformar un entero en un puntero a matriz de punteros a carácter. Y como no es necesario en M.2 conocer el tamaño del objeto señalado por aPp, ya que en este último ejemplo no existe ninguna expresión que implique álgebra de punteros.

  Inicio.


[0] La notación de subíndices del tipo ap[i], implica la utilización del operador [ ] de elemento de matriz ( 4.9.16).

[1]  El resultado concreto depende de las circunstancias. La segunda y tercera salida también pueden producir un error fatal de "runtime". Por ejemplo, el consabido mensaje Windows: El programa ha realizado una operación no válida y será interrumpido... o un volcado de memoria si es Linux.

[2] Borland Cpp 5.5 y MS Visual C++ 6.0.

[3] El tamaño depende de la plataforma; por supuesto es el suficiente para almacenar una dirección de memoria en la arquitectura de la computadora empleada, pero con los compiladores que venimos utilizando en esta obra [2], es el señalado (32 bits).