Sie sind auf Seite 1von 62

Programacion en C Eduard Martín 1995

Estos apuntes de “Programación en C” fueron recopilados y pasados a limpio por Eduard


Martín, alumno de la Escola de Sistemes Informàtics (E.S.I.), a partir de las clases que impartí
en esa escuela durante el curso 1994-1995.

El lenguaje no ha cambiado, por lo que los apuntes siguen siendo vigentes, con la excepción
de las partes que hacen referencia a los modelos de memoria y a los punteros de 16 bits
(especialmente el tema 32, así como algunos comentarios relacionados con el tamaño y
alcance de los punteros).

Sergi Jordà, 2003.

página 1
Programacion en C Eduard Martín 1995

1. Introducción
El lenguaje C se deriva de los lenguajes BCPL y del B. Su "nacimiento" lo podemos
fijar a principios de los años 70 de manos de D. Ritchie. El primer compilador en C se
implementó en una máquina unix, lo que hace pensar que el primer unix no había sido escrito
en C. Sin embargo sí que se pensó en el lenguaje C como en una herramienta para el
desarrollo de ese sistema operativo, cosa que ocurrió en la realidad dando muy buenos frutos,
hasta tal punto que en sus primeros tiempos la comunión unix-C fue total.

En 1978 aparece la primera edición del libro "Programación en C" de Ritchie, y por lo
tanto será a partir de esta fecha cuando se empiece a universalizar el uso de este lenguaje
carente hasta este momento de manual alguno.

En 1987 aparece el ANSI C, considerado como el standar de este lenguaje. Entre sus
mejoras destacan el hecho de que convierte al compilador de C en un compilador riguroso y
muy estricto con los errores, cosa que hasta el momento no era así.

2. Características principales

Las más importantes son las siguientes:

a) Es un lenguaje compilado.

b) Es un lenguaje "estructurado" , aunque a diferencia de los verdaderos lenguajes


estructurados, como Pascal, no permite la declaración de procedimientos dentro de otros
procedimientos -estructuración por bloques- En C podemos tener bloques de código -funciones-
que podemos utilizar e invocar a lo largo del programa, pero no podemos declarar una función
dentro de otra.

c) Es un lenguaje de nivel medio en el sentido de que posee todas las ventajas de un lenguaje
de alto nivel pero también alguna de las características más importantes de los lenguajes de
bajo nivel

d) Permite mezclar tipos, de forma relativa, a la hora de operar con variables.

En cuanto a sus ventajas podriamos señalar:

a) Posee un acceso a bajo nivel muy aceptable.


b) Posee rapidez de ejecución.
c) Es muy transportable pese a que trabaja a bajo nivel en muchos casos. Esto es debido a la
existencia de librerías y funciones estándar.
La existencia de funciones estándar es precisamente lo que hace que C sea un
lenguaje transportable. El hecho de que existan pocas instrucciones pero sí muchas funciones,
que van a funcionar de igual modo en una plataforma que en otra, hace que las aplicaciones
desarrolladas en C sean perfectamente transportables por el hecho de que bastará
recompilarlas para la máquina en cuestión para que funcionen sin problemas.

Por lo que respecta a los inconvenientes de C diremos que los inconvenientes mayores
que presenta este lenguaje vienen dados preferentemente por el hecho de la necesidad de
velocidad de ejecución que se pretendía conseguir con él. Esta necesidad produjo que se
diseñará un lenguaje que no verifica buena parte de los errores en tiempo de ejecución ( por
ejemplo el pasarse de los límites de un array en una búsqueda). En cuanto al control de errores
en la compilación diremos que ha mejorado mucho desde la aparición del ansi C.

página 2
Programacion en C Eduard Martín 1995

Por lo que respecta a la aplicación que se hace de este lenguaje diremos que
fundamentalmente se aplica a la elaboración de sistemas operativos, compiladores, desarrollo
de otros lenguajes (como clipper), e incluso elaboración de aplicaciones científicas, donde ha
comenzado a desplazar a fortran. También, en su evolución -C++- es el lenguaje preferido para
el desarrollo de aplicaciones de tratamiento de gráficos e imágenes.

3. Primeros pasos con C

Veamos un primer ejemplo de programa en C:

#include <stdio.h>
void main()
{
printf("Hola")
}

Como vemos en este programa solo tenemos una función main(). Esto significa que es
la función principal, es decir, aquella que iniciará la ejecución del programa.

En cuanto al tipo y tamaño de la letra, diremos que C es sensible a mayúsculas y


minúsculas, y casi todo C está en minúsculas.

El hecho de que distingamos una función es porque detrás lleva paréntesis. Como
podemos ver esta función no lleva parámetros y no retorna nada void. En este sentido diremos
que en C, una función sin salida equivale a una procedure en pascal.

En cuanto a las llaves {} delimitan el ámbito de función; todas las instrucciones, por
otro lado, terminan con punto y coma.

La función printf es una llamada a una función que se encuentra en una librería, y que
nos imprimirá el string que situemos entre comillas dobles.

Cada vez que construimos una función nueva se deberá declarar el prototipo, que se
declarará al principio del programa e indicará qué es lo que le entrará y qué es lo que devolverá.

#include...
void hola(void);
void main()
{
hola();
}

4. Tipos de datos, variables y constantes

#include <stdio.h>
void main()
{
int pies;
fload metros;
printf("Introduce un número de pies: ");
scanf("%d",&pies);
metros=pies * 0.3048;
printf("%d pies son %f metros",pies,metros);

página 3
Programacion en C Eduard Martín 1995

}
Como vemos en el ejemplo anterior, y para ir avanzando tenemos ya dos tipos de
variables:

a) int: integers o enteros.


b) float: de coma flotante o reales.

En C cuando una variable no se inicializa no quiere decir que este a 0. En este caso el
compilador nos avisaría de este hecho. También podremos declarar en una misma línea varias
variables.

En el ejemplo anterior tenemos por otra parte la función scanf. Este función permite
captar un valor por teclado guardándolo en la variable que le indiquemos. Esta variable deberá
llevar, en principio, aunque luego veremos que no siempre es así, un amperseand delante. Esta
función no es la que utilizaremos en la confección de programas complejos, puesto que suele
fallar, además que no nos depura las entradas.

Por otro lado vemos como el `printf varía del anterior:

a) Hallamos en el literal el símbolo % que hace referencia al tipo de variable del final: %f: hace
referencia a un float; %d hace referencia a números en base decimal

Por otro lado diremos que para realizar comentarios dentro de un código escrito en C lo
haremos acotando estos mediante los símbolos */ y /*.

Hemos visto anteriormente la función scanf. Hemos comentado que no es muy fiable,
pese a ello la utilizaremos por el momento. Con esta función no podemos especificar texto:

scanf ("Introducir : " %d",&pies)

La expresión anterior no es correcta. Sí que es correcto lo que sigue:

scanf("%d %d %d",&pies,&x,&y)

Pasemos a ver los diferentes tipos de variables:

TIPO SUBTIPO TAMAÑO RANGO


ENTEROS char 8 bits -128 / +127
short [int] 16 bits -32768 / +32767
int 16 a 32 bits
long [int] 32 bits -2147483648 / +.... 647
REALES float 32 bits + - 3.4E-38 / + - 3.4 E +38
double 64 bits + - 1.7E-308 / + - 1.7E+308
long double 80 bits E+-4932

Los datos del tipo char sirven para almacenar tanto caracteres ASCII como números y
también se utilizan para cálculos pequeños.

En el caso de los int, su tamaño dependerá del procesador, aunque es preferible que
se especifique el tipo long o short para así facilitar la transportabilidad de los programas.

Por otro lado tenemos las cláusulas unsigned y signed. Con estas clausulas
podemos determinar que una variable no tenga signo o lo tenga. Por defecto todas lo tienen por
lo que deberemos especificar (unsigned int x;).

página 4
Programacion en C Eduard Martín 1995

En el caso de que declaremos una variable como sin signo provocaremos que su
capacidad de admisión de valores se duplique. Así los chars podrán contener valores de 0 a
255 y lo short de 0 a 65535.

El modificador unsigned no puede aplicarse a las variables reales.

5. Códigos de Formato
Son los siguientes:

Tipo variable Formato Descripción


Enteros %d Numero asociado a base decimal (short: hasta 16 bits)
%u Numero asociado a base decimal pero sin signo
%ld Numero asociado a base decimal aplicado a long
%lu Numero asociado a base decimal aplicado a long sin signo
%a Visualiza o coge el caracter asociado
%x Numero hexadecimal
%X Visualiza las letras de hexadecimal en mayúsculas
%o Numero asociado a base octal
Reales
Float %f Visualización cifra.cifra
%e %E Notación exponencial (con E la e en mayúsculas)
%g %G Admite dos entradas y adopta dos salidas (exponencial o no)
Double %ef Idem %f pero en double
%le Idem %e pero en double
%lg Idem %g pero en double
%c Visualiza cadenas
%p Visualiza la dirección de los punteros
Especiales %s Visualiza cadenas
%p Visualiza puntero

6. Constantes

Constantes enteras

Una constante numérica tiene por defecto el tipo integer (un short o un long). En
concreto una constante si pasa de 32 bits será un long.

Cuando queremos pasar un valor short a uno long utlitzaremos una L: 100L.

Con la L lo que indicamos es el valor LONG.

Constantes char

Cuando tenemos una constantes del tipo char la spondremos entre comillas
simples.Si ponemos comillas dobles estaremos ante un string:

De esta manera tenemos que:

a) '\n': salta una línea y equivale a '\10' y a 10 (printf("Hola\n") es lo mismo que printf("Hola\10"))

página 5
Programacion en C Eduard Martín 1995

b) '\r': es un retorno de carro y equivale a '\13' o a 13.

c) '\t': es un tabulador

d) '\b': es un retroceso

e) '\\': visualiza un \

Constantes hexadecimales

Las podemos expresar así: 0xconstante, por ejemplo 0x3A

Constantes octales

Por ejemplo 014 (no sería correcto hacer 09)

Constantes reales

Una constante numérica con un punto se considera double (por ejemplo 101.23 y
101). Si quisieramos que una constante real fuese de 32 bits y no de 64 deberiamos añadir una
f (de float) al final. Por ejemplo 101.23f

Las constantes exponenciales funcionan igual.

Por otro lado diremos que las constantes de carácter pueden entrar en cálculos. Por
ejemplo:

int x;
x 0 10 + 'A' daría 75

7. Variables
La sintaxis general para declarar variables sería tipo nomvariable. Podemos declarar
variables seguidas e inicializarlas, así: int x=1, y=55. Por otro lado será imprescindible
inicializar las variables dado que no se pueden asumir ningún valor.

Variables locales

Las variables locales son las que se declaran al principio de un bloque. Estas variables
pueden corresponder a una función, o a un bloque más extenso de la misma función. De esta
manera una variable dentro de un bloque es reconocida en todos los subbloques que hayan. En
ANSI C las variables tienen que estar definidas al principio de bloque, en C++ pueden
declararse donde se quiera

Las variables declaradas en unbloque no se ven en los que cuelgan de éste. De todas
maneras estas variables son accesibles durante todo el programa si son del procedimiento
principal. De todas maneras en aras a la portabilidad de los programas, es mejor no utilizar
variables globales.

Si las variables locales se declaran dentro de los bloques, las globales se declaran
arriba de todo o entre funciones:

#include....

(AQUI SE DECLARARIAN VARIABLES GLOBALES ACCESIBLES EN TODO EL


PROGRAMA)

página 6
Programacion en C Eduard Martín 1995

void main()
{
...
}
(VARIABLES GLOBALES RECONOCIDAS DESDE ESTE PUNTO EN ADELANTE)
void f()
{
...
}

En caso de declarar una variable local con el mismo nombre que una global mandará la
más interna. Por tanto en ste procedimiento no se conocería la global dentro del procedimiento
sino que se reconocería sólo la local.

8. Especificadores de almacenamiento
Existen cuatro especificadores de almacenamiento que harán que todo lo comentado
acerca de las variables quede matizado. Estos especificadores se situarían delante de la
declaración de cada una de las variables (por ejemplo static int x=0), y son los siguientes:

a) STATIC: puede ser aplicado a variables globales y variables locales. Si lo aplicamos a


globales sólo tiene sentido si se programa en varios módulos que una vez compilados por
separado linkaremos juntos.

Tiene sentido si lo aplicamos a variables locales. Lo que hace es que se cree la


variable pero no se destruya al final de la función, por la cual cosa cuando volvamos a esta
función tendrá el valor con el que terminó en la última llamada a aquélla. Es muy importante
inicializar las variables especificadas como estáticas.

b) REGISTER: sólo se aplica a variables locales y en este sentido sólo a variables de tipo
CHAR o INT. No podemos declarar variables de tipo local con el modificador register. Este
modificador crea la variable sobre el registro en vez de en la memoria o en la pila. En general
esto servirá para ganar en rapidez.

De todas maneras el empleo de este especificador tiene limitaciones puesto que sólo
tendrá efecto si el compilador considera oportuno, según sea posible, que esta variable se cree
en registro. En caso de que no pueda ello no producirá un error de compilación, sino que
simplemente el compilador las situará sobre la pila o la memoria.

Cuando especificamos una variable como register el compilador utilizará normalmente


los registros SI y DI puesto que estos registros sólo son utilizados, por lo general, para operar
con cadenas, por lo que si éstas no existen, es más fácil disponer de ellos.

c) EXTERN: se utiliza únicamente con variables globales y solo tiene sentido cuando tenemos
varios módulos de compilación separados

d) AUTO: es un arcaismo proveniente del lenguaje B que no sirve de nada. Antiguamente


servía para especificar variables cuya permanencia empezaba en la función en que se
declaraban finalizando al llegar a la conclusión de las mismas.

9. Entrada y salida de funciones

página 7
Programacion en C Eduard Martín 1995

Si las funciones no devuelven nada y le pasamos como parámetro alguna variable lo


haremos así:

{....
int x;
....
f(x);
}
void f(int a);
{...
}

Esta función realizará algo con el valor que le pasamos como parámetro. Decimos
valor, porque de momento no pasaremos parámetros por referencia.

En el caso de tener que pasar varios parámetros cuando declaremos la función ésta
deberá de recoger los parámetros en el mismo orden y con el mismo tipo que son invocados:

f(x,y)......... void f(int a , float b)

Las funciones que devuelven algun valor no son declaradas con void, sino con el tipo
del valor que retornan y finalizan con la clausula return. Las funciones pueden devolver,
variables, constantes, expresiones e incluso otras funciones.

Una función de este tipo siempre devolverá el valor que sigue a return:

int f(int a, float b);


{
....
return a
}

El valor que retorna una función puede ser asignado a una variable del mismo tipo de
retorno de la función.

Todos los return que situemos en una función deben retornar el mismo tipo que el de la
función. Si la función es void no podrá tener ningún return.

10 Estructura de un programa en C
La estructura de un programa en C sería la que sigue:

includes....... #include <stdio.h>


.....

prototipos... tipo_salida funcion(tipo,tipo,tipo); Ej: int f(int,float,char);


.....

variables globales int x=0,char r='h';


.....

función principal void main()


{
[instrucciones];
}

página 8
Programacion en C Eduard Martín 1995

funciones int f(int x)


{
[instrucciones];
}

void x()
{
[instrucciones];
}
Como vemos después de las instrucciones a ejecutar se pone un punto y coma a
excepción de en los includes, en las definiciones de las funciones y después de una llave.

En este sentido cabe reseñar a los prototipos. Un prototipo viene a ser un resumen de
la función a la que se refiere. Se indican en primer lugar y van a permitir verificar la existencia
de la función en el código y que los parámetros que se le pasen sean del tipo especificado.

En cuanto a las funciones diremos que su resultado puede ser asignado a una variable
y asimismo pueden anidarse: f(f(y),f(c))

11. Los operadores en C


Los operadores pueden ser:

a) Aritméticos
b) Relacionales
c) Lógicos
d) De bits

a) Aritméticos Los operadores aritméticos en C son de menor a mayor precedencia, los


siguientes:

1) Binarios

(+, -): suma y resta


(*,/, %): producto, división, resto

2) Unarios

(-): cambio de signo


(++,--): incremento, decremento

En cuanto al operador / referido a la división diremos que realiza una división de enteros
si los operandos de la misma son dos enteros. En el momento en que uno de ellos no lo sea el
resultado no lo será. Así si :

int a=5, b=2;


float c;
c=a/b producirá el resultado de 2, aunque la c sea un float porque los operandos eran enteros.

En cuanto al operador % diremos que es el MOD del Pascal y sólo funciona con
operandos enteros. En caso de operar con operandos no enteros produce un error.

El operador unario - cambia el signo cuando se aplica a una variable con signo.

página 9
Programacion en C Eduard Martín 1995

Lo más destacable es todo lo concerniente a los operadores ++ y --. Ambos significan


respectivamente aumento y decremento. Lo interesante es que este decremento o aumento del
valor de la variable a la que aplicamos el operador se realizará antes de la operación que se
desee realizar con aqullélla o al final, en función del lugar en que situemos el operador.Así:

++x; -----> incrementa en uno el valor de x

int x=5,y;
y=++x; -----> y valdrá 6 pues el incremento se hace previamente a la asignación.

int x=5,y;
y=x++; ------> y valdrá 5 puesto que el incremento se hace posteriormente a la
asignación del valor de x a y.

int x=5, y;
y=x++;
printf("%d %d",++x,y++) ---> mostrará por pantalla 7 y 5

int x=5, y;
y=x++;
printf("%d %d",x,y) -----------> mostrará por pantalla 6 y 6

Por lo tanto f(++x) es lo mismo que:

++x;
f(x);

Y f(x++) es lo mismo que:


f(x);
++x;

En el caso de que ++x o x++ se encuentren aisladas en el código sin estar asignado
su valor a nada es indiferente en el orden en que se incremente la variable.

Además en C podemos encontrar expresiones como :

variable operador = expresión ..... x * = 2

Esta expresión es equivalente a decir: x= x*2

Cuando el valor de una variable depende de su antiguo valor utilizaremos este tipo de
expresiones, por ejemplo si tenemos una función que nos convierte metros a pies podriamos
hacer:

float centimetros=125
centimetros+=(conversion(pies)*100)

Como vemos para inicializar una variable no es necesario que lo hagamos con valores
constantes, sino que lo podemos hacer con expresiones. En realidad en C todo tiene algún
valor, a excepción de las funciones void.

Por otro lado también hemos visto como en C es fácil combinar operaciones con
variables de distinto tipo. Ahora bien, es imprescindible tener en cuenta las siguientes reglas de
conversión de tipos:

página 10
Programacion en C Eduard Martín 1995

a) En una expresión donde aparecen varias variables, constantes, etc, todo char promueve
temporalmente a un int. Es decir, se convierte temporalmente a un int

char a=100, b=50


a*b

En principio la operación anterior debería desbordar -puesto que sobrepasa el número


256 máximo de char- sin embargo puesto que temporalmente los char promueven a int sería
posible asignar el resultado de la operación a una variable int, siendo esto totalmente correcto.

b) Todos los float promueven a double. Esto quiere decir que siempre se trabaja en C con
doble precisión.
Otra consecuencia de ello es que trabajar con floats hará que se produzcan los
cálculos de forma más lenta puesto que siempre se convertirán a double para operar y se
recoonvertirán después a float. De todas maneras ganaremos espacio en memoria puesto que
un float ocupa la mitad de espacio que un double.

c) Estamos hablando de operadores binarios y en este sentido cuando uno lo es el otro


también. Siempre que en una operación binaria un operador sea binario el otro temporalmente
pasará a serlo.

d) Sólo es aplicable cuando no es aplicable la tercera regla: cuando exista una variable de tipo
long la otra también lo será. De esta manera si :

int x=100, y=500


long l ;
l=x*y; el resultado será un int pero truncado ya que no cabrá en el rango máximo de los int.
Es int porque las dos variables que operan lo son. Para solucionar este problema C dispone de
un operador llamado cast y que se expresa asï:

(tipo) expresión

Lo que hace este operador es convertir temporalmente al tipo indicado la expresión que
viene a continuación. La prioridad de cast es mayor que la de cualquier operador. Así por
ejemplo:

l= (long) x*y. Es correcto puesto que primero se evalúa el cast y se aplica sobre los operadores
que siguen convirtiéndo la x a long y esto se propaga a la y. No sería correcto hacer
l=(long)(x*y) puesto que primero se evaluaria la expresión x*y y se aplicaría el cast al resultado
de ésta cuando el resultado ya se haya truncado.

De la misma manera:

int x=5, y=2;


%lf <- (double)(x/y): primero opera y después visualizaría 2.0000000.

En caso de que hiciesemos (double) x/y se visualizaría 2.5000000 porque primero se ha


evaluado el cast y después se ha operado.

b) Operadores relacionales

Los operadores relacionales son los siguientes:

> : mayor que


>= : mayor o igual que
< : menor que
<= : menor o igual que

página 11
Programacion en C Eduard Martín 1995

== : igual que
!= : diferente que

A diferencia que otros lenguajes estos operadores en C no devuelven un valor booleano


en puridad, sino que devuelve un 1 en caso de que la evaluación sea cierta y un 0 en caso de
que sea falsa. Este retorno puede ser aprovechado pudiendo darse cosas como ésta:

(5> 2)+4 -----> (1) + 4 = 5

c) Operadores lógicos

Son los siguientes:

&& -> AND


|| -> OR
! -> NOT

En el caso de los dos primeros el repetir el símbolo no es un detalle de más puesto


que el no hacerlo los convierte en operadores de bits pudiendo tener entonces, resultados
erróneos.

Estos operadores tambien devuelven 0 y 1 en función de que la evaluación sea falsa o


cierta. Por otro lado tendremos que en C cualquier expresión que no valga 0 es cierta. Así ( x
&& y ) devolverá 0 si x y y valen diferente de 0.

Hay que reseñar que en C si la evaluación de la primera condición es negativa respecto


de la operación (0 si es &&, 1 si es ||) ya no se procede a la evaluación de la segunda. Esto es
importante en el caso de que en las condiciones se produzcan asignaciones u otras acciones
puesto que hay que tener en cuenta que a veces no se producirían según la construcción de la
expresión.

En cuanto al operador !, niega la evaluación de la expresión que sigue. La negación de


cualquier cosa que no vale 0 es 0. Por tanto la negación de 0 será 1. De esta manera la
evaluación de la siguiente expresión será 1 :

int x=5, y=10,z=0


(x&&!y) || !z;

5y0o1

0o1=1

Por otro lado, != no es lo mismo que =!, ya que :

x!=y ---> vale 1 porque x no es igual a y


x=!y ---> x vale 0 puesto que el negado de 10 es 0

El operador condicional

En C existe el llamado operador condicional. Este operador no es ni aritmético, ni


relacional ni lógico. Es un operando ternario pues posee 3 operandos:

(expr1) ? expr2: expr3

Cada una de estas expresiones es un operando. Este tipo de operando realiza lo


mismo que un IF...ELSE.Así evalúa la primera expresión. Si el resultado de la expresión es
diferente de 0 el valor del total del operador será igual al valor resultante de expr2. Si no el valor
será el del resultado de la expresión 3.

página 12
Programacion en C Eduard Martín 1995

12 Sentencias de Control
Existen tres tipos de sentencias de control:

a) Condicionales (if ... else, switch)


b) Iterativas (while, do...while, for)
c) De salto (return, break, continue, goto)

a) Estructuras condicionales

1- IF ... ELSE

La estructura if es la siguiente:
if (condicion)
{
sentencia;
------------;
------------;
}

En cuanto a la condición puede ser cualquier expresión que tenga un valor, incluso una
variable y una constante... etc.

También en C las condiciones pueden ser asignaciones. Ejemplo:

if (c=f(x))
{
....
}

El ejemplo más claro de if seria:

if (c==f(x))
{
....
}

2- SWITCH

La construcción switch nos permitirá construir estructuras condicionales alternativas


del tipo case. La estructura de switch será la siguiente:

switch (expresion)
{
case const2 : sentencias;
--------------;
......;
case const3 : sentencias;
--------------;
......;
case const4 : sentencias;
--------------;
......;
case const5 : sentencias;

página 13
Programacion en C Eduard Martín 1995

--------------;
......;
case constn : sentencias;
--------------;
......;
[default: sentencias;]
}

La expresión como siempre podrá ser de cualquier tipo de los que hemos hablado.
Además las constantes podrán ser numéricas o de caracter.

Switch evalúa la expresión y compara en cada caso hasta que encuentra el valor
idéntico. Si no lo encuentra entra en el caso default, caso que exista. Ahora bien, hay que
tener en cuenta que en esta construcción se ejecutarán las sentencias que correspondan
según el valor igualado a la constante, pero también todas las sentencias a partir de ese
estadio. Para evitar que ocurra esto y que sólo se ejecuten las correspondientes al caso que
nos interesa deberemos utilizar la instrucción break. Esta instrucción provocará la salida de la
estructura switch.
Aparte de break podemos utilizar también return(), teniendo en cuenta que return provoca la
salida de la función donde se inserte esta estructura:

switch(operador)
{
case '+': return(x+y);

case '-': return(x-y);

Otra de las posibilidades de switch es la de utilizar el hecho de que se ejecuten todas


las instrucciones que se encuentren seguidas hasta una ruptura del mismo, para hacer que se
cumplan varias condiciones, así:

case ‘a’:
case ‘A’:
--------
--------
--------

Este fragmento de código se ejecutará si la opción fuese tanto a, como A, puesto que
después del primer case no existe ninguna instrucción y directamente nos vamos a otro case.

De todas maneras esta no es la mejor solución. La mejor solución en este caso sería
la siguiente:

switch(pasamayusc(opcion())
{
case ‘A’...

Es decir seria mejor pasar primero la opción que se pasa como parámetro a switch, a
mayúsculas, para de este modo tener depurado, de entrada, la entrada en el case.

En C existe una función que hace esto y que es toupper(char), que nos pasa lo que le
pasemos como parámetro a mayúsculas.

Como hemos visto con anterioridad para salir de una estructura case debemos hacerlo
con un break o con un return.

página 14
Programacion en C Eduard Martín 1995

b) Estructuras iterativas.

1.- WHILE

La sintaxis de la orden while sería.

while (condicion)
sentencia;

En este caso la condición puede ser cualquier expresión que tenga un valor: 1 o 0. Si
es uno las acciones del bucle se repetiran -verdadero-, si es 0 no se cumplirá la condición.

Como sentencia podemos tener cualquier sentencia o estructura de sentencias


encerradas entre llaves.

Existe una diferencia con if en el while. Esta diferencia es que podemos tener un while
en una sola línea (while (condicion); esto que en un if no tendría sentido, hará que se repita la
instrucción que se autocontenga en la condición cuando esta se cumpla. Por ejemplo
tendríamos que cuando una función retorne cierto se repetirá su ejecución hasta que devuelve
falso: while (seguir()). Como resulta obvio es preciso que la función devuelva algún valor puesto
que si no no tendría sentido esta sentencia.

En función a esto podriamos pensar en la construcción por ejemplo, de un delay:


x=[valor};
while (x--);

El ejemplo anterior produciría un retardo hasta que la x valiera 0.

En las estructuras iterativas del tipo while también podemos utilizar break para salir de
la misma en un momento determinado:

while (condicion)
{
....
if (condición2) break;
....
}

Con break saltaremos a la instrucción siguiente a la llave final.

También tenemos la cláusula continue. Con continue podremos saltar directamente a


la primera línea del bucle:

while (condicion)
{
....
if (condición2) continue;
....
}

En relaciónal ejercicio propuesto hace unos días (calcular una potencia de un número),
diremos que una manera de resolver éste mediante una estructura while sería la siguiente:

double potencia (double base, unsigned int expo);


{
double resul=1;
while (expo > 0)
{

página 15
Programacion en C Eduard Martín 1995

resul*=base;
-- expo;
}
return resul;
}

También podriamos expresar la condición del while de la siguiente manera:

while (expo--);
resul*=base;

2. DO...WHILE

La estructura do..while sería una estructura “repeat until” que mantiene, sin embargo,
su condición de manera afirmativa. En realidad una estructura repeat until, sería una estructura
“do... while (!condicion).

La sintaxis de esta estructura es la siguiente:

do sentencia;
while (condicion);

3. FOR

La construcción for en C tiene una estructura, sintaxis y funcionamiento particulares.


En principio la sintaxis de la misma es la siguiente:

for (inicializacion(1); condicion(2); sentencia(4))


sentencia(3);

Como siempre, la sentencia (3) puede ser una sola sentencia, un bloque de
instrucciones o nada. En este caso hemos de tener en cuenta que será posible un for de una
sola línea. En cuanto a los elementos que componen esta estructura tenemos:

a) inicialización (1): sólo se realiza una vez. Puede ser el típico “y=1” de los for de otros
lenguajes o bien otras cosas.

b) condicion (2): como de costumbre la condición puede ser tan complicada como deseemos.
La única premisa que debe cumplirse es que valga algo. Si la condición se cumple se ejecutará
la sentencia, sino saldrá fuera de la estructura. Cuando entramos en el for ya se evalúa la
condición.

c) sentencia(4): se ejecuta cuando acaba la sentencia (3). En un lenguaje más simple sería l
atípica modificación del contador (y=y+1). En C en este apartado puede ir cualquier sentencia.
Una vez ejecutada volveremos a la condición.

Un ejemplo de for sería:

for (y=0; y<=10; y++)


printf(“%d,y”);

En este ejemplo lo que se hace es entrar en un bucle que hasta que y sea mayor que
10 imprimirá su valor. El aumento del contador lo podriamos hacer en muchos otros lugares (en
el printf, en la condición...)

Todas las partes de un for son opcionales:

página 16
Programacion en C Eduard Martín 1995

a) Si obviamos la inicialización es porque ya la habremos hecho en otro lugar del programa


b) Si no ponemos la sentencia 4 la podemos poner en otro lugar.
c) Si no ponemos la condición diremos que C ante una condición NULA la evalúa como cierta,
es decir generaría un bucle infinito.

En este sentido diremos que en C no es tan extraño provocar un bucle infinito. Es más
es habitual en programas complejos que lo requieran. La manera de generar un bucle infinito
puede tener varias modalidades. Son especialmente usadas:

while(1) for(;;)
{ {
..... .....
} }

Para poder salir de un bucle infinito deberemos de utilizar la orden break. Esta orden
situada convenientemente dentro de una estructura alternativa y dentro de una iteración nos
permitirá salir del bucle infinito cuando se de determinada circunstancia

13 Funciones de E/S de caracteres por cónsola


Existen diversas funciones que servirán para realizar operaciones de entrada/salida de
caracteres por cónsola. Entre ellas debemos señalar:

a) fflush(stdin): esta función limpia el buffer de teclado.

b) char getchar(void): retorna el caracter o caracteres entrados por teclado. La característica


más importante de esta función es que no retorna ningún caracter hasta que se entra un retorno
o bien se llena el buffer de teclado. En realidad no es una función muy utilizada. Un ejemplo de
getchar equivalente a scanf(“%c”,&car), sería car=getchar().

c) void getche(void),void getch(void): estas dos funciones son más utilizadas que la
anterior. Ambas se utilizan en mayor medida que la anterior. La diferencia entre ambas es que
la primera produce un eco en pantalla de lo introducido por teclado. La segunda no produce
este eco.

d) char putchar(char): devuelve el mismo caracter que entramos por teclado. Este valor de
retorno no suele hacerse servir.

Ejemplos:

putchar (getche()): visualizaría el caracter que entramos por teclado dos veces.

putchar(amayusc(getch())): visualizaría lo escrito en mayúsculas. El funcionamiento sería


entrar un caracter que no se vería por pantalla, al utilizar getch, y pasárselo a putchar que lo
devolvería convertido en mayúsculas gracias a la función amayusc.

14. Preprocesador

página 17
Programacion en C Eduard Martín 1995

El preprocesador recibe este nombre porque en todo programa en C antes de compilar


se realizan algunos cambios, por tanto podría recibir el nombre de precompilador. Este
preprocesador lo que hace es mirar las líneas que empiezan con #, es decir, las directivas de
preprocesador. Las directivas más típicas son define e include.

a) INCLUDE: viene seguido siempre del nombre de un fichero que puede estar entre comillas o
entre mayor y menor. La diferencia entre ambos símbolos que encierran el nombre del fichero
es simplemente relativa al hecho de la ubicación en el arbol de directorios del mismo:

1- “ “: si está entre comillas lo busca en el directorio actual en primer lugar.


2- < >: si está entre estos símbolos lo buscará primero en el directorio especificado como
include en las opciones de compilación. Podemos tener varios directorios o rutas para los
include y las podemos especificar poniendo comas entre cada una de ellas.

También podemos especificar el fichero include haciendo referencia a todo su path si


su ubicación es extraña.

Por otro lado estos ficheros suelen tener extensión h , ahora bien, esto no es
obligatorio.

El efecto de un include es la incorporación del fichero señalado en nuestro código


fuente. Es decir, el fichero señalado en el include se copia en nuestro fuente, por tanto, este
fichero deberá de ser un fichero de texto escrito en c.

Normalmente estos ficheros poseen extensión h, como hemos señalado, aunque esto
no es preceptivo. El nombre de los mismos es el de ficheros de cabecera. Estos ficheros no
tienen los fuentes de las funciones que incluyen si no que, en todo caso, harán referencia a los
prototipos de funciones que se hallen en diferentes librerias.

También es posible incluir como un fichero de cabecera un fichero con extensión c, es


decir podriamos incluir un fichero c nuestro, siempre y cuando tengamos presente que no
podemos tener dos funciones main() en un mismo fuente.

b) DEFINE: un define tiene la siguiente sintaxis: #define HOLA 30. Lo que hace un define es
sistuituir en el fuente el segundo valor por lo expresado en el primero. En general se pone en
mayúsculas el primer valor a fin de evitar las sustituciones no deseadas (cambios parciales... )

Podemos utilizar defines para determinar errores en nuestro programa, así si hacemos
#define ERR1 “Error apertura ficheros” y en el código del programa ponemos print(ERR1),
se nos imprimirá el error señalado.

c) MACROS: una macro no es más que un define con argumentos. Así si ponemos:

#define ABS(x) x>0?x:-x; el preprocesador sustituirá el argumento que encuentre en cualquier


expresión ABS por la expresión señalada en el define. Hay que ir con mucho cuidado en estos
casos pues no siempre se producirán los efectos deseados en el define. Así si tenemos

int=-5;
ABS(x);

En el caso anterior se produciría la sustitución de la x por la expresión mostrada más arriba y


por lo tanto la x de ABS valdra -5 puesto que el resultado no lo hemos asignado a ninguna
variable.

De todas maneras si tuviesemos:

int x=-5, y=4;


y=ABS(x+y);

página 18
Programacion en C Eduard Martín 1995

La expresión x+y sería sustituida por x+y>0?x+y:-x+y. Como vemos esto no da un resultado
correcto. Para que hubiese sido correcto el mismo deberiamos haber definido en la macro esta
expresión entre paréntesis:

(x)>0?(x):(-x)

Es más para evitar sorpresas deberiamos haber escrito la expresión en la macro de la siguiente
manera:

( (x)>0?(x):(-x) )

Por tanto en una macro tanto los argumentos como la expresión deberán ir entre
paréntesis para evitar efectos extraños. Además no pondremos punto y coma al final para evitar
que el preprocesador sustituya todas las incidencias poniendo un punto y coma al final.

15. Introducción a los arrays y cadenas


Un array unidimensional se define en C de la siguiente manera:

tipo nombre[tamaño]

a) Tipo: puede ser de cualquier tipo de datos.


b) Tamaño: el tamaño ha de ser constante. No es posible la siguiente expresión int x [t],
aunque sí que sería posible establecer un #define tam 10 para posteriormente hacer int x
[tam]

Otra cosa que se puede hacer es inicializar un array dando valores a todos sus
elementos:

int tabla []={2,4,6,8,10}

Si inicializamos la tabal de esta manera no hará falta que establezcamos la dimensión


de la misma. Si ponemos un tamaño más grande que los elementos que inicializamos, lo que
ocurrirá es que se asignará valor a los primeros elementos, dejando los últimos con valores
indefinidos. En el caso en que inicialicemos más valores que la dimensión especificada en la
tabla se producirá un error.

Hay que tener en cuenta además que en un array el primer elemento es el elemento 0.
Por tanto, un array de cinco elementos va enumerado, siempre del 0 al 4. Por tanto, el tamaño
de un array siempre será igual a su tamaño más uno. De esta manera una forma típica de
recorrer un array sería:

for (i=0, i < tam, ++i)

Los indices de un array pueden ser de ocho o dieciseis bits, chars o shorts. Además,
es conveniente inicializarlos como sin signo (unsigned).

Otro aspecto importante, es el hecho de que C no controla los límites de un array. Si


tuviesemos un array de 10 elementos e intentásemos dar valor a los elementos 24 y 25 no se
produciría error, pero podriamos estar dañando otros datos.

Cadenas

página 19
Programacion en C Eduard Martín 1995

En cuanto a las cadenas diremos que en C no existe el tipo de datos string, como un
tipo especial. Las cadenas no son más que arrays de caracteres, lo que no quiere decir que
todos los arrays de caracteres sean cadenas. Así la expresión:

char dias[]={31,23,.... 28} no es una cadena puesto que su propio aspecto, además de otras
circunstancias así nos lo determinan. Lo anterior es simplemente un array de chars
representados por su valor decimal.

El array char hola []={‘h’,’o’,’l’,’a’} tampoco es una cadena puesto que para que
podamos utilizar las funciones de cadena que nos proporciona C éstas deben acabar en 0 - el
valor binario 0-. Por tanto sí sería una cadena:

char hola []={‘h’,’o’,’l’,’a’,0}

Ahora bien, tampoco es exacta esta declaración puesto que tenemos una forma más
sencilla de declararlas: utilizando las comillas dobles. Cuando utilizamos las comillas dobles
no hace falta que nos preocupemos del cero final porque se encarga el propio C de hacerlo.

char hola []=“hola”

Podriamos recorrer la cadena imprimiendo cada uno de sus caracteres tratándolos


individualmente así:

for (i=0, hola[i] != 0, ++i)


putchar(hola[i]);

Los arrays multidimensionales

Un array multidimensional se declara así:

tipo nombre [tamaño1], [tamaño2]....[tamaño n]

Por ejemplo podemos tener la tabla: int tabla [3] [5]. Esta tabla nos indica que posee tres filas
de cinco columnas cada una de ellas. Por supuesto podriamos inicializar la misma de la
siguientes maneras:

int tabla[3][5]={1,2,3,4,5,1,2,3,4,5,1,2,3,4,5}

int tabla[3][5]={{1,2,3,4,5},{1,2,3,4,5},{1,2,3,4,5}}

La representación gráfica de esta tabla sería:

1 2 3 4 5

1 2 3 4 5

1 2 3 4 5

Como hemos visto en C los arrays se organizan por filas, siendo estas contiguas en
memoria, esto no ocurre siempre así puesto que existen lenguajes, como el Fortran, en los que
la ordenación de las tablas se realiza en relación a las columnas.

Podemos omitir en la declaración de los arrays el identificativo de las filas de que se


compone la tabla puesto que en función de las columnas que señalemos C lo calculará

página 20
Programacion en C Eduard Martín 1995

automáticamente. Esto es porque el número de columnas es indicativo de la estructura de la


tabla que tenemos. Internamente, diremos al respecto que los desplazamientos en una tabla C
los calcula en base a esta fórmula:

(fila * numero_columnas)+columna

Así si quisieramos referenciar el elemento 2,0 en la tabla anterior hariamos:

(2*5)+0 = 10 que corresponde al número 1 de la tercera fila (es la 2 puesto que se comienza
por 0)

El motivo de que se contabilice a partir de 0 en la gestión de tablas es precisamente el


modo de direccionar que tiene C.

Si tenemos la tabla:

char mes [][11]={“Enero”, “Febrero”...”Diciembre”}.

El elemento 1,2 de la tabla sería la letra ‘b’ de Febrero puesto que el número de
columnas harían referencia al número de letras que componen los meses, por ello ponemos 11
como dimensión , es decir, el número máximo de letras 10 más una más, para señalar el final
del string con un 0

La estructura de la tabla sería:

0 1 2 3 4 5 6 7 8 9 10
E n e r o 0
F e b r e r o 0
M a r z o 0

16. Punteros en C
Los punteros son variables que contienen direcciones de memoria. Los punteros son
diferentes en C que en cualquier otro lenguaje. En ensamblador no existe propiamente el
concepto de puntero, en Pascal son más limitados en todos los sentido que en C. En C es casi
imposible no utilizar punteros. Deberemos de tener en cuenta que:

a) La memoria continua estando segmentada en porciones de hasta 64 Kb.


b) Utilizaremos en todo momento, por ahora, punteros de 16 bits.
c) Una variable puntero no es una variable int, aunque tenga su mismo tamaño. Se declaran de
forma especial y son especiales.

página 21
Programacion en C Eduard Martín 1995

x y
10

1000 1002

p1 1000 p2

1004 1006

Los punteros, aunque especiales, son variables y por tanto también tienen una
dirección.En este sentido aclararemos que:

a) La dirección del puntero p1 es 1004


b) El valor de p1 es 1000
c) El contenido de p1 es 10.

Si nos fijamos en x:

a) La dirección de x es 1000
b) El valor de x es 10
c) X , como no es un puntero, no tiene contenido

Una vez se ha comprendido este extremo podemos continuar diciendo que existen dos
operadores para la gestión de punteros:

* Es un operador unario y por tanto no se puede confundir con el signo de multiplicación. El


asterisco nos devuelve el contenido de una variable puntero o de una variable cuyo valor sea una
dirección de memoria. De esta manera *p1 nos devolverá 10, pues el contenido del puntero p1
es igual al valor de la variable a la que apunta (x). Por tanto, este operador tiene el mismo
significado, en grandes rasgos, a los corchetes en ensamblador.

&. Al igual que el anterior es un operador unario y nos devuelve una dirección de memoria, por
tanto lo podemos aplicar a cualquier variable a excepción, en principio de las variables de tipo
register, pues estas no se hallan en memoria. Si aplicamos &x nos devolverá la dirección de x,
es decir, 1000

Veamos los siguientes ejemplos:

a) p1=&x: si hacemos esto asignaremos como valor de p1 la dirección de x, es decir 1000. A


partir de este momento p1 apuntará a la variable x.

b) &x=p: esta es una operación sin sentido, puesto que significaría el cambio de localización
en memoria de la variable x, la cual cosa es absurda, al igual que lo sería el intentar asignar el
valor de una variable a una constante (3=x).

c) *p1=x: a través de esta operación lo que hacemos es asignar a p1 el valor de x al contenido


de p1 (10), por la cual cosa p1 apuntará a la dirección 10.

d) x=*p1: a través de esta operación asignamos como valor de x el contenido de p1. Es decir
asignamos a x 10.

e) &(*p1): con esta operación se produce la devolución de la dirección del contenido de p1, es
decir devolverá la dirección de la variable donde apunta p1 (1000).

página 22
Programacion en C Eduard Martín 1995

No podemos realizar la operación *1002 puesto que cuando damos contenido de una
dirección va a depender el mismo del tipo asociado al puntero. Esto es lo que conocemos con
el nombre de TIPO BASE DEL PUNTERO. Así el contenido de un puntero a un char es un
byte, el contenido de un puntero a un integer es de dos bytes. Por tanto no podremos mezclar
punteros cuyo tipo base sea distinto.

17. Aritmética de punteros


Antes de pasar propiamente a la aritmética de punteros diremos que los punteros se
declaran de la siguiente manera:

int *p;

También es posible declararlos junto a otras variables: int x,y,*p

Asimismo podemos asignarles un valor en la declaración:

int x;
int *p=&x; En este sentido diremos que aunque pueda parecer que estamos asignando al
contenido del puntero la dirección de x, no es así, el contenido del puntero será el valor de x.
En realidad esta expresión es equivalente a p=&x.

Como vimos con anterioridad no podiamos hacer p=x, siendo p un puntero y x un


integer. Ahora bien, sí que podremos hacer:

int x,*p1,*p2,desp;
p1=&x;
p2=p1+desp;

Como vemos podemos añadir a un puntero un int. No podemos añadir chars ni reales a
un puntero. En cualquier caso esto tiene utilidad para desplazarse por la memoria. Estamos
creando un puntero que apunta a otro lugar. No varía el tipo base del puntero que se crea en
función del primero.

En cuanto a las operaciones que podemos hacer con punteros diremos que podremos
sumar, restar, decrementar e incrementar.

Veamos las siguientes operaciones:

*(p++): En esta expresión primero visualizamos el contenido de p y después incrementamos el


puntero (avanzamos una posicion el mismo).
*(++p): en esta incrementamos el puntero y después visualizamos su contenido
++(*p): aquí incrementamos el contenido del puntero y lo visualizamos
(*p)++: visualizamos el contenido del puntero y lo incrementamos

Hemos de tener en cuenta que cuando añadimos un valor a un puntero añadiremos


posiciones. Por tanto si tenemos un puntero de integers y le añadimos un integer estaremos
añadiendole tantas posiciones como exprese ese integer. Así como un integer son dos bytes,
sumar a un puntero de integer 10 supone sumarle 20. Si el puntero apuntaba a la posición
1000, y le sumamos 10 apuntará a 1020. Si se tratase de floats apuntaría, después de hacerle
la suma a 1040... etc.

Por otro lado la resta de punteros de un mismo tipo devuelve un integer que es igual al
número de elementos que existan entre dos punteros. Por tanto si le restamos 1000, siguiendo
el ejemplo anterior, a 1020, el resultado no será 20, sino 10, puesto que automáticamente C

página 23
Programacion en C Eduard Martín 1995

detectará el tipo de datos de que se trata y nos devolverá el número de elementos de ese tipo
que se ha avanzado.

Puesto que si restamos dos punteros obtendremos un número entero, no podemos


restar punteros de distinto tipo base. Al respecto diremos que para visualizar direcciones
tenemos la máscara %p que nos visualizará cuatro dígitos hexadecimales.

18. Paso de parámetros por referencia


Hasta ahora habiamos visto que en C se pasaban los parámetros por valor. Ahora bien,
existe una forma de pasarlos por referencia, y no es otra que especificarlos con el símbolo &
delante.

Cuando llamamos a una función se crea una variable temporal que es copia de la que le
pasamos. Etas es la razón por la que cuando la función finaliza no se ha alterado el valor de la
variable pasada como parámetro. En caso de que pasemos como parámetro a la dirección
donde se halla esta variable, lo que hacemos es modificar directamente el contenido de aquélla,
con lo que estaremos modificando el valor mismo de la variable:

int x=5;
f(&x);

Para que esto se pueda hacer el argument de la función debe de ser del tipo puntero
del mismo tipo base de la variable pasada por referencia. Así la anterior función , el prototipo
sería void f(int *p).

Siempre que pasamos una dirección a una función ésta la debe de recoger un
puntero.Veamos dos ejemplos clarificatorios:

#include <stdio.h>
#include <conio.h>

void swap(int *p,int *r);

void main()
{
int x=7,y=8;
clrscr();
printf("%d %d",x,y);
swap(&x,&y);
printf("%d %d",x,y);
}

void swap(int *p, int *r)


{
int aux=0;
aux=*p;
*p=*r;
*r=aux;
}

#include <stdio.h>
#include <conio.h>

void vario(int a,int b,int *suma,int *resta,int *prod,float *div);

void main()
{
int r,s,p;

página 24
Programacion en C Eduard Martín 1995

int x=5,y=6;
float d;
clrscr();
vario(x,y,&s,&r,&p,&d);
printf("%d %d %d %f",s,r,p,d);
}

void vario(int a,int b,int *suma,int *resta,int *prod,float *div)


{
*suma=a+b;
*resta=a-b;
*prod=a*b;
*div=(a/(float) b);
}

19. Punteros y arrays de 1 dimensión


Imaginemos el siguiente array: char cad[]=“Hola”,*pc;

La representación gráfica del cual seria:

"H" "O" "L" "A" 0

[0] [1] [2] [3] [4]

De esta manera tenemos que &cad[0] será la dirección inicialde la cadena, que
también podemos referenciar de la siguiente manera pc=cad.

Cuando pasamos una cadena siempre la pasamos por referencia y aque no podemos
pasar una cadena por valor. El nombre del array es la dirección inicial del mismo que es una
constante y no se puede modificar:

p=cad, es lo mismo que cad[0]


*(++p), es lo mismo que cad[1]
*(cad+1), es lo mismo que 0
cad+1, es la dirección cad[0]+1
cad[1] es equivalente a (cad+1)
cad[0] es equivalente a *cad

Cuando pasamos una cadena como argumento se puede recibir en un puntero:

pide cad(cad,&0);

correspondería a :

void pidecad (char *pc, int l) o a void pidecad(char s[],int l)

Como ejemplo de ellos veamos la función que nos mostraría una cadena por pantalla

void visucad (char [ cad[] | *cad] )


{

página 25
Programacion en C Eduard Martín 1995

int i=0;
while(cad[i] !=0)
putchar(cad [i++]);
}

Como vemos el nombre del array es la dirección del primer elemento. Podemos recoger
el nombre de una cadena en un puntero porque el nombre de una cadena no es más que una
dirección, por tanto siendo p un puntero (*p) podemos hacer, siendo cadena un array :
p=cadena. Por tanto, cuando pasamos una cadena por referencia no pasamos en sí un array,
sino que pasamos un puntero. Por ello podemos operar con ella como con un puntero.

cad[i] = *(cad+i)

También podemos pasar una dirección incrementándola: visucad (cadena+2)

Por tanto, es mejor declarar las funciones que reciben un array así: visucad (char
*cad); no siendo necesario pasar la longitud del array, puesto que esto no nos sirve de nada
ya que si el array no es numérico se supone que acabará en 0.

Veamos el anterior ejemplo utilizando punteros:

void visucad (char *cad)


{
while (*cad)
putchar(*cad++);
}

En C para recorres un array de forma secuencial normalmente se utilizan punteros por


la razón de que esta es una manera más óptima que la de utilizar un índice: un incremento es
más rápido que una suma.

Por ejemplo una función que buscase un carácter en una cadena utilizando índices
sería, retornando un puntero que señala a la posición donde se ha encontrado el elemento
buscado:

char * busca(char *s, char c)


{
while (*s && *s!=c)
++s;
if (*s) return s;
else return NULL;
}

En el ejemplo que sigue observamos como podemos hacer una función que devuelva la
longitud de una cadena:

int longcad(char *cad)


{
int=0;
while(*cad)
i++;
return i;
}

página 26
Programacion en C Eduard Martín 1995

20. Otras funciones. Inicialización, asignación y


comparación de cadenas
a) Gets(). Gets nos permite introducir por teclado una cadena pero no controla el tamaño de la
misma. Nos devuelve una dirección, en un puntero, a la cadena introducida. Su prototipo es:

char *gets(char *);

b) Puts(). Puts nos permite visualizar un string. Es por tanto en todo equivalente a hacer un
printf(“%s”,cad), pero con la ventaja de una mayor eficiencia. Nos devuelve un int. Su
prototipo es:

int puts(char *);

En cuanto a la inicialización, asignación y comparación de cadenas, diremos que


teniendo char cad[]=“Hola”, que como sabemos es un string (array de cuatro posiciones
más una última con un cero), si hacemos cad=“Adeu” podremos observar como esta
asignación no es correcta. Si lo hacemos ocurrirá un error (Lvalue required...). Todo ello nos
lleva a pensar en que un literal en C no es más que la dirección de la primera letra o carácter
por ello:

printf(“%s”,cad)
printf(“%s”,”Hola”)

que es lo mismo que si tenemos un puntero *p, y hacemos p=cad, y después:

printf(“%s”,p);

Por tanto, en realidad, un literal es equivalente a un array, es más, podriamos decir que
un literal no es más que un array sin nombre.

Por todo ello para modificar el contenido de un array no podremos utilizar una
asignación vulgar y corriente. Para ello deberemos utilizar una función que vaya susituyendo los
elementos del array uno a uno, mediante punteros.

Por ejemplo, veamos la función char *copiar(char *destino, char *origen) que copia
una cadena a otra:

char *copiar(char *d,char *o)


{
char *aux=o;
while(*(d++)=*(o++));
return aux;
}

Veamos seguidamente la función de comparar cadenas. La comparación cad1==cad2


nunca sería cierta puesto que cad1 y cad2 son direcciones diferentes de memoria por lo que
nunca podrán ser iguales al menos que las hayamos declarado con un mismo valor. En cambio
la comparación if(p==cad1) si previamente hemos hecho p=cad1 es cierto pues p es una
variable.

Dos punteros pueden tener el mismo valor y apuntar a una misma dirección de
memoria. Cuando comparemos dos cadenas debemos por tanto utilizar dos punteros que irán
avanzando por la memoria comparando carácter a carácter. En realidad el resultado que se
podría inferir de la función que tiene como prototipo int comparar(char *s1,char *s2) es un

página 27
Programacion en C Eduard Martín 1995

integer que será 0 si las cadenas son iguales, un número positivo si s1 es mayor que s2 y un
número negativo si s2 es mayor que s1.

La función será la que sigue:

int comparar(char *s1,char *s2)


{
int x=0;
while(!x=(*s1-*s2) && *s1++ & *s2++);
return x;
}

Ahora bien, sería conveniente siempre pasar como parámetro también un integer que
nos sirva de tope a la hora de recorrer la memoria:

int comparar(char *s1,char *s2,int n)


{
int x=0;
while(!x=(*s1-*s2) && *s1++ & *s2++ &&n--);
return x;
}

21. Ejercicios de repaso 2 CDIT


/***************************************************************************************
** EJERCICIO 1 DE C **
** Implementar una función que reciba las direcciones de dos cadenas y **
** que produzca por pantalla la segunda mitad de la primera cadena inver **
** tida y copie en la segunda cadena la primera mitad de la primera **
****************************************************************************************

#include <stdio.h>
#include <conio.h>

void funcion_sol(char *,char *);

void main()
{
char cadena1[20]="Hola amigos mios que";
char cadena2[20];
char *punt1=cadena1;
char *punt2=cadena2;
clrscr();
funcion_sol(punt1,punt2);
puts(punt2);
}

void funcion_sol(char *uno,char *dos)


{
int longitud=0, i=0;
char *aux=uno, *aux2=dos;
longitud=(strlen(uno)/2);
for(i=0;i<=longitud;i++) {*dos++=*uno++;}
*dos=0;
dos=aux2;

página 28
Programacion en C Eduard Martín 1995

while(*aux++);
while(aux--!=uno) {printf("%c",*aux);}
}

página 29
Programacion en C Eduard Martín 1995

/************************************************************************************
** 2 EJERCICIO C **
** Escribir una función que reciba dos cadenas y busque en la primera **
** el primer caracter de la segunda **
*************************************************************************************

#include <stdio.h>
#include <conio.h>

void busca_char(char *,char *);

void main()
{
char cadena1[20]="Hola amigos que tal ";
char cadena2[20];
char *uno=cadena1;
char *dos=cadena2;
clrscr();
gets(cadena2);
busca_char(uno,dos);
getchar();
}

void busca_char(char *uno,char *dos)


{
while(*uno && *uno!=*dos) {uno++;}
(*uno==0)?puts("Caracter no hallado"):puts(uno);
}

/***************************************************************************************
** EJERCICIO 3 DE C **
** Confeccionar una función que recibe dos cadenas y devuelve la direcci**
** ón de la primera donde comienza la segunda subcadena o nulo si no lo *
** encuentra **
***************************************************************************************

#include <stdio.h>
#include <conio.h>

int busca_cad(char *,char *);

void main()
{
char cadena1[40];
char cadena2[40];
char *uno=cadena1;
char *dos=cadena2;
int direccion=0;
gets(cadena1);
gets(cadena2);
printf("La direcci¢n es %d",direccion=busca_cad(uno,dos));
}

int busca_cad(char *continente,char *contenido)


{

página 30
Programacion en C Eduard Martín 1995

int auxiliar=0;
while(*continente)
{
if(*continente==*contenido)
{
auxiliar=&continente;
while(*contenido && *continente && *contenido==*continente)
{
contenido++;
continente++;
}
if(*contenido==0) {return auxiliar;}
}
continente++;
}
return 0;
}
/******************************************************************************************
** 4 EJERCICIO DE C **
** Para almacenar las notas de unos alumnos tenemos tres tablas: alumnos **
** asignaturas y notas por asignatura. Confeccionar una función en la que
** pasaremos el nombre de un alumno, el total de los alumnos y el total
** de las asignaturas y nos devolver la media de las notas de ese alumno.
*******************************************************************************************

#include <stdio.h>
#include <conio.h>

float med_al(char *,int,int);

void main()
{
float media=0.0;
clrscr();
printf("La nota media es %f\n",media=med_al("Manolo",5,5));
}

float med_al(char *alums,int tot_al,int tot_asig)


{
char alumnos[5][15]={"Manolo","Pepe","Jose","Eduardo","Pablo"};
char asignat[5][15]={"Matematicas","Literatura","F¡sica","Lengua","Lat¡n"};
float notas[5][5]={{7.5,8.4,9.3,7.2,4.5},{5.5,4.0,3.2,8.6,9.9},
{6.5,4.3,2.1,7.0,5.6},{4.3,5.6,8.7,9.3,2.1},
{5.5,4.6,2.1,6.6,10.0}};
int contador=1, posicion=0;
float total=0.0;
char auxiliar[15];
/*Busco al alumno dentro de la tabla*/
while(tot_al-- && contador)
{
contador=strcmp(alums,alumnos[tot_al]);
(contador==0)?posicion=tot_al:posicion;
}
contador=tot_asig;
/*Busco sus notas*/
while(tot_asig--) {total+=notas[posicion][tot_asig];}
return total/contador;
}

página 31
Programacion en C Eduard Martín 1995

22. Arrays multidimensionales y punteros


Pensemos en el siguiente ejemplo:

char mes [ ] [11]= {“Enero”,.....,”Diciembre”}

En este ejemplo vemos la declaración de un array bidimensional. El primer índice indica


las filas y puede estar vacío pues el cálculo de las mismas se realiza en función del número de
las columnas, segundo índice. Puesto que estamos ante un array de strings, el segundo índice
nos indicará el número máximo de chars que componen cada string más 1 -para el 0 de final de
cadena-

Evidentemente cualquier array, sea de la dimensión que sea, es almacenado en


memoria de una manera lineal. Para comprender como trabaja C debemos saber que si
referenciamos t[i] es lo mismo que si referenciamos *(t+i). El porque de ello es porque esta
refencia no es más que una dirección de memoria que la recogeremos en un puntero.

Pensemos en como hariamos para presentar por pantalla todos los meses del año,
siguiendo la utilización del ejemplo anterior, sin hacer utilización de punteros:

for (i=0; i<12;++i)


{
j=0;
while(mes[i][j] != 0) {putchar(mes[i][j] ; j++;}
}

En cambio esto lo podemos hacer, en C, así:

for (i=0;i<12;++i) {puts(mes[i]);}

Esto que es posible en lenguaje C, no lo es en otros lenguajes. Así en C un array de 5


dimensiones puede referenciarse con uno,dos,tres,cuatro e incluso ningún índice. Esto es
porque el tratamiento de arrays en C no está protegido. Para C mes[i], no es más que una
dirección de memoria que nos referencia un caracter, es decir no es más que equivalente a la
expresión &mes[i][0]

El nombre de un array es la dirección del primer elemento del mismo en un array de


una sola dimensión, esto está claro. Ahora bien, en un array de dos dimensiones, y por
extensión multidimensional, esto no es tan evidente. Por lógica podriamos pensar lo mismo, es
decir que mes[i] es equivalente a &mes[i][0]. Y ciertamente estas dos expresiones son
equivalentes.

El problema se plantea cuando hablamos del nombre del array, en nuestro caso de
mes.

Intentaremos explicar la problemática con más detenimiento: está claro que el nombre
de un array es la dirección del primer elemento del mismo en un array de una sola dimensión.
Así mes es equivalente a &mes[0]. En un array de dos o más dimensiones el nombre del
primer elemento del array es la dirección del primer elemento del mismo. Así mes[0] es
equivalente a &mes[i][0] El problema se plantea con el objeto mes. Este nombre no es una
variable, en definitiva es una etiqueta con un valor, una dirección de memoria. Es en definitiva
una constante y como tal su valor es permanente e inmutable, ya que posee la dirección inicial
del array. La prueba de ello es que no podemos incrementar su valor pues se nos presentará un
error de compilación.

página 32
Programacion en C Eduard Martín 1995

Tampoco podemos hacer algo como char *p=mes[0] puesto que en este caso lo que
estariamos haciendo es destrozar la estructura de array de dos dimensiones que se halla en
memoria y pasariamos a tratar el array de una forma lineal.

Teniendo claro que mes es una etiqueta, una constante cuando nos refiramos a un
array de dos dimensiones y queramos utilizar un puntero deberemos referenciar al mismo como
un puntero que apunta a un conjunto de elementos del tipo que sea. Por ejemplo para
referenciar el array mes hariamos:

char (*p)[11]=mes; esto significa que tenemos un puntero que apunta a once chars, la
segunda dimensión del array. Con ello obtenemos un comportamiento curioso. Cada vez que
incrementemos el puntero saltaremos una fila. Esta es la manera de pasar un array como
parámetro de una función. De todas maneras esto tiene el inconveniente de que esta función
sólo podrá ser utilizada para trabajar con un array cuya segunda dimensión sea como máximo
de 11 chars.

También podemos pasar un array de dos dimensiones como parámetro de una función
simplemente referenciándolo así mes[][11], la cual cosa es la más práctica si utilizamos
índices para trabajar con ellos.

Es importante no olvidarse los paréntesis al referenciar estos arrays con punteros,


puesto que si no los situasemos correctamente correriamos el peligro de referenciar algo que
no es exactamente un array, sino que estariamos haciendo referencia al contenido de 11
punteros.

A modo de ejemplo veamos el siguiente programa:

/**************************************************************************************************************
*******
** EJERCICIO 5
** Manejo de punteros con arrays de dos dimensiones
** En este ejemplo se demuestra como: Referenciar un array de dos dimensiones con punteros
** Pasar un array de este estilo a una función **
** Comparar diversos modos de visuali zar un array de este
** estilo
**************************************************************************************************************
*******/

#include <stdio.h>
#include <conio.h>

void ver(char (*p)[6]); /*Prototipo de función que recibe un array


de dos dimensiones*/

void main()
{
char cadena[][6]={"Uno","Dos","Tres"}; /*Array de dos dimensiones*/
char (*p)[6]=cadena, (*m)[6]=cadena; /*Punteros a este array*/
int i=0;
clrscr();
ver(cadena); /*Paso a función todo el array*/
for(i=0;i<3;i++) {puts(cadena[i]);} /*Visualizo filas con índice*/
for(i=0;i<3;i++) {printf("%s \n",p++);} /*Visualizo filas con puntero*/
for(i=0;i<3;i++) {printf("%d\n",*m); m++; } /*Visualizo dirección filas*/

void ver(char cad[][6])


{
int i=0;

página 33
Programacion en C Eduard Martín 1995

for(i=0;i<3;i++) {(printf("%s \n",cad++));} /*Visualizo array con puntero pasado como parámetro*/
}

página 34
Programacion en C Eduard Martín 1995

El resultado de la ejecución de este programa será:

Uno
Dos
Tres
Uno
Dos
Tres
Uno
Dos
Tres
-30
-24
-18

Como vemos en las tres últimas líneas se visualiza la dirección del primer char de cada
fila. Como vemos la diferencia es de 6, que coincide con el número máximo de columnas por
fila (5 + 1 para el 0 final).

23. Arrays de punteros


Si tenemos *p=“Hola”lo que no podemos hacer es p=“Adios” puesto que si bien la
cadena “Hola” se perdería ocuparía memoria que no podriamos liberar.

Imaginemos el array anterior, de los meses como una estructura en la que tenemos un
puntero para referenciar cada uno de los elementos que la componen (strings):

char *mes[]={“Enero”,....”Diciembre”}

Si realizamos la anterior declaración mes se convierte en un array de punteros a char.


Por tanto *mes[12] son doce punteros que podemos inicializar

...

114 Marzo
106 Febrero
100 Enero
mes

Para visualizar ello podriamos hacer:

for(i=0;i<12;++i)
puts(mes[i]);

Si hiciésemos puts(++mes[i]) se visualizaría todo menos la primera letra de cada uno


de ellos, es más el puntero quedaría modificado para próximas veces.

Veamos las siguientes fórmulas:

- mes[0]= 100
-*mes[0] = “E”
-*mes = 100 ...... > *(mes+i)
-*mes+1 = 101 ..... >(*mes)+1

página 35
Programacion en C Eduard Martín 1995

-mes[0] = 100
-*(mes[0]+1) = “n”
-*(mes+1) = 106
-++ mes = INCORRECTO, pues mes es el nombre de un array y por tanto una constante.
-*(mes[i]+j) = mes[i][j]

En este sentido diremos que la expresión mes[i] es evaluada por el compilador como
*(mes+i). Por tanto mes[i][j] es evaluado como *(*(mes+i)+j).

Por otro lado si tenemos (mes+1) valdrá la expresión 202 si la dirección de mes es
200, puesto que tenemos un array de punteros y los punteros son de 16 bits el incremento va
de 2 bytes en 2 bytes.

-**mes+1 = vale F porque el contenido de mes es 100 y el contenido de 100 es “E”, si


sumamos 1 a E es la F del abecedario.

-**(mes+1)= vale F

-*(*mes+1) = vale n. La n de Enero

24. Punteros a punteros


Cuando tengamos un array de punteros a char al pasarlo como parámetro de una
función deberá ser recogido por un puntero a un puntero de chars y así según cada tipo:

f(mes) ..............> f(char **ppmes) siendo su prototipo f(char **)

Una variable puntero a puntero se podria referenciar también así: *pp[]. Los punteros a
punteros son muy utilizados para pasar arrays de punteros a funciones y modificar los
contenidos de los mismos

25. Los argumentos de la función main()


Normalmente los programas admiten parámetros. La función main() de un programa C
no es una excepción. El paso de parámetros a esta función es especial puesto que debe ser un
número de parámetros variable y casi siempre optativo su uso. Asimismo la función principal
será capaz de devolver un entero.

Cuando invocamos un programa seguido de parámetros en realidad se están


escribiendo cadenas. Por tanto la función main() recibirá como parámetros un entero -que
determinará el número de parámetros que le pasamos- y un puntero a puntero, que hará
referencia a las cadenas que son en sí los parámetros.

El orden de recepción es importante. En este sentido podriamos decir que el prototipo


de la función principal sería el siguiente: void main( int argc, char **argv)

Si una función principal no ha de recibir parámetros y se los pasan igualmente los


aceptará. En realidad no nos hemos de preocupar de como se hace el llamamiento a los
parámetros, de ello se encarga el sistema operativo. Sólo nos hemos de preocupar de pasarle
las cadenas que son los parámetros, pues el número de ellos también lo ofrece el s.o.

El primer parámetro que se le pasa a la función main, por defecto, es el nombre del
programa incluido su path.

página 36
Programacion en C Eduard Martín 1995

Por tanto si hacemos printf(“El nombre del programa es %s”,*argv[0]) se nos


mostrará el nombre del programa.

Como separador de los parámetros sólo podemos utilizar un espacio o bien


tabuladores. Si pasamos un texto entre comillas se considerará un solo argumento.

26. Memoria dinámica


Por ahora para reservar memoria determinábamos o usábamos mucho los arrays. Esto
supone una gran limitación puesto que no se pueden declarar arrays utilizando como dimensión
de los mismos una variable. Otra gran limitación es que no podemos decidir cuándo destruir el
array. Además los arrays, especialmente los de más de una dimensión, son estructuras muy
rígidas que sólo s epueden tratar con estructuras de ese mismo formato.

La solución a todo ello es la memoria dinámica. Existen tres funciones importantes que
son de Ansi C, aunque dependiendo del compilador, podemos tener otras muchas.

a) malloc: esta función nos sirve para reservar memoria. Nos devuelve un puntero al inicio de
la zona de memoria reservada. El tipo de retorno es un void , es decir un puntero a cualquier
cosa. El prototipo de esta función es:

void *malloc(unsigned int byte)

Así si tenemos la variable int *pi, y queremos reservar espacios para 50 ints lo
podremos hacer de esta manera:

pi=malloc(100) o bien pi=malloc(50*sieof(int))

De todas maneras y aunque lo anterior es correcto el compilador nos devolvería algún


warning. Por ello es preferible, aunque el puntero que nos devuelve malloc es de cualquier tipo,
utilizar un cast antes de la asignación:

pi=(int*)malloc(100)

En cuanto sizeof diremos que es un operador unitario que puede ser un tipo o una
variable y retorna el tamaño del tipo o la variable. Además este operador se calcula en
compilación y no frena los programas.

Por otro lado es habitual ver en las ayudas de los compiladores la expresión size_t en
lugar de unsigned int en el prototipo de malloc y de otras funciones. Estas dos expresiones
son idénticas.

En el caso de que reservemos memoria y no exista suficiente de la misma, malloc nos


devolverá un puntero a NULL. Null no es más que un cero. Por tanto siempre que reservemos
memoria deberemos comprobar que no nos devuelva cero:

if(!(pi=(int*)malloc(100)))
puts(“No existe suficiente memoria”)

Asimismo si reservamos memoria dentro de una función perderemos el puntero al inicio


de este espacio pero no podremos volver a utilizar el mismo a menos que lo liberemos con free.
Lo que haremos entonces si queremos conservar la referencia a este espacio reservado, es
hacer que nuestra función devuelva un puntero al mismo:

página 37
Programacion en C Eduard Martín 1995

{
int *pi;
if(!(pi=(int*)malloc(100)))
puts(“No existe suficiente memoria”);
return pi;
}

También podriamos pasar un puntero por referencia. En este caso deberiamos


recogerlo en un puntero a puntero:

reservar(**pi,int n)
{
if(!(*pi=(int*)malloc(100)))
puts(“No existe suficiente memoria”);
}

b) free: esta función es necesaria para liberar la memoria reservada con malloc. Free libera el
espacio medainte determinar como parámetro a l dirección inicial del espacio a liberar. Su
prototipo es :

void free(void *)

Por tanto podriamos mover el puntero inicial y luego eliminar el que hemos igualado a
aquél:

int *p1,*p2;
p1=(int*)malloc(100);
p2=p1;
p1++;
....
free(p2);

c) Realloc: esta funcón nos permite cambiar el tamaño de una zona reservada: supongamos
que hemos reservado 100 pero queremos más espacio pero que sea contiguo en memoria. El
prototipo de la función es:

void *realloc(void *,unsigned int)

Siendo el primer parámetro la dirección de memoria que se va a cambiar, y el segundo el nuevo


tamaño que queramos que tenga.

Supongamos que hemos reservado un tamaño de memoria determinado y ahora


queremos reducirlo porque no lo vamos a utilizar. Realloc efectuará la operación y liberará la
memoria sobrante al final del espacio liberado

En el caso que pidamos más memoria y no exista memoria suficiente contigua a la que
pedimos, realloc actuará así:

a) Creará el espacio de memoria que deseamos en otro lugar.


b) Copiará lo que existia en el espacio de memoria antiguo al nuevo lugar
c) Realizará un free del espacio inicial.

En cualquier caso realloc nos devolverá el puntero al inicio de la zona que le hemos
solicitado. También es mejor recibir este puntero en otra variable diferente al puntero inicial para

página 38
Programacion en C Eduard Martín 1995

así no perder el contenido de aquella zona en caso que realloc devuelva nullo por no existir
memoria disponible.

27. Operadores binarios


Al igual que en ensamblador, en C poseemos operadores binarios. Los que existen en
C son : and, or, xor, not, shr y shl. Los operadores rol y ror no existen en C.

a) AND: representado en C por el operador &. Este operador nos sirve para determinar si un bit
está activado o no y también para desactivar ciertos bits. La forma de operar de and es que los
bits que pongamos a 1 en la máscara no afectarán a la variable a la que la apliquemos, los que
pongamos a 0 determinarán que el resultado sea 0.

También podemos operar con la máscara, así podemos hacer cosas como x&=masc.
Con esta expresión lo que haremos es desactivar bits

Recordemos pues que

11111111
11110111 resultará después de aplicar el and segundo -> 11110111

b) OR: representado por | en C el efecto que produce es: un uno producirá en el resultado un
uno y un cero no alterará el resultado.

Por tanto:

1010
1100 dará como resultado 1110

c) XOR: simbolizado en C por ^ determina que cuando la variable es diferente a la máscara el


resultado es 1 cuando son iguales es 0. Por tanto cuaando la máscara vale cero no tenemos
cambio, y cuando vale 1 tenemos cambio. Por tanto XOR con unos se utiliza para conmutar
determinados bits.

d) NOT: en C se manifiesta con el símbolo C ~ . Este operador cambia todos los bits de una
variable, es decir, produce la generación del complemento a uno. Se utiliza mucho en
combinación con el AND.

e) Desplazamientos (SHL SHR): si realizamos un desplazamiento a la derecha estaremos


realizando una división por dos o múltiplo de dos; si realizamos un desplazamiento a la
izquierda estaremos realizando una multiplicación por dos o múltiplo de dos.

28. Punteros a funciones


Los punteros a funciones son punteros al inicio de la función en memoria, a la dirección
inicial dentro del código donde comienza la función.

Si los punteros son de 16 bits indican un desplazamiento dentro del segmento, pero un
puntero a una función indica un desplazamiento dentro del segmento de código no dentro del
segmento de datos, o de pila como hasta ahora.

página 39
Programacion en C Eduard Martín 1995

Una aplicación de los punteros a funciones es la de crear un array de funciones: cada


elemento del array toma la dirección de una función y esto puede utilizarse en lugar de un
switch.

También se hace servir para el caso de que tengamos una función que aueramos que
dependiendo de algo nos haga diferentes cosas. De esa manera, podriamos psasar como
parámetro de esta función una función a la que ha de llamar. Esto flexibiliza los códigos.

Para pasar una función com parámetro no podemos hacerlo así: f1(f2()). Lo haremos
así:

a) f1(&(f2)): sin embargo esto causa problemas


b) f1(f2): es la forma de hacerlo.

Ahora bien el prototipo de la función f1 si tenemos que la función f2 tiene el prototipo


int f2 (char *) , sería ? f1(int(*pf)(char *)), siendo *pf una función que retorna un int.

La llamada a la función dentro de f1 sería así:

f1(int(*pf)(char *))
{
char *p;
int a;
....
a=(*pf)(p);

...
}

No existe manera de declarar un puntero a función si no sabemos donde apunta. Lo


que si podemos es no poner nada en los parámetros de la función:

int (*p) ( );
p=f;

Asimismo también podremos poner varios parámetros a la función e incluso que sean
de diferente tipo. Incluso podriamos tener funciones con parámetros variables:

int f(int, char ,...);

Los tres puntos indican que se pueden recibir más parámetros: los que queramos.
Dentro de la función deberemos utilizar variables internas del sistema y lo podemos consultar
con la variable va_arg. Este sistema es el aplicado en funciones como scanf y printf.

Los punteros a funciones se utilizan mucho par aordenaciones. En las ordenaciones se


realizan dos acciones básicas: la de comparación y la de cambio de lugar.

La comparación no es algo trivial. No es vaálido el símbolo < para comparar cadenas,


además no es lo mismo comparar dígitos que campos de registros. Por tanto la tarea de
comaprar es muy variable en el código de cualquier algoritmo de comparación. A una función de
ordenación le podriamos determinar cómo se compara para que fuese genérica. El prototipo de
una función de este estilo sería:

ordenar (void *prin,unsigned int n_elem,unsigned int tam_elem,int(*pf)(void *,void *))

Siendo:

página 40
Programacion en C Eduard Martín 1995

prin
n_elem

tam_elem
Si hemos de ordenar algo raro deberemos construir una función y la salida deberá
retornar 0 mayor que 0 o menor que 0. Esta función existe en toda libreria de ansi C y se llama
qsort, que utiliza el algoritmo quick sort. Lo unico que deberemos escribir par aque funcione es
la función que compara. Podrmeos comparar cualquier cosa que comporta posiciones
adyacentes de memoria. Cada tipo de objeto se comparará de una manera por ello crearemos
una función para cada tipo.

De todas maneras la función antes descrita no está completa puesto que deberia
introducir en su prototipo la cláusula const:

(*pf) (const void *, const void *)

Lo que indicamos con esta cláusula es que el valor que sigue es constante y por tanto
no podrá ser modificado en el código. Si lo hacemos se producirá un error en tiempo de
compilación.

Otra gran aplicación de los punteros a funciones es la de crear menús. Así si tenemos:

int f1(void);
int f2(void);
int f3(void);
....
inf (*pf[3])( ) esto es un array de punteros a tres funciones

Para llamar a una de estas funciones hariamos:

while ((i=opcion( ) ) > 0 && , < 3)


(*pf[i] )( );

29 Creación de proyectos
En C por un proyecto entendemos el grupo de fuentes y librerias necesarias para
generar un ejecutable. No todos los lenguajes incorporan la idea de proyecto. Con la opción
make podemos generar un proyecto. Cuando la ejecutemos nos compilará todos los fuentes
que lo compongan y además linkará estos con las librerias que tengamos configuradas en el
entorno de borland creando un solo ejecutable.

Para manejar librerias utilizamos el comando tlib .

Las ventajas de utilizar proyectos son evidentes pues nos permitirán fragmentar los
trabajos.

Con el comando tlib podremos:

a) crear librerias
b) añadir módulos a las librerias
c) borrar objetos de una libreria

página 41
Programacion en C Eduard Martín 1995

Los mandatos o opciones para conseguir esto los podemos conseguir utilizando el
parámetro /? al llamar a tlib. En general son:

+: añade módulos y si no existe la libreria la crea


-: elimina objetos
*: extrae los módulos de la librería dejándolos como ficheros .obj
+-: cambiar un módulo de la librería.

30. Punteros de 32 bits


Hasta ahora teniamos punteros sólo de 16 bits. Sin embargo los punteros en C pueden
ser tanto de 16 como de 32 bits.

Con los punteros a 16 bitsf sólo nos podemos mover por un solo segmento. Con los
punteros de 32 bits nos podremos mover por más de uno. Es decir, con los punteros de 16 bits
únicamente teniamos la posibilidad de un desplazamiento respecto a un segmento por defecto.
Con un puntero de 32 bits se guarda tanto la referencia al segmento como el desplazamiento
sobre el mismo. En este último caso podemos determinar el segmento y por tanto tener
programas más grandes.

Tenemos dos tipos de desplazamientos con los punteros de 32 bits:

a) FAR: en este caso tendremos las siguientes notas características:

- Cuando se incrementa el puntero unicamente se incrementa su parte de desplazamiento.


Podemos, pues, llegar al punto que si el desplazamiento está representado todo por unos al
volver a incrementarlo valdrá 0 y no nos moveremos de segmento.

- Sirve para bloques de memoria inferiores a 64 kb

- Si deseamos 3 bloques de memoria de 60 kb podemos tener 3 punteros far.

- Si deseamos 1 bloque de memoria de 180 kb no podemos hacer servir 3 punteros far, sino
una huge

Cuando comparamos dos punteros far, únicamente comparamos el desplazamiento de


ambosf. La conclusión de esto es que el resultado de la comparación en muchos casos no
tiene ningún valor: Así por ejemplo si tenemos:

char far *p1,far *p2 y tenemos:

seg des
p1 p1 seg des
p2 p2

Si hacemos p1<p2 debería de dar como resultado cierto pero da falso. P1 está en
posición más pequeña que p2 pero en cambio su desplazamiento es más grando y es lo que

página 42
Programacion en C Eduard Martín 1995

está evaluando. La comparación de punteros far tiene sentido cuando estos punteros se
desplazan en un mismo segmento.

b) HUGE: estos punteros están normalizados, es decir se pueden incrementar sin prblemas y
se pueden comparar sin problemas:

Así cuando tenemos un puntero e incrementamos el mismo en 1 lo que hacemos es


incrementar en uno el segmento y se ponen a 0 los cuatro bits del desplazamiento ( en Msdos
tenemos 20 bits: 16 para el segmento y 4 para el desplazamiento)

En cuanto a las posibles asignaciones tenemos:

Punteros far ,huge Punteros near

32 bits <----------------------------------------------16 CORRECTO


far <----------------------------------------------- huge CORRECTO aunque incoherente
16 bits <-----------------------------------------------32 INCORRECTO

Este último caso es incorrecto puesto que perderiamos el segmento , ya que sólo se
copia el desplazamiento

31. Las estructuras en C

En C las estructuras se definen con la palabra reservada struct. La declaración de una


estructura sería como sigue:

struct nombre
{
tipo campo1;
tipo campo2;
.....
.....
};

Como vemos la llave del final lleva punto y coma. Después deberiamos definir variables
de este tipo:

struct nom_var ....;

Las estructuras pueden declararse dentro o fuera de las funciones. Dentro sólo serán
reconocibles por esa función. Fuera serán reconocibles dentro del fuente. Siutilizamos varios
fuentes deberemos declararla en cada uno de ellos o bien en un fichero de cabecera para que
sean accesibles desde todos los módulos.

También poseemos la cláusula typedef para definirlas. Si utilizamos esta cláusula no


arrastraremos durante todo el código la cláusula struct:

typedef struct
{
.......
} ; nom_estructura

En la declaración de variables tendriamos entonces:

página 43
Programacion en C Eduard Martín 1995

nom_estructura var1,var2...;

Por otro lado también es posible delcarar un tipo e inicializar variables del mismo a la
vez:

struct pila
{

....

}p1,p2;

Todo puede ser utilziado como campo de una estructura, incluso otras estructuras
previamente declaradas

Para acceder a los datos de una estructura poseemos el operador punto (.). Por
ejemplo si tenemos:

struct NODO primero siendo NODO:

struct DATOS
{
int edad;
};

struct NODO
{
struct DATOS info;
struct NODO *pnodo;
};

como vemos aquí hacemos referencia a una estructura -datos- que previamente habrá sido
definida y además se hace referencia a un campo que es la misma estructura que estamos
definiendo. Esto último no es posible en otros lenguajes como pascal.

Suponiendo que quisieramos hacer referencia al campo edad hariamos:

primero.info.edad

Las estructuras se pueden inicializar cuando se declaran:

struct CLIENT
{
char nom[10 ];
int edad;
};

struct CLIENT client1={“Pepe”,53};

Además las estructuras se pueden asignar entre si. Así con el ejemplo anterior
podriamos hacer:

struct client2=client1;

Esto aunque sea incoherente pues en C por ejemplo no podemos asignar arrays a
arrays, se puede hacer puesto que C gestiona las estructuras como paquetes de bytes.

página 44
Programacion en C Eduard Martín 1995

Las estructuras se pueden comparar a igualdad o desigualdad (==, !=), pero no a


mayor o menor. En cambio con los campos sí que podremos realizar todas las operaciones
habituales.

Por tanto si el campo nom es un array no podremos hacer algo así:

client1.nom=“Pepe”;

deberemos hacer:

strcpy(client1.nom,”Pepe”);

Paso de estructuras como argumentos de funciones

Si a una función le pasamos esto f1(client1.nom) lo recibirá como f1(char *p) puesto
que nom es del tipo char *. Esto es absolutamente normal.

Si pasamos toda la estructura f1(client) lo deberemos recoger con el tipo que le hemos
dado a struct: f1(struct client cli). Ahora bien esto lo estamos pasando por valor lo que en el
caso de estructuras grandes hará desperdiciar mucho espacio. Para evitar esto es mejor
pasarlas por referencia, es mejor, incluso, que nunca las pasemos por valor.

Para pasar una estructura por referencia o bien lo haremos a través de un puntero, o
bien pasando su dirección de memoria:

f1(&clien1)

f1(struct client *pcli)

Si noqueremos que la estructura se nos modifique dentro de la función , aunque la


pasemos por referencia, deberemos hacer f1(const struct client *pcli).

Para referenciar un campo de una estructra dentra de la función haremos:

(*pcli).nom o pcli->nom

Si queremos hacer referencia a un subcampo de un campo de una estructura:

pcli->nombre.apellido

Por otro lado lla expresión *pcli->nom o *(pcli->nom) nos mostraría el primer caracter,
en este caso, de nom, puesto que nom es la dirección de un campo de chars.

También será correcto pasar un campo por referencia: f2(&client.edad) y lo


recogeremos así f2(int *pedat)

En el caso de tener un array de estructuras : struct client clients[100, lo recibiremos


en una función con un puntero a estructura: f1(clients)... f1(struct clients *pli) Cuando
incrementemos este puntero saltaremos de estructura en estructura.

Asimismo podemos declarar unpuntero a estructura struct client *pli. Este puntero
para inicializarlo lo podremos hacer así:

a) Darle la dirección de una variable del mimo tipo


b) Poniendo el nombre de una arra de este tipo: pcli=clients
c) Con un malloc: pcli=(struct client *)malloc(szeof(struct client))

página 45
Programacion en C Eduard Martín 1995

Veamos unos ejemplos sobre notación de punteros y estructuras. Si tenemos la


estructura:

struct CLIENT
{
int edad;
};

int i=0;
struct CLIENT clis[2 ];
for(i=0;i<2;++i)
clis[i ].edad=i*10;

tendremos en el array las edades 0 y 10. Si hacemos pcli=clis será correcto. Si


tenemos:

for(i=0;i<2;++i)
printf(“%d \n”,pclis->edad); ... dara 0 0 porque no incrementamos el puntero

pclis[i ]->edad: no es correcto, lo correcto es pclis[i ].edad, y dara 0 y 10


++pclis->edad: veremos 1 y 2 porque lo que incrementa es la edad
pclis->edad++: sale 0 y 1 (se aplica al conjunto)
(++pcli)->edad: saldrá 10 y ? porque primero avanza la posición
pcli->++edad: es ilógico pues edad no es una variable, es un campo.

32. Los modelos de memoria en C


En C tenemos los siguientes modelos de memoria en orden creciente:

-tiny
-small
-compact
-medium
-large
-huge

Tanto los modelos tiny como huge no se pueden utilizar en windows. Nosotros siempre
utilizamos el modelo small que utiliza por defecto punteros de 16 bits pero los podemos
declarar de 32. En los otros hasta en el huge, por defecto, los punteros son far.

Cada modelo de memoria condiciona el tamaño de dos punteros:

a) Los que acceden a los datos.


b) Los que acceden al código (que existen siempre aunque no sea explícitamente). Un
procedimiento near está a menos de 64 Kb. En el caso de programas largos deberiamos utilizar
procesos far.

Modelo tiny

Cuando compilamos en este modelo todo (segmento de datos, pila y código) ha de


caber en un solo segmento de 64 Kb. Por tanto CS=SS=DS.

En este modelo todos los punteros son near . Es imposible declarar puneteros a 32
bits. Normalmente se utilizan para hacer programas .com.

página 46
Programacion en C Eduard Martín 1995

Modelo small
Todos los punteros a funciones (que son
punteros a código) serán de 16 bits en este
cs modelo. Además los datos no ocupan más de
Codigo 64 K un segmento. En C los datos los constituyen las
ds variables globales y las estáticas. Tanto los
ss datos como la pila se incluyen en el segmento
Datos
de datos. La pila incluye las variables locales. La
Heap > memoria dinámica se obtiene del heap que
Pila ^ crece en sentido inverso a la pila.

El farheap se utiliza para pedir memoria


Farheap dinámica utilizando 32 bits. Esto quiere decir
que existirán funciones especiales para su
manejo como por ejemplo farmalloc,
farrealloc,farfree.... Estas funciones utilizarán
forzosamente punteros far.

Por tanto en un programa compilado con el modelo small .

a) Estamos limitados por el tamaño del código.


b) Estamos limitados por la cantidad de variables globales y locales
c) No estamos limitados en memoria dinámica.

La función coreleft nos proporciona información sobre la cantidad de memoria que


tenemos en heap y con farcoreleft nos muestra la cantidad de memoria dinámica que hay libre
en farheap.

Modelo compact

El código debe caber en 64 Kb pero en cambio utiliza punteros de 32 bits para acceder
a los datos.
Aquí no se distingue entre heap y farheap.
Esto es porque todas las funciones que trabajan con
cs punteros lo hacen con punteros de 32 bits y por
Codigo 64 K tanto malloc y farmalloc son lo mismo. En cuanto al
ds código todo es igual: punteros de 16 bits. Como
Datos 64 K vemos con el modelo compact y el small se puede
ss conseguir mucha memoria pero es preferible utilizar el
Pila^ 64 K modelo compact cuando manejemos grandes
cantidades de datos.

Heap >

Modelo medium

Es lo inverso: los datos se comportan como en el modelo small (16 bits... ) pero
podemos tener varios segmentos de código. Esto último quiere decir que el código puede ser
más grande pero debemos tener en cuenta que cada módulo debe caber en 64 kb. Esto lo
debemos hacer nosotros utilizando compilación separada.

Todas las llamadas a las funciones se harán far. Para ir más rápido podemos forzar a
que una función sea near (esto podemos decidirlo si estamos seguros que una función sólo

página 47
Programacion en C Eduard Martín 1995

será llamada por código de su propio segmento). La palabra near se pone entre el retorno y el
nombre de la función : tipo near func().

Modelo large

Combina 32 bits de código y 32 bits de datos.

página 48
Programacion en C Eduard Martín 1995

33 Las listas en C
Supongamos una estructura en la que uno de los campos es un puntero a una
estructura igual:

struct REGISTRE
{
......
......
struct REGISTRE *pregis;
}

Si tenemos una estructura así podriamos tener una lista o un encadenamiento simple
de estructuras. La representación gráfica de ello sería:

registre

pregis
registre

pregis

También podriamos tener este encadenamiento dividiendo las estructuras : datos y


puntero:

struct NODO
{
struct REGISTRE reg;
struct NODO *pregis;
};

siendo registre:

struct REGISTRE
{
....
....
}

Si queremos definir un puntero de este tipo haremos struct NODO *pnodo=NULL

En pascal para inicializar una lista hubiesemos utilizado un new(pnodo). En C


precisaremos utilizar la función malloc:

pNodo=(struct NODO *)malloc(sizeof (struct NODO))

Podriamos generar una función para ello:

página 49
Programacion en C Eduard Martín 1995

struct NODO *reservar()


{
pNodo=(struct NODO *)malloc(sizeof(struct NODO));
}

Podriamos asignar el retorno de esta función al puntero a inicializar para reservar


memoria o bien pasar éste por referencia, en cuyo caso el prototipo de reservar sería:

struct NODO *reservar(struct NODO **)

siendo la llamada a la misma reservar(&pNodo);

Como vemos si el retorno es un puntero a nodo se produce una reiteración en la


asignación: retornamos el puntero que ya estamos modificando. Por ello podriamos retornar un
int que nos servirá para controlar si hay memoria suficiente a la hora de reservarla:

if(!reservar(&pNodo))
puts(“No memoria”);

Imaginemos como será una función de insertar en una lista. La función


insertar_inicio(&plista,&reg), nos insertará un nodo al inicio de la lista y le pasamos como
argumento la direcicón del primer nodo y la dirección de los datos de un nuevo nodo.

34. Las uniones en C


Son un tipo particular de estructuras que tienen la característica de que todos los
campos están en la misma dirección de memoria. Sería como dar sinónimos a la misma zona
de memoria. se suelen aplicar con campos de diferentes tipos:

union NOM
{
.....
.....
};

El tamaño de uan unión corresponde al tamaño del campo más grande.

union NOMBRE
{
int i;
char c;
};

Todas las variables comienzan por la misma dirección (la más baja) Si accedemos a c
es como acceder al byte menos significativo de i:

página 50
Programacion en C Eduard Martín 1995

100 101

Por ejemplo:

union NOM x;
x.i = 0 x 1234 //Tendremos en la parte baja 12 y en la alta 34
printf(“%x”,x.c) //Muestra 34
x.o=0x56 //Tendremos en la parte baja 56 y en la alta12
printf(“%x”,x.i) //Mostrará 1256.

union NOM
{
int i;
char[2 ]; // Con c[.. ] puedo acceder a la parte alta y baja de i
};

Hasta ahora también teniamos medios para hacer esto:

int i;
char *pc;
pc=(char *) &i;
pc[0 ] //byte menos significativo
pc[1 ] //byte más significativo.

Podriamos tener hasta:

struct REG
{
char tiporeg;
union REG3 uni;
};

union REG3
{
struct REGA datosA;
struct REGB datosB;
char +[- ];
};

struct REGA
{
char nom[10 ];
.....
};

struct REGB
{
.....

página 51
Programacion en C Eduard Martín 1995

Para acceder a nom deberemos hacer :

struct REG reg;


reg.uni.datosA.nom

Podemos tener una estructura en la que el primer campo determine el formato de como
acceder. Dependiendo de tiporeg accederemos con REGA, REGB o de forma lineal.

35 Campos de bits
Son tiposs particulares de estructuras en las que el tamaño de sus campos se dan en
bits no en bytes, esto hace posible que el tamaño de cada campo pueda ser cualquier número,
y no necesariamente múltiplo de 8.

En un campo de bit que tiene de tamaño 1 byte por tanto, sólo puede tener como
máximo 8 campos.

El hecho de tener campos de bits elimina la necesidad de operar con operadores de


bits. La estructura, a la inversa que el número de bits, sí que tendrá untamaño múltiplo de 8:
una estructura con 9 campos de bits ocupará dos bytes.

En el caso de tene run campo de bits de 7 dependiendo del procesador nos sobrará el
bit más alto (caso de los Intel y compatibles) o los más bajos (caso de los Motorola).

A diferencia de las uniones, no tienen una palabra reservada. Para declararlos se


utilizará la palabra struct. Los campos de bits sólo pueden ser del tipo unsigned. De algún
modo esto nos viene a indicar que todos los bits son iguales.

struct nombre
{
unsigned nombre_campo : longitud; // la longitud puede ser cualquier valor
positivo
}

struct nom
{
unsigned campo1: 1;
unsigned campo2: 3;
unsigned campo3; 2;
unsigned campo4; 2;
};

La estructura anterior ocupa pues 1 byte.

Para acceder a los campos se hará de la forma habitual. Los campos se utilizarán
como numéricos normales y podrán tener el valor que les permite su longitud. No podemos
tener punteros a campos de bits. No podemos aplicar el operador &. Lo que si que podemos
hacer es lo siguiente:

union byte
{
unsigned char c;
struct bits8 bits;
};

página 52
Programacion en C Eduard Martín 1995

36. Los ficheros en C


En C todo el tema de ficheros se hace con funciones. No existen instrucciones
propiamente dichas que gestionen ficheros. Existe cinco grupos de funciones que son
incompatibles y excluyentes entre sí. Si empezamos a tratar ficheros con una familia no
podremos cambiar a otra.

En el primer C que corría en Unix se crearon dos grupos de funciones:

a) Funciones unix de alto nivel o con buffer o con formato.


b) Funciones unix de bajo nivel o sin buffer o sin formato.

Prácticamente se podía hace rlo mismo con ambo grupos. Al crear un C estándar se
escogió crear un tercer grupo, que será el grupo de funciones de ficheros de ANSI C.

En este sentido todo compilador moderno soporta la el grupo de funciones de ANSI C y


las de unix de bajo nivel, ya que las de unix de alto nivel han dejado de existir en la práctica.

Más tarde al entrar en juego el C++, se ofreció una cuarta alternativa al tratamiento de
ficheros, que serán las funciones de ficheros de C++.

Además al hacer programas de windows aparece un quinto grupo de funciones de


ficheros, que serán propias del sistema operativo windows.

En C todos los dispositivos de entrada / salida se tratan igual que un fichero (por
influencia de unix) esto quiere decir que sería posible utilizar las funciones de ficheros
señalando que el dispositivo de entrada es la cónsola y no utilizar las funciones que hasta
ahora hemos visto.

La abstracción entre lo que es un dispositivo físico y no lógico se consigue con un


nuevo tipo de datos llamado FILE. Por tanto, cada vez que d3eseemos tratar con los
dispositivos o un fichero deberemos de declarar un puntero a un dato del tipo FILE: FILE *`pfi.

Este puntero suele ser llamado en los libros como corriente, canal, secuencia... en
ingles es llamado stream¸que no es lo mismo que un handler.

FILE no es más que una estructura definida en el fichero de cabecera stdio.h.


Normalmente no se hace un acceso directo a estos campos de FILE. Toda la información que
hay en el no tiene nada que ver con las propiedades del fichero a la que está asociada la
estructura.

En un programa serán necesarios tantos punteros a FILE como ficheros tengamos


abiertos de forma simultánea. En general se declarar un puntero por cada fichero que estemos
tratando.

Las funciones que existen en C, en general, son pocas y ofrecen el mínimo


imprescindible para tratar ficheros, es decir:

a) Abrir y crear ficheros.


b) Cerrar ficheros.
c) Leer ficheros
d) Escribir ficheros.
e) Posicionarse en un fichero

página 53
Programacion en C Eduard Martín 1995

Por otro lado existen funciones recogidas en ficheros de cabecera, como dir.h, que
realizan funciones a nivel de sistema operativo como eliminar ficheros (remove en stdio.h) y
otras.

En cuanto a la lectura y escritura de ficheros son independientes del tipo de fichero y


debemos decidir nosotros a la hora de determinar de qué manera vamos a leer y escribir
ficheros:

a) Byte a byte.
b) Línea a línea: secuencia de caracteres con un indicador de final de línea (no tienen un
tamaño fijo).
c) Bloque a bloque: n bytes. El tamaño máximo de un bloque es de 64 Kb en el modelo small
d) Con formato: equivalente a printf y scanf. Se hacen conversiones.

No todos los ficheros permiten un mismo tipo de lectura, escritura , posicionamiento...


etc. Por ejemplo no nos podemos posicionar en el monitor...

Existen dos diferencia que decidimos cuando abrimos un fichero:

a) Abrir en modo texto.


b) Abrir en modo binario.

Tampoco es obligatorio abrir un fichero de texto en modo texto. Esto lo decide el


usuario. Por contra abrir un fichero binario en modo texto, aunque se puede hacer, puede
conllevar algún disgusto.

Un fichero texto es una secuencia de líneas que estan compuestas por caracteres
imprimibles separadas por un indicador de final de línea. Lo malo es que en diferentes sistemas
operativos los indicadores de final de línea son diferentes. Esto llevará a la existencia de
diferencias en la apertura en modo texto o binario. Por ejemplo:

a) Unix ...... indicador 10 LF o \n


b) Mac....... indicador 13 CR o \r
c) Dos........ indicador 10 y 13, LF CR, \n

Es curioso que en C sólo sea preciso \n para saltar de línea cuando en ensamblador es
necesario especificar ambos caracteres. Esto es porque cuando escribimos en un fichero
abierto en modo texto al escribir un 10 se añade automáticamente un 13. Por ello cuando
abrimos un fichero en modo texto desaparecen los 13, esto significa que un fichero abierto en
modo texto en memoria tendrá un byte menos por línea.

Funciones de ficheros

a) fopen: se utiliza para abrir ficheros. Para ello precisaremos: el nombre del fichero, el modo
de apertura que deseamos para éste.

FILE *fopen(char *nom_fich, char *modo_apertura)

El nombre del fichero puede expresarse con la ruta completa de directorio de situación,
pero con la precaución de especificar las barras invertidas con otra delante, para evitar
confusiones. Un ejemplo:

FILE *pf;
pf=fopen(“mi_fich”,-.....);

En caso de que el fichero no se pueda abrir por algún motivo esta función retorna
NULL. Por tanto siempre deberesmo mirar que el retorno de fopen no sea null

página 54
Programacion en C Eduard Martín 1995

En cuanto a los modos de apertura (por defecto son en modo texto si no se especifica t
o b):

a) “rt”, “rb”: lectura en modo texto, y binario.


b) “wt”,”wb”: escritura en modo texto y binario
c) “at”,”ab”: modo añadir en texto y binario.
d) “r+t”,”r+b”: lectura y escritura en cualquier lugar
e) “w+t”,”w+b”: lectura y escritura en cualquier lugar
f) “a+t”,”a+b”: nos podemos posicionar como lectura pero solo podemos escribir al final
(añadir)

Las diferencias entre una r y una w es que los ficheros siempre han de existir. Por
tanto si abrimos un fichero no existente con r se devolverá un null. Si lo abrimos con w se
creará. En caso de existir el fichero se borraría en este último caso.

Con a si existe el fichero se abre y si no existe se crea. Cuando queremos abrir un


fichero cmoo lectura y escritura y no queremos machacar el existente -caso de querer modificar
registros- no podemos utilizar la a. En este caso lo abreriamos de una manera y luego
cambiariamos la modalidad. Podriamos escribir:

FILE *pf;
if(!(pf=fopen(“fich”,”r+”))
if(!(pf=fopen(“fich”,”w+”))
puts(“Error en apertura”);

Si deseamos saber de forma detallada el porqué no se ha podido abrir un fichero lo


podemos conseguir mediante algunas funciones:

perror: admite como argumento una cadena, la que queramos y su retorno es un mensaje con
la cadena que hayamos pasado como argumento más dos puntos y un mensaje del sistema
que indica el error que se ha producido. Este mensaje está en ingles. Si quisieramos
modificarlo para que apareciese en castellano deberiamos primero escribir una función que
hiciese algo asi:

printf(“%s:%s”,”fich”,sys_errlist [errno ]) Esta función hace lo mismo que la anterior, lo que


ocurre es que si sustituimos la función sys_errlist por un array de punteros en el que
escribamos traducidos los errores que encontramos en el fichero errno.h, podremos
visualizarlos en el idioma que queramos

Por otro lado hay que decir que cuando hay un error no se modifica el valor de errno.
Para ello hay que hacer clearerr()

b) fclose: permite cerrar ficheros: int fclose(FILE *). Cuando un programa se ejecuta de forma
normal al finalizar correctamente cierra por sí sólo los ficheros por lo que no precisaremos
cerrar los ficheros con esta instrucción. También una salida con exit cerrará los ficheros.

Funciones de lectura y escritura

a) Lectura y escritura carácter a carácter

Son prácticamente las mismas que para el tratamiento desde la cónsola:

getc -> fgetc.............. char getc(FILE *)


putc-> fputc.............. char putc(char,FILE *)

página 55
Programacion en C Eduard Martín 1995

Son válidos con ficheros abiertos en modo binario o en modo texto. Se puede leer y
escribir cualquier cosa. Al in8iciarse un programa en C se abren tres punteros de tipo FILE. No
son modificables:

stdin: puntero a la entrada estándar o cónsola estándar


stdout: puntero a la cónsola de salida estándar
stderr: puntero a la salida de error.

Estos nombres de pueden utilizar en cualquier función de lesctura /escritura de


ficheros: getc(stdin) = getchar( ); putc(c,stdout) = putchar(c).

Por otro lado también tenemos stdprt y stdaux, que nos redirigen la salida hacia el lpt1
y com1 respectivamente.

Podriamos hacer una función que funcionase igualmente ante un programa que recibe
un fichero como parámetro o bien que se le pasa como argumento de entrada el mismo fichero:

FILE *pf;
if(argc==1)
pf=stdin;
else
....

b) Funciones de lectura/escritura por líneas

Son los siguientes:

a) fgets : obtiene una línea


b) fputs: muestra una línea.

Los prototipos son:

char *fgets(char *,int,FILE *). El int determina el núm. máximo de caracteres a leer.
int fputs(char *,FILE *)

fgets lee del fichero indicado en FILE *, lo guarda en char* y lo hace hasta no encontrar
una /n o se llegue al valor señalado en int. Ofrece esta última ventaja frente a gets( ).

fgets retorna la misma dirección inicial o un NULL en caso de final de fichero. Por tanto
una buena forma de controlar el final de fichero en lecturas por líneas es la siguiente:

while(fgets( ))
{
....
}

Los finales de las cadenas en gets se producían cambiando el enter final por un 0. puts
escribía hasta encontrar un 0 y saltaba líneas.

fgets va leyendo de un fichero hasta alguna de las condiciones anteriores. Pone en


memoria la /n y añade un 0 a continuación.

fputs lo que hace es escribir hasta que encuentra un 0 pero después no añade nada.
Por tanto se producirá el sato también porque habrá encontrado la /n antes del 0.

Por tanto no habrá problema si no mezclamos funciones de cónsola con funciones de


ficheros:

página 56
Programacion en C Eduard Martín 1995

while( )
{
gets(s);
strcat(s,”\n”); añadimos nosotros la /n
fputs(s,pfich);
}

Otra forma más ortodoxa de hacerlo sería:

while( )
{
fgets(s,...,stdin);
fputs(s,pfile);
}

página 57
Programacion en C Eduard Martín 1995

c) Funciones de E/S con formato

Son las siguientes:

int fscanf(FILE *,”....”,....)


int fprintf(FILE *,”.........”)

Estas funciones también se utilizan en ficheros abiertos en modo texto. Cuando


escribimos:

int x;
scanf(“%d”,&x);

y escribimos 25000, scanf lee de la cónsola de entrada y lo convierte a formato decimal


guardándolo en x. Por tanto 25000 pasa de tener 5 bytes a 2 bytes - lo que ocupa en memoria
un integer -

Si hacemos printf(“%d”,x), el contenido de x se convertirá a carácter y saldrá por


pantalla. Por tanto estas funciones trabajará con ficheros de texto o bien con ficheros de bases
de datos que estén en formato ASCII.

fscanf devuelve el número de copias leídos que coincidirá con el número de formatos
leídos.

Recordemos que con fscanf podremos realizar tabulaciones:

%10s: moverá 10 espacios al final justificados a la izquierda.

d) Funciones de E/S por bloques

Esta forma no tiene equivalente por cónsola. Sólo se trabaja con ficheros binarios y la
cónsola simempre se abre en modo texto.

Existen dos funciones:

a) int fread(void *pbuffer,unsigned int tam_elem,unsigned int num_elem,FILE *pfin)


b) int fwrite(void *pbuffer,unsigned int tam_elem,unsigned int num_elem,FILE *pfin)

a) fread

Siempre lee un número entero de bytes. Por ejemplo teniendo un fichero con 55 bytes
si queremos leer 100 int leerá 27 (no puede leer 27’5 X 2). Si decimos que lea dos elementos
de 100 bytes no leera nada.

FF = nos devolverá un int inferior al número de elementos que le hemos indicado.

Por ejemplo para leer 100 enteros haremos:

int t[100 ];
fread(t,sizeof(int),100,pfin);

int x;
fread(&x,sizeof(int),1,pfin);
fread(&c,sizeof(char),1,pfin);

página 58
Programacion en C Eduard Martín 1995

Se puede hacer servir como condición:

while(fread(&c,sizeof(char),1,pfin)
{
...
}

Para leer 50 registros de un fichero declararemos un array de 50 estructuras y


leeremos con fread. A fread no le preocupa lo que lee, pero lo hemos de guardar con el formato
correcto.

fread pretende leer unidades de información enteras. Si no hay suficientes para leer no
acabará nunca de leerlo. Si hay 55 bytes en un fichero y le decimos que lea de 9 en 9 sólo
leerá hasta 45.

Por tanto, para evitar esto, le daremos dos valores: el tamaño de la unidad a leer y el
número de unidades a leer.

fread nos devolverá el número de unidades que ha leido : esto significará por lo
general,que si el númerode unidades devueltas es inferior a las solicitadas, es que estamos en
el final de fichero.

b) fwrite.

Hace exactamente lo mismo que el anterior, pero en lugar de leer ficheros escribe.
Con fread si no tienes reservado suficiente memoria el programa se colgaraá en el ecaso de
fwrite si hacemos por ejemplo:

char *s=“hola”;
fwrite(s,1,1000,pfont);

nos llenará 1000 bytes en pfont a partir del inicio del string s. Lo cierto es que con
fwrite podemos simular un puts:

fwrite(s,1,strlen(s),pfont)

La existencia de estas dos funciones nos obliga a conocer otras dos: la que nos
permite posicionarnos en un fichero y la que nos informa sobre la posición actual en un fichero:

fseek
ftell

Sus prototipos son:

int fseek(FILE *,long desp,int pos) : nos posiciona en un fichero.


long ftell(FILE *): nos informa sobre la posición actual

La posición que nos devuelve ftell está en bytes.

fseek se posiciona en un fichero: sus parámetros son: el fichero, cuántos bytes nos
queremos desplazar y a partir de donde:

0 (seek_set) : a partir del principio.


1 (seek_cur): a partir de la posición actual.
2 (seek_end): contando desde el final (en este caso el valor de desplazamiento será
negativo pues iremos del final hacia el principio)

página 59
Programacion en C Eduard Martín 1995

Para desplazarnos multiplicaremos el desplazamiento por el número de registros a


avanzar. Por ejemplo si queremos ir al registro 53:

fseek(pfin,52*sizeof(struct reg),seek_set)

Si fseek nos devuelve un número indicador del éxito del posicionamiento este número
será un 0.

Para ver el tamaño de un fichero podriamos hacer:

i=fseek(pfin,0L*sizeof(struct reg),seek_set);
j=ftell(pfin);

j contendrá el tamaño del fichero.

Un aspecto importante es que siempre entre un fwrite y un fread nos deberemos


reposicionar en el fichero: deberemos hacer un fseek aunque sea con un desplazamiento 0

Funciones de final de fichero


Cuando leemos un fichero byte a byte podemos utilizar la constante predefinida EOF
que está definida en stdio.h. Esta constante tiene el valor -1.

while ((c=getc(pfin)) !=EOF)


putc(c,pfout);

De la manera anterior copiariamos un fichero. EOF no nos indica que ha leido el final de
fichero sino que ya no puede leer más.

Con un fichero de texto funciona pero con uno binario tendriamos problemas ya que
podriamos encontrar un byte que fuese 11111111 y en cambio no sería el final de fichero.

Para solucionar esto poseemos dos caminos:

a) La función feof: int feof(FILE *). Funcion que:

- Devuelve uno cuando llegamos final de fichero.


- Devuelve cero cuando no llegamos a final de fichero.

feof sólo se activa después de una lectura. Ejemplo:

while(!feof(pfin))
putc(getc(pfin),pfout);

Este bucle copia un byte de más ya que la última iteración, cuando se encuentra el
final de fichero, primero lo capta y después pregunta por él. Por tanto en el fichero de destino
se copiaría un byte de más. Una mejora sería la que sigue:

c=getc(pfin)
while(!feof(pfin))
{
putc(c,pfont);
c=getc(pfin);
}

página 60
Programacion en C Eduard Martín 1995

Aquí primero se lee y después de copia. Por ello la mejor solución sería:

página 61
Programacion en C Eduard Martín 1995

while((c=getc(pfin)) !EOF || !feof(pfin))


putc(c,pfout);

En C si la primera parte de un OR es cierto no se evalúa la segunda parte. En este


caso, cuando encontremos un EOF (en un fichero binario) miraremos si es el final de fichero.

b) Que la variable sea un integer. Cuando trabajamos con integers con las funciones de
ficheros, trabajamos con la parte baja del byte.

while((c=getc(pfin) != EOF)
putc(c,pfout);

Cuando encontramos el pf encontraremos 16 bits a 1 y ya no habrá confusiones entre


los EOF de los ficheros binarios y el final de fichero devuelve 16 unos.

página 62

Das könnte Ihnen auch gefallen