Copy Link
Add to Bookmark
Report
SET 029 0x06
-[ 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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>
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 <stdio.h>". 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 <stdio.h>
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<stdio.h>
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 <stdio.h>
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*