-[ 0x06 ]-------------------------------------------------------------------- -[ Curso de C ]-------------------------------------------------------------- -[ by Lindir ]-------------------------------------------------------SET-29-- The Ancient Art Of C Programming. Introduccion. Ultimamente veo en el foro de SET que hay muchas personas preguntando sobre como programar tal o cual cosa en C/C++. Me alegra que, aunque el tema no sea exactamente hack, sean mensajes constructivos. Estas preguntas, ademas de que aun no he visto ningun documento para iniciarse en el mundo de la programacion que me pareciera a la vez sencillo y completo, me han decidido a escribir el siguiente articulo. Espero que mi poca experiencia y mi mucho interes sean clarificadores para todo aquel que desee comenzar a programar. He elegido el lenguaje C para hacer esto por varios motivos, que son totalmente subjetivos y que expongo a continuacion: -Es un lenguaje sencillo (en su descripcion) -Es un lenguaje actual y util -Es un lenguaje historico -Es elegante -Es de medio-bajo nivel pero puede servir para aplicaciones de alto nivel -Es compilado, y existen compiladores GRATIS y BUENOS. -Es el que mas me gusta, el que mas conozco y en el que tengo mas experiencia. La programacion que voy a tratar aqui es una programacion de micro- procesadores. Los microprocesadores son unos circuitos estupidos que solo saben hacer algunas cosas: -Leer datos desde una zona de memoria -Guardar datos en una zona de memoria -Sumar, restar, multiplicar y dividir numeros -Comparar numeros y comprobar condiciones aritmetico-logicas (es cero, hay acarreo, es negativo, etc.) -Segun los resultados, decidir cual es la siguiente operacion a realizar Todo esto puede parecer trivial, pero lo escribo para que se tenga claro que un ordenador no es inteligente. Si a una persona con conocimientos minimos de aritmetica y logica le dieran tiempo y papel suficiente, seria capaz de hacer lo mismo que un ordenador: seguir las ordenes del programa. Y obtendria el mismo resultado. Lo que hace a los procesadores tan potentes es que son infinitamente mas rapidos que las personas. Pero no son inteligentes. No hay tarjetas inteligentes ni casas inteligentes. Hay tarjetas programadas y casas programadas. Y si el programador no es inteligente, menos aun lo seran las tarjetas y las casas que el programe. Con este curso intentare hacer pensar a las personas que desean aprender a programar, dar una base de algoritmia, de sintaxis de C y -puede que al final- algo de idea de arquitectura de ordenadores. Espero que no me haya fijado una tarea mas alla de mis posibilidades. 1. Los datos. Las computadoras manejan datos. Es su unico fin. ¿Que son los datos? Los datos son trozos de informacion. Un dato puede ser mi edad, mi nombre, mi apodo (lindir), el numero de segundos que ha pasado desde las 00:00:00 del 1 de enero de 1970, si la unidad de disco duro esta ocupada, la direccion de memoria donde esta almacenado el caracter que ocupa la posicion superior izquierda de la pantalla, el valor digitalizado de la tension de salida del microfono, etc. 1.1 Bits. Debido al origen electronico de los procesadores y a la simplicidad del sistema binario, los datos se almacenan en bits. Un bit es un apocope del termino ingles BInary digiT. Un bit es la menor informacion que podemos almacenar, y su valor puede ser cero logico o uno logico. Segun el significado que queramos darle, podemos ver algunos ejemplos. Dato Significado 0 Significado 1 ----------------------------------------------------------------------- a) Sexo del usuario Varon Mujer b) Estado del boton derecho del raton Pulsado No pulsado c) Estado del pixel superior izquierdo de la pantalla en una Apagado Encendido pantalla monocroma 1.2. Numeros. El bit como informacion es muy pobre. Por ello usualmente se agrupan formando octetos (bytes). Un octeto son ocho bits. Puede ocurrir que cada bit de un octeto indique una condicion binaria como las vistas anteriormente (del tipo verdadero/falso), que haya agrupaciones de varios bits (2, 3...) o que todo el octeto guarde solo un dato. El ejemplo mas directo es el octeto que almacena un numero. Los numeros se almacenan en octetos en distintos formatos. El mas sencillo es el numero entero no negativo puro. Puesto que un octeto son ocho bits, se hace algo parecido a lo que se hace en numeracion arabiga en base 10. Cuando tenemos un numero de varias cifras, la primera se multiplica por 1, la segunda por 10, la tercera por 100... y al final se suman todos los resultados. De este modo, 326 = 3*1 + 2*10 + 6*100 En el caso que estamos viendo, el formato binario es el siguiente: el bit menos significativo almacenar un valor 0 aritmetico (cero logico) o 1 aritmetico (uno logico). Este valor se multiplica por 2**0=1 (** representa el operador exponenciacion). El segundo bit menos significativo almacena un valor que se multiplicara por 2**1=2, el tercero por 2**2=4 ,etc. De esta forma, si representamos de izquierda a derecha los valores logicos almacenados en los bits de un octeto, podemos tener los siguientes ejemplos Numero Octeto binario -------------------------------------- a) 1 00000001 b) 2 00000010 c) 5 00000101 d) 9 00001001 e) 15 00001111 Las operaciones serian: a) 1 = 0 * (2**7) + 0 * (2**6) + 0 * (2**5) + 0 * (2**4) + 0 * (2**3) + 0 * (2**2) + 0 * (2**1) + 1 * (2**0) b) 2 = 0 * (2**7) + 0 * (2**6) + 0 * (2**5) + 0 * (2**4) + 0 * (2**3) + 0 * (2**2) + 1 * (2**1) + 0 * (2**0) c) 5 = 0 * (2**7) + 0 * (2**6) + 0 * (2**5) + 0 * (2**4) + 0 * (2**3) + 1 * (2**2) + 0 * (2**1) + 1 * (2**0) d) 9 = 0 * (2**7) + 0 * (2**6) + 0 * (2**5) + 0 * (2**4) + 1 * (2**3) + 0 * (2**2) + 0 * (2**1) + 1 * (2**0) e) 15 = 0 * (2**7) + 0 * (2**6) + 0 * (2**5) + 0 * (2**4) + 1 * (2**3) + 1 * (2**2) + 1 * (2**1) + 1 * (2**0) Esto es solo una forma de almacenar numeros en un octeto. Con este metodo podemos almacenar valores entre 0 y 255. Para almacenar valores mayores pueden usarse dos o mas octetos, y hay otros metodos para valores con signo o con decimales, como signo-magnitud, complemento a 1, complemento a 2, reales en coma fija o los estandares IEEE para numeros en coma flotante que mezclan los anteriores. Como referencia, decir que la mayor parte de los procesadores modernos utiliza la representacion en complemento a 2 (Ca2) para numeros enteros y los estandares IEEE para coma flotante. No se utiliza la representacion en coma fija, pero se puede "emular" la misma con enteros en complemento a 2 y desplazamientos de bits. Si no entendeis nada de esto, no os preocupeis: lo comprendereis cuando os haga falta. Lo importante de esto es ver como con un sencillo bit, que puede representarse electricamente como una tension alta o una tension baja, agrupando varios podemos representar cualquier dato numerico que deseemos. Esto es cierto en forma aproximada, ya que no pueden representarse numeros reales como e o pi de forma exacta. Pero nadie necesita eso en la vida real. 1.3. Caracteres. Ya sabemos como se representan los numeros. Pero... ¿y las letras?. Muy sencillo. Para representar una letra, asignaremos a la misma un numero. De esta forma prodriamos asignar el 1 a la A, el 2 a la B, etc. Y, usando el metodo de un octeto anteriormente visto, almacenar hasta 255 letras. Pero el alfabeto tiene menos de 30 y ademas para escribir hace falta tener ciertos signos de puntuacion. Por ello se usan representaciones estandar como el ASCII. El ASCII (American Standar Code for Information Interchange) no es mas que una relacion entre numeros y caracteres. La tabla ASCII asocia un numero a cada caracter (letra, puntuacion, espacio, tabulador, etc.). En la tabla ASCII el caracter a tiene asociado el numero 97, y el A el 65. Entonces tener almacenado en memoria la palabra ABBA en ASCII seria equivalente a tener en memoria el conjunto de numeros 65 66 66 65 o en binario: 01000001 01000010 01000010 01000010 Existen otras representaciones alternativas (tablas de codigos) para los caracteres, pero hoy en dia la tabla ASCII es la mas utilizada por los "paises occidentales". 1.4. Tipos basicos de datos en C. C ofrece un conjunto limitado de tipos de datos basicos, que son caracteres, numeros enteros con y sin signo y numeros flotantes. A los tipos de datos enteros (int y char) se les puede aplicar el calificador "signed" o el calificador "unsigned", indicando en cada caso si es un valor con signo (positivo o negativo) o sin signo (no negativo). El tipo "char" ocupa un octeto, y sirve para almacenar caracteres, o tambien numeros pequeños de entre 0 y 255 (sin signo) o entre -128 y 127 (con signo). El tipo "int" ocupa distinto segun la maquina para la que se compile el programa. En un 486/Pentium I/Pentium II sera de 32 bits. Podemos añadir el calificador "short" para conseguir un entero corto (16 bits en estos procesadores) o "long" para conseguir un entero largo (32 bits en estos procesadores). Tened cuidado si pensais crear programas portables y tratais con los octetos que forman los numeros enteros por separado: en algunas maquinas los enteros son "little endian" y en otros "big endian". Existen dos tipos de numeros reales en C: el tipo "float" (numeros en coma flotante) y el "double" (numeros en coma flotante de doble precision). Tambien existe el tipo "long double" para numeros de coma flotante con precision extendida. No hablo mas este tipo de datos porque tampoco seran tan importantes para comenzar a programar en C. Cuando os hagan falta, podreis mirar los formatos de coma flotante del IEEE en cualquier documento de internet. C no define ningun tipo basico booleano (verdareo/falso), pero en las expresiones logicas considera un valor 0 como falso y cualquier valor distinto de 0 como verdadero. Si el compilador genera un valor "verdadero" ,por ejemplo mediante la expresion !0 (NOT FALSE), este valor numerico sera uno. Es decir, !0 es igual a 1. Existe un tipo de datos basico especial que son los apuntadores o punteros. Un puntero no es mas que un tipo de datos que almacena la direccion en memoria de otro tipo de datos. Asi podremos tener punteros a caracteres, a enteros, a flotantes, y el puntero especial al tipo void, que se utiliza como puntero generico (ya veremos cuando hay que usarlo). Los punteros son muy utilizados en C, pero su tratamiento lo veremos mas adelante, cuando tengamos mas idea del resto del lenguaje. - Conversion de tipos. Suele ocurrir que necesitamos que un valor con un tipo determinado pase a ser de otro tipo. Por ejemplo, porque ambos formen parte de una expresion. Para ello, existe conversion implicita de tipos que el compilador proporciona. Asi, en la expresion: 2L + 'a' el valor 'a' es de tipo char, mientras que el valor 2L es de tipo long. El compilador "promociona" el tipo menor (char en este caso) al mayor (long) y el resultado sera del tipo mayor (long). El resultado de dicha expresion seria un valor de tipo long igual a 99 (recordar que 'a' en ASCII es 97). Estas conversiones de tipo son automaticas y el programador no tiene que preocuparse por ellas. Existe otra clase de conversiones, la conversion explicita o "cast". Los casts se utilizan cuando deseamos que un tipo determinado se interprete como otro tipo. Esto es especialmente util en el caso de los punteros, como luego veremos. De cualquier modo, si utilizamos un tipo incorrecto (por ejemplo como parametro para una funcion) el compilador NOS DEJARA HACERLO, aunque posiblemente nos indique esta situacion con un aviso o "warning". Si realmente deseamos hacer eso (no es un error que se nos ha colado), podemos evitar el warning con un cast. Por ejemplo podemos hacer esto: int numero = 97; char a = (char) numero; El cast es un operador unario cuya sintaxis es "(tipo_al_que_convertir)". En el ejemplo, se realiza un cast sobre la variable numero (de tipo int) al tipo char. 1.5 Comentarios. C permite dos tipos de comentarios en el codigo de los programas. El primero es el original de C, y consiste en usar las secuencias /* y */ para englobar el texto. De esta forma, un comentario es: /* Esto es un comentario */ Estos comentarios pueden ocupar mas de una linea y no pueden contener las secuencias limitadoras por razones obvias: /* Esto es un comentario de varias lineas */ Esto ultimo significa que los comentarios no pueden estar anidados. No pueden incluirse comentarios dentro de comentarios. Por lo tanto lo siguiente es erroneo: /* Comentario padre /* Comentario hijo */ */ El preprocesador veria un comentario con contenido "Comentario padre /* Comentario hijo" y un finalizador de comentario "*/" sin el iniciador "/*" correspondiente. El otro tipo de comentarios esta tomado de la sintaxis de C++ y son comentarios de una linea, usando la secuencia //: // Esto es un comentario de una linea. Los comentarios no generan codigo ni reservan espacio y son totalmente eliminados por el preprocesador. Su uso es exclusivo para el mantenimiento del codigo: usad comentarios para documentar el codigo que escribais, por si mas tarde teneis que volver a entenderlo para modificarlo o por cualquier razon. No useis comentarios tontos como: if (a == 1){ /* si a vale 1... */ ... Sino algo mas bien como: /* dia_semana: funcion que toma como parametro la fecha y hora en segundos desde "La Epoca" y devuelve el dia de la semana (1 a 7 comenzando por el lunes) */ int dia_semana(int fecha){ ... 1.6 Constantes. Los datos que no cambian se denominan datos constantes, y los que si cambian se llaman datos variables. Las constantes son datos de nuestro programa que no deben cambiar. A veces tambien se conocen con el nombre de "literales". Las constantes tambien tienen su tipo, y pueden ser enteras, de coma flotante, de caracter, de cadena de caracteres y el caso especial de las enumeraciones. En C, las constantes enteras pueden ser de tipo int o long. Ademas, pueden ser signed o unsigned. Si solo se usa el valor numerico, el tipo sera int. Si queremos que sean unsigned, debemos añadir una u o U. Para conseguir una constante "long int" debemos hacer que termine en l o L. Ejemplos: 123 Tipo int 123L o 123 l Tipo long int 123U o 123 u Tipo unsigned int 123UL o 123 ul Tipo unsigned long Ademas, podemos utilizar representaciones en base 10 (123) en hexadecimal si comenzamos por "0x" (0x7b o 0x7B) y en octal si comenzamos por "0" (0173). Mucho cuidado que 066 es distinto de 66. Asimismo las constantes en coma flotante pueden escribirse con un punto decimal (1.7) o con mantisa-exponente (2.8e-7 o 2.8E-7). El tipo por defecto para estas constantes es double. Si queremos una constante float, debemos terminar con F o f (1.2f o 1.2F) y si la queremos long double, con L o l (1.2L o 1.2l). Las constantes de caracter se encierran entre signos ''. Asi, el caracter a se escribe 'a'. Tambien pueden usarse las representaciones '\o141' (en octal) o '\x61' (en hexadecimal). Asimismo, existen los denominados "caracteres de escape", como el de nueva linea ('\n'), la campana ('\b'), el tabulador ('\t'), etc. Recordar que el caracter \ debe escaparse a su vez, y debe ser escrito '\\'. Las constantes de cadena estan formadas (como todas las cadenas en C) por una secuencia de octetos terminados por un octeto a cero (caracter '\0'). La forma de escribir una constante de cadena es "Esto es una constante". Para incluir el caracter '"' en una constante de cadena, hay que escaparlo, es decir, la constante con valor 'Esto es una "constante"' se debe escribir como "Esto es una \"constante\"". C permite (gracias a su preprocesador) asociar identificadores a las constantes mediante la directiva #define. De esta forma, podemos por ejemplo escribir: #define CADENA "Esto es una cadena" /* Constante de cadena */ #define EDAD_MINIMA 18 /* Constante entera */ #define EURO 166.386 /* Constante coma flotante */ Notar que los #define no son sentencias para el compilador (sino para el preprocesador) y por lo tanto no terminan con ; como ocurre con las demas. Por ultimo, se permite una clase de constante numerica especial: las enumeraciones. Una enumeracion es una lista de valores enteros asociados a un nombre. Por ejemplo, para definir la enumeracion de nombre "dias", que asocie a los identificadores "LUNES", "MARTES", etc. los numeros 1,2... debe escribirse: enum dias { LUNES=1, MARTES=2, MIERCOLES=2, JUEVES=4, VIERNES=5, SABADO=6, DOMINGO=7}; O, mas corto: enum dias { LUNES=1, MARTES, MIERCOLES, JUEVES, VIERNES, SABADO, DOMINGO}; Si se omite el primer valor, la enumeracion comienza por cero. La siguiente enumeracion asocia el valor 0 a NADA y el valor 1 a TODO: enum cantidad { NADA, TODO }; Alguien puede preguntarse para que vamos a necesitar una constante o enumeracion, y si no estariamos trabajando el doble al usarlas, ya que podemos obviarla y sencillamente utilizar la constante numerica directamente. Bien, hagamos pensar a ese alguien. Supongamos que tenemos un programa en el que asociamos a cada dia de la semana (comenzando por lunes) los numeros del 1 al 7. Supongamos tambien que el programa realiza copias de seguridad de los archivos los domingos. Entonces escribimos en C: if (dia==7) hacer_bakckup(); Si guardamos ese programa y lo olvidamos hasta pasados unos años, al volver a leer nuestro codigo, ese "7" no nos da ninguna informacion: puede ser que se refiera al domingo, o al dia 7 de cada mes, o al septimo dia del año, o al septimo dia desde que el programa comenzo a funcionar... Es lo que se conoce como un "numero magico". Ahora supongamos que usamos las enumeraciones. El codigo seria: if (dia==DOMINGO) hacer_backup(); En este caso no hay lugar a dudas. Si alguien aun no se ha convencido de que el uso de constantes con identificador y enumeraciones ahorra tiempo, que no las use: cuando realice un proyecto de programacion medianamente complejo (y si es posible con varios colaboradores) aprendera que usar numeros magicos directamente en lugar de #define y enumeraciones suele derivar en una amputacion de gonadas previo dolor de cabeza. 1.7 Variables. Por variables entendemos las posiciones de memoria que almacenan datos que cambian durante nuestro programa (datos variables). Para referirnos a cada una de las variables de nuestro programa les asignamos un identificador, al igual que ocurria con las enumeraciones. Los identificadores validos se componen de letras, numeros y el caracter _ y deben comenzar por una letra o por un _. Por supuesto la lengua de Cervantes queda como siempre excluida dentro del estandar, asi que no intenteis utilizar tildes ni la letra ñ en vuestros identificadores. Todas las variables de nuestro programa deben ser declaradas. La declaracion sirve para indicarle al compilador que queremos usar una variable con un tipo y un identificador determinados. De esta forma, el compilador reserva el espacio necesario para ella, y asocia esta zona de memoria a todas las operaciones que realicemos en las que utilicemos dicho identificador. Como norma general, suelen reservarse los identificadores con todas las letras en mayuscula para las constantes, pero C no impone esto: es solo una regla de estilo. Lo que si que no conviene es usar identificadores del tipo __NOMBRE__, _MIVARIABLE o __MICONSTANTE, puesto que suelen ser usados por las bibliotecas, salvo que estemos escribiendo una biblioteca nosotros mismos, claro esta. Hay quien gusta de mezclar mayusculas y minusculas en los nombres de variables (EstoEsMiVariable o DiaDelMes) quien usa _ para separar palabras (esto_es_mi_variable o dia_del_mes) y quien no usa nada de esto (mivariable o diames). Si no teneis claro que estilo usar, podeis buscar por internet especificacion de estilos (el estilo Kernighan y Ritchie, el GNU, etc.). La declaracion de variable es: tipo nombre [= valor];. La parte "= valor" la escribo entre corchetes porque es opcional. Tambien puede usarse la coma para declarar multiples variables de un mismo tipo asignandoles un valor inicial a cada una, a algunas o a ninguna. Por ejemplo: int dia; int dia=1; int dia=1,mes=1,anno=2004; char letraA = 'A', opcion; 1.8 Operadores. - Operadores aritmeticos. Para trabajar con las constantes y variables se utilizan los operadores. Los primeros en los que alguien piensa son los operadores aritmeticos, asi que veremos cuales nos permite C. Antes que nada, decir que los espacios entre operandos y operadores pueden obviarse, es decir, "1+1" equivale a "1 + 1". Esto ocurre en general para todos los caracteres de espacio de un programa: solo sirven para que el codigo sea claro, puesto que lo primero que hace el preprocesador es eliminar dichos caracteres (siempre que no formen parte de una constante de tipo caracter o de cadena de caracteres, en tal caso se mantienen, por supuesto). En ejemplos anteriores habreis podido notar que a veces utilizo espacios y otras veces no; es indiferente. Los operadores aritmeticos basicos son suma (+), resta (-), multiplicacion (*), division (/) y resto de la division entera o "modulo" (%). Todos ellos pueden usarse con cualquier tipo numerico salvo %, que por razones obvias no puede usarse con numeros en coma flotante. Un ejemplo del uso de estos operadores seria: int a=5,b=2; int c=a*b; /* c vale 10 */ int d=a/b; /* d vale 2 */ int e=a%b; /* e vale 1 */ Existen operadores unarios (toman solo un operando) para el signo. Son + (positivo) y - (negativo). Entonces podemos escribir: a = -2; /* a vale -2 */ b = +5; /* b vale 5 */ Tambien existen operadores unarios para incremento o decremento de variables. Son ++ (incremento en 1) y -- (decremento en 1) y pueden usarse como operadores sufijos o postfijos. Si los usamos como prefijos, i.e. ++numero, primero se realiza el incremento/decremento y luego se evalua la expresion al completo. Si se utilizan como postfijos, numero++, primero se evalua la expresion y luego actua el operador. Asi: int numero = 5; int numero2 = ++numero; /* numero2=6, numero=6 */ int numero3 = numero--; /* numero3=6, numero=5 */ La precedencia de los operadores aritmeticos es: primero los operadores unarios, luego los de multiplicacion (*, / y %) y finalmente los de suma (+ y -). Pero pueden usarse los parentesis para conseguir realizar los calculos en el orden deseado. Por ejemplo, la expresion 1+2*3 es igual a 7, pero si escribimos (1+2)*3 entonces el resultado es 9. La regla general es que los operadores de multiplicacion preceden a los de suma y respecto a los operadores logicos, && precede a ||. De cualquier modo, mejor buscad una lista con los operadores y su precedencia. Hay miles por internet y no os voy a escribir todo, ¿no? Ah, y cuando tengais una duda lo mejor es usar parentesis, que son "gratis" y no pasa nada si son redundantes. - Operadores logicos. Por otro lado tendremos los operadores logicos. Estos son && (AND), || (OR) y ! (NOT). Estos operadores utilizan el algebra de Boole, asi que si alguien tiene alguna duda de como funcionan, que se documente sobre el tema. Lo que debemos saber es que las expresiones logicas se evaluan de izquierda a derecha, y que ademas las subexpresiones logicas no se evaluaran si ya se sabe el resultado final de la expresion. Supongamos que tenemos dos variables de tipo entero a=1 y b=0. Entonces la expresion: (a && b) && (a || b) sera evaluada de la siguiente forma: 1. a&&b es 1&&0 = 0 (FALSO) 2. El resultado final es 0 (FALSO). Vemos que no se ha evaluado la subexpresion a||b porque no hacia falta para hallar el resultado final: FALSO AND X es siempre FALSO, no importa el valor de X. En cambio, si b vale tambien 1, (a&&b)&&(a||b) se evaluaria: 1. a&&b es 1 2. a||b es 1 3. 1 && 1 es 1. El resultado final es 1. En este caso si ha sido necesario evaluar a||b puesto que el resultado de 1&&X depende del valor de X. - Operadores relacionales. Los operadores relacionales o de comparacion permiten determinar si un numero es mayor, menor o igual que otro. Los operadores son > (mayor que), >= (mayor o igual que), < (menor que), <= (menor o igual que), == (igual a) y != (distinto de). Todas las siguientes expresiones devuelven un valor verdadero: 2>1, 2<3, 2>=2, 2<=3, 2==2 y 1!=2. - Operadores de bit. Finalmente, los operadores de bit actuan sobre todos los bits de un valor. Estos operadores siguen tambien el algebra de Boole a nivel de bit, y son & (AND de bit), | (OR de bit), ~ (NOT de bit) y ^ (XOR de bit). Estos operadores suelen utilizarse para "activar", "desactivar" o "testar" bits dentro de un grupo de bits (octeto, palabra de 16 bits, palabra de 32 bits, etc.) utilizando mascaras. ¿Como es esto?. Bien, supongamos la siguiente declaracion: int numero = 5; ¿Como podemos saber si 5 es par? Bueno, direis que ya lo sabemos, pero supongamos que no iniciamos la variable numero nosotros, sino que recibimos su valor por teclado (mas tarde aprenderemos a hacer eso). Hay dos maneras sencillas de ver si un numero es par: 1. Hallar el resto de la division por 2, es decir, evaluar numero%2 y comprobar si es cero. 2. Evaluar el estado del LSB (Least Significant Bit, bit menos significativo) del numero y comprobar si es cero. La segunda forma sera la que utilicemos. Si no comprendeis por que el LSB de los numeros enteros impares esta a 1 (activo), simplemente pensad que comienza valiendo 0 y se va alternando en cada siguiente numero: Numero LSB 0 0 1 1 2 0 3 1 ... etc. Bien, utilizaremos una variable de tipo int como un booleano (0 = FALSO, 1 = VERDADERO) que indicara la paridad de nuestra variable numero. El codigo completo seria: int numero = 5; int esimpar; esimpar = numero & 1; /* Si numero es impar, esimpar = 1 */ ¿Que hemos hecho? Sencillamente la AND de numero con la mascara: 1. La operacion seria: 00000000 00000000 00000000 00000101 (5) 00000000 00000000 00000000 00000001 (1) ----------------------------------- 00000000 00000000 00000000 00000001 (5&1=1 o VERDADERO) Si realizamos la AND con una mascara, el resultado tendra a 0 todos los bits que tambien esten a 0 en la mascara (bits del 1 al 31 en el ejemplo) y el resto de bits tendran el mismo valor que los correspondientes bits en el numero inicial (bit 1 en el ejemplo). Otro ejemplo para dejarlo claro puede ser: 13&9 00000000 00000000 00000000 00001101 (13) 00000000 00000000 00000000 00000101 (9) ----------------------------------- 00000000 00000000 00000000 00000101 (13&9=9) Asimismo podemos usar la OR para activar un bit determinado o un patron de bits. Por ejemplo numero|5 activa los bits 0 y 2 de la variable numero: 00000000 00000000 00000000 100000001 (numero, supongamos = 129) 00000000 00000000 00000000 000000101 (5) ------------------------------------ 00000000 00000000 00000000 100000101 (129|5 = 132) Y utilizar la XOR para cambiar el estado de un bit o patron de bits determinado. Por ejemplo, supongamos que queremos cambiar el estado de los bits 0 y 4 del caracter 'a' (ASCII 97). Seria entonces 'a'^17: 01100001 ('a') 00010001 (17) -------- 01110000 ('a'^17) Podemos ver que el estado de los bits 0 y 4 ha cambiado, y que el resto de bits ha conservado su estado original. Otro tipo de operadores de bit son los operadores de desplazamiento. Estos son << (desplazamiento a la izquierda) y >> (desplazamiento a la derecha). Si se realizan desplazamientos hacia la derecha sobre tipos unsigned, los bits mas significativos (comenzando por el de signo o MSB, bit mas significativo) se pondran a cero. Si se hace sobre cantidades con signo, se pondran a 0 en algunas maquinas y a 1 en otras, asi que mucho ojo con este caso en el que el comportamiento no esta especificado. Para los desplazamientos hacia la izquierda, siempre se rellena con bits a 0. Los operadores de desplazamiento pueden usarse para multiplicaciones o divisiones por potencias de dos (2 elevado a tantos bits como se desplace). Asi, 5>>2 equivale a 5/(2**2) = 1. Tambien 5<<3 equivale a 5*(2**3)=40: 00000101 (5) 00000001 (5>>2=1) 00100100 (5<<3=40) C no proporciona operadores para desplazamientos ciclicos, como puede ocurrir con otros lenguajes. Los bits que se "salen" del espacio de la variable simplemente desaparecen. - Operadores de asignacion. Durante todos los ejemplos anteriores hemos estado usando el operador de asignacion =. Este operador se usa para almacenar un valor en una variable. De este modo la siguiente sentencia almacena el valor 5 en la variable de tipo entero a: a = 5; /* a vale 5 */ Tambien puede hacerse que multiples variables almacenen el mismo valor en una sola sentencia, encadenando el operador = asi: a=b=c=d=5; /* a,b,c y d valen 5 */ Esto puede hacerse ya que el operador = no solo guarda el valor a la derecha en la variable de la izquierda, sino que ademas actua como una expresion que devuelve el valor almacenado. Asimismo, debido a esto la siguiente expresion se evalua como 1 (VERDADERO): 1 < (a=3) Suele ocurrir a menudo que utilicemos expresiones del estilo: operando1 = operando1 OPERADOR operando2; Para que sea mas sencillo de escribir, se permiten los operandos de asignacion. Hay operandos de asignacion asociados a todos los operandos aritmeticos y de bit, por ejemplo +=, /=, ^=, >>=, etc. Asi: int numero = 1; numero +=2; /* numero = 3 */ numero /=2; /* numero = 1 */ numero ^=2; /* numero = 3 */ numero >>=1; /* numero = 1 */ - El operador sizeof() El compilador ofrece un operador muy util para trabajar con la memoria. El operadore sizeof() devuelve el tamaño de una variable o tipo de datos. Por ejemplo, sizeof(char) devuelve 1. Y en un Pentium, sizeof(int) devuelve 4. Otro ejemplo puede ser: short mivariable; int longitud; longitud = sizeof(mivariable); /* longitud vale 2 en un Pentium */ Estos valores se calculan en tiempo de COMPILACION. Es decir, el compilador evalua el tamaño de la variable o tipo de datos y utiliza dicho valor como una constante, no se calcula en tiempo de ejecucion. Esto que ahora puede parecer oscuro quedara mas claro cuando veamos asignacion dinamica de memoria. El operador sizeof() no puede utilizarse para hallar el tamaño de una zona de memoria reservada dinamicamente. En cambio, sizeof() puede usarse para calcular el tamaño de una matriz constante, estatica o automatica, como luego veremos. - Otros operadores. Existen otros operadores, como la desreferencia (*), el operador direccion de (&), el operador de conversion explicita de tipos (cast) visto anteriormente, etc. Los operadores aun no vistos los iremos introduciendo conforme vayamos avanzando con el lenguaje. 1.9 Punteros y matrices. Hasta ahora hemos tratado con tipos de datos basicos: enteros, caracteres y numeros en coma flotante. Hemos hablado algo de cadenas de caracteres, pero aun no esta claro como tratar con ellas. Ademas, hemos dejado aparte un tipo de datos fundamental en C: los punteros o apuntadores. - Punteros. Un apuntador o puntero no es mas que una variable que almacena la direccion de memoria de otra variable. ¿Para que puede servirnos esto? Pues para acceder a esta variable *de forma indirecta*. Ahora veremos esto con mas detenimiento. Para declarar una variable de tipo puntero en C, hay que seguir la siguiente sintaxis: tipo_al_que_apunta * identificador; Por ejemplo, vamos a declarar a continuacion tres variables de tipos puntero a caracter, a entero y a flotante respectivamente: char *pcaracter; int *pentero; float *pflotante; Aunque pcaracter, pentero y pflotante son de distinto tipo, las tres son punteros. Y un puntero no es mas que un numero: la direccion de la variable a la que esta apuntando. En cada maquina, las direcciones ocupan un tamaño determinado. En los x86 modernos (nada de 8086...) de Intel son de 32 bits. Por lo tanto todos los punteros ocupan el mismo espacio en memoria, no importa a que tipo apunten. Bien. Ya sabemos declarar un puntero. ¿Como lo utilizamos? ¿Como podemos almacenar un valor util en el? Bueno, es sencillo. Vamos a utilizar el operador & (direccion de). &mivariable nos devuelve la direccion en la que se almacena la variable "mivariable". Y ese valor es precisamente lo que podemos almacenar en un puntero. Entonces: int numero; int *puntero_a_numero; puntero_a_numero = № A partir de este instante, puntero_a_numero almacena la direccion de la variable numero. Ahora queremos acceder a la variable de forma indirecta, es decir, a traves de su direccion almacenada en el puntero. Para ello, usamos el operador * (desreferencia o indireccion). *puntero_a_numero accede a la direccion de memoria almacenada en puntero_a_numero. Podemos utilizar esto para leer dicha direccion o para escribir en ella. Por ejemplo: int numeroa; int numerob = 5; int *punteroa=&numeroa; int *punterob=&numerob; numeroa = *punterob; /* numeroa vale 5 ahora */ *punteroa = 8; /* numeroa vale 8 ahora */ La orden "numeroa = *punterob;" almacena en la variable numeroa el contenido de la direccion de memoria almacenada en la variable punterob. Puesto que anteriormente (*punterob=&numerob;) hemos almacenado en punterob la direccion de memoria de la variable numerob, lo que hacemos es en definitiva equivalente a la orden numeroa = numerob;. Asimismo, con la orden "*punteroa=8" lo que hacemos es almacenar el valor 8 en la posicion de memoria guardada en la variable punteroa. Como anteriormente hemos guardado la direccion de memoria de numeroa en la variable punteroa, la orden anterior es equivalente a numeroa = 8;. Puede parecer que los punteros no sirven para mucho, porque los ejemplos que hemos visto son sencillos. Pero los punteros son utiles para muchas cosas, por ejemplo: - Paso de parametros "por referencia" - Paso de estructuras como parametros a funciones (por referencia) - Tratamiento de matrices de tamaño variable - Reserva dinamica de memoria y uso de dicha memoria - Definicion y uso de estructuras complejas como pilas, listas, arboles, etc. Todas estas funciones son demasiado complejas para explicarlas aun, pero mas tarde veremos todas y cada una de ellas. De momento, solo trataremos el uso de punteros para acceder a matrices. Aunque en todos los ejemplos anteriores hemos utilizado el operador &, tambien es posible guardar una direccion cualquiera (en forma de un numero) en un puntero, pero en general no sabremos que es lo que hay en dicha direccion. Y si intentamos acceder a una direccion que no pertenece a la zona de memoria de nuestro programa, el sistema operativo (suponiendo un sistema de multiprogramacion como Windows o Unix) lo detectara y terminara la ejecucion del programa. Si el sistema permite acceso total a la memoria (como MS-DOS) y modificamos zonas de memoria aleatoriamente, el comportamiento del sistema puede volverse inestable, incluso colgarse o reiniciarse. Esto ultimo debe ser meditado. ¿Que ocurre si nos equivocamos y escribimos un programa con un puntero que modifica una zona de memoria erronea? Pues que el sistema acabara nuestro programa. Es el tipico mensaje "Segmentation fault" de Unix, o el mensaje "El programa realizo una operacion no valida" en Windows. Asi que el trabajo con punteros debe hacerse de forma cuidadosa. - Matrices. Un tipo de datos muy utilizado en programacion es la matriz o tabla, bien unidimensional o multidimensional. Una matriz no es mas que una serie de valores a los que se accede a traves de uno o varios indices. Veamos el ejemplo mas tipico de una tabla bidimensional: Tabla: Columna | 1 | 2 | 3 | ----+-------+-------+-------+ F 1 | 20 | 16 | -3 | i 2 | 0 | -15 | -8 | l 3 | 12 | 10 | 5 | a 4 | 1 | 0 | -2 | Indicando la fila y columna (los dos indices, en este caso) y el nombre de la tabla, podemos saber a que elemento nos estamos refiriendo. Asi, Tabla[1,3] (indico [fila,columna]) almacena el valor -3. La tabla mas sencilla que puede declararse en C es un vector. Un vector no es mas que una tabla de una fila solamente. Para declarar un vector de 12 elementos enteros llamado lluvias se escribe: int lluvias[12]; El compilador reservara entonces 12 bloques consecutivos de memoria de tamaño sizeof(int). Eso equivale a un bloque de memoria de tamaño 12*sizeof(int). Lo importante es que los elementos se almacenan DE FORMA CONSECUTIVA. Luego veremos que eso nos sirve para recorrer una tabla mediante el uso de punteros. Este vector lluvias puede utilizarse por ejemplo para almacenar el numero de litros por metro cuadrado que ha caido en la ciudad cada mes. Para acceder a cada uno de los elementos del vector, debe usarse la sintaxis lluvias[indice]. El valor indice debe estar comprendido entre 0 y 12-1=11. Por lo tanto, si en Enero (mes 0) cayeron 20 litros/m**2 podemos escribir: lluvias[0] = 20; Siguiendo con este ejemplo, podemos hacer algo mas elegante, que seria lo siguiente: enum meses {ENERO, FEBRERO, MARZO, ABRIL, MAYO, JUNIO, JULIO, AGOSTO, SEPTIEMBRE, OCTUBRE, NOVIEMBRE, DICIEMBRE}; lluvias[ENERO] = 20; Tambien podemos iniciar el vector en su declaracion. Para ello, hay que indicar TODOS los valores separados por comas entre {} de este modo: int lluvias[12]={20, 30, 23, 45, 15, 10, 6, 3, 10, 13, 20, 22}; Asi, lluvias[0] vale 20, lluvias[1] vale 30, lluvias[2] vale 23, etc. Incluso podemos obviar el tamaño del vector y solo indicar los valores de iniciacion. En tal caso, el compilador calcula el tamaño para que quepan todos los elementos de iniciacion y NINGUNO mas. Entonces la siguiente declaracion crea un vector de 5 elementos enteros llamado vector: int vector[] = {1, 2, 3, 2, 1}; Ya sabemos como usar tablas unidimensionales. Las tablas bidimensionales son tambien muy utilizadas. Para declarar una tabla bidimensional de tipo int de 3 filas por 2 columnas de nombre tabla seria: int tabla[3][2]; Tambien se puede iniciar la tabla asi: int tabla[3][2] = { {2,4}, {5,-1}, {4,7} }; Hay que hacer notar que al declarar e iniciar una tabla n-dimensional, todas las dimensiones menos la primera deben ser especificadas. Podemos obviar la primera, pero no las restantes. Lo siguiente sera incorrecto y correcto, respectivamente: int tabla[][] = { {2,4}, {5,-1}, {4,7} }; /* Incorrecto */ int tabla[][2] = { {2,4}, {5,-1}, {4,7} }; /* Correcto */ Un uso claro de una matriz bidimensional puede ser la definicion de un caracter de 8x8 pixels en una pantalla de escala de grises. Cada elemento de la matriz representa el nivel de gris en el punto determinado por los dos indices. Generalmente se comienza por el punto superior izquierdo y se acaba en el inferior derecho. El valor almacenado sera de tipo caracter para tener una escala de 256 tonalidades de grises. El valor 0 indica negro y el 255 blanco. Por ejemplo podemos dibujar el caracter 'a' y ver como seria la representacion correspondiente en C en una matriz. A continuacion un 1 indica la maxima intensidad de luz (255) y un 0 la minima (0). El caracter punto a punto seria: Matriz de puntos Pixels en pantalla +-+-+-+-+-+-+-+-+ |0|0|0|0|0|0|0|0| +-+-+-+-+-+-+-+-+ |0|0|0|0|0|0|0|0| +-+-+-+-+-+-+-+-+ |0|1|1|1|1|0|0|0| * * * * +-+-+-+-+-+-+-+-+ |0|0|0|0|1|1|0|0| * * +-+-+-+-+-+-+-+-+ |0|1|1|1|1|1|0|0| * * * * * +-+-+-+-+-+-+-+-+ |1|1|0|0|1|1|0|0| * * * * +-+-+-+-+-+-+-+-+ |1|1|0|0|1|1|0|0| * * * * +-+-+-+-+-+-+-+-+ |0|1|1|1|0|1|1|0| * * * * * +-+-+-+-+-+-+-+-+ Y la forma de hacer esto en C seria (uso representacion hexadecimal): char caractera[][8] = {{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, {0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}, {0x00, 0x00, 0x00, 0x00, 0xff, 0xff, 0x00, 0x00}, {0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00}, {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00}, {0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x00}, {0x00, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0x00}}; En casi todas las representaciones de graficos se utiliza algo asi. Esto se llama mapa de bits (de ahi la extension de los archivos BMP, Bit MaP, de Windows). Para las imagenes en color pueden utilizarse tres caracteres por punto, cada uno de los cuales indica el nivel de rojo, verde o azul de dicho punto, etc. Asi podemos ver que las tablas son algo muy utilizado en programacion. Por supuesto, tambien pueden declararse matrices tridimensionales o n-dimensionales (con n fijo). Aunque pueda parecer que una tabla de mas de tres dimensiones no es util, las matematicas trabajan con tablas n-dimensionales, y C permite dichas tablas. Pensad por ejemplo en un documento de una hoja de calculo, en la que hay varias paginas, cada una de ellas con varias tablas. Podemos almacenar toda esta informacion en una sola tabla si dedicamos un indice para el numero de pagina, otro para el numero de tabla, otro para la fila de la tabla y otro para la columna. Asi solo necesitamos una tabla de 4 dimensiones que C nos proporciona al instante, en lugar de usar por ejemplo 4 tablas de 3 dimensiones distintas... En fin, que realmente puede ser que os hagan falta, y ahi estaran para cuando lo necesiteis. Hay que hacer notar que en una tabla rectangular como esta, todas las filas tienen el mismo numero de columnas. En otros lenguajes pueden definirse tablas con distinto tamaño de fila, pero no en C. Para hacer algo asi en C se utilizan tablas de punteros, como luego veremos, que ademas son mas versatiles que las simples tablas rectangulares. Por ultimo, podemos utilizar el operador sizeof() si queremos saber cuanta memoria ocupa nuestra tabla. Por ejemplo en la tabla caractera[][] anterior, sizeof(caractera) nos devolvera un valor de 64 (8*8). - Aritmetica de punteros. Los punteros tienen una aritmetica muy bien definida en C. Un puntero puede hacerse apuntar a una zona de memoria, pero luego podemos incrementarlo para que apunte a otra zona. Tambien podemos restar dos punteros para calcular la distancia en posiciones de memoria que separa sus valores. Vamos a ver todo esto. Aedmas, aprovecharemos para establecer relaciones entre matrices y punteros que nos seran muy utiles. Lo primero que vamos a ver es como utilizar un puntero para recorrer un vector de caracteres. Lo vemos en el siguiente ejemplo: char cadena[]={'H','o','l','a','\0'}; char *pcar; pcar = &cadena[0]; /* *pcar vale 'H' */ pcar += 1; /* *pcar vale 'o' */ pcar += 2; /* *pcar vale 'a' */ pcar++; /* *pcar vale '\0' */ Vemos que primero almacenamos la direccion de cadena[0] en pcar. Luego vamos incrementando pcar y en cada caso el puntero apunta a un caracter distinto de la variable cadena, segun el incremento que hayamos aplicado. Conviene comentar que la expresion cadena devuelve precisamente la direcion del primer elemento de cadena, es decir cadena es equivalente a &cadena[0]. Por lo tanto podemos sustituir pcar=&cadena[0] por pcar=cadena. Ademas, vamos a ver otro ejemplo: int datos[] = {1, 2, 3, 4}; int *pdat = datos; pdat++; /* Ahora *pdat vale 2 */ pdat++; /* *pdat vale 3 */ pdat-=2; /* *pdat vale 1 */ Vemos que el compilador sabe que pdat apunta a un tipo int, por lo tanto al incrementar pdat mediante pdat++; el compilador lo incrementa hasta el siguiente entero. Es decir, no lo incrementa en 1, sino en sizeof(int). Esto es algo muy importante que puede dar lugar a errores dificiles de detectar a simple vista. Un puntero siempre se incrementa el tamaño del tipo al que apunte. Asimismo, no hace falta incrementar un puntero para recorrer una matriz. Basta sumar un entero a un puntero y utilizar el operador desreferencia. Por ejemplo podemos hacer: int datos[] = {1, -1, 2, -2, 3, -3}; int valor; int *pdat=datos; valor = *pdat; /* valor vale 1 */ valor = *(pdat+1); /* valor vale -1 */ valor = *(pdat+5); /* valor vale -3 */ valor = *(++pdat); /* valor vale -1 */ valor = *(pdat-1); /* valor vale 1 */ Tambien podemos restar dos punteros o compararlos. Por ejemplo: int datos[] = {1, -1, 2, -2, 3, -3}; int valor; int *pdat1=datos; int *pdat2=&datos[1]; valor = pdat2-pdat1; /* valor vale 1 */ pdat2 += 3; valor = pdat2-pdat1; /* valor vale 4 */ pdat2 = pdat1; valor = (pdat1==pdat2); /* valor vale 1 (VERDADERO) */ valor = (pdat1>pdat2); /* valor vale 0 (FALSO) */ - Relacion entre punteros y matrices. Los punteros y las matrices estan muy relacionados. Principalmente porque podemos hacer con punteros todo lo que se hace con matrices. De hecho, hemos visto que si declaramos una matriz por ejemplo con "char matriz[256];", la expresion "matriz" devuelve un valor de tipo "char *" (puntero a char) que apunta al primer elemento de la matriz. Los punteros suelen utilizarse mucho para recorrer tablas, y sobre todo son fundamentales para trabajar con estructuras de memoria dinamica, como veremos mas tarde cuando tratemos las estructuras de datos clasicas y la memoria dinamica. Aun asi, hay una gran diferencia entre un puntero y una tabla, que a menudo no se comprende bien al principio. Veamos las siguientes declaraciones: char cadena[] = "Esto es un ejemplo"; char *pcadena = cadena; Al procesar la primera declaracion, el compilador reserva 19 (el numero de caracteres de la cadena, mas uno mas para el '\0' o terminador) caracteres de memoria. En la seguna declaracion, el compilador reserva SOLO el tamaño de un puntero (4 caracteres en 486/P/PII/PIII, etc.). Entonces, aunque al evaluar las expresiones cadena[3] y *(cadena+3) el resultado sea el mismo, la memoria reservada en cada caso no lo es. Supongamos otro caso: char cadena[] = "Esto es un ejemplo"; char *pcadena = "Esto es un ejemplo"; La primera declaracion reserva una variable de 19 caracteres que pueden modificarse. De hecho la siguiente proposicion es correcta: cadena[0] = 'e'; En cambio, la segunda reserva una variable de tipo char* y la hace apuntar a una CONSTANTE. De forma que la proposicion: *pcadena = 'e'; tiene un comportamiento indefinido y (aunque el compilador puede aceptarla) no es una expresion correcta, ya que esta intentando modificarse una constante. En cambio si es correcto el siguiente codigo: char *pcadena = "Primer ejemplo"; pcadena = "Segundo ejemplo"; En este caso tenemos una variable de tipo char* que primero apunta a una constante y luego apunta a otra. Sin embargo no podemos compilar el codigo siguiente: char cadena[] = "Primer ejemplo"; cadena[] = "Segundo ejemplo"; El compilador no acepta el codigo anterior como valido, ya que no se puede hacer una asignacion a una matriz de caracteres (por ejemplo en Java y otros lenguajes esto si puede hacerse). Por cierto que si alguien se esta preguntando como se copia una cadena de caracteres en otra, dire que: 1. Es necesario que la cadena destino tenga el tamaño suficiente como para albergar a la cadena origen, terminador ('\0') incluido 2. Hay que copiar la cadena caracter a caracter hasta encontrar el terminador. Tambien pueden usarse las funciones strcpy() y strncpy(), preferiblemente esta ultima por cuestiones de seguridad, pero lo que hacen estas funciones es precisamente una copia caracter a caracter. Luego hablaremos de funciones y de la biblioteca estandar. Otro error comun a la hora de escribir un programa que utiliza punteros es el siguiente: char *pcadena; *pcadena='H'; *(pcadena+1)='o'; *(pcadena+2)='l'; *(pcadena+3)='a'; *(pcadena+4)='\0'; Podria pensarse que el codigo anterior lo que hace es ir escribiendo caracter a caracter ('\0' incluido) la cadena "Hola" en memoria, de forma que la variable pcadena apuntase a la misma. Pues no. De nuevo, el compilador *solo* reserva espacio para un char*, y no para los caracteres que componen la supuesta cadena. Asi que estamos escribiendo en una zona de memoria que, digamos, no nos pertenece. La respuesta a la hora de ejecutar ese codigo es la siguiente: Segmentation fault. Es mas, ni siquiera hemos iniciado el valor al que apunta *pcadena, con lo cual no sabemos en que direccion de memoria intentamos escribir. Conclusiones de este ejemplo: 1. Al declarar un tipo char* (o cualquier otro puntero) el compilador solo nos proporciona el espacio necesario para ese puntero, NO PARA LOS CONTENIDOS A LOS QUE APUNTA. 2. NUNCA hay que utilizar el valor de una variable que no se haya iniciado anteriormente. El valor de una variable puede iniciarse bien en el codigo (variable=2;) bien leyendolo de cualquier sitio (un archivo, el teclado, el valor de retorono de una funcion, etc.). Puede ser que vuestro programa funcione en algunos ordenadores incluso utilizando variables no iniciadas, pero eso no significa que sea un codigo correcto: un programa correcto debe poder funcionar con independencia de las condiciones temporales o circunstanciales del sistema sobre el que se ejecute, y utilizar variables sin iniciarlas impide que esto ocurre. Mucho ojo, porque si "suena la flauta" y el programa funciona en vuestro ordenador va a ser muy dificil encontrar y corregir un fallo de este tipo. Por ello, sobre todo con punteros, conviene iniciar el valor de todas las variables en su declaracion; al menos en los comienzos hasta que domineis este tema. De cualquier modo, un compilador medianamente correcto debe indicaros esta circunstancia mediante un "warning". - Punteros a punteros y tablas de punteros. Alguien puede pensar que un puntero a puntero es la mayor tonteria que se puede inventar y que es totalmente inutil. Nada mas lejos de la realidad. Las tablas de punteros, y por ende los punteros a punteros, son muy utilizados. Supongamos un ejemplo medianamente sencillo: queremos almacenar en memoria los nombres de los dias de la semana: -Solucion 1 (en cierto modo chapucera): tener una tabla de caracteres bidimensional con 7 filas y 10 columnas, para poder almacenar los 9 caracteres de "miercoles" (el nombre mas largo) mas el terminador. -Solucion 2 (la mas versatil): tener una tabla de punteros a caracter y hacer que cada uno de ellos apunte a la cadena que necesitemos. La primera solucion se escribe: char dias[][10]={"Lunes", "Martes", "Miercoles", "Jueves", "Viernes", "Sabado", "Domingo"}; Y la segunda: char *dias[] ={"Lunes", "Martes", "Miercoles", "Jueves", "Viernes", "Sabado", "Domingo"}; ¿Son iguales? No. ¿Por que? Sencillamente porque en el primer caso tenemos una tabla de 7x10 caracteres y en el segundo tenemos una tabla de 7 punteros a caracter, cada uno de ellos apuntando a una cadena de caracteres constante. Hay un cierto "desperdicio" de caracteres en la primera solucion. Pero este no es el problema mas grave. En nuestro caso, sabemos que el nombre de los dias de la semana no va a cambiar. Pero si estuviesemos tratando con variables en lugar de constantes, en el primer caso habria que limitar el tamaño maximo, y este seria el mismo para todas las cadenas. En el segundo caso, podemos tener cadenas de tamaños distintos y, ademas, hacer que en un momento dado el puntero apunte a otra cadena de longitud incluso mayor sin modificar para nada la estructura de la tabla. Esto puede parecer algo oscuro ahora, pero cuando tratemos con estructuras de datos cuyo tamaño es desconocido a la hora de compilar el programa y con memoria dinamica, la ventaja de las tablas de punteros frente a las tablas bidimensionales se hara patente. El ejemplo mas claro es el de los argumentos en linea de comandos que se vera mas adelante, cuando tratemos la funcion main(). 2. El flujo de programa Hasta ahora hemos tratado de los datos, que son la materia prima para los programas. Ahora tocaremos lo que es el "estado" del programa, la evolucion del mismo en el tiempo desde que se comienza a ejecutar. Veremos a continuacion el control de flujo del programa mediante bucles y estructuras if..else o switch..case, que son la base para la generacion de codigo util. Mas tarde hablaremos de las funciones, parametros y valores de retorno, de forma que ya estaremos en condiciones de escribir programas sencillos pero reales (obviando la entrada/salida, que sera explicada mas tarde). Los programas se ejecutan en los procesadores de manera secuencial: una instruccion detras de otra. Realmente no hay ejecucion en paralelo como puede existir en el hardware digital (por ejemplo, pueden hacerse dos sumas a la vez con dos circuitos sumadores). Si tenemos sistemas operativos multitarea no es porque permiten ejecucion de instrucciones a la vez, sino porque ejecutan un trozo de un programa, lo paran y pasan a ejecutar otro trozo de otro. Esto lo hacen de forma ciclica y en intervalos de tiempo tan pequeños que nos parece que ambos programas se ejecutan a la vez. 2.1 Control de flujo. En C, el flujo de programa es, por tanto, secuencial, al contrario de como puede ocurrir por ejemplo con el lenguaje de programacion de logica digital VHDL. Las instrucciones se ejecutan "de arriba a abajo". Pero a menudo hay que hacer que ciertas instrucciones se repitan un numero determinado de veces, o hasta que se cumpla cierta condicion, o que se ejecuten solo si se cumple cierta condicion... para todo ello existen las instrucciones de control de flujo. - if ... else ... El control de flujo mas simple que puede realizarse es el de ejecutar una instruccion solo si se cumple una condicion. Para ello se utiliza la palabra reservada if. La estructura de un bloque if es: if (condicion) sentencia1; else sentencia2; Si la condicion se evalua como VERDADERO, se ejecutara sentencia1. En caso contrario, se ejecuta sentencia2. Por ejemplo if (numero%2==0) par = 1; else par = 0; En este caso, si la variable numero almacena un valor par numero%2==0 se evalua a VERDADERO y se ejecuta par=1. En caso contrario, se ejecuta par=0. Puede ocurrir que queramos ejecutar mas de una sentencia. En tal caso debemos definir un bloque de codigo. Los bloques de codigo se encierran entre caracteres { y }. En el ejemplo: if (numero%2==0){ par = 1; impar = 0; } else { par = 0; impar = 1; } Tras la ejecucion del if, si numero es par, par=1 e impar=0. En caso contrario, par=0 e impar=1. Generalmente es una buena practica de programacion utilizar las llaves {} aunque solo sea para una sentencia dentro del if, puesto que ayuda a que el codigo sea mas claro y evita errores si luego queremos añadir mas proposiciones al cuerpo del if. Asimismo, conviene indentar (introducir espacios o tabuladores en el cuerpo del if y el else) para conseguir un codigo mas legible. Todo esto son consejos de estilo para nuevos programadores, podeis seguirlos o no, pero en principio os permitiran tener las ideas mas claras. Si la parte de else no es necesaria, puede obviarse. Por ejemplo: if (numero==2){ esdos=1; } Tambien pueden utilizarse estructuras del estilo: if (numero==2){ esdos=1; } else if (numero==3){ estres=1; } else{ esdos=0; estres=0; } Solo uno de los tres bloques se ejecutara, aunque para condiciones tan sencillas conviene utilizar las estructuras switch/case, que luego veremos. Por ultimo, existe un operador asociado a if, el operador ternario ?. Este operador tiene la siguiente sintaxis: condicion ? expresion1 : expresion2 El valor devuelto por el codigo anterior sera expresion1 si condicion se evalua a VERDADERO y expresion2 si condicion se evalua a FALSO. Asi, podemos escribir el primer ejemplo de esta seccion como: par = (numero%2==0)?1:0; He escrito la condicion entre parentesis para hacer el codigo mas legible, pero no es necesario. Asimismo, suele utilizarse el hecho de que un valor 0 se evalua como FALSO para escribir las expresiones de forma compacta. Podemos escribir la sentencia anterior como: par = (numero%2)?0:1; Vamos a pensar esto un poco. Si numero es par, numero%2 sera 0, es decir, FALSO. Por lo tanto el operador ternario devolvera el segundo valor y par valdra 1. Si ocurre lo contrario, devolvera el primero y par valdra 0. Podemos escribirlo tambien de una forma mas clara: par = (!(numero%2))?1:0; Esto seria: si numero%2 vale 0, !(numero%2) vale 1 y par valdria 1. En caso contrario, !(numero%2) valdria 0 y par tambien. Finalmente, para dar una vuelta de tuerca mas, podemos hacerlo sin el operador ternario, simplemente: par = !(numero%2); /* Pensadlo un poco */ Estos casos no son tan raros, y suele ocurrir que se utilice una variable como "llave" para entrar o no en una condicion. Asi, podemos escribir: if (llave){ .... } El codigo entre {} se ejecutara si llave es distinto de 0. Si escribimos: if (!llave){ .... } El codigo se ejecutara solo si llave es 0. Como he dicho, estas construcciones son corrientes, asi que mejor que os vayais familiarizando con ellas si pensais leer y escribir programas en C. - while. Otra de las cosas que se necesita a menudo es conseguir que un bloque de codigo se ejecute mientras se cumpla una condicion. Para ello estan los bucles while. Su estructura es: while (condicion) instruccion_o_bloque; Cuando el flujo de programa llega al while, se evalua condicion. Si es VERDADERO, se ejecuta instruccion_o_bloque una vez y se vuelve a evaluar condicion. Este proceso se repite hasta que se evalue condicion y resulte ser FALSO. Por ejemplo: int a=2; while (a>0){ a--; } Este ejemplo se ejecutaria: - a = 2 -> almacena 2 en a. - ¿a>0? Si -> sigue con el bucle. - a-- -> ahora a = 1. - ¿a>0? Si -> sigue con el bucle. - a-- -> ahora a = 0. - ¿a>0? No -> fin del bucle. Podemos utilizar bucles while para muchas cosas. Otro ejemplo sencillo es hacer una multiplicacion como sumas sucesivas: int a=5; int b=2; int a_por_b=0; while (a--) a_por_b += b; La condicion a-- comprueba primero si a es distinto de cero y luego decrementa a en 1. El bucle anterior sirve para multiplicar dos numeros mayores que (o iguales a) cero, 5 y 2 en este caso. La unica instruccion del bucle va sumandole b a la variable a_por_b. De esta forma, el bucle suma a (5) veces el valor b (2) a a_por_b, que es precisamente lo mismo que multiplicar a por b y almacenarlo en a_por_b. - do ... while. Puede ocurrir que deseamos que se ejecute el cuerpo del while al menos una vez, independientemente del valor de la condicion al inicio del bucle. Para ello existe la estructura do ... while, que se escribe: do instruccion_o_bloque; while (condicion); Vamos a ver un ejemplo: int numero = 0; do numero--; while(numero>0); Al ejecutar este codigo, numero valdra -1. Si lo hubieramos escrito con un while: int numero = 0; while(numero>0) numero--; Al ejecutarlo, numero valdra 0. Nunca se entra en el bucle, al contrario que con el do. - for. Los bucles for son muy utilizados para recorrer vectores y matrices, aunque pueden usarse para muchas otras cosas. Su estructura es: for (proposicion1;proposicion2;proposicion3) cuerpo_del_bucle; Hay que tener en cuenta que cualquier proposicion devuelve un valor, es decir, es tambien una expresion evaluable. La ejecucion de esto es como sigue: al llegar al inicio del bucle, se ejecuta proposicion1. Despues se evalua proposicion2, y si es VERDADERO se ejecuta cuerpo_del_bucle. Luego se ejecuta proposicion3. Despues se vuelve a evaluar proposicion2 y se continua de forma ciclica hasta que se evalue proposicion2 y sea FALSO. Un equivalente mediante while es: proposicion1; while (proposicion2){ cuerpo_del_bucle; proposicion3; } Normalmente, proposicion1 se utiliza para la iniciacion de variables, proposicion2 para la comprobacion de fin de bucle y proposicion3 para incrementos de variables indice, etc. Aunque no tiene por que ser asi, Kernighan y Ritchie advierten que es de mal estilo utilizar proposiciones que hagan otras cosas dentro de los parentesis de for. El ejemplo de la multiplicacion puede escribirse de forma mas compacta con for: int a=5,b=2,a_por_b; for (a_por_b=0;a>0;a--) a_por_b += b; Cada una de las tres proposiciones del for puede obviarse. De este modo, un bucle infinito (es decir, que nunca acaba) puede escribirse por ejemplo como: for (;;){ .... /* Cuerpo del bucle infinito */ } - switch ... case .... default .... break y continue. A menudo hay que contrastar un valor numerico contra distintos valores constantes. Para ello se utiliza switch. Su estructura general es: switch (condicion){ case CONSTANTE1: cuerpo1; break; case CONSTANTE2: cuerpo2; break; .... default: cuerpo_default; break; } Veamos un ejemplo para comprender su funcionamiento. Supongamos una variable "opcion" que almacena el numero de opcion seleccionada por teclado por el usuario. Por ejemplo, la opcion 1 sumara dos numeros y la 2 los multiplicara. El resto de opciones no son correctas. El codigo puede ser switch(opcion){ case 1: resultado = a+b; break; case 2: resultado = a*b; break; default: opcion_incorrecta = 1; break; } La palabra clave default indica el inicio de las instrucciones a ejecutar si opcion no concuerda con ninguna de los case anteriores (es decir, es distinto de 1 y de 2). No es necesario incluir un default, y tampoco es necesario que vaya al final de todos los otros case, puede ir donde se desee. Pero si es obligatorio que solo aparezca una vez dentro del switch. La palabra clave break sirve para terminar de forma prematura un bucle o un switch. Puede usarse tambien con for, do y while, aunque se desaconseja por no ser de buen estilo de programacion. Por ejemplo puede hacerse: for (;;){ if (condicion) break; ..... /* Resto del bucle */ } De esta forma conseguimos que el bucle infinito iniciado por for(;;) termine cuando se evalue condicion como VERDARERO. Todo lo que puede hacerse con un break puede hacerse sin el (salvo quizas salir de un switch) escribiendo solo un codigo mas estructurado y puede que usando alguna variable mas. Asi que, en aras del buen estilo, intentad limitar vuestros breaks a los switch. Otra palabra reservada que permite alterar la ejecucion normal de un bucle es continue. Una sentencia continue hace que el control de flujo vuelva a ir al inicio del bucle, es decir, que el resto de la iteracion actual no se ejecute. Por ejemplo: while(a>2){ if (b==1) continue; ... /* Resto de operaciones */ } Si se evalua b==1 como cierto, el resto de operaciones no se realiza y se vuelve a evaluar a>2 y comenzar una nueva iteracion del bucle. En este ejemplo se crearia un bucle infinito que "colgaria" el programa, ya que ninguna instruccion modificaria b si llega alguna vez a valer 1 (esto podria ocurrir solo si se tuviesen hilos y se estuviera compartiendo la variable b, pero eso es harina de otro costal). Asi que aunque continue y break pueden usarse en los bucles, no es muy recomendable y siempre se puede realizar lo mismo con otro codigo mas elegante y estructurado. Volviendo al switch, hay que destacar que si no se utiliza break al final de un case el flujo de programa continuara con el siguiente case. Un ejemplo puede ser el siguiente: queremos que si el numero es 1,2 o 3 la variable menor_que_cuatro tome el valor 1, pero si el numero es 1 tambien queremos que la variable es_uno tome el valor 1. En otro caso, las variables anteriores deberan valer 0. Y queremos hacerlo con switch. Esto seria: switch(numero){ case 1: es_uno=1; /* Sigue con case 2... */ case 2: case 3: menor_que_cuatro=1; break; default: es_uno=menor_que_cuatro=0; break; } Vemos que case 2 y case 3 comparten el codigo. Tambien ocurre lo mismo con case 1, pero ademas hay una parte especifica (es_uno=1;) debido a como hemos estructurado el switch sin break. Para evitar que en una revision posterior pensemos que hemos olvidado el break y lo añadamos por error, se suele indicar que el break no hace falta con un comentario (en el ejemplo, con el comentario /* Sigue con case 2... */). - goto y etiquetas. La palabra reservada goto permite desviar el flujo de programa a la posicion donde este situada la etiqueta a la que se refiere. Esta etiqueta puede declararse antes o despues del goto, el compilador sabra buscarla por todo el archivo correspondiente. La sintaxis es: goto etiqueta; ..... ..... etiqueta: El uso de goto y etiquetas esta totalmente desaconsejado porque crean codigo ilegible, pero si pensais que pudiera haceros falta alguna vez, ahi esta. Como comentario, dire que yo nunca he utilizado un goto en mi codigo. 2.2 Funciones. Hasta ahora hemos visto como se consigue hacer un bucle que repita cierta parte del codigo un numero determinado de veces o hasta que se cumpla una condicion, y como hacer que un bloque de codigo solo se ejecute si se da una condicion determinada. Con estas herramientas podriamos hacer un programa real, pero seguramente habria partes del codigo que tendriamos que repetir, y todo estaria escrito como un continuo de codigo infumable. Las funciones nos proporcionan la forma de hacer nuestro codigo mas compacto, conciso, elegante y facil de leer. Ademas, las funciones permiten la reutilizacion de codigo y son la base de las bibliotecas (y en C++ los metodos, que no son mas que funciones, son una parte muy importante de la posibilidad de reutilizacion de codigo). Una funcion es, en parte, muy parecida a un operador porque puede tomar parametros (algo asi como operandos) y porque puede devolver un valor. Pero tambien, puesto que puede ejecutarse las veces que haga falta escribiendola solo una vez, se parece a un bucle. Una funcion no es mas que un subprograma o subrutina. Es decir, una seccion de codigo que puede ser invocada todas las veces que sea necesario. Por ejemplo, supongamos que tenemos un programa que va leyendo cada linea de un archivo de texto y busca en cada una de ellas la palabra "password". Podriamos un bucle que, mientras no se acabase el fichero, leyese una linea del programa cada vez y buscase en esa linea la palabra password. Pero tambien podriamos hacer dos funciones: una que leyese la siguiente linea del fichero y otra que buscase la palabra password en una linea de texto. Luego veremos algo asi. - Declaracion. Para escribir una funcion, debemos declararla y definirla. La declaracion es simplemente indicar el tipo de datos que devolvera la funcion, su nombre y los parametros que puede tomar. Vamos a escribir la declaracion de una funcion que se llame multiplica, que tome como parametros dos valores short y que devuelva un valor int: Podemos indicar solo el tipo de los parametros o tambien el identificador de los mismos. Para indicar que el primer parametro se llamara numero1 y el segundo numero2 seria: int multiplica (short numero1, short numero2); El tipo del valor de retorno de la funcion puede obviarse, con lo que se tomara el tipo por defecto, int: multiplica(short, short); Tambien puede hacerse una funcion que no tome ningun parametro mediante la palabra clave void: int multiplica(void); E incluso podemos hacer una funcion que no devuelva ningun valor, lo que en otros lenguajes (i.e. Pascal) se conoce como un procedimiento: void multiplica(short, short); - Definicion. La definicion de la funcion no es mas que "llenar" ese prototipo de codigo, escribir el codigo que la funcion va a ejecutar. La definicion de la funcion puede hacerse a la vez que la declaracion o mas tarde en el fichero fuente o en otro fichero, como ya veremos cuando veamos los ficheros de cabecera o headers. El codigo que compone la funcion se indica en la definicion de la misma, y se encierra entre {}. Para definir la funcion multiplica() con declaracion: int multiplica (short, short); podemos hacer: int multiplica(short n1, short n2){ return n1*n2; } Vemos que en la definicion es OBLIGATORIO dar un identificador para cada parametro. El identificador no tiene por que coincidir con el identificador de la declaracion, si es que se les dio nombre. De cualquier modo, es recomendable que los identificadores coincidan para no liar el codigo. Lo que si que debe coincidir en la declaracion y la definicion son tanto los tipos del valor de retorno como los de cada parametro. Tambien debe coincidir el numero de parametros que toma la funcion. No se permite la sobrecarga de funciones como en los lenguajes orientados a objetos (C++, Java, Object-Pascal, SmallTalk, etc.). Por tanto, no es valido: int multiplica (short); int multiplica (short a, short b){ return a*b; } Intentar compilar un programa con ese codigo genera el siguiente error en el caso del compilador GCC: program.c:4: conflicting types for 'multiplica' program.c:3: previous declaration of 'multiplica' La palabra reservada return termina con la ejecucion de la funcion e indica cual es el valor que debe devolver. En el ejemplo, vemos que la funcion devuelve el valor a*b. Por lo tanto, hemos hecho una funcion que toma dos valores short, los multiplica y devuelve el resultado como un int. No es muy util, ¿no? Bueno, esperad un poco. - La funcion main. Todo programa en C consta de la funcion principal o funcion main(). Esta funcion es la primera que se ejecuta y cuando termina el programa tambien termina. Es la funcion madre de todas las demas, puesto que cualquier otra funcion ha debido ser llamada desde la funcion main(). Por defecto toma el tipo int, aunque podemos indicar que devuelva otros tipo, i.e. void. Generalmente el compilador emitira un "warning" si el tipo de main no es int. Si main toma tipo int, el valor de retorno de main puede servir para indicar cualquier condicion al acabar el programa (fin con exito, error de disco, etc.). La funcion main puede tomar parametros, que se utilizan generalmente para procesar los argumentos en linea de comandos, pero tambien podemos "pasar de ellos" indicando que su unico parametro es void, o sencillamente escribiendo: main(){ .... } Finalmente, hemos de decir que la funcion main no debe declararse. Solo la definicion es necesaria. Podemos entonces escribir nuestro primer programa compilable en C. Para ello, creamos un fichero de texto ascii de nombre primero.c con cualquier editor de textos (preferiblemente Joe's Own Editor :D) o con el editor incorporado en la herramientas de programacion si utilizais entornos de compilacion de tipo Borland Builder, etc. El contenido de dicho fichero debe ser: int main(void){ int a=1; int b=2; int c; c = a*b; return 0; /* Un valor de retorno 0 indica exito */ } Podemos compilar el programa. Con el compilador GCC la orden es: gcc fichero.c Ya podemos ejecutarlo. En linux sera: ./a.out Si hacemos esto no veremos nada nuevo, simplemente de nuevo el indicador del shell "khorrorsive$ ", "C:\> " en Windos/MS-DOS. Pero el programa se habra ejecutado, habra reservado espacio para tres variables, habra iniciado dos de ellas a 1 y 2 respectivamente, las habra multiplicado y habra almacenado el resultado en la tercera variable y finalmente habra terminado el programa con un valor de retorno 0. Vale, esto puede parecer una mierda de categoria. Quereis ver algo en la pantalla que os muestre que somos la hostia programando y que podemos hacer un m3g4-h4x0r-programa que multiplique dos constantes ¿no? :-P Bueeeeeeeno. Entonces vamos a añadir un poco de "magia" que comprendereis mas adelante. El programa ahora debe ser: #include int main(void){ int a=1; int b=2; int c; c = a*b; printf ("%d x %d = %d\n",a,b,c); return 0; /* Un valor de retorno 0 indica exito */ } Lo compilamos, lo ejecutamos, y.... ¡Tachan! la salida es: khorrorsive$ ./a.out 1 x 2 = 2 khorrorsive$ Muy bien. Ahora podeis cambiar los valores a los que iniciamos a y b y recompilar para ver que realmente multiplica estos dos valores. O podemos hacer algo un poco mas complicado, como utilizar una funcion multiplica() similar a la que hicimos anteriormente. Para ello: #include int multiplica (int n1, int n2){ return n1*n2; } int main(void){ int a,b,c; a=1; b=2; c=multiplica(a,b); printf("%d x %d = %d\n",a,b,c); return 0; } Si compilamos y ejecutamos este nuevo programa, su salida es de nuevo: khorrorsive$ ./a.out 1 x 2 = 2 khorrorsive$ Pero ahora hemos utilizado una llamada a nuestra funcion multiplica(). La parte multiplica(a,b) es la llamada a funcion. - Explicacion exhaustiva del ejemplo. Puede que esto de las funciones te confunda. Voy a intentar explicarlo mejor. Para ello tomare el ultimo programa y lo explicare paso a paso: #include Esta primera linea no es mas que una directiva para el preprocesador que le dice que antes de comenzar la compilacion busque el fichero stdio.h en los directorios "include" por defecto (el compilador esta configurado y sabe donde tiene que buscar) y que el compilador lo procese antes que el resto del fichero. Esto se hace porque la funcion printf() que se utiliza mas tarde es una funcion de la biblioteca estandar cuya DECLARACION esta escrita en este fichero. Es decir, el include se utiliza para que el compilador sepa que existe la funcion printf() y sepa su prototipo (parametros, tipo del valor de retorno, etc.). int multiplica (int n1, int n2){ return n1*n2; } Esta parte ya hemos visto que es la declaracion y la definicion de una funcion, de nombre multiplica, que toma dos parametros de tipo int (n1 y n2) y que devuelve un valor de tipo int. Asimismo, escribimos el codigo correspondiente a la funcion, que consiste solo en que devuelva el resultado de multiplicar ambos parametros. int main(void){ int a,b,c; a=1; b=2; Esta parte comienza la definicion de la funcion main(), indicando primero que devuelve un valor de tipo int y que no toma parametros. Luego declara tres variables de tipo int a,b y c, y almacena en las dos primeras los valores 1 y 2 respectivamente. c=multiplica(a,b); Esto llama a la funcion multiplica(), pasandole como parametros los valores almacenados en a y b. El valor de retorno de dicha funcion lo almacena en la variable c. Puesto que la funcion multiplica() lo que devuelve es el resultado de multiplicar los parametros de entrada, c almacena ahora el resultado de multiplicar a por b. printf("%d x %d = %d\n",a,b,c); Esta es quiza la parte mas complicada. Lo unico que hace es invocar a la funcion printf, pasandole como parametros la constante de cadena "%d x %d = %d\n", el valor almacenado en a, el valor almacenado en b y el valor almacenado en c. printf() pertenece a un tipo especial de funciones que pueden tomar una lista variable de parametros. En el caso de printf(), el primer parametro debe ser una "cadena de formato" (en realidad, lo que le pasamos es un PUNTERO al primer caracter de la cadena) que indica que es lo que hay que mostrar por pantalla. Cada vez que aparezca el patron %d le estaremos indicando a printf que debe escribir un numero en decimal (base 10). En nuestro caso, le decimos que escriba un numero en base 10, el caracter ' ' (espacio), el caracter 'x', el caracter ' ', otro numero en base 10, el caracter ' ', el caracter '=', el caracter ' ' y otro numero en base 10. Y ¿que numeros seran los que escriba?. Pues el primero de ellos sera el siguiente parametro (a), el segundo el siguiente (b) y el tercero el ultimo (c). Entonces la salida de printf sera "1 x 2 = 2". Es decir, a x b = c. return 0; Finalmente, hacemos main() termine devolviendo un valor 0. - Funciones anidadas. Bien, espero que la cosa este algo mas clara, porque ahora vamos dar otra vuelta de tuerca. Vamos a escribir el programa anterior, pero eliminando la variable c. Si nos fijamos, la variable c solo nos sirve para almacenar el valor multiplica(a,b). Y ese valor solo lo utilizamos una vez, para pasarlo a printf() y que lo saque por pantalla. Asi que lo que vamos a hacer es poner la llamada a multiplica como el ultimo parametro de printf(), en la posicion anterior de c: #include int multiplica (int n1, int n2){ return n1*n2; } int main(void){ int a,b; a=1; b=2; printf("%d x %d = %d\n",a,b,multiplica(a,b);); return 0; } En este caso, el compilador ve que el ultimo parametro de printf() es precisamente el valor de retorno de multiplica(). Por ello el programa primero ejecuta la funcion multiplica() pasandole como parametros los valores almacenados en a y b, y luego llama a printf() pasandole como parametros la cadena de formato, a, b, y el valor devuelto por multiplica(). Las llamadas a funciones pueden estar anidadas, como estamos viendo. De hecho, es un caso muy comun y utilizado, no es nada oscuro del lenguaje. - Parametros por valor y por referencia. NOTA: Para comprender esta seccion es necesario saber sobre punteros en C. Asi que si aun no has leido la seccion de punteros, simplemente pasa esta y vuelve cuando sepas utilizar punteros. En programacion, los parametros pueden pasarse por valor o por referencia. Si se pasan por valor, lo que se esta pasando en realidad es una COPIA del parametro. La funcion puede modificar el parametro a su antojo, pero solo estara modificando su copia interna. Durante toda la ejecucion de la funcion y una vez esta termine, el valor original que se copio sigue valiendo lo mismo y no se ha visto alterado. Cuando se pasa un parametro por referencia, no se pasa el parametro en si, sino la direccion de memoria donde esta almacenado ese valor. De forma que ahora solo hay una copia de ese valor que se comparte entre la funcion padre y la funcion hija que ha sido llamada. Y si la funcion hija modifica ese parametro, lo modifica tambien para la funcion padre. Todos los parametros en C se pasan por valor. C no permite el paso de parametros por referencia, como ocurre en Pascal. Para conseguir un paso por referencia, lo que se hace en realidad es pasar por valor un puntero al parametro. De esta forma, la funcion hija tiene una copia de la direccion de memoria donde se almacena el valor con el que tiene que trabajar. Hay una diferencia sutil con el paso de parametros por referencia de Pascal: aqui el programador *sabe* en cada momento que esta trabajando con un puntero al valor deseado, mientras que en Pascal la sintaxis es la misma salvo en la declaracion del tipo del parametro. Por ejemplo podemos hacer en C: void cambia(int *pa){ *pa = 1; } int main(void){ int a = 0; cambia(&a); printf("a vale: %d\n",a); return 0; } Al ejecutar el programa anterior, la salida sera: khorrorsive$ ./a.out a vale 1 khorrorsive$ Pero si en vez de utilizar el puntero hacemos: void cambia(int pa){ pa = 1; } int main(void){ int a = 0; cambia(a); printf("a vale: %d\n",a); return 0; } La salida sera: khorrorsive$ ./a.out a vale 0 khorrorsive$ Vemos que en este caso la variable a no ha cambiado, aunque dentro de la funcion cambia() hayamos almacenado un valor 1 en el parametro. - Un ejemplo de parametros "por referencia". Bien, vamos a aprovechar esto que ya sabemos para escribir un programa que multiplique dos numeros algo mas elaborado que el anterior. Esta vez no voy a escribir primero el programa y luego lo explico, sino que lo vamos a hacer al reves: primero lo pensamos, y luego lo escribimos. Esta vez vamos a hacer un programa que tome los numeros a multiplicar desde el teclado, de forma que no haya que retocar el programa y recompilarlo cada vez que queremos multiplicar numeros distintos. Y para ello vamos a utilizar la funcion scanf(). Al igual que printf(), scanf() es una funcion de la biblioteca de entrada salida estandar, por lo que debemos dar a conocer su prototipo al compilador mediante "#include ". De forma similar a lo que pasaba con printf(), scanf() recibe un numero variable de parametros, el primero de los cuales es un puntero a una cadena de caracteres de formato. En esta cadena de formato le indicaremos a scanf() que es lo que queremos que reciba por teclado. En nuestro caso, queremos que scanf reciba dos numero enteros, por lo que nuestra cadena de formato sera "%d %d" (traduciendo, dos numeros enteros en forma decimal, base 10 para los amigos). Los siguientes parametros deben ser las variables donde queremos almacenar estos valores... ¡Un momento! ¿Las variables? ¡Noooo! Si pasaramos las variables, en realidad estariamos pasando a scanf() UNA COPIA de sus valores. Y lo que queremos es que scanf() *modifique* estos valores. Es decir, queremos pasar las variables "por referencia", de forma que scanf() almacene en ellas los numeros enteros que el usuario introduzca por teclado. Por lo tanto, lo que debemos pasarle a scanf() son PUNTEROS a las variables. Para ello usaremos el operador & (direccion de). Una vez scanf() haya recibido y procesado los valores, las variables tendran almacenados los mismos. Solo nos queda llamar a printf() para escribir el resultado, al igual que pasaba en el ejemplo anterior. De nuevo, vamos a decirle a printf() que escriba "Entero1 x Entero2 = Resultado", es decir, nuestra cadena de formato para printf() sera "%d x %d = %d\n" (no olvideis incluir el caracter de nueva linea al final, para que quede mas bonito), y el resto de argumentos debe ser las dos variables y la multiplicacion de ambas, respectivamente. ¿Por que no hay que pasarle punteros a las variables a printf()? Porque printf() *no va a modificarlas*, solo va a escribir su valor por pantalla. Entonces, nuestro programa seria: #include int main(void){ int n1, n2; scanf("%d %d", &n1, &n2); /* Recibe los numeros */ printf("%d x %d = %d\n",n1,n2,n1*n2); /* Imprime la operacion */ return 0; /* Indicamos exito en la ejecucion del programa */ } Si compilais y ejecutais dicho codigo, vereis que el programa se queda a la espera de que introduzcais dos numeros. Si lo haceis, imprimira el resultado. Tened en cuenta que si en vez de dos numeros introducis una cadena de caracteres o cualquier otra cosa, el programa no funcionara. Esto es asi porque no he incluido control de errores en la entrada, para no complicar la cosa. Podeis probar por curiosidad a introducir cualquier chorrada no numerica a ver que sale. - Argumentos en linea de comandos. Otro caso util que podemos ver para aprender a utilizar funciones, argumentos y punteros es el del tipico programa que recibe argumentos en linea de comandos. Aunque los sistemas con interfaz grafica que Windows ha puesto de moda han conseguido que haya usuarios que nunca hayan escrito un comando en el interprete de comandos, vamos a arriesgarnos a pulsar el boton de "Instalacion Personalizada (solo para usuarios avanzados)" y a aprender algo util. Los programas pueden recibir argumentos en linea de comandos. Por ejemplo, el caso mas sencillo es quizas el comando "cd" que tanto en Unix como en M$-DoS/Windows sirve para cambiar el directorio (¿o ahora se llaman carpetas? :P) activo. El comando "cd" podria no ser mas que un programa al que se le pasase, en linea de comandos, el nombre del directorio al que queremos cambiar. Muy requetebien. A ver como cohones se programaria eso. Sencillo, hombre, no empieces a sudar. Para ello, vamos a ampliar un poco la definicion de nuestra funcion main(). Ahora, la funcion main() va a tomar dos parametros: un int -que para seguir una tradicion ancestral llamaremos argc- y un char ** -llamado argv-. Entonces... P: Espera, espera, que ahi habia demasiados *. ¿Que leche de tipo tiene argv? R: argv no es mas que un puntero a puntero a caracter. P: ¿Como se come eso? Meloxpliquen. R: Facil hombre, miralo mejor como si fuera char* argv[]. P: Ahhhh, eso es mas facil ¿no? Una tabla de punteros a caracter, es decir, una tabla de cadenas de caracteres ¿Verdad? R: Ahi l'as dao. P: Vale, ¿y para que sirve cada uno de los parametros? R: argc es facil, hombre. argc indica cuantos parametros (incluido el nombre del programa) ha escrito el usuario. P: Entonces si yo llamo a mi programa con la orden "./a.out", argc valdra 0, ¿no? R: ¿No te estoy diciendo que "incluido el nombre de programa"? Si escribes la orden "./a.out", argc valdra 1. P: Vale, vale, no te mosquees. ¿Y argv? R: argv es una tabla, de argc elementos, cada uno de los cuales es una cadena de caracteres. Entonces la primera cadena es el nombre del programa, la segunda el primer argumento, etc. P: ¡Ah, estupendo! Entonces, ¿el nombre del programa es argv[1]? R: No, ceporro. Las tablas en C comienzan por el indice 0. argv[0] es el nombre del programa, y argv[1] es el primer argumento (si existe). P: Vale, pero todos mis programas se llaman a.out (trabajo en Unix). ¿Para que voy a usar entonces argv[0]? R: Si no quieres que tu programa se llame a.out, utiliza la opcion -o nombre_del_programa con el compilador de C. Por ejemplo: gcc programa.c -o miprograma P: Y entonces, ¿argv cuantas filas tiene? R: Pues depende de cada llamada. Precisamente argv no se sabe cuantas filas tiene hasta que el programa no se esta ejecutando. Es un claro ejemplo datos cuyo tamaño no es conocido en tiempo de compilacion. Por eso se utiliza argc, para que el programador pueda saber cuantos argumentos se han pasado en linea de comandos en cada ejecucion. P: Entiendo. Bueno, la ultima pregunta. ¿Por que los llamas argc y argv? R: Utilizo esos nombres porque son los que usa todo el mundo, aunque puedes ponerle los nombres que quieras. argc significa ARGument Count, y argv significa ARGument Vector, nombres que vienen a describir bastante bien el contenido y uso de dichas variables. Bien, despues de este dialogo de besugos, ya sabemos como hay que utilizar argc y argv. Vamos a empezar por un ejemplo sencillo: hacer un programa que no acepte ningun parametro. Lo voy a hacer con el operador ternario para que penseis un poco y os acostumbreis a ello: #include int main(int argc, char *argv[]){ printf(argc==1?"OK\n":"¡Error! No admito argumentos en linea\n"); return argc==1?0:-1; } Vale. Ya esta. Ahora vamos a hacer un programa que escriba la linea de comandos al completo, comenzando por el nombre del programa y siguiendo con todos los argumentos en linea que le hayamos pasado. Para ello, contare algo mas sobre la cadena de formato de printf(). Si queremos que printf() escriba una cadena de caracteres que no sea constante, podemos utilizar la secuencia %s. Es decir, la proposicion printf("%s\n",cadena); escribira en pantalla el contenido de la cadena de caracteres "cadena" hasta que encuentre el terminador, y luego el caracter '\n' (nueva linea). Entonces nuestro programa debe ser: #include int main(int argc, char *argv[]){ int i=0; while(argc--){ printf("%s ",argv[i++]); } printf("\n"); return 0; } Puede que todo esto no te parezca muy util. ¿Para que sirve un programa que escribe los argumentos que recibe? Bueno, para nada; es solo un ejemplo. Pero ya sabes como consigue el compilador gcc averiguar que has utilizado la opcion -o nombre_del_programa para que al compilar tu programa no se llame a.out. Y, lo mas importante, ya puedes ir corriendo a contarselo a tu padre/amigo/hermano/novia para que te mire con cara de "me la suda, colgao" y te diga: "Me da igual, me voy a ver O.T./G.H./Aqui hay tomate/Er furbol" :-/ *EOF*