Professional Documents
Culture Documents
Manual práctico
programación en C
Fundamentos de Informática
Ingeniería Técnica Industrialalidad
Curso 2008/2009
Índice
1.
Introducción. .............................................................................................................. 3
2.
Entrada / salida básica................................................................................................ 4
3.
Sentencias condicionales. ........................................................................................ 12
4.
Bucles....................................................................................................................... 20
5.
Funciones ................................................................................................................. 26
6.
Punteros (paso por referencia) ................................................................................. 30
Paso por referencia...................................................................................................... 31
7.
Estructuras ............................................................................................................... 37
8.
Arrays....................................................................................................................... 40
Ordenación.................................................................................................................. 43
9.
Cadenas de caracteres .............................................................................................. 48
10.
Matrices ................................................................................................................... 52
11.
Ficheros.................................................................................................................... 56
12.
Reserva dinámica de memoria ................................................................................. 63
Apéndice A. Normas de estilo de programación ............................................................ 67
Identificadores significativos ...................................................................................... 67
Constantes simbólicas................................................................................................. 68
Comentarios ................................................................................................................ 69
Estructura del programa.............................................................................................. 70
Indentación o sangrado ............................................................................................... 73
13.
Apéndice B .............................................................................................................. 78
Antonio Becerra Permuy
1. Introducción.
Para crear una nueva aplicación es necesario pinchar sobre la opción de la ventana
cental Create a new project o bien acceder al menú File, ahí seleccionar New y después
Project. Toda aplicación es para Codeblocks un proyecto. Si bien los ejemplos que
veremos en este curso son muy sencillos y todo el código fuente estará únicamente en
un fichero, una aplicación real suele tener su código fuente dividido en varios ficheros,
lo que facilita su desarrollo y posterior mantenimiento, ya sea por una persona o por un
equipo de personas. Un proyecto es, por tanto, un conjunto de ficheros a partir de los
cuales se generará una aplicación.
Tras seleccionar la opción para crear un nuevo proyecto nos aparece el cuadro de
diálogo de la Figura 2.
La instrucción printf que aparece en la línea 6 del programa muestra por pantalla el
mensaje “Hola mundo” en inglés. Este tipo de instrucción forma parte del contenido de
este tema y se verá en detalle a continuación. Decir, por último, que para compilar y
ejecutar este programa de prueba bastaría con ir al menú Build y seleccionar Build and
Run (o bien seleccionar la tecla F9). Los detalles sobre el funcionamiento del resto de
menús y opciones del Codeblocks serán explicadas en clase y además se pueden
consultar en la documentación on-line: http://www.codeblocks.org/user-manual.
int main()
{
float radio;
return 0;
}
El programa sólo usa una variable: radio. Esta variable está definida dentro de la
función main, por tanto es una variable local. Las variables locales sólo son accesibles
dentro de la función en la que son definidas. Si estuviese definida fuera de la función,
después de los #include, por ejemplo, sería una variable global. Las variables globales
son accesibles desde cualquier función de un programa. Como regla general, siempre
que sea posible se deben utilizar variables locales, ya que el mantenimiento de un
programa que utiliza variables globales es más complicado por el mero hecho de que
una variable global puede ser modificada en cualquier parte del programa y encontrar
un fallo relacionado con esa variable es más difícil. En la normas de estilo que aparecen
en el Apéndice A se muestran ejemplos concretos que apoyan esta afirmación.
Otro aspecto a mencionar en este primer programa es que la sintaxis de C fue elaborada
por anglosajones, con lo que:
• Al empezar a escribir un programa, todas las sentencias se escriben de tal forma que
disten la misma distancia del margen izquierdo.
• Sin embargo, cuando se empieza a escribir el código de una función, de una
sentencia condicional, o de una sentencia iterativa, el código se separa x caracteres
adicionales del margen izquierdo, donde x es un número constante para todo el
programa.
• Ninguna línea debe de pasar de 80 caracteres para facilitar su impresión. Si una
sentencia excede de esa longitud, se continúa en la línea siguiente dejando
nuevamente x caracteres adicionales en el margen izquierdo.
A lo largo de los ejemplos presentes en este manual, se podrá observar como se aplican
estas reglas de indentación. En el Apéndice A de este manual, mostramos unas
normas básicas de estilo de programación en C que serán aplicables a cualquier
programa realizado a partir de ahora.
int main()
{
float radio, pi=3.14159;
A diferencia del ejercicio anterior, aquí hemos utilizado una variable pi para almacenar
en ella el valor del número π en vez de usar una constante. En general, siempre es mejor
definir las constantes como tales o almacenarlas en variables que no introducirlas
directamente en las sentencias que las usen, ya que lo primero facilita el mantenimiento
del código (si queremos cambiar la constante sólo es necesario cambiarla en un sitio) y
es menos propenso a cometer errores (cuántas más veces escribamos un número, más
probabilidades hay de que nos equivoquemos haciéndolo).
3*5/2
3 * 5 / 2.0
La primera da como resultado 7, ya que tanto la multiplicación como la división son con
números enteros. Sin embargo, la segunda da como resultado 7.5, ya que al ser 2.0 un
número en punto flotante el resultado de la multiplicación (15) es convertido a otro
número en punto flotante y la división es una división de números en punto flotante.
En los printf de este ejercicio podemos ver también cómo limitar el número de
decimales de un número en punto flotante a la hora de imprimirlo por pantalla: %.3f
provoca que sólo se impriman 3 decimales.
Como se ha comentado con anterioridad, las constantes es mejor definirlas como tales o
introducirlas en variables antes que explicitarlas cada vez que se requieren. Lo primero,
en C, se realiza mediante la directiva #include:
#include <stdio.h>
#include <stdlib.h>
#define PI 3.14159
int main()
{
float radio;
return 0;
}
En programación, una constante se puede pensar como una variable que no es tal, es
decir, a la que no se le puede cambiar el valor inicial una vez se le ha asignado este. Sin
embargo, en C es un poco distinto. Lo que realmente hace #define es sustituir
literalmente en el código fuente, desde el momento en el que aparece el #define, todas
las instancias de lo definido por su valor antes de realizar la compilación. Es decir, en el
ejemplo anterior las apariciones de PI se cambiarían por 3.14159 antes de compilar.
Esto tiene las siguientes implicaciones:
• No se puede cambiar el valor de una constante. Esto es muy fácil de ver. Si tenemos
un
#define NUMERO 2
y, posteriormente, hacemos
NUMERO = 3;
2 = 3;
int main()
{
int x;
Hay que tener cuidado de que no se produzca ningún error sintáctico o semántico en
el código fuente tras sustituir el valor. Por ejemplo, esto sería erróneo (aunque
fácilmente corregible poniendo paréntesis alrededor de x+y en el #define):
#include <stdio.h>
#include <stdlib.h>
int main()
{
int x,y;
En los printf de la última versión mostrada del programa que calcula el área de un
círculo, aparece el carácter especial “\t”, que representa el carácter tabulador y, por
tanto, a la hora de imprimirlo por pantalla se substituye por espacios en blanco.
Para finalizar con el apartado de entrada / salida básica, veremos una versión más del
programa que calcula el área de un círculo:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main()
{
float radio, pi_radio;
Como se puede ver, el número π está aquí definido bajo la constante M_PI y con mucha
mayor precisión que la que nosotros empleamos en los ejemplos anteriores.
Figura 4: Aplicación que calcula la circunferencia y el área del círculo a partir del radio.
3. Sentencias condicionales.
Para ilustrar la utilización de las sentencias condicionales utilizaremos como ejemplo un
programa que calculará las raíces reales de una ecuación de segundo grado.
• Las variables de tipo double. Este tipo de dato se usa para almacenar números en
punto flotante que necesitan de una precisión o rango de representación mayor que
el que proporciona el tipo float. Aunque el número de bytes usado para representar
un float o un double depende de la arquitectura de la máquina y del sistema
operativo en ella instalado, en las máquinas usadas en clase de prácticas se usan 4
bytes para los float y 8 bytes para los double.
• Como las comillas se usan para empezar y finalizar las cadenas de caracteres,
cuando se quiere introducir ese carácter en la propia cadena de caracteres es
necesario anteponer el carácter ‘\’.
• Para imprimir o leer un double se usa el indicador de formato %lf.
• Para calcular la raíz cuadrada de un número usamos la función sqrt, definida en
math.h.
• Los programas deben ser eficientes y evitar repetir cálculos de forma innecesaria:
usamos las variables raiz y dos_a para almacenar resultados temporales.
• Los operadores * y / tienen más prioridad que + y -, por eso es necesario agrupar (-
b+raiz) y (-b-raiz) entre paréntesis, ya que en caso contrario sólo raiz estaría siendo
dividida por dos_a.
Antes de proseguir, es necesario recalcar una cosa: ¡no es suficiente con probar un
programa una vez para asegurar su correcto funcionamiento! Es muy habitual la frase
“pues a mi me funciona” cuando se muestra un fallo en un programa a un alumno.
Aunque la verificación formal no entra dentro del programa de esta asignatura, es
importante tener claro que probar el correcto funcionamiento de un programa no es
trivial. Algunos consejos prácticos son:
Vemos, por tanto, que en este programa es necesario ejecutar unas instrucciones u otras
en función del valor de unas variables. Para ello utilizaremos la instrucción condicional
if (…) … else…, explicada en clase, y que permite precisamente eso, ejecutar un bloque
de instrucciones cuando se cumple una condición y ejecutar otro bloque de
instrucciones distinto en caso contrario. El programa quedaría así:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
Figura 5: Aplicación que calcula las raíces reales de una ecuación de segundo grado
Sobre este programa, recalcaremos varios aspectos que suelen ser fuente de errores al
empezar a programar en C:
• Los operadores para realizar una comparación de igualdad y para realizar una
asignación son distintos. Para lo primero se usan dos símbolos “=” adyacentes
mientras que para lo segundo un único símbolo “=”. Si se usa el operador incorrecto
(asignación en vez de comparación o comparación en vez de asignación), no se
producirá ningún error de compilación, si no que el programa no se comportará
como cabría esperar. Esto es así porque ambos tipos de expresión tienen un
significado completo como sentencia. Una comparación es una expresión que
devuelve “1” si se verifica y “0” en caso contrario. Ponerla en una sentencia como
en el siguiente ejemplo, que siempre imprimirá el valor de x, es sintácticamente
correcto aunque no tenga sentido ya que no se utiliza el resultado para nada:
#include <stdio.h>
#include <stdlib.h>
• No se pone el carácter “;” después de la condición del if. El carácter “;”, en C, sirve
para finalizar sentencias. No tiene sentido ponerlo después de la condición porque,
necesariamente, hay que indicar al menos una instrucción a ejecutar en el caso de
que la condición se cumpla. Nuevamente, si se pone el “;” no siempre se produce un
error (sólo se producirá si hay un else asociado al if), si no que el programa no se
comporta como el alumno espera ya que poner un “;” tiene el significado asociado
de “sentencia vacía”. Supongamos estos dos programas, el primero dará error de
compilación, mientras que el segundo siempre imprimirá el valor de y:
#include <stdio.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdlib.h>
Se pueden anidar expresiones lógicas sencillas para construir otras más complejas
mediante el uso de los operadores lógicos “&&”, “||” y “!”. El siguiente programa, que
pregunta un mes y nos dice cuántos días tiene, es un ejemplo de uso de estos operadores
lógicos para construir expresiones lógicas complejas:
#include <stdio.h>
#include <stdlib.h>
system("PAUSE");
return 0;
}
Nuevamente, dos importantes aspectos a tener en cuenta:
• Salvo que se tenga muy clara la prioridad de los distintos operadores del lenguaje C,
es recomendable usar los paréntesis para agrupar subexpresiones. En el ejemplo
anterior, gran parte de los paréntesis no son necesarios (los que rodean las
comparaciones), pero incluso aunque se sea consciente de este hecho, ponerlos
ayuda a dar mayor claridad al código fuente.
• Al igual que pasaba con los operadores de comparación de igualdad y asignación,
los operadores “&&” y “||” pueden confundirse fácilmente con los operadores bit a
bit “&” y “|” respectivamente y no se producirá ningún error de compilación si no
que el programa puede no funcionar correctamente. El siguiente programa que
calcula el and bit a bit de dos números funciona incorrectamente (cuando el
resultado no es el número 0 imprime siempre 1) por culpa de utilizar “&&” en vez
de “&”:
#include <stdio.h>
#include <stdlib.h>
“if (..) … else …” es la sentencia condicional más usada y más genérica en lenguaje C,
pero existe otra “switch (…) case…” que es interesante en determinadas circunstancias.
Cuando tenemos que mirar el valor de una variable de tipo entero y realizar distintas
acciones en función de dicho valor, usar “switch (…) case…” reduce un poco el tamaño
del código fuente y nos evita usar numerosos “if (..) … else …” encadenados. El
siguiente ejemplo, que calcula el área de una figura geométrica u otra en función de lo
seleccionado por el usuario, ilustra este comentario:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
Observaciones:
• Para almacenar un carácter se usa una variable de tipo char, la cual se lee de teclado
/ imprime por pantalla usando el identificador de formato %c.
• Aunque char se usa para guardar caracteres, es en realidad un tipo de dato entero,
como lo pueda ser int, ya que una letra se representa por un número (código ASCII).
Por eso opcion puede ser un char. Sin embargo, opcion nunca podría ser un número
en punto flotante ni tampoco una cadena de caracteres (como se verá en el capítulo
correspondiente).
• Notar que para indicar que a, b y c son caracteres se rodean por comillas simples. Si
no se usan comillas tendremos un error porque el compilador pensará que son
variables, mientras que si usamos comillas dobles el error se deberá a que el
compilador pensará que son cadenas de caracteres.
• break se usa para salir del switch. Si no se pusiese, la siguiente sentencia que se
ejecutaría sería la escrita en la línea de abajo, aun perteneciendo a un case distinto.
Es, en este sentido, un caso muy particular de C que no se repite para ningún otro
tipo de sentencia. El uso de break en cualquier otra situación, está totalmente
desaconsejado y va en contra de los principios de la programación estructurada, al
romper la secuencia del código y dificultar su comprensión.
4. Bucles
En ocasiones es necesario realizar una misma operación sobre un conjunto de datos, o
repetir esa operación hasta que se cumpla una condición determinada. Un ejemplo de lo
primero, que veremos en el tema en el que se traten los arrays, sería sumar dos vectores.
Un ejemplo de lo segundo es el programa que veremos a continuación: cálculo del
factorial de un número. Para calcular el factorial de un número natural se multiplica éste
por ese mismo número menos 1, el resultado se vuelve a multiplicar por el número
menos dos y así sucesivamente hasta llegar a multiplicar por 1. Si desconocemos el
número del cual hay que calcular el factorial, desconocemos también cuántas
multiplicaciones habrá que hacer, con lo que es necesario utilizar una sentencia que nos
permita realizar la multiplicación mientras no lleguemos al 1. Este tipo de sentencias se
conocen como iterativas y en C disponemos de tres: while, do…while y for. A
continuación mostramos una posible implementación de un programa que resuelve este
problema. Por supuesto, existen muchas otras formas de implementarlo, algunas más
eficientes, pero esta nos permitirá observar claramente las diferencias entre los tres tipos
de sentencias iterativas de C.
#include <stdio.h>
#include <stdlib.h>
Consideraciones:
• Las variables son esta vez de tipo long int. El estándar dice que el rango de números
representable por un long int es igual o mayor que el de un int. En las máquinas del
laboratorio, ambos tipos pueden representar un entero entre -2147483648 y
2147483647, pero otras arquitecturas o con otro compilador los rangos podrían ser
distintos. Puede parecer un rango suficiente, pero el programa, en realidad, sólo
funciona con números menores o iguales a 12. Para números mayores, no
obtendremos ningún error, si no un resultado incorrecto ya que el código generado
por el compilador no detecta situaciones de desbordamiento. Las variables deberían
ser, en realidad, double para tener un mayor rango de representación aunque los
números a almacenar sean enteros. Y aun así, para números no muy grandes, no
tendremos un resultado preciso.
• Al igual que con el if, y por las mismas razones, no se debe poner punto y coma
después de la condición del while.
• Cuando se realiza una operación aritmética con dos operadores donde uno de ellos
es a su vez la propia variable en la cual se va a almacenar el resultado, la asignación
y la expresión aritmética se pueden abreviar como muestra la siguiente tabla:
En la tabla también se muestra que, en el caso particular de sumar uno o restar uno,
existen otras dos formas simplificadas de expresar lo mismo. Es muy importante
recalcar que hay una substancial diferencia semántica entre x++/x-- y ++x/--x. En el
primer caso, primero se utiliza el valor actual de la variable en la sentencia en la que
aparezca y luego se incrementa / decrementa ese valor. En el segundo, caso, primero
se incrementa / decrementa el valor y luego se utiliza el resultado en la sentencia. Se
entenderá mejor con el siguiente ejemplo:
#include <stdio.h>
#include <stdlib.h>
Imprime 9
Como consideración final al respecto de estas formas abreviadas de expresar algunas
operaciones aritméticas en C, decir que sólo se deben de usar si se está
absolutamente seguro de cómo funcionan, ya que se pueden crear sentencias poco
claras, especialmente utilizando ++ y -- en la misma sentencia conjuntamente con
otros operadores. Ej:
#include <stdio.h>
#include <stdlib.h>
#include <stdio.h>
#include <stdlib.h>
• for() permite escribir de forma más compacta los bucles while(). De hecho, se puede
establecer la siguiente equiparación entre for() y while():
A
while(B)
{ for (A; B; D)
C C
D
}
Cualquier bucle puede implementarse con cualquiera de las tres sentencias iterativas,
pero while() tiene la sintaxis más clara y fácil de entender, con lo que, si no se está
seguro de la sintaxis de las otras sentencias, recomiendo optar por el uso de while().
if (suma == candidato)
printf("%d ", candidato);
}
printf("\n");
}
system("PAUSE");
return 0;
}
Como se puede ver en el código anterior, se ha utilizado una sentencia iterativa for que
recorre los números entre 1 y el número máximo que ha introducido el usuario, dentro
de la cual hay otra sentencia iterativa for (a este procedimiento se le denomina anidar
bucles) que recorre los números entre 1 y el número máximo que marca, en este caso, el
bucle más exterior. Es importante hacer notar que la variable suma debe ser puesta a
cero en cada iteración del bucle más exterior (esto es un error común a la hora de
trabajar con variables que acumulan resultados parciales).
A continuación mostramos una versión más optimizada del programa anterior, donde se
ha reducido el número de iteraciones que deben hacer ambos bucles for teniendo en
cuenta que el caso en el que las variables valen 1 puede ser incluido directamente en la
variable suma y, por otro lado, que en el bucle interior no es necesario recorrer los
números hasta el valor de la variable candidato ya que a partir de candidato/2 ya no
pueden ser múltiplos:
#include <stdio.h>
#include <stdlib.h>
Es altamente recomendable que, tras realizar una versión inicial que funcione de un
programa, se dedique un tiempo a tratar de optimizar el código reduciendo el número de
operaciones que se llevan a cabo (en sentencias iterativas normalmente). Este proceso
depende del algoritmo que se deba implementar en cada caso.
5. Funciones
A medida que los problemas a resolver mediante un programa en C se hacen más
complicados, el código se convierte cada vez en más difícil de estructurar y, sobre todo,
de comprender dado el gran número de líneas que lo componen. Una solución consiste
en descomponer los problemas en otros más sencillos que se puedan analizar y
programar de forma independiente. A cada uno de estos problemas más sencillos se les
denomina módulo, y a este tipo de programación, programación modular. En lenguaje
C, los módulos se denominan funciones y es posible establecer la similitud con las
funciones matemáticas que disponen de variables sobre las que operan para obtener un
resultado.
Para dividir un programa en funciones, hay que distinguir las partes que tienen alguna
independencia. Después se intenta dividir estas partes en otras más pequeñas y así
sucesivamente, hasta llegar a fragmentos lo suficientemente simples. Esta forma de
proceder se conoce como diseño Top-Down.
Las funciones pueden (y suelen) recibir parámetros que se declaran a la derecha del
nombre de la función y entre paréntesis. Además, suelen devolver un resultado que
deberá ser tratado en el punto del programa desde el que se llamó a la función.
( )= 3!(44−! 3)! = 4
4
3
#include <stdio.h>
#include <stdlib.h>
if (numero<2)
factorial=1;
else
{
factorial=numero;
system("PAUSE");
return 0;
La fórmula utilizada para resolver el ejercicio implica realizar 3 veces el cálculo del
factorial. Sin programación modular, el código necesario en este caso repetiría 3 veces
las mismas instrucciones y esto es, obviamente, muy ineficiente. Sobre la base de la
programación modular, hemos creado una función (denominada calcularFactorial) que
incluye las líneas de código que calculan dicho factorial una única vez en la totalidad
del programa.
Los punteros se declaran como cualquier otro tipo de variable, eso sí, con un
identificador especial *. Por ejemplo, un puntero de nombre punt y que apunta a una
variable de tipo double, se declara
double *punt;
punt = &a;
printf(“%lf”,*punt);
printf("\nValor de punt=%p\n",punt);
printf("\nDireccion de memoria de la variable a=%p\n",&a);
printf("\nValor de a=%lf\n",a);
printf("\nValor al que apunta punt=%lf\n",*punt);
a=20.1;
printf("\nValor de a=%lf\n",a);
*punt=36.5;
printf("\nValor de a=%lf\n\n",a);
system("PAUSE");
return 0;
}
Vemos cómo la dirección de memoria que almacena punt es la de a, de modo que los
cambios que se realizan sobre *punt afectan al contenido de a.
Por ejemplo, en el siguiente programa, dados tres números se pide crear una función
que calcule el menor, el mayor y la media de dichos números:
#include "stdio.h"
#include "stdlib.h“
if (*num1<*num2)
{
//Se comprueba cual es menor
if (*num1<*num3)
min=*num1;
else
min=*num3;
//Se comprueba cual es mayor
if (*num3<*num2)
max=*num2;
else
max=*num3;
}
else //num2 < num1
{
//Se comprueba cual es menor
if (*num2<*num3)
min=*num2;
else
min=*num3;
//Se comprueba cual es mayor
if (*num3<*num1)
max=*num1;
else
max=*num3;
}
*num3=(*num1+*num2+*num3)/3;
*num1=min;
*num2=max;
}
calcularMedia(&num1,&num2,&num3);
system("PAUSE");
return 0;
}
Queda claro en este ejemplo la necesidad del uso de punteros, ya que así la función
calcularMedia trabaja sobre las variables num1, num2 y num3 del main() y no sobre
copias. De esta forma, las modificaciones que se hacen sobre *num1, *num2 y *num3
afectan a las variables originales (como se puede ver en el último printf del main() que
muestra el contenido de num1, num2 y num3 modificadas en la función). Es importante
resaltar el uso ya explicado del operador & a la hora de mandar las direcciones de las
variables num1, num2 y num3 a la función, que son recogidas, obviamente, por tres
punteros *num1, *num2 y *num3.
#include <stdio.h>
#include <stdlib.h>
maximo=mcd(*numerador,*denominador);
*numerador=*numerador/maximo;
*denominador=*denominador/maximo;
do {
printf("Introduzca el numerador de la primera fraccion: ");
scanf("%i",&num1);
if (num1 <= 0)
printf("El numerador debe ser positivo y mayor que cero\n");
} while (num1 <= 0);
do {
printf("Introduzca el denominador de la primera fraccion: ");
scanf("%i",&den1);
if (den1 <= 0)
printf("El denominador debe ser positivo y mayor que cero\n");
} while (den1 <= 0);
entero = simplificarFraccion(&num1,&den1);
do {
printf("Introduzca el numerador de la segunda fraccion: ");
scanf("%i",&num2);
if (num2 <= 0)
printf("El numerador debe ser positivo y mayor que cero\n");
} while (num2 <= 0);
do {
printf("Introduzca el denominador de la segunda fraccion: ");
scanf("%i",&den2);
if (den2 <= 0)
printf("El denominador debe ser positivo y mayor que cero\n");
} while (den2 <= 0);
entero = simplificarFraccion(&num2,&den2);
sumarFracciones(num1,den1,num2,den2,&num_suma,&den_suma);
system("PAUSE");
return 0;
}
La instrucción:
Declara una variable del tipo "struct Nombre", esto es, el compilador reserva la cantidad
de memoria sufuciente para mantener la estructura íntegra (es decir espacio para
almacenar Campo1, Campo2, ..., CampoN). Cuando se hace referencia a la variable
Var, se esta haciendo referencia a la estructura íntegra.
Un campo de una estructura se utiliza como una variable más. Para referenciar un
campo de una estructura se emplea el operador .
Hoy.Dia = 24;
Hoy.Mes = "Agosto";
Hoy.Anio = 1991;
Ejemplo para verificar año bisiesto:
Bisiesto = Fecha.Anio%4 == 0 && Fecha.Anio%100 != 0 || Fecha.Anio%400 == 0;
El uso de estructuras permite que una función modifique y devuelva más de un tipo de
dato, ya que lo que se devuelve es la estructura. De cara a ilustrar esta posibilidad, a
continuación mostramos un programa que calcula el área y la longitud de un círculo a
partir del radio del mismo que es pedido al usuario por teclado usando, inicialmente,
paso de argumentos por referencia:
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
system("PAUSE");
return 0;
}
struct s_circulo
{
float area;
float circunferencia;
};
circulo = f_circulo(radio);
printf("Area: %.2f Circunferencia: %.2f\n", circulo.area,
circulo.circunferencia);
system("PAUSE");
return 0;
En el programa anterior hemos utilizado una función que devuelve un dato de tipo
estructura struct s_circulo.
8. Arrays
En ocasiones es interesante almacenar en memoria varios datos del mismo tipo sobre los
que se realizará algún tipo de operación común. Por ejemplo, los elementos de un
vector: si nuestro programa necesita almacenar en memoria un vector, casi seguro que
las operaciones que haga con él afectarán a todos sus elementos (ya sea leerlos,
imprimirlos por pantalla, compararlos, etc.). Es para esta necesidad de almacenar en
memoria un conjunto de datos del mismo tipo para lo que se usan los arrays. Un array
es, por tanto, una variable que contiene múltiples datos del mismo tipo y que podremos
tratar, bien de forma conjunta, bien elemento a elemento, según las necesidades del
programa.
#include <stdio.h>
#include <stdlib.h>
Consideraciones:
• En este ejercicio tenemos ejemplo de todo lo visto hasta ahora: E/S, constantes,
sentencias condicionales, condiciones compuestas, sentencias iterativas y arrays.
Recuerda que:
• Las constantes, aunque no es obligatorio, se suelen declarar en mayúsculas.
• Cuando se leen datos con scanf se debe poner un & delante de cada variable en
la que se vaya a guardar un dato, porque scanf necesita saber la dirección de
memoria de esas variables.
• En una expresión lógica, || significa “o” y && significa “y”.
• El operador % significa “módulo” y se usa, por tanto, para saber cuál es el resto
de una división entre dos números enteros.
• No confundas el operador de asignación “=” con el operador de comparación de
igualdad “==”.
• Se pueden concatenar cadenas de caracteres simplemente cerrando una con
comillas y abriendo la siguiente con otras comillas, aunque estén en líneas
distintas. Lo que no se puede hacer es crear una cadena de caracteres en una
línea y continuarla en la siguiente.
• Un array, como cualquier otra variable, es necesario declararlo. En este caso, a la
derecha del nombre, se pone entre corchetes el número de elementos que tiene el
array. Una vez se ha declarado, C no permite cambiar el número de elementos del
array.
• El usuario debe especificar con cuántos números desea trabajar, pero dado que no
hemos visto asignación dinámica de memoria y tenemos que declarar el array antes
de usarlo, debemos crear un array sobredimensionado y controlar que el usuario no
especifica un número de elementos mayor que el tamaño del array.
• Recuerda que para acceder a un elemento concreto del array se pone el nombre de
éste y, entre corchetes, el índice del elemento al que queremos acceder, que será un
número entero entre 0 y N-1 donde N es el número de elementos del array. El índice
puede ser una constante, una variable o incluso una expresión, siempre y cuando el
resultado final de evaluarla sea un número entero. En el programa que acabamos de
ver se usa una variable, i, que va tomando todos los valores desde 0 hasta n-1
porque nos interesa recorrer todos los elementos del array.
• Salvo el caso particular de los arrays de caracteres (strings) que veremos más
adelante, no existen elementos en el lenguaje C ni tampoco funciones que nos
permitan tratar los arrays con una sola instrucción. Es decir, no podemos leer,
escribir, sumar, etc. arrays con una instrucción, si no que tenemos que hacerlo
elemento a elemento.
En cuanto al uso de arrays y funciones, como vimos en el tema anterior, el paso por
referencia y, consecuentemente, el uso de punteros es necesario en C cuando una
función debe modificar varias variables. Por este motivo, en el caso de pasar un array a
una función, siempre se realiza por referencia, por lo que no es necesario devolver dicho
array desde la función al estar modificando directamente el array original (no es
necesario el return).
A continuación mostramos el código del ejemplo anterior realizado ahora usando una
función par_positivo que calcula la suma:
#include <stdio.h>
#include <stdlib.h>
return sum;
}
system("PAUSE");
return 0;
}
Como vemos, el paso del array a la función es simple y podría parecer que se está
haciendo paso por valor ya que no hay distinción respecto a cómo se envía la variable n,
pero como hemos dicho anteriormente, el paso de arrays es siempre por referencia. El
nombre de un array es realmente un puntero a la dirección de memoria donde empieza
el array. Este es el motivo por el cual en este ejemplo no es necesario un return de
matriz, al ser un paso por referencia se está trabajando siempre sobre la matriz del
main(). De hecho, en el siguiente programa mostramos la forma más común de pasar
arrays a funciones utilizando un puntero como argumento:
#include <stdio.h>
#include <stdlib.h>
return sum;
}
system("PAUSE");
return 0;
}
Hemos resaltado la única diferencia respecto a la versión anterior, que consiste en que
ahora el array se recibe como puntero. La función par_positivo no copia localmente el
contenido del array, sino que trabaja con un puntero que apunta al array creado desde el
main(). La declaración de los arrays unidimensionales como punteros dentro de las
funciones es la ideal.
Ordenación
#include <stdio.h>
#include <stdlib.h>
#define MAX 100
Como vemos, en este algoritmo se utiliza un índice i para señalar el elemento que se
toma como referencia en la comparación (bucle exterior) y otro índice j cuyo valor
inicial es i+1 para señalar el elemento que se compara cada vez (bucle interior). De esta
forma, una vez que un elemento señalado por j es menor que el de referencia señalado
por i, simplemente se intercambian. Por este motivo, el método de la burbuja se engloba
dentro de los algoritmos de intercambio, ya que su filosofía de funcionamiento se basa
en la realización de sucesivos intercambios de valores. De hecho, el mayor defecto de
este algoritmo es el elevado número de intercambios innecesarios que se llevan a cabo.
Por ejemplo, si el array que proporciona el usuario estuviese ordenado de mayor a
menor (caso opuesto a lo que queremos obtener), el programa anterior realizaría
continuos intercambios ya que cualquier número es siempre menor que el que se toma
como referencia.
Un método de ordenación más eficiente, que se obtiene como una variación simple del
anterior, es el denominado método de selección directa donde, cada vez que se
encuentra un número menor que el de referencia señalado por el índice i, se almacena
dicho número y su posición en un par de variables y, tras finalizar el bucle en j, se lleva
a cabo un único intercambio. El siguiente código muestra este método aplicado a la
ordenación de un array, de nuevo de menor a mayor:
#include <stdio.h>
#include <stdlib.h>
#define MAX 100
if (k != i)
{
vector[k] = vector[i];
vector[i] = aux;
}
}
printf("\nVector ordenado = ");
for (i=0; i<numelementos; i++)
printf("%i ", vector[i]);
}
system("PAUSE");
return 0;
}
Por tanto, respecto al método de la burbuja se han añadido únicamente dos variables:
aux: que almacena temporalmente el menor valor en cada iteración del bucle i
k: que almacena temporalmente la posición del menor valor en cada iteración del
bucle i
Con esta simple modificación, el método de selección directa se vuelve mucho más
eficiente que el de la burbuja y, como dijimos antes, en el caso extremo de que el array
inicial esté ordenado de mayor a menor, realizaría únicamente tantos intercambios como
elementos tenga el array. Un ejercicio interesante es comprobar este incremento de
eficiencia guardando e imprimiendo una variable contador que aumente su valor en una
unidad cada vez que se lleva a cambio un intercambio en un método y en otro.
Para finalizar este apartado dedicado a los algoritmos de ordenación simples, decir
simplemente que para ordenar de mayor a menor en los ejemplos anteriores, únicamente
sería necesario cambiar el signo de comparación del primer if de < a >. De hecho, a
continuación mostramos un programa que implementa el algoritmo de selección directa
mediante el uso de una función que permite al usuario escoger entre una ordenación
ascendente o descendente:
#include <stdio.h>
#include <stdlib.h>
#define MAX 100
do {
printf("\nIntroduzaca el tipo de ordenacion:\n");
printf("0- Ascendente\n");
printf("1- Descendente\n");
printf("Tipo: ");
scanf("%i", &tipo);
} while ((tipo != 0) && (tipo != 1));
system("PAUSE");
return 0;
}
En este ejemplo se ve claramente el efecto del paso por referencia del array vector, ya
que dentro de la función ordenacion se modifica pero no es necesario un return para
actualizar su valor en el main, ya que la función realmente está modificando el array
declarado en el main mediante el uso del puntero.
9. Cadenas de caracteres
Las cadenas de caracteres son simplemente arrays que almacenan variables de tipo char.
La única característica propia de este tipo de array es que se utiliza el carácter 0 (NULL
o carácter de código ASCII 0) para indicar el final de la cadena (esto no es necesario en
arrays numéricos).
El uso de variables de tipo carácter cobra una gran importancia práctica a la hora de
trabajar con cadenas de caracteres, ya que desde el punto de vista del usuario, esto le
permite utilizar palabras y frases de forma cómoda.
Existen en C una serie de funciones específicas para operar con estas cadenas, por
ejemplo para:
Es importante recalcar que estas funciones sólo sirven para cadenas de caracteres, no
para arrays numéricos.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
longitud = strlen(frase);
if (es_palindromo == 1)
printf("\nLa frase es un palindromo\n");
else
printf("\nLa frase no es un palindromo\n");
system("PAUSE");
return 0;
}
La idea básica para la resolución de este ejemplo es que se debe comparar el primer
carácter del array con el último, el segundo con el penúltimo, etc. En el caso de que una
de estas comparaciones detecte que dos caracteres simétricos no son iguales, la frase ya
no será palíndromo. Previamente a esta comparación, debemos pasar todos los
caracteres a minúsculas (la comparación simple de una letra mayúscula con una
minúscula devolvería que son distintas y la frase sería palíndromo igualmente) y, a
continuación, eliminar los espacios en blanco que contenga la frase para que la
comparación tenga sentido.
scanf("%39[^\n]s", frase);
longitud = strlen(frase);
for(i=0,j=0;i<=longitud;i++)
if (frase[i] != ' ')
{
nueva_frase[j] = frase[i];
j++;
}
longitud = strlen(nueva_frase);
if (es_palindromo == 1)
printf("\nLa frase es un palindromo\n");
else
printf("\nLa frase no es un palindromo\n");
system("PAUSE");
return 0;
}
A continuación veremos el ejemplo de un programa que lee dos matrices por teclado y
calcula la suma de ambas matrices.
#include <stdio.h>
#include <stdlib.h>
#define MAXFIL 20
#define MAXCOL 20
• Una matriz, como cualquier otra variable, es necesario declararla. En este caso, a la
derecha del nombre de la variable, se ponen tantos corchetes como dimensiones
tenga la matriz. En el ejemplo anterior, al ser matrices bidimensionales, se usaron
dos pares de corchetes para indicar en el primero de ellos el número de filas y en el
segundo el número de columnas de la matriz. Al igual que sucedía con los arrays,
una vez se ha declarada la matriz no se permite cambiar el número de elementos de
la misma.
• El programador debe especificar el tamaño (números de elementos) de cada
dimensión. En el ejemplo anterior se especificaron el número de filas y de columnas
de la matriz. Puesto que dicho tamaño no puede ser sobrepasado, será necesario
controlar que el usuario no especifica un número de elementos mayor que el tamaño
de la matriz.
• Recuerda que para acceder a un elemento concreto de la matriz se pone el nombre
de éste y, entre corchetes, el índice del elemento al que queremos acceder, que será
un número entero entre 0 y N-1 donde N es el número de elementos de esa
dimensión de la matriz. El índice puede ser una constante, una variable o incluso
una expresión, siempre y cuando el resultado final de evaluarla sea un número
entero. En el programa que acabamos de ver se usan una variable, i, que va tomando
todos los valores desde 0 hasta numfil porque nos interesa recorrer todos las filas de
la matriz y otra variable numcol para ir recorriendo todas las columnas de la matriz.
Hay que tener en cuenta que para cada fila de la matriz se deberán recorrer todas sus
columnas por ello en el programa anterior el bucle de la variable j (las columnas) es
un bucle anidado dentro del bucle de la variable i (filas de la matriz).
• Al igual que en el caso de los arrays, no existen elementos en el lenguaje C ni
tampoco funciones que nos permitan tratar las matrices con una sola instrucción. Es
decir, no podemos leer, escribir, sumar, etc. matrices con una instrucción, si no que
tenemos que hacerlo elemento a elemento.
En cuanto al uso de matrices y funciones, hay que tener en cuenta que cuando se recibe
una array bidimensional (matriz) en una función, se deben especificar todas las
dimensiones menos la primera, que es opcional. El siguiente ejemplo muestra el uso de
matrices en funciones, donde el objetivo consiste en realizar un programa que ponga a 0
(mediante una función) los elementos de la diagonal principal y secundaria de una
matriz cuadrada:
#include <stdio.h>
#include <stdlib.h>
#define MAX 10
int i,j,dimension,matriz[MAX][MAX];
if (dimension>MAX || dimension<=0)
printf("Error. El numero de elementos es incorrecto (Maximo= %i)\n",MAX);
else
{
for (i=0; i<dimension; i++)
for (j=0; j<dimension; j++)
{
printf("Introduzca el elemento %i, %i: ", i,j);
scanf("%i",&matriz[i][j]);
}
modificarDiagonalesMatriz(matriz,dimension);
for (i=0; i<dimension; i++)
{
for (j=0; j<dimension; j++)
printf("%i\t ", matriz[i][j]);
printf("\n");
}
}//fin else
system("PAUSE");
return 0;
}
Los ficheros de texto pueden ser de dos tipos: de texto o binarios. Los ficheros de texto
almacenan información “legible”, es decir, basada en el código ASCII. Cualquier
fichero que no sea de texto, es binario.
int main()
{
int i,j,k;
int cantidad;
char nombre[35];
FILE *fichero;
strcat(nombre,".txt");
srand(time(NULL));
fclose(fichero);
}
system("PAUSE");
return 0;
}
declaramos un puntero que será utilizado para acceder a los ficheros (realmente es un
puntero a la estructura FILE, declarada en stdio.h).
Antes de usar un fichero es necesario realizar una operación de apertura del mismo y,
posteriormente, si se desea almacenar datos en él hay que realizar una operación de
escritura. Cuando ya no se quiera utilizar el fichero se realiza una operación de cierre.
En este sentido, en el programa anterior se lleva a cabo la apertura de un fichero para
escribir datos de él. El nombre del fichero se solicita al usuario sin extensión, se le
concatena la extensión .txt mediante la función strcat y se almacena en la cadena de
caracteres nombre, mediante la instrucción:
fichero = fopen(nombre, "w");
La función fopen requiere como primer parámetro una cadena de caracteres que
contenga el nombre del fichero que se quiere tratar (y en su caso la ruta de acceso).
Como segundo parámetro es necesario especificar el modo de apertura de dicho fichero,
que es una cadena de caracteres que indica el tipo del fichero (texto o binario) y el uso
que se va ha hacer de él (lectura, escritura, añadir datos al final, etc). En este caso, el
fichero nombre almacenará una serie de números aleatorios, y se abre como escritura. A
partir de esta instrucción, en nuestro programa accederemos al fichero mediante la
variable fichero.
Si existe algún tipo de error al realizar la operación de apertura del fichero (por ejemplo,
porque el disco está lleno, porque el usuario no tiene permisos de escritura en el disco,
porque la ruta especificada no existe, etc), fopen devuelve el valor NULL. Esto nos
permite controlar un error la apertura del fichero mediante una instrucción condicional,
como la utilizada en este caso:
if (fichero == NULL)
Lo siguiente que realiza el programa anterior es pedir al usuario el número de datos que
quiere guardar en el fichero y después genera dicho número de datos mediante el uso de
la función rand(), que devuelve un número entero aleatorio entre 0 y RAND_MAX (el
mayor número entero que puede generar la función rand). Para crear números decimales
entre 0 y 1 basta con normalizar y cambiar el tipo de dato a float tal y como se hace en
el código anterior:
(float)rand()/RAND_MAX
Los números aleatorios que se generan con la función rand deben ser guardados en el
fichero. Para ello es necesario realizar una operación de escritura. En este caso
utilizaremos dentro de un bucle la función fprintf, análoga al printf que hemos usado
hasta ahora pero especificando como primer argumento el puntero al fichero donde
queremos escribir:
fprintf(fichero,"%f\n", (float)rand()/RAND_MAX);
Por último, tras terminar de usar cada fichero, debemos cerrarlo utilizando la función
fclose.
int main()
{
int i,j,k;
int c; /* variable que utilizaremos para borrar el buffer del teclado */
int num_datos=0;
float vector[MAX], aux;
char nombre[35];
FILE *fichero;
strcat(nombre,".txt");
fscanf(fichero,"%f",&vector[num_datos]);
num_datos++;
}
fclose(fichero);
if (k != i)
{
vector[k] = vector[i];
vector[i] = aux;
}
} /* Fin del proceso de ordenacion */
strcat(nombre,".txt");
fclose(fichero);
}
}
system("PAUSE");
return 0;
}
El resultado de este programa será correcto si el archivo creado contiene los mismos
datos que el archivo original, pero ordenados de menor a mayor.
En este caso, para obtener los datos hay que efectuar una operación de lectura de
fichero. Una de las posibles instrucciones de lectura es fscanf, que es la utilizada en el
programa anterior. Su estructura es similar al scanf que ya hemos utilizado en los
capítulos anterior, con la diferencia de que hay que especificar como primer argumento
el fichero en el que leemos:
fscanf(fichero,"%f",&vector[num_datos]);
fscanf(fichero,"%f",&vector[num_datos]);
num_datos++;
}
leen los datos (y los cuentan mediante la variable num_datos) hasta que se alcanza el
final del archivo sin necesidad de que el usuario introduzca cuántos hay.
Una vez que se tienen los datos en el array vector simplemente ejecutamos el algoritmo
de ordenación visto en el tema 5. Antes de añadir estos datos ordenados a un fichero de
destino debemos vaciar previamente el buffer del teclado mediante la expresión:
while((c = getchar()) != '\n' && c != EOF);
Cuando estamos leyendo números la función intenta leer números, saltándose algunos
caracteres de control (si se los encuentra), como el carácter '\n'. Sin embargo, cuando
estamos leyendo un carácter (o varios) la función de lectura leerá el siguiente carácter
que haya en el buffer de teclado, sea cual sea. Si no hay ninguno, entonces, la función se
detiene a la espera de poder leer. Pero si en el buffer existe algún carácter éste será
leído, aunque sea un carácter de control. Para evitar la lectura de caracteres de control y
también para evitar la lectura de otros caracteres anteriores que puede haber en el buffer
del teclado, se suele utilizar la instrucción flush(stdin) que vacía dicho buffer. Pero n
osotros lo desaconsejamos totalmente porque fflush solo está definido en el estándar
para flujos de salida y stdin es un flujo de entrada, y por ello no podemos estar seguros
de cómo se comportará en un caso general. Aunque en algunos compiladores funciona,
no lo hará en todos, por lo cual nosotros recomendamos la utilización de la expresión
anterior como equivalente para el fflush(stdin).
Continuando con el programa anterior, solo nos resta pedir al usuario un nombre para el
fichero destino, abrir el fichero ahora en modo escritura y guardar en él los datos del
array vector.
A continuación mostramos otro ejemplo del uso de archivos en C que lee datos de un
fichero “numeros.txt” que contiene los valores de una matriz de tamaño 10 x 10. Así
cada línea del fichero contiene 3 números, el primero de ellos indica el número de fila,
el segundo el número de columna y el último el valor correspondiente. El objetivo es
realizar un programa en C que lea este fichero “numeros.txt” y muestre por pantalla y
guarde en otro fichero el contenido del fichero original, pero de forma matricial.
#include <stdio.h>
#include <stdlib.h>
#define MAX 10
if (fichero1 != NULL)
{
printf("\nIntroduzca el nombre del fichero destino: ");
scanf(“%s39[^\n]s,nombreFichero);
if (fichero2!=NULL)
{
while (! feof(fichero1))
{
fscanf(fichero1, "%i", &i);
fscanf(fichero1, "%i", &j);
fscanf(fichero1, "%i", &valor);
matriz[i][j]=valor;
}
for (i=0;i<MAX;i++)
{
for (j=0;j<MAX;j++)
{
printf("%4i",matriz[i][j]);
fprintf(fichero2,"%4i",matriz[i][j]);
}
printf("\n");
fprintf(fichero2,"\n");
}
fclose(fichero2);
}
else
printf("\nError al crear el fichero\n");
fclose(fichero1);
}
else
printf("\nError al abrir el fichero\n");
system("PAUSE");
return 0;
}
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
float *numeros, media=0;
int i,n;
if (numeros == NULL)
printf("Memoria insuficiente\n");
else
{
for (i=0;i<n;i++)
{
printf("Introduce el elemento %d: ",i);
scanf("%f",&numeros[i]);
media += numeros[i];
}
media = media/n;
printf("\nLa media es: %f\n\n",media);
free(numeros);
}
system("PAUSE");
return 0;
}
Lo primero que hay que notar del código anterior es que el array está definido como un
puntero a un float en lugar de utilizar la notación para declarar los arrays de manera
estática. Una vez que el usuario introduce el número de elementos que tendrá el array
(variable n), mediante la función malloc() se realiza la reserva de un número de bytes
que permita almacenar n elementos de tipo float. Para ello, utilizamos la función sizeof()
que devuelve el número de bytes del tipo de dato o de la variable que se le pasa como
argumento. Por ejemplo, en este caso sizeof(float) devolvería un 4, que al multiplicarlo
por n nos dará el número exacto de bytes necesario para almacenar en memoria los
datos que introducirá el usuario.
La función malloc() devuelve un puntero de tipo void (es decir, un puntero genérico)
por lo que es necesario hacer una conversión de tipo de puntero (en este caso float *) de
acuerdo con el tipo de dato que se desea almacenar. A partir de este momento, la
variable numero guardará la dirección de comienzo del array en memoria y a partir de
este momento la utilización del array es la común.
Un aspecto que debe ser tratado cuando se trabaja con funciones, es la posibilidad de
que se realice reserva dinámica de memoria dentro de una función. El siguiente código
corresponde a un programa que crea un array de elementos dinámicamente en función
del tamaño que introduce el usuario y, a continuación, pide los elementos del array uno
a uno y calcula la media. En este caso se han utilizado funciones para realizar las
operaciones básicas de reserva de memoria y de cálculo de la media:
#include <stdio.h>
#include <stdlib.h>
numeros = reserva_local(numeros,n);
if (numeros == NULL)
printf("Memoria insuficiente\n");
else
{
for (i=0;i<n;i++)
{
printf("Introduce el elemento %d: ",i);
scanf("%f",&numeros[i]);
}
media = calcula_media(numeros,n);
printf("\nLa media es: %f\n\n",media);
free(numeros);
}
system("PAUSE");
return 0;
}
return(array);
}
for(i=0;i<num;i++)
media += array[i];
return (media/num);
}
Unas normas de estilo en programación, son tan importantes que todas las empresas
dedicadas a programación imponen a sus empleados una mínima uniformidad, para
facilitar el intercambio de programas y la modificación por cualquier empleado, sea o
no el programador inicial. Por supuesto, cada programa debe ir acompañado de una
documentación adicional, que aclare detalladamente cada módulo del programa,
objetivos, algoritmos usados, ficheros...
No existen un conjunto de reglas fijas para programar con legibilidad, ya que cada
programador tiene su modo y sus manías y le gusta escribir de una forma determinada.
Lo que sí existen son un conjunto de reglas generales, que aplicándolas, en mayor o
menor medida, se consiguen programas bastante legibles. Aquí intentaremos resumir
estas reglas:
Identificadores significativos
Es muy normal usar variables como i, j o k para nombres de índices de bucles (for,
while...), lo cual es aceptable siempre que la variable sirva sólo para el bucle y no tenga
un significado especial. En determinados casos, dentro de una función o programa
pequeño, se pueden usar este tipo de variables, si no crean problemas de comprensión,
pero esto no es muy recomendable.
Para los identificadores de función se suelen usar las formas de los verbos en infinitivo,
seguido de algún sustantivo, para indicar claramente lo que hace. Por ejemplo, una
función podría llamarse escribir_opciones, y sería más comprensible que si le
hubiéramos llamado escribir o escrOpc. Si la función devuelve un valor, su nombre
debe hacer referencia a este valor, para que sea más expresivo usar la función en
algunas expresiones, como:
Constantes simbólicas
1. Los programas se hacen más legibles: Es más legible usar la constante simbólica
PI como el valor de π que usar 3.14 en su lugar:
#define PI 3.141592
Como hemos dicho, las constantes se suelen poner completamente en mayúsculas y las
variables no, de forma que leyendo el programa podamos saber rápidamente qué es cada
cosa. En general, se deben usar constantes simbólicas en constantes que aparezcan más
de una vez en el programa referidas a un mismo ente que pueda variar ocasionalmente.
Obsérvese, que aunque el valor de π es constante, podemos variar su precisión, por lo
que es recomendable usar una constante simbólica en este caso, sobre todo si se va a
usar en más de una ocasión en nuestro programa. Puede no resultar muy útil dedicar una
constante para el número de meses del año, por ejemplo, ya que ese valor es
absolutamente inalterable.
Comentarios
No se debe abusar de comentarista, ya que esto puede causar una larga y tediosa lectura
del programa, pero en caso de duda es mejor poner comentarios de más. Por ejemplo, es
absurdo poner:
Los comentarios deben ser breves y evitando divagaciones. Se deben poner comentarios
cuando se crean necesarios, y sobre todo:
• Al principio del programa o de cada fichero del programa que permita seguir
un poco la historia de cada programa, indicando: Nombre del programa,
objetivo, parámetros (si los tiene), condiciones de ejecución, módulos que lo
componen, autor o autores, fecha de finalización, últimas modificaciones
realizadas y sus fechas... y cualquier otra eventualidad que el programador
quiera dejar constancia.
• En cada sentencia o bloque (bucle, if, switch...) que revista cierta
complejidad, de forma que el comentario indique qué se realiza o cómo
funciona.
• Al principio de cada función cuyo nombre no explique suficientemente su
cometido. Se debe poner no sólo lo que hace sino la utilidad de cada parámetro,
el valor que devuelve (si lo hubiera) y, si fuera oportuno, los requisitos
necesarios para que dicha función opere correctamente.
• En la declaración de variables y constantes cuyo identificador no sea
suficiente para comprender su utilidad.
• En los cierres de bloques con '}', para indicar a qué sentencias de control de
flujo pertenecen, principalmente cuando existe mucho anidamiento de sentencias
y/o los bloques contienen muchas líneas de código.
No olvidemos que los comentarios son textos literarios, por lo que debemos cuidar el
estilo, acentos y signos de puntuación.
Para aumentar la claridad no se deben escribir líneas muy largas que se salgan de la
pantalla y funciones con muchas líneas de código (especialmente la función principal).
Una función demasiado grande demuestra, en general, una programación descuidada y
un análisis del problema poco estudiado. Se deberá, en tal caso, dividir el bloque en
varias llamadas a otras funciones más simples, para que su lectura sea más agradable.
En general se debe modularizar siempre que se pueda, de forma que el programa
principal llame a las funciones más generales, y estas vayan llamando a otras, hasta
llegar a las funciones primitivas más simples. Esto sigue el principio de divide y
vencerás, mediante el cual es más fácil solucionar un problema dividiéndolo en
subproblemas (funciones) más simples.
#include <stdio.h>
Naturalmente, este orden no es estricto y pueden cambiarse algunos puntos por otros,
pero debemos ser coherentes, y usar el mismo orden en todos los módulos. Por ejemplo,
es frecuente saltarse la declaración de los prototipos de las funciones poniendo en su
lugar la implementación y, por supuesto, dejando para el final la función main().
#include <stdio.h>
#include <stdlib.h>
El mismo programa, simplemente realizado con variables globales, no funciona (se deja
como ejercicio al alumno averiguar por qué). Aunque la programación se simplifica al
evitar el envío y devolución de parámetros a la función, al final el esfuerzo deberá ser
realizado en la etapa de depuración tratando de encontrar dónde está el fallo:
#include <stdio.h>
#include <stdlib.h>
void f_factorial()
{
while (numero > 1)
factorial *= numero--;
}
int datos[MAX],contador,num_datos;
float media;
void media_acumulada()
{
media = 0;
for (contador=0; contador < num_datos; contador++)
media += datos[contador];
for(contador=0;contador<num_datos;contador++)
datos[contador] = 0;
contador=0;
do {
printf("Introduce un numero entero: ");
scanf("%d",&datos[contador]);
contador++;
media_acumulada();
} while (contador < num_datos);
system("PAUSE");
return 0;
}
Indentación o sangrado
Sentencia if-else
Primera opción: Segunda opción:
if (condición) { if (condición)
sentencia1; {
sentencia2; sentencia1;
... sentencia2;
} ...
else { }
sentencia1; else
sentencia2; {
... sentencia1;
} sentencia2;
...
Sentencia swith
Primera opción: Segunda opción
Sentencia for
Primera opción: Segunda opción:
do { do
sentencia1; {
sentencia2; sentencia1;
... sentencia2;
} while (condición); ...
} while (condición);
Aunque estos formatos no son en absoluto fijos, lo que es muy importante es que quede
bien claro las sentencias que pertenecen a cada bloque, o lo que es lo mismo, donde
empieza y termina cada bloque. En bloques con muchas líneas de código y/o con
muchos anidamientos, se recomienda añadir un comentario al final de cada llave de
cierre del bloque, indicando a qué sentencia cierra. Por ejemplo:
for (exp1;exp2;exp3) {
sentencia1;
sentencia2;
...
while (condición_1) {
sentencia1;
sentencia2;
...
if (condición) {
sentencia1;
sentencia2;
...
while (condición_2) {
sentencia1;
sentencia2;
...
} /*while (condición_2)*/
} /*if*/
else {
sentencia1;
sentencia2;
...
if (condición) {
sentencia1;
sentencia2;
...
}
else {
sentencia1;
sentencia2;
...
}
} /*else*/
} /*while (condición_1)*/
} /*for*/
Por otro lado, las indentaciones para las funciones deben llevar siempre las llaves de
apertura y cierre siempre se ponen a la en la primera columna, como se muestra en la
función factorial() y en el main() del siguiente ejemplo:
if (numero == 0)
printf("El factorial vale 1\n");
else
{
if (numero > 1)
factorial = f_factorial(numero);
printf("El factorial vale %.0lf\n", factorial);
}
}
system("PAUSE");
return 0;
}
Un código mal indentado es difícil de leer y, por tanto, de arreglar en caso de errores. El
siguiente programa muestra un ejemplo claro de programa que tiene un fallo y que,
debido a la mala indentación del mismo, es complicado averiguar donde se encuentra.
Recomendamos al alumno que trate de encontrar dicho fallo arreglando previamente la
indentación:
#include <stdio.h>
#include <stdlib.h>
#define SIZE 25
if ((f1 < 1) || (f1 > SIZE) || (c1 < 1) || (c1 > SIZE) || (f2 < 1) || (f2 >
SIZE) || (c2 < 1) || (c2 > SIZE) || (c1 != f2))
{
printf("Error!");
}
else
{
printf("Introduce los elementos de la primera matriz:\n");
for (i = 0; i < f1; i++)
for (j = 0; j < c1; j++)
scanf("%d", &(matriz1[i][j]));
printf("Introduce los elementos de la segunda matriz:\n");
for (i = 0; i < f2; i++)
for (j = 0; j < c2; j++) scanf("%d", &(matriz2[i][j]));
for (i = 0; i < f1; i++)
for (j = 0; j < c2; j++)
matrizr[i][j] = 0;
for (k = 0; k < c1; k++)
matrizr[i][j] += matriz1[i][k] * matriz2[k][j];
printf("La matriz resultante es:\n");
for (i = 0; i < f1; i++) {
for (j = 0; j < c2; j++)
printf("%d ", matrizr[i][j]);
printf("\n");
}
printf("\n");
}
system("PAUSE");
return 0;
}
13. Apéndice B
Contenido del fichero de texto necesario para la solución del segundo ejemplo de la
parte de ficheros:
0 6 7
7 3 74
2 4 25
5 5 56
0 5 6
0 7 8
4 6 47
8 3 84
4 0 41
5 6 57
9 9 100
5 0 51
8 7 88
1 6 17
3 6 37
4 5 46
9 6 97
0 9 10
2 3 24
5 9 60
7 6 77
5 7 58
2 5 26
7 5 76
2 0 21
3 9 40
6 7 68
4 7 48
9 3 94
5 3 54
6 8 69
4 9 50
3 8 39
2 1 22
1 4 15
4 1 42
6 1 62
4 3 44
0 0 1
0 3 4
1 2 13
0 2 3
6 4 65
9 1 92
3 1 32
0 1 2
1 3 14
9 2 93
7 9 80
6 9 70
6 0 61
8 9 90
1 9 20
6 3 64
3 7 38
1 7 18
8 6 87
8 1 82
0 4 5
3 4 35
2 9 30
9 7 98
9 5 96
8 5 86
5 8 59
3 2 33
8 4 85
9 8 99
6 2 63
6 5 66
7 8 79
9 0 91
5 2 53
6 6 67
2 2 23
7 4 75
4 2 43
4 8 49
1 8 19
7 2 73
1 1 12
8 8 89
0 8 9
8 0 81
7 7 78
2 7 28
1 0 11
1 5 16
3 0 31
3 5 36
9 4 95
7 0 71
7 1 72
5 4 55
8 2 83
2 6 27
5 1 52
4 4 45
2 8 29
3 3 34