CAPITULO 7: PUNTEROS (POINTERS)

1. INTRODUCCION A LOS PUNTEROS Los punteros en el
Lenguaje C , son variables que " apuntan " , es decir que poseen la dirección
de las ubicaciones en memoria de otras variables, y por medio de ellos tendremos
un poderoso método de acceso a todas ellas .
Quizás este punto es el más conflictivo del lenguaje , ya que muchos programadores
en otros idiomas , y novatos en C , lo ven como un método extraño ó al menos
desacostrumbrado , lo que les produce un cierto rechazo . Sin embargo , y
en la medida que uno se va familiarizando con ellos , se convierten en la
herramienta más cómoda y directa para el manejo de variables complejas ,
argumentos , parámetros , etc , y se empieza a preguntar como es que hizo
para programar hasta aquí , sin ellos . La respuesta es que no lo ha hecho
, ya que los hemos usado en forma encubierta , sin decir lo que eran . (
Perdón por el pequeño engaño ).
Veamos primero , como se declara un puntero :
tipo de variable apuntada *nombre_del_puntero ;
int *pint ;
double *pfloat ;
char *letra , *codigo , *caracter ;
En estas declaraciones sólo decimos al compilador que reserve una posición
de memoria para albergar la dirección de una variable , del tipo indicado
, la cual será referenciada con el nombre que hayamos dado al puntero .
Obviamente , un puntero debe ser inicializado antes de usarse , y una de las eventuales formas de hacerlo es la siguiente:
int var1 ; /* declaro ( y creo en memoria ) una variable entera ) */
int *pint ; /* " " " " " un puntero que contendrá
la dirección de una variable entera */
pint = &var1 ; /* escribo en la dirección de memoria donde está el
puntero la dirección de la variable entera */
Como habiamos anticipado en capítulos anteriores " &nombre_de_una_variable
" implica la dirección de la misma . Si se pregunta porque no se usaron otros
símbolos en vez de & y * , que se confunden con la Y lógica de bits y
el producto , ..... consuelese pensando que yo también me hice siempre esa
pregunta . De cualquier manera es siempre obvio , en el contexto del programa
el uso de los mismos .
Esquematicamente , lo que hemos hecho se puede simbolizar de la siguiente
manera : donde dentro del recuadro está el contenido de cada variable .
Pint xxxxxx valor contenido por var1
Dirección de var1
yyyyyy (posición de memoria xxxxxx (posición de memoria
ocupada por el puntero ) ocupada por la variable)
En realidad , como veremos más adelante , en la declaración del puntero
, está implicita otra información : cual es el tamaño (en bytes) de la variable
apuntada. El símbolo & , ó dirección , puede aplicarse a variables
, funciones , etc , pero nó a constantes ó expresiones , ya que éstas no
tienen una posición de memoria asignada.
La operación inversa a la asignación de un puntero , de referenciación del
mismo , se puede utilizar para hallar el valor contenido por la variable
apuntada . Así por ejemplo serán expresiones equivalentes :
y = var1 ;
y = *pint ;
printf("%d" , var1 ) ;
printf("%d" , *pint) ;
En estos casos , la expresión " *nombre_del_puntero " , implica " contenido
de la variable apuntada por el mismo " . Veamos un corto ejemplo de ello
:
#include
main()
{
char var1 ; /*una variable del tipo caracter */
char *pchar; /* un puntero a una variable del tipo caracter */
pc = &var1 ; /*asignamos al puntero la direccion de la variable */
for (var1 = 'a'; var1 <<= 'z'; var1++)
printf("%c", *pchar) ; /* imprimimos el valor de la variable apuntada */
return 0 ;
}
Vemos acá , que en el FOR se incrementa el valor de la variable , y luego
para imprimirla usamos la dereferenciación de su puntero.
El programa imprimirá las letras del abecedario de la misma manera que lo
habría hecho si la sentencia del printf() huiera sido, printf("%c" , var1
) .
Hay un error , que se comete con bastante frecuencia , y es cargar en la
dirección apuntada por un puntero a un tipo dado de variable , el contenido
de otro tipo de las mismas , por ejemplo :
double d = 10.0 ;
int i = 7 , *pint ;
pint = &i ;
*pint = 10 ; /* correcto,equivale a asignar a i el valor 10 */ ;
*pint = d ; /* ERROR se pretende cargar en una variable entera un valor double */
pint = &d ; /* INCORRECTO se pretende apuntar a una variable double con un
puntero declarado como apuntador a int */
pint = 4358 ; /* ?????? */
El primer error , la asignación de un double , produce la pérdida de información
dada por la conversión automática de tipo de variable , ya vista anteriormente
, el segundo produce un llamado de atención rotulado como " asignación sospechosa
de un pointer " . Resumiendo , las variables ó constantes cargadas por dereferenciación
de un puntero , deben coincidir en tipo con la declaración de aquel .
La asignación de una constante a un pointer , y no a la variable apuntada
por él , es un serio error , ya que debe ser el compilador , el encargado
de poner en él el valor de la dirección , aquel así lo declara dando un mensaje
de " conversión de puntero no transportable " . Si bien lo compila , ejecutar
un programa que ha tenido esta advertencia es similar a jugar a la ruleta
rusa , puede "colgarse" la máquina ó lo que es peor destruirse involuntariamente
información contenida en un disco , etc.
Hay un sólo caso en el que esta asignación de una constante a un puntero
es permitida , muchas funciones para indicar que no pueden realizar una acción
ó que se ha producido un error de algun tipo , devuelven un puntero llamado
"Null Pointer" , lo que significa que no apunta a ningun lado válido , dicho
puntero ha sido cargado con la dirección NULL ( por lo general en valor 0
) , así la asignación : pint = NULL ; es válida y permite luego operaciones
relacionales del tipo if( pint ) ..... ó if( print != NULL ) para convalidar
la validez del resultado devuelto por una función .
Una advertencia : si bien volveremos más adelante sobre este tema , debemos
desde ahora tener en cuenta que los punteros no son enteros , como parecería
a primera vista , ya que el número que representa a una posición de memoria
, sí lo es . Debido al corto alcance de este tipo de variable , algunos compiladores
pueden , para apuntar a una variable muy lejana , usar cualquier otro tipo
, con mayor alcance que el antedicho .
2. PUNTEROS Y ARRAYS Hay una relación muy cercana entre
los punteros y los arrays . Yá vimos previamente que el designador ( ó nombre
de un array ) era equivalente a la dirección del elemento [0] del mismo .
La explicación de ésto es ahora sencilla : el nombre de un array , para el
compilador C , es un PUNTERO inicializado con la dirección del primer elemento
del array . Sin embargo hay una importante diferencia entre ambos , que haremos
notar más abajo.
Veamos algunas operaciones permitidas entre punteros :
ASIGNACION
float var1 , conjunto[] = { 9.0 , 8.0 , 7.0 , 6.0 , 5.0 );
float *punt ;
punt = conjunto ; /* equivalente a hacer : punt = &conjunto [0] */
var1 = *punt ;
*punt = 25.1 ;
Es perfectamente válido asignar a un puntero el valor de otro , el resultado
de ésta operación es cargar en el puntero punt la dirección del elemento
[0] del array conjunto , y posteriormente en la variable var1 el valor del
mismo (9.0) y para luego cambiar el valor de dicho primer elemento a 25.1
.
Veamos cual es la diferencia entre un puntero y el denominador de un array
: el primero es una VARIABLE , es decir que puedo asignarlo , incrementarlo
etc , en cambio el segundo es una CONSTANTE , que apunta siempre al primer
elemento del array con que fué declarado , por lo que su contenido NO PUEDE
SER VARIADO . Si lo piensa un poco , es lógico , ya que "conjunto" implica
la dirección del elemento conjunto [0] , por lo que , si yo cambiara su valor
, apuntaría a otro lado dejando de ser , "conjunto" . Desde este punto de
vista , el siguiente ejemplo nos muestra un tipo de error bastante frecuente:
ASIGNACION ERRONEA
int conjunto[5] , lista[] = { 5 , 6 , 7 , 8 , 0 ) ;
int *apuntador ;
apuntador = lista ; /* correcto */
conjunto = apuntador; /* ERROR ( se requiere en Lvalue no constante ) */
lista = conjunto ; /* ERROR ( idem ) */
apuntador = &conjunto /* ERROR no se puede aplicar el operador & (dirección) a
una constante */
Veamos ahora las distintas modalidades del incremento de un puntero :
INCREMENTO O DECREMENTO DE UN PUNTERO
int *pint , arreglo_int[5] ;
double *pdou , arreglo_dou[6] ;
pint = arreglo_int ; /* pint apunta a arreglo_int[0] */
pdou = arreglo_dou ; /* pdou apunta a arreglo_dou[0] */
pint += 1 ; /* pint apunta a arreglo_int[1] */
pdou += 1 ; /* pdou apunta a arreglo_dou[1] */
pint++ ; /* pint apunta a arreglo_int[2] */
pdou++ ; /* pdou apunta a arreglo_dou[2] */
Hemos declarado y asignado dos punteros , uno a int y otro a double ,
con las direcciones de dos arrays de esas caracteristicas . Ambos estarán
ahora apuntando a los elementos [0] de los arrays . En las dos instrucciones
siguientes incrementamos en uno dichos punteros . ¿ adonde apuntaran ahora
?. Para el compilador , éstas sentencias se leen como : incremente el
contenido del puntero ( dirección del primer elemento del array ) en un número
igual a la cantidad de bytes que tiene la variable con que fué declarado
. Es decir que el contenido de pint es incrementado en dos bytes (un int
tiene 2 bytes ) mientras que pdou es incrementado 8 bytes ( por ser un puntero
a double ) , el resultado entonces es el mismo para ambos , ya que luego
de la operación quedan apuntando al elemento SIGUIENTE del array , arreglo_int[1]
y arreglo_dou[1] .
Vemos que de ésta manera será muy facil "barrer" arrays , independientemente
del tamaño de variables que lo compongan , permitiendo por otro lado que
el programa sea transportable a distintos hardwares sin preocuparnos de la
diferente cantidad de bytes que pueden asignar los mismos , a un dado tipo
de variable .
De manera similar las dos instrucciones siguientes , vuelven a a incrementarse
los punteros , apuntando ahora a los elementos siguientes de los arrays.
Todo lo dicho es aplicable , en identica manera , al operador de decremento -- .
ARITMETICA DE DEREFERENCIA Debido a que los operadores * y ++
ó -- tienen la misma precedencia y se evaluan de derecha a izquierda , y
los paréntesis tienen mayor precedencia que ambos , muchas operaciones que
los utilizan en conjunto a todos estos operadores , pueden parecer poco claras
y dar origen a un sinnúmero de errores , (revise un poco la TABLA 13 del
capítulo 3 ) analicémoslas detalladamente , partiendo de :
int *p , a[] = { 0 , 10 , 20 , 30 , 40 , 50 , 60 , 70 , 80 , 90 } ;
int var ;
p = a ;
A partir de aquí , el puntero está apuntando a a[0] . Veamos las distintas variantes que puede tener la siguiente instrucción:
*p = 27 ;
La más sencilla de las opciones , simplemente asignamos al elemento apuntado
por p ( a[0] ) un valor constante . Veamos la inversa de ella:
var = *p ;
var sería asignada al valor 0 (contenido de a[0]) , y p seguiría apuntando
al mismo elemento. Que hubiera pasado, si en vez de ello se hubiera escrito:
var = *( p + 1 ) ;
acá podríamos traducir el sentido de la operación como : cargue var con
el contenido del elemento siguiente al apuntado por p ( a[1] ) . Lo interesante
de remarcar acá es que p , en sí mismo , NO VARIA Y LUEGO DE ESTA SENTENCIA
SEGUIRA APUNTANDO A a[0] . De la misma forma : var = *( p + 3 ) asignará
30 a var , sin modificar el contenido de p .
En cambio la expresión :
var = *( p++ ) ;
podemos leerla como : asigne a var el valor de lo apuntado por p y LUEGO
incremente éste para que apunte al proximo elemento . Así en var quedaría
0 ( valor de a[0] ) y p apuntaría finalmente a a[1] . Si en vez de ésto hubieramos
preincrementado a p tendríamos :
var = *( ++p ) ;
la que puede leerse como : apunte con p al próximo elemento y asigne
a var con el valor de éste . En este caso var sería igualada a 10 ( a[1]
) y p quedaría apuntando al mismo .
En las dos operaciones anteriores los paréntesis son superfluos ya que al
analizarse los operadores de derecha a izquierda , daría lo mismo escribir
:
var = *p++ ; /* sintácticamente igual a var = *(p++) */
var = *++p ; /* " " " var = *(++p) */
3. ARITMETICA DE PUNTEROS La aritmética más frecuentemente
usada con punteros son las sencillas operaciones de asignación , incremento
ó decremento y dereferenciación . Todo otro tipo de aritmética con ellos
está prohibida ó es de uso peligroso ó poco transportable . Por ejemplo no
está permitido , sumar , restar , dividir , multiplicar , etc , dos apuntadores
entre sí . Lo cual si lo pensamos un poco es bastante lógico , ya que de
nada me serviría sumar dos direcciones de memoria , por ejemplo .
Otras operaciones estan permitidas , como la comparación de dos punteros
, por ejemplo ( punt1 == punt2 ) ó ( punt1 < punt2 ) sin embargo este
tipo de operaciones son potencialmente peligrosas , ya que con algunos modelos
de pointers pueden funcionar correctamente y con otros no .
4. PUNTEROS Y VARIABLES DINAMICAS Recordemos lo expresado
en capítulo 5 , sobre el ámbito ó existencia de las variables , la menos
duradera de ellas era la del tipo local a una función , ya que nacía y moría
con ésta . Sin embargo , esto es algo relativo , en cuanto a la función main()
, ya que sus variables locales ocuparán memoria durante toda la ejecución
del programa.
Supongamos un caso típico , debemos recibir una serie de datos de entrada
, digamos del tipo double , y debemos procesar según un determinado algoritmo
a aquellos que aparecen una ó más veces con el mismo valor .
Si no estamos seguros de cuantos datos van a ingresar a nuestro programa
, pondremos alguna limitación , suficientemente grande a los efectos de la
precisión requerida por el problema , digamos 5000 valores como máximo ,
debemos definir entonces un array de doubles capaz de albergar a cinco mil
de ellos , por lo que el mismo ocupará del orden de los 40 k de memoria .
Si definimos este array en main() , ese espacio de memoria permanecerá ocupado
hasta el fín del programa , aunque luego de aplicarle el algoritmo de cálculo
ya no lo necesitemos más , comprometiendo seriamente nuestra disponibilidad
de memoria para albergar a otras variables . Una solución posible sería definirlo
en una función llamada por main() que se ocupara de llenar el array con los
datos , procesarlos y finalmente devolviera algún tipo de resultado , borrando
con su retorno a la masiva variable de la memoria .
Sin embargo en C existe una forma más racional de utilizar nuestros recursos
de memoria de manera conservadora . Los programas ejecutables creados con
estos compiladores dividen la memoria disponible en varios segmentos , uno
para el código ( en lenguaje máquina ) , otro para albergar las variables
globales , otro para el stack ( a travez del cual se pasan argumentos y donde
residen las variables locales ) y finalmente un último segmento llamado memoria
de apilamiento ó amontonamiento ( Heap ) .
El Heap es la zona destinada a albergar a las variables dinámicas , es decir
aquellas que crecen ( en el sentido de ocupación de memoria ) y decrecen
a lo largo del programa , pudiendose crear y desaparecer (desalojando la
memoria que ocupaban) en cualquier momento de la ejecución .
Veamos cual sería la metodología para crearlas ; supongamos primero que queremos
ubicar un único dato en el Heap , definimos primero un puntero al tipo de
la variable deseada :
double *p ;
notemos que ésta declaración no crea lugar para la variable , sino que
asigna un lugar en la memoria para que posteriormente se guarde ahí la dirección
de aquella Para reservar una cantidad dada de bytes en el Heap , se efectua
una llamada a alguna de las funciones de Librería , dedicadas al manejo del
mismo . La más tradicional es malloc() ( su nombre deriva de memory allocation
) , a esta función se le dá como argumento la cantidad de bytes que se quiere
reservar , y nos devuelve un pointer apuntando a la primer posición de la
"pila" reservada . En caso que la función falle en su cometido ( el Heap
está lleno ) devolvera un puntero inicializado con NULL .
p = malloc(8) ;
acá hemos pedido 8 bytes ( los necesarios para albergar un double ) y
hemos asignado a p el retorno de la función , es decir la dirección en el
Heap de la memoria reservada.
Como es algo engorroso recordar el tamaño de cada tipo variable , agravado
por el hecho de que , si reservamos memoria de esta forma , el programa no
se ejecutará correctamente , si es compilado con otro compilador que asigne
una cantidad distinta de bytes a dicha variable , es más usual utilizar sizeof
, para indicar la cantidad de bytes requerida :
p = malloc( sizeof(double) ) ;
En caso de haber hecho previamente un uso intensivo del Heap , se debería averiguar si la reserva de lugar fué exitosa:
if( p == NULL )
rutina_de_error() ;
si no lo fué estas sentencias me derivan a la ejecución de una rutina
de error que tomará cuenta de este caso . Por supuesto podría combinar ambas
operaciones en una sola ,
if( ( p = malloc( sizeof(double) ) ) == NULL ) {
printf("no hay mas lugar en el Heap ..... Socorro !!" ) ;
exit(1) ;
}
se ha reemplazado aquí la rutina de error , por un mensaje y la terminación
del programa , por medio de exit() retornando un código de error .
Si ahora quisiera guardar en el Heap el resultado de alguna operación , sería tan directo como,
*p = a * ( b + 37 ) ;
y para recuperarlo , y asignarselo a otra variable bastaría con escribir :
var = *p ;
5. PUNTEROS A STRINGS No hay gran diferencia entre el
trato de punteros a arrays , y a strings , ya que estos dos últimos son entidades
de la misma clase . Sin embargo analicemos algunas particularidades . Así
como inicializamos un string con un grupo de caracteres terminados en '\0'
, podemos asignar al mismo un puntero :
p = "Esto es un string constante " ;
esta operación no implica haber copiado el texto , sino sólo que a p
se le ha asignado la dirección de memoria donde reside la "E" del texto .
A partir de ello podemos manejar a p como lo hemos hecho hasta ahora . Veamos
un ejemplo
#include
#define TEXTO1 "¿ Hola , como "
#define TEXTO2 "le va a Ud. ? "
main()
{
char palabra[20] , *p ;
int i ;
p = TEXTO1 ;
for( i = 0 ; ( palabra[i] = *p++ ) != '\0' ; i++ ) ;
p = TEXTO2 ;
printf("%s" , palabra ) ;
printf("%s" , p ) ;
return 0 ;
}
Definimos primero dos strings constantes TEXTO1 y TEXTO2 , luego
asignamos al puntero p la dirección del primero , y seguidamente en el FOR
copiamos el contenido de éste en el array palabra , observe que dicha operación
termina cuando el contenido de lo apuntado por p es el terminador del string
, luego asignamos a p la dirección de TEXTO2 y finalmente imprimimos ambos
strings , obteniendo una salida del tipo : " ¿ Hola , como le va a UD. ?
" ( espero que bien ) .
Reconozcamos que esto se podría haber escrito más compacto, si hubieramos
recordado que palabra tambien es un puntero y NULL es cero , así podemos
poner en vez del FOR
while( *palabra++ = *p++ ) ;
Vemos que aquí se ha agregado muy poco a lo ya sabido , sin embargo hay
un tipo de error muy frecuente , que podemos analizar , fíjese en el EJEMPLO
siguiente , ¿ ve algun problema ? .
( CON ERRORES )
#include
char *p , palabra[20] ;
printf("Escriba su nombre : ") ;
scanf("%s" , p ) ;
palabra = "¿ Como le va " ;
printf("%s%s" , palabra , p ) ;
}
Pues hay dos errores , a falta de uno , el primero ya fue analizado antes
, la expresión scanf("%s" , p ) es correcta pero , el error implícito es
no haber inicializado al puntero p , el cual sólo fué definido , pero aun
no apunta a ningun lado válido . El segundo error está dado por la expresión
: palabra = " ¿ Como le va " ; ( también visto anteriormente ) ya que el
nombre del array es una constante y no puede ser asignado a otro valor .
¿Como lo escribiríamos para que funcione correctamente ?
(CORRECTO)
#include
#include
#include
char *p , palabra[20] ;
p = (char *)malloc(sizeof(char)128) ;
printf("Escriba su nombre : ") ;
scanf("%s" , p ) ;
strcpy(palabra , "¿ Como le va " ) ;
printf("%s%s" , palabra , p ) ;
}
Observe que antes de scanf() se ha inicializado a p, mediante el retorno
de malloc() y a al array palabra se le copiado el string mediante la función
vista anteriormente strcpy().
Debemos aclarar también que, la secuencia de control %s en el printf() impone
enviar a la pantalla un string, estando éste apuntado por el argumento siguiente
al control, éste puede ser tanto el nombre de un array, como un puntero,
ya que ambos explicitan direcciones.
Una forma alternativa de resolverlo , sería:
( CORRECTO )
#include
main()
{
char p[20] , *palabra ;
printf("Escriba su nombre : ") ;
scanf("%s" , p ) ;
palabra = "¿ Como le va " ;
printf("%s%s" , palabra , p ) ;
}
Obsérvese , que es idéntico al primero , con la salvedad que se ha invertido
las declaraciones de las variables , ahora el puntero es palabra y el array
es p . Ambas soluciones son equivalentes y dependerá del resto del programa
, cual es la mejor elección .
6. ARRAYS DE PUNTEROS Es una práctica muy habitual ,
sobre todo cuando se tiene que tratar con strings de distinta longitud ,
generar array cuyos elementos son punteros , que albergarán las direcciones
de dichos strings.
Si imaginamos a un puntero como una flecha , un array de ellos equivaldría a un carcaj indio lleno de aquellas .
Asi como:
char *flecha;
definía a un puntero a un caracter , la definición
char *carcaj[5];
implica un array de 5 punteros a caracteres .
INICIALIZACION DE ARRAYS DE PUNTEROS Los arrays de punteros pueden
ser inicializados de la misma forma que un array común , es decir dando los
valores de sus elementos , durante su definición , por ejemplo si quisieramos
tener un array donde el subíndice de los elementos coincidiera con el nombre
de los días de la semana , podríamos escribir :
char *dias[] = {
"número de día no válido" ,
"lunes" ,
"martes" ,
"miercoles" ,
"jueves" ,
"viernes" ,
"sabado" ,
"por fín es domingo"
}
Igual que antes, no es necesario en este caso indicar la cantidad de elementos
, ya que el compilador los calcula por la cantidad de términos dados en la
inicialización. Asi el elemento dias[0] será un puntero con la dirección
del primer string, dias[1], la del segundo, etc.
7. PUNTEROS A ESTRUCTURAS Los punteros pueden también
servir para el manejo de estructuras , y su alojamiento dinámico , pero tienen
además la propiedad de poder direccionar a los miembros de las mismas utilizando
un operador particular , el -> , (escrito con los símbolos "menos" seguido
por "mayor" ) .
Supongamos crear una estructura y luego asignar valores a sus miembros , por los métodos ya descriptos anteriormente :
struct conjunto {
int a ;
double b ;
char c[5] ;
} stconj ;
stconj.a = 10 ;
stconj.b = 1.15 ;
stconj.c[0] = 'A' ;
La forma de realizar lo mismo , mediante el uso de un puntero, sería la siguiente :
struct conjunto {
int a ;
double b ;
char c[5] ;
} *ptrconj ;
ptrconj = (struct conjunto *)malloc( sizeof( struct conjunto )) ;
ptrconj->a = 10 ;
ptrconj->b = 1.15 ;
ptrconj->c[0] = 'A' ;
En este caso vemos que antes de inicializar un elemento de la estructura
es necesario alojarla en la memoria mediante malloc(), observe atentamente
la instrucción: primero se indica que el puntero que devuelve la función
sea del tipo de apuntador a conjunto (ésto es sólo formal), y luego con sizeof
se le da como argumento las dimensiones en bytes de la estructura. Acá
se puede notar la ventaja del uso del typedef , para ahorrar tediosas repeticiones
de texto, y mejorar la legilibilidad de los listados; podríamos escribir:
typedef struct {
int a ;
double b ;
char c[5] ;
} conj ;
conj *ptrconj ;
ptrconj = ( conj *)malloc( sizeof( conj )) ;
Es muy importante acá , repasar la TABLA 13 del final del capítulo 3
, donde se indican las precedencias de los operadores , a fín de evitar comportamientos
no deseados , cuando se usan simultaneamente varios de ellos .
Ya que c es un array podemos escribir :
x = *ptrconj -> c ;
la duda acá es, si nos referimos al contenido apuntado por ptrconj ó por c.
Vemos en la tabla que, el operador -> es de mayor precedencia que la de
* (dereferenciación), por lo que, el resultado de la expresión es asignar
el valor apuntado por c, es decir el contenido de c[0] .
De la misma forma:
*ptrconj -> c++ ; incrementa el puntero c , haciendolo tener la direccion
de c[1] y luego extrae el valor de éste .
++ptrconj -> c ; incrementa el valor de c[0] .
En caso de duda , es conveniente el uso a discreción de paréntesis , para
saltar por sobre las , a veces complicadas , reglas que impone la precedencia
así , si queremos por ejemplo el valor de c[3] , la forma más clara de escribir
es:
*( ptrconj -> ( c + 4 ) ) ;
(Recuerde que c[3] es el CUARTO elemento del array ).
8. PUNTEROS Y FUNCIONES La relación entre los punteros
y las funciones , puede verse en tres casos distintos , podemos pasarle a
una función un puntero como argumento (por supuesto si su parámetro es un
puntero del mismo tipo ) , pueden devolver un puntero de cualquier tipo ,
como ya hemos visto con malloc() y calloc() , y es posible también apuntar
a la dirección de la función , en otras palabras , al código en vez de a
un dato.
PUNTEROS COMO PARAMETROS DE FUNCIONES . Supongamos que hemos declarado
una estructura , se puede pasar a una función como argumento , de la manera
que ya vimos anteriormente:
struct conjunto {
int a ;
double b ;
char c[5] ;
} datos ;
void una_funcion( struct conjunto datos );
Hicimos notar, en su momento, que en este caso la estructura se copiaba
en el stack y así era pasada a la función, con el peligro que esto implicaba,
si ella era muy masiva, de agotarlo.
Otra forma equivalente es utilizar un puntero a la estructura :
struct conjunto {
int a ;
double b ;
char c[5] ;
} *pdatos ;
void una_funcion( struct conjunto *pdatos ) ;
Con lo que sólo ocupo lugar en el stack para pasarle la dirección de la
misma. Luego en la función, como todos los miembros de la estructuras son
accesibles por medio del puntero, tengo pleno control de la misma.
Un ejemplo de funciones ya usadas que poseen como parámetros a punteros son:
scanf(puntero_a_string_de_control , punteros_a_variables) printf(puntero_a_string_de_control , variables )
En ambas vemos que los strings de control son , como no podría ser de
otro modo , punteros , es decir que los podríamos definir fuera de la función
y luego pasarselos a ellas :
p_control = "valor : %d " ;
printf( p_control , var ) ;
PUNTEROS COMO RESULTADO DE UNA FUNCION Las funciones que retornan
punteros son por lo general aquellas que modifican un argumento , que les
ha sido pasado por dirección ( por medio de un puntero ) , devolviendo un
puntero a dicho argumento modificado , ó las que reservan lugar en el Heap
para las variables dinámicas , retornando un puntero a dicho bloque de memoria
.
Así podremos declarar funciónes del tipo de:
char *funcion1( char * var1 ) ;
double *funcion2(int i , double j , char *k ) ;
struct item *funcion3( struct stock *puntst ) ;
El retorno de las mismas puede inicializar punteros del mismo tipo al
devuelto , ó distinto , por medio del uso del casting . Algunas funciones
, tales como malloc() y calloc() definen su retorno como punteros a void
:
void *malloc( int tamano ) ;
de esta forma al invocarlas , debemos indicar el tipo de puntero de deseamos
p = (double *)malloc( 64 ) ;


© Derechos Reservados, Copyright, DATA-2013, 1998-2020.

|