I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
FUNDAMENTOS DE PROGRAMACIÓN Tema 4
Comenzando a programar
1º Administración de Sistemas Informáticos I.E.S. Francisco Romero Vargas Departamento de Informática __________________________________________________________________________________________________________ Comenzando a programar 1
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
1. LAS FUNCIONES Y EL DISEÑO MODULAR. Hasta ahora, aunque en los programas hemos utilizado diversas funciones de diferentes bibliotecas del C, sólo hemos definido una función -la función main()debido a que la complejidad de los problemas a resolver no era grande; sin embargo, cuando los problemas son arduos, es aconsejable distribuir el programa en módulos (en funciones). Precisamente, la filosofía de diseño del lenguaje C está basada en el empleo de funciones, que juegan el mismo papel que las subrutinas o los procedimientos de otros lenguajes. Así pues, los programas en C comienzan ejecutándose por la función main(), y ésta puede llamar a otras funciones. -
-
-
La principal ventaja del diseño modular es que si un trozo de código -que realiza una tarea determinada- se repite a lo largo del programa, es más cómodo y económico implementarlo una sola vez, como una función, que podremos utilizar en diferentes situaciones y localizaciones del programa, cada vez que se necesite, con sólo escribir el nombre de la función y sin necesidad de repetir las mismas líneas cada vez. Incluso, si la función es lo suficientemente general, se podrá utilizar en diferentes programas. De todos modos, aunque la porción de programa que realiza una cierta tarea se haya de emplear una sola vez a lo largo del mismo, es conveniente implementarlo como una función ya que los programas modulares son más fáciles de leer, de depurar y de mantener. Al utilizar nombres descriptivos (significativos) para las funciones queda más claro cómo está organizado el programa. Además, cada función se puede afinar por separado hasta conseguir que haga lo que se pretenda de ella. Las funciones se pueden considerar como "cajas negras", definidas exclusivamente por la información que hay que suministrarles (su entrada) y el producto que devuelven (su salida). De esta manera, podremos interesarnos sólo por el diseño global del programa dejando para más tarde el resolver los detalles.
..... void main(void) { leer_datos (...); calcular_area (...); ver_resultado (...); } void leer_datos (....) { ...... } void calcular_area(....) { ...... } void ver_resultado (....) { ...... } __________________________________________________________________________________________________________ Comenzando a programar 2
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
2. ESTRUCTURA DE UNA FUNCIÓN. El formato de cualquier función es igual al de la función principal main(), es decir, consta de dos partes principales: el encabezamiento y el cuerpo -que va entre llaves-. A su vez, el cuerpo incluye las definiciones y/o declaraciones de variables a emplear y las sentencias. En el encabezamiento de una función se colocan: 1º. Las instrucciones de preprocesador, donde se pueden definir las macros o constantes simbólicas, declarar prototipos de funciones e incluir ficheros que se necesitan en la función y que no hagan falta en main(). 2º.
La clase, el tipo, el nombre y los argumentos de la función.
- La clase (static o extern): Informa de su accesibilidad o visibilidad, es decir, su ámbito de actuación dentro del programa. Dicho de otro modo: la clase indica desde qué partes del programa son accesibles las funciones. Se estudiará con más detalle en el apartado de este tema “Modos de almacenamiento”. - El tipo: Es el correspondiente al valor que retorna la función. Si la función no retorna ningún valor se utilizará el tipo void que significa vacío. - El nombre y los argumentos: Al definir una función, inmediatamente detrás del nombre de la función se disponen unos paréntesis entre los cuales van los posibles argumentos o parámetros formales, separados por comas si son varios. Para cada parámetro formal hay que especificar su tipo y su identificador. A través de éstos la función intercambia información con aquella desde la que ha sido llamada. Al igual que antes, se insertará la palabra void entre los paréntesis si la función no recibe argumentos. Cuando se define (se implementa) una función NO se coloca punto y coma detrás de los paréntesis. La sentencia return : Detrás del encabezamiento se escribe el cuerpo de la función entre llaves. El cuerpo contiene las diversas sentencias que forman la función. Entre éstas podremos encontrar la sentencia return mediante la cual la función retorna un valor. Las funciones usadas por main() hay que declararlas antes de ésta. Esta declaración previa de cada función se conoce como declaración del prototipo de función que es usado por el compilador para comprobar que cada vez que se llama a esa función se le mandan los argumentos en número y tipo correctos y que el valor retornado se trata correctamente. Cada declaración de prototipo debe coincidir con la correspondiente definición de función, excepto que no es necesario precisar los identificadores de los argumentos, si los hubiere, y que detrás de los paréntesis SÍ se coloca un punto y coma. En C++, a la hora de declarar el prototipo de una función que no utiliza argumentos, es equivalente colocar entre paréntesis la palabra void o no colocar nada. __________________________________________________________________________________________________________ Comenzando a programar 3
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
Para llamar a una función (para que se ejecute) se escribe su nombre seguido por paréntesis (dentro de éstos los posibles parámetros actuales) y detrás un punto y coma con el fin de crear una sentencia. Si una función NO recibe argumentos, a la hora de llamarla se colocan detrás del nombre los dos paréntesis, sin expresar nada entre ambos. Cuando el programa en su ejecución se encuentra con una llamada a una función, realiza las instrucciones indicadas en ésta y cuándo termina, regresa a la siguiente línea a la de la llamada, es decir, a la posterior a la que causó su ejecución en el módulo de llamada. #include <string.h> #include #define NOMBRE "ORDENATAS,S.A." #define DIRECC "Plaza del Byte, 16" #define CIUDAD "08008 Villabits" // Prototipos de las funciones: void asteriscos(void); void espacios(int); // La función espacios es llamada 3 veces, void main(void) // utilizando como argumento efectivo: { // 1º:una constante int salta; // 2º:una variable asteriscos(); // 3º:una expresión espacios(33); // Como 14 es la longitud de NOMBRE, printf ("%s\n",NOMBRE); // (80 - 14) / 2 da como resultado 33 salta = (80-strlen(DIRECC))/2; //strlen : longitud de una cadena espacios(salta); printf ("%s\n",DIRECC); espacios ( ( 80 - strlen (CIUDAD) ) /2 ); printf("%s\n", CIUDAD); asteriscos(); getch(); } // Declaración de funciones void asteriscos(void) { int cont; for (cont=1; cont <= 80; cont++) putchar('*'); } void espacios (int numero) // numero es argumento formal { int cont; for (cont =1; cont<=numero; cont++) putchar(' '); }
3. ARGUMENTOS DE FUNCIONES (Paso por valor). Ya hemos comentado que una función puede apoyarse en otra u otras para realizar la tarea que tiene encomendada. Y es frecuente que cuando una función f1() __________________________________________________________________________________________________________ Comenzando a programar 4
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
llame a otra, f2(), exista un intercambio de información, es decir, la función f1() pasará unos valores o argumentos a la función f2(). Por su parte, apuntamos anteriormente que f2() podrá retornar a f1() un valor a través de la sentencia return, pero también puede retornar otros valores mediante los mencionados argumentos o parámetros (se verá en el apartado PASO POR REFERENCIA). Ya sabemos que al definir una función se pueden colocar entre los paréntesis unas variables, precedidas de sus respectivos tipos, que se denominan argumentos formales. Mediante éstos se intercambian valores con la función desde la que se realiza la llamada. Para darle un valor al argumento formal se utiliza, en la sentencia de llamada a la función, un argumento efectivo o parámetro actual, que puede ser una constante, una variable o incluso una expresión más complicada. En resumen, el parámetro enviado es un valor específico que se asigna a la variable conocida como argumento formal (esta forma de enviar valores a la función se denomina PASO POR VALOR). La variable que recibe el valor tiene su propia dirección de memoria: Los argumentos formales son locales a la función que los utiliza, o sea, sólo son conocidos por ella y son de uso interno de la misma, y las demás funciones ignoran su existencia. Cuando se necesita enviar más de un parámetro se puede formar una lista (de argumentos efectivos/argumentos formales) separándolos por comas. Los argumentos efectivos y los formales, para una determinada función, deben coincidir en número y en tipo, y por supuesto, la asignación de valores se hace uno a uno según el orden en que están dispuestos. #include <stdio.h> #include <string.h> #include #define MAX_OPC 3 #define MAX_ALU 10 int menu(int); float leer_notas(int); float calcular_media (float, int); void main(void) { int opcion=0, hay_datos = 0; float total= 0.0, media=0.0, max_media=0.0; while (opcion != MAX_OPC) { opcion = menu(MAX_OPC); switch (opcion) { case 1: total = leer_notas(MAX_ALU); hay_datos = 1; break; case 2: if (hay_datos) { media = calcular_media(total, MAX_ALU); if (media > max_media) max_media = media; } } } printf("\n\nLa maxima media obtenida ha sido %.2f ", max_media ); getch(); } //***************************************************************** int menu(int tope_op) { int opcion_menu; __________________________________________________________________________________________________________ Comenzando a programar 5
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
clrscr(); printf("\t1. Introducir notas\n"); printf("\t2. Calcular media.\n"); printf("\t3. Finalizar.\n"); do { printf("\n\n\tElija opcion: "); scanf("%d", &opcion_menu); fflush(stdin); } while ( opcion_menu < 1 || opcion_menu > tope_op); return opcion_menu; } //***************************************************************** float leer_notas(int tope) { int i; float total=0.0 , nota; clrscr(); printf("Introduzca las notas (valores reales 0-10). \n\n"); for (i=1; i <= tope ; i++) { do { printf("Nota %d) ",i); scanf("%f", ¬a); } while (nota < 0.0 || nota > 10.0); total += nota; } printf("\n\nPulse una tecla para volver al menu."); getch(); return total; } //****************************************************************** float calcular_media (float total_notas, int tope) { clrscr(); printf("Calculando la media de las notas de %d alumnos.\n", MAX_ALU); printf("Total alumnos = %d\n", MAX_ALU); printf("Total puntos = %.2f\n", total_notas); printf("Media aritm. = %.2f\n", total_notas/tope); printf("\n\nPulse una tecla para volver al menu."); getch(); return total_notas/tope; }
4. LA INSTRUCCIÓN return La palabra reservada return hace que el valor de cualquier expresión que aparezca a continuación quede asignado como valor de retorno de la función que contiene dicho return. En el programa ejemplo anterior se usan tres funciones y al final de cada de una de ellas se utiliza esta instrucción.... En la 1ª: return opcion_menu; (devuelve un valor entero) En la 2ª: return total; (devuelve un valor real) En la 3ª: return total_notas/tope; (devuelve un valor real) El valor devuelto puede ser asignado en el módulo de llamada a una variable, o utilizado como parte de una expresión. __________________________________________________________________________________________________________ Comenzando a programar 6
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
Siguiendo con el ejemplo anterior, si observamos la función principal ( main() ) veremos que los valores retornados por cada una de las tres funciones son asignados a variables. opcion = menu(MAX_OPC); total = leer_notas(MAX_ALU); media = calcular_media(total, MAX_ALU); Sin embargo, hubiera sido perfectamente válido que una sentencia de la función principal fuese así: printf (“Se ha pulsado la opción %d \n”,menu(MAX_OPC)); El tipo del valor que se retorna debe coincidir con el que se escribe delante del nombre de la función cuando se declara el prototipo de la función (que a su vez debe ser idéntico al que se antepone al nombre de la función en su definición). Así, en el programa anterior se usan 3 funciones, cuyos prototipos son los siguientes: int menu(int); (efectivamente la 1ª función devuelve un entero) float leer_notas(int); (la 2ª retorna un valor real) float calcular_media(float, int); (la 3ª retorna un valor real) El valor de retorno no tiene que proceder obligatoriamente de una variable, sino que detrás de return puede situarse una expresión cualquiera. La expresión de retorno puede ir encerrada entre paréntesis para mejorar la claridad del programa, pero no es necesario. El uso de return tiene el efecto adicional de finalizar la ejecución de la función y devolver el control a la sentencia siguiente a la de la llamada. Esto ocurre incluso si la sentencia return no es la última de la función. Así, por ejemplo, si se emplea una sentencia como: return; se provoca que la función que la contiene acabe su ejecución y devuelva el control a la función de llamada. Al no haber expresión alguna detrás (no es necesario incluir los paréntesis), no se devuelve ningún valor. #include <stdio.h> #include unsigned abs(int); // Prototipo de la función abs void main(void) { int a=10, b=0, c=-22; int d,e,f, result; d = abs(a); e = abs(b); f = abs(c); result = d + 5 * abs(c-a); printf (" %d %d %d %d %d %d", d, e, f, abs(-3), abs(3), result ); getch(); } //*********** Función valor absoluto. Definición unsigned abs(int x) __________________________________________________________________________________________________________ Comenzando a programar 7
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
{ unsigned y; y = (x<0)? -x : x; return (y); }
En el ejemplo, la variable y es interna de la función abs(), pero el valor de dicha variable se comunica a la función desde la que se ha realizado la llamada por medio de return. #include <stdio.h> #include <stdlib.h> #include #include #define TOPE_NUM 1000 #define MAX_OPORT 5 int pide_numero (int, int); int suma_dig (int); void main(void) { int num_aleat, intentos = 0, adivina = 0 ; clrscr(); randomize(); printf ("Adivine un numero entre 1 y %d.\n", TOPE_NUM); printf ("Tiene %d oportunidades\n", MAX_OPORT); num_aleat = 1 + rand() % TOPE_NUM; printf("AYUDA : Sus digitos suman %d\n\n", suma_dig ( num_aleat) ); while (intentos < MAX_OPORT && !adivina ) { intentos++ ; adivina = pide_numero (intentos, num_aleat); } if (!adivina) printf("Lo siento. El numero a adivinar era %d \n", num_aleat ); getch(); } //****************************** int suma_dig ( int aleat) { int total = 0; while ( aleat ) { total += aleat % 10; aleat /= 10; } return total; } //****************************** int pide_numero ( int intentos, int aleat) { int numero; do { printf("Intento numero %d = ", intentos); scanf("%d", &numero); } while (numero < 1 || numero > TOPE_NUM); if (numero == aleat) { printf("ENHORABUENA.\n\n"); return 1; } // else no hace falta, aunque sería más correcto utilizarlo if (numero < aleat) printf("El numero es mayor que el introducido.\n\n"); else printf("El numero es menor que el introducido.\n\n"); return 0; }
__________________________________________________________________________________________________________ Comenzando a programar 8
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
5. PUNTEROS. Los punteros son direcciones: Un puntero contiene (y es la representación simbólica de) una dirección de memoria donde probablemente habrá algún dato que nos interesa. Esto significa que un puntero apunta o señala a un espacio físico en memoria RAM y puede referenciar cualquier objeto que se encuentre en ella: variables de cualquier tipo básico, arrays, estructuras, etc. Los punteros por tanto, permiten acceder a los datos de forma indirecta, a través de su dirección, y entre sus ventajas están: - Hacer que una función devuelva más de un valor. - Crear un código más compacto y eficiente ya que al usarlos nos acercamos a la forma de trabajar de la máquina. - Manejar los arrays y cadenas de forma eficiente. - Soportar el uso de estructuras dinámicas, etc. •
El operador &
El operador & (ampersand) es un operador unario que devuelve la dirección de memoria de su operando. Se le puede llamar operador de dirección (aunque también tiene otro uso que es realizar una operación AND a nivel de bits). Por ejemplo, si las variables nieve y bola están en las direcciones de memoria 6800 y 7200, respectivamente, serán válidas las asignaciones siguientes: ptr_int1 = &nieve; ptr_int2 = &bola; Se asigna a la variable puntero ptr_int1 el valor de la dirección que ocupa la variable entera nieve, o sea, 6800. Asimismo, se asigna a la variable puntero ptr_int2 el valor de la dirección que ocupa la variable entera bola , o sea, 7200. Por su parte, las variables puntero ptr_int1 y ptr_int2 tienen cada una, evidentemente, su propia dirección de memoria (por ejemplo, 6950 y 7050, respectivamente). Tales direcciones no son modificadas por las asignaciones realizadas: de hecho, ninguna de las cuatro variables utilizadas en el ejemplo podrá cambiar su dirección de memoria durante la ejecución del programa. También hay que observar que no importan en esas asignaciones el valor que en ese momento puedan tener almacenado las variables enteras nieve y bola. #include <stdio.h> #include void f2(int); void main(void) { int x=24,y=5; printf ("En main(), x=%d y su dirección es %p\n", x, &x); //Resultado: x=24 Dirección=6618624 printf ("En main(), y=%d y su dirección es %p\n", y, &y); //Resultado: y=5 Dirección=6618620 f2(x); getch(); } //********************************************************** void f2(int y) // parámetro pasado por valor { // y -variable local- recibe el valor de la x de main() __________________________________________________________________________________________________________ Comenzando a programar 9
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
int x=10; // Esta x es distinta a la de main() printf ("En f2(), x=%d y su dirección es %p. \n", // Resultado x=10 Dirección=6618604 printf ("En f2(), y=%d y su dirección es %p. \n", // Resultado: y=24 Dirección=6618626 }
-
x, &x); y, &y);
Variables y constantes puntero. Existen tanto constantes puntero como variables puntero.
- &x es una constante puntero y representa la dirección de la variable x. Es una constante ya que x no va a cambiar de dirección durante la ejecución del programa, y se dice &x apunta (o es un puntero) a x. - Si una variable contiene la dirección de otra variable (de un objeto, en general), se dice que la primera es un puntero a la segunda. Una variable puntero toma como valor direcciones y se declara escribiendo el tipo del puntero (tipo base del puntero) y un asterisco seguido del nombre de la variable puntero. tipo *nombre ; Por ejemplo: int *ptr_int1 , *ptr_int2; Se dice que la variable ptr_int1 (al igual que ptr_int2) es un puntero de tipo int, lo que significa que podrá contener la dirección de cualquier variable de tipo entero. Sea el caso de la variable x de tipo int, se podrá realizar la asignación... ptr_int1 = &x ; y se dice que "ptr_int1 apunta a x". Posteriormente, al puntero se le podrá asignar otro valor de manera que apunte a otra dirección. Así, suponiendo que z sea de tipo int ptr_int1 = &z ; Ahora “ptr_int1 apunta a la variable z”. Y de este modo, cuantas veces sea preciso. - Por otra parte, también es fácil conseguir que una variable puntero apunte al mismo lugar que otra: ptr_int2 = ptr_int1 ; De este modo, si ptr_int1 apuntaba a z, ahora también ptr_int2 apunta a z, con lo cual tenemos dos punteros que apuntan a la misma dirección. Lo habitual es que una variable puntero apunte a variables del mismo tipo que el tipo que se ha declarado para ella. __________________________________________________________________________________________________________ Comenzando a programar 10
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
•
El operador *
El operador de indirección * aplicado a un puntero, da el valor almacenado en la dirección apuntada por el mismo. O sea, que suponiendo que en un momento del programa... var_fl = 23.56; ptr_fl = &var_fl; se puede obtener el valor almacenado en la variable de tipo float var_fl (o sea, 23,56) de la siguiente forma: *ptr_fl #include <stdio.h> #include void main(void) { int x=5; int y; int *dir; //Declaración de puntero: dir es puntero de tipo int dir = &x; // & es operador de dirección, dir "apunta a" x printf("Valor de x = %d, su dirección = %p\n", x, &x); // Resultado: Valor=5, Dirección=6618624 //El operador de indirección * seguido por un puntero da el //valor almacenado en la dirección apuntada por el mismo printf("Valor de x = %d, su dirección = %p\n", *dir, dir); // Resultado: Valor=5, Dirección=6618624 y = *dir + 3; printf ("Valor de y = %d.\n", y); // Resultado: y=8 dir = &y; // Ahora dir apunta a y printf("Dirección de y = %p \n", dir); // Resultado: Dirección y=6618620 printf("Valor y=Contenido posicion apuntada por dir es %d\n", *dir); getch(); // Resultado: Valor y = 8 }
Evidentemente el operador de dirección o ampersand (&) y el operador de indirección o asterisco (*) se anulan al aplicarlos a la vez de la forma: *(&x). Esta expresión da el valor almacenado en x, de igual forma que si hubiéramos utilizado directamente x. Al declarar una variable como puntero, no basta con decir que dicha variable es un puntero sino que además hay que indicar el tipo de variable a la que está apuntando (tipo base del puntero), ya que las variables de tipos distintos ocupan diferentes cantidades de memoria, y existen operaciones con punteros que requieren conocer el tamaño de almacenamiento (se estudiarán más adelante en ARITMÉTICA DE PUNTEROS). #include <stdio.h> #include //Ahora si funciona bien INTERCAMBIA void intercambia(int *,int *); void main(void) { int x=5, y=10; printf ("En principio x=%d, y=%d.\n", x, y); //x=5, y=10 intercambia ( &x, &y); printf ("Ahora x=%d, y=%d.\n", x, y); //x=10, y=5 getch(); } void intercambia(int *u, int *v) __________________________________________________________________________________________________________ Comenzando a programar 11
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
{ int aux *u *v }
aux; = *u; = *v; = aux;
#include <stdio.h> #include void incrementa(int *); void main(void) { int x=150; printf("En función principal, x = %d.\n",x); // x=150 incrementa(&x); printf("Función incrementa() cambia su valor: x = %d\n",x); // x=160 getch(); } void incrementa(int *ptr) { *ptr += 10; }
6. ARGUMENTOS DE FUNCIONES (Paso por referencia). A continuación comprobaremos cómo el hecho de que los parámetros formales sean variables locales produce algunos inconvenientes: por ejemplo, al intentar intercambiar el valor de dos variables mediante una función. Tampoco nos sirve utilizar return porque únicamente nos permitiría retornar un único valor a la función que realiza la llamada, y el otro se perdería. La solución en C está en usar punteros. #include <stdio.h> #include void intercambia(int,int); void main(void) { // NO SE INTERCAMBIAN LOS VALORES DE LAS VARIABLES int x=5, y=10; printf("En principio x= %d, y= %d.\n", x, y); // x=5, y=10 intercambia (x, y); printf("Ahora x= %d, y= %d.\n", x, y); // x=5, y=10 getch(); } //*********************************************************** // Aunque en la función INTERCAMBIA los parámetros formales // se llamen X e Y en lugar de U y V, los valores no se intercambiarán // en la función que realiza la llamada. //*********************************************************** void intercambia(int u,int v) { int aux; //Los valores sólo se intercambian dentro de esta función. // Antes de intercambiar: u=5, v=10 aux = u; u = v; v = aux; } // En este momento: u=10, v=5
__________________________________________________________________________________________________________ Comenzando a programar 12
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
Aunque el convenio de paso de parámetros en C es la llamada por valor, se puede crear una llamada por referencia pasando un puntero al argumento. Como lo que se pasa a la función es la dirección del argumento, se puede modificar el valor del argumento dentro de la función. Los punteros se pasan a las funciones como cualquier otro valor. Por supuesto es necesario declarar los parámetros de tipo puntero. Por ejemplo, a continuación se presenta la función intercambia() para intercambiar el valor de dos argumentos enteros: void intercambia(int *u, int *v) { int aux; aux = *u; //almacena el valor de la vble. a la que apunta U *u = *v; *v = aux; }
El operador * se utiliza para acceder a la variable a la que apunta su operando. De esta forma se intercambia el contenido de las variables utilizadas en la llamada a la función. A las funciones que usan parámetros de tipo puntero se deben llamar con la dirección de los argumentos (PASO POR REFERENCIA). La forma correcta de llamar a la función anterior sería: intercambia(&x,&y); #include <stdio.h> #include //Ahora si funciona bien INTERCAMBIA void intercambia(int *,int *); void main(void) { int x=5, y=10; printf ("En principio x=%d, y=%d.\n", x, y); //x=5, y=10 intercambia ( &x, &y); printf ("Ahora x=%d, y=%d.\n", x, y); //x=10, y=5 getch(); } void intercambia(int *u, int *v) { int aux; aux = *u; *u = *v; *v = aux; }
Ejemplo de llamada por referencia: #include <stdio.h> #include void incrementa(int *); void main(void) { int x=150; printf("En función principal, x = %d.\n",x); // x=150 incrementa(&x); printf("Función incrementa() cambia su valor: x = %d\n",x); // x=160 getch(); } __________________________________________________________________________________________________________ Comenzando a programar 13
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
void incrementa(int *ptr) { *ptr += 10; }
7. MODOS DE ALMACENAMIENTO. Ya sabemos que cada variable tiene un tipo, un identificador, un valor y, ahora estudiaremos que además, también tiene también un modo de almacenamiento. Existen 4 palabras clave para describir los modos de almacenamiento: extern, auto, static y register. El modo de almacenamiento de una variable queda establecido por el lugar donde se define y por la palabra clave empleada, y determina: 1º. Las funciones en las que dicha variable es accesible. Se llama alcance de una variable a la mayor o menor accesibilidad de la misma. 2º. Cuánto tiempo va a persistir una variable en memoria. •
auto
Todas las variables declaradas en una función son, por defecto, automáticas; es decir, que lo son si no se utiliza ninguna palabra clave. Opcionalmente, se puede utilizar la palabra auto para declararlas. Los argumentos formales son necesariamente variables automáticas. Las variables automáticas tienen alcance local, o sea, sólo son conocidas en la función donde se han definido. Cuando la función acaba su tarea, las posiciones de memoria empleadas para sus variables locales se emplearán para otros usos. Por ello, está permitido emplear los mismos identificadores para variables diferentes en distintas funciones. •
extern
Cuando una variable se define fuera de una función se dice que es externa. Dicha variable externa puede ser declarada dentro de la función que la emplea utilizando la palabra clave extern: esta palabra clave informa al ordenador que debe buscar la definición de la variable fuera de la función. Las variables externas tienen alcance global y permanecen en memoria durante toda la ejecución del programa, ya que al no pertenecer a ninguna función en concreto no pueden eliminarse al acabar ninguna de ellas. Nota: extern int equivale a extern. #include <stdio.h> #include int cont=10; //cont es una variable externa void mensaje1(void); void mensaje2(void); void main(void) { int i; extern minimo; //declaración de minimo printf("Cuenta por lo menos hasta %d.\n\n", minimo); for (i=1; i <= cont ; i++) printf("Cuento %d.\n",i); mensaje1(); mensaje2(); __________________________________________________________________________________________________________ Comenzando a programar 14
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
getch(); }//************************************************************ void mensaje1(void) //Aqui se ignora la var. global cont puesto que se define { //una variable local con ese mismo nombre. auto int cont = 7; // si se omite auto funciona igual printf("\nEs verdad que ha contado hasta %d ? \n",cont); printf("No es verdad..."); } //************************************************************ int minimo = 5; //definición de la variable externa minimo void mensaje2(void) { printf("...En realidad ha contado hasta %d.\n",cont); }
-
Definiciones y declaraciones.
Una declaración externa, o bien, una declaración de variable automática, hace que se reserve espacio de almacenamiento para una variable, por tanto, constituye en realidad una definición de dicha variable. Por el contrario, cuando dentro de la función se utiliza la palabra clave extern en una declaración, se está indicando al compilador que busque la variable en otro sitio, o sea, que utilice una variable que ya fue creada: no se trata pues de una definición, sino de una declaración. Luego, no tiene sentido usar la palabra clave extern para realizar una definición externa. Se puede omitir por completo el grupo de declaraciones extern si las definiciones originales aparecen en el mismo fichero y antes de la función que las utiliza. Por ejemplo, obsérvese la variable cont en la función mensaje2(). Sin embargo, el uso de la palabra clave extern permite que una función emplee una variable externa que haya sido definida después de la función en el mismo fichero, o incluso en un fichero diferente (que deben linkarse juntos). Por ejemplo, la variable minimo en la función main(). Cuando se omite la palabra clave extern en la declaración de la variable en una función, y su identificador coincide con el de una variable externa, se crea una nueva variable distinta y automática con el mismo nombre. Conviene en estos casos etiquetar esta segunda variable con la palabra "auto" que se utiliza expresamente cuando se quiere mostrar que intencionadamente se ha evitado una definición de variable externa. Por ejemplo, la variable cont en la función mensaje1(). -
Variables globales y variables locales.
A diferencia de las variables locales, las variables globales son accesibles a lo largo de todo el programa y se pueden utilizar en cualquier parte del código. También hemos estudiado cómo estas variables externas (globales) se crean definiéndolas fuera de cualquier función (habitualmente se definen al principio del programa), de modo que una expresión puede acceder a ellas independientemente de la función en la que se encuentre. (Atención: si la definición de la variable global está por debajo de la función o en otro archivo, será necesario declararla en la función que se quiere emplear usando la palabra clave extern). Además, las variables globales mantendrán sus valores durante toda la ejecución del programa. Si dentro de una función se define una variable local con el mismo nombre que una variable global, todas las referencias a ese nombre dentro de la función donde se ha __________________________________________________________________________________________________________ Comenzando a programar 15
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
declarado la variable local hacen referencia a la variable local y no tienen efecto sobre la variable global. El almacenamiento de las variables globales tiene lugar en una región de memoria fija definida para este propósito por el compilador. Las variables globales son muy útiles cuando se utilizan los mismos datos en muchas funciones del programa. Sin embargo, se debería evitar el uso innecesario de variables globales por tres razones: 1) 2) 3)
Consumen memoria durante toda la ejecución del programa, y no sólo cuando son necesarias. La utilización de una variables global donde se podría declarara una variable local hace que una función sea menos general debido a que depende de algo que debe estar definido fuera de ella. La utilización de una gran número de variables globales puede provocar errores en el programa debido a efectos colaterales desconocidos y no deseados.
Por otra parte, las variables locales son variables que sólo son conocidas por las funciones que las usan. Incluso en el caso de usar el mismo nombre de variable en distintas funciones, el ordenador es capaz de distinguirlas puesto que tienen un ámbito distinto y por tanto, a todos los efectos, son diferentes. Si en un programa no queremos utilizar variables globales y necesitamos comunicar valores entre las distintas funciones hay que utilizar argumentos y return. Es decir, una función podrá recibir todos los valores que necesite a través de los parámetros formales, y podrá devolver un valor a la función que la ha llamado a través de la expresión que se coloca detrás de return. El único problema aquí es que a través de return sólo puede retornar 1 valor. Veremos más adelante cómo salvar esta dificultad mediante el empleo de punteros. •
static
Las variables static son variables permanentes dentro de su propia función o archivo. Estas se diferencian de las variables globales en que no se pueden referenciar fuera de su función o archivo, pero mantienen sus valores entre llamadas. Esta característica las hace muy útiles cuando se escriben funciones generalizadas y bibliotecas de funciones, que puedan utilizarlas otros programadores. Debido a que el efecto de static sobre variables locales es distinto de su efecto sobre globales, se examinarán por separado. -
Variables estáticas locales.
Tienen un alcance local a la función donde se definen, pero no desaparecen cuando la función que las contiene finaliza su trabajo, sino que el ordenador recuerda sus valores si la función vuelve a ser llamada otra vez. Como consecuencia, una variable estática se inicializa una sola vez, cuando se compila el programa. #include <stdio.h> #include int tope_preguntas = 3; int num_pregunta = 0; //En general, no es aconsejable utilizar var. globales void soy_un_mandao(void); __________________________________________________________________________________________________________ Comenzando a programar 16
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
void main(void) { int i; for (i=1; i <= tope_preguntas ; i++) { printf("(Pregunta %d) Cuanto vale un peine?\n", ++num_pregunta ); soy_un_mandao(); printf("\n\n...Y con la rebaja,...\n"); } getch(); } //********************************************** void soy_un_mandao(void) { // no es imprescindible la declaración: extern int num_pregunta; static unsigned precio = 100; printf("\nHoy vale %d. Ya me lo ha preguntado %d %s.\n",precio++, num_pregunta, num_pregunta>1 ? "veces": "vez" ); }
Cuando se aplica static a una variable local, se induce al compilador a reservar memoria permanente para almacenarla de la misma forma que se hace para una variable global. La diferencia clave entre una variable local static y una variable global es que a la variable local static se puede acceder sólo en el bloque en el que está declarada. En términos sencillos, una variable local static es una variable local que mantiene su valor entre llamadas a la función. Precisamente son muy útiles cuando se escriben rutinas que deben conservar un valor entre llamadas, ya que de no existir este tipo de variables tendrían que utilizarse variables globales, abriendo la puerta a posibles efectos colaterales. #include <stdio.h> #include int cuenta (int i); void main(void) { do { cuenta(0); } while (!kbhit()); //kbhit devuelve 0 si no se ha pulsado ninguna tecla printf("Se ha pulsado la tecla %c\n\n", getch()); printf("Funcion cuenta() ha sido llamada %d veces", cuenta(1)); getch(); } int cuenta (int i) { static int c=0; if (i) return c; else c++; return 0; }
En el ejemplo anterior observamos cómo la misma función cuenta() lleva el control de las veces que ha sido llamada mediante una variable estática en lugar de hacerlo mediante una variable global; del mismo modo, podría ampliarse este ejemplo para poder controlar en un programa cuántas veces se llama a cada una de las funciones que incluye. Otro buen ejemplo de una función que necesitaría una variable local static consiste en un generador de números pseudoaleatorios que genere un nuevo número a __________________________________________________________________________________________________________ Comenzando a programar 17
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
partir del último. Se puede declarar una variable global para este valor. Sin embargo, cada vez que se utiliza la función en un programa, tendríamos que acordarnos de declarar dicha variable global y asegurarnos que no entra en conflicto con cualquier otra variable global ya declarada, un gran inconveniente. También, la utilización de una variable global haría difícil colocar esta función en una biblioteca de funciones. La mejor solución es declarar como static la variable que almacena el número generado, como en este fragmento de programa: int aleatorio (void) { static int azar; azar=(azar*25173+13849) % 65536; return azar; }
En este ejemplo, la variable azar mantiene su valor entre las llamadas la función, en lugar de crearse e inicializarse como ocurriría con una variable local normal. Esto significa que cada llamada a aleatorio() puede generar un nuevo elemento de la serie basado en el último número sin la declaración global de dicha variable. Un detalle importante es que la variable estática azar nunca se inicializa explícitamente. Esto significa que la primera vez que se llama a la función aleatorio() tendrá el valor cero por defecto. Aunque esto es aceptable en algunas aplicaciones, la mayoría de los generadores de series necesitan un punto de partida flexible. Para conseguir esto es necesario que se inicialice azar antes de la primera llamada a aleatorio(), lo que podría realizarse fácilmente si azar fuera una variable global. Sin embargo, el evitar tener que hacer global a azar fue el motivo de comenzar la declaración con static. Esto conduce al segundo uso de static : como variable global. -
Variables estáticas globales.
Cuando se aplica el especificador static a una variable global, se indica al compilador que cree una variable global que sólo se pueda acceder en el archivo en el que está declarada la variable global. Esto significa que, aunque la variable sea global, otras rutinas de otros archivos NO pueden tener acceso a ésta o modificar su contenido directamente; en consecuencia no está sujeta a efectos colaterales. Para las escasas situaciones en las que una variable local static no pueda realizar el trabajo, se puede crear un pequeño archivo que contenga sólo las funciones que necesitan la variable global static, compilar el archivo por separado y utilizarlo sin temor a efectos colaterales. Para ver cómo se puede utilizar una variable global static, se vuelve a escribir el ejemplo del generador de números pseudoaleatorios, de modo que se pueda utilizar un valor inicial «semilla» para inicializar la serie a través de una llamada a una segunda función denominada inicia_semilla. #include <stdio.h> #include #define LIMITE 10 unsigned aleatorio(void); void inicia_semilla(void); void main(void) { unsigned cont; inicia_semilla(); for (cont=1;cont<=LIMITE; cont++) printf("%u\n",aleatorio()); __________________________________________________________________________________________________________ Comenzando a programar 18
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
getch(); } static unsigned azar; void inicia_semilla(void) { printf("Introduzca un número como semilla (entre 1 y 65535): "); scanf("%u",&azar); } unsigned aleatorio(void) { azar=(azar*25173+13849) % 65536; return (azar); }
La llamada a inicia_semilla() con algún valor entero conocido inicializa el generador de números pseudoaleatrios. Después de eso, la llamada a aleatorio() generará el siguiente elemento de la serie. Los nombres de las variables locales static sólo se pueden acceder en la función o el bloque de código en el que están declaradas y los nombres de las variables globales static sólo se pueden acceder en el archivo en el que residen. Esto significa que si se colocan las funciones aleatorio() e inicia_semilla() en archivos separados, se pueden utilizar las funciones, pero no se puede hacer referencia a la variable azar. Ésta está inaccesible desde el resto del código del programa. De hecho, incluso se puede declarar y utilizar otra variable llamada azar en el programa (en otro archivo, por supuesto) sin que exista confusión alguna. Esencialmente, el modificador static permite que las variables sean conocidas en las funciones que las necesitan, sin confusión con otras funciones. Las variables static permiten que se hagan inaccesibles ciertas partes del programa desde otras partes. Esto puede suponer una ventaja tremenda cuando se intente realizar un programa muy grande y complejo. #include <stdio.h> #define LIMITE 10 unsigned aleatorio(void); void iniciasem(unsigned); void main(void) { unsigned int cont,semilla; printf("La semilla debe ser numero entre 1 y 65535(0=FIN).\n "); do { printf("\n\nSemilla= "); scanf("%u",&semilla); if (semilla) { iniciasem(semilla); for (cont=1;cont<=LIMITE; cont++) printf("%6u", aleatorio()); } } while (semilla); } //************************************************************ static unsigned azar; void iniciasem(unsigned x) { azar=x; //Se inicializa una sola vez } //************************************************************ __________________________________________________________________________________________________________ Comenzando a programar 19
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
unsigned aleatorio(void) { azar=(azar*25173+13849) % 65536; return (azar); }
•
register.
C dispone de un último especificador de almacenamiento que, originariamente, sólo se aplicaba a variables de tipo int y char. Sin embargo, el estándar ANSI de C ha ampliado su ámbito. El especificador register solicita al compilador que almacene una variable declarada con este modificador de una manera que permita el tiempo de acceso más rápido posible. Para enteros y caracteres, normalmente, esto significa en el registro de la CPU en vez de en memoria, donde se almacenan las variables normales. Para otros tipos de variables, el compilador puede utilizar cualquier otro que signifique disminuir su tiempo de acceso. De hecho, también puede simplemente ignorar por completo la solicitud. En Borland C++, el especificador register se puede aplicar a las variables locales y a los parámetros formales de una función. No se puede aplicar register a las variables globales. Además, debido a que una variable register puede estar almacenada en un registro de la CPU, no se puede obtener la dirección de una variable register (esta restricción sólo se aplica a C, y no a C++). En general, las operaciones sobre variables register se realizan mucho más rápido que sobre variables almacenadas en memoria principal. De hecho, cuando el valor de una variable se encuentra en la CPU, no se requiere acceso a memoria para determinar o modificar su valor. Esto hace que las variables register sean ideales para el control de bucles. A continuación se muestra un ejemplo de cómo declarar una variable register de tipo int y utilizarla para controlar un bucle: esta función calcula una potencia para enteros. int potencia_ent (register int m, register int e) { register int temp= 1; for (; e ; e-- ) temp *= m ; return temp; }
En la práctica general, las variables register se utilizan donde se comportan mejor; esto es, en lugares donde se realizan muchas referencias a la misma variable. Esto es importante puesto que no todas las variables pueden optimizar su tiempo de acceso. Es importante comprender que el especificador register es sólo una solicitud al compilador, que éste es libre de ignorar. Sin embargo, en general, se puede contar con al menos dos variables register de tipo char o int, que se mantengan realmente en un registro de CPU, para cualquier función. Las variables register adicionales se optimizarán en base a la capacidad del compilador.
8. EL PREPROCESADOR.
__________________________________________________________________________________________________________ Comenzando a programar 20
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
El código fuente de un programa en C (o en C++) puede incluir varias instrucciones para el compilador. Toda las directivas del preprocesador comienza con un signo # y cada directiva del preprocesador debe encontrarse en una línea. •
#define
La directiva #define especifica un identificador y una secuencia de caracteres que se sustituirá por el identificador cada vez que se encuentre en el archivo fuente. El identificador se llama nombre de macro y el proceso de sustitución se llama sustitución de macro. El formato general de la directiva es: #define
nombre_macro
secuencia_caracteres
Obsérvese que no aparece un punto y coma en esta instrucción. Puede existir cualquier número de espacios entre el identificador y la secuencia de caracteres, pero una vez que ésta comienza, sólo puede terminar con un carácter de nueva línea. Por ejemplo, si se desea utilizar TRUE en lugar del valor 1y FALSE en lugar del valor 0, entonces se podrían crear dos macros #define: #define #define
TRUE FALSE
1 0
Esto provoca que el compilador sustituya en el archivo fuente el nombre TRUE por un 1 y el nombre FALSE por un 0, cada vez que se los encuentre. Por ejemplo, la siguiente instrucción visualiza «0 1 2» en la pantalla: printf(“%d %d %d”, FALSE, TRUE, TRUE+1); Una vez que se define un nombre de macro, se puede utilizar como parte de la definición de otros nombres de macro. Por ejemplo, el siguiente código define los nombres UNO, DOS y TRES como sus valores respectivos: #define UNO #define DOS #define TRES
1 UNO+UNO UNO+DOS
La sustitución de macro es simplemente el reemplazamiento de un identificador, por su cadena asociada. Por tanto, si se desea definir un mensaje de error estándar, se podría escribir algo como lo siguiente: #define MSJE_ERR
“Error en la entrada de datos.\n”
y luego se podrá utilizar: printf (MSJE_ERR); El compilador sustituye la cadena «Error en la entrada de datos.\n» cuando se encuentra el identificador MSJE_ERR. Para el compilador, la instrucción printf() realmente sería __________________________________________________________________________________________________________ Comenzando a programar 21
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
printf (“Error en la entrada de datos.\n”); Si la cadena supera la longitud de una línea, se puede continuar en la siguiente línea colocando una barra invertida al final de la línea, como se muestra en este ejemplo: #define CADENA_LARGA “Esto es una cadena muy larga que \ se utiliza como ejemplo.” Es una práctica común entre los programadores de C utilizar letras mayúsculas para los identificadores definidos. Esta convención ayuda a cualquiera que lea el programa a descubrir de una ojeada que va a tener lugar una sustitución de macro. También, es mejor poner todos los #define al principio del archivo o, tal vez, en un archivo separado, en lugar de distribuirlos a lo largo del programa. •
#include
La directiva #include indica al compilador que incluya otro archivo fuente en el que aparece dicha directiva. El nombre del archivo fuente adicional se debe encerrar entre comillas dobles o los símbolos < y >. Es correcto que los archivos incluidos, a su vez, contengan directivas #include. Esto se refiere a inclusiones anidadas. Si se especifican nombres con camino explícito como parte del identificador del nombre de archivo, sólo se buscarán los archivos incluidos en estos directorios. En otro caso, si el nombre del archivo se encierra entre comillas, primero se busca en el directorio de trabajo actual. Si el archivo no se encuentra, se busca en el directorio estándar. Si no se especifican nombres con camino explícito y el nombre del archivo se encierra entre los símbolos < y >, se busca el archivo en los directorios estándares. En ningún momento se busca en el directorio actual de trabajo. #include <string.h> // strlen() #include <stdio.h> #include #define DENSIDAD 0.97 //densidad del hombre en Kg/litro #define PIROPO "guapeton/a!" void main(void) { float peso, volumen; int sitio, letras; char nombre[15]; printf ("Hola! Dime tu nombre:\n"); scanf ("%s", nombre); printf ("%s, y ahora tu peso en kg:\n", nombre); scanf ("%f", &peso); sitio = sizeof nombre; letras = strlen (nombre); volumen =peso/DENSIDAD; printf("Bien, %s, %s, tu volumen es %.2f litros.\n",nombre,PIROPO, volumen); printf("Ademas, tu nombre tiene %d letras, y \n", letras); printf("disponemos de %d bytes para guardarlo.\n", sitio); // 15 bytes printf ("La frase del piropo tiene %d letras ", strlen(PIROPO)); printf ("y ocupa %d posiciones de memoria.\n", sizeof PIROPO); getch(); } __________________________________________________________________________________________________________ Comenzando a programar 22
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
Hola! Dime tu nombre: Pepe Pepe, y ahora tu peso en kg: 70 Bien, Pepe, precioso/a!, tu volumen es 72.16 litros. Ademas, tu nombre tiene 4 letras, y disponemos de 15 bytes para guardarlo. La frase del piropo tiene 11 letras y ocupa 12 posiciones de memoria.
En capítulos posteriores veremos otras directivas del preprocesador: #pragma, #if, #else, #undef, #ifdef, etc.
9. COMPILACIÓN Y ENLAZADO. •
Biblioteca y enlace.
Hablando técnicamente, es posible crear un programa en C que sea funcional y útil y que conste únicamente de las instrucciones creadas realmente por el programador. Sin embargo, esto es bastante raro, debido a que dentro de la definición real del lenguaje C no se ofrece ningún método para realizar operaciones de entrada/salida (E/S). Por tanto, la mayoría de los programas incluyen llamadas a varias funciones que se encuentran en la biblioteca estándar de C. El lenguaje C define una biblioteca estándar que proporciona funciones que llevan a cabo las tareas necesarias más comunes. Cuando se llama a una función que no forma parte del programa, el compilador «recuerda» su nombre. Posteriormente, el enlazador combina el código escrito con el código objeto que se encuentra actualmente en la biblioteca estándar. Este proceso se denomina enlazado. •
Compilación separada.
La mayoría de los programas pequeños en C están contenidos completamente en un archivo fuente. Sin embargo, a medida que el programa se hace más grande aumenta el tiempo de compilación y, por otra parte, es necesario aplicar las técnicas de programación modular para facilitar el diseño del programa, su depuración y su mantenimiento. Por ello, el C permite dividir un programa en varios archivos y compilarlos por separado. Una vez compilados todos los archivos, estos se enlazan entre sí, junto con las rutinas de la biblioteca, para generar el código objeto completo del programa. Así se obtienen todas las ventajas de la programación modular como, por ejemplo, el que una modificación que se realice en el código de uno de los archivos no implique volver a compilar el programa completo sino únicamente el módulo afectado. Otra ventaja de la compilación por separado es que, en todos los proyectos, excepto en los más simples, el ahorro de tiempo es sustancial. •
Mapa de memoria en un programa en C.
Un programa compilado en C crea y utiliza cuatro regiones de memoria lógicamente diferentes que se utilizan para funciones específicas. __________________________________________________________________________________________________________ Comenzando a programar 23
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
-
-
•
La zona de memoria que contiene el código del programa. A continuación, otra zona donde se almacenan las variables globales. Después, la zona de memoria dinámica (heap o montículo) que es la región de memoria libre que puede utilizar el programa mediante las funciones de asignación dinámica de C para guardar estructuras como listas enlazadas y árboles. Crece hacia las direcciones altas de memoria. Y, por último, la pila (stack) que se utiliza para una gran cantidad de objetos mientras tiene lugar la ejecución del programa: Mantiene las direcciones de retorno para las llamadas a las funciones, argumentos de las funciones y variables locales. También se utiliza para almacenar el estado actual de la CPU. Crece desde el límite superior de la memoria asignada al programa hacia las direcciones bajas de memoria.
Cómo se compila y enlaza un programa en C.
Aunque hoy en día ya no se hace así, en los primeros tiempos de la era informática al compilar un programa se obtenía código absoluto y todas las direcciones a que se hacía referencia en el programa se fijaban en tiempo de compilación. De esta forma, el programa sólo se podía cargar y ejecutar exactamente en una zona de memoria: la zona para la que se compiló. Fases de la traducción y ejecución
Utilidad
Traducción del código fuente a código máquina
Compilador
Montaje de un archivo ejecutable, con todos los módulos que componen el programa completo adecuadamente enlazados Carga del programa en memoria principal
Montador enlazador
Ejecución del programa
Sistema operativo
Cargador
Actualmente, y desde hace ya tiempo, cuando se compilan los programas se obtiene código reubicable: •
Normalmente, los programas hacen llamadas a diversas rutinas o módulos, bien del propio usuario, o bien del sistema (funciones de la biblioteca), que deben unirse al programa principal. Las diferentes rutinas del usuario pueden estar repartidas en varios archivos. El enlazador será el encargado de combinar físicamente todos esos archivos para generar el archivo ejecutable. Antes de unirse o montarse los diferentes archivos deben estar compilados.
•
Todos los archivos objeto a enlazar han de ser reubicables, en el sentido de que el direccionamiento que utilizan es relativo, o sea, consideran que su primera instrucción comienza en la dirección 0 de memoria. Además, cada vez que desde un archivo se accede a código de otro archivo (cuando se produce una llamada a una función o se utiliza una variable global situadas en otro archivo) el compilador crea una referencia externa. Pues bien, es el enlazador el encargado de sustituir las referencias externas por las direcciones relativas apropiadas.
__________________________________________________________________________________________________________ Comenzando a programar 24
o
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
•
Cuando el archivo se carga en memoria para su ejecución, el cargador convierte automáticamente las direcciones relativas en direcciones absolutas según la posición de memoria en la que se carga el programa. Esto significa que un programa reubicable se puede cargar y ejecutar desde varias posiciones diferentes de memoria.
10. ARCHIVOS DE CABECERA FRENTE A ARCHIVOS OBJETO. Aunque las bibliotecas son parecidas a los archivos objeto, tienen una diferencia crucial: no todo el código de la biblioteca se añade al programa. Cuando se enlaza un programa que consta de varios archivos objeto, todo el código de cada archivo objeto forma parte del programa ejecutable final. Esto sucede tanto si se utiliza como si no se utiliza el código. En otras palabras, todos los archivos objeto especificados en tiempo de enlace se «añaden conjuntamente» para formar el programa. Sin embargo, no es éste el caso de los archivos de biblioteca. Una biblioteca es una colección de funciones. A diferencia de un archivo objeto, un archivo de biblioteca almacena el nombre de cada función, el código objeto de la función y la información de reubicación necesaria para el proceso de enlace. Cuando un programa hace referencia a una función contenida en una biblioteca, el enlazador busca esa función y añade el código al programa. De esta forma, sólo se añaden al archivo ejecutable las funciones que se utilizan en el programa. Puesto que las funciones que son facilitadas con los compiladores de C se encuentran en una biblioteca, sólo se incluirán en el código ejecutable del programa las que éste utilice (si se encontraran en archivos objeto, todos los programas que se escribieran tendrían un tamaño de varios cientos de miles de bytes). •
Bibliotecas estáticas y bibliotecas dinámicas.
En una biblioteca estática el enlazador extrae los módulos precisos cuando enlaza, y los inserta en el fichero que contendrá la imagen ejecutable. En cambio en una biblioteca dinámica el enlazador solamente guarda en el fichero ejecutable unas referencias a las posiciones donde están las funciones requeridas en la biblioteca. Al ejecutar el programa en realidad se ejecuta un programa llamado cargador en tiempo de ejecución (run-time loader) que carga en memoria las funciones necesarias de la biblioteca dinámica en ese momento y ejecuta así el programa completo. Al usar bibliotecas estáticas los ficheros ejecutables son más grandes, pues contienen el código de las funciones de biblioteca. El gasto de memoria del sistema es mayor, pues para cada programa distinto que se esté ejecutando en cierto momento y que use cierta función, provocará que dicha función esté repetida en memoria. Al usar bibliotecas dinámicas los ficheros ejecutables son más pequeños, pues sólo contienen referencias a las funciones, pero se tarda más en cargar el programa en memoria: al iniciar la ejecución, el cargador en tiempo de ejecución debe buscar las funciones y cargarlas. Si además la biblioteca es compartida y si cualquier función ya está en memoria porque algún otro programa la esté usando, se utilizará esa misma copia, por lo cual el gasto de memoria es menor. Un inconveniente de las bibliotecas dinámicas es que los archivos deben estar siempre disponibles.
11. LA BIBLIOTECA ESTÁNDAR ANSI DE C FRENTE A LAS __________________________________________________________________________________________________________ Comenzando a programar 25
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
EXTENSIONES DE BORLAND. Como se sabe, C es un lenguaje bastante pequeño, ya que sólo tiene treinta y dos palabras reservadas; por sí mismo no tiene capacidades de entrada/salida, ni de manejo de memoria dinámica, etc. Todo eso se lleva a cabo mediante funciones externas. Poco transportable sería un programa, y de nada serviría tener un lenguaje normalizado, si para hacer cualquier cosa hubiera que usar una función proporcionada por un fabricante, y cada uno de ellos tuviera una distinta para hacer lo mismo. Por tanto se hizo evidente para el comité ANSI, encargado de la normalización del lenguaje, que también había que encargarse de un conjunto de funciones mínimo pero lo más general y extenso posible. Este conjunto de funciones se ha basado en su mayor parte en unas que han existido prácticamente desde que Dennis Ritchie construyó el primer compilador, o que se han ido añadiendo en máquinas UNIX a lo largo del tiempo. No obstante, y por supuesto, todas funcionan en cualquier compilador ANSI y en cualquier sistema operativo; si no, no se hablaría de estándar, evidentemente. Así que en 1989 el comité X3JI1 del organismo ANSI (American National Standards Institute) terminó de escribir el documento de normalización para el lenguaje y las funciones; poco antes, otro Organismo internacional, ISO (International Standards Organization), formó otro Comité para lo mismo, pues veían que el de ANSI era demasiado americano. Éstos, para evitar que hubiera dos estándares distintos, se pusieron de acuerdo y aceptaron incluir en la biblioteca unas cuantas funciones nuevas y modificar otras, de forma que se pudiera escribir un programa que tuviera en cuenta las características culturales de cada país, como los separadores de decimales en números, o letras formadas por más de un carácter. Aunque ISO acabó su trabajo en 1990, éste sólo difiere del de ANSI en la redacción. El comité de ISO ha seguido trabajando y, a propuesta de varios grupos y subcomités, en septiembre de 1994, publicó un artículo suplementario. A partir de 1995 nuevas reuniones quizá produzcan la versión 2 del estándar ISO de C, y quizá esto influya en modificaciones a la biblioteca estándar. Por supuesto, las funciones normalizadas que conforman la biblioteca estándar estarán presentes en un ambiente de cómputo normal, un sistema con teclado y monitor; los programadores de sistemas como robots o máquinas, así como los escritores de la propia biblioteca pueden no tener ésta presente y usar solamente C puro. Por otra parte existen evidentemente muchísimas más bibliotecas especializadas en cualquier tema: gráficos, sistemas de ventanas, red, seguridad, bases de datos, procesos, matemáticas especializadas, etcétera. Pero si un programa se puede escribir usando solamente la biblioteca estándar, podemos estar (casi) seguros de que se podrá transportar a cualquier ordenador que tenga un compilador C ANSI/ISO. Borland C++ cumple con el estándar ANSI y suministra todas las funciones definidas por éste. Sin embargo, para permitir una utilización y un control de la computadora lo más completos posible, Borland C++ contiene muchas funciones adicionales no definidas por el estándar ANSI. Dichas extensiones incluyen un conjunto completo de funciones de pantalla y gráficos para DOS, funciones especiales de asignación de 16 bits y funciones de directorio. Tal como acabamos de decir, siempre que no se piense transportar los programas que se escriban a un entorno diferente, se pueden utilizar perfectamente estas funciones extendidas.
12. ARCHIVOS DE CABECERA. __________________________________________________________________________________________________________ Comenzando a programar 26
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
Las funciones de la biblioteca usan ampliamente una serie de tipos y macros del preprocesador, que se definen apropiadamente en el fichero de cabecera adecuado. En estos ficheros también se declaran los prototipos de las funciones correspondientes, y quizá alguna variable. Existen 15 cabeceras normalizadas; cada una de ellas corresponde a un grupo de funciones con un determinado campo de acción; por ejemplo, en time.h se declaran tipos, macros y funciones relativas a la información sobre la fecha y hora. No se debe confundir biblioteca con cabecera. Una biblioteca es un archivo que contiene funciones y objetos compilados, en código máquina; podemos decir que ahí están las definiciones de las funciones; el enlazador las extraerá y se cargarán en su momento para su ejecución. En cambio, una cabecera suele ser un fichero de texto con código fuente C; ahí están fundamentalmente las declaraciones de las funciones; el preprocesador le pasa su contenido al compilador para su traducción. Existen quince cabeceras normalizadas por ANSI, y tres más que fueron añadidas en septiembre de 1994 por ISO y tratan de la internacionalización en más detalle que el original ANSI/ISO de 1990. Cabeceras normalizadas assert.h ctype. h errno. h float . h limits.h iso646.h
locale. h math. h setjmp.h signal.h stdarg.h wctype.h
stddel. h stdio. h stdlib.h string. h time.h wchar.h
Algunas notas de interés sobre las cabeceras: -
-
-
Las cabeceras no tienen por qué ser ficheros de texto fuente; en algunos sistemas pueden estar precompiladas, en un formato especial para que el compilador tarde menos en hacer su trabajo. En otros ni siquiera tiene que existir un fichero con ese nombre; puede que simplemente el preprocesador ya sepa qué es lo que tiene que hacer al recibir la directiva include. Sin embargo en UNIX las cabeceras sí son ficheros con texto, código fuente en C; y normalmente están en el directorio /usr/include, con lo que podemos curiosear tranquilamente. Todos los identificadores declarados en las cabeceras deben considerar se como reservados; esto es, no deben redefinirse, ni usarse para otro propósito distinto de aquél para el cual están pensados. Todos los nombres de macros o identificadores que empiecen por el signo de subrayado (_) deben considerarse como de la categoría anterior. Nunca utilice un identificador en su programa que empiece por dicho signo. Las cabeceras pueden incluirse en cualquier orden, incluso más de una vez, pero antes de que se use cualquier función o marro definida en ella. Suelen ponerse al principio del fichero. Use siempre la notación de ángulos para las cabeceras estándar; por ejemplo, #include <stdio.h>
en lugar de
#include "stdio.h”
__________________________________________________________________________________________________________ Comenzando a programar 27
I.E.S. Francisco Romero Vargas –Departamento de Informática Fundamentos de Programación __________________________________________________________________________________________________________
APÉNDICE. Aritmética de Punteros. En los punteros sólo se pueden utilizar dos tipos de operaciones aritméticas: la suma y la resta. Siempre que se incremente o decremente un puntero, éste apuntará a la posición de memoria del elemento siguiente o del anterior de su tipo base. Veámoslo: #include <stdio.h> #include void main(void) { int *ptri, i; double *ptrf, f; ptri = &i; printf("Memoria que ocupa un tipo int = %u bytes \n\n",sizeof(int)); printf("Direccion inicial = %u\n", ptri ); // direccion ptri = 6618620 ptri++; printf("Direccion +1 = %u\n", ptri ); // direccion ptri = 6618624 ptri+=3; printf("Direccion +3 = %u\n", ptri ); // direccion ptri = 6618636 ptri-=2; printf("Direccion -2 = %u\n", ptri ); // direccion ptri = 6618628 ptri--; printf("Direccion -1 = %u\n", ptri ); // direccion ptri = 6618624 //********************************************************************** ptrf = &f; printf("\n\nMemoria que ocupa un double = %u bytes\n\n",sizeof(double)); printf("Direccion inicial = %u\n", ptrf ); // direccion ptrf = 6618608 ptrf++; printf("Direccion +1 = %u\n", ptrf ); // direccion ptrf = 6618616 ptrf+=3; printf("Direccion +3 = %u\n", ptrf ); // direccion ptrf = 6618640 ptrf-=2; printf("Direccion -2 = %u\n", ptrf ); // direccion ptrf = 6618624 ptrf--; printf("Direccion -1 = %u\n", ptrf ); // direccion ptrf = 6618616 getch(); } Memoria que ocupa un tipo int = 4 bytes Direccion Direccion Direccion Direccion Direccion
inicial +1 +3 -2 -1
= = = = =
6618620 6618624 6618636 6618628 6618624
Memoria que ocupa un double = 8 bytes Direccion Direccion Direccion Direccion Direccion
inicial +1 +3 -2 -1
= = = = =
6618608 6618616 6618640 6618624 6618616
Es decir, si por ejemplo consideramos un tipo float, que ocupa 4 bytes, cada vez que se le sume una cantidad k a un puntero a float , el número de posiciones o bytes que avanza el puntero es de 4 x k. Así pues, cuando un puntero se incrementa (decrementa) avanza (retrocede) tantos bytes como los que ocupa su tipo base; y si se le suma (resta) un entero k, avanzará (retrocederá) k por número de bytes que ocupa su tipo base. Además de la suma y resta entre punteros y enteros, la única operación aritmética permitida es la resta entre punteros. Este tipo de operación sólo tiene sentido cuando ambos punteros apunten a un objeto común, como por ejemplo, un array. Este tipo de resta obtiene el número de elementos del tipo base que separan el valor de los dos punteros. Salvo estos tipos de operaciones, el resto de las operaciones aritméticas no están permitidas: no se pueden multiplicar, dividir ni sumar punteros; tampoco se puede sumar o restar tipos float o double a punteros. __________________________________________________________________________________________________________ Comenzando a programar 28