Documentos de Académico
Documentos de Profesional
Documentos de Cultura
I
“El único lugar donde el éxito viene antes del trabajo es en el diccionario.”
1. Concepto de puntero.
2. Operadores de punteros.
3. Paso por valor y paso por referencia.
4. Inicialización de punteros.
5. Aritmética de punteros.
6. Tipo void y punteros.
Objetivos:
Estudio Independiente
int N=4;
Observe que en la función scanf no pasamos como parámetro la variable N como en la función printf sino que
pasamos &N. La expresión &N, como veremos a continuación es un puntero a la variable N.
Desarrollo
En este tema se comenzará a estudiar una de las características más potentes del lenguaje de
programación C: el uso de los punteros.
A través del uso de punteros en C es posible simular el llamado de funciones por referencia, es decir,
de modo que las funciones puedan modificar el contenido de variables que están definidas fuera de la
función.
El concepto de puntero
Un puntero contiene una dirección de memoria. Cuando una variable contiene la dirección de otra variable se dice
que la primera variable apunta a la segunda, de ahí que se le llame también apuntadores a los punteros.
Las variables apuntadores contienen direcciones de memoria como sus valores. Por lo general, una variable
contiene directamente un valor específico. Sin embargo, un apuntador contiene la dirección de memoria de
una variable que, a su vez, contiene un valor específico. En este sentido, el nombre de una variable hace
referencia directa a un valor, y un apuntador hace referencia indirecta a un valor. Al proceso de hacer
referencia a un valor a través de un apuntador se le conoce comúnmente como indirección.
Declaración e inicialización de variable puntero.
Una variable puntero normalmente debe apuntar a otra variable de un tipo de dato determinado.
A continuación se declara una variable puntero a otra variable de tipo entero (int):
int *Ptr;
En este caso se está declarando una variable llamada Ptr que puede contener la dirección en la memoria de
cualquier variable del tipo entero.
Para declarar una variable puntero se indica el tipo de dato al que apuntará, y a continuación se emplea el
símbolo * seguido del nombre que se le dará a la variable puntero.
Los apuntadores deben ser inicializados cuando son declarados o en un enunciado de asignación antes de
ser utilizados.
Un apuntador puede ser inicializado con 0 (NULL) o con una dirección. Cuando un puntero contiene 0 (NULL)
se toma como convenio que no está apuntando a alguna variable. 0 es el único valor entero que puede ser
asignado a una variable de tipo puntero.
La forma general para declarar una variable puntero es: tipo *nombre; donde tipo es cualquier tipo válido de
C (también llamado tipo base) y nombre es el nombre de la variable puntero. Nota: A cada variable que se declara
como apuntador se le debe anteponer un asterisco (*).
Dpto. Automática, FIAB Ybrain Hernández López ybra@automatica.cujae.edu.cu
Programación I
“El único lugar donde el éxito viene antes del trabajo es en el diccionario.”
Una variable puntero normalmente debe apuntar a otra variable de un tipo de dato determinado.
Aunque no es un requerimiento, incluir las letras Ptr en los nombres de las variables apuntadores deja claro que
estas variables son apuntadores, y que deben tratarse de manera acorde. Los apuntadores deben ser inicializados
cuando son declarados o en un enunciado de asignación antes de ser utilizados.
Operadores de punteros
Existen dos operadores especiales de punteros: & y *. Estos dos operadores son monarios.
El operador & aplicado a una variable devuelve la dirección de la variable a la que se aplique. Por ejemplo si se
ejecutan las siguientes declaraciones:
int y = 5;
Se obtendrá una variable entera y cuyo valor inicial es 5, así como una variable yPtr cuyo valor inicial es la
dirección de la variable y. Esta última relación se suele indicar esquemáticamente como aparece en la figura
a continuación:
El operador de dirección también se puede aplicar a las variables puntero, por ejemplo, si además se ejecuta la
instrucción:
Entonces se obtendrá, además, una variable yPtrPtr puntero a puntero a entero, la cual contiene la dirección de
la variable yPtr, esquemáticamente:
El operador de indirección *
El símbolo * cuando se aplica a una variable puntero se le llama operador de indirección y devuelve el contenido
de la variable apuntada por el puntero.
Dpto. Automática, FIAB Ybrain Hernández López ybra@automatica.cujae.edu.cu
Programación I
“El único lugar donde el éxito viene antes del trabajo es en el diccionario.”
Continuando con el ejemplo anterior las siguientes instrucciones printf imprimirían el valor 5:
printf ( “%d\n”, *yPtr );
printf ( “%d\n”, **yPtrPtr );
Una expresión como **yPtrPtr se evalúa de la forma siguiente *(*yPtrPtr) y por ello también daría el contenido
de la variable y.
Por otra parte, cuando usamos el operador de indirección aplicado a una variable a la izquierda de una expresión
de asignación, es posible cambiar el contenido de dicha variable, por ejemplo
*yPtr = 6;
printf ( “%d\n”, *yPtr );
printf ( “%d\n”, **yPtrPtr );
Ahora ambas instrucciones printf mostrarán en la pantalla el valor 6.
La posibilidad de cambiar el contenido de una variable a través de un puntero es lo que permitirá definir
funciones que sean capaces de modificar variables que están declaradas fuera de ellas, es decir, simular
parámetros por referencia en C.
Ejemplos:
Realizar un programa que permita modificar el valor de una variable utilizando un puntero. El programa deberá
imprimir en pantalla las direcciones de memoria de la variable y del puntero.
{ &x = 8FC4:0FFE
px = &x; *px = 5 x = 5
Observaciones:
En el ejemplo anterior se observa que hay tres valores asociados a los punteros:
Inicialización de punteros
También un puntero se puede inicializar para que no apunte a ninguna dirección inicial, esto se hace utilizando la
constante simbólica NULL, que está definida como 0. En la práctica a la dirección de memoria 0 no es posible acceder
pues está reservada para uso exclusivo del BIOS/SO, por lo que este valor se usa para indicar que el puntero no
apunta a ninguna variable en particular.
char* C1 = NULL;
En C, todos los llamados de función son llamados por valor. Es decir, en C las funciones solamente pueden tener
parámetros por valor. Esto significa que al llamarse la función se maneja el parámetro como una variable local en la
cual se copia el valor que hemos pasado como argumento, de ahí que no sea posible modificar el contenido de una
variable declarada fura de la función sustituyéndola como argumento en un parámetro por valor.
Para que una función de C pueda modificar una variable que ha sido declarada fuera de ella, debemos pasar como
parámetro la dirección de ésta, es decir, el llamado por referencia se simula usando parámetros puntero por valor,
como en la función scanf. Resumiendo:
o En el caso de que necesitemos devolver más de un valor debemos utilizar este método.
Esta sería la primera aplicación de la utilización de punteros en que se pueden utilizar los apuntadores y el operador
de indirección para simular llamadas por referencia.
Cuando usted necesite modificar los argumentos de una función, debe pasar la dirección de las
variables y no su valor.
Esto se lleva a cabo utilizando el operador &
Luego utilizando el operador indirección (*) se puede modificar el valor de esa posición de memoria.
Ejemplo:
Defina una función en C qué al recibir el radio y altura de un cilindro permita devolver el área y volumen del mismo.
Escriba un programa principal que permita obtener el radio y la altura del cilindro de teclado e imprima en pantalla,
usando la función definida, el volumen y área de la figura.
¿Sería posible una versión del programa anterior usando solamente dos variables en el programa principal?
Ejemplo
Suponga que se quiere realizar una función que permite intercambiar los contenidos de dos variables flotantes
cualesquiera. A esta función se le denominará Swap (que se traduce al español como intercambio).
Si se le aplica la función Swap a estas dos variables el resultado que obtendríamos sería el mostrado en la
figura siguiente:
El algoritmo en seudocódigo para la función Swap es realmente sencillo, solo debemos tener en cuenta que
debemos usar una variable flotante extra para guardar temporalmente el valor de una de las variables, o sea:
{
float temp = x;
x = y;
y = temp;
}
La función anterior es correcta sintácticamente pero no lo es semánticamente ya que los parámetros x e y serían
parámetros por valor, por lo que los argumentos que se sustituyan por estos parámetros al llamar a la función
se copiarían en las variables x e y, las cuales son variables con alcance de función y que, por tanto, no tienen
nada que ver con las variables x e y que están declaradas fuera de la función. El resultado es que la función
anterior, al llamarla, no intercambia el contenido de las variables que se sustituyan como argumentos, solamente
intercambiaría el contenido de dos copias de esas variables, las cuales se destruyen una vez que termine el
llamado a la función.
La forma correcta de programar Swap es usando parámetros de tipo puntero a float en lugar de parámetros
de tipo float, o sea:
Observe un esquema paso a paso de lo que ocurre al hacer el llamado anterior a la función:
- Cuando comienza a ejecutarse la función se crean las variables locales ptrx y ptry cuyos valores iniciales
son la dirección de la variable x y de la variable y respectivamente, o sea:
- Al ejecutarse la instrucción
float temp = *ptrx;
Dpto. Automática, FIAB Ybrain Hernández López ybra@automatica.cujae.edu.cu
Programación I
“El único lugar donde el éxito viene antes del trabajo es en el diccionario.”
Se crea la variable local temp cuyo valor inicial es el contenido de la variable apuntada por ptrx, es decir, el
contenido de x, que es 2.5.
- Al ejecutarse la instrucción
*ptrx = *ptry;
Se asigna a la variable apuntada por ptrx el contenido de la variable apuntada por ptry, es decir, es como si se
asignara a x el valor de la variable y.
Se asigna a la variable apuntada por ptry el valor que está guardado en temp
Al retornar la función se destruyen las variables locales de la función y como consecuencia de la ejecución de
esta función quedan intercambiados los contenidos de las variables x e y, es decir:
Aritmética de punteros
Sean P una variable de tipo puntero a cualquier tipo T. Sobre P solo se pueden aplicar los operadores aritméticos de
suma y resta. La cantidad adicionada o restada al puntero es siempre multiplicada por el tamaño de la variable a la
que apunta P. Esto es:
Este comportamiento es muy utilizado en el trabajo con arreglos que estudiaremos en la clase próxima.
Aplicados a punteros tiene otro uso: los punteros a void son punteros genéricos que pueden apuntar a cualquier
objeto o tipo de dato. Pero los punteros a void no pueden ser referenciados (utilizando *) sin utilizar moldes, puesto
que el compilador no puede determinar el tamaño del objeto al que apunta el puntero. Vea el siguiente segmento de
código:
int x; float f;
*(int *) p = 2;
p = &f; /* p apunta a f */
*(float *) p = 1.1;
Conclusiones:
Se vio el concepto de puntero, que es una variable cuyo valor es una dirección de memoria donde se encuentra otra
variable, cómo declararlos, inicializarlos y la importancia. Se vieron varias aplicaciones de los punteros: paso por
paso por referencia y aritmética de punteros.
Dpto. Automática, FIAB Ybrain Hernández López ybra@automatica.cujae.edu.cu
Programación I
“El único lugar donde el éxito viene antes del trabajo es en el diccionario.”
Estudio Individual:
1. Estudio de la recursividad.
2. Variables estáticas. Uso.
Estudio de la recursividad.
Hasta ahora los programas que hemos estudiado llaman a otras funciones de forma "disciplinada" y jerárquica. Pero:
Una función recursiva es aquella que se llama a sí misma, ya sea directa o indirectamente a través de otra función:
Ej:
f1 ‐ f2‐ f1
El tema de recursividad es un tema complejo analizado con profundidad en cursos de ciencia de la computación. LT
Se trata en Capítulos del 5‐12
Conceptualmente la recursividad:
Es descomponer una tarea en tareas más simples del mismo tipo que la original.
La función recursiva solo resuelve el caso más simple devolviendo un resultado. A este caso más simple se le
llama CASO O CASOS BASE.
Si la función es llamada para resolver el caso BASE devuelve un resultado, de lo contrario, la función divide
el problema en dos partes, una que ya sabe cómo resolver, y otra que no sabe cómo ejecutar.
De esta manera el problema inicial complejo se resuelve:
1‐. Llamando a la función se divide el problema en un problema con solución conocida y una algo más simple
que el primero del mismo tipo .
2‐. La función llama a una copia de sí misma para que resuelva ahora el problema ligeramente más sencillo.
3‐. Se repite el paso 1 hasta que el problema desconocido se convierta de tanto dividirlo en más simples
cada vez en un problema con solución conocida caso base .
4‐. Cuando ya no es necesario seguir llamando a la función recursivamente, esta retorna el resultado de
resolver el problema a la copia que la llamó, y esta a su vez a la otra, y así hasta que se llega a la inicial, la cual,
ahora con la solución del problema, regresa el valor final.
Este análisis debe de realizarse antes de crear cualquier algoritmo. Sin tener el caso base, ni la descomposición del
problema original en problemas del mismo tipo, pero más simples NO se puede programar recursivamente.
Veremos Ejemplos simples y luego a lo largo del curso se verán otros ejemplos.
Ejemplo 1 de recursividad:
Como se sabe esto se denota como n! y se pronuncia "factorial de n". La expresión que lo resuelve es:
n* n‐1 * n‐2 * n‐3 ......*1 con 1! 1 y 0! definido como 1.
Ej: 5! 5*4*3*2*1 120.
Esta expresión puede ser reescrita de matemáticamente de forma recursiva:
n! n* n‐1 !
5! 5*4!
4! 4*3!
3! 3*2!
2! 2*1!
1! 1
o sea, 5! 5*4! 5*4*3! 5*4*3*2! 5*4*3*2*1! 5*4*3*2*1 120
Veamos finalmente la solución en C:
1‐. printf invoca a factorial 3 esperando como tipo de valor de regreso un entero.
a factorial 3 devuelve 3*factorial 2 1ra llamada recursiva
b factorial 2 devuelve 2*factorial 1 2da llamada recursiva
c factorial 1 devuelve 1 llegamos al caso base
d se regresa a la copia que llamó a factorial de 1.
e la copia factorial 2 devuelve 2*1 2; 2 es devuelto y se regresa a la copia que llamó a factorial de 2.
f la copia factorial 3 devuelve 3*2 6; 6 es devuelto y se regresa a la copia que llamó a factorial de 3.
¿Cuándo usar recursividad en la programación de nuestras funciones?
Es muy importante conocer cuando utilizar la programación recursiva pues no siempre es más eficiente ni es más
evidente llegar a un algoritmo recursivo. Algunas reflexiones importantes sobre este tema son:
1‐. Casi cualquier problema puede resolverse de forma iterativa y de forma recursiva.
2‐. Las dos implican repetición: en la variante iterativa se hace de forma explícita; la recursión lo consigue mediante
llamados sucesivos a la función.
3‐. Las dos involucran una prueba de terminación: la iteración termina cuando la condición de continuación del ciclo
es FALSE; la recursión termina cuando se llega al caso base.
4‐. Ambas pueden producir ciclos infinitos: la iteración si la condición de continuación del ciclo nunca es FALSE; la
recursión si nunca se llega al caso base.
5‐. La recursión tiene muchas desventajas: invoca de forma repetida al mecanismo y por tanto a la sobrecarga de
llamadas a la función. Esto puede resultar costoso en tiempo y en memoria cada llamada consume tiempo y cada
copia de la función de hecho de las variables de la función consume memoria .
6‐. La iteración por lo general ocurre dentro de una función y por tanto no ocurre la sobrecarga de llamadas repetidas
de función y asignación extra de memoria.
¿Cuándo y por qué escoger entonces la recursión?
1‐. Cuando el enfoque recursivo es más natural al problema y resulta en un algoritmo y un programa más fácil de
comprender y de depurar.
2‐. Cuando la solución iterativa no resulta aparente.
¿Cuándo evitarlo?
1‐. Cuando se requiere rendimiento. Las llamadas recursivas consumen tiempo y memoria adicional.
2‐. Cuando no sea evidente este tipo de solución.
Variables estáticas.
En clases anteriores vimos que cuando se declaran variables, el alcance de ellas queda definido por el contexto donde
son creadas, o sea, si se crean dentro de una función existen solo dentro de la función no se puede acceder a sus
atributos fuera de la función , y durante el tiempo de ejecución de la función se crean en memoria cuando comienza
la función y se destruyen al concluir la ejecución de la función . Lo mismo sucede si las variables se crean dentro de
un bloque identificados por .
En ocasiones, es conveniente indicarle al procesador que no elimine las variables locales, para así poder utilizar el
valor que ellas tienen en sucesivas llamadas a la función. En estos casos se debe utilizar una clase de almacenamiento
diferente a la que hemos estado utilizando hasta el momento por defecto llamada auto . La clase de almacenamiento
será static.
Esta clase de almacenamiento, que se define para cada variable en el momento en que se crea de la forma: clase
almacenamiento tipo nombre_variable, le indica al compilador que esa variable cumplirá las siguientes reglas:
Supongamos que en el ejemplo anterior queremos ver la cantidad de llamadas recursivas que se realizan a la función
factorial. El código habría que modificarlo solo en la función factorial de manera que quede:
int factorial int x
static int i 1;
printf "Llamada recursiva no.%d \n",i ;
if x 1
return 1;
else
return x * factorial x‐1 ;
Dpto. Automática, FIAB Ybrain Hernández López ybra@automatica.cujae.edu.cu
Programación I
“El único lugar donde el éxito viene antes del trabajo es en el diccionario.”
Por ejemplo, si ahora ejecutamos el programa para calcular el valor de 5, nos imprimirá:
Si no hubiéramos especificado que i es estática, entonces la ejecución hubiera dado como resultado: