Punteros I

Declaracion e inicializacion

Un puntero, como cualquier variable u objeto, ademas de ser declarado (para comenzar a existir) necesita ser inicializado (darle un valor de modo controlado), lo cual se realiza mediante el operador de asignacion ('='). Desde que el puntero es declarado almacena un valor, el problema es que se trata de un valor aleatorio, intentar operar con un puntero sin haberlo inicializado es una frecuente causa de problemas.

Asignacion erronea: "Cannot assign..."

En primer lugar veremos un caso simple de asignacion erronea para comprender el mensaje enviado por el compilador.

void main(){
int a;
int* b;
b = a;         //Error
}

El programa no compila, y recibimos el mensaje de error:

"Cannnot assign 'int' to 'int near*' en function main();

Hemos tratado de inicializar el puntero 'b' asignandole un valor equivocado, de otro tipo. El analisis de los mensajes de error siempre es instructivo, profundizemos en este.

1- En primer lugar: ¿Que es un "int near*" ?. Los punteros pueden ser clasificados como 'near' (cercano) o 'far' (lejano) de acuerdo a si la direccion apuntada se encuentra en el mismo segmento que el puntero. Las principales diferencias se exponen en el siguiente cuadro.

Tipo de puntero Caracteristicas Cantidad de bytes que utiliza el puntero
near La direccion apuntada se encuentra
en el mismo segmento que el puntero
dos - (offset)
far Se encuentran en diferente segmento cuatro - (segmento::offset)

Si un programa no requiere de una gran cantidad de datos significa que pueden entrar en un solo segmento, y los punteros seran 'near' por defecto, en caso contrario el default sera 'far'. Esto es determinado directamente por el modelo de memoria utilizado por nuestro programa.

2-En segundo lugar, el mensaje nos indica una discordancia de tipos. Uno es 'int', y el otro es 'int near*', si obviamos la caracteristica de 'near' vemos que la expresion "int*" coincide con nuestra declaracion del puntero. Lo mas instructivo de esto es comprender que el asterisco pertenece al tipo (type), no al nombre ('b'). Algunos autores discuten sobre cual de las dos siguientes declaraciones es la mas adecuada para declarar un puntero:

int *b;
int* b;

Ambas son perfectamente validas, la unica diferencia es que el primer caso se sugiere que '*' forma parte de 'b', y en el segundo que '*' forma parte del tipo.
Lo recomendable es adoptar la segunda forma, la primera se presta a confundir el operador '*' con el operador de 'indirection', y es muy importante comprender que aqui no hay nada de 'indirection', es solo una declaracion de un identificador (b), ligado a un tipo (int*). Es el mensaje del compilador el que nos indica esta ultima interpretacion.

Para que el programa compile sin problemas es necesario utilizar el operador '&' antes del nombre de la variable, el efecto del operador es devolver la direccion en memoria de la variable, la cual se asigna naturalmente a un puntero.

void main(){
int a;
int* b;
b = &a;         //El puntero 'b' apunta a 'a'.
}

Una variable individual de tipo 'T' y un array de elementos de tipo 'T' pertenecen a tipos diferentes, no es posible la asignacion entre un entero y un array de enteros. Intentemos sin embargo tal asignacion:

void main(){
int a;
int b[4];
a = b;         //Asignacion erronea, no compila
}

Lo mas interesante del ejemplo es que el mensaje de error es similar (pero inverso) al de nuestro primer ejemplo fallido:

"Cannnot assign 'int near*' to 'int' en function main();

Lo cual puede resultar sorprendente, pues en el ejemplo no hemos declarado ningun puntero, solo un entero y un array de enteros. Lo que esta sucediendo es que el compilador se esta refieriendo al array de enteros como un "int near*", como un puntero.
Un array y un puntero no son exactamente lo mismo, hay algunas diferencias, pero la relacion que existe entre ambos es muy estrecha y la sintaxis aplicable a ambas entidades es en gran parte identica. Esta relacion explica que el siguiente ejemplo compile bien sin ninguna complicacion:

void main(){
int a [4];
int* b;
b = a;        //o bien --> b = &a[0];
}

Podria haberse esperado algun problema, puesto que no hemos obtenido la direccion del array con el operador '&', pero no ocurre asi, el solo nombre del array es tomado como sinonimo de la direccion de su primer elemento (o puntero a su primer elemento).

Bien, esta ha sido una introduccion para comprender el mensaje de error tipico en una inicializacion fallida. El intento de asignar una variable individual a un array produce un mensaje de error distinto ("Lvalue requerido"), ver Anexo2.
Veamos ahora ejemplos de inicializaciones de punteros correctas.

Opciones de inicializacion

Un puntero puede ser inicializado con la direccion de memoria de un objeto, tal objeto debe pertenecer a un tipo acorde al tipo al que apunta el puntero. Puede tratarse de la direccion de un elemento de un array o de una variable individual, el operador '&' antepuesto a un objeto nos devuelve su direccion de memoria. Tambien puede utilizarse un "literal", ya sea numerico, de caracter, o de otro tipo, y puede inicializarse como puntero nulo, en este caso esta permitido usar el 0, el unico entero permitido, por su equivalencia con el valor NULL.

Suponiendo un tipo cualquiera "T", son inicializaciones validas las siguientes:

Puntero inicializado a
partir de:
Declaracion e inicializacion
en una misma linea
Declaracion e inicializacion
desdobladas
Un objeto individual
T x;
T* ptr = &x; T* ptr;
ptr = &x;
Un array de objetos

T x [10];

T* ptr = &x[0]; T* ptr;
ptr = &x[0];
T* ptr = x; T* ptr;
ptr = x;
Otro puntero del
mismo tipo
T* x;
T* ptr = x; T* ptr;
ptr = x;
Valor 0 = puntero nulo
Null=0
T* ptr = 0; T* ptr;
ptr = 0;
T* ptr = NULL; T* ptr;
ptr = NULL;
Un literal T* ptr = [literal] T* ptr;
ptr = [literal];

Sobre este cuadro caben las siguientes aclaraciones:
1-Inicializar un puntero apuntando al primer elemento de un array admite dos notaciones equivalentes, en la segunda se sobreentiende que el elemento apuntado es el primer elemento del array.
2-La equivalencia entre el valor 0 (cero) y NULL es de uso general, sin embargo existen compiladores que dan a NULL un valor diferente a cero.
3-Un 'literal' debe ser el apropiado para el tipo de puntero inicializado. Si es un puntero a char, una cadena de caracteres cualquiera (ej: "hola") sera un literal adecuado, si se trata de tipo numerico, para un int "4" sera apropiado.

Si tomamos como ejemplo el tipo "char", siguiendo al cuadro anterior, tenemos las siguientes opciones de inicializacion:

Puntero inicializado a partir
de:
Declaracion e inicializacion en
una misma linea
Declaracion e inicializacion
desdobladas
Un elemento

char ch;
char* p = &ch; char* p;
p = &ch;
Un array

char cad[10];
char* p = cad; char* p;
p = cad;
char* p = &cad[0]; char* p;
p = &cad[0];
Valor 0 = puntero nulo
Null=0
(0 es el unico valor
entero que puede
inicializar un puntero)
char* p = 0; char* p;
p = 0;
char* p = NULL; char* p;
p = NULL;
Otro puntero (ya inicializado)
char *ptr;
char* p = ptr; char* p;
p = ptr;
Un literal de cadena
"casa";
char* p = "casa"; char* p;
p = "casa";

Se ha insistido lo suficiente en que un puntero almacena como valor una direccion de memoria, por eso la presencia de un 'literal', en esta ultima tabla el literal de cadena "casa", puede sorprender. Es importante tener claro que todos los literales se almacenan desde el comienzo del programa en un lugar del segmento de datos, no es posible obtener su direccion (por medios normales) pero existe, es al comienzo de dicho segmento, el mismo sitio que se reserva a valores constantes y variables globales.

Un literal es tratado como constante, esto es lo que permite que una funcion pueda retornar una constante sin temor a que dicho almacenamiento se pierda al salir de la funcion, un literal no es una variable 'local'. No hay obstaculos para inicializar un puntero con una variable constante, por lo tanto lo mismo se aplica a literales de cualquier tipo.

Las tablas anteriores no abarcan todos los casos posibles de inicializacion de punteros, aun no se han mencionado los casos donde el puntero apunta a una funcion o un objeto miembro de una clase, ni la opcion de inicializar a traves de memoria dinamica.

Inicializacion a traves de memoria dinamica

Esta modalidad se diferencia de todas las enumeradas hasta ahora y puede considerarse como la principal. Todas las formas vistas hasta aqui asignan al puntero la direccion de memoria de otra entidad (elemento, array, puntero, literal) ademas del caso especial del valor NULL. Ya se ha mencionado que la declaracion de un puntero no implica la reserva de memoria, salvo 2 bytes para almacenar una direccion, por esa razon podria decirse que el puntero, cuando es inicializado por otro elemento, 'vive' de la memoria que le aporta el objeto al que apunta.

La reserva de memoria dinamica requiere el uso obligado de un puntero, el cual apuntara al comienzo de la zona reservada. Lo diferente aqui es que se trata del unico caso donde el puntero no necesita de otro elemento que le aporte memoria necesaria, no necesita apuntar a algun otro objeto. Cuando reservamos dinamicamente 40 bytes para un puntero a char, operaremos con el puntero 'como si' apuntara a un segundo objeto (un array de caracteres), pero tal array no existe.

A pesar de no existir propiamente un segundo objeto, sigue siendo esencial, el tipo (type) segun el cual se declara el puntero, pues esto determina el modo en que el puntero nos permitira acceder a tal zona de memoria. No hay un 'objeto apuntado', pero el puntero se conduce igual que si lo hubiera, por esa razon hablaremos en general del 'objeto apuntado' por el puntero, sin aclarar el caso especial que estamos considerando.

En C++ la reserva y liberacion de memoria dinamica se realiza a traves de los operadores new y delete, y su sintaxis, para un puntero de nombre 'ptr' es la siguiente:

Reserva Liberacion
Elemento individual
de tipo 'T'
T* ptr = new T; delete ptr;
Array de 'n' elementos
de tipo 'T'
T* ptr = new T[n]; delete [] ptr;

Atraves del operador new solicitamos una cierta cantidad de memoria dinamica, es posible que no exista suficiente memoria disponible, en tal caso el operador nos devolvera un puntero NULL (o apuntando a 0), y es por esta razon que luego de una solicitud es recomendable inspeccionar si el puntero devuelto es nulo. Esta seria la respuesta 'clasica' a una reserva fallida de memoria dinamica, sin embargo existen diferentes compiladores que, ajustandose al standard C++ no devuelven un puntero nulo sino que lanzan una excepcion (bad_alloc).

Algunos viejos compiladores no reconocen la opcion de borrar el puntero con corchetes vacios y nos exigen que especifiquemos el numero de bytes a borrar (los mismos que los reservados), TC++ a partir de su version 3.0 admite esa notacion. Escribir "delete ptr;", sin los corchetes, solo libera el primer elemento del array, y es por lo tanto un error importante.

Desreferenciacion ("indirection")

Concepto

Un puntero almacena una direccion de memoria de alguna entidad, esto en si mismo no seria demasiado util si no fuera posible, a traves del puntero, acceder a lo que esta almacenado en esa direccion. Segun el creador de C++:  "La operacion fundamental de un puntero es desreferenciar, es decir, referir al objeto al que apunta el puntero." (Stroustrup, 1997). A continuacion desarrollaremos esta definicion.

La funcion de 'desreferenciar' un puntero es llevada a cabo por el operador '*', que ademas cumple otras funciones en C++. Como su papel es complementario a una de las funciones del operador '&' se comenzara estudiando la relacion y diferencia de estos dos operadores
Ambos operadores tienen mas de un sentido dependiendo del contexto en que aparecen, por lo tanto son casos de sobrecarga de operadores. Veamos sus distintos usos:

OPERADOR                   *                                                  &                            
Usos Multiplicacion
int a = 3, b=2,c;
c = a * b;
Operacion Bitwise AND
char a=0x37;
a &=0x0F;
Declaracion type puntero
int n;
int* p = n;
Declaracion del type referencia
int a;
int &b = a;
Dereferencing (indirection)
cout<<*p;
Referencing
cout<<&a;

El primer uso de cada operador se distingue claramente de los otros dos, derivan de C y no tienen relacion con el tema punteros. Los que figuran en segundo lugar pertenecen a la sintaxis basica de declaracion de punteros y referencias. Nos concentraremos en el tercer significado de estos operadores.

El papel opuesto y complementario del tercer uso de ambos operadores se podria sintetizar asi:
dadas las siguientes declaraciones:

int v = 4;
int* p = &v;
 El puntero 'p' es equivalente a la direccion de memoria
 a la que apunta.

 cout<<p     saca en pantalla una direccion de memoria
                  (por ej: 0x8f70fff0)
 La variable 'v' es equivalente al valor que almacena


 cout<<v              saca en pantalla '4'
 Mientras que la expresion '*p' es sinonimo del elemento individual  que se encuentra en la localidad apuntada por el puntero

cout<<*p   saca en pantalla '4'
 Mientras que la expresion '&v' es un sinonimo de la direccion  de memoria donde se encuentra esa variable

 cout<<&v     saca en pantalla una direccion de memoria
                     (ej: 0x8f70fff0) 

Como puede observarse, el efecto de ambos operadores es inverso, en un caso dada una localidad de memoria se accede al elemento almacenado en ella (el caso de '*'), en el otro ('&') dada una variable accedemos a la direccion de memoria donde almacena su valor.

El termino usado para este efecto del operador '*' es el de 'indirection' o 'dereferencing' traducido generalmente como 'indireccion' o 'desreferenciacion'. Su sentido mas llano seria: operador que permite referirnos al elemento individual apuntado por el puntero, en lugar de la direccion en que ese elemento se encuentra almacenado.

A veces se utiliza, para ejemplificar la 'indirection', un puntero que apunta a char, estos ejemplos pueden oscurecer el sentido del termino 'indirection', en especial porque con tales punteros la linea "cout<<p" no hubiera sacado en pantalla una direccion de memoria, sino una cadena de caracteres.


El caso especifico de un puntero a char

Dadas las siguientes declaraciones e inicializaciones:

char cad[] = "hola";
char* ptr = cad;          //Aqui el '*' es un indicador de tipo, no de 'indirection'

El puntero 'ptr' apunta a 'cad', al char inicial de 'cad'. Veamos ahora que saldria en pantalla con 'p' y con '*p', para el caso volveremos a usar la libreria "iostream.h" de C++, pues las funciones de C impondrian su formato a la salida.

cout<<ptr;                //sale en pantalla:  "hola"
cout<<*ptr;               //sale en pantalla:  'h'

Lo que puede desorientar aqui es que 'ptr' no imprima en pantalla una direccion de memoria, que es lo esperable tratandose de un puntero. Se trata de una caracteristica propia de las funciones que tratan con punteros a char, y no de un rasgo diferencial de los punteros a char, estos tienen las mismas caracteristicas generales de cualquier puntero.

Utilizando una funcion C de "stdio.h", las lineas anteriores son equivalentes a

printf("%s", ptr);
printf("%c", *ptr);

Analicemos el funcionamiento de printf. Esta funcion, recibe como argumento un puntero a char, algo cuyo tipo es char*, es decir una direccion de memoria. En C o C++ no hay otro modo de pasar un array a una funcion que a traves de una direccion de memoria. El especificador de formato, "%s", le indica a la funcion que interprete esa direccion como siendo el comienzo de una cadena de caracteres (la 's' es de 'string'). Lo que la funcion hace es interpretar los bytes, uno a uno, como indicando caracteres ascii, y los sacara ordenadamente en pantalla hasta encontrar un '\0', sin importar donde se encuentre ese '\0' o si excede o no la capacidad del array original.
En C++, el flujo de salida 'cout' y el operador de insercion '<<', no requieren de un formato especifico para sacar algo en pantalla, esto significa que imponen un formato predeterminado segun el dato enviado como parametro. En el caso de que este parametro sea un puntero a char imponen el formato "cadena de caracteres", exactamente igual que printf con formato "%s". Esto no es obvio, dado que se trata de un puntero podrian sacarlo en pantalla como una direccion de memoria, pero no ocurre asi.
Es por esta razon que la idea de 'indirection' se oscurece en relacion a 'punteros a char', pues las funciones standard de impresion en pantalla de C y C++ no tratan a tal puntero como una direccion de memoria mas (aunque lo sea).

Siendo 'p' un puntero a tipo char, para las funciones standard de impresion: 'p' es la cadena apuntada, '*p' el caracter individual apuntado.

Asignacion de punteros

Un puntero puede ser asignado a otro puntero del mismo tipo a traves del operador '='. El significado de tal asignacion es similar al de una asignacion entre variables, el valor almacenado en el elemento de la derecha se copia en el elemento de la izquierda. Solo que en el caso de punteros este valor es una direccion de memoria, y esto puede producir un efecto distinto al esperado.

void f (char* cad1, char* cad2) {
cad1 = cad2;
*cad1 = '3';                        //Efecto: modificacion de cadena "dos".
//.................................
}

char uno = "1111";
char dos = "2222";
f(uno, dos);                  //Llamado a funcion f();

La funcion 'f()' recibe dos punteros a char desde otra funcion, sigue luego una asignacion de 'cad2' en 'cad1', y una modificacion de un char a traves de desreferenciacion del puntero.
Si la intencion era copiar el contenido de la cadena original "dos" en la cadena "uno", para modificar "uno" sin alterar "dos", estamos ante un error. El caracter '3' se copiara en la cadena original "dos", por la razon de que luego de la asignacion de punteros (cad1=cad2) ambos apuntan a "dos".

Hay casos donde puede ser util que dos punteros apunten a una misma direccion, y entonces sera correcto asignar punteros mediante el operador '=', pero si lo que se busca es copiar el contenido de un array, entonces se debe hacer de otro modo, copiando uno a uno los elementos de dicho array.

Dados dos punteros ("pt1" y "pt2") a array, que apuntan a direcciones diferentes (son dos arrays diferentes), los efectos de una asignacion de punteros y copia de array son los siguientes:

Operacion Efecto
pt1 = pt2;
Asignacion de punteros. Lo que se copia realmente son los 2 (o 4) bytes de direccion de memoria.
El puntero 'pt1' deja de apuntar a al array original, ahora apunta a la misma direccion que 'pt2'.
while (*pt2!=0) *pt1=pt2; Copia de array. La copia se realiza elemento por elemento. Se copian tantos elementos como caracteres tenga el array 'pt2'. En el caso de una cadena de caracteres,podemos confiar en el '\0' para saber cuantos elementos copiar.

Es muy importante diferenciar ambas operaciones. Un array no puede ser copiado mediante el operador de asignacion '=', hay que copiar elemento por elemento, un puntero puede ser copiado con tal operador, pero el efecto provocado puede ser distinto al efecto deseado.
La confusion entre copia de punteros y copia de array puede provocar otro tipo de problemas en relacion a memoria dinamica o constructores de copia, problemas que se analizan mas adelante.


PRINCIPAL

Free Web Hosting