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.