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.9.11  Operadores de puntero

§1  Sinopsis

Recordemos que un puntero p ( 4.2) es una variable cuyo Rvalue es justamente la dirección en memoria de un objeto x. Es decir:

Rvalue(p) == Lvalue(x)    ( 2.1)

Además de los operadores aritméticos ( 4.9.1), de los que existen versiones para los tipos numéricos y para los punteros, C++ dispone de dos operadores específicos para estos últimos (punteros de cualquier tipo): la indirección * y la referencia & . Además existen dos operadores específicos para punteros-a-clases .

§2  Operador de indirección *

Este operador unitario "deferencia" el operando (que debe ser un puntero). Es decir, devuelve el valor del objeto señalado por el operando [1].

Nota: recordemos que el símbolo  * tiene tres usos en C++: operador de multiplicación ( 4.9.1), indicador de tipo de variable (tipo puntero 4.2.1a) y operador de indirección, que es el que aquí nos ocupa:

§2.1  Sintaxis

* cast-expresion

  El operando cast-expresion debe ser una expresión que se resuelva a un puntero-a-objeto, o a-función.

Cuando se usa como operador de indirección se espera que el operando sea una variable de tipo puntero de cualquier tipo (a objeto, o a función). Si el operando es un puntero-a-función, el resultado es un designador de función [2] que puede ser utilizado para invocación de la misma ( 4.2.4b).

  Si ptr es un puntero-a-X, entonces se cumple que *ptr es el objeto apuntado por ptr y su tipo es el de X. Es decir ( 4.9.14):

typeid(*ptr) == typeid(X)

§2.2  Comentario

La expresión n = *ptr; asigna a la variable n el valor del objeto apuntado por ptr y a la inversa: la expresión *ptr = n; asigna al objeto apuntado por ptr el valor de la variable n.

Si pt1 y pt2 son dos punteros a las variables v1 y v2, la expresión *pt2 = *pt1; asignaría a la variable apuntada por pt2 el valor de la variable apuntada por pt1, es decir, equivale a v2 = v1;.

  Recuerde que los punteros-a-void no pueden ser deferenciados sin un modelado explícito. Ver ejemplo ( 4.2.1d).


§2.3  Considere el siguiente ejemplo y suponga que las diversas instrucciones que se comentan constituyen una secuencia (un trozo de programa).

int y = 100;

Declaración de tipo + iniciación:

Declara e inicia y como variable tipo int; guarda en la dirección de y el valor 100

implica: Lvalue(y) ← 100, en consecuencia Rvalue(y) == 100

(en lo que sigue suponemos que Lvalue(y) == 150 )

int z;

Declaración de tipo:

Declara z como variable tipo int

int* ptr;

Declaración de tipo:

Declara ptr como puntero-a-entero (int*)

ptr = &y;

Iniciación:

Guarda en la dirección de ptr la dirección (Lvalue) de y. A partir de este momento, ptr apunta a y.

Implica: Lvalue(ptr) ← Lvalue(y), en consecuencia, Rvalue(ptr) == 150

z = *ptr;

Iniciación:

Guarda en la dirección de z el valor apuntado por ptr, Rvalue(y)

Implica: Lvalue(z) ← Rvalue(*ptr)

equivale a: Lvalue(z) 100, en consecuencia Rvalue(z) == 100, y z == y

Las expresiones z = *ptr y z = y son equivalentes (ptr apunta a y)

*ptr = 0;

Asignación:

Guardar 0 en la dirección (Lvalue) de *ptr.

equivale a: Lvalue(*ptr) ← 0, en consecuencia, Rvalue(y) == 0


§2.4
  El resultado de la operación de indirección sobre un puntero es indefinido si se da alguno de los casos siguientes (aquí "indefinido" significa que se obtiene basura o incluso un error irrecuperable):

  • El operando cast-expresion es un puntero nulo ( 4.2.1). En este caso, lo más probable es que se obtenga un error irrecuperable, como ocurre por ejemplo, con los compiladores Borland C++ 5.5 y MS Visual C++ 6.0 en la situación siguiente:

    int x;

    int* ptr = 0;

    x = *ptr;       // Error!!

  • El operando cast-expresion es un puntero a una variable automática y su bloque ha terminado la ejecución ( 4.1.8a). Ejemplo:

    int* iptr;

    ...

       {

         int x = 0;

         iptr = &x

         ...

       }

    ...

    int z = *iptr;   // Error!!

  • El operando cast-expresion es un puntero a un elemento después del último de una matriz ( 4.2.2). Ejemplo:

    char* nombre = "Bond";
    cout << "Letra: " << *nombre << endl;       // Ok.
    cout << "Letra: " << *(nombre+5) << endl;   // Error!!

§2.5  La indirección múltiple

Se ha señalado que los punteros pueden a su vez señalar a punteros ( 4.2.1). En estos casos, el valor del objeto señalado en el extremo de la cadena de punteros debe obtenerse mediante varias indirecciones.

Considere el siguiente ejemplo:

int x = 10;            // entero

int* iptr = &x;        // puntero a entero

int** pptr = &iptr     // puntero a puntero a entero

cout << "Valor x = " << *iptr << endl;

cout << "Valor x = " << **pptr << endl;

Salida:

Valor x = 10
Valor x = 10

§2.6  La indirección con punteros a función

Hay que resaltar que la indirección de punteros a funciones no se utiliza para obtener la dirección de comienzo del código ( 4.2.4) sino para invocar la función señalada por el puntero y que esta invocación tiene una sintaxis un tanto especial. En efecto, sea el código:

void somefunc (...);            // una función con sus parámetros ...

void (*fptr)(...) = &somefunc;  // fptr es puntero a somefunc


En este caso, según la definición de puntero, ocurre que: *fptr == somefunc (ambas expresiones son equivalentes), por lo que en la invocación de la función podemos sustituir el nombre por *fptr, lo que nos conduciría a la notación: *fptr( ...); como fórmula de invocación. Sin embargo, esta expresión debe ser utilizada con paréntesis: (*fptr)( ...);. Se tiene así que las dos sentencias que siguen son igualmente válidas para invocar a la función ( 4.2.4b), pero observe que la segunda tiene una sintaxis especial:

somefunc(...);    // L.1  Ok invocación tradicional

(*fptr)(...);     // L.2  Ok invocación a través de puntero

*fptr(...);       // L.3  Error de sintaxis!!

Nota: la razón de la necesidad de paréntesis en L.2, es que el operador ( ) de invocación de función tiene precedencia más alta que el de indirección *, por lo que L.3 equivaldría a la indirección del valor devuelto por fptr(...).

  Temas relacionados
  • Ver en 4.2.4a la forma correcta de definir un puntero a función.
  • Sobrecarga del operador de indirección ( 4.9.18c).
§3  La indirección de punteros a clases y a miembros

En la programación C/C++ los punteros y sus operaciones constituyen parte fundamental del lenguaje, por lo que es muy frecuente el uso del operador de indirección * para acceder a entidades señaladas por punteros, y desde luego, este operador (herencia del C clásico), cubre suficientemente las necesidades de acceso a través de punteros de cualquier tipo. Sin embargo C++ va un paso más allá cuando se trata de punteros a clases o a sus miembros (también a estructuras y uniones) y ofrece dos operadores adicionales para estos casos [4].

Para situar al lector en la cuestión, antes de seguir adelante, ilustraremos un sencillo ejemplo de acceso a instancias de clase y a sus miembros mediante la indirección estándar de punteros.

#include <iostream>
using namespace std;
class C {
  public:
  int x;
  int C::* mpint;         // L.6 miembro puntero-a-miembro-int
  C (int n = 0) {         // constructor por defecto
    x = n;
    mpint = &C::x;
  }
};
int (C::* pmint) = &C::x; // L.13 puntero-a-miembro-int

int main (void) {         // ========================
  C c1(10);               // M.1 Instancia
  C* pc = &c1;            // M.2 puntero-a-instancia de C
  cout << "c1.x == " << c1.x           << endl;  // M.3
  cout << "c1.x == " << c1.*pmint      << endl;  // M.4
  cout << "c1.x == " << c1.*(c1.mpint) << endl;  // M.5
  return 0;
}

Salida:

c1.x == 10
c1.x == 10
c1.x == 10

Comentario

El programa ilustra un caso muy sencillo de una clase en la que uno de sus miembros recibe dos punteros; uno de ellos es miembro de la propia clase, el otro es exterior a la misma.

El cuerpo de main instancia un objeto c1 de la clase y define un puntero pc al mismo. A continuación se obtiene el miembro x de tres formas diferentes (sentencias M.3/4/5):

En la primera forma, el valor del miembro se obtiene mediante la utilización directa de un operador . denominado selector directo de miembro ( 4.9.16). La segunda y tercera muestran la forma correcta de utilizar el operador de indirección * sobre los punteros-a-miembro según que el puntero sea o no miembro de la clase.

Nota: existe una completa descripción de estos punteros en "Punteros a clases" ( 4.2.1f) y "Punteros a miembros" ( 4.2.1g).

§3.1  Operador de indirección de punteros-a-miembro  .*

Es importante señalar que los símbolos  .* en M.4 y M.5 del ejemplo anterior no son considerados por el lenguaje como una simple yuxtaposición de los operadores . y *. Por contra, representan un operador único denominado indirección de punteros-a-miembros que dispone de su propia precedencia y asociatividad ( 4.9.0a).

Nota: advierta que las expresiones c.*m y c.(*m) pueden no ser equivalentes [3].  Precisamente el hecho de que la indirección de puntero-a-miembro .* sea un operador único, de precedencia menor que el selector directo ., hace que el término c1.*(c1.mpint) de la sentencia M.5 sea equivalente a c1.*c1.mpint.


El operador .* de indirección de puntero-a-miembro, es un operador binario cuyo resultado es un objeto-valor (indirección de un puntero-a-propiedad) o un objeto-algoritmo (indirección de un puntero-a-método). Su sintaxis es la siguiente:

<objeto> .* <puntero-a-miembro>

  <objeto> es la estructura de datos (objeto) en la que se aplicará el desplazamiento ("offset") señalado por el puntero.

  <puntero-a-miembro> es el puntero que se deferencia


Para que la indirección funcione correctamente, objeto debe ser la instancia de una clase C, mientras que puntero-a-miembro debe ser del tipo X C::*, siendo X el tipo de um miembro de C. Además, el objeto deberá ser accesible desde el puntero. Si el resultado de la indirección es una función (método), solo podrá ser utilizado como operando con el operador de invocación de función ( ) ( 4.9.16).

Señalar que el puntero-a-miembro representado por el operador derecho no tiene porqué ser un miembro del objeto señalado por el operador izquierdo (de hecho puede no ser un miembro de clase). En otras palabras, no tiene porqué existir ninguna conexión entre las entidades representadas por ambos operadores [5]. Lo ilustraremos con un ejemplo:

#include <iostream>
using namespace std; 

class A {
  public:  int x;
  A (int n = 0) { x = n; }  // constructor por defecto
};
class B {
  public:
  int x;
  int A::* iAptr;           // puntero-a-miembro
  int B::* iBptr;           // puntero-a-miembro
  B (int n = 0) {           // constructor por defecto
    x = n;
    iAptr = &A::x;
    iBptr = &B::x;
  }
};
int A::* iAptr = &A::x;     // puntero-a-miembro

int main (void) {           // =================
  A a1(20), a2(22);
  B b1(10), b2(11);
  cout << "S1- " << a1.*(b1.iAptr) << endl;
  cout << "S2- " << a2.*(b1.iAptr) << endl;
// cout << "S3- " << a1.*(b1.iBptr) << endl;  Error!!
  cout << "S4- " << b1.*(b1.iBptr) << endl;
  cout << "S5- " << b2.*(b1.iBptr) << endl;
  cout << "S6- " << a1.*iAptr << endl;
  return 0;
}

Salida:

S1- 20
S2- 22
S4- 10
S5- 11
S6- 20

Comentario

El programa define tres punteros-a-miembro; dos de ellos son a su vez miembros de clase. El tercero es una variable del espacio global. Uno de los punteros-miembro (iBptr) señala a un miembro de la propia clase, mientras que el otro (iAptr) señala un objeto de otra clase.

La función main utiliza seis expresiones con el operador de indirección de punteros-a-miembro. En todas ellas el resultado es el miembro x de un objeto.

En la salida S1 el operador derecho b1.iAptr (miembro del objeto b1) es deferenciado y el "offset" obtenido se aplica sobre el objeto a1 señalado por el operador izquierdo. En S2 el mismo "offset" es aplicado sobre un objeto distinto, con lo que se obtiene un resultado también distinto, aunque el puntero deferenciado ( b1.iAptr ) es el mismo en S1 y S2. Observe que aunque en S2 el operando derecho es miembro de clase, no pertenece al objeto a2 señalado por el operando izquierdo.

La tercera salida se ha dejado como testimonio. El compilador produce un error (muy significativo por cierto en el caso de MS VC++):

'.*' : cannot dereference a 'int B::*' on a 'class A'...

Se refiere a que el puntero b1.iBptr que se pretende deferenciar, cuyo tipo es int B::*, no puede aplicarse al subespacio A:: al que pertenece el operador izquierdo a1.

Las salidas 4 y 5 son análogas a S1/S2; aquí el puntero deferenciado b1.iBptr  (que es miembro de b1), es aplicado sobre el mismo objeto b1 (S4) y sobre un objeto distinto b2 (S5) aunque de la misma clase.

En la S6 el operando derecho es desde luego un puntero-a-miembro, aunque aquí no es un miembro de clase, sino un objeto del espacio global (::iAptr); el resultado se aplica sobre el objeto a1 obteniéndose el valor esperado. Observe que esta última indirección es semánticamente equivalente a: a1.*::iAptr.

§3.2  Operador de indirección de puntero-a-clase & puntero-a-miembro  ->*

Aprovechando que pc es un puntero al objeto c1, y que *pc == c1, las expresiones M3/4/5 del ejemplo anterior , pueden ser sustituidas por:

cout << "c1.x == " << (*pc).x              << endl;   // M.3a
cout << "c1.x == " << (*pc).*pmint         << endl;   // M.4a
cout << "c1.x == " << (*pc).*((*pc).mpint) << endl;   // M.5a


Estas expresiones son desde luego correctas y proporcionan los mismos resultados que las originales. Sin embargo, C++ dispone de un operador específico, denominado selector indirecto -> ( 4.9.16d) que permite acceder a un miembro m de instancia c a través de un puntero pi a la instancia, de forma que:

(*pi).m  ==  pi->m

Aplicando esta equivalencia a las expresiones anteriores, se obtienen las siguientes:

cout << "c1.x == " << pc->x            << endl;     // M.3b
cout << "c1.x == " << pc->*pmint       << endl;     // M.4b
cout << "c1.x == " << pc->*(pc->mpint) << endl;     // M.5b


Resulta así que los grupos de sentencias a y b son equivalentes, aunque la sintaxis del grupo a (operador de indirección & puntero & selector directo de miembro) es desaconsejada en favor de utilizar la notación b (puntero & selector indirecto). Es decir:

C puntero-a-objeto = new C;
...
(*puntero-a-objeto).*puntero-a-miembro;  // obtiene c.miembro (válido aunque desaconsejado)
puntero-a-objeto->*puntero-a-miembro;    // obtiene c.miembro (preferible)


Es pertinente hacer aquí una observación paralela a la reseñada en el epígrafe anterior : en las expresiones M.3b/4b/5b, la combinación de símbolos ->* no son considerados en C++ como una simple yuxtaposición del selector indirecto -> y el de indirección * . Representan un operador único denominado indirección de puntero-a-clase & puntero-a-miembro, que dispone de su propia precedencia y asociatividad ( 4.9.0a). En consecuencia, tenga también presente que las expresiones c->*m y c->(*m) no son necesariamente equivalentes.

Observe que también aquí puede efectuarse una simplificación en la sentencia M.5b, paralela a la efectuada en el epígrafe anterior con M.5. El hecho de que ->* sea un operador único, de precedencia menor que el selector indirecto ->, hace que el término pc->*(pc->mpint) de M.5b sea equivalente a pc->*pc->mpint.

Ver un resumen completo de las sustituciones enunciadas a lo largo del ejemplo y de las simplificaciones admitidas por la sintaxis ( Ejemplo)

  Tenga en cuenta que obtener la dirección de propiedades estáticas de clases exige cierta condición especial ( 4.2.1g).

§4  Operador de referencia &

Este operador unitario es complementario del de indirección. Cuando se aplica a un objeto devuelve la dirección de almacenamiento del objeto (valor que puede ser asignado a un puntero).

Recordemos que en C++ el símbolo & se utiliza también como declarador de referencia ( 4.2.3); casi siempre para pasar argumentos a funciones.  Véase al respecto la discusión sobre las diversas formas de paso de parámetros a funciones ( 4.4.5b Argumentos por valor y por referencia). Como puede ver, existen declarador y operador "de referencia".  No confundir ambos conceptos!!

§4.1  Sintaxis

& cast-expresion

  El operando cast-expresion debe ser alguno de los siguientes:

§a  Un identificador cualificado ( 4.1.11c). Ejemplo:

class A {....};

int A::iptr = &A::miembro_x;

§b  El designador de una función.  Ejemplo:

void func() { /* ... */ };  // funcion

void (*fptr)() = &func;     // Ok. puntero-a-funcion

§c  Un Lvalue designando un objeto X que no sea un campo de bits ni tenga un especificador de almacenamiento tipo registro. Recuerde que no es posible obtener la dirección de una variable de registro ( 4.1.8b). Es decir, no se le puede aplicar le operador de referencia a una de estas variables. Tampoco se puede aplicar al valor devuelto por una función, en consecuencia, la expresión x = &func(x); es incorrecta, ya que el valor devuelto por func() es del tipo registro (está en la pila que no es direccionable por este método). Regla: el operador de referencia no puede ser aplicado al valor devuelto por una función ( 4.4.7).

Observe que existe una sutil diferencia entre los enunciados de estas dos últimas condiciones. Según §b debe ser posible una expresión del tipo:

ptr = &func;           // Ok! (aunque innecesario 4.2.4a)

aunque §c anuncia que no es posible algo como:

int* p = &func(...);   // Error!

Por supuesto, no existe contradicción entre ambas;  §b indica que puede igualarse un puntero a función (del tipo adecuado) con la dirección de esta, mientras §c indica que no puede asignarse el valor devuelto por una función a un puntero (no importa el tipo).

Según la regla §c, tampoco es posible

int* iptr = &(4+3);    // Error!! 4+3 no es un Lvalue


§4.2  Si cast-expresion es un objeto tipoX, el resultado puede ser asignado a un puntero-a-tipoX. Es decir, si TipoX es instancia-de-x, y TipoX* es puntero-a-tipoX. Puede establecerse:

puntero-a-tipoX == &instancia-de-x;

o lo que es lo mismo

TipoX* == &TipoX

§4.3  Comentario

El operador unitario & devuelve la dirección de memoria (Lvalue) del operando. Es decir: & x == Lvalue(x)

int x;     // x es un int
int* p;    // p es un puntero-a-int
p = &x;    // Asignación: p contiene la dirección de x (es puntero de x)

La declaración e inicialización de p podría haber sido en una sola sentencia:

int x;
int* p = &x;

Otro ejemplo:

p = &a[4];    // p contiene la dirección del 5º elemento de a


Consideremos el ejemplo siguiente, donde T representa un tipoX cualquiera:

 T t1 = 1, t2 = 2;  // t1 y t2 son tipo T, inicializados (a 1 y 2)
 T* ptr = &t1;      // ptr puntero-a-T inicializado (señala a t1)
 *ptr = t2;         // Equivalente a t1 = t2 (en este contexto)


§4.4  Siguiendo una larga tradición informática, por defecto las direcciones se muestran en formato hexadecimal, pero puede efectuarse un modelado ( 4.9.9) para obtenerlas en decimal.

Compare las salidas del ejemplo:

#include <iostream.h>
int main() {
  int x = 5;
  cout << "Direccion de x: " << &x << endl;
  cout << "Direccion de x: " << (long)&x << endl;
  cout << "Direccion de x: " << unsigned(&x) << endl;
}

Salida:

Direccion de x: 0065FE00
Direccion de x: 6684160
Direccion de x: 6684160


§4.5    Algunos identificadores que no son Lvalues, como nombres de funciones o de matrices, son automáticamente convertidos a punteros-a-tipoX cuando aparecen en ciertos contextos. Por ejemplo, cuando se pasa el identificador de un matriz como argumento formal a una función. En estos casos puede utilizarse el operador &, pero resulta redundante y se desaconseja.

Considere atentamente la salidas del siguiente ejemplo en el que se pone en evidencia la observación anterior para el caso del identificador de una matriz que pasa como argumento a una función:

#include <iostream>
using namespace std;
#include <typeinfo>

void elemento(int matriz[], int miembro) {        // L.5
  const type_info & refx = typeid(matriz);
  cout << "El tipo es: " << refx.name() << endl;
  cout << "Elemento: " << miembro << " == " << matriz[miembro] << endl; // L.8
}

void main() {        // ===============
  int m[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};    // M.1
  const type_info & refx = typeid(m);             // M.2
  cout << "El tipo es: " << refx.name() << endl;  // M.3
  elemento(m, 5);                                  // M.4
}

Salida:

El tipo es: int[10]
El tipo es: int *
Elemento: 5 == 6

Comentario

En M.2 se obtiene el tipo de la variable m definida en M.1. El resultado, que constituye la primera salida, se muestra en M.3. El valor es el esperado, ya que m se ha definido como matriz de 10 enteros.

En M.4 la matriz m es pasada como argumento de la función elemento, en la que dicho parámetro está definido como matriz de enteros e identificado como matriz (L.5).

En el interior de la función se vuelve a obtener el tipo de la variable recibida como argumento. Este valor se muestra la segunda salida. Vemos que (a pesar de su definición), el compilador considera que matriz es de tipo int* (puntero-a-antero), y en consecuencia ha sustituido automáticamente el valor m de tipo int[10] por un valor igual, pero de tipo puntero al primer elemento de la matriz.

En L.8 comprobamos que a pesar de esta nueva consideración (ser un puntero), se acepta sin problema la notación de subíndices para la variable matriz. ( 4.3.2).

  Inicio.


[1]  No estoy muy de acuerdo con la nomenclatura utilizada, aunque tampoco conozco otra mejor. Aquí deferencia/deferenciar se utiliza en sentido contrario a referencia/referenciar. Si un puntero P referencia o señala a un objeto O, "deferenciar" sería la operación de obtener el objeto a través de su puntero. Se supone que esta operación (que realiza el compilador) viene señalada en el fuente mediante *P, de modo que * es el operador que realizaría esta operación al ser aplicado sobre P. Resulta así que existen dos operadores C/C++ que realizan operaciones inversas, cada uno recibe dos nombres:

*    Operador de deferencia o indirección.    *P  == O

&   Operador de referencia o dirección.       &O ==  P

[2]  Observe que en el caso de punteros-a-función no puede afirmarse que este operador devuelva el "valor" del objeto señalado, ya que las funciones no tienen valor, solo código.

[3]  No obstante lo anterior, el significado del operador .* coincide con el que se podría suponer a la yuxtaposción de ambos. La diferencia de considerar que a.*b no es exactamente a.(*b) no es solo una mera cuestión gramatical introducida con objeto de que el conjunto pueda tener una precedencia y asociatividad propias. También permite resolver ciertos problemas que se presentan en los punteros a miembros. Recordemos de paso que el nuevo operador no admite sobrecarga ( 4.9.18).

[4]  Por esta razón, a los operadores  .* y ->*, que son específicos de C++, se les denomina a veces "operadores añadidos".

[5]  Ver en la página adjunta una explicación de la necesidad de este diseño en el operador de indirección de puntero-a-miembro ( Nota).