Está en la página 1de 28

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 <conio.h>
#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 <conio.h>
#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", &nota);
}
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 <conio.h>
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 <time.h>
#include <conio.h>
#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 <conio.h>
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", x, &x);
// Resultado x=10 Dirección=6618604
printf ("En f2(), y=%d y su dirección es %p. \n", y, &y);
// Resultado: y=24 Dirección=6618626
}

- 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 <conio.h>
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 <conio.h> //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;
aux = *u;
*u = *v;
*v = aux;
}

#include <stdio.h>
#include <conio.h>
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 <conio.h>
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 <conio.h> //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 <conio.h>
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 <conio.h>
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) Consumen memoria durante toda la ejecución del programa, y no sólo


cuando son necesarias.
2) 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.
3) 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 <conio.h>
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 <conio.h>
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 <conio.h>
#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 TRUE 1
#define FALSE 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 1
#define DOS UNO+UNO
#define TRES 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 <conio.h>
#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, Montador o
con todos los módulos que componen el programa completo enlazador
adecuadamente enlazados
Carga del programa en memoria principal Cargador
Ejecución del programa Sistema operativo

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
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 locale. h stddel. h
ctype. h math. h stdio. h
errno. h setjmp.h stdlib.h
float . h signal.h string. h
limits.h stdarg.h time.h
iso646.h wctype.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 <conio.h>
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 inicial = 6618620


Direccion +1 = 6618624
Direccion +3 = 6618636
Direccion -2 = 6618628
Direccion -1 = 6618624

Memoria que ocupa un double = 8 bytes

Direccion inicial = 6618608


Direccion +1 = 6618616
Direccion +3 = 6618640
Direccion -2 = 6618624
Direccion -1 = 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

También podría gustarte

  • Protocolos
    Protocolos
    Documento18 páginas
    Protocolos
    api-3813882
    100% (1)
  • TEMA4
    TEMA4
    Documento28 páginas
    TEMA4
    api-3813882
    Aún no hay calificaciones
  • Config Trijne
    Config Trijne
    Documento1 página
    Config Trijne
    api-3813882
    Aún no hay calificaciones
  • TEMA3
    TEMA3
    Documento40 páginas
    TEMA3
    api-3813882
    Aún no hay calificaciones
  • TEMA5
    TEMA5
    Documento38 páginas
    TEMA5
    api-3813882
    Aún no hay calificaciones
  • Presentacion 4
    Presentacion 4
    Documento62 páginas
    Presentacion 4
    api-3813882
    Aún no hay calificaciones
  • Funciones Paso Por Valor Ejercicios
    Funciones Paso Por Valor Ejercicios
    Documento2 páginas
    Funciones Paso Por Valor Ejercicios
    api-3813882
    100% (1)
  • TEMA1 Apendice
    TEMA1 Apendice
    Documento6 páginas
    TEMA1 Apendice
    api-3813882
    Aún no hay calificaciones
  • Funciones Conio
    Funciones Conio
    Documento2 páginas
    Funciones Conio
    api-3813882
    Aún no hay calificaciones
  • TEMA5
    TEMA5
    Documento38 páginas
    TEMA5
    api-3813882
    Aún no hay calificaciones
  • Ejemplos de Algoritmos
    Ejemplos de Algoritmos
    Documento12 páginas
    Ejemplos de Algoritmos
    api-3813882
    96% (26)
  • Presentacion 4
    Presentacion 4
    Documento62 páginas
    Presentacion 4
    api-3813882
    Aún no hay calificaciones
  • TEMA3
    TEMA3
    Documento41 páginas
    TEMA3
    api-3813882
    100% (1)
  • TEMA3
    TEMA3
    Documento41 páginas
    TEMA3
    api-3813882
    100% (1)
  • TEMA1 Apendice
    TEMA1 Apendice
    Documento6 páginas
    TEMA1 Apendice
    api-3813882
    Aún no hay calificaciones
  • Presentacion 6
    Presentacion 6
    Documento25 páginas
    Presentacion 6
    api-3813882
    Aún no hay calificaciones
  • Presentacion 2
    Presentacion 2
    Documento45 páginas
    Presentacion 2
    api-3813882
    Aún no hay calificaciones
  • Presentacion 5
    Presentacion 5
    Documento65 páginas
    Presentacion 5
    api-3813882
    Aún no hay calificaciones
  • Presentacion 1
    Presentacion 1
    Documento32 páginas
    Presentacion 1
    api-3813882
    Aún no hay calificaciones
  • Presentacion 3
    Presentacion 3
    Documento62 páginas
    Presentacion 3
    api-3813882
    Aún no hay calificaciones