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.1a Declaración de punteros

§1 Presentación

La declaración de punteros utiliza un asterisco *, que en este caso actúa como calificador de tipo, en una sintaxis muy parecida a la utilizada en la declaración de objetos normales. En este capítulo nos referimos exclusivamente a declaración de punteros a objetos; la declaración de punteros a función será comentado en otro apartado ( 4.2.4). Cuando los punteros que señalan a miembros de clase (propiedades o métodos) presentan ciertas características especiales por lo que también serán tratados aparte ( 4.2.1g).


§2 La sintaxis general de la declaración de puntero a objeto es:

<tipo_objeto> * <etiqueta_puntero> [ = <iniciador> ]


<tipo_objeto> tipo del objeto al que apuntará el puntero que se declara. Puede ser cualquiera, incluso otro puntero; de hecho existen tantas clases de punteros como tipos de objetos. Un puntero debe ser declarado como apuntando a algún tipo particular, incluso si el tipo es void (que en realidad significa puntero a nada), o a un tipo definido por el usuario (por ejemplo una clase). En lo sucesivo, el puntero declarado queda constreñido a apuntar a este tipo de objeto.

Ejemplos:

int * ptr;           // declara ptr puntero a entero (int)

void * ptr;          // declara ptr puntero a void (genérico)

char * ptr;          // declara ptr puntero a carácter (char)

En estas declaraciones se utiliza el asterisco (*) como especificador de tipo, indicando que la variable es de tipo puntero en alguna de sus variedades (ver comentario 4.2.1aw1). Considere como modifica el asterisco el significado de las siguientes sentencias:

int ptr;     // declara que ptr es int
int* ptr;    // declara que ptr es puntero-a-int


También hay que advertir que es indiferente la colocación de un espacio antes o después del asterisco. Las expresiones que siguen son equivalentes [1]:

int * ptr;
int* ptr;
int *ptr;
int*ptr;


Recordemos que declaraciones de este tipo no son definiciones (no inician el puntero) y que es necesario iniciar los punteros antes de su utilización. Recordar también que, además de especificador de tipo, el operador "*" puede ser usado como operador de indirección ( 4.9.11a) y como operador de multiplicación ( 4.9.1), 


§2.1 Aunque según lo dicho hasta aquí, una expresión con int* no tiene significado, es frecuente encontrar este tipo de expresiones. Por ejemplo, void* para indicar, en abstracto, un puntero-a-void. En general, la expresión tipoX* significa puntero-a-tipoX. Ejemplos:

  • "the void* pointer type was invented for ANSI C and first implemented in C++" ( Stroustrup 1ª Ed. Pág. 5).
  • double func(char*); indica que func devuelve un double y acepta por argumento un puntero-a-carácter (recuerde que los prototipos de funciones pueden omitir los nombres de los parámetros).
  • return (char*)ptr; indica que el valor a devolver, ptr, debe ser promovido a puntero-a-carácter (modelado  4.2.1b).
  • p = (int *) malloc(SIZE * sizeof(int)); obtener memoria para almacenar SIZE enteros en forma de un puntero-a-int señalando al comienzo de dicho espacio.
§3 Peligro en la declaración de punteros

Aunque las declaraciones múltiples (en la misma sentencia) son aceptables en C++, con los punteros deben evitarse, porque es más que probable que el compilador interprete erróneamente las declaraciones sucesivas. Ejemplo:

int * ptr1 ,ptr2;   // Posible error !!

En estos casos es mejor hacer:

int * ptr1;

int * ptr2;

Ver ejemplo al respecto ( 4.9.14).

§4 Creación y destrucción

Los punteros siguen las reglas de creación y destrucción del resto de las variables, sin embargo hay que recordar que los objetos tienen duración independiente de los posibles que los señalan, de forma que cuando un puntero-a-objeto sale de ámbito, no se invoca implícitamente ningún destructor para el objeto señalado. A la inversa, la destrucción del objeto señalado no supone necesariamente la destrucción de los punteros que los referencian.

En la práctica pueden presentarse ambas circunstancias, ocasionando situaciones potencialmente peligrosas o erróneas. Por ejemplo, el primer caso puede ocurrir con objetos persistentes creados con los operadores new o new[]. Este tipo de objetos son accesibles a través de un puntero, generalmente un objeto automático que es destruido automáticamente al salir de ámbito. Pero el objeto señalado permanece ocupando memoria, por lo que se hace necesaria una invocación explícita al operador delete ( 4.11.2d2) para destruirlo antes que sea destruido el puntero, pues de lo contrario se produciría una perdida irrecuperable de memoria.

El segundo caso, la destrucción de un objeto sin que sean destruidos los punteros que lo señalan, también es bastante frecuente, dando lugar a los denominados punteros descolgados ("Dangling pointers"), cuyo uso inadvertido es especialmente peligroso, pues señalan a zonas de contenido erróneo.

§5 Punteros constantes y a-constantes

La expresión de declaración de un puntero puede llevar opcionalmente el especificador const ( 4.2.1e) para indicar que es:

  • Puntero-a-constante: Apunta a un objeto de tipo constante. El puntero puede variar, con lo que apuntaría a otro objeto; en cambio el objeto señalado no puede cambiar su valor. En consecuencia, el puntero no puede ser utilizado para cambiar indirectamente el valor del objeto al que señala.

int * pt1;             // declara pt1 puntero variable a int variable

const int * pt2;       // declara pt2 puntero variable a constante tipo int

int const * pt2;       // equivalente al anterior

char const * pt3;      // declara pt3 puntero variable a constante tipo char

const char * pt4;      // equivalente al anterior

Como se ha señalado ( 3.2.1c), int y const int son tipos distintos, por lo que a su vez pt1 y pt2 son punteros de tipo distinto.

  • Puntero-constante: El puntero es constante; el objeto señalado podría variar, pero el valor del puntero -dirección de memoria que señala- no. Esto significa que está indefectiblemente ligado a un mismo objeto.

    int* const pt5 = &e1;     // define pt5 puntero constante a int variable

    char* const pt6 = &e2;    // define pt6 puntero constante a char variable

    Naturalmente ambas condiciones pueden darse simultáneamente: puntero-constante (que no puede alterar su valor) señalando a un objeto-constante:

    const int* const pt7 = &kte; // define pt7 puntero constante a int constante


    Observe que, como ocurre con otros tipos de constantes, cuando el puntero es constante, la iniciación debe realizarse forzosamente en el momento de su declaración. Ejemplo:

    const int ki = 33;
    int* const pk;
    pk = &ki;             // Error!! asignación a constante
    int* const pk = &ki   // Ok.

§6 Ejemplos:

Como puede verse, la sintaxis de declaración de punteros sigue reglas análogas a la declaración de objetos, con la diferencia de incluir el símbolo de declaración de puntero * entre el calificador de tipo y el nombre del objeto. Con objeto de familiarizar al lector con la notación utilizada, se exponen varios casos de declaraciones y definiciones de punteros de tipos diversos.

Declaración de objeto

Declaración de puntero-al-objeto

int x;

x es un entero (no iniciado)

int* ptr;

ptr puntero-a-entero (no iniciado)

int x = 25;

x es un entero (iniciado)

int* ptr = &x;

ptr puntero-a-entero (iniciado)

int* ptr = &x;

ptr puntero-a-entero (iniciado)

int** pptr = &ptr;

pptr puntero-a-puntero a entero (iniciado)

tipoX x;

x es un objeto tipoX

tipoX* ptx;

ptx es un puntero a tipoX (no iniciado)

const int ci = 7;

ci es una constante tipo int iniciada a 7

const int* pci;

pci es un puntero a constante tipo int

tipoX m[3];

m es una matriz de 3 objetos tipoX

tipoX* ptr = &m[0];

ptr es un puntero al primer elemento de m (iniciado). Es por tanto un puntero a tipoX

tipoX (*ptr)[] = &m;

ptr es puntero-a-matriz abierta de tipoX (iniciado)

tipoX (*ptr)[3] = &m;

ptr es puntero-a-matriz de tres objetos tipoX (iniciado)

tipoX* mp[3];

mp es una matriz de 3 punteros-a-tipoX

tipoX* (*ptr)[] = &mp;

ptr es puntero-a-matriz abierta de punteros-a-tipoX (iniciado)

char* aP[] = {"Bien", "Mal"};

aP matriz de punteros-a-char (iniciada)

char* (*aPp) [] = &aP;

aPp puntero-a-matriz de punteros-a-char (iniciado)

char (* mi[2])[3];

mi matriz de dos punteros-a-matriz-de-tres char

char (*(* ptr)[2])[3] = &mi;

ptr puntero-a-matriz de dos punteros-a-matriz de tres char

§6.1 Otras expresiones con punteros

tipoX (*ptr)[5][7];

ptr es puntero-a-matriz de tipoX de dimensiones 5 y 7

void* ptr;

ptr puntero-a-void (genérico 4.2.1d)

Ejemplo:

int n = 33;
void* ptr = &n;

const void* ptr;

ptr puntero-a-void (genérico) a constante

Ejemplo:

const int ki = 33;
const void* ptr = &ki;

tipoX* func();

Funcion devolviendo puntero-a-tipoX

void*& pword(int);

Función que acepta un int y devuelve una referencia 4.2.3) a un puntero genérico 4.2.1d).

void func(int*);

Función devolviendo void y recibiendo puntero-a-int

tipoX (*ptr)();

ptr es puntero a función que devuelve tipoX ( 4.2.4)

char* const ptr = "Hola";

ptr es puntero constante a cadena-de-char ( 3.2.3f)

char const* ptr = "Hola";

ptr es puntero-a-constante tipo cadena-de-char

const char* ptr = "Hola";

Equivalente a la anterior


Observe que definiciones como las dos últimas, crean una constante tipo cadena (en este caso, de contenido "Hola\0"), alojada en algún lugar de la memoria, pero sin un identificador para manejarla. Solo es posible referenciarla (dirigirse a ella) a través del puntero. En otras palabras, no es posible usarla por su nombre, sino por su dirección.

Observe que en las declaraciones de punteros en las que intervienen matrices y/o funciones, es muy importante la correcta situación de los paréntesis:

char* ptr [];      // Matriz de punteros-a-char
char (* ptr) [];   // Puntero-a-matriz de char

char* (* ptr) [];  // Puntero-a-matriz de punteros a char

Ejemplo:

#include <iostream>
using namespace std; 

char achar[5] = {'A','E','I','O','U'};
char * punt1 [5];
char (* punt2) [5];
char* (* punt3) [5];

int main (void) {    // ==================
  punt1[0] = &achar[0]; punt1[4] = &achar[4];
  cout << "Comienzo: == " << *punt1[0] << endl;
  cout << "Final: == " << *punt1[4] << endl;

  punt2 = &achar;
  cout << "Contenido: == " << *punt2 << endl;

  punt3 = &punt1;
  cout << "Contenido: == " << *punt3 << endl;
  cout << "Contenido: == " << **punt3 << endl;
  return 0;
}

Salida:

Comienzo: == A
Final: == U
Contenido: == AEIOU
Contenido: == 0047A85C
Contenido: == AEIOU

§7 Expresiones peligrosas

Se ha dicho que "No hay nada más temible que un puntero descontrolado". Generalmente dan lugar a uno de los problemas más difíciles de depurar en un programa y en consecuencia, hay que prestar especial atención en todas las sentencias relacionadas con ellos. Por ejemplo, sentencias como las que siguen pueden causar un desastre.

int* iptr;

*iptr = 100;    //muy peligroso!!

La razón es que en la primera línea declaramos iptr como puntero-a-entero, no se inicia, por lo que no sabemos a donde apunta. Posiblemente el Lvalue de la variable esté ocupado por basura, aunque puede tratarse de una zona sensible (actualmente apunta a un sitio impredecible). En la segunda línea el sitio apuntado por iptr lo sustituimos con 100, posiblemente machaquemos una zona de memoria equivocada con las consecuencias fáciles de prever. Es más correcto hacer:

int z;           // L.1

int* iptr = &z;  // L.2

*iptr = 100;     // L.3

La explicación es que en L1 el compilador ha iniciado una variable z con una dirección de memoria adecuada, aunque momentáneamente no inicializada (contiene basura 4.1.2). En L2 el puntero apunta a ese valor. Finalmente, la basura original es sustituida por un valor conocido en L3.

  Inicio.


[1] Por cuestión de gusto personal, la mayoría de las veces utilizaremos la segunda forma: int* ptr;. Además, tipoX* aisladamente tiene sentido por si mismo; indica "puntero-a-tipoX" de forma genérica, sin referirse a ningún identificador concreto .