Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Departamento de Fsica
Facultad de Ciencias
Universidad de Chile
Vctor Munoz G.
Jose Rogan C.
Indice
iii
iv INDICE
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
#include <iostream>
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
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; // 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.
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).
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;
int i = 10;
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:
produce el output
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.
#include <iostream>
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>
int main(){
int i,j;
cout << "Ingrese dos numeros enteros: ";
cin >> i >> j;
cout << "Los numeros ingresados fueron: " << i << " y " << j << endl;
return 0;
}
5 10
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
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.
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;
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.
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;
}
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.
int i=0;
while (i < 3){
14 CAPITULO 1. UNA BREVE INTRODUCCION A C++
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.
#include <iostream>
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>
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++
#include <iostream>
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
#include <iostream>
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;
}
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 potencia(double,int);
potencia(5, 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>
void mensaje(int);
void mensaje(double);
int main(){
int i=3;
double r=6.3;
mensaje(i);
mensaje(r);
return 0;
}
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.
main f
int main(){ k
k = 1964; 1964
k k
f(k); 1964 1964
k
... 1964
#include <iostream>
void f(int);
int main(){
int k=1964;
f(k);
cout << k << endl;
return 0;
}
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>
int main(){
int k=1964;
f(k);
cout << k << endl;
return 0;
}
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.
int main(){
int k=1964;
f(k);
1.3. FUNCIONES 25
int main()
{
double r1 = raiz(5);
double r2 = raiz(5,3);
...
return 0;
}
De este modo, todas las siguientes formas de llamar a la funcion son aceptables:
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);
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).
#include <iostream>
int f();
int main(){
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);
}
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; ...
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
#include <iostream>
int main()
{
int dim = 5;
double matriz[dim] = {3.5, 5.2, 2.4, -0.9, -10.8};
PrintMatriz(dim, matriz);
return 0;
}
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>
int main(){
string palabra1="Hola";
string 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>
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++
#include <iostream>
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;
}
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
#include <iostream>
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).
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>
int main(){
ofstream nombre_logico("nombre_fisico.dat");
int i = 3, j;
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>
int main(){
ifstream nombre_logico("nombre_fisico.dat");
int 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>
int main(){
fstream nombre_logico;
nombre_logico.open("nombre_fisico.dat",ios::out);
int i = 4,j;
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++
#include <iostream>
#include <fstream>
int main(){
ofstream nombre_logico("nombre_fisico.dat",ios::app);
int i = 3, j;
return 0;
}
#include <iostream>
#include <cmath>
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>
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.
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>
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;
return 0;
}
El resultado sera que en pantalla apareceran los numeros 2 y 3, que son las partes real e
imaginaria del objeto z.
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++
class Complejo{
private:
double real;
double imag;
public:
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};
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>
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();
};
double Complejo::get_real(){
return real;
}
double Complejo::get_imag(){
return imag;
}
int main(){
Complejo z;
z.set_real(2);
z.set_imag(-3);
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>
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.
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"
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"
int main () {
Complejo z;
z.set_real(2);
z.set_imag(-3);
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
class Complejo{
private:
double real;
double imag;
public:
Complejo();
void set_real(double);
void set_imag(double);
double get_real();
double get_imag();
};
#endif
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
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 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;
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:
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
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
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.
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)};
//----------Archivo complejo.h----------
#ifndef COMPLEJO_H
#define COMPLEJO_H
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:
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"
void f(Complejo);
int main () {
Complejo z = Complejo(-3.6,4.75);
Complejo w=z;
f(z);
return 0;
}
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.
//----------Archivo complejo.h----------
#ifndef COMPLEJO_H
#define COMPLEJO_H
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();
};
#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:
double b = z.get_imag();
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.
#include <cmath>
#include <iostream>
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);
En efecto, deseamos que el operador + acepte dos sumandos de tipo Complejo, y devuelva
un nuevo Complejo.
En complejo.cc estara la implementacion:
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
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
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
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++
int k = 3;
cout << k << endl;
//----------Archivo complejo.h----------
#ifndef COMPLEJO_H
#define COMPLEJO_H
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);
#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.
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:
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:
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>
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:
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>
int main(){
int i=3, j=5;
intercambio(i,j);
return 0;
}
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>
int main(){
int i=3, j=5;
intercambio<int>(i,j);
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:
(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>
El siguiente ejemplo muestra como se usara la clase Punto, tanto con int como double:
#include <iostream>
#include "punto.h"
int main(){
int i=3, j=5;
Punto<int> p_i;
p_i.set_x(i);
p_i.set_y(j);
Punto<double> p_d;
p_d.set_x(r);
p_d.set_y(s);
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
[(3.6,2), (-5000,-3)]
O incluso una clase de puntos, en que sus coordenadas sean, a su vez, puntos:
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);
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>
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();
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>
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
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>
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");
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:
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 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:
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>
class Poligono{
private:
int n_lados;
double perimetro;
public:
Poligono(int,double);
~Poligono();
int get_lados();
double get_perimetro();
};
Poligono::~Poligono(){
}
int Poligono::get_lados(){
return n_lados;
}
double Poligono::get_perimetro(){
return perimetro;
}
1.12. HERENCIA 73
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);
#include<iostream>
class Poligono{
protected:
int n_lados;
double perimetro;
public:
Poligono(int,double);
~Poligono();
int get_lados();
double get_perimetro();
};
Poligono::~Poligono(){
}
int Poligono::get_lados(){
1.12. HERENCIA 75
return n_lados;
}
double Poligono::get_perimetro(){
return perimetro;
}
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;
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++
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);
};
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:
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
<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.
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.
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
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.
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:
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.
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.
\documentclass[12pt]{article}
\usepackage{graphicx}
\begin{document}
Una figura:
\includegraphics{figura.eps}
\end{document}
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:
Ahora podemos incluir este comando como una regla adicional en el Makefile:
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++
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
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
$(figuras):%.eps:%.fig
fig2dev -L eps $< $@
clean:
rm texto.dvi texto.log texto.aux $(figuras)
figuras=figura.pdf otra_figura.pdf
$(figuras):%.pdf:%.fig
fig2dev -L pdf $< $@
clean:
rm texto.pdf texto.log texto.aux $(figuras)
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
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++
############################################################
# Makefile for C++ program EMBAT
# Embedded Atoms Molecular Dynamics
#
# You should do a "make" to compile
############################################################
#Compiler name
CXX = g++
#Linker name
LCC = g++
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:
Metodos Numericos
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.
#include <iostream>
91
92 CAPITULO 2. METODOS NUMERICOS
int main(){
int a[3];
int b[3];
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 reales:
No son ortogonales
1.01958e-56
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.
(1)s c bq .
Como no podemos poner infinitos numeros en el significante, ello impide representar nume-
ros reales con demasiados decimales.
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)
double nFactorial=1;
for(int i=1; i <=n; i++) nFactorial *=i ;
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).
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.
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:
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
fi (xi ) = yi
fi (xi+1 ) = yi+1
f1 (x0 ) = y0
f9 (x10 ) = y1 0
a)
2
f (t + ) = f (t) + f (t) + f (t) +
2
2
f (t + ) = f (t) + f (t) + f () ,
2
6. Error lineal en .
7. Metodo de Euler.
a) Notacion tn = n , f (tn ) = fn .
b)
8. Metodo de Euler-Cromer.
a)
d ) plot_proyectil_euler_cromer.m
e) Error es marginalmente menor que con Euler
a)
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
a) Error de truncamiento O( 2 ).
b) Segunda derivada centrada
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
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. Builtin: rand()
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.
a) random_number.cc
b) plot_random_number.m
a) random_class_example.cc
a) Rejection method
1) Se escoge una funcion para generar la distribucion, f (x).
2.4. NUMEROS ALEATORIOS 105
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
3. Generemos puntos sobre esta recta, con un cierto error anadido, e intentemos ajustarlo
de vuelta: lineal.cc, plot_data.m
6. lineal_ajuste.cc, plot_data_ajuste.m
8. Ajuste semilog:
y = Aex
ln y = ln A + x
9. Ajuste log-log:
y = Ax
ln y = ln A + ln x
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.
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
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 .
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
8. Regla de Simpson
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
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
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
13. Montecarlo
2.9. Sort
1. Bubble sort
2. Selection sort
3. Insertion sort
4. Mergesort
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:
ordenada: 1 3 8
g) Lo mismo con las siguientes dos:
sin orden: 0 9 5 7 (0<5)
ordenada:
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
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
6. Quicksort
2 = ,
3. Diferencias finitas
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
10. Notemos que lo que estamos haciendo es aproximar una derivada por los valores de la
funcion en puntos seleccionados.
13. Se puede usar una grilla no homogenea, pero hay que usar otro modo de aproximar las
derivadas.
17. Esto sugiere que, en general, cualquier interpolacion adecuada nos puede servir para
discretizar la 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.
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.
23.
2 2
+ = (x, y)
x2 y 2
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)
6. Estrategia:
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).
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.
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 ).
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
Condiciones de borde
1. Dirichlet
i+1 i
(xi ) = ,
x
se tiene
1 = ax + 0 ,
N = bx + N 1 ,
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)
h(0) = h(1) = 0 .
Luego: Z 1 Z 1
dx (x)h (x) = dx (x)h(x)
0 0
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.
Definiendo
Z 1
Aij = dx hj (x)hi (x) ,
0
Z 1
bi = dx (x)hi (x) ,
0
queda X
j Aij = bj ,
j
Transformada de Fourier
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
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
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