Document

Anuncio
9. APUNTADORES EN C
9.1. MOTIVACIÓN
Cuando nos enfrentamos a un problema que tenemos planeado resolver con un programa escrito
en C, empezamos modelando dicho problema (i.e. haciendo explicitos cuales son los datos de
entrada y salida del problema y como los representaremos en el computador). Este modelaje,
hasta el momento a supuesto que el tipo de información y la cantidad de está, es conocida. Este
no es el caso en muchos programas, por ejemplo en un programa para calcular la mediana de las
calificaciones de un curso, se requieren todas las calificaciones; la cantidad de calificaciones
dependen de la cantidad de estudiantes y este puede ser un dato desconocido al desarrollar el
programa. Muy probablemente la estructura de datos escogida para almacenar las calificaciones
sería un vector de flotantes. Cuantos elementos debe tener el vector? 30? 50?. No faltará el
práctico que declare un vector de 1000 posiciones, pero si tal programa se emplea para un curso
de 15 estudiantes, habrá 985 posiciones del vector desperdiciadas.
Una de las aplicaciones de los apuntadores es permitir manejar estructuras que pueden cambiar de
tamaño en tiempo de ejecución (es decir mientras el usuario final esta empleando el programa),
en el ejemplo anterior, puede emplearse un apuntador y memoria dinámica para crear un vector
con tantas posiciones como lo desee el usuario final.
Los apuntadores también facilitarán la creación y mantenimiento de estructuras mucho más
complejas que vectores o matrices, como es el caso de listas, arboles y grafos.
Otra aplicación de apuntadores será permitir variables por referencia en las funciones. Es decir
permiten que una función modifique el contenido de las variables que recibe como parámetros.
Además dada la intima relación entre vectores y apuntadores, las operaciones con vectores, como
se verá, pueden también hacerse con apuntadores y en muchos casos con más facilidad.
La total comprensión del uso de apuntadores requiere un conocimiento más profundo del
hardware y del sistema operativo, la sección 9.2. está dedicada a tratar algunos conceptos claves
de estos temas. Despúes en la sección 9.3. se tratará la sintaxis para declarar y usar apuntadores y
se explicará su semántica. Finalmente en la sección 9.4. se pondrá en práctica la teoría, y se
darán varias aplicaciones, entre ellas el uso de memoria dinamica.
9.2 LAS VARIABLES EN LA MÁQUINA
Para comprender que es y como se usa un apuntador es útil comprender como se representan las
variables en la memoria del computador.
Primero que todo recordemos que la memoria del computador
puede representarse como un conjunto de celdas (o mejor un vector
de celdas), cada una de las cuales puede almacenar un byte1.
0
1
.
.
.
Para el caso mostrado en la Figura 1 se tiene una memoria que
podrá almacenar m bytes
m
Figura 1
Supongamos que hacemos el siguiente programa:
int
main() {
int a;
a=0;
return a;
}
Cuando se ejecute este programa, se creará una zona de memoria dedicada a la variable a, en la
figura 2.1. se muestra esta zona, se trata de las posiciones i e i+12. Cuando se escriban datos en
la variable a, se escribirá en la zona
0
0
reservada para a, y cuando se extraiga el
1
1
valor de a, se sacará el dato almacenado
.
en esa zona.
.
i
75
Zona
i
0
Ya es más claro porque las variables
i+1
23
de a
i+1
0
deben ser inicializadas siempre?
.
Que había en la zona de la variable a
.
antes de que esta fuese reservada? No
m
m
podemos saberlo, podría ser 0, 8, 1043,
Figura 2.1.
Figura 2.2.
cualquier número dejado allí por otros
programas, o por el sistema operativo.
El programa de ejemplo, después llenará el espacio reservado para a, con el valor 0, como se
muestra en la figura 2.2. Y finalmente recuperará el contenido de la variable a (es decir la
información que hay en la zona de a) y lo retornará. Como el valor contenido en la zona de a es
0, retornará 0.
1
Un byte es una agrupación de 8 bits. Un bit puede pensarse como un 0 o un 1.
Vale la pena aclarar que las direcciones de memoria no son necesariamente números enteros. La expresióni+1, se
refiere a la posición de memoria que le sigue a la posición i.
2
Nota Histórica
Ahora surgen nuevas preguntas:
•
•
•
•
¿ Como se escoge el sitio donde quedará
cada variable en memoria?
¿ De que tamaño es la zona de memoria
reservada?
¿ Si el computador sólo trabaja con unos
y ceros como se puede trabajar con
variables de tipo entero, flotante, carácter
y vector?
¿ Como se asegura que la zona de
memoria reservada para una variable no
será escrita por otra variable del
programa o por el programa mismo?
Cuando comenzó el auge de los computadores
personales, a principios de la década de los 80,
eran comunes computadores cuyas memorias
no sobrepasaban los 64 KB (1KB equivale a
1024 bytes), tal era el caso del ZX Spectrum,
Commodore 64, Color Computer I y II, etc.
El procesador 8088 de Intel permitía
direccionar hasta 1 MB de memoria (1MB
equivale a 1024KB). Esto fue aprovechado
por IBM que lanzó sus primeros PC con
512KB en RAM. Tiempo después aparecieron
nuevos procesadores para microcomputadores,
que permitían direccionar más memoria.
9.2.1. Representación de datos
Ya sabemos que las variables son en realidad zonas de memoria, asignar un valor a una variable
equivale a llenar con el valor dado la zona de la variable, y recuperar el valor de una variable
equivale a sacarlo de la zona de memoria.
Ahora se tratará de responder la pregunta: ¿Como almacena el computador un entero, un flotante,
un carácter? o en otras palabras ¿Como representa los datos el computador?
Los computadores actuales trabajan con 2 niveles de voltaje (e.g. +5V y 0V), uno es el nivel alto
y otro el nivel bajo, al nivel alto de voltaje se le asocia con el 1 y al nivel bajo con el 0. Las
memorias de los computadores se basan en circuitos que pueden almacenar uno de estos niveles
de voltaje3 (es decir pueden almacenar un bit). Así mismo toda la transmisión de información por
los buses y cables, se presenta como señales de voltaje (nivel alto y nivel bajo). En resumen toda
la información que puede trabajar un computador debe en últimas representarse como secuencias
de unos (1) y ceros (0).
Como sólo tenemos disponibles los dígitos 0 y 1, para representar la información empleamos
números en base 2. Por ejemplo el entero 6 (escrito en base 10), se escribe como 110 en base 2.
Esta secuencia de dígitos (110) si puede ser almacenada en la memoria del computador
empleando 3 flip-flops o su equivalente: El primero almacena un nivel alto de voltaje, el segundo
un nivel alto, y el tercero un nivel bajo.
Para representar caracteres se emplea la tabla ASCII (Ver el Apendice B), de forma que todo
carácter se representa como un número entero y este a su vez se expresa en base 2. Por ejemplo
el valor ASCII asociado a la letra ‘C’ es 67, entonces en el computador se almacenará como la
secuencia de bits: 1000011.
3
Un Flip-Flop es una componente electrónico que permite almacenar un bit, ya sea 1 (nivel alto de voltaje) o 0
(nivel bajo)
Ya se explico como se representan los enteros positivos. Para representar enteros tanto positivos
como negativos se emplea uno de los bits del número que es 0 si el signo es positivo y 1 si es un
entero negativo (a este bit se le llama bit de signo). Además se trabaja en complemento a 2
(puede consultar la bibliografía para saber en que consiste).
La representación de números flotantes es un poco más complicada, la mayoría de computadores
se han acogido a un estándar definido por la IEEE. Cada número flotante se escribe empleando
notación científica, como un dígito, con una expansión decimal truncada (es decir sólo se toma
cierta cantidad de dígitos) y un exponente. (e.g. 54.32 se escribe como 5.432E-1). El lector
interesado puede remitirse a la bibliografía.
9.2.2. Tamaño de los tipos de datos
Las secuencias de unos y ceros que puede almacenar un computador, se agrupan en bytes (ocho
(8) bits forman un (1) byte).
Como sabemos cuando se declara una variable de tipo entero, se reserva un espacio en la
memoria del computador, para mantener el valor de tal variable. El tamaño de la zona reservada,
depende del tipo de variable que hayamos declarado y del computador en el cual este
ejecutándose el programa. Este tamaño reservado limita los valores que puede tomar la variable.
Por ejemplo si se reservan 2 bytes para almacenar números enteros, podrá mantener secuencias
de a lo sumo 16 unos y ceros, es decir podrá representar a lo sumo 65536 valores diferentes (el
primero corresponde a la secuencia 0000000000000000 y el último corresponde a la secuencia
1111111111111111).
Como se dijo este tamaño depende de la máquina en la que se este ejecutando el programa, del
compilador y del tipo, algunos tamaños típicos se listan en la tabla 14
Maquina
PC/BC++
DEC PDP-11
Honeywell 6000
IBM 370
int
float
char
16 bits
16 bits
36 bits
32 bits
32 bits
32 bits
36 bits
32 bits
8 bits
8 bits
9 bits
8 bits
Tabla 1
long int
double
32 bits
32 bits
36 bits
32 bits
64 bits
64 bits
72 bits
64 bits
9.2.3. Duración de variables en C
En los programas realizados hasta ahora se ha visto que las variables declaradas sólo existen al
interior del bloque donde fueron declaradas y sólo pueden usarse allí, excepto las variables
globales cuya existencia se asegura a lo largo de todo el programa y pueden ser usada desde
cualquier parte. Esto se debe al sitio donde C reserva la zonas de memoria para las variables.
Para las variables globales se reserva una zona de memoria destinada con ese propósito, que
ninguna otra parte del programa emplea, y que puede ser accedida por cualquier función del
programa. Por el contrario para las variables locales, se reserva una zona temporal que sólo
existe mientras que la función donde se declaró la variable esté ejecutándose.
4
Tomado de [2]. Pag.36
Este comportamiento puede ser modificado empleando los prefijos static, auto y register
al declarar variables. Para conocer como afectan la duración de las variables, puede referirse a la
bibliografía.
PREGUNTAS
1. Escriba en base 2 los siguientes enteros (que están escritos en base 10):
a) 7
b) 8
c) 45
d) 192
e) 2334
2. Cuantos bytes se requieren para almacenar un flotante en un PC?
3. Cuantos valores distintos podrá almacenar una variable tipo carácter en una máquina IBM PC?
4. Cuantos enteros distintos podrá representarse con una variable tipo long int en una máquina
Honeywell 6000?
5. a) Explique como se declaran variables globales y como pueden usarse. b) Que diferencias
hay con respecto a variables locales?
6. ¿Que capacidad de memoria RAM tienen los supercomputadores?
9.3. ¿QUE ES UN APUNTADOR?
La discusión desarrollada en la sección 9.2 pretende dar algunas nociones básicas que permitirán
entender con mayor facilidad el concepto de apuntador.
int
main () {
int a;
int *ap;
a=0;
ap=&a;
*ap=1;
return a;
}
El ejemplo mostrado es un programa muy sencillo que ilustra el
uso de apuntadores. Primero se declara una variable a de tipo entero
y una variable ap tipo apuntador a entero. En memoria se reserva un
espacio para la variable a (supongamos que estamos trabajando en
una máquina 80x86 bajo DOS con BC++, entonces serán 2 bytes) y
otro para el apuntador (en maquinas 80x86 bajo DOS con BC++
serán 4 bytes), este proceso se ilustra en la figura 3.1.
La variable a tiene asociados los bytes i e i+1, y la variable ap tiene
asignados los bytes j, j+1,j+2 y j+3.
Cuando se ejecute la asignación a=0, las posiciones i e i+1 obtendrán el valor 0 (sus 16 bits
estarán en 0).
El operador & es un operador unario prefijo, que se aplica
sobre variables y retorna la dirección de la variable que opera
(esto es la posición en memoria donde comienza la zona
reservada para la variable), por ejemplo &a será la dirección de
la variable a, en el caso del ejemplo será i.
Las direcciones, no se consideran como enteros ni como
flotantes ni como caracteres en C. Por esta razón no puede
asignarse una dirección a una variable de alguno de estos tipos.
En el programa de ejemplo se hace la asignación ap=&a, es
decir asignamos la dirección de a a la variable ap.
Esta
asignación si es valida porque ap está declarado como
apuntador a entero. Cuando se realice la asignación, el espacio
en memoria de ap contendrá la dirección i.
0
1
.
.
i
j
a
.
.
ap
m
Figura 3.1.
La utilidad de los apuntadores es que permiten referirse directamente a direcciones de memoria.
En nuestro ejemplo ap podrá referirse a la dirección i (que corresponde a la zona de memoria de
la variable a). Para referirse a la dirección de memoria contenida en un apuntador se usa el
operador unario prefijo *. Así en la asignación *ap=1, estamos asignando el entero 1 a la
dirección apuntada por ap, es decir asignamos 1 a la zona de memoria iniciada en la dirección i.
Como efecto de esta operación el valor de la variable a cambia de 0 a 1 (precisamente porque
modificamos la zona reservada a la variable a).
Cuando se ejecute la instrucción return a, el programa terminará y retornará el valor 1.
9.3.1. Declaración de apuntadores
La declaración de apuntadores puede realizarse en cualquier parte del programa donde sea valida
la declaración de variables. La sintaxis es:
<tipo> *<identificador>;
Donde <tipo> es un tipo valido en C (e.g. int, long
<identificador> es un nombre de variable válido.
int, double, etc.), e
<tipo> será el tipo de datos a los cuales apuntará la variable de nombre <identificador>.
Algunos ejemplos de declaraciones correctas son:
int *ap_a_entero;
char *dcar;
unsigned long int *ap_a_numerote;
void *apuntador_generico;
La variable dcar será de tipo apuntador a carácter, esto significa que contendrá la dirección
donde comienza un carácter. La variable ap_a_numerote podrá contener la dirección donde
comienza un entero largo sin signo.
9.3.2. Asignación de apuntadores
A una variable de tipo apuntador sólo pueden asignársele una dirección de memoria. Para obtener
la dirección de una variable (inclusive de un apuntador), se emplea el operador & a la izquierda
del nombre de la variable.5
El operador & sólo puede aplicarse a variables y a elementos de un arreglo, no es valido &3 o &
(x+1). Tampoco puede aplicarse este operador a variables declaradas como register.
La dirección de una variable no puede ser asignada, el siguiente ejemplo muestra que NO es
valido con el operador &:
int a,b;
&a=&b;
A una variable tipo apuntador puede asignársele el valor de otra variable del mismo tipo como se
ejemplifica a continuación:
double d;
double *pd1;
double *pd2;
pd1=&d;
5
También puede asignarse otras direcciones de memoria, pero debe conocerse el modo de direccionamiento de la
máquina y tener grandes precauciones en el uso de tales apuntadores.
pd2=pd1;
El operador & tiene mayor precedencia que los operadores aritméticos.
A una variable de tipo apuntador, sólo pueden asignársele direcciones de memoria, la única
excepción es el entero 0. El valor 0 es tratado de forma especial con apuntadores, cuando se le
asigna 0 a un apuntador estamos diciendo que tal variable no está apuntando a zona alguna de
memoria.
En la librería stddef.h se declara la constante NULL con el valor 0, este es el símbolo que
emplearemos para referirnos a apuntadores que no están direccionando alguna zona de memoria.
A continuación se muestra un ejemplo de su uso:
int *api;/* !!Peligro. apint apunta a algun sitio desconocido */
api=NULL; /* Ahora api no apunta a zona alguna */
9.3.3. Uso de apuntadores
Para referirse a la posición de memoria contenida en un apuntador se usa el operador * a la
izquierda del apuntador.
A diferencia del operador &, este operador si puede colocarse al lado izquierdo de una asignación
(así como al lado derecho). Por ejemplo:
char c;
char *pc;
pc=&c;
*pc=‘c’;
c=*pc+2;
El operador * sólo puede aplicarse a variables de tipo apuntador. El siguiente ejemplo muestra
un empleo incorrecto de este operador
int c;
*c=0;
Este operador tiene mayor precedencia que los operadores aritméticos.
9.3.4. Operaciones entre apuntadores
Ya se explico que no pueden asignarse enteros a variables de tipo apuntador, ni direcciones a
variables de tipo entero. Aún así hay varias operaciones en las que pueden emplearse enteros y
apuntadores, estas son adición (+) y resta (-).
Veamos un ejemplo
float f[5];
float *apf1;
float *apf2;
int d;
apf1=&(f[0]);
apf2=apf1;
apf1=apf1+3;
apf2=apf1-2;
d=apf1-apf2;
En este ejemplo se declaran dos apuntadores a flotantes, un vector
para 5 flotantes y un entero.
Primero se asigna la dirección del comienzo del vector f al
apuntador apf1 .(la asignación también pudo haberse hecho con
apf1=f;) Después esa misma dirección es asignada a la variable
apf2.
Analicemos ahora la asignación apf1=apf1+3. La expresión
apf1+3 será la dirección de apf1 incrementada en tres posiciones
(teniendo en cuenta que apf1 apunta a flotantes), es decir equivale a la dirección del cuarto
flotante del vector f (porque apf1 apunta al primero). La nueva dirección así calculada se
asigna a apf1.
En la instrucción apf2=apf1-2, asignamos a la variable apf2 la dirección del segundo flotante
del vector f.
Finalmente en la instrucción d=apf1-apf2 obtenemos la distancia (en flotantes) entre los
apuntadores apf1 y apf2, esto es la cantidad de flotantes que pueden ponerse entre ambas
direcciones, para este caso particular d obtendrá el valor 2.
Por ahora no es tan importante que comprenda el ejemplo en su totalidad pero será conveniente
que vuelva a este ejemplo después de ver la utilidad de los operadores + y - al trabajar con
vectores.
9.3.5. Representación gráfica de apuntadores
Para referirnos a vectores, es usualmente más cómodo emplear una gráfica como la siguiente:
v1:
...
0
1 2
n-1
Cuando se trata de apuntadores también suele emplearse una representación gráfica, se trata de
una celda de la cual parte una flecha. El otro extremo de la flecha se ubica en otra celda, la cual
representa la variable (o zona de memoria) que está siendo apuntada.
Para ejemplificar como se usa esta representación gráfica retomaremos el ejemplo 1 de la sección
9.3. Para cada operación importante del programa (con respecto a las variables a y ap) se hacen
los diagramas correspondientes.
int
main () {
int a;
int *ap;
a=0;
ap=&a;
*ap=1;
return a;
}
2.
1.
ap
a
ap
0
a
3.
ap
0
a
ap
1
a
4.
Empleando estos esquemas, se pretende ocultar la forma real de
operación de los apuntadores (es decir a nivel de memoria).
PREGUNTAS
1. Supongamos que a y b son variables de tipo entero, cual será el valor final de b después de
ejecutar las siguientes instrucciones?
a=a*a;
b=*(&a);
2. La siguiente declaración es valida:
int **app;
¿De que tipo es la variable app? ¿Como puede asignarse un valor a esta variable? ¿Cómo puede
referenciarse memoria con ella?
3. ¿Qué función conoce que reciba un apuntador como uno de sus parámetros? (Ayuda: repase
las funciones de la librería stdio.h) ¿Cómo cree que es el encabezado (prototipo) de esa
función?
4. En el ejemplo de la sección 9.3.3. ¿Cual es el valor final de *pc y el de c?
5. ¿Es el siguiente programa correcto? ¿Cual es el valor final de a?
int a;
int *b;
a=2;
b=&a;
*b=(*b)*(*b);
6. Haga un diagrama de la memoria y explique paso a paso lo que ocurre al ejecutar el programa
de ejemplo de la sección 9.3.4.
7. ¿Porque cree que se considera peligroso el uso de apuntadores? ¿Que puede ocurrir con un
apuntador no inicializado?
8. Como se representa gráficamente el apuntador a apuntador del ejercicio 2?
9.4. APLICACIONES DE APUNTADORES
9.4.1. Variables por referencia
En PASCAL pueden pasarse argumentos a funciones o procedimientos por valor o por referencia
(Empleando la palabra reservada VAR en los parámetros del encabezado de la función, se pasan
variables por referencia). En C por el contrario siempre se pasan parámetros por valor, es decir
que una función no puede modificar globalmente el valor de sus parámetros. Los cambios que
haga a sus parámetros, sólo serán locales a la función, una vez, esta termine y la función
llamadora retome el control, el valor de los argumentos pasados será el mismo.
Sin
embargo
muchas
veces
requerimos
que
una
función
modifique el valor de sus parámetros
en el resto del programa.
Los
apuntadores dan un medio para lograr
esto: podemos hacer funciones que
reciban como parámetros apuntadores
a los valores y no los valores mismos.
Una función que requiere modificar
sus parámetros, es la función que
intercambia el valor de dos variables.
No podemos hacer una función que
reciba dos enteros e intercambie sus
valores, pero si una que reciba dos
apuntadores a enteros e intercambie
los valores referenciados por los
apuntadores.
#include <stdio.h>
void intercambia(int *x, int *y);
/* Intercambia los valores de x e y */
int
main () {
int a,b;
a=100;
b=200;
intercambia(&a,&b);
printf(“a=%i, b=%i”,a,b);
}
void
intercambia(int *x, int *y) {
int t=*x;
*x=*y;
*y=t;
}
Vale la pena aclarar que los
apuntadores son tratados como tipos en C, por esta razón es posible declarar funciones que
retornen apuntadores.
9.4.2. Vectores y apuntadores
Los vectores en memoria son zonas de memoria consecutiva. Así un vector de 20 flotantes, en
memoria corresponde a una zona de memoria donde hay espacio para 20 flotantes (en el caso del
IBM PC, esto equivale a 20*4 = 80 bytes).
Un vector, como buena zona de memoria, tiene una dirección donde comienza, es posible tener
un apuntador con tal dirección. Aún más es posible referenciar por medio de apuntadores,
cualquier posición del vector.
Estudiemos en detalle el ejemplo presentado: Primero reservamos espacio para un contador
(cont), una zona capaz de almacenar 20 flotantes de doble precisión consecutivos (vec), y una
para almacenar una dirección (pvec). Después a pvec le asignamos la dirección de la primera
posición del vector vec (esto es la posición cuyo índice
es 0). El estado de las variables puede representarse
gráficamente así:
int cont;
double vec[20];
double *pvec;
pvec=&vec[0];
cont=0;
while (cont<20) {
*pvec=(double)cont;
cont++;
pvec=pvec+1;
}
cont 0
pvec
vec :
? ? ? ... ? ?
0 1 2
19
La variable cont es inicializada en 0, y entramos en un ciclo que se repetirá 20 veces. En cada
iteración le asignamos al número apuntado por pvec el valor de cont. Note que para esta
asignación hacemos un casting o conversión de tipo. Convertimos el valor de tipo int
almacenado en cont, al mismo valor pero con tipo double.
Empecemos con la primera iteración: cont tiene el valor 0, entonces en la posición 0 de vec
almacenamos un 0, después incrementamos el valor de cont (queda en 1), y así mismo
incrementamos en uno el valor de pvec. Este incremento, como se explicó en la sección 9.3.4,
hace que la nueva dirección que pvec contiene sea la de un dato delante del que apuntaba, es
decir la del double siguiente. En este caso el siguiente dato corresponde a la posición 1 del
vector vec. Después de la primera iteración el estado de las variables será:
pvec
cont 1
vec :
0 ? ? ... ? ?
0 1 2
19
Otra forma de hacer tal incremento es empleando el operador ++ prefijo o postfijo: pvec++ o
++pvec.
Después de la segunda iteración:
pvec
vec :
cont 2
0 1 ? ... ? ?
0 1 2
19
y finalmente después de la vigésima:
pvec
vec :
cont 20
0 1 2 . . . 18 19
0 1 2
18 19
Se recomienda volver ahora a la sección 9.3.4. y repasar el ejemplo y los operadores +, y - con
apuntadores.
Note que después del ciclo el apuntador pvec, queda apuntando a una zona de memoria que no
hace parte del vector vec.
En general las operaciones sobre vectores con apuntadores suelen abreviar aún más los
programas y eventualmente volverlos más eficientes, pero esto se paga con dificultad para leer y
entender los programas.
Para completar, C internamente trata los vectores como apuntadores.
vector
Cuando declaramos un
int vi[10];
C entiende que vi será un apuntador a enteros, lo pone a apuntar a una zona de memoria con
espacio para 10 enteros y no permite que el programador cambie la dirección a la que vi apunta.
La primera ventaja radica en poder tratar la variable vi como un apuntador (excepto que no
podemos cambiar su valor), podemos asignar directamente el valor de vi a otro apuntador a
entero:
int *pvi=vi;
Cuando se referencia alguna posición del vector vi, por ejemplo la posición 5:
vi[5]=0;
C traduce y entiende:
*(vi+5)=0;
que significa el sexto entero a partir del apuntado por vi (lo mismo que vi[5]).
En los encabezados de funciones, hemos recibido vectores sin especificar su tamaño, como en la
siguiente caso:
int strlen(char s[]);
Esta declaración es equivalente a:
int strlen(char *s);
sólo que en el primer caso, al interior de la función no podrá cambiarse el valor de la variable s,
mientras que en el segundo caso si.
Ahora ilustraremos el uso de apuntadores para manejo de cadenas:
int
strlen(char *s) {
/* PRE: Recibe una cadena */
char *p;
for (p=s;*p!=‘\0’;p++);
return p-s;
/* POST: Retorna la longitud de la cadena recibida */
}
En el ejemplo anterior, podemos reemplazar la condición del for (i.e *p!=‘\0’), simplemente
por *p. Funciona porque ‘\0’ es el caracter cuyo valor ASCII es 0. Entonces si vale *p==‘\0’
es porque *p tiene el valor ASCII 0, y por tanto *p!=‘\0’ es falso. (Recuerde que en C, el
entero 0 equivale a falso y cualquier otra valor entero es verdadero)
char *strchr(char *s,char c) {
/* PRE: Recibe una cadena s y un caracter c */
for (;*s!=‘\0’ && *s!=c;s++);
if (*s==‘\0) {
return NULL;
}
return s;
/* POST: Retorna un apuntador al primer sitio dentro de la
cadena donde este el caracter c.
En caso de no estar retorna NULL */
}
De la función anterior es importante destacar que retorna un apuntador a caracter, que emplea la
constante NULL para indicar que no encontró el caracter c dentro de la cadena s. Otra
característica importante es que no requiere variables globales, emplea al parámetro s, para
recorrer la cadena. Esta función ilustra plenamente la simplificación que pueden tener las
funciones cuando se emplean apuntadores, pero también muestra lo complicada que puede ser su
lectura.
En general los parámetros de las funciones de las librerías string.h, son apuntadores.
Volvamos a la discusión iniciada al final de la sección anterior: Aritmética de apuntadores, para
ello ilustraremos con ejemplos las operaciones permitidas:
int vent[20];
int *ape1,*ape2;
ape1=vent;
ape2=ape1+5; /* Suma de apuntador con entero */
ape1=ape2-5; /* Resta de apuntador con entero */
vent[0]=ape2-ap1; /* Resta de un apuntador con otro apuntador */
ape1++; /* Incremento */
*ape1=ape1<ape2;
/* También es valida la comparación de apuntadores */
/* cando ambos apuntan a elementos de un mismo vector */
/* Dara verdadero y lo asignará a vent[0] */
*ape2=ape1>ape2;
SISTEMA OPERATIVO
9.4.3. Memoria dinámica
La memoria es un importante
recurso del computador, es
necesario administrarla de
alguna forma para asegurar
que no se desperdicie o
malgaste.
Una de las
funciones de un sistema
operativo es administrar la
memoria.
Cuando se ejecuta un
programa,
el
sistema
operativo le asigna varias
zonas de memoria para que
“Un sistema operativo (SO) puede ser contemplado como una
colección organizada de extensiones software del hardware,
consistente en rutinas de control que hacen funcionar un
computador y proporcionan un entorno para la ejecución de los
programas. Otros programas se apoyan en las facilidades
proporcionadas por el sistema operativo para obtener acceso a
los recursos del sistema informático, tales como archivos y
dispositivos de entrada/salida (E/S). Los programas invocan
generalmente los servicios del sistema operativo por medio de
llamadas al sistema operativo. Además los usuarios pueden
interactuar con el sistema operativo directamente por medio de
órdenes del sistema operativo. En cualquier caso, el sistema
operativo actúa como interfaz entre los usuarios y el hardware
de un sistema informático.
Internamente, un sistema operativo actúa como gestor de los
recursos del sistema informático, tales como el procesador, la
memoria, los archivos y los dispositivos de E/S.” [4]
las emplee, se tratan de una zona para los datos del programa, una zona para el programa mismo
y otra para el control de la ejecución y datos temporales.
Cada programa debe usar sólo la memoria que le ha sido asignada, de no ser así un programa
inquieto podría leer información de otras zonas de memoria o aún peor escribir en partes que
pertenecen al sistema operativo o a otros programas.
El sistema operativo DOS es especialmente frágil en la administración de memoria, a diferencia
de otros sistemas como UNIX o Windows NT, donde el sistema vela para que ningún programa
(excepto los designados en el kernel) escriba o lea de zonas de memoria que no le corresponden.
Una vez se termina la ejecución de un programa, el sistema operativo debe encargarse de
“liberar” la memoria que había asignado para ese programa, y de esta forma hacerla disponible
para otros programas.
Puede ocurrir que un programa requiera más memoria de la que el sistema operativo le asignó al
iniciar la ejecución, esto puede hacerse desde C empleando varias funciones para administración
de memoria. Algunas de estas funciones están declaradas en la librería stdio.h y otras en la
librería alloc.h:
void
void
void
void
*malloc(int size);
free(void *block);
*calloc(int nitems, int size);
*realloc(void *block, size_t size);
Por ahora nos dedicaremos a las funciones malloc y free. Con la función malloc, un
programa puede pedir una cantidad determinada de memoria, el compromiso que adquiere
entonces es liberar esa memoria antes de terminar la ejecución con la función free.
ADVERTENCIA
Es muy importante liberar la memoria que se solicite, porque de lo contrario habrá memoria
reservada sin emplear, memoria que otros programas o el sistema operativo podrían necesitar.
Así mismo no debe emplearse memoria que no haya sido localizada o que haya sido liberada.
9.4.3.1. Apuntadores genéricos: Las funciones malloc y free trabajan con apuntadores a
void, estos son apuntadores genéricos, que pueden ser convertidos a otros tipos de apuntadores
por medio de casting. Veamos un ejemplo de como hacer casting entre apuntadores y de sus
peligros:
int *apin;
float *apfloat;
int e;
apin=&e;
apfloat=(float *)apin;
*apfloat=10.5;
En el contexto de este programa una asignación como:
apfloat=apin;
no es valida, porque apfloat es un apuntador a flotante
mientras que apin es un apuntador a entero, cuando se
compilará reportaría un error.
Sin embargo la asignación
apfloat=(apfloat *)apin
si es valida, hará que apfloat y apint apunten a la misma dirección de memoria Aún así este
programa tiene un gravísimo problema cuando se ejecute. Haciendo un diagrama de memoria
(suponiendo que estamos ejecutándolo en un sistema donde los tipos float e int tengan
diferente tamaño) puede notarse el problema que genera la última instrucción.
9.4.3.2. malloc: Esta función recibe como parámetro el tamaño, en bytes, del bloque de
memoria que el programa requiere. Si hay suficiente memoria para reservarlo, retorna un
apuntador al bloque recién localizado. Si la memoria no alcanza retorna NULL.
Supongamos que estamos trabajando en un IBM PC, y queremos pedir espacio para almacenar un
double. La siguiente porción de programa realiza correctamente esta tarea (con excepción de que
no libera memoria y tampoco verifica si malloc logra localizar memoria):
double *apd;
apd=(double *)malloc(8);
*apd=0.00023222;
Como resulta muy incomodo tener que conocer el tamaño en bytes de cada tipo de datos que
queremos localizar C ofrece la primitiva sizeof, que recibe como parámetro un tipo o expresión
y retorna el tamaño en bytes del tipo correspondiente. El ejemplo anterior empleando sizeof
puede quedar así:
double *apd;
apd=(double *)malloc(sizeof(double));
*apd=0.00023222;
Otra gran ventaja de sizeof es que no compromete la portabilidad del programa. Un programa
escrito en C es portable, cuando al pasar las fuentes de una máquina a otra, por compiladores
apropiados para cada máquina, se obtiene programas ejecutables que funcionan igual en ambas
máquinas. Todos los programas que hemos escrito hasta el momento son portables (excepto el
primer ejemplo de esta sección y la porción anterior que dice apd=(double *)malloc(8);),
es decir que si los compilamos en una máquina IBM PC (por ejemplo con el compilador BC++
3.1) o si los compilamos en un 486 (por ejemplo con el compilador gcc), obtendremos
programas ejecutables en cada uno de ellos que se comportan exactamente igual.
9.4.3.3. free: Recibe un apuntador a un bloque de memoria previamente localizado con alguna
de las funciones para localizar memoria: malloc, calloc y realloc. Libera tal bloque para
que pueda ser empleado por otros programas o por otras partes del mismo programa. Si reserva
un bloque de memoria y luego lo libera con free, no debe volver a referenciar tal bloque de
memoria (ya no está asignado y contendrá información de otra parte del programa)
Completemos el último ejemplo (aún falta verificar que hubiese memoria):
double *apd;
apd=(double *)malloc(sizeof(double));
*apd=0.00023222;
free(apd);
El programa mostrado a continuación
es un ejemplo completo del uso de #include <string.h>
memoria dinámica.
#include <stdio.h>
#include <alloc.h>
Note que este programa verifica que
malloc asigne la memoria solicitada, int main()
en caso de que malloc falle en asignar {
los 10 bytes solicitados, terminará y char *cad;
retornará 1.
/* Localizamos memoria */
Si malloc logra localizar la memoria, cad = (char *) malloc(10);
copia en la zona asignada la cadena if (cad==NULL) {
"Hola" terminada en '\0', es decir
printf("No hay Memoria\n");
emplea 5 de los 10 bytes localizados y
return 1;
apuntados por cad.
}
Además cuando termina de emplear la
zona de memoria asignada la libera y /* copia "Hola" a la cadena */
retorna 0.
strcpy(cad, "Hola");
Es bastante común que los programas /* muestra la cadena */
retornen 0 cuando no hay errores printf("cad es %s\n", cad);
durante la ejecución y otros enteros
como códigos de error.
/* libera memoria */
Ahora podemos hacer programas que free(cad);
pueden emplear diferentes cantidades
de memoria según sus necesidades en return 0;
tiempo de ejecución.
Veamos el }
ejemplo completo de la mediana de un
conjunto de números:
1. Planteamiento del Problema y Teoría preliminar:
n
Dados n números reales, la mediana es el dato que quede en la posición   después de ordenar
2
ascendentemente los n números.
Por ejemplo la mediana de los 5 números: 3,2,5,9,2 es 3.
3,2,5,9,2
Ordena
2,2,3,5,9
3er dato
3
2. Macro Algoritmo
a) Pedir cantidad de datos y datos (se localiza memoria según la cantidad de datos)
b) Ordenar los datos suministrados
c) Extraer el dato de la mitad y mostrarlo como resultado.
3. Especificación de funciones
void Recibe(int n, float vec[]);
/* PRE: Recibe la cantidad de datos (n) y un vector con espacio
suficiente para n datos flotantes */
/* POST: Llena el vector vec, con datos suministrados por el usuario */
void Ordena(int n, float vec[]);
/* PRE: Recibe la cantidad de datos (n) y un vector con n flotantes */
/* POST: Modifica el vector vec para que tenga los datos originales
pero ordenados ascendentemente */
4. Implementación
#include <stdio.h>
#include <alloc.h>
void inter_float(float *x,float *y);
void Ordena(int n, float vec[]);
void Recibe(int n,float vec[]);
int main() {
int n;
float *vf;
printf("Calculo de Mediana\n"
"Cuantos datos ? ");
scanf("%i",&n);
vf=(float *)malloc(n*sizeof(float));
if (vf==NULL) {
printf("No hay suficiente memoria\n");
return 1;
}
Recibe(n,vf);
Ordena(n,vf);
printf("La mediana es: %f\n",vf[n/2]);
free(vf);
return 0;
}
void inter_float(float *x,float *y) {
float t;
t=*x;
*x=*y;
*y=t;
}
void Ordena(int n, float vec[]) {
int i;
float t;
for (i=0;i<n-1;) {
if (vec[i]>vec[i+1]) {
inter_float(vec+i,vec+i+1);
i=0;
}
else {
i++;
}
}
}
void Recibe(int n,float vec[]) {
int i;
for (i=0;i<n;i++) {
printf("Dato %i: ",i+1);
scanf("%f",vec+i);
}
}
Del programa recién presentado se recomienda
observar:
• Asigna sólo la memoria necesaria para almacenar
los datos y la libera cuando ya no requiere más su contenido.
• Verifica que malloc haya podido localizar la memoria solicitada. En caso de que no hubiese
podido muestra un mensaje y retorna un código de error.
• Emplea la función inter_float que intercambia el valor de dos variables de tipo flotante.
• Las funciones Ordena y Recibe reciben un vector de flotantes como parámetro, aunque
desde el main se está pasando un apuntador.
• La invocación de las funciones scanf en Recibe e inter_float en Ordena, emplea suma
de apuntadores con enteros
• El ordenamiento se hace por el algoritmo de burbuja.
9.4.4 Vectores de apuntadores
Como los apuntadores son tipos, pueden hacerse vectores de apuntadores. (No confundir con un
apuntador a un vector). Una aplicación típica es una lista de nombres, dado que cada nombre es
una cadena y las cadenas son vectores que pueden ser referenciados por medio de apuntadores.
Veamos gráficamente un vector de apuntadores a vectores:
vap
0
1
2
3
'J' 'u' 'a' 'n' '\0'
La declaración de vap es:
'A' 'n' 'a' '\0'
char *vap[4];
'P' 'e' 'd' 'r' ''o' '\0'
'M' 'a' 'r' 'i' 'a' '\0'
{"Juan","Ana","Pedro","Maria"};
O aún más breve:
Como es un arreglo, podemos inicializarlo
durante la declaración de la siguiente forma:
char
*vap[4]=
char *vap[]={"Juan","Ana","Pedro","Maria"};
Una vez se haya inicializado de esta forma el vector vap se tendrá:
printf("%c",*vap[0]);
/* Imprime el caracter 'J' */
printf("%s",vap[2]);
/* Imprime la cadena "Pedro"*/
printf("%c",*(*(vap+2)+3)); /* Imprime el caracter 'r' */
Una estructura similar pudo haberse logrado con una matriz de caracteres, en la que se colocará
en cada fila una matriz. La diferencia es que tendría que haberse dado desde el comienzo una
cantidad de columnas fijo, igual al tamaño de la cadena más grande, además su inicialización
sería más dispendiosa:
char mcar[4][6]="{{'J','u','a','n','\0',' '},
{'A','n','a','\0',' ',' '},
{'P','e','d','r','o','\0'},
{'M','a','r','i','a'}};
Además no podría usarse el identificador de formato %s en un printf para imprimir cadenas.
printf("%c",mcar[2][2]); /* Imprime 'd' */
Para imprimir un nombre debemos usar un ciclo:
int y,x;
for (y=1,x=0;mcar[y][x];x++) {
printf("%c",mcar[y][x]);
}
/*Imprime "Ana" */
Ahora se desarrollará una aplicación completa que emplea un vector de apuntadores y ejemplifica
los conceptos estudiados en todo el capítulo, se trata del programa SORT.
1. Planteamiento del problema
El programa SORT del DOS (y también de UNIX) recibe del usuario una lista de cadenas, y
después se las muestra ordenadas de menor a mayor.
2. Macro Algoritmo
a) Pedir al usuario la cantidad de cadenas a ordenar
b) Localizar memoria para el vector de cadenas
c) Pedir al usuario las cadenas por ordenar y para cada una localizar memoria y ponerla en el
vector
d) Ordenar las cadenas recibidas
e) Mostrar por pantallas las cadenas ordenadas
f) Liberar toda la memoria solicitada
3. Especificación de funciones
void PideCadenas(int n,char *vc[]);
/* PRE: Recibe la cantidad de cadenas a pedir y un vector para
apuntarlas */
/* POST: Por cada cadena dada por el usuario se localiza memoria
suficiente y se apunta con un elemento del vector. Puede retornar 0 si
no hay error y 1 si no hay memoria suficiente */
void Ordena(int n,char *vc[]);
/* PRE: Recibe la cantidad de cadenas y el vector vc con apuntadores
a tales cadenas */
/* POST: Reorganiza los apuntadores del vector vc para que las cadenas
a las que apuntan quede ordenadas alfabeticamente */
void MuestraCadenas(int n,char *vc[]);
/* PRE: Recibe la cantidad de cadenas (n) y el vector vc con
apuntadores a tales cadenas */
/* POST: Muestra las cadenas apuntadas por los elementos de vc, desde
vc[0] hast vc[n-1]. */
5. Implementación
La función scanf, cuando tiene el especificador de formato %s, espera del usuario una palabra
(sin espacios) y la coloca en la cadena que recibe, como en este caso necesitamos que espere una
línea entera del usuario y la almacene en una cadena, empleamos la función gets definida en
stdio.h, su especificación es la siguiente:
char *gets(char *s);
/* PRE: Recibe una cadena s, con suficiente espacio para la linea que
ingresará el usuario */
/* POST: Llena el vector s, con los caractéres de la linea dada por el
usuario (terminada con '\n') y retorna un apuntador a tal linea (s). Si
encuentra un fin de archivo (EOF) retorna NULL*/
Además no es conveniente mezclar la función scanf con gets, pues puede generar
comportamientos a primera vista extraños. Por tal motivo el primer entero solicitado se pide
como una línea y después se convierte a entero empleando la función atoi, declarada en la
librería stdlib.h y cuya declaración y especificación se presentan a continuación:
int atoi(const char *s);
/* PRE: Recibe una cadena s */
/* POST: Convierte la cadena s a entero y lo retorna. Asume que la
cadena s se compone de: 1. Una cadena opcional de espacios o
tabuladores, 2. Un signo opcional (+/-), 3. Una cadena de digitos.
no puede completar la conversión retorna 0*/
Si
La implementación se muestra en las páginas siguientes.
6. Inconvenientes
El programa mostrado, compara cadenas teniendo en cuenta capitalización (mayúsculas y
minúsculas). e.g. La cadena "Zimbawe" es menor que "altura".
En realidad el programa SORT, no pregunta al usuario la cantidad de líneas a ordenar, sino que
empieza a leer líneas hasta que el usuario da el comando fin de archivo o EOF. (En un IBM PC,
bajo sistema operativo DOS, se produce presionando la tecla F6 , en una máquina UNIX
presionando [Control]-[D])
Usa la función fgets que no es segura.
Emplea un algoritmo de ordanamiento poco eficiente.
#include
#include
#include
#include
<stdlib.h>
<stdio.h>
<alloc.h>
<string.h>
#define MAX_TAM_LINEA 100
/* Tamaño máximo que puede tener una linea */
int Halla_Max(int n,char *cad[]); /* Usado por la funcion de ordenar */
void MuestraCadenas(int n,char *vc[]);
void Ordena(int n,char *vc[]);
int PideCadenas(int n,char *vc[]);
int
main() {
int n; /* Número de lineas */
char **Lineas; /* Apuntará al vector de apuntadores */
/*??? Porqe debe ser un doble apuntador ??? */
int i;
char lin[MAX_TAM_LINEA];
printf("SORT\nOrdena lineas\n\nCuantas lineas va a ordenar? ");
gets(lin);
n=atoi(lin);
if (n<1) {
printf("Número de lineas no valido \n");
return 2;
}
/* Localizamos memoria para el vector */
Lineas=(char **)malloc(sizeof(char *)*n);
/*??? Porqe debe se hace un casting a char ** ??? */
/*??? Porqe se toma el tamaño de un char * ??? */
if (Lineas==NULL) {
printf("Memoria insuficiente\n");
return 1;
}
if (PideCadenas(n,Lineas)) {
/* ??? Porque la condición de este if no tiene operadores de comparación ???*/
printf("Memoria insuficiente\n");
return 1;
}
Ordena(n,Lineas);
printf("\n-----------------------------------------------\n");
MuestraCadenas(n,Lineas);
printf("\n-----------------------------------------------\n");
/* Ahora liberamos memoria reservada en PideCadenas */
for (i=0;i<n;i++) {
free(Lineas[i]);
}
free(Lineas);
return 0;
/* Liberamos también el vector de apuntadores */
}
int Halla_Max(int n,char *cad[]) {
/* PRE: Recibe la cantidad de cadenas (n) y el vector vc con apuntadores a
tales cadenas */
/* POST: Retorna la posición dentro del vector que apunta a la mayor
cadena encontrada */
char *max; /* Apuntador a la cadena más grande encontrada */
int pmax; /* Posición dentro del vecto a la cadena más grande */
int i;
for (i=1,pmax=0,max=cad[0];i<n;i++) {
/* ??? Porque i se inicializa en 1 y no en 0 ???*/
if (strcmp(cad[i],max)>0) {
/*??? Porque aqui no dijo simplemente max>cad[i] ??? */
/*??? Que ocurriría si dijera strcmp(max,cad[i]) ??? */
max=cad[i];
pmax=i;
}
}
return pmax;
}
void
MuestraCadenas(int n,char *vc[]) {
/* PRE: Recibe la cantidad de cadenas (n) y el vector vc con apuntadores a tales cadenas */
/* POST: Muestra las cadenas apuntadas por los elementos de vc, desde vc[0] hast vc[n-1] */
int i;
for (i=0;i<n;i++) {
printf("%s\n",vc[i]);
}
}
void Ordena(int n,char *vc[]) {
/* PRE: Recibe la cantidad de cadenas y el vector vc con apuntadores a tales cadenas */
/* POST: Reorganiza los apuntadores del vector vc para que las cadenas a las que apuntan quede
ordenadas alfabeticamente */
int i;
int pmax;
char *t;
for (i=n;i>1;i--) {
pmax=Halla_Max(i,vc);
t=vc[pmax];
vc[pmax]=vc[i-1];
vc[i-1]=t;
}
}
int PideCadenas(int n,char *vc[]) {
/* PRE: Recibe la cantidad de cadenas a pedir y un vector para apuntarlas */
/* POST: Por cada cadena dada por el usuario se localiza memoria suficiente y
se apunta con un elemento del vector.
Si tiene exito retorna 0,
Si se agota la memoria retorna 1 */
/*??? Porque es importante que PideCadenas retorne un entero? */
int i;
char linea[MAX_TAM_LINEA];
/*??? Para que se usa la variable linea aqui, en vez de usar directamente el vector
recibido */
for (i=0;i<n;i++) {
printf("Teclee la linea %i:\n",i+1);
/*??? Porque se imprime i+1 y no i ???*/
gets(linea);
vc[i]=(char *)malloc(sizeof(char)*(strlen(linea)+1));
/*??? Porque es importante aqui strlen(linea)+1 ???*/
if (vc[i]==NULL) {
return 1;
}
strcpy(vc[i],linea);
}
return 0;
}
Argumentos de un programa
Hasta el momento se ha trabajado con una función main que no recibe argumentos y retorna un
entero:
int main()
En realidad la función main, si puede tener parámetros, corresponderán a los argumentos pasados
en la línea de comandos al invocar el programa. Por ejemplo la primitiva copy en DOS
(programa cp en UNIX) encargada de copiar archivos, recibe dos argumentos, el primero es el
nombre del archivo fuente y el segundo el nombre del archivo destino.
Cuando se invoca un programa escrito en C, estos argumentos quedarán organizados en un vector
de cadenas, este vector siempre se identifica con el parámetro argv , la cantidad de parámetros
recibidos queda en una variable de tipo entera que puede recibir el main como argc. La
definición del main, cuando se van a recibir los parámetros deberá comenzar así:
int
main(int argc,char *argv[])
Cada cadena de argv, será un parámetro de la línea de comandos, el primero es el nombre del
programa (por está razón argc, siempre es mínimo 1) por ejemplo el comando
echo Esta es una prueba
tanto en DOS como en UNIX imprimirá el argc
mensaje "Esta es una prueba". Si echo fuese un
5
programa escrito en C, los parámetros argc y argv
serán como se muestran en la figura.
Una vez dentro del main, podrán usarse los
argumentos recibidos, como se ejemplifica a en el
siguiente programa que imprime todos los
argumentos que recibe:
argv
"Echo"
"Esta"
"es"
"una"
"prueba"
#include <stdio.h>
int
main(int argc,char *argv[]) {
int i;
for (i=0;i<argc;i++) {
printf("%i: %s \n",i,argv[i]);
}
return 0;
}
*9.5. Apuntadores a funciones
PREGUNTAS
1. Por que la función scanf (librería stdio.h) recibe como parámetros apuntadores? Que
ocurriría si en vez de esto recibiera valores?
2. Que valor adquiere la variable i, una vez se ejecuta la siguiente porción de programa?
int i;
char c;
‘A’
(int)c;
3. Suponga que el siguiente programa se ejecuta en una IBM PC:
int a;
long b=800000000;
a=(int)b;
a. Una vez se ejecute esta porción de código, la variable a tendrá el valor 800000000? (Ayuda:
Mire el rango de valores que puede adquirir una variable tipo int en un PC)
b. Explique en que casos el casting preserva el valor y en cuales no.
4. La función strchr mostrada en uno de los ejemplos de la sección 9.4.3, puede ser aún más
breve, aprovechando que el ASCII de ‘\0’ es 0. Escriba nuevamente esta función
aprovechando esto. (Ayuda: Puede aplicarse 2 veces)
5. Cual es el estado de las variables cuando se termina de ejecutar la porción de código del
último ejemplo de la sección 9.4.2 ?
6. Haga diagramas de memoria para la porción de programa del primer ejemplo de la sección
9.4.3. Explique cual es el problema que tiene ese programa al ejecutarse.
7. El siguiente programa tiene un problema. Cual es?
int *i;
(int *)malloc(sizeof(int));
free(i);
printf ("El valor de *i es %i",*i);
8. Haga diagramas de memoria para mostrar lo que ocurre al ejecutar el ejemplo completo de la
sección 9.4.3.3
9. Porque cree que a una compañía de software le interesan lenguajes de programación
portables?
10.Declare un apuntador a flotantes, y empléelo para apuntar a un vector de 10 flotantes
localizado dinámicamente con malloc. Llene este vector con los flotantes 0.0, 1.1, 2.2, ...,
9.9. Finalmente imprima el contenido de todo el vector y libere la memoria que empleó.
11.En el ejemplo de la mediana de la sección 9.4.3, la función Ordena, hace la invocación:
inter_float(vec+i,vec+i+1), que ocurre si cambiamos esa invocación por:
inter_float(vec[i],vec[i+1]) ?
12.Defina un vector de apuntadores a vectores de caracteres (o más breve un vector de cadenas),
con las siguientes cadenas (en este orden): "El", "Lenguaje", "de", "Programación", "C".
También defina una matriz de caracteres con las mismas cadenas.
13.Que ventajas tiene un vector de cadenas (Ejemplo 1 sección 9.4.4) sobre una matriz de
caracteres? Piense por ejemplo en espacio requerido por cada uno y facilidad de uso.
14.Haga la función atoi cuya especificación se presenta en el ejemplo SORT de la sección 9.4.4.
15.En el código fuente del programa SORT se presentan varias líneas de comentarios,
comenzadas con /*???. Son preguntas referentes al programa, contéstelas todas.
9.6. EJERCICIOS
Sección 9.4.1.
1. Se requiere una función que reciba 3 enteros y retorne los 3 enteros ordenados de menor a
mayor.
2. Haga la función Halla_Max, que recibe un vector de enteros y el tamaño de tal vector.
Retorna la posición del máximo y el máximo elemento del vector. (Ayuda: Puede retornar uno
o ambos valores en parámetros pasados por referencia)
Sección 9.4.2.
1. Haga la versión con apuntadores de la función strcat de la librería string.h. La siguiente
especificación fue tomada del Help del BC ++ 3.1
Agrega una cadena al final de otra
Declaración:
char *strcat(char *dest, const char *src);
Comentarios:
strcat concatena una copia de src al final de dest. La longitud de la cadena resultante es
strlen(dest) + strlen(src).
Valor retornado:
strcat retorna un apuntador a las cadenas concatenadas (es decir a src)
2. Haga la versión con apuntadores de la función strchr de la librería string.h. La siguiente
especificación fue tomada del Help del BC ++ 3.1. Mire con cuidado los comentarios y notará
que la función presentada en este capítulo no cumple todos los requerimientos.
Busca en una cadena la primera ocurrencia de un caracter dado
Declaración:
char *strchr(char *s, int c);
Comentarios:
strchr hace un recorrido hacia adelante, buscando el caracter especificado. Encuentra la
primera ocurrencia del caracter c en la cadena s.
El caracter de fin de cadena (‘\0’) es considerado como parte de la cadena; por ejemplo, strchr
(strs,0) retorna un apuntador al caracter de fin de la cadena strs.
Valor retornado:
Si tiene éxito, retorna un apuntador a la primera ocurrencia del caracter c en la cadena s.
En caso de error (si c no ocurre en s) retorna NULL.
3. Haga la versión con apuntadores de la función strcmp de la librería string.h. La siguiente
especificación fue tomada del Help del BC ++ 3.1.
Compara dos cadenas
Declaración:
int strcmp(char *s1,char*s2);
Comentarios:
strcmp hace una comparación sin signo de las cadenas s1 y s2.
La comparación de las cadenas comienza con el primer carácter de cada cadena y continua con
los siguientes hasta que los caracteres correspondientes difieran o hasta que termine se alcance el
final de la cadena.
Valor retornado:
Esta rutina retorna un valor de tipo int, que cumple:
< 0 si s1<s2
si s1>s2
== 0 si s1==s2
4. Haga la versión con apuntadores de la función strcpy de la librería string.h. La siguiente
especificación fue tomada del Help del BC ++ 3.1.
Copia una cadena fuente en una destino
Declaración:
char *strcpy(char *dest, char *src);
Comentarios:
Copia la cadena src en dest, deteniéndose después de haber copiado el caracter de terminación
de cadena.
Valor retornado:
Apuntador a la cadena destino dest
5. Haga la versión con apuntadores de la función strlen de la librería string.h. La siguiente
especificación fue adaptada del Help del BC ++ 3.1.
Calcula la longitud de una cadena
Declaración:
int *strlen(char *s);
Comentarios:
Calcula la longitud de la cadena s
Valor retornado:
Retorna el número de caracteres de la cadena s, sin contar el caracter de fin de cadena.
6. Haga la versión con apuntadores de la función strlwr de la librería string.h. La siguiente
especificación fue tomada del Help del BC ++ 3.1.
Convierte una cadena a minúsculas
Declaración:
char *strlwr(char *s);
Comentarios:
Convierte las letras mayúsculas (A - Z) de la cadena s a letras minúsculas (a - z).
Valor retornado:
Un apuntador a la cadena s
Sección 9.4.4.
1. Haga la función Ordena del programa ejemplo SORT, pero empleando el algoritmo de
ordenación por burbuja.
2. Haga los cambios necesarios al programa SORT para que no distinga mayusculas de
minusculas.
3. Modifique el programa SORT, de forma que no pida la cantidad de cadenas al comienzo, sino
que pida lineas al usuario hasta que él envie un fin de archivo EOF. (Ayuda: La función gets
retorna NULL cuando esto ocurre)
4. Haga un programa de nombre TESTARG que imprima en pantalla el mensaje "Probando"
cuando se invoque como:
TESTARG -test
y la cadena "Parámetro invalido" en otro caso.
9.7. PROYECTOS
1. Existen programas que permiten obtener estadísticas de datos, (SPSS es uno de los más
famosos).
Inclusive las calculadoras sencillas comúnmente traen algunas funciones de
estadística. Haga un programa que permita:
1. Editar los datos por analizar (el número de datos debe ser variable)
2. Calcular promedio
3. Calcular mediana
4. Calcular desviación estándar
5. Calcular error cuadrático medio
Como restricción, debe mantener los datos en un vector que se localice dinámicamente.
2. Haga un rograma que cuente la frecuencia de las palabras provenientes de la entrada estándar.
Por ejemplo si el usuario tecleara:
Solo se que nada se
wc retornaría una lista como
Solo
1
se
2
que
1
nada
1
Haga este programa, de forma que espere líneas provenientes de la entrada estándar hasta que
llegue un caracter fin de archivo, y muestre por la salida estándar una lista de las palabras del
texto de entrada, junto con su frecuencia, sin tener en cuenta capitalización ni signos de
puntuación. Debe emplear un vector de apuntadores para mantener las palabras digitadas por el
usuario y otro para mantener la frecuencia. Opcionalmente la salida puede estar ordenada
alfabéticamente.
BIBLIOGRAFÍA
[1]
BORLAND C++ 5.0. Programmer´s Guide. 1996
[2]
KERNIGHAN, Brian. RITCHIE, Dennis M. El lenguaje de programación C.
Prentice Hall. 1985
Editorial
[3]
1992.
DEITEL, H. M. DEITEL, P.J. C How to Program. Editorial Prentice Hall.
[4]
MILENKOVIC, Milan.Sistemas Operativos: Conceptos y diseño. Mc Graw Hill.
1994.
CREDITOS, PROPIEDAD INTELECTUAL:
[email protected]
Dominio
público.
2004.
Sin
garantías.
Descargar