Sei sulla pagina 1di 139

Apuntes de un curso de

PROGRAMACION Y METODOS NUMERICOS


AVANZADO

Departamento de Fsica
Facultad de Ciencias
Universidad de Chile

Vctor Munoz G.
Jose Rogan C.
Indice

1. Una breve introduccion a C++ 1


1.1. Estructura basica de un programa en C++ . . . . . . . . . . . . . . . . . . . . 1
1.1.1. El programa mas simple . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.1.2. Haciendo el codigo mas legible . . . . . . . . . . . . . . . . . . . . . . . 2
1.1.3. Nombres de variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.1.4. Tipos de variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.1.5. Ingreso de datos desde el teclado . . . . . . . . . . . . . . . . . . . . . 6
1.1.6. Operadores aritmeticos . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.1.7. Operadores relacionales . . . . . . . . . . . . . . . . . . . . . . . . . . . 8
1.1.8. Operadores de asignacion . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.1.9. Conversion de tipos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10
1.2. Control de flujo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.2.1. if, if... else, if... else if . . . . . . . . . . . . . . . . . . . . . 11
1.2.2. Expresion condicional . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.2.3. switch . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.2.4. while/do... while . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13
1.2.5. for . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.2.6. goto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.3. Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.3.1. Definicion de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . 16
1.3.2. Funciones tipo void . . . . . . . . . . . . . . . . . . . . . . . . . . . . 18
1.3.3. Funciones con parametros . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.3.4. Sobrecarga . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19
1.3.5. Parametros por valor y por referencia . . . . . . . . . . . . . . . . . . . 21
1.3.6. Variables tipo const . . . . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.3.7. Parametros default . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.3.8. Alcance, visibilidad, tiempo de vida . . . . . . . . . . . . . . . . . . . . 26
1.3.9. Variables estaticas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.3.10. Recursion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
1.4. Matrices . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.4.1. Declaracion e inicializacion . . . . . . . . . . . . . . . . . . . . . . . . . 28
1.4.2. Matrices como parametros de funciones . . . . . . . . . . . . . . . . . . 29
1.4.3. Matrices multidimensionales . . . . . . . . . . . . . . . . . . . . . . . . 29
1.4.4. Matrices de caracteres: cadenas (strings) . . . . . . . . . . . . . . . . . 29
1.5. Punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31

iii
iv INDICE

1.6. Argumentos de main . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32


1.7. Manejo de archivos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33
1.8. Funciones matematicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 36
1.9. Clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 37
1.9.1. Definicion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38
1.9.2. Variables miembros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 39
1.9.3. Funciones miembro; miembros publicos y privados . . . . . . . . . . . . 40
1.9.4. Creacion de header files . . . . . . . . . . . . . . . . . . . . . . . . . . 44
1.9.5. Constructor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46
1.9.6. Destructor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
1.9.7. Matrices de clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
1.9.8. Constructor de copia . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
1.9.9. Sobrecarga de funciones . . . . . . . . . . . . . . . . . . . . . . . . . . 52
1.9.10. Sobrecarga de operadores . . . . . . . . . . . . . . . . . . . . . . . . . 53
1.9.11. Coercion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 54
1.9.12. Interaccion con la pantalla/teclado o archivos . . . . . . . . . . . . . . 56
1.10. Algunas clases utiles . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
1.10.1. string . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
1.10.2. complex . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
1.11. Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
1.11.1. Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
1.11.2. Clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62
1.11.3. Standard Template Library (STL) . . . . . . . . . . . . . . . . . . . . . 65
1.12. Herencia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70
1.12.1. public, private, protected . . . . . . . . . . . . . . . . . . . . . . . 74
1.12.2. Herencia multiple . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76
1.13. Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 77
1.14. make . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
1.14.1. Un ejemplo sencillo en C++ . . . . . . . . . . . . . . . . . . . . . . . . 79
1.14.2. Creando un Makefile . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80
1.14.3. Un ejemplo con varias dependencias: LATEX . . . . . . . . . . . . . . . . 82
1.14.4. Variables del usuario y patrones . . . . . . . . . . . . . . . . . . . . . . 84
1.14.5. C++: Programando con mas de un archivo fuente . . . . . . . . . . . . 85
1.14.6. Un ejemplo completo . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88

2. Metodos Numericos 91
2.1. Precision . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91
2.1.1. Errores de escala. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
2.1.2. Errores de redondeo. . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
2.2. Interpolacion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
2.3. Ecuaciones diferenciales ordinarias . . . . . . . . . . . . . . . . . . . . . . . . . 99
2.4. Numeros aleatorios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
2.5. Sistemas de ecuaciones lineales . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
2.6. Sistemas de ecuaciones no lineales . . . . . . . . . . . . . . . . . . . . . . . . . 107
2.7. Regresion lineal . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
INDICE v

2.8. Integracion (cuadratura) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109


2.9. Sort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113

3. Metodos Numericos Avanzados 119


3.1. Ecuaciones con derivadas parciales . . . . . . . . . . . . . . . . . . . . . . . . 119
3.1.1. Ecuacion de Laplace . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119
3.1.2. Ecuacion de difusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
3.1.3. Ecuacion de conveccion/adveccion . . . . . . . . . . . . . . . . . . . . . 126
3.1.4. Ecuacion de ondas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
3.2. Redes neuronales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126
3.3. Automatas celulares . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128
3.4. Algoritmos geneticos y Montecarlo . . . . . . . . . . . . . . . . . . . . . . . . 134
Captulo 1

Una breve introduccion a C++

version 8 enero 2014

El objetivo de este curso es entregar algunos elementos basicos de programacion cientfica.


Para ello hemos escogido el lenguaje de programacion C++ , un lenguaje compilado, moderno y
multiproposito. Los lenguajes interpretados (perl, python, ruby, etc.) pueden ser muy utiles
para realizar tareas relacionadas con investigacion cientfica, en particular por su facilidad de
uso y la rapidez con la que se puede escribir el codigo y obtener resultados.
Sin embargo, cuando se trata de resolver un problema de mayor escala, un lenguaje
compilado es lo adecuado. A pesar de que puede ser mas lento desarrollar el codigo, este, una
vez compilado, es ejecutado mucho mas rapidamente.
En esta seccion entregaremos una descripcion breve de los principales elementos del len-
guaje de programacion C++ , que posteriormente utilizaremos para resolver problemas de
interes en Fsica.

1.1. Estructura basica de un programa en C++


1.1.1. El programa mas simple
El primer ejemplo de todo manual es el que permite escribir Hola en la pantalla del
computador. Utilizando cualquier editor de texto, creemos un archivo hola.cc, con el si-
guiente contenido:

#include <iostream>

using namespace std;


int main(){
cout << "Hola." << endl;
return 0;
}

La primera lnea define las instrucciones adecuadas para enviar luego un mensaje a pan-
talla, que estan contenidas en el header iostream. La funcion main es donde comienza a
ejecutarse el programa. Puede haber codigo antes o despues de la aparicion de main, pero

1
2 CAPITULO 1. UNA BREVE INTRODUCCION A C++

siempre debe haber una (y solo una) funcion main en nuestro programa. Luego, cout enva
el mensaje a pantalla, y endl agrega un cambio de lnea al final del mensaje. Por ultimo, se
devuelve un valor entero al sistema operativo para indicarle que la ejecucion del programa
termino correctamente (mas detalles sobre esto, mas adelante).
Como crear este archivo y como compilarlo para obtener un ejecutable, dependen del
sistema operativo en el que trabajemos. En un sistema Linux, lo usual es que los archivos
que contienen el codigo fuente tengan extension cc, y se compilan con la instruccion:

g++ hola.cc

Esto da como resultado un archivo a.out, que puede ser ejecutado con la instruccion:

./a.out

En el caso del codigo anterior, el resultado sera que, en pantalla, aparezca la palabra Hola.
Normalmente se desea que el ejecutable tenga un nombre mas significativo que el generico
a.out. Para indicar el nombre del archivo de salida se usa el flag -o, de modo que

g++ -o hola hola.cc

da como resultado un archivo ejecutable hola en el directorio actual.

1.1.2. Haciendo el codigo mas legible


El codigo anterior es solo un ejemplo de como podra escribirse el archivo hola.cc. C++
otorga amplia libertad al programador para escribir el codigo fuente. En efecto, los cambios
de lnea son equivalentes a espacios en blanco y caracteres de tabulacion. Por lo tanto,

return 0;

es equivalente a

return 0;

ya

return
0
;

El fin de una instruccion esta determinado por el cierre de un parentesis o por un punto
y coma, no por el fin de una lnea.
Tampoco es necesario que los parentesis cursivos vayan en la posicion del ejemplo:

int main(){ cout << "Hola." << endl;


return 0;}
1.1. ESTRUCTURA BASICA DE UN PROGRAMA EN C++ 3

habra funcionado igual de bien.


Toda esta libertad, por supuesto, tiene su precio, ya que un codigo fuente formateado
con demasiada libertad puede terminar siendo inentendible para el propio autor, haciendo el
codigo difcilmente reutilizable y por lo tanto, en la practica, inutil.
Nuestra recomendacion es que el programador encuentre el modo de escribir su codigo
que sea mas logico y legible para s mismo, y que mantenga esa estrategia consistentemente
en todos sus archivos.
Otro modo de hacer el codigo mas legible es introduciendo comentarios. Hay dos modos
de hacerlo en C++ . Para comentarios de una lnea, //, y para comentarios de mas de una
lnea, el par /*-*/. Usando el ejemplo anterior:

/* En este codigo, enviamos la palabra Hola a pantalla.


Es un codigo muy breve, en realidad */
#include <iostream>

using namespace std;

// El programa esta a punto de comenzar

int main(){
cout << "Hola." << endl; // Aqui enviamos el mensaje
return 0; /* Esta es la ultima linea que
se ejecutara en nuestro codigo */
}

Todo lo que esta despues de un //, hasta el final de la lnea respectiva, y todo lo que
esta entre un /* y un */ es ignorado por el compilador, de modo que este ultimo codigo es
completamente equivalente al primero.
Sin embargo, la legibilidad de un codigo puede aumentar considerablemente con el uso
adecuado de comentarios, por lo tanto es otra practica que todo buen programador debera
tener.
En el ejemplo anterior tambien podemos notar que no hay ninguna necesidad de que
los comentarios esten en una lnea separada, lo cual permite al programador insertar los
comentarios del modo y en el punto del codigo que le parezca mas conveniente.

1.1.3. Nombres de variables


Como en todo lenguaje de programacion, en un codigo C++ se pueden definir variables, que
luego pueden alojar valores determinados, que pueden o no ser modificados en el transcurso
del programa. Estas variables pueden tener en principio cualquier nombre, pero naturalmente
es recomendable que tengan nombres intuitivos, ya que a medida que hagamos codigos mas
largos sera cada vez mas importante no perder tiempo en recordar que significa cada variable.
Las unicas reglas para los nombres de variables son los siguientes:

Deben comenzar con una letra.


4 CAPITULO 1. UNA BREVE INTRODUCCION A C++

Mayusculas y minusculas son distintas.

Pueden contener numeros.

Pueden contener el smbolo _ (underscore).

Pueden tener longitud arbitraria.

No pueden corresponder a una de las palabras reservadas de C++ (en el ejemplo sencillo
de la Sec. 1.1.1, encontramos, por ejemplo, include, int, main, return).

1.1.4. Tipos de variables


Las variables, en todo codigo, en cualquier lenguaje de programacion, pueden ser de
distintos tipos, segun las necesidades del programa en cuestion. Por ejemplo, puede haber
variables que alojen numeros, texto, direcciones de memoria, etc. Los numeros, a su vez,
podran ser por ejemplo enteros, reales o complejos. Distintos lenguajes de programacion
tienen distintas reglas para manejar variables.
En particular, C++ es un lenguaje que, tecnicamente, se denomina en ingles strongly typed .
Esto significa que cada variable tiene un tipo bien definido, el cual no puede ser cambiado
en ningun punto del programa. En particular, si una variable es de un tipo determinado no
se puede intentar asignarle un valor que pertenece a otro tipo (por ejemplo, intentar asignar
un numero real a una variable entera).1
Para conseguir lo anterior, todas las variables a usar deben ser declaradas de acuerdo a su
tipo. Una vez declaradas pueden ser inicializadas Por ejemplo, si usamos una variable i que
es un numero entero, debemos, antes de usarla, declararla, y solo entonces podemos asignarle
un valor:

int i;
i=10;

No es posible, como dijimos, cambiar el tipo de una variable, de modo que si luego
queremos que i sea un numero real con doble precision (double), el codigo

int i;
i=10;
double i;

es incorrecto, y el compilador reclamara que la variable i ya fue definida. De hecho, es-


ta caracterstica es tan estricta que ni siquiera intentar declararla nuevamente como int
esta permitido.
Lo que s esta permitido es reunir las acciones de declaracion e inicializacion en una misma
lnea:
1
Existen diversas razones por las cuales uno s querra asignar, por ejemplo, un numero real a un entero o
viceversa. C++ proporciona una protocolo especfico para conseguirlo, pero lo importante es tener muy claro
que C++ espera que toda variable tenga un tipo conocido, y que solo se permiten aquellos cambios que esten
explcitamente permitidos.
1.1. ESTRUCTURA BASICA DE UN PROGRAMA EN C++ 5

int i = 10;

Los tipos de variables disponibles son2 :


int Enteros entre 215 = 32768 y 215 1 = 32767
o entre 231 = 2147483648 y 231 1 = 2147483647
short int
short Enteros entre 215 y 215 1.
long int
long Enteros entre 231 y 231 1.
unsigned int Enteros entre 0 y 216 1
o entre 0 y 232 1.
unsigned short Enteros entre 0 y 216 1.
unsigned long Enteros entre 0 y 232 1.
char Caracteres.
float Reales en los intervalos [1.7 1038 , 0.29 1038 ],
[0.29 1038 , 1.7 1038 ]
(Precision de unos 7 dgitos decimales.)
double Reales en los mismos intervalos que float,
pero con precision de 16 decimales,
o en los intervalos [0.9 10308 , 0.86 10308 ]
y [0.86 10308 , 0.9 10308 ], con precision de
15 decimales.
Al ver esta tabla, aunque sea referencial, podemos entender por que es conveniente que
cada variable tenga su tipo bien definido. En efecto, al crear una variable se reserva un cierto
espacio de memoria que alojara el eventual valor que se le asigne (el valor 10, en el ejemplo
de mas arriba). Pero distintos valores exigen distintos tamanos de memoria, y as por ejemplo
un entero requiere mucha menos memoria que un real de precision simple, y este menos que
un real de precision doble. Sin embargo, si necesitamos que una variable i recorra los enteros
de 1 a 10, por ejemplo, es un desperdicio de recursos reservar la misma memoria que si fueran
numeros reales. Por lo tanto, al declarar los tipos de cada variable, le decimos al programa
que reserve la cantidad de memoria estrictamente necesaria para nuestros propositos. De este
modo, los recursos de la maquina utilizada, tanto en espacio de memoria ocupada como en
tiempo de ejecucion pueden ser optimizados.
Un ejemplo interesante es la version unsigned de los tipos de variable. Hay muchas
ocasiones en que sabemos que una variable entera debe ser siempre positiva (el numero de
protones en un nucleo atomico, el numero de letras en una palabra, etc.). Al usar el tipo
unsigned int, decimos que no requerimos guardar informacion sobre el signo del numero
entero, pequena optimizacion que puede no ser importante para programas sencillos, pero
que puede hacer una gran diferencia en codigos mas complejos y que usan masivamente los
recursos de memoria de la maquina.
Todos los tipos de variables anteriores estan preparados para alojar numeros, con la
excepcion de char, que aloja caracteres. En su caso, la inicializacion es un poco diferente:
2
Los valores de los rangos indicados son simplemente representativos y dependen de la maquina utilizada.
6 CAPITULO 1. UNA BREVE INTRODUCCION A C++

char c = a;
Ademas de los caracteres usuales (mayusculas, minusculas, smbolos como &, *, :, etc.,)
hay algunos caracteres especiales (caracteres de escape) que es posible asignar a una variable
char. Ellos son:
newline \n
horizontal tab \t
vertical tab \v
backspace \b
carriage return \r
form feed \f
alert (bell) \a
backslash \\
single quote \
double quote \"
Por ejemplo, la lnea:

cout << "Primera columna\t Segunda columna\n


Segunda linea" << endl;

produce el output

Primera columna Segunda columna


Segunda linea

Notar que el caracter \n tiene el mismo efecto que endl: ambos producen un cambio de
lnea. Sin embargo, \n siempre significa un cambio de lnea si esta dentro de una variable
tipo char, en tanto que endl solo introduce un cambio de lnea si esta encadenado con un
cout previo.

1.1.5. Ingreso de datos desde el teclado


Para ingresar datos desde el teclado basta usar la instruccion cin:

#include <iostream>

using namespace std;

int main(){
int i;
cout << "Ingrese un numero entero: ";
cin >> i;
cout << "El numero ingresado fue: " << i << endl;

return 0;
}
1.1. ESTRUCTURA BASICA DE UN PROGRAMA EN C++ 7

Este codigo escribe en pantalla el mensaje "Ingrese un numero entero: ", y queda
esperando que el usuario ingrese algo desde el teclado. Si el usuario ingresa un numero entero
y luego Enter, entonces dicho numero quedara asignado a la variable i.
Si se desea que el usuario ingrese mas de un numero desde el teclado, basta encadenar
varios >>. El usuario, por su parte, debe ingresarlos separados por espacios o por cambios de
lnea:

#include <iostream>

using namespace std;

int main(){
int i,j;
cout << "Ingrese dos numeros enteros: ";
cin >> i >> j;
cout << "Los numeros ingresados fueron: " << i << " y " << j << endl;

return 0;
}

Como dijimos, separar los numeros ingresados por espacios:

5 10

o por cambios de lnea:

5
10

es equivalente para cin. Lo que debemos imaginarnos es, primero, que espacios, tabulaciones
y cambios de lnea son iguales para efectos de cin, y que uno o mas espacios en blanco son
equivalentes a uno solo. Y luego, que el ingreso de datos desde el teclado es como un flujo de
elementos, separados entre s por espacios en blanco.
Lo que hace cin, entonces, es atender a este flujo de informacion que ingresa desde el
teclado, y cada vez que recibe un blanco (espacio/tabulacion/cambio de lnea) coloca el
elemento recibido en la variable correspondiente (en el ejemplo anterior, el primer elemento
en i, el segundo en j), hasta que se le acaban las variables.
Por lo tanto, si uno ingresa menos palabras que las que el codigo espera, el programa
se mantendra esperando desde el teclado hasta tener el input suficiente. Por el contrario, si
uno ingresa mas palabras que las que el codigo espera, el programa solo considerara las
que necesita, y el resto se perdera. As, si en el codigo anterior ingresamos tres numeros:

5 10 15

el output sera el mismo que si el 15 se omite:

Los numeros ingresados fueron: 5 y 10


8 CAPITULO 1. UNA BREVE INTRODUCCION A C++

1.1.6. Operadores aritmeticos


Existen operadores para la suma, la resta, la multiplicacion, la division:
+ - * /
Otro operador util es %, que entrega el resto de una division por un entero:
int r = 10%3
deja en r el valor 1, pues ese es el resto de la division de 10 por 3.
Observar que este operador nos permite determinar rapidamente si un numero es divisible
por otro (por ejemplo, si un numero es par, o una potencia de 10).
Varios operadores pueden aparecer en una misma lnea de codigo, posiblemente con
parentesis:
int a=2+5*20-6+8/2*3+(2-5)*2;
Para poder calcular esto, el codigo se ejecutara de acuerdo a las siguientes reglas de prece-
dencia:
1. Primero, se calculan las operaciones dentro de cada uno de los parentesis.
2. Luego, las multiplicaciones y divisiones.
3. Luego, las sumas y restas
4. Si hay varias operaciones con igual prioridad (varias multiplicaciones y divisiones, por
ejemplo), se calcularan desde izquierda a derecha.
Con estas reglas, el valor de a en el ejemplo anterior es 102.
Todos los operadores, los operadores aritmeticos que acabamos de ver y otros que revi-
saremos mas adelante tienen una precedencia especfica, de modo que el programa siempre
sabe en que orden debe ejecutar las operaciones para cualquier lnea de codigo. Por ejemplo,
el operador % tiene la misma precedencia que * y /.

1.1.7. Operadores relacionales


Los smbolos para las relaciones de igualdad, desigualdad, menor, menor o igual, mayor
y mayor o igual son:
== != < <= > >=
Para las relaciones logicas AND, OR y NOT:
&& || !
Estos operadores entregan un resultado 1 si la expresion es verdadera, 0 si es falsa. Por
ejemplo:
int i=3, j=2, k=1;
int v = (i==(j+k));
int f = (i==j);
Las lneas anteriores son equivalentes a v=1 y f=0.
1.1. ESTRUCTURA BASICA DE UN PROGRAMA EN C++ 9

1.1.8. Operadores de asignacion


El signo = ya lo hemos usado abundantemente y sabemos lo que la siguiente instruccion
hace:
int i = 1;
int j = i;
Pero analicemos esto con mas detalle.
Para hacerlo, notemos que cada variable en nuestro programa, representa una direccion
de memoria, en la cual se aloja un cierto valor. Ahora, entonces, entendemos que = es un
operador, que toma el valor de la variable que esta a su derecha (1 en ambas lneas del
ejemplo), y lo coloca en la direccion de memoria de la variable que esta a su izquierda. Esto
es lo que denominamos asignacion simple.
Notemos, en particular, que en la primera lnea se usa la direccion de memoria asociada
a i, y en la segunda se usa el valor contenido en dicha direccion de memoria.
Esto es interesante, porque es justamente lo que permite que una expresion como
x = x + 2
tenga sentido, aunque matematicamente no lo tenga. En efecto, a la derecha, x participa
como el valor contenido en ella, y a la izquierda participa como su direccion de memoria.
Expresiones como la anterior se utilizan habitualmente en programacion, y por ello en
C++ existe un operador especial para ella, +=. As, x=x+2 se puede reemplazar por
x += 2
Esto se denomina asignacion compuesta. Existen varios operadores analogos: Existen los
operadores
+= -= *= /=
Aun mas habitual que lo anterior es cuando se desea incrementar una variable en 1:
x = x + 1
Esto se puede reescribir con el operador:
x += 1
o bien con el operador de incremento:
x++
Analogamente, existe el operador --, equivalente a x -= 1 o a x = x - 1.
Los operadores de incremento o decremento se pueden utilizar como prefijos o sufijos.
Observemos el siguiente codigo:
int i=1;
int j=i++;
int k=++i;
10 CAPITULO 1. UNA BREVE INTRODUCCION A C++

Al cabo de estas tres lneas de codigo, i tiene el valor 3, claramente, porque fue incrementado
en 1 dos veces. Pero la segunda y tercera lnea son muy diferentes. En la segunda, el operador
++ aparece como sufijo, por lo tanto primero se hace la asignacion simple (j toma el valor
actual de i), y a continuacion i es incrementado en 1. En la tercera lnea, en cambio, ++ es
prefijo: primero se hace el incremento de i, y luego se hace la asignacion a k. Como resultado,
al final del codigo i vale 3, j vale 1 y k vale 3.

1.1.9. Conversion de tipos


C++ es estricto respecto a los tipos de las variables, y eso significa, en particular, que las
operaciones matematicas entre variables deben ser tambien entre variables de un mismo tipo:
se deben sumar int entre s, double entre s, etc. Sin embargo, es evidente que se necesita
una manera de obviar esta dificultad, porque matematicamente tiene sentido sumar enteros
con reales.
Observemos, por ejemplo, el siguiente codigo:

int i = 3;
float x = 43.8;
cout << "Suma = " << x + i << endl;

Aqu el computador debe sumar dos variables de tipos distintos. Como solo se pueden sumar
entre s variables del mismo tipo, para que el codigo tenga sentido debe primero deben
convertirse ambas a un tipo comun antes de efectuar la suma. Existen dos modos de proceder:

a) Conversion explcita.
Se puede convertir la variable i a una tipo float, con float(i):

int i = 3;
float x = 43.8;
cout << "Suma = " << x + float(i) << endl;

Naturalmente, tambien sera posible convertir x en una variable tipo int:

int i = 3;
float x = 43.8;
cout << "Suma = " << int(x) + i << endl;

Esto, sin embargo, hara perder precision, ya que int(x) va a tomar solo la parte entera
de x (43). Normalmente, no es lo que queremos, pero el lenguaje nos da la posibilidad
de hacerlo si lo deseamos.
La conversion explcita es una solucion que puede ser bastante tediosa, por cuanto
el programador debe realizar el trabajo de conversion personalmente cada vez que se
necesita.
1.2. CONTROL DE FLUJO 11

b) Conversion implcita.
El compilador realiza las conversiones de modo automatico, prefiriendo siempre la con-
version desde un tipo de variable de menor precision a uno de mayor precision (de int
a double, de short a int, etc.).
Es interesante notar como las conversiones implcitas de tipos pueden tener consecuen-
cias insospechadas.
Consideremos las tres expresiones:

i) x = (1/2) * (x + a/x) ;
ii) x = (0.5) * (x + a/x) ;
iii) x = (x + a/x)/2 ;

Si inicialmente x=0.5 y a=0.5, por ejemplo, i) entrega el valor x=0, mientras ii) y iii)
entregan el valor x=0.75. Lo que ocurre es que 1 y 2 son enteros, de modo que 1/2 = 0.
De acuerdo a lo que dijimos, uno esperara que en i), como conviven numeros reales
con enteros, los numeros enteros fueran convertidos a reales y, por tanto, la expresion
tuviera el resultado esperado, 0.75. El problema, sin embargo, es la prioridad de las
operaciones (ver Sec. 1.1.6).
En efecto, en i), la primera operacion es 1/2, una division entre enteros, i.e. cero. En
ii) no hay problema, porque todas son operaciones entre reales. Y en iii) la primera
operacion es el parentesis, que es una operacion entre reales. Al dividir por 2 este es
convertido a real antes de calcular el resultado.
i) aun podra utilizarse, cambiando el prefactor del parentesis a 1.0/2.0, una practica
que sera conveniente adoptar como standard cuando queremos utilizar enteros dentro
de expresiones reales, para evitar errores que pueden llegar a ser muy oscuros.

1.2. Control de flujo


En principio, un codigo se podra ejecutar desde la primera hasta la ultima lnea, sin
alteraciones. Sin embargo, tal codigo sera muy limitado, pues hara siempre lo mismo. Por
lo tanto, todo lenguaje de programacion debe tener un modo de alterar el flujo de ejecucion,
dependiendo de que ciertas condiciones sean verdaderas o falsas. Ello permite adaptar el
mismo codigo a diversas situaciones y hacerlo, por ende, mas util. Las siguientes estructuras
de control de flujo estan disponibles en C++ .

1.2.1. if, if... else, if... else if


a) Esta es la version mas sencilla: si una cierta expresion es verdadera, ejecuta una deter-
minada porcion de codigo. Si es falsa, la ejecucion prosigue a continuacion del parentesis
de cierre }.
12 CAPITULO 1. UNA BREVE INTRODUCCION A C++

if (a==b){
cout << "a es igual a b" << endl;
}

b) En este caso, si la expresion es falsa se ejecuta el codigo entre los parentesis asociados
a else.

if (c!=d){
cout << "c es distinto de d" << endl;
}
else{
cout << "c es igual a d" << endl;
}

c) En este tercer caso, si la primera expresion es falsa, se verifica si una segunda expresion
es verdadera o falsa. Puede haber una cantidad arbitraria de else if a continuacion,
para verificar sendas condiciones adicionales. El ultimo else, que es opcional, se ejecuta
solo si ninguna de las expresiones anteriores resulto verdadera.

if (e > f){
cout << "e es mayor que f" << endl;
}
else if (e== f){
cout << "e es igual a f" << endl;
}
else{
cout << "e es menor que f" << endl;
}

1.2.2. Expresion condicional


Estructuras del tipo:
if (a==b){
c = 1;
}
else{
c = 0;
}
son muy habituales. Por ello, C++ ofrece una expresion abreviada equivalente, la expresion
condicional . En efecto, la estructura anterior se puede reemplazar por
c = (a==b) ? 1 : 0;
(a==b) puede ser reemplazada por cualquier expresion que pueda ser verdadera o falsa.
1 y 0 pueden ser reemplazados por cualquier expresion que devuelva un numero del mismo
tipo que c. Podran ser porciones mas complicadas de codigo, o incluso funciones:
1.2. CONTROL DE FLUJO 13

c = verdadero_o_falso()?funcion_si_verdadero():funcion_si_falso();

La expresion condicional no solo permite abreviar codigo, sino que su ejecucion es tambien
mas eficiente.

1.2.3. switch
Esta expresion permite seleccionar una accion a realizar dependiendo del valor de una
variable entera. Por ejemplo, si dicha variable es i, la siguiente estructura escribe en pantalla
"Caso 1" si i=1, "Caso 2" si i=2, y "Otro caso" si i tiene cualquier otro valor:

switch (i){
case 1:
{
cout << "Caso 1." << endl;
}
break;
case 2:
{
cout << "Caso 2." << endl;
}
break;
default:
{
cout << "Otro caso." << endl;
}
break;
}

La instruccion break permite que la ejecucion del programa salte a la lnea siguiente
despues de la serie de instrucciones asociadas a switch. Por ejemplo, si no existieran los
break, e i=1, entonces veramos en pantalla las lneas Caso 1., Caso 2. y Otro caso. La
instruccion default es opcional. Podemos observar que el ultimo break es innecesario, pero
lo hamos puesto por completitud y consistencia con el resto de la estructura.
Notar que la expresion anterior es equivalente a una estructura if/else if. Pero eso se
debe a la presencia de los break. Si no estuvieran, como se indico en el parrafo anterior, la
ejecucion continuara durante todo el resto del codigo contenido dentro de switch, lo cual
puede ser precisamente lo que queremos, y que sera mas difcil de hacer con if/else if.

1.2.4. while/do... while


Otro tipo de situacion usual es tener que ejecutar una seccion del codigo mientras alguna
condicion se cumpla. Esto se hace con while. Por ejemplo:

int i=0;
while (i < 3){
14 CAPITULO 1. UNA BREVE INTRODUCCION A C++

cout << i << endl;


i++;
}
enva a pantalla los numeros 0, 1, 2 antes de detenerse.
A veces deseamos que el codigo a repetir se ejecute siempre al menos una vez, en cuyo
caso es mejor poner la verificacion de la condicion al final. Esto se hace con do... while:
int i=3;
do{
cout << i << endl;
i++;
} while (i < 3)
En este caso, se alcanza a enviar a pantalla el numero 3, porque primero se ejecuta el codigo y
luego se estudia la condicion. Si se hubiera puesto la condicion al principio, con while(i<3),
esta porcion de codigo no se habra ejecutado y no habra habido salida a pantalla.

1.2.5. for
La estructura mas general para repetir codigo en un programa C++ esta dada por for,
que necesita tres argumentos: una condicion de inicio, una condicion de continuacion y una
instruccion a ejecutar al final de cada iteracion, antes de comenzar la siguiente. Por ejemplo,
si queremos mostrar en pantalla todos los numeros enteros entre 1 y 9, podemos hacerlo as:
for (i = 1; i < 10; i++){
cout << "Valor del indice: " << i << endl;
}
Los argumentos de for pueden tener muchas formas, lo cual da a for una enorme flexibilidad.
Por ejemplo, notemos que en el codigo anterior la variable i debio haber sido necesariamente
declarada antes del for. Pero es posible, en el primer argumento, declarar las variables que
se usaran al interior del for, las cuales seran locales al mismo:
int i=-3;
for (int i=1; i < 10; i++){
cout << "Valor del indice: " << i << endl;
}
cout << "Valor de i: " << i << endl;
En este caso, la variable i que esta dentro de for se incrementa en 1 cada vez, y a la salida
del for se imprime el valor -3, que nunca fue alterado.
Pero tambien podra hacerse un for usando dos variables, las cuales pueden ser ambas
locales, la condicion de termino puede (o no) involucrar a ambas, y la instruccion a ejecutar
al final de cada iteracion puede (o no) involucrar a ambas tambien. Por ejemplo:
for (int i=0, k=20; (i<10) && (k<100); i++, k+=6){
cout << "i + k = " << i + k << endl;
}
1.2. CONTROL DE FLUJO 15

Se inicializan dos variables, i a cero, k a 20. En cada iteracion se imprime la suma de ellas,
y luego se incrementa i en 1 y k en 6. El proceso se repite mientras i sea menor que 10 y k
sea menor que 100.
Es claro que for puede reemplazar a while en general. En efecto, el codigo de ejemplo
que se mostro en la Sec. 1.2.4 es equivalente a
for (int i=0; i < 3; i++){
cout << "Valor del indice: " << i << endl;
}
for es tan general, que cualquiera de sus argumentos puede ser vaco. Por ejemplo, el
codigo anterior es equivalente a
int i=0;
for(; i < 3; i++){
cout << "Valor del indice: " << i << endl;
}
donde el primer argumento no se ha usado porque fue reemplazado por una instruccion
anterior.
Y tambien es equivalente a
for(int i=0; i < 3;){
cout << "Valor del indice: " << i << endl;
i++;
}
Aqu, la instruccion a ejecutar al final de cada iteracion ha sido puesta al final del ciclo for, lo
cual tiene el mismo efecto. Naturalmente, tambien podra haberse omitido simultaneamente
el primer argumento:
int i=0;
for(; i < 3;){
cout << "Valor del indice: " << i << endl;
i++;
}
Omitir el segundo argumento tambien es posible, pero en ese caso, al no haber ninguna
condicion para que el ciclo continue o se detenga, el codigo se ejecutara infinitas veces:
for(int i=0;;i++){
cout << "Valor del indice: " << i << endl;
}
Esto imprimira todos los numeros, sin detenerse nunca (a menos que reciba una senal del
sistema operativo), desde 0 en adelante.
Un uso mas util de for es introducir una pausa en un codigo, durante suficientes itera-
ciones para que el codigo no avance, y tengamos, por ejemplo, tiempo de leer los resultados
escritos en pantalla. En este caso, el cuerpo del for puede estar vaco, pues solo se desea
incrementar el valor del contador muchas veces:
16 CAPITULO 1. UNA BREVE INTRODUCCION A C++

for(int i=0;i<1000000;i++){
}

Un caso extremo de la ausencia de todo argumento en el for sera el siguiente ciclo infinito:

for(;;);

1.2.6. goto
Existe tambien en C++ una instruccion goto que permite saltar de un punto a otro
del programa (goto salto; permite saltar a la lnea que contiene la instruccion salto:).
Sin embargo, se considera una mala tecnica de programacion usar goto, y siempre se puede
disenar un programa evitandolo. Altamente no recomendable.

1.3. Funciones
Las funciones nos permiten programar partes del procedimiento por separado. De modo
analogo a las variables, las funciones deben ser primero declaradas y luego implementadas. La
declaracion define el nombre de la funcion (que tiene las mismas restricciones que el nombre
de las variables vistas en la Sec. 1.1.3), los argumentos de la funcion y el tipo de retorno (si
se espera que el resultado de la funcion sea un entero, real de precision simple o doble, etc.).
La implementacion es el codigo que debe ejecutarse cada vez que se llama la funcion.

1.3.1. Definicion de funciones


Consideremos un ejemplo muy sencillo, de una funcion que solo entrega el valor de una
cierta variable entera:

#include <iostream>

using namespace std;

int f(){
int i=2;
return i;
}

int main(){
int j = f();
cout << "El valor de la variable i es " << j << endl;
cout << "El valor de la variable i es " << f() << endl;
return 0;
}

f() es una funcion que no espera argumentos (notar que, aunque no tiene argumentos, los
parentesis son obligatorios) y entrega como resultado un numero entero. Este resultado se
1.3. FUNCIONES 17

entrega a traves de la instruccion return, que puede tener a continuacion una constante, una
variable, una operacion, o incluso otra funcion:

return 2;
return i;
return 5+j;
return otra_funcion();

Naturalmente, lo que sea que haya luego de return debe ser algo que tenga el mismo tipo
que la funcion.
Una vez que la funcion se termina, su valor de retorno queda disponible para ser usado
por el codigo que la llamo. En nuestro ejemplo, f() se usa de dos modos: o bien se asigna
su resultado a una variable entera, o bien se enva directamente a pantalla. En cualquiera de
los dos casos, se obtiene el mismo resultado: en pantalla aparece el mensaje:
"El valor de la variable i es 2".
En el ejemplo anterior, la funcion fue declarada e implementada simultaneamente. Tam-
bien es posible, al igual que con las variables, separar la declaracion y la implementacion.
As, el ejemplo anterior puede reescribirse:

#include <iostream>

using namespace std;

int f();

int main(){
int j = f();
cout << "El valor de la variable i es " << j << endl;
cout << "El valor de la variable i es " << f() << endl;
return 0;
}

int f(){
int i=2;
return i;
}

La implementacion no solo esta separada de la declaracion, sino que incluso esta des-
pues de main, que es donde se usa. Podramos haber colocado la implementacion entre la
declaracion de f() y el comienzo de main, pero esto nos permite observar lo siguiente:
Para que la compilacion sea exitosa, lo unico que necesita el compilador es que las funcio-
nes esten declaradas antes de usarlas o implementarlas. La razon es que el compilador solo
necesita verificar que una funcion con ese nombre (f() es este caso) existe, y que tiene los
argumentos y el tipo de retornos correctos. En nuestro ejemplo, la primera lnea en main
intenta asignar el resultado de f() a una variable int, por lo tanto el compilador necesita
verificar que existe una funcion f() que devuelve una variable int.
18 CAPITULO 1. UNA BREVE INTRODUCCION A C++

Luego, es al ejecutar el codigo que se necesita realmente la implementacion de f(), porque


es en ese momento que el programa necesita saber que es lo que hace f(). Por eso necesitamos
poner la declaracion de f() antes de main, y podemos poner su implementacion despues de
main.3
Con todo lo dicho, entendemos ahora claramente que main() es una funcion, como cual-
quier otra del lenguaje, salvo porque es una palabra reservada, y no puede haber otra funcion
main() en nuestro codigo. Pero aparte de eso, de todos los ejemplos anteriores se deduce que
main() es una funcion, que es llamada sin argumentos, y que debe devolver un entero, int.
Ese valor es devuelto al sistema operativo, que toma dicho valor para detectar si el codigo se
ha ejecutado correctamente o no, y por eso se requiere una instruccion del tipo return 0;
al final de main().
Por ultimo, hacemos notar que la variable i en el ejemplo anterior es local a la funcion.
Si hubiera otra variable i en main, no se interferiran.

1.3.2. Funciones tipo void


Un caso especial de funciones es aquel en que el programa que llama la funcion no espera
que esta le entregue ningun valor al terminar. Por ejemplo, digamos que en nuestro sencillo
programa que dice Hola en pantalla, Sec. 1.1.1, delegamos el envo de dicho mensaje a una
funcion. La funcion no necesita argumentos, pero tampoco tiene que devolver nada a main,
porque solo enva un mensaje a pantalla. Pero entonces esta funcion no tendra tipo de
retorno, lo cual impedira verificar que la funcion es usada correctamente cuando se llame
(ya que, como dijimos, esa verificacion implica la consistencia del nombre de la funcion, sus
argumentos y su tipo de retorno). Para solucionar esto, C++ tiene un tipo especial de retorno
para aquellas funciones que no devuelven nada: void.
En el ejemplo citado, podemos reescribir el codigo de la siguiente manera:

#include <iostream>

using namespace std;

void PrintHola();

int main(){
PrintHola();
return 0;
}

void PrintHola(){
cout << "Hola." << endl;
}

3
Mas adelante, en la Sec. 1.14.6, refinaremos estos conceptos.
1.3. FUNCIONES 19

1.3.3. Funciones con parametros


Las funciones de los ejemplos anteriores entregan siempre el mismo resultado, ya que
ejecutan el mismo codigo incondicionalmente, siempre de la misma manera y con los mismos
valores de las variables involucradas.
Por supuesto, tambien es posible definir funciones que acepten parametros de entrada.
Tambien ejecutaran el mismo codigo cada vez que sean llamadas, pero su resultado depen-
dera en general de los parametros que se le entreguen.
Por ejemplo, podemos definir una funcion para encontrar la pendiente de una recta que
pasa por los puntos (x1 , y1 ) y (x2 , y2). Sus argumentos deberan ser cuatro numeros reales, y
su resultado debe ser otro numero real:

#include <iostream>

using namespace std;

double pendiente(double,double,double,double);

int main(){
cout << "La pendiente de la recta que pasa por (3.0,-2.5) y
(11.1,2.25) es " << pendiente(3.0,-2.5,11.1,2.25) << endl;
return 0;
}

double pendiente(double x1,double x2,double y1,double y2){


return (x2-x1)/(y2-y1);
}

Observar como en la declaracion solo se indican cuantos parametros tiene la funcion, y


de que tipo debe ser cada uno. Ni siquiera se necesita el nombre de cada parametro. Eso se
requiere en la implementacion de la funcion.
Es importante notar que el nombre de los parametros es local a la definicion de la funcion,
lo cual permite no preocuparse de posibles conflictos entre el codigo que implementa una
funcion y el resto del codigo. Podra haber otra variable x1 en el codigo, sin problemas.

1.3.4. Sobrecarga
Como C++ es muy estricto respecto a los tipos de las variables, si deseamos definir la
pendiente para puntos cuyas coordenadas son enteras, entonces tendremos que definir una
nueva funcion que acepte numeros enteros:

double pendiente2(int,int,int,int);

double pendiente2(int x1,int x2,int y1,int y2){


return (x2-x1)/(y2-y1);
}
20 CAPITULO 1. UNA BREVE INTRODUCCION A C++

Notemos que el tipo de retorno debe seguir siendo double.


Naturalmente, esto no es muy conveniente, porque ademas deberamos definir una tercera
funcion en caso de que solo el segundo argumento sea entero, una cuarta en caso de que solo
el segundo y el tercero, y un aburrido etcetera.
Afortunadamente esto es innecesario, porque el compilador convertira automaticamente
entre int y double cuando lo necesite. Entonces, habiendo definido solo
double pendiente(double,double,double,double), si la llamamos con uno o mas de sus
argumentos tipo int, el compilador promovera dicho int a double, y luego llamara la funcion
pendiente correctamente.
Sin embargo, este comportamiento solo es posible si el compilador sabe convertir de un
tipo a otro. Que sucede si eso no es posible, ya sea porque no hay una regla escrita para
convertir de un tipo a otro, o porque no existe una unica manera de hacerlo, o porque no
existe una manera matematicamente correcta de hacerlo, etc.?
Por ejemplo, consideremos la funcion potencia, que eleva un numero a cierto exponente.
Podramos definir una funcion potencia, en la forma:

double potencia(double,int);

El primer argumento es la base de la potencia, el segundo (en nuestro ejemplo, un entero), el


exponente. Si se invoca a potencia(5.3,4), el codigo de implementacion debera entregar el
resultado de multiplicar 5.3 cuatro veces por s mismo. Pero que sucede si queremos definir
la potencia de una matriz cuadrada? Es una operacion perfectamente valida, pero no existe
una manera de convertir un entero en una matriz, y por lo tanto no deberamos esperar y
no sucede que el compilador tenga un modo de conversion automatico entre estos tipos de
variable.4
En este caso, definir una segunda funcion sera imprescindible. Tendremos entonces una
funcion para encontrar la potencia de un real:

potencia(5, 2)

y otra funcion para calcular la potencia de una matriz. Simbolicamente:


  
2 3
potencia2 ,2
4 2

Aunque esta es una solucion posible, no es nada de elegante, porque, matematicamente,


no hay ninguna diferencia profunda entre elevar un real o una matriz a una potencia, y de
hecho se usa la misma notacion para ambas:
 2
2 2 3
5 , .
4 2

No sera mas conveniente, entonces, usar el mismo nombre para ambas funciones? Bueno,
ello es en efecto posible, y este procedimiento se llama sobrecargar una funcion, y se logra
simplemente declarando ambas funciones con el mismo nombre. Uno podra tener, entonces,
4
Si uno tiene un real x, uno podra pensar en multiplicar x por la matriz identidad, pero no hay una
manera unica de hacerlo, porque la dimension de la matriz aun estara indeterminada.
1.3. FUNCIONES 21

varias versiones de la misma funcion, distinguidas entre s solo por su tipo de retorno, el
numero y/o el tipo de sus argumentos. Cada vez que la funcion sea llamada, el compilador
verificara si hay alguna version de la funcion que sea compatible con esa llamada, y esa sera la
version que se ejecutara. Por ejemplo, si deseamos tener una funcion que enve un mensaje
diferente dependiendo de si una variable es entera o no, podramos crear el siguiente codigo:

#include <iostream>

using namespace std;

void mensaje(int);
void mensaje(double);

int main(){
int i=3;
double r=6.3;
mensaje(i);
mensaje(r);
return 0;
}

void mensaje(int j){


cout << "La variable " << j << "es entera." << endl;
}

void mensaje(double j){


cout << "La variable " << j << "es real de doble precision." << endl;
}

Se dice que la funcion mensaje() es polimorfica, ya que adquiere distintas expresiones de-
pendiendo de como es llamada.
Es importante notar que la sobrecarga de funciones es solo posible justamente porque C++
es estricto en cuanto al control de tipos de variables. Solo as es posible resolver la ambiguedad
de tener funciones distintas que se llamen igual.

1.3.5. Parametros por valor y por referencia


Cada vez que se declara una variable, ello implica reservar un cierto espacio de memoria.
Luego, cuando se inicializa, se aloja un determinado valor en dicho espacio de memoria.
Cuando se llama a una funcion con argumentos, se crean copias de dichos argumentos, los
cuales son usados al interior de la funcion. Al terminar la funcion, dichos espacios de memoria
son liberados. Esquematicamente:
22 CAPITULO 1. UNA BREVE INTRODUCCION A C++

main f

int main(){ k
k = 1964; 1964

k k
f(k); 1964 1964

k
... 1964

A este procedimiento se le llama pasar los argumentos de la funcion por valor .


Como en cada llamada de la funcion se realiza una copia del contenido de la variable
original en otra direccion de memoria, los argumentos son locales a dicha funcion, y por eso
se puede usar el mismo nombre sin conflictos entre el cuerpo de f y de main. Por ejemplo,
si dentro de la funcion se incrementa el valor del parametro, nada sucede con la variable
original. En el ejemplo siguiente, la salida a pantalla sera 1964:

#include <iostream>

using namespace std;

void f(int);

int main(){
int k=1964;
f(k);
cout << k << endl;
return 0;
}

void f(int k){


k++;
}

Esquematicamente:
1.3. FUNCIONES 23

main f

int main(){ k
k = 1964; 1964

k k
f(k); 1964 1964

k
... 1964

}
k
void f(int k){ 1964
k
k++; 1965

Por todas estas razones, pasar argumentos por valor parece una estrategia muy razonable,
pero que pasa si queremos llamar una funcion con un argumento matricial? Una matriz
cuadrada de 106 106 ? En este caso, es evidente que es un modo muy ineficiente de proceder,
porque se va gastar una cantidad enorme de memoria para albergar la copia de la matriz, y
una gran cantidad de tiempo en llenar dicha copia con los contenidos originales. El problema
es claro cuando queremos hacer calculos de grandes proporciones. En este caso, lo adecuado
es usar otra estrategia: pasar los argumentos por referencia. Esto significa que no se hace una
copia de la variable, sino que se le entrega a la funcion la direccion de memoria de la variable
original. De este modo, la funcion no necesita duplicar la informacion, sino que sabe donde
buscarla, ahorrando todo ese tiempo de creacion de memoria y de copia de contenidos.
Para hacerlo, basta incluir en la declaracion de la funcion el smbolo &:

#include <iostream>

using namespace std;

void f(int &);

int main(){
int k=1964;
f(k);
cout << k << endl;
return 0;
}

void f(int & k){


k++;
}
24 CAPITULO 1. UNA BREVE INTRODUCCION A C++

En este caso no hay ninguna duplicacion de memoria, lo cual resulta muy conveniente para
ahorrar recursos computacionales, de memoria y tiempo de ejecucion.
Junto con las ventajas mencionadas, los parametros pasados por referencia pueden ser
modificados, precisamente porque la funcion tiene acceso a la direccion de memoria de la
variable original, y no a una copia:
main f

int main(){ k
k = 1964; 1964

k k
f(k); 1964 1964

k
... 1965

}
k
void f(int & k){ 1964
k
k++; 1965
k
}
1965

Un uso tpico del paso de parametros por referencia es intercambiar entre s el valor de
dos variables, digamos a1=1 y a2=3. Luego de ejecutar la funcion queremos que a1=3 y a1=1,
es decir, precisamente que el valor de las variables originales cambie. El uso de parametros
por referencia es la tecnica a usar en esta situacion.

1.3.6. Variables tipo const


Por otra parte, esta capacidad se puede convertir en un riesgo, ya que es en la declaracion
de la funcion que se determina si un argumento se pasa por referencia o no, no habiendo modo
de distinguirlo en la llamada. Por lo tanto, una variable podra ser modificada por error.
Para evitar este problema, cada vez que se desea definir una funcion con parametros pasados
por referencia, y asegurarse al mismo tiempo que dichos parametros no sean modificados al
interior de la funcion, se puede anteponer la palabra reservada const:
#include <iostream>

using namespace std;

void f(const int &);

int main(){
int k=1964;
f(k);
1.3. FUNCIONES 25

cout << k << endl;


return 0;
}

void f(const int & k){


...
}
En este caso, el codigo de implementacion solo puede tener instrucciones que no modifiquen
el parametro k. Una instruccion como k++ dara un error de compilacion.
const, de hecho, se puede usar antepuesta a la declaracion de cualquier variable, no solo los
argumentos de funciones. Al declarar una variable tipo const, el compilador se asegurara de
que nunca se intente cambiar su valor. Por ejemplo, si queremos definir el valor de en
nuestro codigo, para mayor comodidad, podemos declararla as:
const double pi = 3.14159

1.3.7. Parametros default


Hay ocasiones en que deseamos definir una funcion que tome uno o mas parametros por
defecto, es decir, que si no los especificamos,
asuman determinados valores predeterminados.
3
Por ejemplo, consideremos las races: 5, 5. Si no lo indicamos, se supone que es el numero
tal que elevado al cuadrado nos da el subradical. Si hicieramos una funcion en C++ , nos
gustara entonces llamarla como raiz(5), en cuyo caso se entiende que es la raz cuadrada,
o, digamos, como raiz(5,3), en cuyo caso el segundo argumento indica el grado de la raz.
En C++ esto se hace en la declaracion, del siguiente modo:
double raiz(double,int = 2);

int main()
{
double r1 = raiz(5);
double r2 = raiz(5,3);

...

return 0;
}

double raiz(double x,int n){


...
}
Observar que es en la declaracion solamente que se indica el valor default, no en la imple-
mentacion.
Las funciones pueden tener un numero arbitrario de parametros default. La unica restric-
cion es que si los hay, todos deben estar al final de la lista de parametros. Por ejemplo, si una
funcion tiene 5 parametros, tres de los cuales son opcionales, una declaracion posible sera:
26 CAPITULO 1. UNA BREVE INTRODUCCION A C++

double f(int,int,double = 0.0,double=1.0,int=-3);

De este modo, todas las siguientes formas de llamar a la funcion son aceptables:

double x = f(0,0); // Equivale a f(0,0,0.0,1.0,-3);


double x = f(0,0,9.0); // Equivale a f(0,0,9.0,1.0,-3);
double x = f(0,0,9.0,-6.0); // Equivale a f(0,0,9.0,-6.0,-3);
double x = f(0,0,9.0,-6.0,2);

Notar que no es posible llamar a la funcion especificando solo el tercer y quinto parametro.
Un intento como el siguiente:

double x = f(0,0,9.0,5);

no sera equivalente a f(0,0,9.0,1.0,5);, sino que el cuarto parametro sera promovido de


int a double, y resultara ser equivalente a f(0,0,9.0,5.0,-3). Los parametros opcionales
son asignados de izquierda a derecha en estricto orden de aparicion.

1.3.8. Alcance, visibilidad, tiempo de vida


Con el concepto de funcion hemos apreciado que es posible que coexistan variables con el
mismo nombre en puntos distintos del programa, y que signifiquen cosas distintas. Conviene
entonces tener en claro tres conceptos que estan ligados a esta propiedad:

Alcance La seccion del codigo durante la cual el nombre de una variable puede ser usado.
Comprende desde la declaracion de la variable hasta el final del cuerpo de la funcion
donde es declarada.
Si la variable es declarada dentro de una funcion es local . Si es definida fuera de todas
las funciones (incluso fuera de main), la variable es global.

Visibilidad Indica cuales de las variables actualmente al alcance pueden ser accesadas. En
nuestros ejemplos (subseccion 1.3.3), la variable i en main aun esta al alcance dentro
de la funcion funcion, pero no es visible, y por eso es posible reutilizar el nombre.

Tiempo de vida Indica cuando las variables son creadas y cuando destruidas. En general
este concepto coincide con el alcance (las variables son creadas cuando son declaradas y
destruidas cuando la funcion dentro de la cual fueron declaradas termina), salvo porque
es posible definir: (a) variables dinamicas, que no tienen alcance, sino solo tiempo de
vida (un ejemplo de ello son las variables creadas como punteros, como veremos mas
adelante, en la Sec. 1.5) ; (b) variables estaticas, que conservan su valor entre llamadas
sucesivas de una funcion (estas variables tienen tiempo de vida mayor que su alcance).

1.3.9. Variables estaticas


La palabra reservada static permite definir variables que conservan su valor entre lla-
madas sucesivas de una funcion.
Consideremos el siguiente ejemplo:
1.3. FUNCIONES 27

#include <iostream>

using namespace std;

int f();

int main(){

cout << f() << endl;


cout << f() << endl;

return 0;
}

int f(){
static int x=0;
x++;
return x;
}

Al declarar x como static, se consigue que la primera vez que sea llamada la funcion,
la variable sea creada, asignandosele el valor 0. Luego su valor es incrementado en uno.
Al terminar esa primera llamada, x no es destruida, de modo que cuando f() es llamada
por segunda vez, x no es inicializada de nuevo, y su valor es nuevamente incrementado. El
resultado en pantalla son los numeros 1 y 2.

1.3.10. Recursion
C++ soporta un tipo especial de tecnica de programacion, la recursion, que permite que
una funcion se llame a s misma. Esto permite definir de modo muy compacto una funcion
que calcule el factorial de un numero entero n:

int factorial(int n)
{
return (n<2) ? 1: n * factorial(n-1);
}

Es natural el uso de la recursion, ya que el factorial se puede definir recursivamente:


n! = n (n 1)!.
La estrategia entonces es la siguiente: para calcular el factorial de, por ejemplo, 5, se
debe llamar factorial(5). Como el argumento es mayor que 2, la expresion condicional se
evalua a 2*factorial(4); y as sucesivamente hasta llegar a factorial(2), que se evalua
a 2*factorial(1). Aqu pasa algo distinto, porque el argumento de esta ultima llamada
es 1, y por tanto factorial(1) se evalua como 1. No hay una nueva llamada a la funcion,
y la cadena se devuelve: factorial(1) se evalua como 1; luego factorial(2) se evalua como
2*factorial(2)=2*1, y as sucesivamente, hasta llegar a factorial(5) = 5*(4*(3*(2*1))).
28 CAPITULO 1. UNA BREVE INTRODUCCION A C++

Notamos que es importante, para cualquier llamada recursiva, que exista una condicion
para detener la recursion. De otro modo, se entrara en un ciclo infinito y el programa se
caera. En el caso de la funcion factorial descrita, la detencion se logra con la condicion
n<2, que, cuando es cierta, impide que se llame nuevamente a la misma funcion.

1.4. Matrices
1.4.1. Declaracion e inicializacion
Podemos declarar (e inicializar inmediatamente) matrices (o vectores, o arreglos, arrays
en ingles) de enteros, reales de doble precision, caracteres, etc., segun nuestras necesidades.

int a[5];
double r[3] = {3.5, 4.1, -10.8};
char palabra[5];

Una vez declarada la matriz (digamos a[5]), los valores individuales se accesan con a[i],
con i desde 0 a 4. Por ejemplo, podemos inicializar los elementos de la matriz as:

a[0] = 3;
a[3] = 5; ...

o si queremos ingresarlos desde el teclado:

for (int i = 0; i < 5; i++){


cin >> a[i];
}

Y si deseamos escribirlos en pantalla:

for (int i = 0; i < 5; i++){


cout << a[i];
}

Podemos pensar en un arreglo como una serie de direcciones de memoria consecutivas.


Por ejemplo, en el caso de r[3] definido mas arriba:
r

3.5 4.1 10.8


r[0] r[1] r[2]

Un error comun es tratar de accesar o asignar un arreglo fuera del rango que le correspon-
de. Por ejemplo, r[-1] o r[3] en el ejemplo anterior. En ese caso, se estara invadiendo una
region de memoria que no se debera estar accesando, y el resultado en general no tendra sen-
tido o causara un error durante la ejecucion.
1.4. MATRICES 29

1.4.2. Matrices como parametros de funciones


Una matriz puede ser entregada como argumento a una funcion. En ese caso, es necesario
agregar [] al tipo de variable en la declaracion. Ademas, se debe entregar como parametro
independiente la dimension de la matriz, ya que esa es una informacion que no esta contenida
dentro de la propia matriz (de otro modo, se correra el riesgo de accesar regiones incorrectas
de memoria). Por ejemplo, deleguemos a una funcion el mostrar en pantalla los elementos de
una matriz:

#include <iostream>

using namespace std;

void PrintMatriz(int,double []);

int main()
{
int dim = 5;
double matriz[dim] = {3.5, 5.2, 2.4, -0.9, -10.8};
PrintMatriz(dim, matriz);
return 0;
}

void PrintMatriz(int i,double a []){


for (int j = 0; j < i; j++){
cout << "Elemento " << j << " = " << a[j] << endl;
}
}

1.4.3. Matrices multidimensionales


Tambien es posible utilizar matrices de dos dimensiones:
double arreglo_1[10][8];
int arreglo_2[2][3] = {{1, 2, 3},
{4, 5, 6}};
Al declarar arreglo_1[10][8], se esta indicando, en el fondo, que arreglo_1 es un vector
de dimension 10, cada uno de los cuales, a su vez, es un vector de dimension 8. Esto permite
entender la forma de inicializar arreglo_2 en los ejemplos anteriores: dos vectores, cada uno
de dimension 3.

1.4.4. Matrices de caracteres: cadenas (strings)


Un caso especial de arreglos son las cadenas (strings, en ingles), las cuales permiten
representar texto. Ahora bien, esto introduce un problema nuevo: nos gustara que un string
pueda contener texto de longitud variable, por ejemplo que sea definida primero como "Hola",
30 CAPITULO 1. UNA BREVE INTRODUCCION A C++

y luego como "Hola de nuevo". Como los arreglos no tienen incorporada la informacion sobre
su propia dimension, se requiere otra manera de indicar cuando termina la variable. Ello se
logra con un caracter de termino: \0. As, una cadena es un arreglo de caracteres que
termina con el char nulo: \0:
char palabra[5] = {H, o, l, a, \0};
palabra

H o l a \0

Como con cualquier arreglo, se pueden accesar los elementos individuales (los caracteres)
con [], y si queremos enviar esta variable a pantalla, podemos hacerlo caracter por caracter:
for (i = 0; i < 5; i++){
cout << palabra[i];
}
o, equivalentemente, con cout << "Hola". De hecho, la declaracion de palabra podra ha-
berse escrito:
char palabra[5] = "Hola";
Notemos que, en cualquier caso, la dimension del arreglo debe ser uno mas que el numero de
caracteres visibles.

<string>
El modo anterior para manejar texto es correcto, pero incomodo por diversas razones. Por
ello, C++ proporciona un tipo especial de variable, string, que cumple el mismo objetivo, y
con muchas otras caractersticas que la hacen mas practica. Para usarla, se debe incluir el
header <string>. Una variable tipo string se puede usar del mismo modo que los otros tipos
basicos que hemos visto hasta el momento. En el siguiente ejemplo, se definen dos variables
tipo string, una de ellas se ingresa desde el teclado, y luego ambas se escriben en pantalla:
#include<iostream>
#include<string>

using namespace std;

int main(){
string palabra1="Hola";
string palabra2;

cin >> palabra2;

cout << "La primera palabra es: " << palabra1 << endl;
cout << "La segunda palabra es: " << palabra2 << endl;
1.5. PUNTEROS 31

return 0;
}
Mas adelante, en la Sec. 1.10.1, revisaremos caractersticas adicionales de string.

1.5. Punteros
Una de las ventajas de C++ es permitir el acceso directo del programador a zonas de
memoria, ya sea para crearlas, asignarles un valor o destruirlas. Para ello, ademas de los tipos
de variables ya conocidos (int, double, etc.), C++ proporciona un nuevo tipo: el puntero.
El puntero no contiene el valor de una variable, sino la direccion de memoria en la cual dicha
variable se encuentra.
Un pequeno ejemplo nos permite ver la diferencia entre un puntero y la variable a la cual
ese puntero apunta:
#include <iostream>

using namespace std;

int main(){
int i = 42;
int * p_i = &i;
cout << "El valor del puntero es: " << p_i << endl;
cout << "Y apunta a la variable: " << *p_i << endl;

return 0;
}
En este programa definimos una variable i entera, y luego un puntero a esa variable, que
en este caso denominamos p_i. Observemos la presencia de los smbolos * y &: int * es un
puntero a una variable tipo int; por su parte, en el lado derecho de esa asignacion, &i es la
direccion de memoria de la variable i. De ah en adelante, la variable p_i aloja la direccion de
memoria en la que se encuentra la variable i; y si se quiere acceder a los contenidos dentro de
esa direccion de memoria, se obtiene con *p_i (que, en este caso, sera equivalente a usar la
variable original, i). Se dice que & es el operador de referencia (observar que no es casualidad
su uso al pasar argumentos de funciones como referencia, visto en Sec. 1.3.5), y que * es el
operador de derreferencia.
Al ejecutar este programa, se obtiene una salida en pantalla de la forma:
El valor del puntero es: 0x7fff953449e4
Y apunta a la variable: 42
La primera lnea nos da el valor del puntero, que es algun numero hexadecimal imposible
de determinar a priori , y que corresponde a la direccion de memoria donde quedo ubicada
la variable i. La segunda lnea nos da el valor de la variable que esta en esa direccion de
memoria: 42.
32 CAPITULO 1. UNA BREVE INTRODUCCION A C++

En el ejemplo anterior, utilizamos un puntero a una variable previamente declarada.


Tambien es posible declarar un puntero a una variable no existente, usando new. As, el
codigo anterior se puede reescribir as:

#include <iostream>

using namespace std;

int main(){
int * p = new int;
*p = 42;
cout << "El valor del puntero es: " << p << endl;
cout << "Y apunta a la variable: " << *p << endl;

delete p;

return 0;
}

En la primera lnea, declaramos p como un puntero a entero, y le asignamos una nueva


direccion de memoria suficiente para alojara un int. Ahora esta variable existe solo como
puntero, y el unico modo de accesarlas es a traves de su direccion de memorial. Salvo por
eso, se pueden manipular como cualquier variable, y aca vemos como se le puede asignar y
recuperar un valor. Finalmente, delete libera la memoria asignada a p. A estas variables,
solo existen como punteros que se pueden crear y destruir, se les llama variables dinamicas, y
fueron mencionadas antes, en la Sec. 1.3.8, como variables que no tienen alcance, solo tiempo
de vida, determinado este por el tiempo de ejecucion del programa desde que se crean con
new hasta que se destruyen con delete u otro evento como el fin de la funcion main.
Los punteros tienen gran importancia cuando de manejar datos dinamicos se trata, es
decir, objetos que son creados durante la ejecucion del programa, en numero imposible de
predecir al momento de compilar. Por ejemplo, un ambiente grafico como Gnome o KDE
que crea una, dos, tres, etc. ventanas a medida que el usuario lo requiere. En este caso, cada
ventana es un objeto dinamico, creado durante la ejecucion, y la unica forma de manejarlo
es a traves de un puntero a ese objeto.
Los punteros son sin duda elementos muy interesantes, que permiten una gran flexibilidad
en la programacion. Sin embargo, si no son usados con cuidado pueden ser fuente de grandes
problemas, ya sea por la legibilidad del codigo o por un mal manejo de la memoria.

1.6. Argumentos de main


En la Sec. 1.3.1 se hizo notar que main no es sino una funcion (con nombre reservado), con
un tipo de retorno entero. Ahora bien, si es una funcion, debera poder recibir argumentos.
Eso es as, en efecto, y esos argumentos se pueden usar para entregarle parametros a nuestro
codigo desde la lnea de comandos. Nosotros hemos usado eso muchas veces. Por ejemplo,
para copiar un archivo con otro nombre, se utiliza un comando del tipo:
1.7. MANEJO DE ARCHIVOS 33

cp archivo1.txt archivo2.txt

Pues bien, cp es en realidad un archivo ejecutable que es capaz de recibir dos argumentos,
correspondientes a los archivos de origen y destino. Esos argumentos son pasados al codigo
como argumentos de la funcion main.
Para ejemplificarlo, consideremos un codigo, que simplemente escribe en pantalla los ar-
gumentos que se le entregaron desde la lnea de comando. Por ejemplo, si el codigo esta en
un archivo argumentos.cc, el resultado de ./argumentos uno dos tres sera:

uno
dos
tres

El resultado anterior se puede conseguir con el siguiente codigo:

#include <iostream>

using namespace std;

int main(int argc, char * argv[]){


for (int i=0;i<argc;i++){
cout << argv[i] << endl;
}
return 0;
}

El primer argumento de main es siempre un int, que registra el numero de argumentos que
le fueron entregados (en el ejemplo, tres). El segundo argumento de main es un puntero a un
arreglo de caracteres, donde el elemento i-esimo de dicho arreglo es el argumento i-esimo en
la lnea de comandos.
Es interesante notar como distintas caractersticas de C++ que hemos revisado hasta ahora
(caracteres, arreglos, punteros), convergen en este punto para hacer posible entregar argu-
mentos desde la lnea de comandos, donde cada argumento es un arreglo de caracteres, cada
uno de dimension variable (numero de letras arbitrario en cada argumento), y a su vez se
puede tener un numero arbitrario de estos arreglos de caracteres (numero arbitrario de ar-
gumentos).

1.7. Manejo de archivos


Hasta el momento toda la salida de nuestros codigos ha sido ha pantalla, y todo el ingreso
de datos ha sido desde el teclado. Naturalmente la entrada y salida de datos se puede hacer
desde archivos, y esas capacidades son las que revisaremos a continuacion.
(Hacemos notar de inmediato que la sintaxis que se utiliza para manejar archivos es un
poco diferente de la que hemos usado hasta ahora. No intentaremos justificarla por ahora,
y sera completamente evidente una vez revisada la Sec. 1.9. Sin embargo, es importante
que aprendamos a manejar rapidamente archivos y strings, por razones practicas, antes de
34 CAPITULO 1. UNA BREVE INTRODUCCION A C++

continuar con aspectos mas tecnicos del lenguaje, y por esa razon preferimos presentar este
topico en este momento, antes de tener todos los fundamentos.)
El siguiente codigo de ejemplo muestra como escribir dos variables, una definida dentro
del codigo, la otra ingresada desde el teclado, en un archivo:

#include <iostream>
#include <fstream>

using namespace std;

int main(){

ofstream nombre_logico("nombre_fisico.dat");
int i = 3, j;

nombre_logico << i << endl;


cin >> j;
nombre_logico << j << endl;

nombre_logico.close();
return 0;
}

Observamos que se debe incluir el header fstream (por file stream, analogo al input-output
stream que ya conocamos. Para abrir un archivo, lo declaramos con ofstream (output fi-
le stream), que corresponde a un archivo llamado nombre_fisico.dat, el cual sera creado
(si no existe) o sobreescrito (si existe) en disco, y que sera identificado dentro del codi-
go como una variable llamada nombre_logico. El nombre "nombre_fisico.dat" puede
ser cualquier nombre valido para el sistema operativo, incluyendo el uso de paths abso-
lutos o relativos, de modo que expresiones tales como "../directorio/archivo.dat" o
"/home/usuario/otro_archivo.txt" son completamente aceptables.
Luego, para escribir en el archivo, se usa la misma instruccion que usaramos para escribir
en pantalla, reemplazando cout por nombre_logico.
As que, en definitiva, la escritura en archivos sigue las mismas reglas que la escritura en
pantalla, reemplazando el output stream por un file stream.
Al final, cerramos el stream con close. (En rigor, no es completamente necesario que lo
hagamos explcitamente, porque el compilador cerrara el stream automaticamente si nosotros
no lo hacemos. Lo presentamos mas bien por completitud.)
Para ingresar datos desde un archivo la idea es exactamente la misma, pero en vez de ha-
cerlo desde cin, que representa al teclado, lo hacemos desde un input file stream (ifstream):

#include <iostream>
#include <fstream>

using namespace std;


1.7. MANEJO DE ARCHIVOS 35

int main(){

ifstream nombre_logico("nombre_fisico.dat");
int i,j,k,l;

nombre_logico >> i >> j >> k >> l;

cout << i << "," << j << "," << k << "," << l << endl;
nombre_logico.close();

return 0;
}

En este caso, el mismo archivo anterior es usado como un objeto ifstream, y cuatro variables
enteras se leen desde el.
En los dos codigos anteriores, un mismo archivo se uso primero como salida y luego como
entrada, pero esto sucedio en dos programas separados. Sera posible usar el mismo archivo
tanto para entrada como para salida, dentro del mismo codigo? La respuesta es s. En ese
caso, no se puede declarar el archivo como ofstream ni como ifstream, sino como algo
generico: fstream. Este stream generico puede ser abierto como entrada o salida segun lo
necesitemos. Observemos el siguiente ejemplo:

#include <iostream>
#include <fstream>

using namespace std;

int main(){

fstream nombre_logico;
nombre_logico.open("nombre_fisico.dat",ios::out);
int i = 4,j;

nombre_logico << i << endl;


nombre_logico.close();

nombre_logico.open("nombre_fisico.dat",ios::in);

nombre_logico >> j;
j = j++;
cout << j << endl;
nombre_logico.close();

return 0;
}
36 CAPITULO 1. UNA BREVE INTRODUCCION A C++

Notemos que la variable nombre_logico se declara como un fstream. Hasta el momento


no pasa nada, no esta asociado a ningun archivo fsico. En la lnea siguiente sucede eso,
cuando se abre, con open, que toma dos argumentos: el primero es el nombre del archivo
en disco (nombre_fisico.dat), y el segundo es una variable, definida en alguna parte de
las libreras del sistema, que determina el modo de apertura. En este caso, ios::out es una
constante, que lo unico que importa por ahora es que significa abrir en modo de escritura.
La siguiente lnea escribe la variable i en el archivo.
A continuacion deseamos leer el valor escrito en el archivo, y ponerlo en una nueva variable
j. Para ello, debemos cerrar el stream (ahora close no es opcional), y volver a abrirlo, pero
en modo de lectura (ios::in).
Estas operaciones de apertura y cierre se pueden hacer un numero arbitrario de veces,
permitiendonos interactuar con el mismo archivo del modo que nos convenga.
(Notemos que las dos primeras lneas podran haberse escrito en una sola lnea as:
fstream nombre_logico("nombre_fisico.dat",ios::out); En el ejemplo lo escribimos
separado para que se viera la completa simetra entre los casos de lectura y de escritura.)
Abrir un archivo como ofstream o, equivalente, como fstream en modo ios::out, tiene
el efecto de sobreescribir los contenidos. Si uno desea agregar contenidos, el stream debe
abrirse en modo append :

#include <iostream>
#include <fstream>

using namespace std;

int main(){

ofstream nombre_logico("nombre_fisico.dat",ios::app);
int i = 3, j;

nombre_logico << i << endl;


cin >> j;
nombre_logico << j << endl;

return 0;
}

1.8. Funciones matematicas


Otro header que es importante conocer es cmath. Con el se tiene acceso a multiples
funciones y constantes matematicas. Algunos ejemplos:

#include <iostream>
#include <cmath>

using namespace std;


1.9. CLASES 37

int main(){
double a = M_PI; // valor de la constante pi
double b = sqrt(a); // raiz cuadrada
double c = log(b); // logaritmo natural
double d = sin(c); // seno
double e = cos(d); // coseno
double f = fabs(e); // valor absoluto
double g = exp(f); // exponencial
// etc.
return 0;
}

1.9. Clases
C++ dispone de una serie de tipos de variables con los cuales nos esta permitido operar:
int, double, char, etc. Creamos variables de estos tipos y luego podemos trabajar con ellas:

int x, y;
x = 3;
y = 6;
int z = x + y;

No hay, sin embargo, en C++ , una estructura predefinida que corresponda a numeros
complejos, vectores de dimension n o matrices, por ejemplo. Y sin embargo, nos agradara
disponer de numeros complejos que pudieramos definir como
z = (3,5);
w = (6,8);
y que tuvieran sentido las expresiones
a = z + w;
b = z * w;
c = z / w;
d = z + 3;
f = sqrt(z);
Todas estas expresiones son completamente naturales desde el punto de vista matematico,
y sera bueno que el lenguaje las entendiera. Esto es imposible en el estado actual, pues, por
ejemplo, el signo + es un operador que espera a ambos lados suyos un numero. Sumar cualquier
cosa con cualquier cosa no significa nada necesariamente, as que solo esta permitido operar
con numeros. Pero los humanos sabemos que los complejos son numeros. Como decrselo
al computador? Como convencerlo de que sumar vectores o matrices es tambien posible
matematicamente, y que el mismo signo + debera servir para todas estas operaciones?
La respuesta es: a traves del concepto de clases. Lo que debemos hacer es definir una clase
de numeros complejos. Llamemosla Complejo. Una vez definida correctamente, Complejo
38 CAPITULO 1. UNA BREVE INTRODUCCION A C++

sera un tipo mas de variable que el compilador reconocera, igual que int, double, char, etc.
Y sera tan facil operar con los Complejos como con todos los tipos de variables preexistentes.
Esta facilidad es la base de la extensibilidad de que es capaz C++ , y por tanto de todas las
propiedades que lo convierten en un lenguaje muy poderoso.
Las clases responden a la necesidad del programador de construir objetos o tipos de datos
que respondan a sus necesidades. Si necesitamos trabajar con vectores de 5 coordenadas,
sera natural definir una clase que corresponda a vectores con 5 coordenadas; si se trata de
un programa de administracion de personal, la clase puede corresponder a un empleado, con
sus datos personales como elementos; si es una simulacion de vida artificial, la clase puede
corresponder a una celula.
Si bien es cierto uno puede trabajar con clases en el contexto de orientacion al procedi-
miento, las clases muestran con mayor propiedad su potencial con la orientacion al objeto,
donde cada objeto corresponde a una clase. Por ejemplo, para efectuar una aplicacion para un
ambiente grafico, cada uno de los objetos involucrados (la ventana principal, las ventanas de
los archivos abiertos, la barra de menu, las cajas de dialogo, los botones, etc.) estara asociado
a una clase.

1.9.1. Definicion
En lo que sigue, crearemos una clase para numeros complejos, a traves de sucesivos pasos
que la haran cada vez mas funcional. Naturalmente, esto es solo una ilustracion. De hecho,
en C++ ya existe, en la librera estandar, una clase para numeros complejos. Desde ese punto
de vista, puede parecer un ejercicio inutil, pero tiene varias ventajas: (a) tenemos una clara
intuicion de que caractersticas necesitamos que tenga la clase; (b) nos permitira introducir
conceptos aplicables a cualquier clase que deseemos crear; (c) construir nuestra propia clase
nos permitira evitar el aura de caja negra que suelen tener las soluciones que vienen
previamente instaladas con el sistema.
Digamos entonces que queremos una clase para representar a los numeros complejos.
Llamemosla Complejo. Usaremos la convencion de que los nombres de las clases comiencen
con mayuscula. Esto es porque las clases, recordemos, corresponderan a tipos de variables
tan validos como los internos de C++ (int, char, etc.). Al usar nombres con mayuscula
distiguimos visualmente los nombres de un tipo de variable interno y uno definido por el
usuario.
El siguiente codigo muestra la estructura mnima necesaria para que la clase Complejo
exista:

#include<iostream>

using namespace std;

class Complejo{
};

int main(){
1.9. CLASES 39

Complejo z;

return 0;
}

Eso es todo. Todas las caractersticas de la clase se definiran luego entre los parentesis
cursivos, pero con la estructura anterior logramos la funcionalidad mas basica: podemos
declarar un objeto tipo Complejo del mismo modo como declaramos cualquier otra variable
estandar del lenguaje:

int i;
double f;

Al menos a este nivel, nuestra clase y los tipos de variables estandar disponibles son indis-
tinguibles. Naturalmente, esta clase es bastante limitada, porque no podemos hacer nada
mas que declarar objetos tipo Complejo, y en lo que sigue iremos agregandole cada vez mas
funcionalidad.

1.9.2. Variables miembros


Para que la clase anterior represente efectivamente numeros complejos, debe necesaria-
mente tener, de alguna forma, la informacion sobre las partes real e imaginaria de dicho
numero. Para lograrlo, definiremos dos variables miembros de la clase Complejo, una de las
cuales correspondera a la parte real y la otra a su parte imaginaria:

class Complejo{
public:
double real;
double imag;
};

Cualquier tipo de variable aceptada por el sistema puede ser variable miembro de una clase.
En este caso, necesitamos dos variables tipo double. La palabra public que las antecede es
necesaria para que las variables sean accesibles desde el exterior de la clase (mas sobre eso
en la Sec. 1.9.3).
El codigo anterior permite que cada vez que se declara un objeto de tipo Complejo,
tendra sus propias variables real e imag. Como siempre se llaman igual para todos los
objetos Complejo, para distinguir las variables real e imag de objetos distintos, se utiliza el
operador de seleccion (.):

#include<iostream>

using namespace std;

class Complejo{
public:
double real;
40 CAPITULO 1. UNA BREVE INTRODUCCION A C++

double imag;
};

int main(){

Complejo z;

z.real=2;
z.imag=-3;

Complejo w;

w.real=1;
w.imag=0;

cout << z.real << endl;


cout << z.imag << endl;

return 0;
}

El resultado sera que en pantalla apareceran los numeros 2 y 3, que son las partes real e
imaginaria del objeto z.

1.9.3. Funciones miembro; miembros publicos y privados


En este momento, los miembros real e imag de nuestra clase estan disponibles para su
modificacion y acceso desde main y cualquier otra funcion que llame a objetos tipo Complejo.
Sin embargo, esto no es necesariamente una buena idea. Por ejemplo, digamos que nuestra
clase es usada por muchos codigos distintos, incluso por muchas personas distintas. Luego,
unos meses despues, nos damos cuenta de que era mas conveniente haber definido los numeros
complejos en terminos de sus coordenadas polares, modulo y fase. O algo mucho menos
profundo: decidimos que es mejor escribir menos, y queremos cambiar los nombres real e
imag a x e y, respectivamente.
Introducir mejoras como esas sera realmente impractico, porque todos los codigos esperan
que las variables de todo Complejo sean la parte real e imaginaria, y que se llamen real e
imag. Esto limita profundamente las capacidades de mejora del diseno de una clase, ya que
las decisiones iniciales se vuelven, en la practica, definitivas.
Para evitar lo anterior, lo usual es que las variables miembros no sean accesibles desde
fuera de la clase, y que la interaccion con el exterior sea exclusivamente a traves de funciones.
Tecnicamente, la clase contara con una estructura interna privada, accesible solo a variables
y funciones propias de la clase, y una interfase externa publica, que es la que los usuarios
de la clase tienen permitido conocer y usar. De este modo, la estructura privada podra
ser modificada y optimizada repetidas veces, para hacer la clase cada vez mejor, pero si la
interfase publica se mantiene invariable, los usuarios o codigos que utilicen la clase recibiran
1.9. CLASES 41

todos los beneficios de la nueva estructura, sin las dificultades asociadas a los cambios de
diseno, que permaneceran invisibles para ellos.
Todo esto se logra definiendo miembros privados y publicos de una clase, identificados
con las palabras private y public. En el ejemplo de la seccion anterior, la clase tiene dos
variables miembros publicas y ningun miembro privado.
Si queremos ahora esconder la estructura interna de la clase, debemos definir dichas
variables como privadas, y proporcionar funciones que permitan accesar y/o modificar dichas
variables:
class Complejo{
private:
double real;
double imag;
public:
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};
Ahora la clase tiene cuatro funciones miembro, todas publicas. Dos de ellas van a permitir
cambiar el valor de las partes real e imaginaria (deben aceptar como argumento una variable
tipo double, que correspondera al nuevo valor de la parte real o imaginaria segun correspon-
da); dichas funciones son tipo void, ya que no requieren devolver nada. En cuanto a las otras
dos, ellas permitiran obtener el valor de las partes real e imaginaria actuales; no necesitan
argumentos, pero deben devolver una variable double, justamente el valor de la parte real o
imaginaria segun corresponda. Cualquier tipo de retorno o de argumentos es aceptable para
funciones miembros de una clase; incluso es posible usar el propio tipo Complejo, como si ya
existiera, de modo que una hipotetica funcion miembro llamada mi_funcion(), que acepte
como argumento un Complejo y devuelva un Complejo, se podra declarar de la siguiente
forma:
class Complejo{
private:
double real;
double imag;
public:
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
Complejo mi_funcion(Complejo);
};
En este momento, las funciones han sido solo declaradas. Ahora deben ser implementa-
das. La implementacion podra hacerse dentro del codigo de la clase, pero nosotros elegire-
mos separar el codigo e implementarlas fuera de la clase. Como podra existir otra funcion
42 CAPITULO 1. UNA BREVE INTRODUCCION A C++

set_real(double), para identificar que queremos definir la funcion set_real(double) co-


rrespondiente a la clase Complejo, se usa el operador ::. El codigo completo que declara la
clase e implementa sus cuatro funciones miembros es el siguiente:

class Complejo{
private:
double real;
double imag;
public:
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};

void Complejo::set_real(double x){


real = x;
}

void Complejo::set_imag(double x){


imag = x;
}

double Complejo::get_real(){
return real;
}

double Complejo::get_imag(){
return imag;
}

Ahora estamos listos para usar esta clase desde main o cualquier otra funcion que lo
requiera. Para acceder a las funciones miembro usamos el operador de seleccion, como antes.
Presentamos el codigo completo:

#include<iostream>

using namespace std;

class Complejo{
private:
double real;
double imag;
public:
void set_real(double);
void set_imag(double);
1.9. CLASES 43

double get_real();
double get_imag();
};

void Complejo::set_real(double x){


real = x;
}

void Complejo::set_imag(double x){


imag = x;
}

double Complejo::get_real(){
return real;
}

double Complejo::get_imag(){
return imag;
}

int main(){

Complejo z;

z.set_real(2);
z.set_imag(-3);

cout << z.get_real() << endl;


cout << z.get_imag() << endl;

return 0;
}
Si bien es cierto para esta clase particular, en el estado en que se encuentra en este
momento, el cambio que hemos introducido parece puramente cosmetico, en realidad es una
estrategia de programacion que recomendamos enormemente, cuyo valor es cada vez mayor
en la medida que los codigos se van complicando.
Como dijimos, si en el futuro decidimos que es mejor utilizar las coordenadas polares,
entonces podremos redefinir la clase de la forma:
#include<iostream>

using namespace std;

class Complejo{
private:
44 CAPITULO 1. UNA BREVE INTRODUCCION A C++

double modulo;
double fase;
public:
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};
Y ahora, por ejemplo, set_real(double) sera una funcion mas complicada, porque el
usuario querra que z.set_real(2) siga teniendo el efecto de cambiar la parte real de z. Para
lograrlo, la funcion debera transformar modulo y fase simultaneamente, de modo que la parte
real sea ahora 2. Y cuando el usuario pida la parte real, con get_real(), la implementacion
de esta funcion tambien tendra que hacer algo un poco mas complicado:
double Complejo::get_real(){
return modulo*sin(fase);
}
Pero lo importante es que, como hemos dicho, independiente de las decisiones de diseno
interno de la clase, la interaccion con el exterior se ha mantenido invariante, y ninguna lnea
de main() necesita ser cambiada.

1.9.4. Creacion de header files


El codigo anterior es correcto, pero aparte de que la clase Complejo tiene muy poca fun-
cionalidad, tiene un problema importante: la idea de definir un nuevo tipo de variable es
interesante, pero un poco inutil si ese nuevo tipo no se puede hacer disponible a muchos
otros codigos. Tal como esta definida la clase ahora, si necesitamos escribir otro codigo que
use nuestra clase Complejo, tendramos que copiar todo el codigo correspondiente a la clase
en el archivo correspondiente. En este momento eso no es problema en principio, porque son
unas pocas lneas, pero a medida que la clase se vaya complicando esto sera cada vez menos
practico. Ademas, incluso si copiaramos todo el codigo en cada archivo que lo necesite, hay
otro problema aun mas grave: si en el futuro decidimos modificar la clase para mejorarla
progresivamente (lo que haremos en las secciones siguientes), si queremos que esas modifi-
caciones se propaguen a todos los codigos que la usan deberamos copiar las modificaciones
una vez mas en todos los archivos involucrados. No es una manera eficiente de trabajar.
La solucion a todos estos problemas es escribir todo el codigo correspondiente a nuestra
clase en archivos separados, que son luego llamados por cada codigo que lo necesite. As, cada
modificacion a la clase Complejo podra ser propagada de inmediato a todos los codigos que
la requieran.
Lo que haremos entonces es escribir toda la declaracion de la clase en un header file
llamado, digamos, complejo.h, de la siguiente forma:
//----------Archivo complejo.h----------
#ifndef COMPLEJO_H
#define COMPLEJO_H
1.9. CLASES 45

using namespace std;

class Complejo{
private:
double real;
double imag;
public:
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};
#endif

Las instrucciones ifndef, define y endif son directivas de preprocessador. Aunque no son
necesarias para los ejemplos que revisaremos a continuacion, por razones que seran evidentes
mas adelante (ver Sec. 1.14.6) es bueno incluirlas.
Por su parte, la implementacion de las funciones miembros va en el archivo asociado
complejo.cc:

//----------Archivo complejo.cc----------
#include "complejo.h"

void Complejo::set_real(double x){


real = x;
}

void Complejo::set_imag(double x){


imag = x;
}

double Complejo::get_real(){
return real;
}

double Complejo::get_imag(){
return imag;
}

Hacemos notar que la separacion en dos archivos, uno con extension .h y otro con ex-
tension .cc, no es exclusiva para clases. Podramos tambien hacer un header file con cual-
quier funcion (por ejemplo, la funcion PrintHola() de la Sec. 1.1.1). La estrategia es la
misma: poner la declaracion de ella y otras funciones que necesitemos en un archivo como
funciones_utiles.h, y sus respectivas implementaciones en funciones_utiles.cc.
46 CAPITULO 1. UNA BREVE INTRODUCCION A C++

Notemos que este es un nivel mas alto de encapsulamiento que el que hemos visto. No solo
podemos definir funciones, y no solo podemos separar su declaracion y su implementacion,
sino que ademas podemos separar su declaracion y su implementacion en archivos distintos.
Con esta estructura de archivos, el codigo de nuestro programa queda de la siguiente
forma:
#include<iostream>
#include "complejo.h"

using namespace std;

int main () {

Complejo z;

z.set_real(2);
z.set_imag(-3);

cout << z.get_real() << endl;


cout << z.get_imag() << endl;

return 0;
}
Para tener acceso a la funcionalidad de Complejo, basta con incluir el archivo complejo.h.
Las comillas permiten distinguirlo de los headers de sistema, que son delimitados por <>.
Entre comillas puede ir el nombre de archivo con path, relativo o absoluto. Si no se indica el
path, el compilador asume que debe buscar el archivo complejo.h en el directorio actual.
Notemos que complejo.h tiene solo la declaracion de las funciones. Para que las imple-
mentaciones esten disponibles, se debe incluir complejo.cc en la lnea de compilacion. As,
si el codigo anterior esta en un archivo llamado programa.cc, la lnea de compilacion sera:
g++ -o programa programa.cc complejos.cc

1.9.5. Constructor
Al declarar una variable, el programa crea el espacio de memoria suficiente para alojarla.
Cuando se trata de variables de tipos predefinidos en C++ esto no es problema, pero cuando
son tipos definidos por el usuario C++ debe saber como construir ese espacio. La funcion
que realiza esa tarea se denomina constructor.
El constructor es una funcion publica de la clase, que tiene el mismo nombre que ella.
Agreguemos un constructor a la clase Complejo:
//----------Archivo complejo.h----------
#ifndef COMPLEJO_H
#define COMPLEJO_H
1.9. CLASES 47

using namespace std;

class Complejo{
private:
double real;
double imag;
public:
Complejo();
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};
#endif

Y en complejo.cc debera agregarse el siguiente codigo:

Complejo::Complejo(){
}

Notemos que el constructor es una funcion especial, ya que no tiene tipo de retorno (ni
siquiera void). Ademas tiene que ser una funcion publica, ya que debe poder ser accesible
desde fuera de la clase, naturalmente. El constructor anterior es bastante inutil, porque no
hace nada. De hecho, podramos omitirlo (como lo hicimos antes), porque si una clase no tiene
constructor, el compilador creara un constructor default, suficiente para los propositos mas
sencillos. Por lo tanto, agregar este constructor, que no hace nada realmente, no es necesario.
En este momento, solo podemos construir un objeto Complejo con una lnea como esta:

Complejo z;

Pero los tipos de variable preexistentes tambien permiten implementar junto con declarar.
La manera de lograrlo con nuestra clase es hacer un constructor no default, que acepte
argumentos. Lo logico, en nuestro caso, es que esos argumentos sean dos: uno que sera la
parte real del nuevo objeto, y el otro su parte imaginaria.

//----------Archivo complejo.h----------
#ifndef COMPLEJO_H
#define COMPLEJO_H

using namespace std;

class Complejo{
private:
double real;
double imag;
public:
Complejo();
48 CAPITULO 1. UNA BREVE INTRODUCCION A C++

Complejo(double,double);
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};
#endif

Notemos que no hay ningun conflicto entre los dos constructores, ya que el numero y ti-
po de argumentos son distintos, y el programa siempre sabra cual version del constructor
debera llamar cada vez que lo necesite. En complejo.cc la funcionalidad se implementa as:

Complejo::Complejo(double x,double y){


real = x;
imag = y;
}

Con esto, las siguientes declaraciones e implementaciones son validas:

Complejo w(10.5,-8.0);
Complejo r = Complejo(-3.6,4.75);

La segunda forma es interesante, porque es la que se parece mas a la forma usual en que
declaramos e inicializamos una variable de tipo predefinido:

int i = 10;

No se ve exactamente igual, pero recordemos que la siguiente sintaxis tambien es correcta:

int i = int(10);

Y esta s es exactamente igual a la forma en que se definio el objeto r. Hemos conseguido que,
al menos desde el punto de vista de la inicializacion, nuestras variables son indistinguibles de
los tipos de variables predefinidas.
En general, los constructores pueden ejecutar tareas bien elaboradas. Parte de ello puede
ser inicializar variables privadas con los argumentos entregados. Para ejecutar ese trabajo, la
implementacion se puede hacer con una sintaxis alternativa:

Complejo::Complejo(double x,double y):real(x),imag(y){


}

En el cuerpo de la funcion, entre los parentesis cursivos, pueden ir todas esas tareas adicionales
que no consisten simplemente en asignar valores a las variables privadas.
1.9. CLASES 49

1.9.6. Destructor
As como es necesario crear espacio de memoria al definir una variable, hay que deshacerse
de ese espacio cuando la variable deja de ser necesaria. En otras palabras, la clase necesita
tambien un destructor . Esta es otra funcion publica de la clase, cuyo nombre es igual al de
la clase, precedida por un ~:

//----------Archivo complejo.h----------
#ifndef COMPLEJO_H
#define COMPLEJO_H

using namespace std;

class Complejo{
private:
double real;
double imag;
public:
Complejo();
Complejo(double,double);
~Complejo();
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};
#endif

En complejo.cc debe estar la implementacion:

Complejo::~Complejo(){
}

En este caso, el destructor no hace nada muy interesante, y sera equivalente a no haberlo
declarado. En ese caso, igual que con los constructores, el compilador genera un destructor
default, suficiente para las tareas mas sencillas.

1.9.7. Matrices de clases


Una clase es un tipo de variable como cualquier otro de los predefinidos en C++ . Es posible
construir matrices con ellas, del mismo modo que uno tiene matrices de enteros o caracteres.
As, el siguiente codigo es valido:

Complejo v[3];

v[0]=Complejo(2.1,4.1);
v[1]=Complejo(6.3,-2.1);
50 CAPITULO 1. UNA BREVE INTRODUCCION A C++

v[2]=Complejo(4.1,-3.8);

Complejo w[2]={Complejo(3.5,-0.8),Complejo(-2,4)};

1.9.8. Constructor de copia


Ademas del constructor usual, existe un constructor especial, llamado constructor de
copia, y que es llamado especficamente cuando se necesita copiar los contenidos de un objeto
en el otro. Como con los otros constructores y con los destructores, si uno no lo define el
compilador proporciona uno por defecto, que es el que hemos estado usando por defecto.
El constructor de copia acepta como argumento una referencia a un objeto del mismo tipo
(lo cual permita que pueda acceder a sus variables privadas, cuyos contenidos podra luego
copiar en otro objeto). Como se debe pasar ese argumento por referencia, es buena idea
declararlo tambien const, para no correr el riesgo de modificar los contenidos de la variable
original por error. La declaracion de la clase quedara as:

//----------Archivo complejo.h----------
#ifndef COMPLEJO_H
#define COMPLEJO_H

using namespace std;

class Complejo{
private:
double real;
double imag;
public:
Complejo();
Complejo(double,double);
Complejo(const Complejo &); // Constructor de copia
~Complejo();
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};
#endif

Y su implementacion en complejo.cc:

Complejo::Complejo(const Complejo & z){


real = z.real;
imag = z.imag;
}
1.9. CLASES 51

De este modo, los valores de las partes real e imaginaria del objeto de origen son copiados
a las variables respectivas del objeto que se esta creando.
Una manera sencilla de verificar que el constructor de copia (y no otro) esta siendo llamado
es alterarlo, de modo que no haga lo esperado. Por ejemplo, consideremos el siguiente codigo:
Complejo::Complejo(const Complejo & z){
real = z.real*z.real;
imag = z.imag*z.imag;
}
Es decir, en vez de copiar las variables se copian sus cuadrados. Con el constructor de copia
as modificado, podremos observar la salida del siguiente codigo:
#include<iostream>
#include "complejo.h"

using namespace std;

void f(Complejo);

int main () {

Complejo z = Complejo(-3.6,4.75);

Complejo w=z;

f(z);

cout << z.get_real() << endl;


cout << z.get_imag() << endl;

cout << w.get_real() << endl;


cout << w.get_imag() << endl;

return 0;
}

void f(Complejo w){


cout << w.get_real() + 2 << endl;
}
Podremos verificar una salida de la forma:
-3.6
4.75
12.96
22.5625
14.96
52 CAPITULO 1. UNA BREVE INTRODUCCION A C++

Esto muestra que en la primera lnea se usa el constructor usual para z. Luego, en la asignacion
w=z es el constructor de copia el que se usa para pasar los valores de z a w. Y tambien se usa
cuando se deben duplicar una variable cuando esta se entrega como parametro por valor a
una funcion.

1.9.9. Sobrecarga de funciones


Para que la definicion de nuevos objetos sea realmente util, hay que ser capaz de hacer
con ellos muchas acciones que nos seran naturales. Como ya comentamos al introducir el
concepto de clase, nos gustara sumar numeros complejos, y que esa suma utilizara el mismo
signo + de la suma usual. O extraerles la raz cuadrada, y que la operacion sea tan facil como
escribir sqrt(z). Ya sabemos, por lo visto en la Sec. 1.3.4, que lo que debemos hacer es
sobrecargar las funciones, de modo que sqrt(z) sea aceptable tambien si z es un Complejo.
Para hacerlo, basta con declarar la nueva funcion sqrt() en complejos.h:

//----------Archivo complejo.h----------
#ifndef COMPLEJO_H
#define COMPLEJO_H

using namespace std;

class Complejo{
private:
double real;
double imag;
public:
Complejo();
Complejo(double,double);
Complejo(const Complejo &);
~Complejo();
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};

Complejo sqrt(Complejo); // sobrecarga de sqrt()

#endif

Mientras la nueva declaracion no entre en conflicto con ninguna definicion previa de sqrt(),
este codigo es valido.
Luego, en complejo.cc, lo implementamos:

Complejo sqrt(Complejo z){


double a = z.get_real();
1.9. CLASES 53

double b = z.get_imag();

double real = sqrt((a+sqrt(a*a+b*b))/2.0);


double imag = sqrt((-a+sqrt(a*a+b*b))/2.0);

return Complejo(real,imag);
}
Notemos que intencionalmente hemos usado los nombres real e imag para las partes real e
imaginaria del valor de retorno, para ilustrar que estas variables no estan en conflicto con las
partes real e imaginaria de z. La funcion sqrt() esta definida dentro del archivo complejo.h,
pero como fue declarada fuera del cuerpo de la clase Complejo, no tiene acceso a las variables
privadas.
Con la definicion anterior podemos obtener la raz cuadrada de un numero complejo
simplemente con las instrucciones:
Complejo z(1,3);
Complejo raiz = sqrt(z);
Observemos finalmente que en la definicion anterior hemos usado el polimorfismo de
sqrt() a nuestro favor, ya que la definicion de la raz cuadrada para numeros complejos
usa, a su vez, la definicion de la raz cuadrada para numeros reales. Y no hay confusion
respecto a cual funcion llamar en cada instancia, ya que las distintas versiones de la funcion
se distinguen por el numero y tipo de sus argumentos.

1.9.10. Sobrecarga de operadores


Como le decimos al computador que el signo + tambien puede aceptar numeros com-
plejos? La respuesta es facil, porque para C++ un operador no es sino una funcion (llama-
da operator +), que acepta dos argumentos (los dos sumandos). Por lo tanto, sobrecargar
un operador es tan sencillo como sobrecargar una funcion. Por lo tanto, bastara poner en
complejo.h el siguiente codigo:
#ifndef COMPLEJO_H
#define COMPLEJO_H

#include <cmath>
#include <iostream>

using namespace std;

class Complejo{
private:
double real;
double imag;
public:
Complejo();
54 CAPITULO 1. UNA BREVE INTRODUCCION A C++

Complejo(double,double=0);
Complejo(const Complejo &);
~Complejo();
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};

Complejo sqrt(Complejo);

Complejo operator + (Complejo,Complejo); // sobrecarga de +

En efecto, deseamos que el operador + acepte dos sumandos de tipo Complejo, y devuelva
un nuevo Complejo.
En complejo.cc estara la implementacion:

Complejo operator + (Complejo z1,Complejo z2){


return Complejo(z1.get_real()+z2.get_real(), z1.get_imag()+z2.get_imag());
}

Ahora el siguiente codigo es completamente valido:

Complejo z = Complejo(-3.6,4.75);
Complejo w = Complejo(2,9);

Complejo r = z+w;

1.9.11. Coercion
Sabemos definir a + b, con a y b complejos. Pero que pasa si a o b son enteros? O reales?
Pareciera que tendramos que definir no solo

Complejo operator + (Complejo a, Complejo b);

sino tambien todas las combinaciones restantes:

Complejo operator + (Complejo a, int b);


Complejo operator + (Complejo a, float b);
Complejo operator + (int a, Complejo b);

etcetera.
En realidad esto no es necesario. En efecto, un numero real es un numero complejo
con parte imaginaria nula. Entonces, si encontramos una expresion como z+2.3, con z un
numero complejo, entonces una solucion sencilla sera promover automaticamente al 2.3 a
un numero complejo (2.3 + i 0), y entonces la suma estara bien definida. Sabemos que
esta accion, llamada coercion sucede con los tipos de variables predefinidas (ver Sec. 1.1.9).
Lo que queremos, entonces, es que haya coercion desde los numeros reales a los numeros
1.9. CLASES 55

complejos (claramente es todo lo que necesitamos, porque como la coercion desde los int,
float, etc. a los double ya existe, entonces sumar cualquier tipo de numero con un Complejo
va a funcionar).
Recordando lo visto en la Sec. 1.3.7, notamos que la manera de lograr esta coercion
entre double y Complejo es definir un constructor tal que su segundo parametro (la parte
imaginaria) tenga un valor default igual a cero. De este modo, las dos expresiones:

Complejo z1(2);
Complejo z2(2,0);

seran equivalentes.
Ya sabemos que lograr esto es sencillo: basta modificar el constructor con dos argumentos
ya declarado:

//----------Archivo complejo.h----------
#ifndef COMPLEJO_H
#define COMPLEJO_H

using namespace std;

class Complejo{
private:
double real;
double imag;
public:
Complejo();
Complejo(double,double=0); // Constructor con parametro default
Complejo(const Complejo &);
~Complejo();
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};

Complejo sqrt(Complejo);

#endif

Los contenidos de complejo.cc quedan intactos.


Con esta pequena modificacion, el siguiente codigo es valido y hace lo que uno espera:

Complejo z = Complejo(-3.6,4.75);
Complejo r = 2+z;

Gracias entonces a los constructores con parametro default, solo es necesario definir la
suma entre numeros complejos, complementada con una coercion.
56 CAPITULO 1. UNA BREVE INTRODUCCION A C++

1.9.12. Interaccion con la pantalla/teclado o archivos


Nuestra clase Complejo tiene ya bastante funcionalidad interesante, que la hace equiva-
lente a cualquier otro tipo de variable predefinida en el sistema. Hay un aspecto que todava
no hemos implementado: la salida a pantalla o archivo. Porque el siguiente codigo:

int k = 3;
cout << k << endl;

es valido, pero si k fuera una variable de tipo Complejo, no lo sera.


Para solucionar esto, basta con notar que << es un operador, como cualquier otro, y por
lo tanto puede ser sobrecargado. Esta sobrecarga se hace de la siguiente forma: primero, la
declaracion en complejo.h:

//----------Archivo complejo.h----------
#ifndef COMPLEJO_H
#define COMPLEJO_H

using namespace std;

class Complejo{
private:
double real;
double imag;
public:
Complejo();
Complejo(double,double=0);
Complejo(const Complejo &);
~Complejo();
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};

Complejo sqrt(Complejo);

ostream & operator << (ostream &,Complejo); // Sobrecarga de << para


// salida a pantalla

#endif

Analicemos la situacion: En una expresion como cout << z << endl, se realiza primero la
operacion mas a la izquierda, es decir cout << z. Como con todo operador, esto es equiva-
lente a operator << (cout,z). Esto es lo que esta definiendo nuestra sobrecarga: << es un
operador que espera un primer argumento de tipo ostream (ahora podemos ver que cout no
es sino un objeto de cierto tipo, ostream, definido en <iostream>; endl es otro objeto de
1.10. ALGUNAS CLASES UTILES 57

tipo ostream), y un segundo argumento de tipo Complejo (o del tipo que queramos enviar a
pantalla).
El resultado de esta operacion (llamemoslo output_parcial) es del mismo tipo ostream,
ya que eso permite que la siguiente operacion operator << (output_parcial,endl) sea una
operacion entre dos objetos ostream, para lo cual ya existe una sobrecarga en <iostream>.
Por lo tanto, lo unico que se requiere definir es la sobrecarga de << actuando sobre un
segundo argumento de tipo Complejo, y de todo lo demas se encargan las sobrecargas ya
definidas en <iostream>.
Notemos, incidentalmente, que en realidad el objeto ostream (cout) es pasado por re-
ferencia, ya que el flujo de salida debe ser modificado como resultado del propio acto de
salida.
Solo queda por implementar la sobrecarga en complejo.cc, lo que podra ser de la forma:
ostream & operator << (ostream & os,Complejo c){
os << c.get_real() << " + " << c.get_imag() << "i";
return os;
}
Con esto logramos finalmente que el codigo:
Complejo z = Complejo(-3.6,4.75);
cout << z << endl;
sea valido, dando como resultado la salida en pantalla:
-3.6 + 4.75i
Notemos que tenemos completa libertad para formatear la salida a pantalla del modo que
nos parezca. Si decidieramos que la salida debe ser en la forma de par ordenado, basta con
cambiar la implementacion de la sobrecarga de << adecuadamente.
De modo analogo se pueden definir sobrecargas para el operador de entrada, >> (hay
que reemplazar el tipo ostream por istream, y las sobrecargas para entrada y salida desde
archivos, en cuyo caso se pueden usar los tipos ofstream y ifstream segun corresponda.

1.10. Algunas clases utiles


1.10.1. string
En la Sec. 1.4.4 mostramos que existe el tipo de variable string, que permite manejar
arreglos de caracteres de modo muy conveniente. Ahora que tenemos el lenguaje adecuado,
podemos advertir que este tipo de variable corresponde a una clase string, definida en el
header <string>. Por una parte, tiene una funcionalidad parecida a las matrices de caracteres,
en el sentido que permite alojar texto, y los caracteres individuales se pueden extraer como
si fuera un vector:
string palabra="ejemplo";
string c = palabra[3];
58 CAPITULO 1. UNA BREVE INTRODUCCION A C++

El resultado sera que c contendra "m" (como con los otros vectores de C++ , el primer elemento
corresponde al ndice 0.
Pero ademas hay muchas otras caractersticas que no poseen las matrices de caracteres
usuales. De acuerdo a lo visto en la Sec. 1.4.4, existen sobrecargas de los operadores << y
>> para objetos tipo string. Pero tambien existen numerosas otras sobrecargas y funciones
utiles. Por ejemplo, se pueden sumar dos string:

string texto1 = "Primera parte";


string texto2 = "Segunda parte";
cout << texto1 + ", " + texto2 << endl;

El resultado en pantalla sera

Primera parte, Segunda parte

Tambien existen funciones miembro de la clase que permiten extraer texto desde una
posicion hasta otra determinada, encontrar la primera aparicion de un caracter en un string,
o la ultima:

string texto = "Un texto de prueba";


string texto1 = texto.substr(6,4);
string texto2 = texto.find("e");
string texto3 = texto.rfind("e");

En el codigo anterior, texto1 sera igual a "to d": 4 caracteres a partir de la posicion 6,
ocupada por "t"; texto2 sera igual a 4 (la posicion de la "e" en "texto"); y texto3 sera igual
a 15 (la posicion de la "e" en "prueba").
Ademas, la clase proporciona diversas funciones utiles para manejar palabras (convertir
todas las letras en mayusculas, contar el numero de caracteres, etc.).
Una funcion interesante, en particular, es la que permite ingresar lneas completas:

string texto;
cout << "Ingrese una frase completa: " << endl;
getline(cin,texto);

Como resultado de este codigo, la variable texto contendra el texto que el usuario ingrese
hasta que pulse Enter. Esto permite resolver el problema de que el flujo de datos de entrada
y salida siempre esta delimitado por espacios en blanco, lo cual hara que el siguiente codigo

string texto;
cout << "Ingrese una frase completa: " << endl;
cin >> texto;

deje en texto solo una palabra, ya que el flujo se cortara al encontrar el primer espacio en
el input.
1.11. TEMPLATES 59

1.10.2. complex
En la Sec. 1.9 hemos ilustrado la utilidad de las clases en C++ mediante la construccion de
una clase para numeros complejos. En realidad, en la librera estandar de C++ s existe una
clase para numeros complejos, con toda la funcionalidad que necesitamos. Esta contenida en
el header complex, y el siguiente codigo muestra algunos ejemplos de su uso:

#include<iostream>
#include<cmath>
#include<complex>

using namespace std;

int main () {
complex<double> z(-3.6,4.75);
complex<double> r = 2.0+z;
cout << r << endl;
cout << sqrt(r) << endl;
cout << sin(z*r) << endl;

return 0;
}

Inicialmente se define un complejo z, con parte real igual a -3.6 y parte imaginaria 4.75.
La sintaxis es similar a la que nosotros usamos: un constructor con dos argumentos (ver
Sec. 1.9.5. Luego, construimos un segundo complejo r, que es el anterior mas el numero real
2 (el segundo parametro del constructor tiene un valor default igual a cero, ver Sec. 1.9.11; y
el operador de suma esta sobrecargado para complejos, ver Sec. 1.9.10). Finalmente, diversas
funciones matematicas estan ya sobrecargadas para los complejos (ver Sec. 1.9.9).

1.11. Templates
La sintaxis presentada en la Sec. 1.10.2 es curiosa, porque no habamos visto antes, salvo
al incluir headers, el uso de <...>. En realidad, la clase complex es un ejemplo de una tecnica
permitida por C++ , que es el uso de templates. En esta seccion revisaremos algunas de sus
caractersticas y su utilidad.

1.11.1. Funciones
Supongamos que queremos implementar una funcion para intercambiar entre s dos va-
riables, tal como lo describimos en la Sec. 1.3.5. Si las variables son, digamos, tipo int, un
modo de hacerlo sera el siguiente:

void intercambio(int & a, int & b){


int temp=a;
a=b;
60 CAPITULO 1. UNA BREVE INTRODUCCION A C++

b=temp;
}
Que pasa ahora si queremos intercambiar dos variables tipo double? Tendramos que du-
plicar el codigo para sobrecargar la funcion, y el codigo final podra quedar as:
#include <iostream>

using namespace std;

void intercambio(int &, int &);


void intercambio(double &, double &);

int main(){
int i=3, j=5;
intercambio(i,j);

cout << i << " " << j << endl;

double r=3.5, s=10.9;


intercambio(r,s);

cout << r << " " << s << endl;

return 0;
}

void intercambio(int & a, int & b){


int temp=a;
a=b;
b=temp;
}

void intercambio(double & a, double & b){


double temp=a;
a=b;
b=temp;
}
Hay algo claramente no eficiente en este codigo, ya que el cuerpo de ambas funciones es
exactamente el mismo. Lo unico diferente es el tipo de las variables.
La solucion para este problema es el uso de templates: son funciones abstractas, definidas
para un tipo de variable generico. El template adecuado para nuestro problema es el siguiente:
template <class T>
void intercambio(T & a, T & b){
T temp=a;
1.11. TEMPLATES 61

a=b;
b=temp;
}

La estructura es la misma que antes, pero ahora el tipo de variable no es ni int, ni double,
ni ningun tipo conocido en particular, sino uno generico que llamamos (arbitrariamente, solo
por simplicidad) T. Para usar esta funcion con variables tipo int (digamos i y j), basta con
llamarla de esta forma:

intercambio<int>(i,j);

El codigo completo que utiliza ambas versiones, para int y double, sera:

#include <iostream>

using namespace std;

template <class T>


void intercambio(T & a, T & b){
T temp=a;
a=b;
b=temp;
}

int main(){
int i=3, j=5;
intercambio<int>(i,j);

cout << i << " " << j << endl;

double r=3.5, s=10.9;


intercambio<double>(r,s);

cout << r << " " << s << endl;

return 0;
}

Esto es muy util, porque de hecho, por el solo hecho de haberla definido como template, la
funcion se puede utilizar con cualquier tipo de variable conocido por nuestro codigo, no solo
por los dos en los que habamos pensado originalmente. Por ejemplo, podemos intercambiar
dos numeros complejos z y w:

intercambio<complex<double> > (z,w);

(El unico cuidado que hay que tener es no olvidar el espacio entre el > de complex y el > de
intercambio, ya que eso sera interpretado como el operador >>, que tiene otro significado.)
62 CAPITULO 1. UNA BREVE INTRODUCCION A C++

Aparte de anteponer template <class T>, la otra diferencia con las declaraciones usuales
de funciones es que la declaracion no puede ir separada de la implementacion en templates.
esa es la razon por la cual, mientras en el codigo de la pag. 60 la implementacion se puso
despues de main, en el codigo con template, en la pag. 61 no fue as.
La razon de este comportamiento inusual es que, como hemos indicado antes, al intentar
usar una funcion en el codigo, el compilador requiere la declaracion para revisar consistencia
de tipos de retorno y argumentos. Cuando se trata de un template, en cambio, esa revision
es imposible, precisamente porque es generica, y en definitiva se requiere la implementacion
explcito de la funcion para que se pueda insertar el codigo adecuado en el punto en que la
funcion es llamada, y la compilacion sea exitosa.
Una consecuencia adicional de esto es que, si se desea colocar la definicion de templates
en header files, no se puede poner la declaracion en el archivo .h y la implementacion en el
.cc correspondiente; todo debe estar en el .h.

1.11.2. Clases
Tambien es posible definir templates para clases. Digamos que necesitamos una clase
Punto, que aloja las coordenadas cartesianas (un par ordenado) de un punto en el espacio.
Podramos tener algo basico con este codigo, si las coordenadas fueran int:

class Punto{
private:
int x;
int y;
public:
void set_x(int);
void set_y(int);
int get_x();
int get_y();
};

El mismo problema de la Sec. 1.11.1 aparece: si quisieramos ahora un Punto con coor-
denadas double, deberamos crear una nueva clase, y escribir las implementaciones de las
funciones correspondientes.
Con templates, dicho trabajo es innecesario. El siguiente codigo muestra como sera la
clase Punto definida como un template, junto con las implementaciones de todas sus funciones
miembro, y una sobrecarga para la salida a pantalla. Digamos que este codigo esta contenido
en un archivo punto.h:

//----------Archivo punto.h----------
#ifndef PUNTO_H
#define PUNTO_H

#include <iostream>

using namespace std;


1.11. TEMPLATES 63

template <class T>


class Punto{
private:
T x;
T y;
public:
void set_x(T);
void set_y(T);
T get_x();
T get_y();
};

template <class T>


ostream & operator << (ostream & os, Punto<T> p){
os << "[" << p.get_x() << ", " << p.get_y() << "]";
return os;
}

template <class T>


void Punto<T>::set_x(T a){
x=a;
}

template <class T>


void Punto<T>::set_y(T a){
y=a;
}

template <class T>


T Punto<T>::get_x(){
return x;
}

template <class T>


T Punto<T>::get_y(){
return y;
}
Dos observaciones:
Si la clase es declarada como template (con un tipo generico T en nuestro ejemplo)
todas las funciones miembros que necesiten, ya sea como tipo de retorno o como alguno
de sus argumentos, el tipo T, deben ser tambien declaradas como templates.
Las implementaciones de las funciones y las sobrecargas deben referirse a la clase como
Punto<T>.
64 CAPITULO 1. UNA BREVE INTRODUCCION A C++

El siguiente ejemplo muestra como se usara la clase Punto, tanto con int como double:

#include <iostream>
#include "punto.h"

using namespace std;

int main(){
int i=3, j=5;

Punto<int> p_i;
p_i.set_x(i);
p_i.set_y(j);

cout << "Punto de enteros: " << p_i << endl;

double r=3.6, s=-5e3;

Punto<double> p_d;
p_d.set_x(r);
p_d.set_y(s);

cout << "Punto de dobles: " << p_d << endl;

return 0;
}

Del ejemplo anterior podemos entender que la clase de complejos definida en <complex> es
en realidad un template, y cuando declaramos un numero complejo como complex<double>,
estamos indicando que usaremos dicha clase con partes real e imaginaria dadas por variables
tipo double, pero del mismo modo podramos tener variables int o float, por ejemplo, y las
declaraciones correspondientes seran complex<int> y complex<float>, respectivamente.
El output del programa anterior es:

[3, 5]
[3.6, -5000]

La primera lnea es un punto con coordenadas enteras, la segunda un punto con coordenadas
reales.
Pero, como dijimos, lo interesante es que, habiendo definido esta clase como un template,
esta habilitada para funcionar con coordenadas de cualquier tipo previamente definido. Por
ejemplo, teniendo nuestra clase de numeros complejos, podramos crear, con el mismo codigo,
una clase de puntos con coordenadas complejas:

complex<double> r_c(3.6,2);
complex<double> s_c(-5e3,-3);
1.11. TEMPLATES 65

Punto<complex<double> > p_c;


p_c.set_x(r_c);
p_c.set_y(s_c);

cout << p_c << endl;

que tendra como salida a pantalla:

[(3.6,2), (-5000,-3)]

O incluso una clase de puntos, en que sus coordenadas sean, a su vez, puntos:

double r=3.6, s=-5e3;

Punto<double> p_d;
p_d.set_x(r);
p_d.set_y(s);

Punto<double> p_d2;
p_d2.set_x(s);
p_d2.set_y(r);

Punto<Punto<double> > p_p;


p_p.set_x(p_d);
p_p.set_y(p_d2);

cout << p_p << endl;

cuyo output sera:

[[3.6, -5000], [-5000, 3.6]]

1.11.3. Standard Template Library (STL)


Aprovechando el poder de los templates, las instalaciones normales de C++ incluyen una
librera estadar de templates (Standard Template Library, o STL. Destacamos algunas de las
clases disponibles en la STL, y algunas de sus funciones miembros mas utiles. Existen muchas
clases y funciones disponibles, cuyo uso se puede consultar en la documentacion respectiva.

vector
Funcionalidad similar a los arreglos de C++ (Sec. 1.4), pero con muchas propiedades adi-
cionales muy utiles. Para usarlos, se debe incluir el header <vector>. En su forma mas simple,
se declaran y se usan de modo analogo a los arreglos usuales:
66 CAPITULO 1. UNA BREVE INTRODUCCION A C++

#include<iostream>
#include<vector>

using namespace std;

int main () {
vector<int> w(5);

for(int i=0;i<5;i++){
w[i]=i*i;
}

for(int i=0;i<5;i++){
cout << w[i] << " ";
}
cout << endl;

return 0;
}
Primero se define un vector de variables int, de dimension 5. Luego cada elemento se llena
y se enva a pantalla. Para acceder al elemento i-esimo, basta con escribir w[i].
Sin embargo, a diferencia de los arreglos usuales, un vector tiene dimension variable. As,
por ejemplo, se pueden agregar elementos al final de un vector preexistente con la funcion
miembro push_back():
vector<int> w(5);

for(int i=0;i<5;i++){
w[i]=i*i;
}

w.push_back(-10);
Al ejecutar este codigo, los elementos de w son:
0 1 4 9 16 -10
Esto permite, por ejemplo, partir con un vector vaco, e irle agregando elementos progre-
sivamente:
vector<int> w;

for(int i=0;i<5;i++){
w.push_back(i*i);
}
Tambien se puede eliminar el ultimo elemento de un vector, con pop_back():
1.11. TEMPLATES 67

vector<int> w;

for(int i=0;i<5;i++){
w.push_back(i*i);
}

w.pop_back();
Al ejecutar este codigo, los elementos de w son:
0 1 4 9
De modo analogo, existen las funciones push_front() y pop_front() que agregan o
quitan elementos del comienzo del vector.
A diferencia de los arreglos usuales, el largo de un vector s se puede saber, gracias a la
funcion size(). Por ejemplo:
int n = w.size();
cout << w.size() << endl;
La funcion unique() (disponible al incluir el header <algorithm>), por su parte, permite
crear un nuevo vector que tiene los elementos de otro vector, pero eliminando todos los
duplicados. Por ejemplo, en el siguiente codigo creamos un vector con dos elementos repetidos,
y, con unique, otro sin dichos duplicados:
vector<int> w(5);

for(int i=0;i<5;i++){
w[i]=i*i;
}

w.push_back(4);

v=unique(w);
Con este codigo, v y w tienen, respectivamente, los elementos:
0 1 4 9 9
0 1 4 9
La funcion clear() permite borrar todos los elementos de un vector:
vector<int> w(5);

for(int i=0;i<5;i++){
w[i]=i*i;
}

int n=w.size();
68 CAPITULO 1. UNA BREVE INTRODUCCION A C++

w.clear();

int m=w.size();

Con este codigo, las variables n y m son 5 y 0, respectivamente.

list
Existe tambien otro tipo de objeto, llamado list, definido en el header <list>. Es pa-
recido a vector, y tiene muchas funciones iguales de hecho. La gran diferencia es que los
elementos individuales no se accesan con los parentesis [], sino a traves de objetos auxiliares
llamados iteradores. En el siguiente ejemplo, creamos una lista vaca, y la llenamos, usando
push_back(), con los cuadrados de los primeros 5 numeros enteros. Hasta este punto, el
codigo es el mismo que con vector. Pero a continuacion, se muestran los elementos de la
lista en pantalla, y el codigo es muy diferente al que usaramos con vector:

#include<iostream>
#include<list>

using namespace std;

int main () {
list<int> v;

for(int i=0;i<5;i++){
v.push_back(i*i);
}

for(list<int>::iterator it=v.begin();it!=v.end();it++){
cout << *it << endl;
}

return 0;
}

Para entender la logica de esto, recordemos que los arreglos en C++ estan alojados en
zonas contiguas de memoria. Para los arreglos usuales y los objetos vector, esas zonas de
memoria se recorren con un numero entero. Las listas, en cambio, son objetos mas abstrac-
tos, y se deben recorrer con un iterador. Como las listas son templates, los elementos de una
lista podran ser de cualquier tipo de variable, y por tanto se requiere un iterador asociado
a cada lista posible, con cada tipo posible de variable. Por eso, el iterador no se denomi-
na simplemente iterator, sino que list<int>::iterator (un iterador para una lista de
enteros).
Podemos imaginarnos un iterador como un puntero, que indica la direccion de memoria
que aloja a cada elemento de la lista. Existen algunos iteradores especiales: v.begin() es el
1.11. TEMPLATES 69

iterador (puntero) asociado al primer elemento de v, y v.end() es el iterador asociado al


ultimo elemento de v. Para avanzar en la lista, se incrementa el iterador en uno.
Todo lo anterior nos permite entender el ciclo for en el codigo anterior: definimos un
iterador para listas de enteros (list<int>::iterator it), y partimos definiendolo como el
comienzo de la lista (it=v.begin()); luego lo incrementamos en uno (it++), para recorrer
todos los elementos de la lista, y el ciclo se detiene cuando el iterador llega al final de la lista
(it!=v.end()).
Una vez posicionados en un elemento de la lista, usamos el operador de derreferencia (ver
Sec. 1.5) para acceder al elemento alojado en dicha posicion (*it).
As como push_back(), las funciones pop_back(), push_front(), pop_front() y muchas
otras que estan definidas para vectores, estan tambien disponibles para listas.

map
Un tercer objeto que queremos destacar por su utilidad es map, disponible con el header
<map>. map es una estructura que asocia un valor a una cierta clave (key). Supongamos que
tenemos las direcciones electronicas de nuestros amigos, a los cuales identificamos con apodos:
pedro -> pedro@servidor1.com
juan -> juan@servidor2.com
diego -> diego@servidor3.com
Podemos construir y usar un map con estos datos de la siguiente forma:

#include<iostream>
#include<map>
#include<string>

using namespace std;

int main () {
map<string,string> v;

v.insert(pair<string,string>("pedro","pedro@servidor1.com"));
v.insert(pair<string,string>("juan","juan@servidor2.com"));
v.insert(pair<string,string>("diego","diego@servidor3.com"));

map<string,string>::iterator it = v.find("juan");

cout << (*it).first << endl;


cout << (*it).second << endl;

return 0;
}
Primero, declaramos el map como un template. Cada elemento del map tiene dos elemen-
tos: el key, ("pedro", "juan", "diego"), que es un string, y el valor asociado a ese key (las
70 CAPITULO 1. UNA BREVE INTRODUCCION A C++

correspondientes direcciones electronicas), que tambien son string. Cada uno de estos ele-
mentos, entonces, es un par, formado por dos objetos tipo string. Constuimos entonces un
par con otro objeto, pair, que tambien es un template: pair<string,string> (notemos,
de paso, que un template no tiene por que ser de la forma clase<tipo>, sino que puede estar
definido en terminos de varios tipos, clase<tipo1,tipo2,...>). Finalmente, agregamos
este pair al map, con la funcion miembro insert.
A continuacion buscamos un elemento. Queremos encontrar la direccion de juan. Lo
hacemos con la funcion miembro find. Notemos que find no entrega la direccion electronica
en s, sino que entrega un iterador (puntero) a la direccion de memoria donde esa informacion
se encuentra (analogamente a los iteradores de los objetos tipo list, de la Sec. 1.11.3). En esa
direccion se encuentran, por supuesto, dos datos: el key ("pedro"), y la direccion electronica,
de modo que el iterador, a diferencia de lo que sucede con las listas, debe ser un template
con dos tipos. En este caso, ambos elementos son objetos tipo string, y por eso el iterador
es del tipo map<string,string>::iterator.
Finalmente, teniendo la direccion de memoria podemos acceder al valor de las variables
contenidas en ella. Igual que en la Sec. 1.11.3, lo hacemos con *it. Pero ahora este es un
elemento de un mapa, con dos variables (un alias y una direccion), as que hay que distinguir
entre ambos. Eso se logra con dos variables miembros, first y second, que contienen, por
supuesto, el primer y el segundo elemento, respectivamente. Por lo tanto, (*it).first tiene
el valor del key, e (*it).second la direccion electronica asociada.
Observemos que tenemos, por primera vez, una situacion en que un iterador (o un pun-
tero), es en realidad un objeto que tiene variables miembros (a diferencia de lo que suceda
en las Secs. 1.5 o 1.11.3). En general, cuando un iterador (o puntero) p tiene una variable
miembro m, la notacion usual (*p).m se puede reemplazar por p->m. Por lo tanto, las ultimas
dos lneas del codigo anterior podran haber sido:

cout << it->first << endl;


cout << it->second << endl;

Finalmente, si deseamos borrar un elemento del mapa (por ejemplo, la direccion de


"pedro"), usamos la funcion miembro erase. En el caso del codigo anterior:

v.erase("pedro");

Los objetos tipo map tienen la restriccion, comprensible, de que todos los keys deben ser
unicos. Si tenemos un libro de direcciones, nos interesa que "pedro" tenga un significado
unico. En otras aplicaciones, sin embargo, podramos necesitar un map que permita keys
duplicados. Eso existe, pero corresponde a un template distinto, multimap.

1.12. Herencia
Herencia es el mecanismo mediante el cual es posible definir clases a partir de otras,
preservando parte de las propiedades de la primera y agregando o modificando otras.
Lo importante de esta tecnica es que nos evita repetir codigo. Por ejemplo, digamos que
definimos un objeto tipo Poligono. Todo polgono tiene un cierto numero de lados, cada
1.12. HERENCIA 71

uno de cierta longitud. Si quisieramos definir luego un objeto tipo Triangulo necesitaramos
la misma informacion. Pero ademas un triangulo tiene caractersticas adicionales que no
todos los polgonos tienen. Por ejemplo, tiene transversales de gravedad. Entonces, al definir
Triangulo, es vez de definir todas sus variables y funciones miembro desde cero, podemos
hacer que herede propiedades de una clase madre (Poligono), y agregarle el resto de las
propiedades que consideremos necesarias. De este modo, no tenemos que escribir codigo
repetido, evitando la aparicion de nuevos errores o inconsistencias, y haciendo mas facil
la mantencion del codigo, ya que si necesitamos cambiar algo que sea comun a todos los
polgonos, basta con editar el codigo de la clase madre.
Desarrollemos el ejemplo anterior con un codigo muy elemental, para entender como se
implementa la herencia en C++ .
El siguiente codigo crea primero una clase Poligono, que tiene dos variables miembro, una
que es el numero de lados (int) y la otra que es la magnitud de su permetro (double). Tiene
un constructor que asigna valores a ambas variables, y funciones miembros para obtener sus
valores desde fuera de la clase:

class Poligono{
private:
int n_lados;
double perimetro;
public:
Poligono(int,double);
~Poligono();
int get_lados();
double get_perimetro();
};

Poligono::Poligono(int n,double p):n_lados(n),perimetro(p){


}

Poligono::~Poligono(){
}

int Poligono::get_lados(){
return n_lados;
}

double Poligono::get_perimetro(){
return perimetro;
}

Ahora definimos una clase Triangulo, heredada desde Poligono. Tendra las mismas
variables y funciones que Poligono, con una salvedad: el numero de lados esta fijo en 3. El
siguiente codigo lo logra:

class Triangulo: public Poligono {


72 CAPITULO 1. UNA BREVE INTRODUCCION A C++

public:
Triangulo(double);
};

Triangulo::Triangulo(double p):Poligono(3,p){
}

La primera lnea dice que Triangulo es una clase heredada desde Poligono (heredada de
forma publica discutiremos eso mas adelante, en la Sec. 1.12.1). No hay ninguna variable ni
funcion declarada en Triangulo, porque no es necesario. Lo unico que tenemos que declarar
especficamente es el constructor, porque los triangulos son polgonos de tres lados, pero
obviamente el permetro esta indeterminado. El constructor tiene una sola variable, tipo
double, y observamos en el codigo anterior que el constructor de Triangulo invoca, a su vez,
al constructor de Poligono, con el primer parametro igual a 3.
Y eso es todo. En lo sucesivo, podemos construir polgonos o triangulos, y pedirles su
permetro o su numero de lados indistintamente. Pongamos todo esto en un unico codigo:
#include<iostream>

using namespace std;

class Poligono{
private:
int n_lados;
double perimetro;
public:
Poligono(int,double);
~Poligono();
int get_lados();
double get_perimetro();
};

Poligono::Poligono(int n,double p):n_lados(n),perimetro(p){


}

Poligono::~Poligono(){
}

int Poligono::get_lados(){
return n_lados;
}

double Poligono::get_perimetro(){
return perimetro;
}
1.12. HERENCIA 73

class Triangulo: public Poligono {


public:
Triangulo(double);
};

Triangulo::Triangulo(double p):Poligono(3,p){
}

int main () {
Poligono p(3,63.6);

cout << p.get_lados() << " " << p.get_perimetro() << endl;

Triangulo t(3.5);

cout << t.get_lados() << " " << t.get_perimetro() << endl;

return 0;
}
Como indicamos anteriormente, a la clase Triangulo se le pueden agregar otras propie-
dades, que solo le pertenecen a ella, y no a la clase madre. Por ejemplo, podramos definir
una funcion que entregue la longitud de las alturas o de las transversales de gravedad, trazos
que solo tienen sentido para un triangulo, pero no para un polgono en general.
Consideremos como ilustracion la siguiente modificacion a la definicion de la clase Triangulo:
class Triangulo: public Poligono {
public:
Triangulo(double);
~Triangulo();
double get_promedio_longitud_lados();
};

double Triangulo::get_promedio_longitud_lados(){
return perimetro/n_lados;
}
Hemos agregado a Triangulo una funcion que entrega el promedio de longitud de los
lados. Como la definimos para Triangulo, en el siguiente codigo:
Poligono p(3,63.6);
Triangulo t(3.5);

cout << p.get_promedio_longitud_lados() << endl;


cout << t.get_promedio_longitud_lados() << endl;
74 CAPITULO 1. UNA BREVE INTRODUCCION A C++

la cuarta lnea no tiene sentido y la siguiente s.


Desafortunadamente, si tratamos de compilar un codigo como el anterior, incluyendo la
nueva funcion para Triangulo, no va a compilar . La razon es que, en la implementacion de
esta nueva funcion, se necesitan las variables perimetro y n_lados, pero ninguna de las dos
aparece en la declaracion de dicha clase. La razon es que las variables eran privadas en la
clase madre, y en eso C++ es estricto: ni siquiera las clases hijas tienen acceso a ellas.
Como solucionamos el problema? Un modo sera hacer las variables publicas, pero con
eso destruiramos todo el concepto de encapsular codigo, todas las buenas razones por las
cuales es necesario que haya miembros publicos y privados en una clase. Necesitamos poder
hacer herencia de clases, incluyendo herencia de miembros de la clase madre, manteniendo
la posibilidad de que haya miembros accesibles desde el exterior, y otros que no.
Esto se soluciona agregando una palabra adicional a nuestro lenguaje: protected.

1.12.1. public, private, protected


En realidad, no son dos, como indicamos en la Sec. 1.9.3, sino tres los tipos de miembros
que puede tener una clase: public, private y protected. Los miembros protegidos de una
clase funcionan como miembros privados: no son accesibles desde el exterior. Pero se com-
portan de manera distinta ante la herencia: mientras los miembros privados no son accesibles
desde una clase derivada, los miembros protegidos s son accesibles, y quedan como variables
protegidas de la clase derivada.
Por lo tanto, el codigo que s compilara sera el siguiente:

#include<iostream>

using namespace std;

class Poligono{
protected:
int n_lados;
double perimetro;
public:
Poligono(int,double);
~Poligono();
int get_lados();
double get_perimetro();
};

Poligono::Poligono(int n,double p):n_lados(n),perimetro(p){


}

Poligono::~Poligono(){
}

int Poligono::get_lados(){
1.12. HERENCIA 75

return n_lados;
}

double Poligono::get_perimetro(){
return perimetro;
}

class Triangulo: public Poligono {


public:
Triangulo(double);
double get_promedio_longitud_lados();
};

Triangulo::Triangulo(double p):Poligono(3,p){
}

double Triangulo::get_promedio_longitud_lados(){
return perimetro/n_lados;
}

int main () {
Poligono p(3,63.6);

cout << p.get_lados() << " " << p.get_perimetro() << endl;

Triangulo t(3.5);

cout << t.get_lados() << " " << t.get_perimetro() << endl;

cout << t.get_promedio_longitud_lados() << endl;

return 0;
}

El unico cambio respecto al codigo anterior es que las variables que eran privadas en
Poligono, ahora son protegidas. Para los objetos tipo Poligono esto no hace ninguna dife-
rencia, pero para los tipo Triangulo s, porque les son accesibles.
El comportamiento anterior se puede modificar cambiando el tipo de herencia. Observa-
mos anteriormente que, al heredar la clase Triangulo desde Poligono, insertamos la palabra
public. Eso significa que la herencia es publica, y es la razon por la cual los miembros he-
redados se comportan como hemos indicado. Y al igual que para los miembros de una clase,
existe una herencia publica, privada y protegida. La siguiente tabla muestra como se heredan
los distintos tipos de miembros segun el tipo de herencia:
76 CAPITULO 1. UNA BREVE INTRODUCCION A C++

Herencia public protected private


public public protected
protected protected protected
private private private

Como vemos, variando el tipo de los miembros y de la herencia, es posible controlar


que variables se heredan y como a las clases hijas, nietas, etc.

1.12.2. Herencia multiple


Tambien es posible derivar clases a partir de varias clases, permitiendo que una clase
herede propiedades de dos o mas clases distintas.
Siguiendo con nuestro ejemplo con polgonos, digamos que queremos crear una clase
Lamina, que represente a un objeto real, un polgono hecho de cierto material, con cierta
masa. Podramos crear una clase Cuerpo, cuya unica propiedad es su masa (si quisieramos
agregar el material del que esta hecho, podemos agregar una variable tipo string para iden-
tificarlo):
class Cuerpo{
protected:
double masa;
public:
Cuerpo(double);
double get_masa();
};

Cuerpo::Cuerpo(double m):masa(m){
}

double Cuerpo::get_masa(){
return masa;
}
Ahora podemos crear una clase Lamina, derivada simultaneamente de Poligono y Lamina,
y que por lo tanto tendra un cierto numero de lados, un permetro y masa:
class Lamina: public Poligono, public Cuerpo{
public:
Lamina(int,double,double);
};

Lamina::Lamina(int n,double p,double m):Poligono(n,p),Cuerpo(m){


}
La herencia multiple simplemente se hace indicando mas clases base en la declaracion de
la clase, y en el constructor de la nueva clase hay que indicar como se usan los constructores
de las distintas clases base a partir de los argumentos de la clase derivada (en el ejemplo,
1.13. DEBUGGING 77

los dos primeros parametros del constructor de la clase derivada se usan para Poligono, y el
tercero para Cuerpo).
La herencia desde cada clase base puede ser publica, privada o protegida. En este caso,
Lamina es heredada de forma publica desde ambas clases base.

1.13. Debugging
Esta seccion y la siguiente no estan dedicadas a un aspecto particular de C++ , sino
a tecnicas de desarrollo de software que pueden ser muy utiles para utilizar con cualquier
lenguaje de programacion.
En esta seccion, en particular, revisaremos brevemente el uso de un debugger , que es
un software especializado para encontrar errores (bugs) en un codigo. Notemos que estamos
hablando de errores de ejecucion: tenemos un codigo que compila, pero que no hace lo que
debera, se cae porque agota la memoria disponible o porque encuentra una division por cero,
cae en un loop infinito, etc. Creemos que un buen programador debera manejar el uso de
algun debugger , que le permitira detectar rapidamente estos errores y otros, particularmente
en programas complejos.
Como ejemplo, utilizaremos el programa gdb (GNU Debugger ), que se maneja desde la
lnea de comando.
El debugger es un software que mira el ejecutable, y en principio es posible correr un
debugger incluso si no se tiene el codigo fuente. Sin embargo, los mensajes del debugger no
seran muy utiles, porque, por ejemplo, no sabra a que lnea del codigo fuente corresponde
una determinada porcion del ejecutable. Por ello, es mejor compilar el ejecutable de modo
que incorpore esa informacion. Al usar g\+\+, eso se logra con el flag -c:

g++ -g hola.cc -o hola

El ejecutable resultante sera en general mas lento que el normal, porque tiene toda esta
informacion extra sobre el codigo fuente. Por ello, las compilaciones con -g se hacen mientras
se esta desarrollando el software, y una vez que se tiene un programa sin errores, se hace una
compilacion final sin -g.
Una vez compilado as, podemos invocar el debugger en la siguiente forma, desde la lnea
de comandos:

gdb hola

El resultado sera un mensaje de la forma:

GNU gdb (GDB) 7.6 (Debian 7.6-5)


Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
78 CAPITULO 1. UNA BREVE INTRODUCCION A C++

<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from <path>/hola...done.
(gdb)
Y quedaremos en un shell , indicado por el prompt (gdb), esperando algun comando de
nuestra parte.
Entre las tareas que puede realizar un debugger estan:
Ejecutar el programa de la forma usual.

Ejecutar el programa, deteniendose en un punto determinado, y continuar luego a partir


de el.

Ejecutar el programa lnea por lnea

Examinar el valor de las variables usadas en el programa.

Entrar (o no) selectivamente a funciones para examinar la ejecucion de cada una de ellas.
Algunos de los comandos disponibles para estas tareas:
quit Salir del debugger.

run Ejecuta el programa que se esta depurando (hola, en este caso). Se vera todo el output
normal del programa, con un mensaje final del tipo
"[Inferior 1 (process 4317) exited normally]" si todo termino normalmente. Si
hubo un error, el mensaje contendra informacion sobre el error, incluyendo la lnea del
archivo fuente en el cual esa instruccion se encuentra. Esto permite encontrar rapida-
mente la fuente de los problemas mas sencillos.

break Agrega un punto de detencion (breakpoint en una lnea o al comienzo de una funcion
determinada del programa. Esto es muy util para determinar si el problema ocurre
antes o despues de determinada lnea, para examinar con cuidado lo que sucede en
cierta seccion del codigo, etc.
Por ejemplo, break hola.cc:main pone un breakpoint al comienzo de la funcion
main(). Esto hara que cuando se de run, el programa se detendra de inmediato (porque
main es la primera lnea en ser ejecutada).
break hola.cc:20 pone un breakpoint en la lnea 20 de hola.cc

cont Continua la ejecucion luego de encontrado un breakpoint, hasta que encuentre el proxi-
mo breakpoint.

next Ejecuta la siguiente lnea en el codigo fuente. Si el debugger acaba de detenerse en la


lnea 20 porque encontro un breakpoint, ejecutara solo la lnea 21. Otro next ejecutara la
lnea 22, y as sucesivamente.

finish Continua la ejecucion hasta que sale de la funcion en la que actualmente se encuentra.
Por ejemplo, si hemos puesto un breakpoint en una lnea que esta dentro de una funcion
f1(), finish continuara a partir de ese breakpoint hasta que pueda salir de la funcion.
1.14. MAKE 79

print Imprime el valor de una variable. Ejemplo: print p.


help Ayuda sobre el uso de gdb. help <comando> proporciona ayuda sobre <comando>.
Un concepto importante para entender como funciona un debugger es el de frame. De
alguna manera, podemos pensar un frame como una funcion, que puede llamar a otras fun-
ciones. Cada vez que el debugger encuentra una funcion, queda en un frame determinado,
y si esa funcion llama a otras va creando sucesivos frames que van quedando anidados. La
funcion main() es el frame mas exterior.
Esto es util de saber porque, en general, los programas que se estan depurando pueden
ser muy complejos. Por ejemplo, pensemos en un codigo que desde main() llama a una
funcion f1, la cual a su vez llama a una funcion f2, y esta a una funcion f3. Digamos que
gdb detecta un error en una lnea correspondiente a f3. gdb se detendra ah y terminara la
ejecucion del programa. Lo interesante es que, aunque la ejecucion termino, gdb todava tiene
la informacion sobre el ejecutable en memoria, entonces uno puede examinar el valor de las
variables que estan visibles en ese momento (con print). Y eso quizas le de a uno una clave
de que es lo que paso (Ah, p = 125, con razon se cayo el programa!).
Pero puede que no sea suficiente informacion, y sabemos que p = 125 cuando el programa
se cayo, pero no entendemos porque tomo ese valor. Entonces quizas debamos subir un nivel,
para saber como fue llamada f3. Lo hacemos con el comando up. Esto lleva al debugger al
interior de la funcion f2, en el punto en que f3 fue llamada. Podemos entonces examinar
detalladamente lo que estaba pasando en el codigo cuando f3 fue llamada. Si queremos volver
al frame inferior, usamos down.
Los comandos up y down, en conjunto con comandos como print, por ejemplo, pueden
ser de extraordinaria ayuda al momento de pesquisar errores en codigos complejos.

1.14. make
make es un comando particularmente util cuando se trata de hacer tareas repetitivas y que
involucran a un numero posiblemente grande de archivos distintos para producir un resultado
final. La idea es sencilla: dar el comando make significa, basicamente, decirle al sistema:
haga todo lo que sea necesario hacer para crear una version actualizada del resultado final,
considerando que algunos de los archivos necesarios para producirlo pueden haber cambiado
desde la ultima vez. A traves de algunos ejemplos simples veamos como es posible traducir
este mandato en instrucciones que un computador pueda entender.

1.14.1. Un ejemplo sencillo en C++


Consideremos un programa sencillo en C++, hola.cc que solo dice Hola:
#include <iostream>

using namespace std;

int main() {
cout << "Hola" << endl;
80 CAPITULO 1. UNA BREVE INTRODUCCION A C++

return 0;
}

El comando g++ -o hola hola.cc creara un ejecutable a partir de esta fuente. Por su-
puesto, al dar nuevamente el comando g++ -o hola hola.cc el proceso de compilacion
comenzara nuevamente. Esto no es muy eficiente, porque hola solo depende de hola.cc, y
no necesita ser recreado a menos que hola.cc sea modificado.
Usemos, en cambio, el comando make. Creemos el archivo hola.cc (si ya existe, modifi-
quemoslo agregandole un espacio en blanco, por ejemplo), y luego ejecutemos el comando:

user@hostname:~$ make hola

La siguiente lnea es escrita en el terminal:

g++ hola.cc -o hola

y un ejecutable hola es generado.


Ahora ejecutemos nuevamente make:

user@hostname:~$ make hola


make: hola is up to date.

Esta vez make no tiene ningun efecto. Esa es toda la gracia de make: darse cuenta de
que necesita ser recreado, porque sabe que resultado depende de que archivo fuente.

1.14.2. Creando un Makefile


En el ejemplo anterior, make detecto automaticamente que hola depende de hola.cc. En
realidad, lo que ocurre es que existen una serie de reglas predefinidas. En este caso, make
busco en el directorio actual un archivo del cual pudiera depender hola, encontro a hola.cc,
y ejecuto g++ con las opciones adecuadas. Pero si hubiera encontrado un hola.f, hubiera
entendido que deba ejecutar el compilador de Fortran. A veces estas reglas son insuficientes y
uno debe decirle a make explcitamente como proceder con cada dependencia. O simplemente
no deseamos confiar en el azar. Cualquiera que sea el caso, podemos extender o modificar las
reglas de make usando un Makefile.
La estrategia, nuevamente, es sencilla: cree en el directorio de trabajo un archivo Makefile,
con las instrucciones necesarias (en este apendice hay algunos ejemplos). Cuando make sea
ejecutado, leera este archivo y procedera en consecuencia.
El siguiente es un Makefile para el ejemplo sencillo de la Sec. 1.14.1:

hola: hola.cc
g++ -o $@ $<

La sintaxis es la siguiente:

hola: es un target (blanco). Indica que la siguiente regla sirve para generar el archivo
hola.
1.14. MAKE 81

Luego de los dos puntos, vienen las dependencias de hola. En este caso, solo es hola.cc.

En la segunda lnea, viene la regla para generar hola. Aqu se han usado dos variables
predefinidas de make (hay muchas mas): $@ es el target de la regla, y $< es la dependencia
de la regla. En este caso, entonces, una vez que make reemplaza dichas variables, la regla
queda como g++ -o hola hola.cc, que es por supuesto lo que queremos.
Un punto muy importante: En la regla, antes de los caracteres g++, no hay 8 espacios
en blanco, sino un caracter de tabulacion, TAB. De hecho, si uno reemplazara ese TAB por
8 espacios, make se dara cuenta. La version 3.81 de GNU make, por ejemplo, entrega el
siguiente mensaje al intentar ejecutarlo:

user@localhost:~$ make
Makefile:2: *** missing separator (did you mean TAB instead of 8 spaces?). Stop.

Este Makefile hace exactamente lo mismo que en la Sec. 1.14.1, pero ahora no necesitamos
descansar en las dependencias default: make hola busca el target hola en el archivo Makefile
en el directorio actual, y revisa sus dependencias. Si alguna de las dependencias es mas
reciente que el archivo hola, entonces ejecuta las lneas que constituyen la regla. (En el
ejemplo analizado, la regla consta de una sola lnea, pero podra ser por supuesto mas de una
lnea si para generar el target se debe ejecutar mas de un comando.)
Observemos que, puesto que make toma decisiones solo en base a las fechas de modificacion
de los archivos, para probar que las reglas funcionan adecuadamente no es necesario editar
hola.cc. Basta con touch hola.cc, con lo cual la fuente aparecera como mas reciente que
el ejecutable.
En un mismo Makefile puede haber mas de una regla. Por ejemplo:

hola: hola.cc
g++ -o $@ $<

chao.dvi: chao.tex
latex $<

En este caso, ademas del ejecutable hola, podemos generar un archivo chao.dvi, compi-
lando con LATEX el archivo chao.tex. Es claro, entonces, que un solo Makefile nos permite
generar muchos archivos distintos, con una unica lnea de comando, del tipo make <target>.
Si se omite el argumento, make ejecuta el primer target que encuentra en el Makefile. Por
ello, puede ser util, dependiendo de la aplicacion, poner al principio un target que simplemente
genere todos los archivos necesarios:

all:
make hola
make chao

hola: hola.cc
g++ -o $@ $<
82 CAPITULO 1. UNA BREVE INTRODUCCION A C++

chao.dvi: chao.tex
latex $<

As, el comando make, sin argumentos, compilara lo necesario para obtener los archivos finales
actualizados.
Notamos que all es un target especial, pues no tiene dependencias. Es decir, se ejecuta
incondicionalmente. Otro uso habitual de un target sin dependencias es el siguiente:

all:
make hola
make chao

hola: hola.cc
g++ -o $@ $<

chao.dvi: chao.tex
latex $<

clean:
rm hola chao.dvi chao.log chao.aux

Ahora, make clean borra todos los productos de compilacion y archivos auxiliares que
hayan podido generarse, quedando solo lo importante: las fuentes, y el propio Makefile.

1.14.3. Un ejemplo con varias dependencias: LATEX


En el ejemplo anterior, el resultado final, hola, depende de un solo archivo, hola.cc. Es
posible, sin embargo, que el archivo a generar tenga mas de una dependencia, y que incluso
esas dependencias deban generarse a partir de otras dependencias. Ilustraremos esto con un
ejemplo en LATEX.
Consideremos el siguiente archivo, texto.tex:

\documentclass[12pt]{article}
\usepackage{graphicx}
\begin{document}

Una figura:

\includegraphics{figura.eps}

\end{document}

El archivo compilado, texto.dvi, depende de texto.tex, por cierto, pero tambien de


figura.eps. Un Makefile adecuado para esta situacion es:
1.14. MAKE 83

texto.dvi: texto.tex figura.eps


latex texto

clean:
rm texto.dvi texto.log texto.aux

Que pasa ahora si figura.eps, a su vez, es generada a partir de otros archivos fuente? Por
ejemplo, digamos que creamos, con xfig, el archivo figura.fig. En principio, si editamos
la figura, podemos no solo grabarla como un archivo fig, sino tambien como un archivo
postscript, con lo cual la figura queda actualizada. Existe otro modo, un poco mas eficiente:
en xfig grabamos solo la version fig, y generamos el eps con la lnea de comandos:

username@host:~$ fig2dev -L eps figura.fig figura.eps

Ahora podemos incluir este comando como una regla adicional en el Makefile:

texto.dvi: texto.tex figura.eps


latex texto

figura.eps: figura.fig
fig2dev -L eps $< $@

clean:
rm texto.dvi texto.log texto.aux figura.eps

Para cada dependencia declarada, make verifica si existe alguna regla para construirla. En
el caso de texto.tex no la hay, as que continua. Para figura.eps la hay: verifica la fecha
de modificacion de figura.fig, y determina si es necesario actualizar figura.eps. Ahora
que todas las dependencias de texto.dvi estan actualizadas, revisa si es necesario actualizar
texto.dvi.
Son claras entonces las ventajas de make: Un proyecto dado, por complicado que sea, se
puede escribir en terminos de productos finales y dependencias, posiblemente multiples, y
posiblemente encadenadas. Naturalmente, proyectos mas complicados tendran un Makefile
mas complicado. Pero una vez construido este, el producto final se obtiene simplemente con
el comando make.5
Observemos, adicionalmente, que en el ultimo Makefile agregamos al target clean la
orden de borrar figura.eps. Ahora dicho archivo puede ser generado siempre a partir de
figura.fig, por tanto no es necesario mantenerlo en disco.

5
El software distribuido en forma de codigo fuente habitualmente hace uso de esta herramienta. As,
independiente de la complejidad del software, el usuario simplemente debe dar un solo comando, make, para
obtener el ejecutable final.
84 CAPITULO 1. UNA BREVE INTRODUCCION A C++

1.14.4. Variables del usuario y patrones


En nuestro ejemplo sencillo de Makefile de la seccion anterior, hemos usado dos varia-
bles internas del sistema, $< y $@. Hay mas, por supuesto, y sugerimos leer la documen-
tacion de make para ello. Pero ademas, el usuario puede definir sus propias variables. Por
ejemplo, supongamos ahora que nuestro archivo LATEX necesita dos figuras, figura.eps
y otra_figura.eps, ambas provenientes de sendos archivos fig. Debemos modificar el
Makefile en tres puntos:

Agregar la dependencia otra_figura.eps a texto.dvi.

Agregar una regla para construir otra_figura.eps a partir de otra_figura.fig.

Agregar otra_figura.eps a la lista de archivos a borrar con make clean.

Es claro que este proceso es cada vez mas engorroso mientras mas figuras tenga un archi-
vo. Aqu es donde es util definir variables de usuario, para evitar modificar tantas veces el
Makefile, con la consiguiente ganancia de tiempo y reduccion de la probabilidad de come-
ter errores. Definiremos entonces una variable figuras, para agrupar a los archivos eps de
figuras. La sintaxis es parecida a la de bash, tanto para definir la variable como para usarla:

figuras=figura.eps otra_figura.eps

texto.dvi: texto.tex $(figuras)


latex texto

figura.eps: figura.fig
fig2dev -L eps $< $@

otra_figura.eps: otra_figura.fig
fig2dev -L eps $< $@

clean:
rm texto.dvi texto.log texto.aux $(figuras)

De este modo, agregar mas figuras implica solo dos modificaciones: agregar un valor a la
definicion de figuras, y agregar la regla correspondiente.
Pero aun es posible una simplificacion adicional: si todas las figuras eps van a venir de
fig, sera posible que make entienda eso de una vez y para siempre, y no tener que darle
reglas redundantes? S, es posible, y eso se logra gracias a los patrones, simbolizados con el
caracter especial %. Observemos el siguiente Makefile, equivalente al anterior:

figuras=figura.eps otra_figura.eps

texto.dvi: texto.tex $(figuras)


latex texto
1.14. MAKE 85

$(figuras):%.eps:%.fig
fig2dev -L eps $< $@

clean:
rm texto.dvi texto.log texto.aux $(figuras)

Se ha puesto una dependencia en la forma $(figuras):%.eps:%.fig. Esto significa lo


siguiente: de todos los nombres contenidos en la variable $(figuras), seleccione los eps.
Cada eps, a su vez, depende de un archivo que se llama igual, pero con extension fig. El
patron % representa en este caso, al nombre sin extension del archivo.
As, este Makefile permite, para todas las figuras (variable figuras), crear los eps a
partir de los fig correspondientes, a traves de la misma regla (fig2dev). Ahora s, agregar
mas figuras es trivial: basta con modificar solo una lnea del Makefile, la definicion de
figuras. El tiempo invertido y las posibilidades de error ahora son mnimos.
Naturalmente, estrategias similares se pueden emplear si deseamos generar, en vez de un
dvi, un pdf. En tal caso, las figuras deben ser convertidas a pdf:

figuras=figura.pdf otra_figura.pdf

texto.pdf: texto.tex $(figuras)


pdflatex texto

$(figuras):%.pdf:%.fig
fig2dev -L pdf $< $@

clean:
rm texto.pdf texto.log texto.aux $(figuras)

1.14.5. C++: Programando con mas de un archivo fuente


Hasta el momento, hemos centrado la mayor parte de la discusion en un ejemplo de
LATEX, pero el uso de make es por supuesto mucho mas general, y el mismo tipo de ideas se
pueden usar para cualquier tipo de proyecto en que se deben realizar tareas automaticas y
posiblemente en secuencia. En lo que sigue, continuaremos la discusion con un ejemplo en
C++. No introduciremos conceptos nuevos de make que los vistos en secciones anteriores,
sino que veremos como se pueden utilizar en un proyecto de programacion mas complicado,
consistente en varios archivos fuentes.
En principio, un programa en C++, o en cualquier lenguaje de programacion, puede
constar de un solo archivo que contenga todas las instrucciones necesarias. Para escribir
"Hola mundo" en pantalla se requieren unas pocas lneas de codigo, que pueden estar con-
tenidas en un archivo hola.cc. En ese caso, se puede crear un ejecutable hola a partir de
dicha fuente, en la forma

user@hostname:~$ g++ hola.cc -o hola


86 CAPITULO 1. UNA BREVE INTRODUCCION A C++

Sin embargo, cuando el programa es un poco mas complicado, puede ser necesario, y de
hecho es aconsejable, dividir el programa en varios archivos fuentes. As, si nuestro programa
hola usa una funcion auxiliar, PrintHola(), para enviar el mensaje a pantalla, una posibili-
dad es poner la declaracion de dicha funcion en un header , que podemos llamar, por ejemplo,
print_hola.h, y su implementacion en el correspondiente archivo print_hola.cc. En ese
caso, ademas de incluir en el archivo hola.cc una lnea del tipo #include "print_hola.h",
es neceario cambiar la lnea de compilacion a:
user@hostname:~$ g++ print_hola.cc hola.cc -o hola
El ejemplo anterior puede parecer banal, pero es una situacion usual cuando se construye
un programa en C++ usando clases. En ese caso, si se define una clase Complejo, para utilizar
numeros complejos, entonces las declaraciones de la clase y sus funciones asociadas estaran
en un archivo llamado, por ejemplo, complejo.h, y la implementacion correspondiente en
complejo.cc.
Ya que mencionamos este ejemplo, imaginemos un programa llamado "mi_gran_programa",
que dice "Hola mundo" (usando la funcion PrintHola() anterior), y sabe manipular matri-
ces de numeros complejos. Los numeros complejos estan definidos con la clase Complejo, en
complejo.h y complejo.cc, y las matrices con la clase Matriz, en matriz.h y matriz.cc.
La lnea de compilacion sera, entonces,
user@hostname:~$ g++ print_hola.cc complejo.cc matriz.cc \
mi_gran_programa.cc -o mi_gran_programa
Ya ni siquiera nos cabe en una lnea! Y no es un programa tan complicado. Ahora
el problema: si realizo una pequena modificacion en complejo.cc, hay que dar toda esta
lnea de compilacion de nuevo. No solo eso, sino que el compilador es obligado a leer y
codificar los contenidos de print_hola.cc, print_hola.h, complejo.cc y complejo.h, y
mi_gran_programa.cc, a pesar de que ninguno de ellos fue modificado. Esto, claramente, es
un desperdicio de recursos, que va a ser mas evidente mientras mas archivos fuentes (*.cc)
compongan en proyecto, o mientras mas grandes sean dichos archivos.
Podemos intuir, con lo que ya sabemos, que este es el tpico problema en que make nos
sera de gran ayuda. Pero antes, debemos conocer un poco mas como es el proceso de creacion
de un ejecutable en C++.
En realidad, crear un ejecutable consta de dos procesos: compilacion y linkeo. El linkeo es
el proceso mediante el cual se une un codigo fuente con codigo proveniente de libreras del
sistema o generadas por el propio usuario, para agregar funcionalidad inexistente en el codigo
fuente. Complicado? No tanto. El sencillo programa hola.cc enva un mensaje a pantalla,
pero en realidad no contiene el codigo necesario para ello, salvo la llamada a la funcion cout,
que no esta definida en hola.cc. El codigo necesario se encuentra en una librera del sistema.
Entonces, el comando g++ primero compila el codigo fuente en hola.cc, convirtiendolo a un
lenguaje mas cercano a la maquina, y luego lo linkea, en este caso a las libreras del sistema.
Solo entonces se tiene un ejecutable con toda la funcionalidad deseada.
Ahora podemos ver que, para no tener que pasar por todos los archivos fuentes cada
vez que se hace una modificacion en uno de ellos, hay que separar compilacion y linkeo.
Primero, generamos object files (archivos con extension .o), a partir de cada archivo fuente,
compilando con la opcion -c:
1.14. MAKE 87

user@hostname:~$ g++ -c print_hola.cc -o print_hola.o


user@hostname:~$ g++ -c complejo.cc -o complejo.o
user@hostname:~$ g++ -c matriz.cc -o matriz.o
Finalmente linkeamos los tres object files, junto al codigo que contiene a main(), para generar
el ejecutable final:
user@hostname:~$ g++ print_hola.o complejo.o matriz.o hola.cc -o hola
Esto genera exactamente el mismo ejecutable que la larga lnea de comandos anterior. Pero
ahora, si modificamos complejo.cc, basta con rehacer el correspondiente object file y linkear
todo nuevamente:
user@hostname:~$ g++ -c complejo.cc -o complejo.o
user@hostname:~$ g++ print_hola.o complejo.o matriz.o hola.cc -o hola
Como vemos, no es necesario que el compilador vuelva a leer los archivos matriz.cc y
print_hola.cc. Esto reduce el tiempo de compilacion considerablemente si los archivos fuen-
te son numerosos o muy grandes.
Ahora podemos automatizar todo este proceso a traves del comando make. Basta con
construir un Makefile con las siguientes condiciones:
El ejecutable final (hola) depende de los object files involucrados, y se construye lin-
keandolos entre s.
Cada object file depende de su respectiva fuente (archivos .cc y .h), y se construye
compilando con g++ -c).
make clean puede borrar todos los object files y el ejecutable final, pues se pueden
construir a partir de las fuentes.
Usando lo aprendido en las subsecciones anteriores (dependencias, variables de usuario e
internas, patrones), podemos construir el siguiente Makefile, que hace lo pedido:

objects=print_hola.o complejo.o matriz.o

hola: $(objects)
g++ $(objects) hola.cc -o $@

$(objects):%.o:%.cc %.h
g++ -c $< -o $@

clean:
rm hola $(objects)

Observemos como hemos usado los patrones para crear dependencias multiples: cada
archivo o depende de dos archivos con el mismo nombre, pero con extensiones cc y h.
Ya con este Makefile, de ahora en adelante, crear el ejecutable hola sera mucho mas
sencillo: editar los archivos fuentes y dar el comando make.
88 CAPITULO 1. UNA BREVE INTRODUCCION A C++

1.14.6. Un ejemplo completo


Terminamos nuestra discusion del comando make con un ejemplo completo, un Makefile
para compilar un programa de dinamica molecular llamado embat:

############################################################
# Makefile for C++ program EMBAT
# Embedded Atoms Molecular Dynamics
#
# You should do a "make" to compile
############################################################

#Compiler name
CXX = g++
#Linker name
LCC = g++

#Compiler flags you want to use


MYFLAGS = -Wall
OPFLAGS = -O4

CPPFLAGS = $(MYFLAGS) $(OPFLAGS)

#Library flags you want to use


MATHLIBS = -lm
CLASSLIBS= -lg++
LIBFLAGS = $(MATHLIBS) $(CLASSLIBS)
############################################################

OBJFILES = embat.o lee2.o crea100.o errores.o\


archivos.o azar.o lista.o rij.o\
escribir2.o velver.o cbc.o force.o\
fMetMet.o fDf.o relaja.o funciones.o\
initvel.o leerfns.o spline.o

embat : $(OBJFILES)
$(LCC) -o embat $(OBJFILES) $(LIBFLAGS)

$(OBJFILES): %.o:%.cc
$(CXX) -c $(CPPFLAGS) $< -o $@

############################################################
clean:
rm embat $(OBJFILES)
############################################################
1.14. MAKE 89

Podemos reconocer varias de las tecnicas discutidas en las secciones anteriores. El eje-
cutable embat se construye a partir de ciertos OBJFILES, que se construyen compilando los
archivos cc respectivos. Se compila usando g++, desplegando todos los warnings (opcion
Wall), con nivel de optimizacion 4 (opcion O4), y usando las libreras matematicas de C++
(opcion m). Observamos tambien algunos aspectos adicionales:

Se pueden poner lneas de comentarios con #.

Todos los compiladores y opciones de compilacion y linkeo se han puesto dentro de


variables (p. ej. CXX, LCC, MYFLAGS, MATHLIBS, etc. Esto permite mayor flexibilidad a
futuro. Por ejemplo, podramos desear compilar con una version mas reciente, o mas
antigua, del compilador, o cambiar el nivel de optimizacion (variable OPFLAGS). Con
el Makefile anterior, es posible hacerlo solo cambiando una lnea: la definicion de la
variable correspondiente.

El caracter \ indica continuacion de lnea. En este caso, en vez de definir OBJFILES en


una sola lnea de muchos caracteres (lo que probablemente no sera comodo de ver en
un editor de texto o en un terminal), se separa en lneas conectadas por \.
90 CAPITULO 1. UNA BREVE INTRODUCCION A C++
Captulo 2

Metodos Numericos

version 24 enero 2014

En este captulo revisaremos metodos basicos para resolver problemas de Fsica, y que
pueden ser utiles transversalmente, aplicables a diversas areas de investigacion.

2.1. Precision
El primer problema que hay que tener presente al resolver problemas numericamente, con
ayuda de un computador, es que todo numero real tiene una representacion en una cantidad
finita de bits, y por lo tanto toda representacion computacional de un numero real es en
general imprecisa.

Objetivo de esta parte del curso.

a) Computacion cientifica es fundamental hoy en da.


b) Tiene mucho de arte.
c) Ganar intuiciones, ver algunos ejemplos basicos, revisar algunas dificultades o errores
usuales.
d ) De cada topico se podra hacer un curso entero, nosotros solo vamos a rozar la super-
ficie, para dar un conocimiento amplio, general, y tener elementos para profundizar
cuando sea necesario.
e) Los numeros no son numeros.

Un problema sencillo: determinar si dos vectores son ortogonales.

Matematicamente sabemos resolver el problema, debera ser sencillo implementarlo.

El siguiente codigo implementa la idea basica:

#include <iostream>

using namespace std;

91
92 CAPITULO 2. METODOS NUMERICOS

int main(){

int a[3];
int b[3];

cout << "Ingrese el primer vector" << endl;


cin >> a[0] >> a[1] >> a[2];

cout << "Ingrese el segundo vector" << endl;


cin >> b[0] >> b[1] >> b[2];

int punto=a[0]*b[0]+a[1]*b[1]+a[2]*b[2];

if(punto==0){
cout << "Son ortogonales" << endl;
}
else{
cout <<"No son ortogonales" << endl;
}
return 0;

Numeros enteros: (1,0,0), (0,0,1). No hay problemas.

Numeros reales:

Ingrese el primer vector : 1.5 2.5 0


Ingrese el segundo vector : 0.6 -0.36 0
Son ortogonales

Multiplicar por un factor global no debera afectar la ortogonalidad:

Ingrese el primer vector : 1.5e-5 2.5e-5 0


Ingrese el segundo vector : 0.6e-5 -0.36e-5 0
Son ortogonales

Pero si usamos numeros mas chicos:

Ingrese el primer vector : 1.5e-20 2.5e-20 0


Ingrese el segundo vector : 0.6e-20 -0.36e-20 0
No son ortogonales

Diagnostico: entregar el resultado de ~a ~b para el ultimo caso:


2.1. PRECISION 93

No son ortogonales
1.01958e-56

El problema, entonces, es que el cero no es realmente cero.

Ojo: precision numerica. Esto tiene que ver conque un numero real debe ser representado
por un numero finito de bits, y ello da una precision finita.

En general, uno puede representar numeros reales, en base b (2 o 10), como:

(1)s c bq .

s: signo, c: significante, q: exponente.


Ej: s = 1, c = 12345, q = 3, el numero es 12.345.

Como no podemos poner infinitos numeros en el significante, ello impide representar nume-
ros reales con demasiados decimales.

Lo anterior lleva a los llamados errores de escala.

2.1.1. Errores de escala.


Un computador almacena numeros de punto flotante usando solo una pequena cantidad
de memoria. Tpicamente, a una variable de precision simple (un float en C++) se le
asignan 4 bytes (32 bits) para la representacion del numero, mientras que una variable de
doble precision (double en C++, por defecto en Python y Octave) usa 8 bytes. Un numero de
punto flotante es representado por su mantisa y su exponente (por ejemplo, para 6.6251027
la mantisa decimal es 6.625 y el exponente es 27). El formato IEEE para doble precision
usa 53 bits para almacenar la mantisa (incluyendo un bit para el signo) y lo que resta, 11
bit para el exponente. La manera exacta en que el computador maneja la representacion de
los numeros no es tan importante como saber el intervalo maximo de valores y el numero de
cifras significativas.
El intervalo maximo es el lmite sobre la magnitud de los numeros de punto flotante
impuesta por el numero de bit usados para el exponente. Para precision simple un valor
tpico es 2127 1038 ; para precision doble es tpicamente 21024 10308 . Exceder el
intervalo de la precision simple no es difcil. Consideremos, por ejemplo, la evaluacion del
radio de Bohr en unidades SI,

40 ~2
a0 = 5.3 1011 [m] . (2.1)
me e2
Mientras sus valores caen dentro del intervalo de un numero de precision simple, el intervalo
es excedido en el calculo del numerador (40~2 1.24 1078 [kg C2 m]) y del denominador
(me e2 2.34 1068 [kg C2 ]). La mejor solucion para lidiar con este tipo de dificultades de
intervalo es trabajar en un conjunto de unidades naturales al problema (e.g. para problemas
atomicos se trabaja en las distancias en Angstroms y la carga en unidades de la carga del
electron).
94 CAPITULO 2. METODOS NUMERICOS

Algunas veces los problemas de intervalo no son causados por la eleccion de las unida-
des, sino porque los numeros en el problema son inherentemente grandes. Consideremos un
importante ejemplo, la funcion factorial. Usando la definicion

n! = n (n 1) (n 2) . . . 3 2 1 , (2.2)

es facil evaluar en C++,

double nFactorial=1;
for(int i=1; i <=n; i++) nFactorial *=i ;

donde n es un numero dado.


Infortunadamente, debido a problemas de intervalo, no podemos calcular n! para n > 170
usando estos metodos directos de evaluacion (2.2).
Una solucion comun para trabajar con numeros grandes es usar su logaritmo. Para el
factorial
log(n!) = log(n) + log(n 1) + . . . + log(3) + log(2) + log(1) . (2.3)
Sin embargo, este esquema es computacionalmente pesado si n es grande. Una mejor estra-
tegia es combinar el uso de logaritmos con la formula de Stirling1

 
n n 1 1
n! = 2nn e 1+ + + (2.4)
12n 288n2
o  
1 1 1
log(n!) = log(2n) + n log(n) n + log 1 + + + . (2.5)
2 12n 288n2
Esta aproximacion puede ser usada cuando n es grande (n > 30), de otra manera es preferible
la definicion original.
Finalmente, si el valor de n! necesita ser impreso, podemos expresarlo como

n! = (mantisa) 10(exponente) , (2.6)

donde el exponente es la parte entera de log10 (n!), y la mantisa es 10a donde a es la parte
fraccionaria de log10 (n!). Recordemos que la conversion entre logaritmo natural y logaritmo
en base 10 es log10 (x) = log10 (e) log(x).

2.1.2. Errores de redondeo.


Supongamos que deseamos calcular numericamente f (x), la derivada de una funcion
conocida f (x). En calculo se aprendio que la formula para la derivada es

f (x + h) f (x)
f (x) = , (2.7)
h
en el lmite en que h 0. Que sucede si evaluamos el lado derecho de esta expresion,
poniendo h = 0?. Como el computador no entiende que la expresion es valida solo como un
1
M. Abramowitz and I. Stegun, Handbook of Mathematical Functions ( New York: Dover 1972).
2.1. PRECISION 95

lmite, la division por cero tiene varias posibles salidas. El computador puede asignar el valor,
Inf, el cual es un numero de punto flotante especial reservado para representar el infinito. Ya
que el numerador es tambien cero el computador podra evaluar el cociente siendo indefinido
(Not-a-Number), NaN, otro valor reservado. O el calculo podra parar y arrojar un mensaje
de error.
Claramente, poniendo h = 0 para evaluar (2.7) no nos dara nada util, pero si le ponemos
a h un valor muy pequeno, digamos h = 10300 , usamos doble precision? La respuesta aun
sera incorrecta debido a la segunda limitacion sobre la representacion de numeros con punto
flotante: el numero de dgitos en la mantisa. Para precision simple, el numero de dgitos
significantes es tpicamente 6 o 7 dgitos decimales; para doble precision es sobre 16 dgitos.
As, en doble precision, la operacion 3 + 1020 retorna una respuesta 3 por el redondeo;
usando h = 10300 en la ecuacion (2.7) casi con seguridad regresara 0 cuando evaluemos el
numerador.

(Ver Figura 7.3 en Apuntes de un curso de Programacion y Metodos Numericos)

Figura 2.1: Error absoluto (h), ecuacion (2.8), versus h para f (x) = x2 y x = 1.

La figura 2.1 ilustra la magnitud del error de redondeo en un calculo tpico de derivada.
Definimos el error absoluto

f (x + h) f (x)
(h) = f (x) . (2.8)
h

Notemos que (h) decrece cuando h se hace mas pequeno, lo cual es esperado, dado que
la ecuacion (2.7) es exacta cuando h 0. Por debajo de h = 1010 , el error comienza a
incrementarse debido a efectos de redondeo. En valores menores de h el error es tan grande
que la respuesta carece de sentido. Volveremos en el proximo captulo a la pregunta de como
mejorar el calculo de la derivada numericamente.
Para testear la tolerancia del redondeo, definimos como el mas pequeno numero que,
cuando es sumado a uno, regresa un valor distinto de 1. En Octave, la funcion integrada
eps devuelve 2.22 1016 . En C++, el archivo de encabezamiento <cfloat> define
DBL_EPSILON (2.2204460492503131e-16) como para doble precision.
Debido a los errores de redondeo la mayora de los calculos cientficos usan doble precision.
Las desventajas de la doble precision son que requieren mas memoria y que algunas veces (no
siempre) es mas costosa computacionalmente. Los procesadores modernos estan construidos
para trabajar en doble precision, tanto que puede ser mas lento trabajar en precision simple.
Usar doble precision algunas veces solo desplaza las dificultades de redondeo. Por ejemplo, el
calculo de la inversa de una matriz trabaja bien en simple precision para matrices pequenas
de 50 50 elementos, pero falla por errores de redondeo para matrices mas grandes. La
doble precision nos permite trabajar con matrices de 100 100, pero si necesitamos resolver
sistemas aun mas grandes debemos usar un algoritmo diferente. La mejor manera de trabajar
es usar algoritmos robustos contra el error de redondeo.
96 CAPITULO 2. METODOS NUMERICOS

2.2. Interpolacion
1. Interpolacion de dos puntos: recta que los une.

a) Interpolacion lineal
b) Interpolacion por parabola

2. Para tres puntos? Una cubica. Hay varios modos de escribir el polinomio. Una de ellas
es el polinomio de interpolacion de Lagrange:

(x x2 )(x x3 ) (x x1 )(x x3 ) (x x1 )(x x2 )


p(x) = y1 + y2 + y3
(x1 x2 )(x1 x3 ) (x2 x1 )(x1 x3 ) (x3 x1 )(x3 x2 )

3. Notar que p(xi ) = yi , y que es cubico.

4. Interpolacion de Lagrange

a) Tomemos tres puntos pertenecientes a una parabola. curva.cc, [3, 8], 3 puntos,
(a, b, c) = (1, 2, 3) La salida es curva.dat, que tiene los puntos calculados, y
curva_N.dat, el numero de puntos calculados.
b) Grafiquemos: graficar_curva.m
c) lagrange_3_puntos.cc: Toma los datos de la curva generada anteriormente, y
caclula la curva interpolada.
d ) Graficar: graficar_interpolacion.m
e) Excelente.
f ) Se puede generalizar el procedimiento para utilizar mas puntos.
g) Version generalizada: lagrange.cc
h) Volver a correr curva.cc, generando la misma secuencia de puntos anteriores, con
3 puntos
i ) lagrange.cc, graficar_interpolacion.m. Todo bien.
j ) Ahora con 10 puntos: curva.cc, lagrange.cc, graficar_interpolacion.cc. To-
do bien.
k ) 30 puntos: Los extremos se estropean horriblemente.
l ) Este es un problema conocido: en realidad, la curva interpolada presenta oscila-
ciones, que eventualmente se amplifican. El polinomio de Lagrange asegura, por
construccion, que la curva va a pasar por los puntos de referencia, pero no se puede
asegurar que la interpolacion tenga sentido a medida que el numero de nodos N
crece.
m) Como el problema es en los extremos, una solucion posible es tomar mas nodos
cerca de ellos, para que el ajuste sea mejor en esa zona. En otras palabras, utilizar
nodos no equidistantes.
2.2. INTERPOLACION 97

n) Una estrategia son los nodos de Chebyshev. Si queremos escoger N puntos no


equiespaciados en [1, 1] podemos hacerlo tomando los valores del coseno en [0, ]:
 
2i 1
xi = cos , i = 1, . . . N .
2N
Notemos que los puntos del argumento estan separados por t = /N, t1 =
/2N), tN = (2N 1)/2N, de modo que cuando N , t1 = 0 y tN = , y
x1 = 1 y xN = 1.
xi , entonces, resultan ser N puntos diferentes, acumulados en los bordes (que es
precisamente donde la interpolacion de Lagrange falla).
n) Para un arbitrario [a, b] general, y si queremos que queden ordenados de menor a
mayor:  
1 1 2i 1
xi = (a + b) + (b a) cos
2 2 2N
Con (b a) frente al coseno quedan ordenados de menor a mayor, y esa es la
forma que usamos.
o) Generamos un conjunto de datos a interpolar, como antes, pero ahora los tomamos
segun los nodos de Chebyshev: curva_chebyshev.cc
p) Los datos quedan en curva_chebyshev.dat, y el numero de puntos en curva_chebyshev_N.dat
q) Calculamos la interplacion, con el mismo codigo, pero tomando los nuevos nodos:
lagrange_chebyshev.cc
r ) graficar_interpolacion.m. Perfecto!
5. Splines
a) Otra solucion al problema: Hacer interpolacion polinomial entre puntos sucesivos,
y conectar polinomios contiguos suavemente.
b) Estas curvas polinomiales a pedazos (piecewise) se conocen como splines.
c) Los polinomios de interpolacion van a tener un determinado grado. Se dice que el
grado de la curva spline (n) es el mayor de los grados de dichos polinomios
d ) Los puntos entre los cuales se hace la interpolacion son puntos de control
e) Sera derivable n 1 veces en todos sus puntos (en particular, los puntos de union
de los polinomios).
f ) Si se usan intervalos de igual longitud: spline uniforme. Diferente longitud: spline
no uniforme.
g) Ejemplo: spline cubica: splines.pdf
1) Hacer una curva arbitraria, entre x0 y xN
2) 10 puntos de control (2 en los extremos)
3) Los polinomios elementales son cubicos:
fi (x) = ai + bi x + ci x2 + di x3 , xi x xi+1 .
4 coeficientes a determinar.
98 CAPITULO 2. METODOS NUMERICOS

4) Son 9 intervalos, en cada uno una cubica: 36 incognitas.


5) Continuidad (las curvas pasan por los puntos de control):

fi (xi ) = yi
fi (xi+1 ) = yi+1

2*8=16 ecuaciones para los puntos interiores

f1 (x0 ) = y0
f9 (x10 ) = y1 0

2 ecuaciones para los extremos.


En total, 18 ecuaciones
6) Primera derivada continua: 8 ecuaciones
7) Segunda derivada continua: 8 ecuaciones
8) Llevamos 18+16=34 ecuaciones
9) Dos ecuaciones adicionales. Por ejemplo, fijar la primera derivada en los ex-
tremos a un valor dado.
10) La spline cubica natural se obtiene imponiendo que la curvatura (segunda
derivada) en los extremos sea cero.
11) Ejemplo explcito con tres puntos
2.3. ECUACIONES DIFERENCIALES ORDINARIAS 99

2.3. Ecuaciones diferenciales ordinarias


1. Necesitamos una estrategia para calcular la derivada.

2. Derivada derecha o adelantada

a)

2
f (t + ) = f (t) + f (t) + f (t) +
2
2

f (t + ) = f (t) + f (t) + f () ,
2

con t < < t + .


b)
f (t + ) f (t)
f (t) = f () .
2
3. No conocemos , as que truncamos.

4. Error de truncamiento (diferente al error de redondeo)

5. Notacion O( ). (Carta Donald Knuth.)

6. Error lineal en .

7. Metodo de Euler.

a) Notacion tn = n , f (tn ) = fn .
b)

~vn+1 = ~vn + ~an


~rn+1 = ~rn + ~vn

c) proyectil_euler.cc (dt = 0.1, 0.01,0.001)


d ) plot_proyectil_euler.m
e) Error disminuye con dt, pero el crecimiento sigue siendo lineal

8. Metodo de Euler-Cromer.

a)

~vn+1 = ~vn + ~an


~rn+1 = ~rn + ~vn+1

b) Error de truncamiento tambien O( 2 ).


c) proyectil_euler_cromer.cc (dt = 0.1, 0.01,0.001)
100 CAPITULO 2. METODOS NUMERICOS

d ) plot_proyectil_euler_cromer.m
e) Error es marginalmente menor que con Euler

9. Metodo del Punto Medio.

a)

~vn+1 = ~vn + ~an


~vn+1 + ~vn
~rn+1 = ~rn +
2

b) Resulta
1
~rn+1 = ~rn + ~vn + ~an 2
2
c) Error de orden 2 en posicion, 3 en velocidad.
d ) proyectil_punto_medio.cc (dt = 0.001)
e) plot_proyectil_punto_medio.m
f ) Error es manifiestamente menor que con Euler y Euler-Cromer

10. Eleccion de . Escala en que el movimiento es lineal.

11. Sugerencia: probar distintos valores de (adaptativo)

12. Derivada centrada. (p. 262 pdf)

a) Error de truncamiento O( 2 ).
b) Segunda derivada centrada

13. Metodo del salto de la rana (leapfrog)

a) Derivada centrada para velocidad, centrada en t = 0.


b) Derivada centrada para posicion, centrada en t = (porque la derivada para
velocidad depende de ~v (t + ), y para que la derivada para la posicion la tome, es
necesario usar ese centramiento para ~r.
c) Se necesita un primer valor para iniciar el metodo. Ejemplo: Euler hacia atras
para calcular ~v1
d ) proyectil_leapfrog.cc (dt = 0.001)
e) plot_proyectil_leapfrog.m
f ) Ventaja: conservacion de energa

14. Metodo de Verlet

a) Derivada centrada para velocidad.


b) Segunda derivada centrada para posicion.
2.3. ECUACIONES DIFERENCIALES ORDINARIAS 101

2
c) ~r1 = ~r0 ~v0 + 2
~a(~r0 ) para iniciar.
d ) proyectil_verlet.cc (dt = 0.001)
e) plot_proyectil_verlet.m
f ) Ventaja: menor error de truncamiento
g) Si la fuerza no depende de la velocidad, no se requiere calcular la velocidad

15. Mencionar: velocity Verlet (aceleracion independiente de velocidad, pero el algoritmo


considera la velocidad en el calculo), Martys y Mountain (aceleraciones dependientes
de la velocidad).

16. Ejemplo: Perodo pendulo, conservacion energa.

a) Euler: angulo pequeno, perodo razonable, mal amplitud


b) Euler: angulo pequeno, perodo razonable, mal amplitud, vigilando energa
c) Euler: angulo pequeno, mejora con paso de tiempo mas pequeno, no perfecto
d ) Verlet: angulo pequeno, bien amplitud y perodo.
e) Verlet: angulo grande, bien amplitud y perodo.

17. Ejemplo: Orbita planetaria. (p. 283 pdf)

a) Euler: no se cierra, energa no constante.


b) Euler-Cromer: se cierra, energa constante.
c) Euler-Cromer: problemas con orbita muy elptica (mitad de velocidad inicial).
d ) Euler-Cromer: mejora bajando el paso de tiempo, precesion.

18. Runge-Kutta segundo orden

a) Tipo Euler (Sec. 9.2.1)


b) Ejemplo en 1 dimension: edo_rk2.cc
c) Ejemplo en 2 dimensiones, Euler vectorizado: pendulo_vector_euler.cc
d ) Pendulo con RK2: 13_pendulo_rk2.cc
e) Tipo Punto medio [Ecs. (9.23) y (9.24)]
f ) Caso general [Ecs. (9.25)-(9.29)]

19. Runge-Kutta cuarto orden (Sec. 9.2.3)

a) Pendulo con RK4: 14_pendulo_rk4.cc


b) Con 200 iteraciones, se ve igual. Con 2000, RK4 es mejor.
c) Orbita circular: no hay precesion, se conserva muy bien la energa (Fig. 9.6):
15_orbita_rk4.cc
d ) Mejora harto para la orbita muy elptica
102 CAPITULO 2. METODOS NUMERICOS

20. Runge-Kutta adaptativo (Sec. 9.3)

a) Figs. 9.7, 9.8

21. Predictor-corrector

a) Filosofa general
b) Caso sencillo: metodo de Euler trapezoidal. Igual a segunda formula para RK2.
x es el predictor, y el segundo paso, para calcular x(t) [ec. (9.23)] es el corrector.
Excepto que el segundo termino es iterado [el valor obtenido es considerado un
nuevo predictor, y se vuelve a usar la formula (9.23)], hasta que converge:

y0p = yi + f (ti , yi )

y1p = yi + (f (ti , yi ) + f (ti+1 , y0p))
2

p
y2 = yi + (f (ti , yi ) + f (ti+1 , y2p))
2
..
.

ynp = yi + (f (ti , yi ) + f (ti+1 , ynp )) ,
2
p
hasta que los valores sucesivos de ynp no varan mucho entre s: |ynp yn+1 | .
p
Entonces se toma yi+1 = yn .
2.4. NUMEROS ALEATORIOS 103

2.4. Numeros aleatorios


1. The generation of random numbers is too important to be left to chance (Robert R.
Coveyou, Oak Ridge National Laboratory)

2. Builtin: rand()

a) Numero aleatorio entre 0 y RAND_MAX


b) Ejemplo sencillo.
builtin.cc
c) Test de aleatoriedad. Distribucion uniforme, pocos puntos.
builtin_distribution_small.cc
d ) Test de aleatoriedad, muchos puntos.
builtin_distribution.cc
e) Tecnica de generacion. Numero seudoaletorio:

Ij+1 = aIj + c mod m ,


con m el modulo, a el multiplicador, y c el incremento. Si c 6= 0, esto es un LGC
(linear congruential generator ). Si c = 0, un MLGC (multiplicative LGC ). I0 es
la semilla.
(Hacer dibujo para tener intuicion.)
Si se usa un periodo muy grande, esto, en la practica, se ve como un numero
aleatorio.
f ) Ventaja: repetibilidad de la secuencia (importante para simulaciones, en caso de
que se quiera generar exactamente la misma simulacion en un tiempo posterior).
Desventaja: repetibilidad de la secuencia.
builtin_repeat.cc
g) Se repite despues de 2888000745 iteraciones.
h) Cambio de semilla, para cambiar la secuencia.
builtin_seed.cc

3. Builtin (mejor algoritmo, mas portable): random()

a) The random() function uses a non-linear additive feedback random


number generator employing a default table of size 31 long
integers to return successive pseudo-random numbers in the range
from 0 to RAND_MAX. The period of this random number generator
is very large, approximately 16*((2**31)-1).

4. Mejora: Semilla aleatoria. azar.h

a) azar_example.cc
104 CAPITULO 2. METODOS NUMERICOS

5. Pool de entropa. La idea es usar como fuente de numeros aleatorios efectos impre-
dictibles disponibles para un computador (ruido termico, efectos cuanticos, etc.), que
son convertidos en una senal electrica y luego a un numero aleatorio. Son tpicamente
usados como semilla para un generador seudoaleatorio.
Se mantiene un pool de entropa de bits aleatorios, que se va refrescando o aumentando
a medida que se necesita o aparezcan eventos (por ejemplo, presionar una tecla). En
Linux, /dev/random es un generador de numeros aleatorios, gracias a que puede acceder
a ruido ambiente obtenido de otros dispositivos.

6. Otros metodos mas modernos de generar numeros aleatorios, por ejemplo, xorshift de
64 bits: Si x 6= 0 es un entero, alojado en 64 bits. Se eligen ciertos numeros a1 , a2 y a3 ,
y se realizan las siguientes operaciones:

x1 = x (x >> a1 )
x2 = x1 (x1 << a2 )
x3 = x (x2 >> a3 )

o bien

x1 = x (x << a1 )
x2 = x1 (x1 >> a2 )

Con ai adecuados, se puede tener un generador de perodo 264 1 1.8 1019 . Ej.:
a1 = 21, a2 = 35, a3 = 4.
es el XOR logico
<< (>>) es el desplazamiento de bits hacia la izquierda (derecha):
00101 << 1 = 01010, 00101 >> 1 = 10010

7. Combinar metodos.

8. Libreras disponibles.

9. GSL, distribucion uniforme y gaussiana

a) random_number.cc
b) plot_random_number.m

10. Clase a partir de GSL

a) random_class_example.cc

11. Distribucion arbitraria

a) Rejection method
1) Se escoge una funcion para generar la distribucion, f (x).
2.4. NUMEROS ALEATORIOS 105

2) Se escoge un valor de x al azar, de una distribucion uniforme.


3) Se toma un valor de y al azar, de una distribucion uniforme.
4) Si y < f (x) (esta por debajo de la curva), se acepta x como un numero
aleatorio. En caso contrario, se rechaza.
5) Resultado: Los x para los cuales f (x) es muy alto son favorecidos, en desmedro
de aquellos con f (x) pequeno. En promedio, x sera seleccionado un numero
f (x) de veces (porque y se escoge de distribucion uniforme), y por tanto la
distribucion de probabilidad de x tendra justo la forma de f (x).
b) random_distribucion.cc
c) plot_random_distribution.m
106 CAPITULO 2. METODOS NUMERICOS

2.5. Sistemas de ecuaciones lineales


1. Regla de Cramer

a) Sistematica, poco eficiente

2. Eliminacion Gaussiana (Sec. 10.1.2)

3. Pivoteo (Sec. 10.1.3)

4. Determinante (Sec. 10.1.4)

5. Matriz inversa (Sec. 10.2)


2.6. SISTEMAS DE ECUACIONES NO LINEALES 107

2.6. Sistemas de ecuaciones no lineales


1. Problema mas difcil, sin embargo existen diversos algoritmos utiles. Pensemos primero
en una dimension, y en la siguiente funcion:
f (x) = 5t3 2t cos(t/5) .

2. plot_funcion.m
3. Ceros en 0.6, 0, 0.6.
4. Tratemos de buscar el cero a la derecha del origen.
5. Primera idea: Reticulado en busqueda de un cero. Desventaja, posiblemente lento si el
cero esta lejos del extremo de donde partimos.
6. Biseccion
a) Refinamiento de la idea anterior.
b) biseccion.cc
c) Ventaja: no falla
d) Desventaja: necesita que haya un solo cero en el intervalo (y que la funcion sea
continua)
7. Newton
a) Explicar (Sec. 10.3.1)
b) newton.cc
c) Ventaja: rapidez de convergencia
d) Desventajas: necesita derivada, problemas cerca de un cero de la derivada, cuen-
ca de atraccion no garantizada. La semilla se puede disparar. Comparar x0 =
.8, .1, .5 en newton.cc. La primera llega al cero de la derecha, la seguna al ori-
gen, y la tercera al cero de la izquierda
e) Cuenca de atraccion
8. Secante
a) secante.cc
b) Como Newton, pero la derivada calculada con diferencia finita.
c) Ventaja: analogo a Newton, un poco mas lento solamente, no necesita derivada
d) Desventaja: necesita dos semillas iniciales
9. En general, lo usual es combinar algoritmos: biseccion para una busqueda segura, luego
Newton para un refinamiento muy rapido.
10. Para varias variables, se puede generalizar el metodo de Newton. (Sec. 10.3.2)
11. Finalmente, hacemos notar que todos los problemas de optimizacion son equivalentes
a este.
108 CAPITULO 2. METODOS NUMERICOS

2.7. Regresion lineal


1. Si tenemos un conjunto de datos, que queremos ajustar por una curva, una alternativa
sera la interpolacion. Sin embargo, en ese caso debemos hacer pasar una curva por
todos los puntos dados, lo cual introduce problemas como vimos con el polinomio de
Lagrange. Por otro lado, puede que sepamos que los datos correspondan a un fenomeno
lineal, pero debido a incertezas experimentales o ruido proveniente de otras fuentes los
puntos no caigan exactamente a una recta. Como podemos ajustarlos a una recta con
el menor error posible?

2. Por ejemplo, tomemos f (x) = 3.5x 1.1.

3. Generemos puntos sobre esta recta, con un cierto error anadido, e intentemos ajustarlo
de vuelta: lineal.cc, plot_data.m

4. Mnimos cuadrados, con error (Sec. 11.1.2)

5. Caso particular: ajuste lineal (Sec. 11.1.3)

6. lineal_ajuste.cc, plot_data_ajuste.m

7. Algunos problemas se pueden convertir en un problema lineal:

8. Ajuste semilog:

y = Aex
ln y = ln A + x

9. Ajuste log-log:

y = Ax
ln y = ln A + ln x

10. Caso general (Sec. 1.11.4)

11. Ejemplo: Ajuste polinomial

12. Un ajuste por una funcion es tambien un problema de optimizacion. Otros metodos mas
refinados de ajuste se pueden utilizar empleando tecnicas mas refinadas de optimizacion
(redes neuronales, algoritmos geneticos, CSA)
2.8. INTEGRACION (CUADRATURA) 109

2.8. Integracion (cuadratura)


1. Nombre: los griegos entendan que calcular un area corresponda a construir (geometri-
camente) un cuadrado que tenga la misma superficie que la buscada.

2.

t2
Z Z  
2
I= dtf (t) = dt 6 + cos t
0 0 2

t3

t sin(2t)
= 6t + +
6 2 4
0
 2 
11
=
6 2
= 12.1110468146939

3. plot_function.m

4. Estrategia: subdividir el intervalo [a, b] en puntos xi , con x0 = a, xN = b, y evaluar la


funcion solo en fi = f (xi ).

5. Metodo mas sencillo: aproximar por barras de altura f (xi ) (o f (xi+1 )). Equivale a una
integral de Riemann superior o inferior, deberan ser iguales a la integral en el lmite
N .

6. Mejor idea: Regla trapezoidal (Sec. 12.2)

a) Trazar lneas rectas entre fi y fi+1 . El resultado son trapecios en vez de rectangulos.
b) En general, para puntos no equidistantes, se calculan las areas de los trapecios y
se suman.
c) Puntos equidistantes: expresion mas sencilla, equivalente a la idea ingenua, pero
con rectangulos de altura (fi + fi+1 )/2, algo as como el algoritmo de punto medio
para ecuaciones diferenciales ordinarias.
d ) trapezoidal_idea.cc: I=-12.11078843, bastante bueno
e) El codigo se puede optimizar, haciendo algun trabajo analtico sobre la suma de
areas [Ec. (12.2)]
1) trapezoidal.cc: N = 100, 200, 300, setprecision
2) N = 200: I=-12.11098222
3) N = 300: I=-12.11101811

7. Interpolacion por polinomios (Sec. 12.3)

a) No equiespaciados: Polinomio de interpolacion por diferencias divididas de Newton


b) Equiespaciados: Formula de interpolacion hacia adelante de Newton-Gregory
110 CAPITULO 2. METODOS NUMERICOS

c) Si se usa la formula de Newton-Gregory hasta primer orden en , lo que significa


interpolar entre dos puntos por una recta, se recupera la regla trapezoidal [Sec.
12.4, Ecs. (12.5) y (12.7)]

8. Regla de Simpson

a) Newton-Gregory, con tres puntos [Sec. 12.4, Ecs. (12.6) y (12.8)]


b) simpson.cc
c) El error es mas grande que con trapezoidal, por la misma razon que un polinomio
de grado mayor no asegura una mejor aproximacion.

9. Error regla trapezoidal [Sec. 12.4, Ec. (12.9)]

10. Cuantas subdivisiones? Achicar el intervalo, y ver si la integral cambia su valor.

11. Romberg se inspira en esta estrategia (Sec. 12.5)

a) Regla trapezoidal, dividiendo por dos la separacion entre puntos. Esto permite
que no se tengan que recalcular todos los puntos, solo los intermedios.
b)

1
IT (h3 ) = h3 [f (a) + f (b)] + h3 f (a + h3 ) + h3 f (a + 2h3 ) + h3 f (a + 3h3 )
2
11 1
= h2 [f (a) + f (b)] + h2 f (a + h2 ) + h3 f (a + h3 ) + h3 f (a + 3h3 )
22 2
1
= IT (h2 ) + h3 f (a + h3 ) + h3 f (a + 3h3 )
2

Cuantos terminos adicionales?


Para h4 = h3 /2, habra terminos f (a + h4 ), f (a + 2h4 ), . . . f (a + 6h4 ), f (a + 7h4 ).
Todos los pares se escribiran como f (a + kh3 ), y formaran IT (h3 ). Quedaran solo
los impares, f (a + h4 ), f (a + 3h4 ), f (a + 5h4 ), f (a + 7h4 ). 4 terminos adicionales.
Entonces, para h1 son 0, para h2 son 1 = 20 , para h3 son 2 = 21 , para h4 son
4 = 22 . En general, para hn son 2n2.
En general, entonces,
2n2
1 X
IT (hn+1 ) = IT (hn ) + hn f (a + (2i 1)hn )
2 i=1

c) Podemos utilizar este metodo recursivo hasta que converja. Una mejor idea es
construir la matriz triangular (12.13)(12.15).
d ) En una fila, el primer termino es la regla trapezoidal recursiva, la segunda columna
es eso, menos el termino h2 del error, la siguiente columna le resta a eso el termino
h4 del error, etc.
e) romberg.cc,
2.8. INTEGRACION (CUADRATURA) 111

1) Con 7 filas uno ya tiene 8 decimales correctos.


f ) Calculo de la funcion error: romberg_erf.cc
g) El algoritmo se puede hacer adaptativo, deteniendolo cuando se alcance una pre-
cision dada.

12. Cuadratura de Gauss

a) Todas las reglas anteriores significan aproximar la integral por el integrando eva-
luado en ciertos puntos, multiplicado por ciertos coeficientes.
b) Distintas reglas tienen que ver con distintos pesos.
c) Idea: elegir tambien distintas abscisas.
d ) Elegir puntos no equidistantes, y pesos adecuados
e) Gauss-Legendre ([1, 1]), Gauss-Laguerre ([0, ])
f ) La idea es que una aproximacion del tipo
Z b N
X
W (x)f (x) dx wj f (xj ) .
a j=1

podra ser exacta si f (x) es un polinomio, si escogemos adecuadamente los pesos.


Por ejemplo, Z 1
exp( cos2 x)
dx,
1 1 x2
nos sugerira una cuadratura de Gauss basada en elegir
1
W (x) =
1 x2
Definiendo g(x) = W (x)f (x), vj = wj /W (xj ), se puede reescribir
Z b N
X
g(x) dx vj g(xj ) ,
a j=1

desaparece aparentemente W (x), pero esta implcita en la eleccion de los pesos.


Y por la forma de W (x), funciones que sean justo polinomios por W (x) van a
resultar muy bien ajustadas, porque el lado derecho va a ser exacto, o muy bueno,
y funciones de otra forma no van a ser tan bien aproximadas.
g) Ejemplo: Gauss-Legendre (Sec. 12.6)
1) Mapear intervalo [a, b] a [1, 1].
2) Abscisas xi : ceros de los polinomios de Legendre
3) Pesos wi : Ec. despues de (12.18)
4) Tabla 12.1
5) Calculo funcion error: Ecs. (12.22)(12.24)
112 CAPITULO 2. METODOS NUMERICOS

h) Problema de cuadraturas: mayor numero de puntos (mayor orden de la apro-


ximacion) no significa necesariamente mayor precision. Similar al problema del
polinomio de Lagrange.

13. Montecarlo

a) Area bajo la curva, encerrada en un rectangulo.


b) Elegir numeros al azar, ver cuantos caen bajo la curva.
c) La proporcion de puntos que caen bajo la curva, multiplicada por el area del
rectangulo, es la integral.
d ) montecarlo.cc (Cuidado con los signos)
e) Problema: converge muy lentamente.
f ) N = 100, N = 200, N = 300. . .
g) N = 1000000, 2 decimales correctos.
h) Ideal para integrales multidimensionales (area del crculo).
2.9. SORT 113

2.9. Sort
1. Bubble sort

a) Recorre la lista completa, intercambia dos consecutivos cuando estan en orden


incorrecto.
b) bubble.cc
c) Sencillo, muy lento, no recomendable
d ) O(n2 )

2. Selection sort

a) Encuentra el mnimo, lo pone en la primera posicion. Repite con la parte no


ordenada.
b) selection.cc
c) Sencillo, lento.
d ) O(n2 )

3. Insertion sort

a) Revisa cada elemento y lo inserta en la posicion correcta en la parte ordenada de


la lista.
b) No tan ingenuo
c) insertion.cc
d ) Bueno para listas casi ordenadas o de pocos elementos.
e) O(n2 )

4. Mergesort

a) John von Neumann, 1945


b) Dividir en aproximadamente la mitad
c) Ordenar cada sublista recursivamente, aplicando este mismo algoritmo.
d ) Mezclar listas adyacentes ordenadas (el primer elemento de cada sublista es con
seguridad el mnimo, luego solo hay que comparar los primeros elementos de cada
lista).
e) Primero divide y ordena las unidades mas pequenas
3 8 1 9 0 5 7 6 2 4 (inicio)
3 8 1 9 0 5 7 6 2 4 (divide)
3 8 1 9 0 5 7 6 2 4 (divide)
3 8 1 9 0 5 7 6 2 4 (divide)
3 8 1 0 9 5 7 6 2 4 (ordena)
114 CAPITULO 2. METODOS NUMERICOS

f ) Ahora compara los primeros elementos de las dos primeras sublistas, y los coloca
en orden:
sin orden: 3 8 1 (1<3)
ordenada:

sin orden: 3 8 (nada con que comparar)


ordenada: 1

sin orden:
ordenada: 1 3 8
g) Lo mismo con las siguientes dos:
sin orden: 0 9 5 7 (0<5)
ordenada:

sin orden: 9 5 7 (5<9)


ordenada: 0

sin orden: 9 7 (7<9)


ordenada: 0 5

sin orden: 9 (nada con que comparar)


ordenada: 0 5 7

sin orden:
ordenada: 0 5 7 9
h) Mezcla sucesivamente las distintas sublistas:
3 8 1 0 9 5 7 6 2 4
1 3 8 0 5 7 9 2 4 6
0 1 3 5 7 8 9 2 4 6
0 1 2 3 4 5 6 7 8 9
i ) Escala bien para listas largas, estandard en Perl, Python, Java.
j ) O(n log(n)).
k ) Idea: una lista de pocos elementos se ordena mas rapido.

5. Heapsort

Construye una heap (montculo, arbol binario, cada hijo es menor que su padre)
Saca el elemento mas grande (la raz, intercambiandolo por el ultimo elemento) y lo
pone en la lista ordenada.
Reconstruye la heap
itera.
2.9. SORT 115

Es como insertion, con el anadido de usar un arbol binario.


Mas eficiente.
3 8 1 9 0 5 7 6 2 4
Primer elemento en el
3
3 - 8
38, debe intercambiar
Intercambia
8-3
8-3
-1
8-3-9
-1
39, debe intercambiar
8-9-3
-1
8-9, debe intercambiar
9-8-3
-1
9-8-3
-0
-1
9-8-3
-0
-1-5
Intercambia 1 y 5:
9-8-3
-0
-5-1
9-8-3
-0
-5-1
-7
Intercambia 5 y 7
9-8-3
-0
-7-1
-5
116 CAPITULO 2. METODOS NUMERICOS

9-8-3-6
-0
-7-1
-5
Intercambia 3 y 6:
9-8-6-3
-0
-7-1
-5
9-8-6-3
-2
-0
-7-1
-5
9-8-6-3
-2
-0-4
-7-1
-5
Intercambia 0 y 4:
9-8-6-3
-2
-4-0
-7-1
-5
Ahora comienza a ordenar. Intercambia la raiz (9) con el ultimo (0)
0-8-6-3
-2
-4-9
-7-1
-5
Y pone el 9 en la lista ordenada.
0-8-6-3
-2
-4
-7-1
-5
Intercambia el 0 con el 8, y asi sucesivamente, hasta que esten todos ordenados de
nuevo:
2.9. SORT 117

8-0-6-3
-2
-4
-7-1
-5

8-6-0-3
-2
-4
-7-1
-5

8-6-3-0
-2
-4
-7-1
-5

Intercambia la raiz (8) con el ultimo (2), y elimina el 8, poniendolo en la lista ordenada
(9,8)
2-6-3-0
-4
-7-1
-5

6-2-3-0
-4
-7-1
-5

7-2-3-0
-4
-6-1
-5

7-3-2-0
-4
-6-1
-5

7-4-2-0
-3
-6-1
-5

Ahora interambia el 7 con el 0, y pone el 7 en la lista ordenada (9,8,7). Etc.


O(n log n)
118 CAPITULO 2. METODOS NUMERICOS

6. Quicksort

a) Elige un elemento de la lista (pivote).


b) Reordena la lista, poniendo todos los elementos menores que el pivote antes que
el, y los mayores despues.
c) El pivote queda en el lugar correcto.
d ) Ordenar recursivamente las sublistas a cada lado.
e) Las versiones mas eficientes son muy rapidas, necesita poca memoria, estandar en
libreras.
f ) O(n log n)
Captulo 3

Metodos Numericos Avanzados

version 19 junio 2014

En este captulo profundizaremos en algunos de los problemas revisados en el Captulo


anterior, revisando metodos avanzados para resolverlos.
En la Sec. 3.1, revisaremos metodos para resolver ecuaciones diferenciales a derivadas par-
ciales, para los cuales, en general, los metodos ya vistos para resolver ecuaciones diferenciales
ordinarias (con derivadas totales, Sec. 2.3) no son utiles. En la Sec 3.2 estudiaremos redes
neuronales, como un ejemplo de generalizacion de los metodos de interpolacion estudiados en
la Sec. 2.2. En la Sec. 3.3, estudiaremos los metodos de automatas celulares, que nos ofrece
otra alternativa de generalizacion para resolver ecuaciones diferenciales, parciales o totales.
Finalmente, en la Sec. 3.4, revisaremos algoritmos geneticos y metodos de Montecarlo, que
podemos utilizar como metodos de optimizacion, generalizando as lo estudiado en la Sec. 2.6.

3.1. Ecuaciones con derivadas parciales


3.1.1. Ecuacion de Laplace
Diferencias finitas
1. Ecuacion a resolver: Laplace/Poisson

2 = ,

con (0) = a, (L) = b (una dimension).

2. Tener condiciones de borde en vez de condiciones iniciales cambia el problema, no


podemos usar metodos tipo Runge-Kutta para integrar la ecuacion diferencial.

3. Diferencias finitas

4. Segunda derivada centrada:

n+1 2n + n1
= n .
x2

119
120 CAPITULO 3. METODOS NUMERICOS AVANZADOS

5. C.B.: 0 = a, N = b.

6.

2 21 + a = x2 1
3 22 + 1 = x2 2
..
.
b 2N 1 + N 2 = x2 N 1

7.
2 1 0 1 x2 1 a
1 2 1 2
0
2 x 2

0 2
1 2 1 0 3 = x 3

.. ..

. . .

. .
2
0 0 2 1 N 1 x N 1

8. La solucion es invertir la matriz.

9. La ecuacion de Laplace es lo mismo, basta hacer = 0.

10. Notemos que lo que estamos haciendo es aproximar una derivada por los valores de la
funcion en puntos seleccionados.

11. Lo anterior corresponde a una grilla homogenea.

12. Si vara muy rapidamente en un sector, se puede aumentar la discretizacion, pero


aumentara el numero de puntos a calcular en todo el intervalo. Mejor usar una grilla
no homogenea.

13. Se puede usar una grilla no homogenea, pero hay que usar otro modo de aproximar las
derivadas.

14. Ej.: tomar 3 puntos de la grilla, no necesariamente equiespaciados, y usar el polinomio


de interpolacion de Lagrange para encontrar una funcion aproximante, y luego decir que
las derivadas de la funcion de interpolacion son las derivadas de la funcion a resolver
(). (Sec. 1.1)

15. Interesante: al calcular la primera y segunda derivada del polinomio de interpolacion


de Lagrange con 3 puntos, y considerar una grilla homogenea nuevamente, resulta:
1
p = (fn+1 fn1 )
2x
1
p = (fn1 2fn + fn+1 )
x2

exactamente lo que tenamos antes.


3.1. ECUACIONES CON DERIVADAS PARCIALES 121

16. Discretizar con derivadas centradas es equivalente a calcular la interpolacion de 3 pun-


tos, y calcular la derivada de dicha interpolacion, en el caso de grilla homogenea.

17. Esto sugiere que, en general, cualquier interpolacion adecuada nos puede servir para
discretizar la derivada.

18. f (x) = 1 g1 (x) + 2 g2 (x) + 3 g3 (x), con gi (x) funciones conocidas.

19. Interpolacion cubica: usar el polinomio de interpolacion de Lagrange de la forma p(x) =


0 +1 x+2 x2 +3 x3 , con cuatro puntos. Esto permite calcular hasta la tercera derivada,
lo cual es util si la ecuacion diferencial es de grado 3. Para la ecuacion de Laplace, basta
con una interpolacion que de la segunda derivada.

20. splines. Las ecuaciones que siempre faltan en las splines, en los bordes, aca emergen
naturalmente, porque el problema es de condiciones de borde.

21. Caractersticas del metodo:

a) Sencillo de implementar
b) El error se puede disminuir disminuyendo el tamano de la grilla.
c) La matriz a invertir puede ser prohibitivamente grande.
d ) Error en redondeo por discretizacion demasiado pequena.

22. Dos dimensiones

23.
2 2
+ = (x, y)
x2 y 2

24. Discretizacion homogenea:

i+1,j 2i, j + i1,j i,j+1 2i, j + i,j1


+ = i,j
x2 x2

25. Tambien se puede poner en forma matricial, y resolver por eliminacion Gaussiana, por
ejemplo.

26. Posibles problemas: Ademas de los problemas del caso unidimensional, matriz no sen-
cilla de resolver si el borde no es rectangular.

Metodo de relajacion
1. (Sec. 1.2.2)

2. La solucion de la ecuacion de Laplace satisface que el promedio de la funcion sobre una


esfera es igual al valor de la funcion en el centro de la esfera.
122 CAPITULO 3. METODOS NUMERICOS AVANZADOS

3. En la discretizacion, tambien se puede ver. Del punto 4 en la pag. 119:


1
n = [n1 + n+1 + x2 n ]
2

Para el caso de Laplace, n = 0, y queda justamente que n es el promedio de los


valores a su alrededor.

4. En dos dimensiones, del punto 24 en 121:


1
i,j = [i+1,j + i1,j + i,j+1 + i,j1 x2 i,j ]
4
Para Laplace, nuevamente i,j es el valor promedio sobre los 4 puntos mas cercanos.

5. Si es Poisson, no es particularmente mas difcil.

6. Estrategia:

a) Asignar los valores conocidos en el borde


b) Asignar valores arbitrarios o elegidos con algun criterio astuto en los puntos inte-
riores.
c) Recorrer los puntos interiores, reasignandoles el promedio de los 4 puntos mas
cercanos.
d ) Iterar sobre toda la grilla hasta que converja.
e) Caractersticas:
1) Geometras arbitrarias
2) Convergencia lenta, puede servir como muy buen precursor para metodos mas
refinados (Gauss-Seidel, succesive over-relaxation).

Shooting method
1. Si uno conociera (0), podra integrar la ecuacion de Poisson. Sin embargo, integrando
simbolicamente uno puede encontrar (0) en terminos de las condiciones de borde y
la integral de la inhomogeneidad en todo el intervalo. (Sec. 1.1)

2. Con (0) y (0) se puede integrar la solucion como un problema de condiciones ini-
ciales.

3. (Sec. 2.1.1) Otra forma de hacerlo es tomar una adivinanza para la derivada en cero,
s = (0). Se integra hasta el borde izquierdo, obteniendose una solucion (s, L).
Definimos
g(s) = (s, L) (L) ,
y el problema se resuelve encontrando el cero de la funcion g(s).

4. Newton funcionara si pudieramos derivar g(s), pero como se obtiene implcitamente


no es factible, pero biseccion y la secante pueden utilizarse.
3.1. ECUACIONES CON DERIVADAS PARCIALES 123

5. Inestabilidades numericas pueden hacer no aconsejable el procedimiento anterior. Como


se integra desde la izquierda, dependiendo del metodo de integracion puede ir aumen-
tando la incerteza a medida que se avanza hacia la derecha; dependiendo de (x), esas
incertezas numericas se pueden convertir en inestabilidades importantes en la integra-
cionon. Una solucion para este u otros problemas es simplemente integrar desde la
derecha, usando como condicion inicial (L), e integrando hacia la izquierda.

6. Otra alternativa, que puede ser util si la integracion debe ser hecha con mucha precision
en ambos extremos, es integrar desde ambos extremos simultaneamente, y hacer que
las funciones empalmen en el centro (multi shooting method).

7. La funcion a minimizar es la suma de las distancias entre los valores del potencial y su
derivada en el centro, o bien solo el valor del potencial (confiando en que la solucion al
problema debe ser diferenciable una vez para que sea solucion de la Ec. de Poisson).

8. Sera una funcion de dos variables, g(s(1) , s(2) , donde s(1) es el valor de la derivada en el
extremo izquierdo, y s(2) su valor a la derecha.

9. En dos dimensiones, biseccion no funcionara.

10. El metodo de la secante podra tener sentido, pero en dos dimensiones hay varias
direcciones en las cuales tomar una derivada, y habra que escribir una version matricial
del metodo de la secante (ej., metodo de Broyden), del mismo modo que existe la version
matricial del metodo de Newton.

11. Otra alternativa: moverse en la direccion opuesta del gradiente, pensando que dado
un punto inicial ~r0 , el mnimo de la funcion debera estar en la direccion opuesta del
gradiente (gradient descent method ).

12. En nuestro caso, buscamos un cero.

13. El problema es cuanto moverse en la direccion del gradiente. A diferencia del metodo
de Newton o de la secante, no hay una prescripcion definida del monto en que hay que
desplazarse, solo que el mnimo esta en la direccion del gradiente. En una dimension:
g(sn) g(sn1)
sn+1 = sn ,
sn sn1
con algun valor dado, que en general debera ir adaptandose en cada iteracion ya que
si es demasiado grande podramos caer demasiado lejos del cero.

14. En dos dimensiones, se pueden considerar las derivadas a lo largo de cada eje:
" (1) (2) (1) (2)
#
() g(s n , s n ) g(s n1 , s n1 )
sn+1 = s()
n () ()
,
sn sn1

con = 1, 2, y se puede escoger constante, variable en cada iteracion, y tambien


podra usarse un en cada direccion, (1) 6= (2) .
124 CAPITULO 3. METODOS NUMERICOS AVANZADOS

Condiciones de borde

1. Dirichlet

a) Fijas: (0) = a, (L) = b. Al discretizar: 0 = a, N = b.


b) Periodicas: N = 0 .

2. von Neumann: (0) = a, (L) = b. Como

i+1 i
(xi ) = ,
x
se tiene

1 = ax + 0 ,
N = bx + N 1 ,

3. Mixtas: (0) = a, (L) = b, por ejemplo.

4. Libres: una onda se propaga hacia la derecha, llega al borde y se va, sin reflejarse.
3.1. ECUACIONES CON DERIVADAS PARCIALES 125

Elementos finitos

1. (Sec. 1.2.3)

2. Multiplicamos la Ec. de Poisson por una funcion h(x), e integramos en un intervalo,


digamos [0, 1]:
Z 1 Z 1

dx (x)h(x) = dx (x)h(x)
0 0

3. Integrando por partes


Z 1 Z 1
1
(x)h(x)|0 dx (x)h (x) = dx (x)h(x)
0 0

As que necesitamos que h(x) tenga derivada.

4. El termino de borde es incomodo, lo eliminamos con

h(0) = h(1) = 0 .

Luego: Z 1 Z 1

dx (x)h (x) = dx (x)h(x)
0 0

5. El termino de la derecha se ve como un producto interno:


Z 1

h , h i = dx (x)h(x)
0

6. Se ve como la descomposicion de (x) en componentes en una base h(x). Explo-


temos esa idea.

7. Propongamos X
(x) = i hi (x)
i

8. Puede parecer extrano, pero es el mismo tipo de expansion que uno hace con una serie
de Fourier, por ejemplo, o con splines.

9. Multiplicando por hi (x) la Ec. de Poisson, e integrando por partes:


Z 1 Z 1

dx (x)hi (x) = dx (x)hi (x)
0 0
X Z 1 Z 1
j dx hj (x)hi (x) = dx (x)hi (x)
j 0 0
126 CAPITULO 3. METODOS NUMERICOS AVANZADOS

Definiendo
Z 1
Aij = dx hj (x)hi (x) ,
0
Z 1
bi = dx (x)hi (x) ,
0

queda X
j Aij = bj ,
j

convirtiendo el problema en uno de algebra lineal.

Transformada de Fourier

3.1.2. Ecuacion de difusion


Hasta ahora hemos visto solo la ecuacion de Laplace, ahora introducimos una primera
derivada temporal.

Captulo 3 (archivo capitulo_1.pdf)

3.1.3. Ecuacion de conveccion/adveccion


Ecuacion con primera derivada temporal y primera derivada espacial

Captulo 4 (archivo capitulo_1.pdf)

3.1.4. Ecuacion de ondas


Ecuacion con segundas derivadas temporal y espacial

Captulo 6 (archivo capitulo_1.pdf).

3.2. Redes neuronales


Inspirados en como los sistemas nerviosos biologicos procesan informacion.

Elementos interconectados (neuronas) que trabajan en conjunto para resolver problemas.

Aprendizaje por ejemplos

Se configuran para una aplicacion especfica (reconocimiento de patrones, clasificacion de


datos, interpolacion, etc.)

Extraccion de patrones y tendencias a partir de datos complicados y/o imprecisos.


3.2. REDES NEURONALES 127

Aplicaciones: modelo y diagnostico de sistema cardiovascular, deteccion de olores, mercado,


regresion no lineal, optimizacion, memoria asociativa (recuerdo basado en parcial), proce-
samiento de senales, robotica, reconocimiento de voz, produccion de voz, vision, juegos,
etc.

Caso sencillo: una neurona con varias entradas y una salida.

Modo de entrenamiento: se le ensena a dispararse o no dependiendo de determinados


patrones de entrada
Modo de uso: si se le entrega un input conocido, entrega el output ense nado. Si el input
es desconocido, se aplica la firing rule para determinar si se dispara o no.
Ej: 3 entradas, se le ensena disparar (output=1) si 111 o 101, y a no disparar (output=0)
si 000 o 001.
Si se ingresa 010, se aplica un criterio de cercana con los patrones conocidos: difiere
en 2 de 111, en 3 de 101, en 1 de 000, y en 2 de 001. El mas cercano es 000, y el output
por lo tanto es 0.
Ej: reconocer T y H

Entradas con pesos. Las entradas con pesos se suman, y si estan sobre un cierto valor
umbral, la neurona dispara. Estas neuronas pueden adaptarse cambiando sus pesos y/o su
umbral.

Referencias:
http://www.doc.ic.ac.uk/~nd/surprise_96/journal/vol4/cs11/report.html
http://www.willamette.edu/~gorr/classes/cs449/intro.html
http://www.ai-junkie.com/ann/evolved/nnt1.html
128 CAPITULO 3. METODOS NUMERICOS AVANZADOS

3.3. Automatas celulares


(Basado en Fenomenologa 2009.)

Modo de integracion numerica ODE o PDE

Historia: idea de von Neumann, de maquina autorreplicante. Eventualmente, modelo compu-


tacional (estaba comenzando esa era en ese momento), y mostro que era posible tener
sistemas que fueran capaces de autorreplicarse.

Estos son los elementos que uno debe tener para construir un automata celular:

Discretizacion del espacio: uno divide el espacio en una grilla de cierto tamano.
Estado de las celdas: uno debe decir en que estados puede estar cada celda, y que significa
(ej.: 1/0, encendido/apagado, viva/muerta, etc.).
Discretizacion del tiempo: dada una cierta configuracion inicial dada de las celdas, uno
hara evolucionar el sistema de acuerdo a ciertas reglas, para llegar a un tiempo t1 , y
as sucesivamente.
Reglas de evolucion: se deben dar reglas que permiten determinar, dada una configu-
racion de celdas en tn , la configuracion de celdas en tn+1 . Las reglas son usualmente
sencillas e involucran el estado de la propia celda a evolucionar y las celdas vecinas. Son
posibles muchas variaciones en este aspecto: que significa celda vecina (las celdas a
cada costado, las cuatro en los cuatro puntos cardinales, las ocho vecinas, tomar celdas
a segundos vecinos, etc.), y por supuesto que tipo de regla se va a usar.
Ejemplo sencillo. (Hacer con cuadritos pintados en vez de numeros.) 111->0, 110->1, 011->0, 010->0,
O sea, la unica manera de que la celda quede viva es que tenga una celda viva a su iz-
quierda solamente. Evolucionaremos estas reglas en una grilla de 33, con condiciones de
borde periodicas. Dibujamos una H, evolucionamos una vez, quedan dos celdas pintadas,
evolucionamos otra vez y desaparece todo.
Este es un caso particularmente sencillo y quizas poco interesante, pero de esto, con
algunas variaciones, se trata siempre hacer un automata celular.
Wolfram sistematizo el estudio de automatas celulares unidimensionales.
Ejemplo, Rule 30. (Rule30.html)
Explicar por que 30, explicar regla de evolucion, explicar figura hasta 16 iteraciones
(todo esto en detalle, porque servira para la tarea, en que simplemente se les pide llegar
a mas iteraciones).
Lo interesante es que, al hacer evolucionar esto, emerge un patron que a uno le recuerda
cosas que ha visto. Mostrar concha de cono textil. En el fondo esto tiene sentido, porque
uno aprende que una celula va a expresar un determinado pigmento o no dependiendo
de las interacciones con sus vecinos. . . y eso es un automata celular! Tiene que resultar!
Uno empieza a mirar alrededor entonces, y se da cuenta de que en todas partes hay
patrones complicados, y uno dice: Como es posible que las cebras tengan esas rayas?
Como es posible que los leopardos tengan esas manchas? Y las jirafas esas manchas?
3.3. AUTOMATAS CELULARES 129

Y uno ve que, quizas, aca esta la respuesta. No se necesitan reglas complicadas, sino sen-
cillas y locales. Sera posible hacer un automata celular que de origen a las rayas de una
cebra. . . ? S! A las manchas de una jirafa. . . ? S! Lo cual, nuevamente, nos confirma
que, quizas, lo que acabamos de hacer dice algo profundo acerca de la Naturaleza.
Pilas de arena
El modelo es muy sencillo: es un automata celular. Pensemos en una dimension. Discre-
tizamos el espacio en cierto numero de sitios. Al comenzar, ponemos un grano de arena
en el sitio de mas a la izquierda. En la siguiente iteracion, otro en el mismo lugar, y
as sucesivamente. Ahora, diremos que cuando la diferencia de altura entre dicho sitio
(1) y el siguiente (2) supera una cierta altura crtica hc (por ejemplo, 4), el ultimo grano
cae hacia la derecha. Si sucediera ahora que, despues de mover el granito hacia el sitio
2, la diferencia entre el sitio 2 y el 3 es mayor que hc , entonces volvemos a mover el
granito hacia la derecha, y as sucesivamente, hasta que se llega a una situacion en que
ninguna diferencia es mayor que hc . Decimos entonces ue el sistema ha relajado. A todo
el conjunto de eventos que ocurrio desde que se agrego el granito que desencadeno la
relajacion, hasta que la relajacion concluye, le llamamos una avalancha.
Procedemos entonces as: colocamos un granito en el sitio 1. Si h > hc para algun sitio,
movemos el granito un sitio a la derecha. Volvemos a revisar la condicion y mover un
granito si corresponde, hasta que h < hc en todas partes. Entonces dejamos caer otro
granito en el sitio 1. Y as sucesivamente.
Discutir la condicion de borde.
Mostrar el codigo. (sandpile.cc)
Simulacion con N = 5 sitios y pocas iteraciones. (sandpile.param) Mostrar el archivo
de texto, donde se ven los numeros, y ver el estado final: una piramide, una pila de
arena.
El modelo anterior se denomina un sandpile model, y es este modelo tan sencillo el que
se nos propone como representante de una gran clase de procesos fsicos.
Ahora que sabemos como responde el sistema, con avalanchas, podemos preguntarnos
por el tamano de ellas. Mediremos el tamano contando el numero de sitios involucrados
en una avalancha dada. En el archivo de texto, la ultima columna nos dice, con un 1,
si hubo algun movimiento de granitos, y por lo tanto, el problema se reduce a contar el
numero de unos consecutivos, y eso diremos que es el tamano de la avalancha.
Grafico de tamano de avalanchas. (plot_sandpile_size.m) No es muy lindo, pero se ve
que hay alguna irregularidad al comienzo, y de ah en adelante es constante. En lenguaje
conocido, diramos que hay un transiente, y que luego de el el sistema cae en un atractor
o punto fijo estable. Interesante.
Corramos el sistema durante mas iteraciones (5000), para tener mas estadstica. Ve-
mos que sucede lo mismo. Visualizamos el estado final, una pila de arena, y el tamano
evoluciona igual, llegando a un punto fijo estable.
Tratemos de entender. Pensemos en el sistema en el estado final, una linda pila de arena.
Cuando ha transcurrido suficiente tiempo, podemos pensar h = hc en todas partes (si
fuera mayor, relajara y no sera estacionario). Entonces, al agregar un granito en el sitio
130 CAPITULO 3. METODOS NUMERICOS AVANZADOS

1, h = 4, y el granito tiene que correrse a la derecha. Pero ahora tambien h = 4,


en el sitio siguiente, y tiene que correrse el granito al sitio 3. Y as sucesivamente: el
granito cae de sitio en sitio, y el numero de sitios involucrados en la avalancha es justo la
longitud del sistema: ese es el tamano de las avalanchas siempre. Y eso es justamente lo
que dice nuestro grafico: 20 sitios, avalanchas de tamano siempre despues del transiente.
As que el problema en una dimension no era tan interesante despues de todo, podramos
haber adivinado perfectamente lo que iba a suceder. Sin embargo, hemos desarrollado
un modelo que tiene algunas caractersticas importantes que luego nos van a servir.
Tecnicamente, lo que hemos hecho es tener un sistema que es estimulado en una cierta
escala de tiempo (el granito agregado en el sitio 1) que es mucho menor que la escala
en que el sistema relaja, o disipa energa (la avalancha). Esta separacion de escalas de
tiempo es crucial para este modelo.
El segundo elemento importante de este modelo es que existe una magnitud umbral,
tal que cuando es superada, se gatilla una avalancha. El sistema, entonces, responde a
perturbaciones infinitesimales (un grano de arena contra posiblemente muchos que tiene
la pila de arena), con avalanchas.
Bajo estas condiciones, lo que ha sucedido es que el sistema se ha organizado en un estado
tal que es globalmente estable, mirado desde lejos. Porque si lo miramos de cerca, estan
ocurriendo avalanchas todo el tiempo. Pero de lejos, la forma del sistema es siempre la
misma: una pila de arena.
Ahora complejicemos un poco el sistema, y veamos que es lo que ocurre en dos dimen-
siones.
La regla de evolucion es la misma. Agregaremos granos en el centro de una grilla cua-
drada. Ahora, si la diferencia de altura entre un grano y alguno de sus cuatro vecinos en
lnea recta es mayor o igual que cuatro, entonces se produce una avalancha, repartiendo
los cuatro granos de exceso entre sus cuatro vecinos, uno para cada uno. Al cabo de un
cierto tiempo, debera producirse una pila de arena, con el punto mas alto en el centro
de la grilla.
Correr simulacion con N = 5, y 10 iteraciones, para mostrar el output, el conteo de sitios
activos, y el estado final (una meseta mas que una pila, en este caso). (sandpile_2d.cc,
sandpile_2d.param, sandpile_2d.dat)
Correr simulacion con N = 10 y 400 iteraciones, para mayor estadstica, sin tirar las
fotografas a un archivo. (plot_sandpile_size_2d.m)
La evolucion del tamano de los eventos ahora es completamente distinto: no parece
haberse llegado a un estado estacionario, sino que hay siempre avalanchas pequenas y
grandes, sin ninguna regularidad. Hay algo como un transiente al principio, pero de
ah adelante, esta este estado en que el tamano de las avalanchas no es predecible. Y
ojo, que la perturbacion sigue siendo infinitesimal.
Lo que ha ocurrido, entonces, es que el sistema responde a perturbaciones a avalanchas
que son potencialmente de cualquier tamano. Por esto, en analoga a sistemas termo-
dinamicos en transiciones de fase (explicar), decimos que el sistema esta en un estado
crtico, donde el sistema puede responder de manera global a perturbaciones infinitesi-
males. Esto explica una parte del ttulo del paper de BTW.
3.3. AUTOMATAS CELULARES 131

(How Nature Works)


Hoy vamos a hablar de esto. En 1988, Bak escribio un paper proponiendo un modelo
extremadamente sencillo, pero que presenta caractersticas comunes a una enorme familia
de problemas. De alguna manera, el modelo propuesto captura la esencia de una gran
multitud de fenomenos, y, con justicia, es uno de los papers mas influyentes en la historia
de la Fsica, dando origen a toda una nueva area de investigacion cientfica, muy fertil,
extremadamente interesante, y todava muy activa. En este artculo se propone el estudio
de un fenomeno muy particular, que los autores bautizan self-organized criticality,
como dice el ttulo del paper. En esta clase, intentaremos entender el significado de
este ttulo, y la profunda consecuencia que han tenido estos estudios en la imagen que
tenemos de la naturaleza.
A diferencia de la criticalidad en transiciones de fase, aqu no hay un parametro externo,
como la temperatura, que yo este ajustando para llegar al estado crtico. Aca, hay un
unico parametro libre, hc , y podemos darnos cuenta de que el sistema llegara al mismo
estado, independiente de hc . Ademas, no estoy perturbando mucho el sistema, solamente
agregando granitos de vez en cuando, y cuando hay avalanchas dejo que el sistema relaje,
y no lo vuelvo a perturbar hasta que termina. Por ello, decimos que este estado crtico
es auto-organizado. Y ello explica la otra parte del ttulo.
Lo que tenemos entonces es que este modelo de pila de arena presenta el fenomeno de
self-organized criticality (SOC).
Magnetosfera, terremotos (modelo OFC)
Diffusion Limited Aggregation (DLA)
dla.cc, dla.eps
Como decamos, los automatas celulares tenan su propia historia, pero saltaron a la
fama en 1970, con un automata celular particularmente interesante disenado por John
Conway en 1970, y que se llamaba El Juego de la Vida (Game of Life). Este juego
aparecio comentado en una revista de divulgacion cientfica, Scientific American, en 1970,
en un artculo de Martin Gardner, el cual lanzo a la fama al juego y a los automatas
celulares en general.
Mostrar version en XEmacs. Uno parte con cierta condicion inicial, y el sistema comienza
a evolucionar de maneras extranas.
Las reglas son sencillas:
Una celda viva, muere si tiene uno o ningun vecino, de soledad.
Una celda viva, con cuatro o mas vecinos muere, por sobrepoblacion.
Una celda viva, con dos o tres vecinos, sobrevive.
Una celda muerta, con tres vecinos vivos, se vuelve viva.
Lo interesante, y esta es la clave de los automatas celulares, es que a partir de estas reglas
sencillas, al hacerlas evolucionar, aparecen comportamientos completamente inesperados.
xlife
Mostrar que ponemos una condicion inicial arbitraria, y evoluciona, y de hecho evoluciona
a ciertas estructuras estables.
132 CAPITULO 3. METODOS NUMERICOS AVANZADOS

Estructuras fijas: block (ver en detalle en la pizarra, es evidente que va a ser una estruc-
tura invariante; mostrarla en xlife), beehive, boat, ship, loaf (mostrarlas solamente en
pizarra y en xlife).
Estructuras oscilantes: blinker, toad (en pizarra y xlife).
Estructuras moviles: gliders. (Pizarra.) Evolucionarlas en xlife. Esto es impresionante.
Es una ameba! Hemos creado, con reglas completamente sencillas, un bicho que repta
en este mundo de dos dimensiones.
Estructuras evolucionantes: Queen Bee Shuttle. (Pizarra.) Evolucionarlas en xlife. Avan-
za, se deforma, deja un beehive, se devuelve, deja otro beehive, y vuelve a devolverse.
(Esa es la razon del nombre.) Completamente inesperado.
Nada de esto estaba en las reglas iniciales, y sin embargo ah esta. Ese es el punto
central. Reglas sencillas, locales, dan origen a comportamientos globales organizados.
Es lo que uno llama comportamientos emergentes. Y eso es lo que da origen a un
area muy fascinante de la Fsica, que son los Sistemas Complejos. Sistemas en que
interacciones locales, de corto alcance, de alguna forma dan origen a comportamientos
globales organizados, coherentes, que uno llama emergentes, que no es evidente que
esten contenidos en las sencillas reglas de interaccion originales.
Observar que, para una semilla inicial dada, lo que esta pasando aca, en el lenguaje de
caos que ya aprendimos, es que el sistema, despues de un transiente, llega a los atractores,
a los puntos fijos estables, y hay algunos de perodo uno, otros de perodo dos, etc.
Podemos usar el mismo lenguaje adquirido antes para describir el comportamiento de
un automata celular.
Hay otros automatas celulares posibles. Unos particularmente interesantes son los siste-
mas presa-predador. En ellos, uno tiene celdas de dos tipos, presa o predador. El resto
es escencialmente lo mismo.
Mostrar el ejemplo en JavaScript.
Correrlo de a una iteracion, con los parametros default, simplemente para mostras que
conejitos y lobitos parecen evolucionar como uno espera: los conejitos arrancan a per-
derse, alejandose de los lobitos, y los lobitos los persiguen.
Correr 25 iteraciones, para ver comportamiento global.
Mostrar el diagrama de espacio de fase.
Mostrar reglas de evolucion. Son sencillas y razonables.
Hay una parte no determinista, que es la tasa de crecimiento y muerte. Para cada celda,
ademas de determinar su estado futuro usando las reglas conocidas, se lanza un dado,
es decir un numero al azar. Si el numero esta bajo/sobre un cierto valor, que se ajusta
a conveniencia, eso determina un comportamiento u otro.
Otro ejemplo: bandadas de pajaros. Pelcula.
Los pajaros evolucionan coherentemente, rodean obstaculos, etc., como si fueran inteli-
gentes, pero en realidad todo esto esta determinado por reglas de comportamiento muy
sencillas.
3.3. AUTOMATAS CELULARES 133

En realidad, esto no es automata celular, porque no hay discretizacion del espacio y del
tiempo, pero la idea es similar.
Reglas: separacion (dos pajaros trataran de no estar demasiado cerca), alineamiento (un
pajaro tratara de moverse en la direccion promedio de sus vecinos), cohesion (un pajaro
tratara de moverse hacia la posicion promedio de sus vecinos). El resultado es, como
siempre, completamente inesperado, organizado.
Uno empieza a entender, entonces, que la complejidad de la Naturaleza no emerge,
necesariamente, de la complejidad, sino de reglas sencillas de comportamiento.
Ademas de lo anterior, estos experimentos computacionales permiten avances en la ani-
macion digital. Por ejemplo, en Batman Returns de Tim Burton, las secuencias con
bandadas de murcielagos fueron creadas precisamente con versiones modificadas del soft-
ware que acabamos de ver.
Otro ejemplo es la secuencia de la estampida en The Lion King. Mostrar video.
Todo esto es un mundo muy fascinante y, como hemos dicho, nos permite descubrir algo
aparentemente profundo acerca de la Naturaleza.
Mostrar fotos de estorninos en Roma, particularmente famosas, admirarse de las estruc-
turas completamente complicadas y fascinantes que los pajaros describen.
Mostrar fotos de portada de Physics Today de Octubre 2007, y como estos sistemas
uno puede, al menos, imaginarse que pueden ser descritos por automatas celulares o por
reglas sencillas de interaccion.
Lo mismo se hace con cardumenes de peces (mostrar foto), colonias de hormigas (mostrar
foto), evacuaciones de peatones de un recinto cerrado. . . Todos estos comportamientos
colectivos pueden ser descritos por reglas mas o menos sencillas.
Hasta ahora, hemos visto automatas celulares como generadores de patrones de com-
portamiento emergentes. Tambien uno puede usarlos, y esto es otro mundo en s, para
generar patrones espaciales emergentes.

Self-organized criticality
134 CAPITULO 3. METODOS NUMERICOS AVANZADOS

3.4. Algoritmos geneticos y Montecarlo

Potrebbero piacerti anche