Ayudas para el desarrollo de programas

Vladimir Támara Patiño

2006. Dominio público.

Este escrito se dedica a nuestro Padre Creador, a su santo Espíritu y a Jesús su Hijo y maestro nuestro.
Agradecemos los aportes de Rafael Barros.

Capítulo 1  Repaso de C, C++, PHP y Java

1.1  Introducción a tipos, expresiones y asignaciones

Indicadores de logro: Para la ejecución de un lenguaje imperativo se emplea la noción de estado, el estado incluye memoria de datos, contador del programa que indica próxima instrucción a ejecutar y una pila.

La memoria puede accederse mediante variables, estas son nombres1 que representa posiciones de memoria. El tipo de una variable determina el espacio que empleará en memoria y la forma como se codificará la información.

La ejecución de las sentencias de un programa escrito en lenguaje imperativo se puede entonces describir en términos del estado.

1.1.1  Un programa de ejemplo

Como programa de ejemplo emplearemos uno que interactúa con el usuario empleando entrada y salida estándar para leer un entero e imprimir el sucesor, en cada uno de los 4 lenguajes. Además de la sintaxis de este programa en cada lenguaje, se presenta la forma de compilar y ejecutar en un ambiente tipo Unix.

C

#include <stdio.h>
    
int
main(int argc, char *argv[])
{  
  int k;
  printf("Por favor ingrese un número: ");
  scanf("%i",&k);
  k=k+1;
  printf("El sucesor del número que ingresó es: %i",k);
}
Si el anterior programa se mantiene en un archivo de nombre lee-esc-ent.c, desde la línea de comandos es posible compilarlo a código objeto (lee-esc-ent.o), después encadenarlo en un binario en formato ELF (lee-esc-ent-C) y ejecuta con:
$ cc -c lee-esc-ent.c
$ cc -o lee-esc-ent-C lee-esc-ent.o
$ ./lee-esc-ent-C

C++

#include <iostream>
using namespace std;
    
int
main()
{  
        int k;
        cout << "Por favor ingrese un número: ";
        cin >> k;
        k=k+1;
        cout << "El sucesor del número que ingresó es: " << k << "\n";
 return 0;
}
Si el anterior programa se mantiene en un archivo de nombre lee-esc-ent.cpp (note que la extensión típica para programas en C es .c, mientras que para programas en C++ es .cpp), desde la línea de comandos es posible compilarlo a código objeto (lee-esc-ent.o), después encadenarlo en un binario en formato ELF (lee-esc-ent-CPP) y ejecutarlo con:
$ c++ -c lee-esc-ent.cpp 
$ c++ -o lee-esc-ent-CPP lee-esc-ent.o
$ ./lee-esc-ent-CPP

PHP

Por favor ingrese un número:
<?php
        fscanf(STDIN, "%d\n", $k);
        $k=$k+1;
        echo "El sucesor del número que ingresó es: $k\n";
?>
De los cuatro PHP es típicamente interpretado (los otros requieren una compilación prevía). Si el anterior programa se mantiene en un archivo de nombre lee-esc-ent.php (la extensión típica para programas en PHP es .php), desde la línea de comandos es posible interpretarlo con:
$ php lee-esc-ent.php

Java

import java.io.*;

public class LeeEscEnt {
        public static void main(String args[]) {
                int k;

                System.out.print("Por favor ingrese un número: ");

                try {   
                        k=leeEntero();
                        k=k+1;
                        System.out.println("El sucesor es: "+k);
                }
                catch (IOException ioe) {
                        System.out.println("Error de E/S: " + ioe);
                }
        }

        static int leeEntero() throws IOException {
                int n=0;
                int sgn=1;

                int l=System.in.read();

                while(l!= -1 && (char)l==' ') {
                        l=System.in.read();
                }
                if (l=='-') {
                        sgn=-1;
                } else if (l=='+') {
                        sgn=1;
                }

                while(l != -1 &&  (char)l>='0' && (char)l<='9') {
                        n=n*10+(l-(int)'0');
                        l=System.in.read();
                }

                return n*sgn;
        }

}
Desde la línea de comandos este programa (mantenido en un archivo de nombre LeeEscEnt.java) se compila en bytecode (LeeEscEnt.class) que después puede ser ejecutado con la máquina virtual Java así:
$ export PATH=$PATH:/usr/local/jdk1.3.1-linux/bin/
$ javac LeeEscEnt.java
$ java LeeEscEnt

Sobre la compilación y ejecución

C y C++ son lenguajes diseñados para ser compilados, es decir las fuentes se traducen en un programa binario que puede ser ejecutado directamente por el hardware con asistencia del sistema operativo.

El proceso de compilación se divide en generación de código objeto de cada módulo y encadenamiento de los diversos códigos objeto que compongan un programa. Durante el encadenamiento también deben encadenarse las librerías que se empleen (excepto las librerías estándar de C que por defecto se encadenan automáticamente).

Por su parte las fuentes de una aplicación Java deben traducirse a un código binario (bytecode) para una máquina virtual (JVM). Para ejecutar tal código se requiere un simulador de esa máquina virtual.

En el caso de PHP tipicamente se interpreta una código fuente con un programa llamado interprete, este primero revisa la sintaxis del programa completo y después procede a ejecutarlo línea a línea, interpretando cada una en el ambiente sobre el cual funciona.

1.1.2  Tipos básicos


Tipo C99 C++ PHP Java
Booleanos bool bool (bool) o (boolean) boolean
Caracter char     char
Enteros small, int, long, los mismos de C (int) o (integer) byte, short, int,
  long long     long, char
Punto flotante float, double float, double (float) float, double
Cadena char * char *, string (string) String

En PHP no se declaran tipos de forma explicita, pero cada expresión escalar tiene uno de los siguientes tipos: booleano, entero, números de punto flotante o cadena. Otro tipo no escalar que podemos introducir ahora es NULL cuyo único valor es null (con cualquier capitalización). Toda cadena que se evalue como número (digamos en una suma) se convierte en número, puede ser flotante si incluye alguno de los caracteres `.', `e', o `E'. La longitud de cada tipo depende de la plataforma, por ejemplo típicamente (y en i386) un entero se representa en 32 bits, y el mayor flotante representable es aprox. 1.8e308 con precisión de 14 dígitos decimales (64 bits). Las constantes booleanas son true y false escritas con cualquier capitalización.

En Java los tipos básicos tiene longitud fija en todas las plataformas: byte 8, short 16, int 32 y long 64. char es un entero de 16 bits sin signo (para representar caracteres Unicode). Los datos tipo float son de 32 bits y los datos tipo double de 64. Los posibles valores para el tipo boolean son las palabras reservadas true y false.

Por el contrario en C y C++ la longitud depende de la plataforma. Para arquitectura 386 y compatibles de 32 bits los compiladores más populares emplean: char 8, short 16, int 32, long 64, long long 64, float 32 y double 64. Puede ver valores límites en /usr/include/sys/limits.h. En el caso de C para emplear el tipo bool, es indispensable emplear antes:
#include <stdbool.h>
Los valores posibles son true y false que en realidad son enteros, false se representa con 0, mientras que true con un valor diferente a 0.

Una cadena en C se representan como un vector de caracteres cuyo fin se marca con el caracter '
0'. En C++ puede emplearse la misma convención o puede usarse la clase string. En Java las cadenas son instancias de la clase java.lang.String.

1.1.3  Introducción a la declaración de variables

Las siguientes declaraciones de variables con sus tipos y posiblemente definición de un valor inicial son válidas en Java, C y C++:
int i;
int j=3, k;
En PHP no hay declaración de variables, el tipo de cada variable se infiere del uso y puede cambiar durante la ejecución. En el siguiente ejemplo la variable $i es de tipo entero en la primera línea, tipo flotante en la segunda (con valor 5.5) y de tipo cadena en la tercera.
$i=3;
$i=$i+2.5;
$i="1"+$i;
$i="x";

1.1.4  Introducción a la asignación

La asignación permite almacenar un valor en una posición de memoria típicamente representada por una variable (aunque también puede ser un apuntador en el caso de C y C++).

Los cuatro lenguajes que estudiamos emplean la misma sintaxis para asignación, Su forma más básica es: lvalue=rvalue; donde lvalue es típicamente el nombre de una variable y rvalue es una expresión cuyo tipo es el mismo de lvalue o que puede convertirse a tal tipo. Note que en PHP toda variable debe comenzar con el carcter `$'.

1.1.5  Lecturas recomendadas

En el caso de C y C++ ha habido grupos internacionales de la ISO que los han estandarizado. Los estándares oficiales no están disponibles en Internet, pero hay borradores publicados (ver [2] y [3]) que son muy buena referencia.

Los lenguajes PHP y Java no han sido sometido a un proceso de estandarización tan riguroso como C y C++. En el caso de PHP las referencias son la documentación oficial (ver [7] --que es pedagógica) y las fuentes del interprete. En el caso de Java está disponible en Internet una especificación que se ha considerado como estándar en Java (ver [12]). Para aprender Java puede usarse como referencia [5] o alguno de los tutoriales disponibles en Internet (e.g
http://www.particle.kth.se/fmi/kurs/PhysicsSimulation/index.html ).

1.1.6  Ejercicios para afianzar teoría

  1. Construya tablas de precedencia de operadores para tipos enteros en los 4 lenguajes (los de C y C++ son los mismos).

  2. Construya tablas de precedencia de operadores para tipos booleanos en los 4 lenguajes.

  3. Hay operadores de incremento prefijos y posfijos en los 4 lenguajes, ¿como es la sintaxis y cual es la diferencia entre ellos?x.

1.1.7  Ejercicios de programación

  1. Implemente y ejecute en cada uno de los 4 lenguajes un programa que solicite al usuario 2 valores enteros y presenta la suma, la resta, la multiplicación, el cociente del primero entre el segundo y el residuo.

1.2  Introducción a estructuras de control

Indicadores de logro: Dado que la ejecución de un programa imperativo incluye como parte del estado el ``contador del programa'' para indicar cual será la próxima instrucción a ejecutar, es importante saber como es modificado tal contador con cada instrucción.

Tras una asignación el contador del programa avanza a la instrucción siguiente. Para controlar el contador pueden emplearse instrucciones condicionales que omitirán o ejecutaran parte del código sólo cuando se cumplan ciertas condiciones (especificadas en una expresión booleana), así como ciclos que bajo condiciones especificadas por el programador podrán hacer retroceder el contador del programa para volver a ejecutar una o más instrucciones.

1.2.1  Condicionales

Los 4 lenguajes emplean la misma sintaxis para el condicional if:
if (exprb) {
     bloque
}
En la forma anterior se ejecutará el bloque de instrucciones sólo cuando la expresión exprb sea verdadera. En PHP también puede usarse la sintaxis alternativa:
if (exprb) :
     bloque
endif
En la forma que sigue se ejecutará el primero bloque si la expresión es verdadera y el segundo si la expresión es falsa:
if (exprb) {
     bloque
1
} else {
     bloque
2
}
En rigor si uno de los bloques de instrucciones consta de una sola instrucción no son necesarias las llaves que encierran el bloque, sin embargo procurando disminuir fallas se recomienda emplear las llaves en todos los casos (así se evita errar poniendo más instrucciones en lo que se cree que es un bloque con llaves cuando en realidad no las hay).

La sintaxis alterna para un if con else en PHP es:
if (exprb) :
     bloque
1
else :      bloque
2
endif
En PHP puede usarse elseif para hacer un condicional con diversos casos, en la sintaxis alterna quedaría:
if (exprb1) :
     bloque
1
elseif (exprb
2) :
     bloque
2
else :
     bloque
3
endif
En Java el tipo boolean no es compatible con tipos numéricos (i.e no es posible hacer conversión por ejemplo a int). Pero tanto en C como C++ la expresión del condicional (también llamada guarda) puede ser una expresión entera cualquiera, que si evalúa a 0 se interpreta como falso y en los demás casos representa verdadero. En PHP todos los tipos pueden evaluarse en contexto de un booleano, los valores numéricos son verdadero excepto los siguientes: false, 0, 0.0, "", "0", arreglo con 0 elementos, el tipo especial NULL y variables no asignadas. Esta suele ser una causa común de problemas; problemas que pueden evitarse empleando expresiones netamente booleanas en guardas, es decir expresiones que incluyan variables tipo bool, las constantes true y false y los siguientes operadores booleanos (presentados en su orden de precedencia, primero los de mayor precedencia):

Tipo de operador Sintaxis Asociatividad
Relacionales expr1 < expr2 na
  expr1 > expr2  
  expr1 <= expr2  
  expr1 >= expr2  
Igualdad expr1 == expr2 na
  expr1 != expr2
  expr1 === expr2
  expr1 !== expr2
Conjunción (y) exprb1 && exrpb2 Izquierda a derecha
Disyunción (o) exprb1 || exrpb2 Izquierda a derecha

En esta tabla expr1 y expr2 se refieren a expresiones comparables, es decir expresiones de tipo numérico o en el caso de C y C++ también pueden ser de tipo apuntador (aunque sólo tienen pleno sentido si se trata de apuntadores a un mismo arreglo). bexpr1 y bexpr2 se refieren a expresiones booleanas. Tenga en cuenta (y aproveche) que en los cuatro lenguajes la conjunción da falso si el primer operando es falso (sin evaluar el segundo) y la disyunción da verdadero si el primer operando es verdadero. En PHP otros operadores de igualdad de la misma precedencia de los presentados son === y !== que tienen en cuenta el tipo de los operandos, por ejemplo la primera de las siguientes expresiones en PHP es cierta, mientras que la segunda es falsa:
"3" == 3
"3" === 3
Otro condicional común a los 4 lenguajes es switch cuya sintaxis es:
switch (expr) {
   case const
1:
     bloque
1
   case const
2:
     bloque
2
   ···
   case const
n:
     bloque
n
   default:
     bloque
n+1
}
Este evalúa la expresión expr y compara el resultado con cada uno de los valores constantes const1, const2, ... constn para ejecutar desde el bloque correspondiente a la primera constante igual a la expresión evaluada hasta la primera instrucción break. Por esto es importante recordar agregar la instrucción break al final de cada bloque. En los 4 lenguajes la etiqueta default y su respectivo bloque es opcional, se ejecutará tal bloque bien porque la ejecución viene del bloque anterior sin break o bien porque ninguna de las constantes const1 a constn resultó igual al valor de la expresión.

En PHP la sintaxis alterna remplaza el { después del switch con : y el } final con endswitch. Tenga en cuenta que al interior de un switch continue actua como break ---para lograr que en un ciclo pase a la siguiente iteración usar continue 2.

1.2.2  Ciclos

Los 4 lenguajes soportan los mismos tipos de ciclos:
while (exprb) {
     bloque
}
Que repite el bloque mientras la expresión booleana exprb sea verdadera.

La sintaxis alterna en PHP es:
while (exprb) :
     bloque
endwhile
Otro ciclo común a los cuatro lenguajes es:
for (inst1; exprb; inst2) {
     bloque
}
Que primero ejecuta inst1 (típicamente consta de inicialización de variables --asignación-- posiblemente varias separadas por comas), después repite el bloque mientras la expresión booleana exprb sea verdadera, asegurándose de ejecutar inst2 después de completar cada ejecución del bloque (por esto inst2 suele constar de una o más asignaciones que actualizan variables para lograr que exprb llegue a ser falsa en algún momento). Su forma alterna en PHP es:
for (inst1; exprb; inst2) :
     bloque
endfor
Y la última forma de ciclo común es:
do {
     bloque
} while (exprb);
que ejecuta el bloque, después evalúa la expresión booleana exprb para continuar repitiendo este orden (bloque y después evaluación) mientras exprb sea verdadera.

Alteraciones al control de flujo

En ciclos hay dos instrucciones que alteran la ejecución típica:

1.2.3  Lecturas recomendadas

La mejor forma de entender y aprender a emplear condicionales y ciclos (y en general a programar) es programando.

Recomendamos de forma especial la sección sobre "Pruebas de Escritorio" de la guía sobre while de un curso virtual y gratuito de C organizado en la EAN en el 2001 [14]. De tal material (de dominio público) se han extraído porciones que se han incorporado a esta guía.

1.2.4  Ejercicios para afianzar teoría

  1. Que imprimirá cada una de las siguientes porciones de código cuando se ejecuten:

1.2.5  Ejercicios de programación

  1. Escriba en C++ un programa que pida al usuario un entero e imprima la tabla de multiplicar de ese entero (es decir el resultado de multiplicar ese entero por los números de 1 a 10).

  2. Escriba un programa en C que pregunte enteros al usuario hasta que él/ella introduzca el entero 5, después el programa debe presentar la suma de todos los enteros digitados (excepto el 5).

  3. Desde un interprete de comandos ejecute el programa arithmetic y observe la funcionalidad, después implemente su propia versión de ese programa en el lenguaje de programación que prefiera. Ayuda: En C y C++ puede generarse un número aleatorio entre 0 y 10, incluyendo al comienzo #include <stdlib.h> y empleando la expresión rand()%11. En PHP use rand(0,10). En Java lo mismo se logra importando al comienzo import java.lang.* y usando Math.round(Math.random()*11). Otra opción es implementar (en su lenguaje de programación) las siguientes funciones sugeridas en [3]:
    static unsigned long int next = 1;
    
    int rand(void)   // RAND_MAX assumed to be 32767
    {
           next = next * 1103515245 + 12345;
           return (unsigned int)(next/65536) % 32768;
    }
    
    void srand(unsigned int seed)
    {
           next = seed;
    }
    

1.3  Vectores y funciones

Indicadores de logro:

1.3.1  Arreglos

Para mantener en memoria una secuencia de datos la estructura más directa y sencilla que puede emplearse es un arreglo. Se trata de un área de memoria contigua para acomodar varios datos de un mismo tipo (i.e un mismo tamaño).

En C y C++ puede definirse la creación de un arreglo estático unidimensional para 20 enteros con:
int v[20];
los enteros de este vector se numeran de 0 a 19 y para asignarlos o examinarlos se emplean construcciones análogas a las siguientes:
v[3]=4;
printf("%i",v[19]);
En PHP no hay declaración de arreglos y se trata más bien de tablas asociativas, que relacionan índices con valores, son además de localización dinámica pues van localizando y liberando memoria a medida que se usan, por otra parte cuentan con el concepto de posición actual. Tanto los indices como los valores pueden ser de cualquier tipo. Algunos ejemplos de uso:
$v[3]=4;
$v['casa']='carro';
print_r($v);
Note que la tercera línea imprimirá el arreglo completo, relacionando cada índice definido con su valor --separando uno de otro con los caracteres: =>

También es posible crear arreglos multidimensionales, por ejemplo en C y C++ se declaran con:
int c[10][4][8];
permite indexar 320 elementos, c[0][0][0], c[0][0][1], ... c[0][0][7], c[0][1][0], ... c[9][3][7].

En Java, C y C++ el tipo de un arreglo puede ser cualquier tipo primitivo o uno definido por usuario (incluyendo estructuras o clases).

En C y C++ también pueden crearse arreglos en tiempo de ejecución usando apuntadores como se estudiará más adelante (ver 1.7.3), en Java y PHP los arreglos sólo pueden crearse en tiempo de ejecución y en Java son objetos. Por ejemplo en Java puede crearse un arreglo de 20 enteros2 con:

int[] jv=new int[20];

o bien con:

int jv[]=new int[20];

La forma de usar las posiciones de este vector es la misma que en C, C++ y PHP.

En Java también es posible crear un arreglo de arreglos, como la siguiente matriz de enteros de 20× 20: int mj[][]=new int[20][20];

así como crear e inicializar: int mj2[][]=1,2,3, 4,5,6, 7,8,9; int[] mj3[]=1,2,3, null;

C y C++ no ofrecen mecanismos para detectar cuando el programador sale de los límites de un arreglo, por ejemplo si v es un arreglo de 20 posiciones y se asigna:
v[20]=4;
se está escribiendo en memoria que no fue localizada para el arreglo, lo cual podría causar un comportamiento indefinido. En Java cuando el programador intenta escribir o leer de posiciones fuera de un arreglo se genera la excepción ArrayIndexOutOfBoundsException. PHP permite escribir en posiciones no definidas (quedan definidas con el valor que se escribe) y al leer posiciones no definidas se produce un error no fatal (Notice).

En C y C++ el tamaño de un arreglo estático declarado como variable global o variable local3 puede ser obtenido con ayuda del operador sizeof. Por ejemplo con la declaración int v[20]; la expresión sizeof(v) / sizeof(v[0]) equivale a 20. En Java siempre es posible obtener el tamaño de un arreglo con una expresión análoga a jv.length.

En C y C++ pueden definirse los valores iniciales de un arreglo como se hace en el siguiente ejemplo:
float af[3]={2.4, -3.2, 11.0};
PHP permite indicar explicitamente índices y valores, por ejemplo:
$af = array( 'a' => 'abeja', 'd' => 'danta', 'e' => 'elefante');
o indicar sólo valores ---en este caso los indices serán enteros comenzando con 0:
$af = array( 1, 3, 10, 17);

Uso de arreglos multidimensional en C y C++

En realidad un arreglo multidimensional en C y C++ es un vector que el compilador trata de manera especial. Las siguientes porciones de código son equivalentes en C y C++:
int m[3][3]={{1,2,3}, {4,5,6}, {7,8,9}};

m[0][0]=11;
m[2][1]=15;
y
int m[9]={1,2,3, 4,5,6, 7,8,9};

m[0*3+0]=11;
m[2*3+1]=15;
En otras palabras declarar un arreglo multidimensional de la forma

int m[t1][t2]...[tn]

es equivalente a declarar un arreglo unidimensional como

mp[t1× t2×... tn]

Y usar el primero como

m[i1][i2]...[in]

equivale a usar el segundo como

mp[i1× t2× t3... tn+i2× t3× t4... tn+... in-1× tn+in]

Por esto mismo al declarar e inicializar matrices, así como al pasar matrices a una función es indispensable pasar tamaños constantes de todas las dimensiones excepto de la primera, por ejemplo:
double saca(double mat[][5], int i, int j) {
 /* PRE: 0<=i, 0<=j<=4, mat[i][j] está localizado */
 return mat[i][j];
}
Para hacer funciones que reciban arreglos multidimensionales en general (con tamaños diversos) deben pasarse como arreglos unidimensionales
double saca_2(double mat[], int m, int n, int i, int j) {
  /* PRE: 0<=i<=m, 0<=j<=n mat tiene al menos m*n posiciones */
 return m[i*n+j];
}

Uso de arreglos en PHP

Además de los usos ya presentados PHP ofrece otras construcciones del lenguaje: ciclos con foreach, asignación a varias variables list, elección automática de índices con [] y examinar datos de la posición actual y pasar a la siguiente con each.

Hay dos formas de emplear foreach:
  1. foreach($af as $v) {
      echo "$v, ";
    }
    
    Recorre todas las posiciones del arreglo $af y en cada iteración asigna a $v el valor de una posición del vector $af. De forma que si el vector $af es 'x' => 1, 'y' => 2, 'z' => 'm' presentará 1, 2, m,

  2. foreach($af as $i => $v) {
      echo "$i ~ $v, ";
    }
    
    Recorre todas las posiciones del arreglo $af, en cada iteración asigna a $k la llave y a $v el valor de una posición del vector $af. Con el vector del ejemplo anterior la salida sería
    x~1, y~2, z~m
    
Pueden asignarse las variables $a, $b y $c con los valores de las tres primeras posiciones del vector $af con:
list($a, $b, $c)=$af;
Si hay más variables en el list que elementos en el arreglo, las que falten quedarán con el valor null.

Un nuevo índice entero para el arreglo $af se escoge automáticamente con:
$af[]='caiman';
each retorna el indice y el valor de la posición actual de un vector y avanza a la posición siguiente:
list($indice, $valor)=each($af);
reset devuelve la posición actual al primer elemento del vector.

1.3.2  Funciones

Una función es como un subprograma, permite aislar una porción de código tanto para organizar fuentes como para permitir reutilizar (llamar) la misma porción varias veces. Además de la porción de código (llamada cuerpo), una función puede tener parámetros, es decir valores que espera recibir al ser llamada y puede retornar un valor el cual debería ser recibido en el sitio o en los sitios desde donde se llame la función.

En el primer programa de ejemplo en Java (ver 1.1.1) se definieron dos funciones: main (que es punto de inicio de la ejecución) y leeEntero (que lee un entero de entrada estándar y lo retorna). Como parte del cuerpo de la función main se llama a la función leeEntero y el valor que tal función retorna lo almacena en la variable k de tipo entero. Note además que la variable k es local a la función main, por su parte leeEntero tiene dos variables locales: n y sgn.

En tiempo de ejecución cada vez que se llama una función en la pila (que hace parte del estado durante la ejecución de un programa) se agregan: la dirección de retorno4, los parámetros que reciba la función y las variables locales a la función.

En PHP una función se define con:

function nombre(parámetros)
{
     bloque
}


En C, C++ y Java la forma general de definir una función es:

tipo nombre(parámetros)
{
bloque
}


aunque en Java pueden indicarse excepciones que la función llamadora debería manejar (como se verá más adelante), así como modificadores antes del tipo (e.g static5 o public6.)

tipo será el tipo de valor que retorna la función o void si la función no retorna un valor (también se llama procedimiento a una función que no retorne valor alguno). El nombre de la función debe constar de una letra o _ seguido de caracteres alfanuméricos o _, aunque nos parece apropiada la recomendación de ([13]): ``los nombres de funciones deberían estar basados en verbos activos, quizá seguidos de sustantivos.''

En los cuatro lenguajes pueden no pasarse parámetros o pueden pasarse más de uno separando uno de otro con una coma. En C, C++ y Java de cada parámetro debe especificarse el tipo seguido del nombre del parámetro (se trata de un nombre arbitrario con los mismos requerimientos de nombres de variables.)

El primer programa de ejemplo en Java (1.1.1) la función main recibe un parámetro args que es un vector de cadenas. Las cadenas son los argumentos con los que se ejecutó el programa desde la línea de comandos. El siguiente ejemplo imprime todos los argumentos con los que es ejecutado un programa en Java:
import java.io.*;

public class SimpMain {

        public static void main(String args[]) {
                int i;
                System.out.println("Cantidad de argumentos ="+args.length);
                for (i=0; i<args.length; i++) {
                        System.out.println("args["+i+"]="+args[i]);
                }
        }
}
En C, C++ y Java el punto de inicio de la ejecución es una función main. En PHP se ejecuta toda instrucción de la primera línea hacia la última que no sea una clase ni una función. En el caso de C (o C++) el programa equivalente al anterior es:
#include <iostream>

int 
main(int argc, char *argv[]) {
        int i;
        cout << "argc=" << argc << "\n";
 cout << "sizeof(argv)/sizeof(argv[0])=" << sizeof(argv)/sizeof(argv[0]) << "\n";
        for (i=0; i<argc; i++) {
                cout << "argv[" << i << "]=" << argv[i] << "\n";
        }
        return 0;
}
Note que una función que recibe un arreglo en C y C++ no requiere indicar en el parámetro el tamaño (basta usar [] a continuación del nombre).

En PHP (4.3.0 o superior), el interprete para Unix (php) define las variables $argc y $argv con la cantidad de parámetros pasados desde la línea de comandos y un vector con los argumentos ---esto no ocurre en el módulo para Apache. Por esto el programa anterior sería:
<?php
echo "argc=$argc\n";
        foreach($argv as $k => $v) {
                echo "argv[$k]=$v\n";
        }
?>

1.3.3  Lecturas recomendadas

Puede ver detalles precisos sobre arreglos en la sección 8.3.4 del borrador con el estándar de C++ [2].

1.3.4  Ejercicios para afianzar teoría

  1. Revise la documentación del programa /usr/games/banner típico en sistemas tipo Unix, úselo y después revise las fuentes de ese programa en su implementación para OpenBSD (En un sistema OpenBSD las fuentes pueden estar en /usr/src/games/banner). ¿Para que son las opciones -d y -t que este programa puede recibir por línea de comandos? ¿Para que son los enteros del arreglo asc_ptr ?

  2. Busque en Internet un programa corto en Java que emplee arreglos y en el que se defina al menos dos funciones y expliquelo.

1.3.5  Ejercicios de programación

  1. En el lenguaje que prefiera haga una función que reciba como parámetros un vector vec, su longitud lvec y un entero n, y retorne la cantidad de veces que n aparece en vec.

  2. En el lenguaje que prefiera haga una función que reciba como parámetros dos vectores de caracteres junto con sus longitudes y retorne 1 si ambos vectores son iguales o cero si no lo son.

1.4  Retomando detalles de expresiones

Indicadores de logro:

1.4.1  Comentarios

En los 4 lenguajes puede iniciarse un comentario con /*, el comentario puede ocupar tantas líneas como lo requiera y termina en el primer */ que haya (no se anidan comentarios).

También puede emplearse // para iniciar un comentario que termina donde termina la línea, esto en PHP también puede lograrse con el caracter #.

Como dice en [13]: ``Los comentarios sirven para ayudar al lector a entender partes del programa que no se comprenden directamente en el código mismo. Si es posible, escriba código fácil de entender; mientras mejor lo haga, requerirá menos comentarios. El código bueno necesita menos comentarios que el malo.''

1.4.2  Conversión de tipos

Algunas operaciones convierten valores de un tipo a otro, esto puede hacerse explicito en una expresión poniendo como prefijo del valor a convertir el tipo al que se desea convertir entre paréntesis7, por ejemplo la expresión (int)3.4 equivale a 3. En C++ también puede emplearse float(3.4). Los posibles castings en PHP son: (int) (integer) que convierte a entero, (bool), (boolean) a booleano, (float), (double), (real) a flotante y (string) a cadena. En PHP la conversión de cadena a valor númerico se hace examinando el número entero o flotante con el que comience la cadena ---si la cadena no comienza con un número se convertira en 0.

Tipos en C, C++ y Java

En C, C++ y Java los rangos de unos tipos están incluidos en los rangos de otros, por ejemplo en C los tipos enteros están incluidos así: char Ì short int Ì int Ì long int Ì long long, y los tipos flotantes así float Ì double Ì long double. Puede hacerse conversión de un tipo de rango menor a uno de rango mayor sin dificultad, pero al convertir de un tipo de rango mayor a uno de rango menor si el valor convertido sobrepasa el rango del tipo al que se convierte se pierde parte de la información.

Para convertir de flotante a entero se descarta la parte decimal (mientras el entero resultante esté dentro del rango del tipo al que se convierte).

Con respecto a operaciones aritméticas, la operación se realiza en el mayor tipo de los operandos por ejemplo al sumar un float con un int el entero se convierte a float antes y se realiza entonces la suma en el rango de los float.

En una asignación el valor por asignar es convertido al tipo de la variable (o del lvalue) al que se asigna.

Al llamar una función, los argumentos son convertidos al tipo que la función espera.

En el caso de if en C++ la guarda es convertida a bool, mientras que en Java el compilador genera un error si la guarda no es de tipo boolean.

En Java el operador + se usa también para concatenar cadenas; si uno de sus operandos es cadena el otro es convertido automáticamente a cadena. También en Java hay conversiones no permitidas, por ejemplo de un tipo entero a bool.

Tipos en PHP

Este lenguaje es debilmente tipado.

1.4.3  Más sobre expresiones

Entre las expresiones más simples están las constantes de cada tipo primitivo, las cadenas y las variables. A partir de estas se emplean paréntesis8, llamadas a funciones, indexación de arreglos, operadores unarios, operadores binarios o un operador triario para formar expresiones más complejas.

En los cuatro lenguajes hay operadores de asignación que pueden abreviar el efectuar una operación y hacer una asignación (e.g i++).

Estas posibilidades pueden ayudar a escribir código muy difícil de entender o mantener, de ahí la importancia de [13]:

Enteros

Además de poder escribir constantes enteras en decimal (e.g 18) también es posible escribirlas en hexadecimal9 con el prefijo 0x por ejemplo 0x12 o en octal10 precediendo el número con 0 por ejemplo 022.

Como posfijos puede emplearse L o l para indicar que se hacer referencia un entero largo (long).

En C también pueden emplearse los sufijos u o U para indicar entero sin signo (i.e positivo) y ll o LL para indicar long long. Por ejemplo los siguientes son equivalentes:

Lenguaje Constante Equivale a
Todos 014 (int)12
Todos 0x12 (int)18
C y C++ 20u (unsigned int)20
C y C++ 100ul (unsigned long int)100

Algunas constantes predefinidas se presentan a continuación (para usarlas en C o C++ debe incluir el encabezado limits.h):

Significado Java C y C++
Máximo entero Integer.MAX_VALUE INT_MAX
Mínimo entero Integer.MIN_VALUE INT_MIN
Máximo entero largo Long.MAX_VALUE LONG_MAX
Mínimo entero largo Long.MIN_VALUE LONG_MIN

A los tipos numéricos en C y C++ puede agregarseles la palabra reservada unsigned para referirse a un tipo del mismo tamaño pero que sólo representa números positivos. Por ejemplo el tipo unsigned int representa enteros positivos de tantos bits como un int (e.g 32).

Números de punto flotante

En general se escriben en decimal y constan de una parte fraccional (i.e que puede tener signo, parte entera posiblemente separada de decimales con un punto), eventualmente un exponente con el prefijo e o E y eventualmente un sufijo. Algunos ejemplos son: Como posfijo en Java puede emplearse D para indicar que un flotante es tipo double (e.g 2.0D), mientras que en C y C++ pueden emplearse L (e.g 2.0L)

En C y C++ también pueden especificarse un flotante en hexadecimal si comienza con 0x y en tal caso si hay exponente debe usarse el prefijo p (o P) y el exponente debe escribirse en binario.

Algunas constantes predefinidas se presentan a continuación (en C debe incluir el encabezado limits.h):

Significado Java C y C++
Máximo flotante Float.MAX_VALUE FLT_MAX
Mínimo valor positivo flotante Float.MIN_VALUE FLT_MIN
Máximo double Double.MAX_VALUE DBL_MAX
Mínimo valor positivo double Double.MIN_VALUE DBL_MIN

En Java pueden usarse las constantes Float.POSITIVE_INFINITY y
Float.NEGATIVE_INFINITY para indicar infinito positivo y negativo respectivamente del tipo flotante así como Float.NaN para representar un valor que no corresponde a un número flotante (por ejemplo al dividir entre 0). También existen las constantes análogas para el tipo double cambiando Float por Double.

Por su parte en C y C++ pueden usarse entre otras las constantes FLT_EPSILON que es la diferencia f-1 siendo f el mínimo número representable mayor que 1 y sus análogas DBL_EPSILON, LDBL_EPSILON.

Caracteres

Aunque en los tres lenguajes el tipo char es numérico, es posible representar constantes de este tipo con caracteres generables con el teclado (i.e letras, números, signos excepto cambio de línea o retorno de carro). Para eso se pone el caracter entre apostrofes por ejemplo 'a', 'á', '.'. También pueden emplearse las siguientes secuencias de escape:

Secuencia de escape Significado ASCII
\b espacio atrás (BS) 8
\t tabulador 9
\n Cambio de línea (Line Feed, LF) 10
\r Retorno de carro (Carriage Return, CR) 13
\f Cambio de página (Form Feed FF) 12
\" Comilla (útil en cadenas) 34
\' Apostrofe (útil en caracteres) 39
\\ Backslash 92

Así mismo puede especificarse un número ASCII escrito en octal, por ejemplo '\323' es el caracter 'Ó' (ASCII 211).

En Java puede usarse el prefijo \u precedido de un número en hexadecimal de 4 dígitos (excepto \u000a y \u000d) para representar el caracter Unicode con el código dado11.

En C y C++ puede emplearse el prefijo \x para representar un caracter con un ASCII en hexadecimal. Las siguientes secuencias de escape son particulares de C y C++:

Secuencia de escape Significado ASCII
\v tabulador vertical 11
\a Alerta audible o visual 7
\? Interrogante 63

Cadenas

iteral-cadena

Las cadenas se colocan entre comillas, pueden constar de caracteres arbitrarios (con las mismas restricciones de literales tipo caracter) y pueden incluir las secuencias de escape recién introducidas12.

En C y C++ dos cadenas consecutivas se concatena por ejemplo
"Dios es " "amor"
es equivalente a "Dios es amor". En Java se concatenan cadenas con el operador +

Otros operadores

Entre los operadores aritméticos que pueden emplearse entre operandos de tipo numérico están (en orden de precedencia): Note que el programador debe cuidar no salirse del rango del tipo en el que se realiza la operación o en el que almacena el resultado.

También es posible realizar operaciones entre los bits de enteros (i.e los dígitos al escribirlo en binario): En Java los operadores &, ^ y | también se definen entre booleanos, & da el mismo resultado que &&, mientras que | da el mismo resultado de || pero & y | evalúan los dos operandos (mientras que && y || en caso de ser suficiente sólo evalúan el primero).

Los tres lenguajes soportan un operador condicional que espera 3 operandos e1 ? e2 : e3 que evalúa a e2 si e1 es verdadero y a e3 si e1 es falso. En Java e1 debe ser de tipo boolean.

Otro operador binario de C y C++ es el operador coma: e1 , e2 que evalúa e1 ignorando el resultado, para después evaluar y retornar el valor de e2. El siguiente ejemplo tomado de [3] ilustra su uso: f(a, (t=3, t+2), c) se trata de una llamada a la función f con 3 argumentos (note que la coma también se usa para separar argumentos pasados a una función). El valor del segundo argumento es 5.

1.4.4  Más sobre asignación

En los 3 lenguajes una asignación es una expresión, el valor que retorna es el valor asignado, por ejemplo la siguiente porción de código en C asigna 3 a la variable k y además imprime 3:
int k;
printf("%d\n",(k=3));
Por otra parte antes de asignar un valor a un lvalue el valor se convierte al tipo del lvalue por ejemplo en:

int r=-3.4;

la constante -3.4 es convertida a int y el valor resultante (i.e -3) es almacenado en r.

Además del operador =, pueden emplearse los operadores de asignación presentados en la tabla ?? (cada uno de ellos espera a su izquierda un lvalue y a su derecha un rvalue):

Operador Equivale a
e1 *= e2 e1 = (e1) * (e2)
e1 /= e2 e1 = (e1) / (e2)
e1 %= e2 e1 = (e1) % (e2)
e1 += e2 e1 = (e1) + (e2)
e1 -= e2 e1 = (e1) - (e2)
e1 <<= e2 e1 = (e1) << (e2)
e1 >>= e2 e1 = (e1) >> (e2)
e1 &= e2 e1 = (e1) & (e2)
e1 ^= e2 e1 = (e1) ^ (e2)
e1 |= e2 e1 = (e1) | (e2)

Table 1.1: Operadores de asignación además de =


También pueden usarse los operadores ++ y -- de incremento y decremento respectivamente. Pueden usarse como prefijo o sufijo de un lvalue, en general la forma prefija realiza el incremento/decremento antes de retornar el valor, mientras que la forma posfija realiza el incremento/decremento después de retornar el valor.

La siguiente porción de código al ejecutarse da:
i=1, j=0
i=1, j=1
lo que ilustra la diferencia entre la forma prefija y la forma sufija del operador ++:
#include <stdio.h>

void main()
{

  int i=0,j;

  j=i++;
  printf("i=%d, j=%d\n",i,j);

  i=0;
  j=++i;
  printf("i=%d, j=%d\n",i,j);

}

1.4.5  Variables predefinidas en PHP

Hay algunas variables predefinidas en PHP que por ejemplo permiten al interprete de PHP que funciona en Apache recibir datos que son enviados por un formulario:

Manteniendo estado Cookies y sesiones

Para mantener el estado del programa entre la ejecución de un script PHP y la ejecución de otro, pueden emplearse dos mecanismos: La siguiente porción de código ejemplifica el uso de cookies. Note que no se envia información al navegador sin hasta después de ejecutar la función setcookie. Pruebe ejecutando varias veces y entre una ejecución y otra cierre el navegador para comprobar que funciona:
<?php
$r="";
if (isset($_COOKIE['hola'])) {
        $r.="Ya definido en ".$_COOKIE['hola']."<br>";
        if (setcookie('hola', $_COOKIE['hola']+1, time()+60*60*24*30)) {
                $r.="incrementando<br>";
        }
        else {
                $r.="falló incremento<br>";
        }
}
else {
        $r.="inicializando en 0<br>";
        setcookie('hola', 0, time()+60*60*24*30);
}

?>
<html>
<body>
<?php echo $r; ?>
</body>
</html>
y una análoga con variables de sesión:
<html>
<body>
<?php
session_start();
if (isset($_SESSION['hola'])) {
        echo "Ya definido en ".$_SESSION['hola']."<br>";
        $_SESSION['hola']++;
        echo "incrementando<br>";
}
else {
        echo "inicializando en 0<br>";
        $_SESSION['hola']=0;
}

?>
</body>
</html>
En esta versión que usa las variables de sesión,

1.4.6  Lecturas recomendadas

La información más precisa sobre expresiones (incluyendo conversiones, literales y operaciones) la puede ver en los estándares o sus borradores de C ([3]), C++ ([2]) y Java ([12]).

1.4.7  Ejercicios para afianzar teoría

  1. Repasa conversión de una base a otra. a) Indique a que valor en decimal corresponden cada una de las siguientes expresiones: 0x22F, 0237 mostrando explícitamente como realiza la conversión. b) Escriba en hexadecimal y en octal el número decimal 124 (incluya el procedimiento de conversión).

  2. Es posible convertir rápidamente entre las bases 2, 8 y 16, pues un dígito en hexadecimal corresponde a 4 dígitos en binario y un dígito en octal corresponde a 3 dígitos en binario. Convierta el número binario 11001101 a octal y a hexadecimal. Convierta 0xF23 a binario. Convierta 0273 a binario y después a hexadecimal.

  3. En un sistema OpenBSD en plataforma x86, revise el archivo /usr/include/sys/limits.h y después escriba en decimal los rangos de los siguientes tipos: char, short, int, unsigned int, long.

1.4.8  Ejercicios de programación

  1. En el lenguaje Java escriba una función que reciba dos vectores de enteros, cada uno debe tener sólo unos y ceros y cada uno representa un número binario. La función debe implementar la multiplicación de los dos números en binario y debe retornar la respuesta como un vector de enteros.

  2. Este ejercicio es tomado de [13] (1-6). Pruebe la siguiente porción de código con todos los compiladores que pueda y consigne sus resultados en una tabla:
    n=1;
    printf("%d %d\n",n++,n++);
    

1.5  Apuntadores y referencias

Indicadores de logro:

1.5.1  Apuntadores y referencias

Un programa puede tener diversos tipos de memoria: Un apuntador corresponde a una dirección en memoria de datos, dirección que puede ser conocida y modificada explícitamente por el programador. El siguiente ejemplo de un programa en C ilustra esto (en plataformas donde los apuntadores tengan tantos bytes como los datos tipo int) :
#include <stdio.h>

int main() {
        char c='A';
        char *a=&c, *b;

        printf("a como apuntador es %p\n",(void *)a);
        printf("a como entero es %u\n",(unsigned int)a);
        printf("Tanto c como el caracter apuntado por a son: %c \n",*a);

        *a='X';

        printf("El valor de c después de modificar *a es %c: \n",c);

        return 0;
}
Este programa declara una variable c de tipo caracter y dos variables (a y b) que son apuntadores a caracter (note que el tipo de a ambos es char * en lugar de char). Este programa introduce además dos nuevos operadores prefijos utilizables en C y C++: Este programa también introduce los especificadores de conversión %p y %u de la función printf. El primero imprime un apuntador a void (todo apuntador puede convertirse de y a void *); mientras que el segundo imprime un entero sin signo (i.e unsigned int).

Estos operadores pueden emplearse tanto en C como en C++, ni en Java ni en PHP existe el concepto de apuntador (evitando así algunos errores de programación que suelen cometerse al usar apuntadores pero a la vez limitando posibilidades del programador quien ya está bastante separado de la máquina sobre la que corre el programa por la máquina virtual Java o por el interprete de PHP). Para lograr algunos efectos típicos de apuntadores se usan referencias. En Java todo tipo es bien primitivo (los tipos numéricos y bool.) o referencia (i.e vectores, clases e interfaces).

Hay un valor especial para apuntadores en C y C++ (NULL declarado en stddef.h) y uno para referencias de Java (null), que indica: este apuntador/referencia no está apuntando a una zona de memoria.

Entre las aplicaciones más comunes de apuntadores en C y C++ están: Puede consultar sobre varias de estas aplicaciones y el uso detallado de apuntadores en C en [15].

En C++ además de apuntadores pueden emplearse referencias para permitir que una función modifique el valor de una variable de la función llamadora. Por ejemplo:
#include <iostream>

using namespace std;

void inicializa(int &x) {
        x=20;
}

int main() 
{
        int y=0;
 int &z=y;
        inicializa(y);  
        cout << " el valor de y es " << y << 
                " y su dirección es " << (&y) <<"\n";
        cout << " el valor de z es " << z << 
                " y su dirección es " << (&z) <<"\n";
        return 0;
}
Este programa al ejecutarse imprime algo como:
 el valor de y es 20 y su dirección es 0xcfbc38bc
 el valor de z es 20 y su dirección es 0xcfbc38bc
Note que en la función inicializa se recibe un dato de tipo int & y que cout imprime también apuntadores (como entero en hexadecimal). Por su parte la variable z es una referencia a la variable y local a la función main.

Los tipos de datos que son referencias en Java (vectores, clases e interfaces) no requieren una declaración especial, por ejemplo la siguiente función modificará el vector que recibe también en la función llamadora:
import java.io.*;

public class Referencias {
        public static void main(String args[])
        {
                int k;
                int[] mv=new int[20];
                inivect(mv,5);

                for (k=0; k<mv.length; k++) {
                        System.out.print("mv[k]="+mv[k]+",  ");
                }
        }

        public static void inivect(int[] v, int val)
        {
                int i;
                for (i=0; i<v.length; i++) {
                        v[i]=val;
                }
        }
}
En PHP las referencias permiten accesar la misma información con diversos nombres. Para emplearlas puede usarse el operador =&. Por ejemplo
$a = 1;
$b =& $a; // Tanto $a como $b referencia la misma memoria que tiene un 1
$b = 2;
echo "a es $a\n" // Imprimirá 2
Note que el operador = crea una copia en lugar de una referencia, por ejemplo:
$v = array(1=>'x', 2=>'y', 3=>'z');
$w =& $v; // w y v referencian el mismo arreglo.
$z = $w;  // z referencia una copia de lo que había en w
$z[1]='uno';
print_r($z);  // imprimirá (1=>'uno', 2=>'y', 3=>'z');
print_r($v);  // imprimirá (1=>'x', 2=>'y', 3=>'z');
$w[1]='cero';
print_r($v);  // imprimirá (1=>'cero', 2=>'y', 3=>'z');
En funciones es posible recibir argumentos por referencia, de forma que el valor de una variable que se pase como parámetro desde otra función pueda ser modificado en la función llamadora. Por ejemplo:
function inc(&$n) 
{
 $n++;
}

$v=5;
inc($v);
echo $v; // imprimirá 6
Note que en el ejemplo anterior la variable $n se recibe por referencia (lo indica el operador & que le precede), si no se recibiera por referencia la copia.

1.5.2  Lecturas recomendadas

Puede conocer más sobre apuntadores en C en [15] y sobre referencias en Java en [12].

1.5.3  Ejercicios para afianzar teoría

  1. Tras revisar [3], enumere y describa los especificadores de formato de la función printf (Ayuda: son los mismos de la función fprintf).

  2. Elija una plataforma (procesador y compilador) y con un programa en C o C++ determine los tamaños de los siguientes tipos: int *, char *, void *, double * (Ayuda: use el operador sizeof el cual puede aplicarse bien a una variable o bien a un tipo, por ejemplo sizeof(int) da el tamaño en bytes de un dato tipo int mientras que si x es una variable (o una expresión) sizeof(x) corresponde al tamaño del tipo de x, si se trata de un vector estático declarado global o localmente sizeof da la cantidad de posiciones con el que fue declarado).

  3. ¿Hay equivalentes a malloc y free en Java? ¿Cómo se localiza memoria dinámica en Java y cómo se libera?

1.5.4  Ejercicios de programación

  1. Escriba equivalentes en C y en Java para el programa que emplea referencias en C++ (Ayuda: En C use apuntadores, en Java consulte sobre la clase java.lang.Integer).

1.6  Declaración, tipos y clases

Indicadores de logro:

1.6.1  Declaración

En Java, C y C++ pueden declararse variables antes de de definirse. En C y C++ también pueden declararse funciones antes de definirse, a tales declaraciones se les llama prototipos y sólo constan del comienzo de la función pero en lugar del cuerpo entre llaves seguidas de ;. Pueden verse ejemplos en el archivo encabezado conjunto.h que acompaña estas guías. También es posible preceder el prototipo de la función con la palabra reservada extern si se quiere hacer explicito que se trata de una función cuya definición está en un módulo diferente a aquel en el que se declara. Ni en PHP, ni en Java es necesario ni posible declarar funciones (y en PHP ni siquiera se declaran variables). En el caso de C y C++ las variables que estén fuera de toda función son variables globales que pueden ser usadas desde toda función y su duración es la misma del programa, es decir se reserva espacio para estas al inicio del programa y ese espacio sólo se libera cuando el programa termina (el espacio es reservado al comienzo del programa y liberado automáticamente al finalizar por código generado por el compilador). También pueden declararse variables locales a funciones. Tales variables sólo pueden usarse en la función en la que se declaran (i.e su alcance es sólo la función en la que se declaran) y por defecto duran mientras se ejecute la función es decir se crean cuando el control entra a la función y se liberan cuando la función retorna información o termina. En C y C++ este comportamiento puede cambiarse empleando la palabra reservada static para que la variable tenga la misma duración que la de una variable global pero cuyo alcance sea sólo la función en la que se declara (la inicialización que se haga en la declaración, se ejecutará sólo una vez en lugar de ejecutarse cada vez que entre a la función). En PHP las variables definidas fuera de funciones son globales y quedan referenciadas en el arreglo $GLOBALS (ver ??). En las funciones toda variable definida se supone local, excepto los arreglos superglobales predefinidos (como $GLOBALS) y las variables globales explicitamente declarada con global. En el siguiente ejemplo al interior de la función f, la variable a es local, mientras que b es la variable global:
<?php
 $a=1;
 $b=10;
 function f() 
 {
  global $b;
  $a=5;
  $b=5;
  echo "GLOBALS['a'] es ".$GLOBALS['a']."\n";
 }

 f();
 echo "a es $a\n";
 echo "b es $b\n";
?>
Tras ejecutarlo presenta:
GLOBALS['a'] es 1
a es 1
b es 5
En funciones de PHP también pueden declarse variables estáticas en el mismo sentido de variables estáticas de C y C++ con la palabra reservada static y pueden usarse variables para nombres de variables, anteponiendo el símbolo $, por ejemplo:
$b=0;
$a='b';
$$a=2;
echo $b;
Esta porción de código imprimirá 2, porque en la asignación $$a=2; debe entenderse $b=2; ---pues $a es justamente 'b'. En Java también es posible definir variables locales a una función, y aunque no existen variables globales, cada clase15 puede tener variables utilizables por todos los métodos de la clase (y por otras clases si se preceden de la palabra reservada public.) La palabra reservada static en Java sólo puede aplicarse a variables definidas en una clase (así como a funciones), a tales variables se les llama variables de clase, su duración es la misma que la de la clase y a diferencia de las otras variables (llamadas variables de instancia) pueden usarse sin necesidad de crear objetos de la clase. Por ejemplo en la clase Math del paquete java.lang se define la variable de clase PI que puede ser empleada sin crear objetos de la clase Math con la sintaxis Math.PI. En Java la palabra reservada public puede aplicarse a una clase (para hacerla utilizable por clases definidas en otros archivos), así como a variables y funciones (para hacerlas utilizables desde otras clases).

1.6.2  Constantes

En C (99) y C++ pueden definirse constantes (i.e como variables pero cuyo valor no cambia) usando la palabra reservado const, por ejemplo
const int K=5;
declara una constante K cuyo valor será siempre 5. Esta palabra reservada puede usarse también para parámetros de funciones. Como se sugiere en [13] en C es mejor emplear enum para declarar constantes enteras (pues pueden usarse como tamaño en arreglos, lo cual no ocurre con const), por ejemplo:
enum { K= 5,
J=3 };
declara las constantes K y J. En Java puede lograrse este efecto usando la palabra reservada final, (también utilizable con clases y funciones), por ejemplo en el archivo fuente java.lang.Math.java se encuentra:
    /**
     * The <code>double</code> value that is closer than any 
     * other to <i>pi</i>, the ratio of the circumference of a 
     * circle to its diameter. 
     */
    public static final double PI = 3.14159265358979323846;
Note que hemos incluido el comentario que hay en las fuentes y que describe la constante. En PHP se definen y usan constantes (de tipos boolean, integer, float y string) como se ejemplifica a continuación:
define("dbEngine","postgres7");
echo "el valor de la constante dbEngine es".dbEngine;
note que las constantes se usan como si fueran variables, pero no se preceden del símbolo $. El alcance de cada constante definida es global. Puede revisarse si una constante ha sido definida con la función defined() que recibe el nombre de la constante y retorna verdadero sólo cuando está definida. El valor de una constante también puede obtenerse con la función constant() y puede obtenerse la lista de las constantes definidas con la función get_defined_constants(). Por otra parte en C y C++ es posible, pero no recomendado, emplear el preprocesador para introducir constantes (que en realidad son macros), así por ejemplo la definición anterior también pudo haberse escrito como:
#define PI 3.14159265358979323846
El preprocesador es ejecutado normalmente por el compilador de C y C++, antes de compilar un programa. Este programa procesa las líneas que comienzan con el caracter #. Por ejemplo:

1.6.3  Tipos abstractos de datos y clases

Una técnica recomendable para programar y desarrollar código reusable es agrupar algunas estructuras de datos y algoritmos que operan sobre estas en Tipos Abstractos de Datos (TADs). Por ejemplo dado que es común emplear listas en diversos programas, una buena opción es programar un TAD para esto, probarlo y reusarlo. La idea es poder compilar versiones modificadas del TAD (por ejemplo versiones que tengan fallas corregidas o que tengan diferencias en la implementación) con cada uno de los programas que lo usen, sin modificar el programa que usa el TAD. Para lograrlo es importante contar con interfaces sencillas y claras. Estas interfaces son en realidad: nombre del tipo o tipos que conforman el TAD, variables y constantes definidas en el TAD y prototipos de funciones del TAD. En C puede hacerse un módulo por cada TAD. Por ejemplo para el TAD conjunto disponible junto con estas guías, se han declarado constantes, tipos, variables y prototipos de funciones públicas en conjunto.h mientras que la implementación de tales funciones se mantiene en conjunto.c (en este último podrían mantenerse otros datos y funciones privadas o auxiliares). Las fuentes de un programa que emplee tal TAD deben incluir el archivo encabezado y al momento de encadenarse debe encadenarse también el código objeto conjunto.o. Tal vez no sobre decir que cada TAD debería tener un programa que efectúe pruebas de regresión para facilitar la detección de fallas cuando se hagan cambios (o si se realizan las pruebas antes o al tiempo con la implementación y para detectar fallas rápidamente). En los lenguajes orientados a objetos se cuenta con elementos que permiten modelar TADs, se trata de clases las cuales encapsulan estructuras de datos y algoritmos (los algoritmos se implementan en funciones de la clase o métodos). Puede verse un ejemplo de una clase en Java en (1.1.1), a continuación se presenta una clase de ejemplo en C++:
class Pareja {

        public:
        int x;
        int y;

        int suma() {
                return x+y;
        }

        int resta() {
                return x-y;
        }
};
Esta clase podría probarse por ejemplo con:
#include <iostream>

int main() 
{
        Pareja p;
        p.x=1;
        p.y=2;

        cout << "suma: " << p.suma() << "\n";
        cout << "resta: " << p.resta() << "\n";

        return 0;
}
Lo análogo en PHP puede hacerse con:
<?php
class Pareja {
        var $x;
        var $y;

        function suma() 
 {
  return $this->x+$this->y;
        }

        function resta() 
 {
  return $this->x-$this->y;
        }
};

$p=new Pareja();
$p->x=1;
$p->y=2;

echo "suma: ".$p->suma()."\n";
echo "resta: ".$p->resta()."\n";
?>

1.6.4  Tipos

En C y en C++ pueden declararse nuevos tipos, fuera de funciones, con la palabra reservada typedef, usando el nombre del nuevo tipo como si fuera el nombre de una variable del tipo que se define. Por ejemplo: typedef char carac; declara un nuevo tipo de nombre carac y que corresopnde al tipo char. Después de esta declaración puede emplearse carac como tipo por ejemplo en carac x;

1.6.5  Ejercicios para afianzar teoría

  1. Las fuentes de las librerías estándar de Java pueden estar empaquetadas en el directorio de su distribución de Java en el archivo src.jar. Descomprima tales fuentes desde un directorio donde tenga permiso de escritura con jar xvf /usr/local/jdk1.3.1-linux/src.jar (remplazando la ruta por la apropiada para su plataforma). Después revise la implementación de la clase java/lang/Math.java y transcriba la declaración de la constante E allí escrita.
  2. Escoja una de las clases Integer, Double, Float y Bool (del paquete java.lang) y traduzca una parte de la documentación (ver [1]).

1.6.6  Ejercicios de programación

  1. En lenguaje C implemente el algoritmo de ordenamiento quicksort presentado en [13]. Impleméntelo en un módulo que tenga un archivo encabezado en el prototipo de la función. Después haga un programa para probarlo, preferiblemente usando check (ver guía sobre pruebas).
  2. Haga un programa que haga pruebas de regresión a la siguiente clase escrita en Java (Ayuda: idee su propio mecanismo de hacer pruebas de regresión o emplee una herramienta como JUnit):
    /**
     * Mathematical functions related with Wavelets
     * Sources in the public domain. 2004. No warranties.
     * @author Vladimir Tamara. 2000
     **/
    
    import java.lang.*;
    
    /**
     * Mathematical functions related with Wavelets
     * @author Vladimir Tamara
     **/
    public class WavMath
    {
    
        static double ZERO_JAVA=1.0E-12; // Numbers whit absolute value less are 0
        
      /**
       * Decides if a given integer is power of 2
       * @param n Integer to test
       **/
      static public boolean isPowerTwo(int n)
      {
        int i;  /* Counter (0..31) */
        int p2; /* p2=2^i */
    
        for (i=0,p2=1;i<31;i++,p2<<=1)
          { 
            if (n==p2)
              { return true; }
          }
        
        return false;
      }
    
    
      /**
       * Decides if a given integer is power of 2
       * @param n Integer to test
       **/
      static public boolean isPowerTwo(long n)
      {
        int i;  /* Counter */
        long p2; /* p2=2^i */
    
        for (i=0,p2=1;p2<n;i++,p2<<=1) {
        }
        
        return p2==n;
      }
    
      /**
       * Calculates integer part of a logarithm in base 2
       **/
      static public int log_2(int n)
      {
        int i;  /* Counter (0..31) */
        int p2; /* p2=2^i */
    
        for (i=0,p2=1;i<31;i++,p2<<=1)
          { 
            if (n<p2)
              { return i-1; }
          }
        
        return i-1;
      }
    }
    
  3. Investigue como puede separarse una clase en C++ para que la declaración quede en un encabezado (.h) mientras que la definición de las funciones de la misma quede en un archivo .c Modifique el ejemplo presentado en este capítulo para que siga las convenciones que propuso y pruebelo.


1.7  Estructuras de Datos

Indicadores de logro:

1.7.1  Cadenas

Las literales para cadenas constantes ya se introdujeron (ver ??), pero no se ha mencionado la implementación de cadenas en cada lenguaje.

En C y C++ una cadena es un vector de caracteres, los caracteres de la cadena comienzan en la posición 0 del vector y termina en la posición anterior al primer caracter con ASCII 0 que haya en el vector i.e '\0' (de forma que el espacio localizado para una cadena puede ser mayor que la longitud de la cadena).

En C, C++ y Java un arreglo puede inicializarse poniendo los valores entre llaves (pero sólo durante la inicialización, no en asignaciones posteriores), por ejemplo:
int v[]={1, 2, 4};  /* Creará un arreglo de tamaño 3 */
char cad2[5]={'x', 'y', 'z', 'a', 'b'}; /* Tamaños deben coincidir */
En el caso de cadenas en C y C++ pueden emplearse dos formas de inicialización adicionales:
char cad2[]="esperanza";
char cad3[4]="mano";
Para convertir cadenas en valores numéricos pueden emplearse las siguientes funciones de la librería estándar y declaradas en stdlib.h: En C++ además de poder tratar cadenas como vectores de caracteres, puede emplearse la clase string, que representa cadenas cuyo tamaño puede crecer en tiempo de ejecución. Esta clase define varios operadores y cuenta con variedad de métodos, algunos de los cuales se ejemplifican a continuación:

#include <string>
#include <iostream>

using namespace std;

int main() {

        string x;
        x="hola"; // Asignación de literal
        x+=" mundo"; // Operador + es concatenación
        cout << "x=" << x <<"\n";  // Operador << 
        cout << "x.length()=" << x.length() <<"\n";  // Longitud (o size)
        x.insert(2,"ar");
        cout << "x.length()=" << x.length() <<"\n";  // Longitud (o size)

        return string("hola").length();
}
En Java la situación es diferente porque una cadena NO es un vector de caracteres, es una instancia de la clase String. Tal clase representa cadenas constantes de caracteres Unicode. Incluso los literales para cadenas son objetos17 de esta clase, de forma que es posible escribir "cadena".length que equivale al entero 6 (length es un dato público de la clase String que mantiene la longitud de la cadena). Además el operador binario + crea cadenas implícitamente cuando uno de los operandos es una cadena (ver sección sobre memoria dinámica en esta guía).

En PHP una cadena tampoco es un vector de caracteres, es un tipo escalar, y se localiza y relocaliza dinámicamente para soportar cualquier longitud.

1.7.2  Estructuras y clases

En C es posible agrupar información en una estructura, por ejemplo la siguiente estructura agrupa un entero y un caracter:
struct ec {
        int e;
        char c;
};
La anterior definición de estructura podría contar con una declaración (que permitiría emplear la estructura antes de definirla) como:
struct ec;
Una estructura se comporta como un nuevo tipo de datos, tipo del cual pueden declararse nuevas variables. Los componentes (i.e campos) de la estructura pueden referenciarse con la sintaxis variable.campo tanto para extraer información como para asignarla:
void ejec() {
        struct ec x;

        x.e=5;
        x.c='A';

        printf("x.e es %d, x.c es %c\n", x.e, x.c);
}
Otro operador que puede emplearse con estructuras es -> , usado con apuntadores a datos cuyo tipo es estructura. La construcción apuntador->campo es equivalente a (*apuntador).campo.

Un concepto derivado del de estructuras es el de clases. En PHP y Java en lugar de definirse estructuras se definen clases y en C++ es posible definir bien estructuras o bien clases18.

1.7.3  Memoria dinámica

En el caso de C, C++ y Java el espacio en memoria para los datos declarados en un programa o en una función es localizado automáticamente por código generado por el compilador. En PHP todo el espacio es localizado mientras se interpreta.

En C, C++ y Java embargo también es posible que el programador reserve o libere memoria de manera explicita para algunos datos. Por ejemplo en Java puede reservarse memoria para un nuevo objeto de la clase String con:
String s;
s=new String("Dios nos ama");
Esta memoria será reservada en tiempo de ejecución. Si no hay suficiente espacio para reservarla se generará una excepción. En Java también reserva memoria en tiempo de ejecución el operador + entre cadenas, el cual a partir de dos cadenas (o convirtiendo uno de los operandos en cadena19) crea una nueva cadena que corresponde a la concatenación.

Dado que Java emplea un recolector de basura, el programador no necesita liberar la memoria que reserva. El inconveniente de tal recolector es que requiere ejecutarse en un hilo de ejecución propio, consumiendo más recursos en tiempo de ejecución.

En C y C++ para localizar memoria pueden emplearse algunas funciones de la librería estándar de C declaradas en stdlib.h: malloc, calloc y realloc. Todas estas funciones reservan espacio en memoria (la cantidad es especificada por parámetros) y retornan un apuntador a la zona reservada o NULL si no hay suficiente espacio. Para liberar memoria se emplea la función void free(void *ptr), que libera memoria apuntada por ptr. Note que no debe usar ni leer memoria que no haya sido localizada por el compilador (por ejemplo variables estáticas) o por usted.

En C++ puede emplearse new para localizar memoria y delete para liberarla. La siguiente porción de código muestra como reservar espacio para un entero y para un vector de 20 flotantes. Así mismo muestra como se libera la memoria reservada.
        int *i=new int(5);
        double *vd=new double[20];
...
        delete i;
        delete[] vd;
El operador new puede generar una excepción cuando la memoria solicitada no está disponible, para manejarla podría usarse:
        int *i;
        double *vd;

        try {
                i=new int(5);
                vd=new double[20];
...
                delete i;
                delete[] vd;
        }
        catch(bad_alloc) {
                cout<<"No hay memoria suficiente";
        }
Si no se desea o no es posible manejar excepciones puede usarse otra forma del operador new que retorna NULL si la memoria no puede localizarse:
#include<new>
...
        int *i=new(nothrow) int(5);
 if (i==NULL) {
  cout <<"No puede localizarse espacio para i";
  ...
 }
Como se explica más adelante en PHP se localizan objetos con el operador new y pueden destruirse variables (tanto escalares como objectos) con unset. La función isset permite determinar si una variable se ha establecido o no.

1.7.4  Apuntadores a funciones

En C y C++ es posible declarar y usar apuntadores a funciones (i.e la dirección en memoria de una función). Para referirse a la dirección de una función basta emplear el nombre de la función pero sin los paréntesis ni los parámetros. Para declarar un apuntador debe emplearse el prototipo de la función, sin los nombres de los parámetros pero solo con sus tipos y remplazando el nombre de la función por un asterisco y el nombre del apuntador ambos entre paréntesis.

Por ejemplo:
float sgn(float x) 
{
         if (x>0) {
          return 1;
         } 
         else if (x==0) {
          return 0;
         }
         else {
          return -1;
         }
}

float (*apfun)(float)=sgn;
En este ejemplo se define una función que recibe un flotante y retorna un flotante, después la dirección de tal función se asigna al apuntador apfun.

Estos apuntadores son útiles por ejemplo para implementar funciones más generales. La siguiente función calcula una aproximación a la derivada de una función:
float apder(float (*f)(float), float x, float h)
{
        assert(h>0);
        return ((f(x+h)-f(x))/h);
}
Note que la función anterior emplea assert para verificar una precondición de la función.

Junto con la función sgn antes definida podría usarse así:
int main()
{
        float d=apder(sgn, 10.0, 0.5);
        printf ("Aproximación a derivada en x=10.0 es %f\n", d);
        return 0;
}

1.7.5  Estructuras de datos

Empleado estructuras o clases es posible implementar estructura de datos como listas, árboles, grafos, de forma que puedan crecer o disminuir en tiempo de ejecución.

Como ejemplo puede considerarse la implementación del TAD Lista (doblemente encadenada con ventana) disponible en el sitio de distribución de estas guías (lista.c, lista.h y prueba_lista.c. En tal implementación cada nodo es del siguiente tipo:
struct sNodoLista {
        /*@shared@*/void *d;
        /*@shared@*/struct sNodoLista *sig;
        /*@shared@*/struct sNodoLista *prev;
};
Note que cada nodo tiene información (apuntada por d) un apuntador al siguiente nodo de la lista (sig) y un apuntador al anterior (prev), --si tuviera sólo un apuntador al siguiente nodo sería ``sencillamente encadenada''. Con esta implementación es fácil agregar nuevos nodos (cuya memoria ya esté localizada --bien estática o dinámicamente) o desencadenar nodos ya encadenados.

Una lista se basa en la siguiente estructura de datos:
struct sLista {
        int n; /* Cantidad de nodos */
        int c; /* Posición del nodo actual */
        /*@shared@*/NodoLista *head; /* Primer elemento */
        /*@shared@*/NodoLista *cur;  /* Elemento actual */
};
Note que se mantiene un apuntador al ``nodo actual'' (i.e la ventana).

En PHP el equivalente es:
class NodoLista {
        var d;     /* Información */
 var sig;   /* Referencia al siguiente */
 var prev;  /* Referencia al anterior */
};
class Lista {
        var n; /* Cantidad de nodos */
        var c; /* Posición del nodo actual */
        var cabeza;  /* Referencia al primer elemento */
        var actual;  /* Referencia al elemento actual */
};

1.7.6  Lecturas recomendadas

Puede ver detalles sobre localización de memoria en Java en la sección 4.3.1 de [12], el manejo de memoria en C estándar se describe en la sección 7.20.3 de [3].

Puede ver detalles de la implementación de algunas estructuras de datos en [13].

1.7.7  Ejercicios para afianzar teoría

  1. Consulte funciones para operar con cadenas en C (Ayuda: string.h).

  2. Consulte operadores y métodos para operar con cadenas usando el tipo string en C++.

  3. Consulte métodos para operar con cadenas en Java.

  4. En la implementación del TAD Lista mencionado en esta guía, revise la función lista_inv que chequea el invariante de una lista. ¿Qué convención se emplea para una lista vacía en los campos n y c ? ¿Que debe cumplir el apuntador sig del último nodo de la lista? -

1.7.8  Ejercicios de programación

  1. Implemente en C el algoritmo de búsqueda binaria descrito en el capítulo 2 de [13]. Pruebelo para buscar una cadena en un vector de cadenas.

  2. En C pruebe la función qsort para ordenar un vector de enteros. El vector debe ser localizado dinámicamente (recuerde liberar la memoria al terminar).

  3. Use el TAD Lista mencionado en esta guía en un programa que reciba enteros por entrada estándar (hasta recibir el entero 0) y los vaya agregando a una lista. Después debe imprimirlos en el mismo orden en el que los recibió (Nota: No olvide liberar memoria al final de su programa).

1.8  Librerías

Indicadores de logro:

1.8.1  Uso de bibliotecas estándar

Java

La librería estándar de Java consta de gran cantidad de clases organizadas en paquetes Algunos de los paquetes disponibles son: El bytecode de esta librería está en el directorio donde se instala Java en el archivo jre/lib/rt.jar20. Las fuentes de esta librería también están disponibles en el directorio donde se instala java, en src.jar. Pueden examinarse los archivos que las conforman con jar tvf src.jar o pueden extraerse con jar xvf src.jar.

Para emplear una clase de un paquete puede
  1. dar el nombre de la clase precedido del paquete, por ejemplo java.lang.System, o más en concreto: java.lang.System.println("gracias");
  2. al comienzo del archivo en el que se usa la clase importarla, por ejemplo con
    import java.lang.System;
    
    también es posible importar todas las clases de un paquete de manera simultanea, por ejemplo:
    import java.lang.+;
    
  3. El caso de las clases del paquete java.lang es especial porque estás son importadas de forma automática por el compilador.

C

Para emplear macros, funciones o tipos de La librería estándar de C debe incluirse el archivo encabezado que declara lo que se desea usar. Algunos de los archivos encabezados son:

C++

En C++ hay un espacio de nombres global y es posible definir espacios de nombres que agrupen ciertas funciones, clases, tipos (todo excepto macros). El espacio de nombres para las librerías estándar de C++ es std, por esto es posible escribir: Además como parte de la biblioteca estándar de C++ están los siguientes encabezados que corresponden a encabezados de la biblioteca estándar de C, pero con funciones definidas en el espacio de nombres std22: cassert, cctype, cerrno, cfloat, ciso646, climits, clocale, cmath, csetjmp, csignal, cstdarg, cstddef, cstdio, cstdlib, cstring, ctime, cwchar, cwctype.

Para emplear funciones, clases, macros o tipos de la librería de C++ es necesario incluir el encabezado en el que se declara. Por ejemplo para usar la macro NULL puede incluirse el encabezado cstddef23, con
#include <cstddef>
La biblioteca puede dividirse en:

1.8.2  Bibliotecas no estándar

Es posible compilar programas a partir de varias fuentes, así como crear bibliotecas que puedan ser reutilizadas por diversos programas. Para cada uno de los 3 lenguajes pueden encontrarse y usarse librerías con propósitos específicos.

Caso C

El proceso completo de compilación en C y C++ es: Una biblioteca es una colección de varios códigos objeto. El proceso de creación de una librería es el mismo de un ejecutable excepto porque no debe haber una función main en ninguno de los códigos objeto y porque en lugar de encadenar los diversos códigos objeto, estos deben coleccionarse en un archivo.

A manera de ejemplo presentamos como usar el algoritmo md5 de la librería stdcrypt de skalibs26 y el compilador gcc en un entorno tipo Unix para obtener una firma de una cadena. skalibs puede obtenerse de http://www.skarnet.org/software/skalibs/ una vez descargado y descomprimido, pueden consultarse la documentación para encontrar que las librerías se crean con:
cd prog/skalib-*
package/install
lo cual dejará archivos encabezados en prog/skalibs/includes y las librerías en prog/skalibs/library (en particular las librerías libstdcrypto.a y libstddjb).

Después puede escribir el siguiente código en un archivo ejmd5.c:
/* Ejemplo de uso de rutina md5 de biblioteca stdcrypt de skalibs.
 * Dominio público. 2004. Sin garantías. */

#include <stdio.h>
#include "stdcrypto.h"

int printhex(unsigned char *m, int len) 
{
        int i;
        for (i=0; i<len; i++) {
                printf("%x ",(m[i]));
        }
}

int main() 
{
        MD5Schedule ctx;

        char mens[]="Jesús es el camino, la verdad y la vida";
        unsigned char digest[16];

        md5_init(&ctx);
        md5_update(&ctx, mens, strlen(mens));
        md5_final(&ctx, digest);

        printf("mens=\"%s\"\n", mens);
        printhex(digest,15);
        printf("\n");
 
        return 0;
}
que puede preprocesarse, compilarse y encadenarse con las librerías apropiadas con:
gcc -o ejmd5 -I prog/skalibs/include ej_md5.c -L prog/skalibs/library/ -lstdcrypto -lstddjb
Note que entre los parámetros pasados a gcc están: En cuanto a la creación de una biblioteca estática utilizable con gcc, debe pasar los códigos objeto primero al programa ar para archivarlos (se sugiere un archivo que comience con lib y termine con la extensión .a) y después generar un índice para la biblioteca con el programa ranlib.

Caso C++

El proceso de creación de bibliotecas es el mismo de C.

A manera de ejemplo presentamos como compilar un programa de ejemplo en un sistema tipo Unix con la librería de dominio público libreadline_cpp disponible en http://s11n.net/download/#readline_cpp

El programa es adaptado de un comentario en las fuentes de src/Readline.hpp (tras modificar reader por readline27):
#include "Readline.hpp"

using namespace readlinecpp;
int main() 
{
           string theline;
           string prompt = "prompt: ";
           bool breakout;
           Readline reader;

           do
           {
               theline = reader.readline( prompt, breakout );
               if( breakout ) break;
               if( theline.empty() ) continue;
               if( theline == "history" )
               { // sample of implementing simple commands (map<string,functor> is prolly better)
                   cout << "History:"<<endl;
                   reader.save_history( std::cout );
                   // or: 
                   //   std::copy( reader.history().begin(), reader.history().end(), std::ostream_iterator<string>(cout,"\n") );
                   continue;
               }
               reader.add_history( theline );
           } while(1);
}
Suponiendo que este ejemplo se deja en un archivo ejemplo.cpp en el mismo directorio de fuentes de libreadline_cpp, se compilaría con:
g++ -o ejemplo -Isrc -Lsrc -lreadline_cpp ejemplo.cpp
Dado que libreadline_cpp es una biblioteca dinámica, para poder ejecutar este programa sin instalar la librería en la localización estándar para librerías en sistema (/usr/lib y /usr/local/lib) se usa:
LD_LIBRARY_PATH=src ./ejemplo
dejando en lugar de src la ruta donde se encuentra libreadline_cpp.so.2004 después de compilar esa biblioteca.

Para compilarla, como dice la documentación de la biblioteca, se requieren las fuentes de los scripts de soporte toc disponibles en el mismo sitio de libreadline_cpp (deben ubicarse en el directorio de libreadline_cpp). Tras esto se puede ejecutar ./configure --without-libreadline28 y tras esto gmake.

1.8.3  Lecturas recomendadas

Para utilizar bibliotecas estándar es fundamental tener a mano la documentación:

1.8.4  Ejercicios para afianzar teoría

  1. Traduzca la documentación de la clase Double de java.lang

1.8.5  Ejercicios de programación

  1. En Java como puede lograr que una función retorne a la función llamadora un valor flotante y a la vez un valor booleano? Implemente un ejemplo (Ayuda: referencias y tipo Double o Bool).

  2. Con las ayudas de esta guía compile y ejecute los ejemplos de uso de librerías no estándar.

1.9  Herencia, interfaces

Indicadores de logro:

1.9.1  Herencia

Tanto C++ como Java son orientados a objetos y por esto permiten definir clases que heredan de otras.

C++

En C++ puede definirse una clase Persona como:
class Persona {

        private:
        string id;

        protected:
        string nombres;
        string apellidos;

        public:
        Persona(string id, string nombres, string apellidos);
        string sacaApellidos();
        void cambiaNombres(string nombres);
        string nombreCompleto();
};
Note que una parte de los miembros de esta clase están en la sección privada29 (los que están a continuación de la palabra reservada private:), otra en la sección protegida (a continuación de protected:) y otra en la sección pública (a continuación de public:). Los miembros de la sección privada sólo pueden accederse desde métodos de la clase, los de la sección protegida pueden accederse desde clases derivadas y clases amigas (como se explica más adelante), los de la sección pública pueden leerse y modificarse desde cualquier otra clase o función.

Con la clase recién definida es necesario especificar en algún módulo las funciones, por ejemplo:
Persona::Persona(string id, string nombres, string apellidos)
{
        this->id=id;
        this->nombres=nombres;
        this->apellidos=apellidos;
}

void
Persona::cambiaNombres(string nombres)
{
        this->nombres=nombres;
}

string
Persona::sacaApellidos()
{
        return this->apellidos;
}


string
Persona::nombreCompleto()
{
        return nombres+" "+apellidos;
}
En este ejemplo particular es posible leer y modificar los datos privados de la clase usando funciones públicas. Note que también se define una función constructora, que permitirá instanciar e inicializar objetos de esta clase, como se hace en la siguiente porción de código:
        Persona p("111", "Pablo", "Ramirez");
        Persona *p2=new Persona("1121", "Angela", "Gonzáles");

        p2->cambiaNombres("Marta");
        s=p.sacaApellidos();
        s[0]='X';
        cout << s <<"\n";
        cout << p.sacaApellidos() <<"\n";
        cout << p2->nombreCompleto()<<"\n";
En C++ las clases se usan como nuevos tipos, los objetos30 se copian al pasar por parámetro, al ser retornadas por funciones o al asignarlo (es decir que se comportan como los tipos básico).

La palabra reservada this puede usarse en todo método no estático31 de una clase, se trata de un apuntador al objeto del cual el método llamado hace parte.

Una clase puede heredar los miembros de otra como se ejemplifica a continuación:
class Estudiante:public Persona {
        string carne;
        public:
        Estudiante(string id, string nombres, string apellidos, string carne):
                Persona(id,nombres,apellidos) {
                this->carne=carne;
        }
        string sacaCarne() {
                return carne;
        }

        string nombreCompleto();
};
 
string
Estudiante::nombreCompleto() {
        return nombres+"-"+apellidos;
}
Decimos que Estudiante es clase derivada de Persona, o que Persona es clase base de Estudiante. Los objetos de la clase Estudiante podrán acceder a los miembros públicos de la clase Persona.

En C++ también es posible hacer que una clase herede de varias (herencia múltiple).

Una clase que hereda de otra puede redefinir funciones heredadas (en el ejemplo esto ocurre con nombreCompleto, en tales casos sólo podría llamarse a la función de la clase base haciendo conversión de tipos. En una clase puede definirse que una función debe ser definida en clases derivadas usando la palabra reservada virtual.

Además de funciones virtuales una clase puede tener una o más funciones virtuales puras, en tal caso se dice que la clase es abstracta, porque no puede instanciarse, sólo sirve como clase base para otras o para declarar apuntadores. Puede declararse un método virtual puro por ejemplo con: virtual void rota(int) = 0; .

Java

Entre las diferencias básicas entre aspectos orientados a objetos de C++ y Java están: El ejemplo anterior en Java sería:
import java.lang.*;

class Persona {

        String id;
        String nombres;
        StringBuffer apellidos;

        public Persona(String id, String nombres, String apellidos)
        {
                this.id=id;
                this.nombres=nombres;
                this.apellidos=new StringBuffer(apellidos);
        }

        public StringBuffer sacaApellidos()
        {
                return apellidos;
        }

        public void cambiaNombres(String nombres)
        {
                this.nombres=nombres;
        }

        public String nombreCompleto()
        {
                return nombres+" "+apellidos;
        }
};


class Estudiante extends Persona {
        String carne;

        public Estudiante(String id, String nombres, String apellidos, String carne)
        {
                super(id,nombres,apellidos);
                this.carne=carne;
        }

        public String sacaCarne()
        {
                return carne;
        }

        public String nombreCompleto()
        {
                return nombres+"-"+apellidos;
        }
};

public class Herencia {

        public static void main(String[] args)
        {
                Persona p=new Persona("111", "Pablo", "Ramirez");
                Persona p2=new Persona("1121", "Angela", "Gonzáles");
                Estudiante e=new Estudiante("088", "Tomas", "Guerrero", "c22");

                StringBuffer s;

                p2.cambiaNombres("Marta");
                s=p.sacaApellidos();
                s.append(" Cordoba");
                System.out.println(s);
                System.out.println(p.sacaApellidos());
                System.out.println(p2.nombreCompleto());
                System.out.println(e.nombreCompleto());
                System.out.println(e.sacaCarne());
        }

}
Una diferencia es en el manejo del dato Apellido que se maneja como StringBuffer en lugar de String, la principal diferencia entre ambos es que una cadena de tipo StringBuffer puede modificarse después de creada, mientras que las cadenas tipo String son constantes. Hicimos el cambio para ejemplificar referencias, pues al ejecutar este programa la llamada a la función s.append(" Cordoba"); modificará tanto a s como al dato apellidos del objeto p ---lo cual no ocurre en C++, excepto si se pasan apuntadores o referencia de forma explicita con el operador & .

1.9.2  Ejercicios de programación

  1. Escriba un programa en el que se definan las clases Persona y Estudiante presentadas en esta sección, así como una nueva clase derivada EstudiantePers, la nueva clase debe tener una función para escribir los datos de un estudiante en un archivo plano (digamos void escribe(char *nomarchivo);) y otra para leerlos de un archivo plano (por ejemplo void lee(char *nomarchivo);. En el programa que haga pruebe las funciones introducidas.

  2. Investigue el uso de interfaces en Java y escriba un programa corto que las use.

1.10  Lo propio de C++

Indicadores de logro: Las diversas características de Java y PHP pueden implementarse en C++ (por ejemplo una interfaz es una clase en la que todas las funciones son virtuales puras), pero en esta sección presentamos carácterísticas de programación que sólo están disponibles en C++.

Se presenta algunos ejemplos suponiendo que se dispone de Objeto.hpp con:
/**
 * Objeto genérico. Imitando Java.
 * @author Dominio público. 2006. vtamara@pasosdejesus.org
 **/

#ifndef OBJETO_HPP
#define OBJETO_HPP

class Objeto {
        public:
        virtual Objeto *clonar()=0;
        /** Retorna una cadena con una representación presentable del objeto 
        * como cadena.
        * Localiza memoria para la nueva cadena con new.  Es responsabilidad 
        * de la función llamadora liberarla.
        * Lanza una excepción si no logra localizar memoria.
        */
        virtual char *aCadena()=0;
};
#endif
y Entero.hpp (además de comentarios y #ifndef) con:
class Entero: public Objeto {
        private:
                int e;
        public:
                Entero(int n);
                int valor();
                Objeto *clonar();
  char *aCadena();
};
Entero.cpp con:
/**
 * Entero descendiente de Objeto genérico.
 * @author Dominio público. 2006. vtamara@pasosdejesus.org
 **/

#include <algorithm>
#include <cassert>
using namespace std;

#include "Entero.hpp"

Entero::Entero(int n)
{
        e=n;
}
int Entero::valor()
{
        return e;
}
Objeto *Entero::clonar()
{
        Entero *n=new Entero(e);

        return (Objeto *)n;
}
char *Entero::aCadena()
{
        double ld= e==0 ? 0 : log10((double)abs(e));
        int l = (int)(ld+1);
        char *s;
 l = (e<0) ? l+1 : l;
 s = new char[l+1];
        snprintf(s, l+1, "%d", e);
        return s;
}

1.10.1  Constructoras

Una constructora por defecto es una constructora que no recibe parámetros y será la llamada por el código generado por el compilador en situaciones como esta:
Entero e;
Entero v[100];
En el ejemplo anterior puede agregarse constructora por defecto o agregar en la declaración de la constructora (pero no en la implementación) que hay un parámetro por defecto:
Entero(int n=0);
La constructora anterior también será usada al intentar conversión de tipo de int a Entero:
Entero x=(Entero)9;
Una constructora de copia debe recibir como primer parámetro una referencia a un dato del mismo tipo de la clase, por ejemplo podría agregarse en la declaración:
inline Entero(const Entero &x):e(x.e) { };
Note que inline hará que se use como un macro, es decir que el cuerpo de la función sea insertado en cada sitio donde se use la función ---en lugar de hacer una llamada a la función. Por otra parte se usa una forma especial de inicialización, que llamara a la constructora de e con el parámetro x.e, la función anterior resulta equivalente a:
inline Entero(const Entero &x) { e=x.e; }
Una constructora de copia debe inicializar el objeto como copia del dato que recibe y podría ser usada (dependiendo del compilador) en situaciones como la siguiente
Entero un_cinco()
{
        Entero x(5);
        return x;
}

int main()
{
        Entero e=un_cinco();
        Entero f=e;
        cout << f.valor() <<"\n"; //Imprimirá 5
        return 0;
}
Note que en la función un_cinco el compilador localiza un entero local a la función (y por tanto será destruido una vez concluya la función), pero el return y la declaración de la función hacen que se saque una copia y que la copia sea el valor retornado.

Por el contrario una función como la siguiente presentará problemas en tiempo de ejecución:
Entero *ref_cinco()
{
        Entero x(5);
        return &x;
}
Esto es porque el return devuelve una copia de la dirección de x, pero esta variable es local a la función y por tanto será liberada antes de que el control salga de la función y por tanto la dirección retornada no será memoria validamente localizada.

Algunos compiladores pueden sintetizar automáticamente constructoras de copia.

1.10.2  Destructoras

Una destructora es una función que no recibe parámetros y que será llamada al eliminar un objeto, no es muy util con la clase Entero recién presentada, pues esta clase no debe hacer una labor especial cuando se destruye un objeto, pero puede agregarse explicitamente en la declaración con:
~Entero() {};
Una destructora es útil en clases que manejen memoria dinámica, que sea localizada por ejemplo en la constructora o durante uso de la clase. Un objeto de tal clase debería liberar la memoria localizada durante su uso o brindar facilidades para hacerlo y documentación al respecto.

En una clase descendiente de otra, la destructora llamará a la destructora de la clase mamá.

Las constructoras de la clase serán usadas durante la inicialización, incluso en el siguiente ejemplo que empleará la constructora Entero(int):
Entero c=3;

1.10.3  Sobrecarga

Las constructoras de la sección anterior también ejemplifican sobrecarga de operadores. El compilador eligirá la función cuyos tipos (de parámetros y retorno) coincidan con el uso que se da en el momento de llamarla. Otro ejemplo es:
int suma(int x, int y) 
{ 
        return x+y;
}
float suma(float x, float y)
{
        return x+y;
}
Estas funciones pueden usarse en situaciones como:
cout << suma(0.0, 0.0);
cout << suma(0, 0);
Pero no bastan para el siguiente ejemplo que debe generar un error en tiempo de compilación:
cout << suma(0, 0.0);
Note que la sobrecarga implementa de alguna manera algo de polimorfismo, pues con un mismo nombre puede operarse datos de diversos tipos. Claro que se esperan soluciones que abrevien los programas y con esta forma de polimorfismo el programador debe implementar por separado la función para cada tipo con el que operará. Las otras dos formas de polimorfismo son enlaces dinámicos (herencia con funciones virtuales) y plantillas.

1.10.4  Operadores

Es posible sobrecargar los siguientes operadores:
 new  delete    new[]     delete[]                             
+    -    *    /    %    ^    &    |    ~                     
!    =    <    >    +=   -=   *=   /=   %=  
^=   &=   |=   <<   >>   >>=  <<=  ==   !=                    
<=   >=   &&   ||   ++   --   ,    ->*  ->                    
()   []        
Para sobrecargarlos se emplea como nombre de función operator@ donde @ es uno de los operadores. Estas funciones en algunos casos deben implementarse como miembros de la clase para la cual se redefinen operadores, por ejemplo si a es un objeto de alguna clase que tenga definido el operador unario -, podría usarse:
a.operator-();
En algunos casos se sobrecarga una función global, por ejemplo si se ha sobrecargado el operador unario - para la clase del objeto a, podría usarse:
operator+(a);
Los operadores =, [] y -> deben definirse como miembros de clase, los demás puede definirse bien como miembros de clase o como funciones globales.

Operadores unarios

Se trata de los operadores prefijo: + -   ! y de otros relacionados con memoria (ver sección al respecto)

Por ejemplo en el caso de la clase Entero podría sobrecargarse el operador unario - con un nuevo método en la clase Entero, cuya declaración sería:
Entero operator-();
y cuya implementación puede ser:
Entero operator-();
{
        return Entero(-e);
}
Alternativamente podría sobrecargarse el operador con una función global:
Entero operator-(const Entero a&) 
{
        return Entero(-a.valor());
}
Note que sólo debe implementarse una de las dos formas para que el compilador no encuentre ambigüedades. Con respecto a la declaración const Entero a& en la segunda forma, es la manera estándar de declarar parámetros para operadores en C++, a se convierte en un alias del argumento con el que sea llamado la función, pero no se permite su modificación en la función. Otra posibilidad es Entero a que permitiría modificaciones locales a a, pero que puede restar oportunidad de optimización a un compilador.

La última función podría abreviarse un poco si se declara como función amiga de la clase Entero. Para esto en la declaración de esta clase se agregaría:
friend Entero operator+(const Entero &);
con lo cual el operador tendría acceso a datos privados:
Entero operator-(const Entero &a) 
{
        return Entero(-a.e);
}

Operadores binarios

Son los operadores infijos + - * / % &| == < > && != <= >= || junto con otros que se explican más adelante.

Por ejemplo la expresión a + b generaría bien la llamada a.operator+(b) o bien operator+(a,b).

Tengase en cuenta que pueden sobrecargarse estos operadores con diversos tipos mientras no haya ambiguedades. Por ejemplo el operador + para Enteros podría sobrecargarse así:
void operator+(Entero &a, Entero b)   //a por referencia, b por copia
{
        a.poner(a.valor()+b.valor());
}
de forma que el siguiente código modificará el entero x:
Entero x(9);
Entero y(5);
x+y;
cout << x.valor(); // Presentará 14
Sin embargo una forma más esperada de sobrecargar este operador es declarando funciones amigas en la clase Entero:
friend Entero operator+(const Entero &x, const Entero &y);
friend Entero operator+(const Entero &x, const int &y);
friend Entero operator+(const int &x, const Entero &y);
E implementandolas de forma análoga a:
Entero operator+(const Entero &x, const Entero &y)
{
 return Entero(x.e+y.e);
}

Llamada de función

El operador () permite manejar un número arbitrario de parámetros (con sobrecarga) y será empleado cuando se use un objeto como una función. Por ejemplo al agregar a la declaración de la clase Entero:
int operator()(float x);
y a su definición:
int Entero::operator()(float x)
{               
        e+=(int)x;
        return e;
}
el siguiente código no fallará (assert detiene la ejecución cuando el argumento que recibe es falso --debe incluirse <cassert> ):
Entero x(99);
int rx=x(3.0);
assert(rx==102 && rx.valor==102);

Operadores de asignación

Son operadores binarios infijos = += -= *= /= %= ^= &= |= que típicamente deberían modificar el objeto y retornar una referencia al mismo objeto (i.e. return *this). Tenga en cuenta que el operador = no será usado durante inicialización, sólo las constructoras.

Por ejemplo el operador de asignación típico para la clase Entero sería:
Entero &Entero::operator=(const Entero &f)
{
        e=f.e;
        return (*this);
}
que permitiría:
Entero a(99);
Entero b,c;
b=c=a;
assert(a.valor()==99 && b.valor()==99 && c.valor()==99);
En el caso de la clase Entero podría definirse otro operador de asignación:
Entero &Entero::operator=(const int &i)
{
        e=i;
        return (*this);
}
que permitiría :
Entero b;
b=5;
assert(b.valor()==5);
El definir operadores de asignación asegurese de que el código funcione cuando el objeto recibido por parámetro es justamente this, es decir que funcione en el caso de auto-asignación.

Operador de subindexado

Se trata del operador unario posfijo []. La expresión x[y] se interpreta como x.operator[](y). Típicamente retorna una referencia para que puedan emplearse expresiones como x[4]=5;.

Aunque en el caso de la clase Entero este operador no se sobrecarga de forma intuitiva, puede definirse:
Entero &Entero::operator[](const int &i)
{
        return (*this);
}

Operadores relacionados con memoria

Se trata de los operadores unarios * &, el operador binario -> y los operadores new, new[], delete y delete[].

En general no es ni necesario ni sugerido sobrecargarlos.

La expresión x->m se interprea como (x.operator->())->m. Para la clase Entero resultaría un uso intuitivo de los tres primero operadores (definidos en la declaración):
Entero *operator->() { return this; } 
Entero operator*() { return *this; } 
Entero *operator&() { return this; } 
Una expresión de a forma new T (siendo T un tipo/clase) es interpretada como operator new(sizeof(T)), una de la forma new(5) T se convierte en la llamada (T *)(operator new(sizeof(T), 5)). La expresión new T[10] se convierte en (T *)(operator new[](sizeof(T)*10)) mientras que new(5, 'x', 3) T[21] se convierte en operator new[](sizeof(T)*21, 5, 'x', 3). Los parametro opcionales que puede recibir new indica la constructora que debe llamarse para inicializar el objeto.

La expresión delete o; se convierte en operator delete((void *)o, sizeof(o)). Por lo anterior una implementación natural (adaptada de http://www.informit.com/articles/article.asp?p=30642&rl=1) de los operadores para localizar y liberar memoria de la clase Entero serían:
void *operator new(size_t s) {
        return ::operator new(s);
}
void operator delete(void *p, size_t s) {
        ::operator delete(p);
}
void *operator new[](size_t s) {
        return ::operator new[](s);
}
void operator delete[](void *p, size_t s) {
        ::operator delete(p);
}
Note que se usan los operadores globales (por lo que precedemos la palabra operator con ::

Operadores de incremento/decremento prefijo y posfijo

++x se interpeta bien como x.operator++() o bien como operator++(x).

Por su parte x++ se interpreta como x.operator++(0) o bien como operator++(x,0). Más en general los demás operadores unarios cuando se usan como postfijo x@ se interpretan como x.operator@(0).

1.10.5  Streams

Al incluir <iostream> , se incluye declaración de las clases istream y ostream, se trata de flujos de entrada y de salida. La primera permite recibir información de un flujo en un objeto y la segunda permite enviar información de un objeto a un flujo.

El objeto de la clase istream más empleado hasta el momento es cin que es un flujo asociado a entrada estándar (i.e stdin). Por su parte los objetos de la clase ostream típicamente usados son cout, cerr y clog, el primero de estos asociado a salida estándar (i.e stdout) y los dos últimos a error estándar (i.e stderr).

La utilidad de este enfoque se ve en el uso de los operadores y >> en el contexto de flujos. Por ejemplo la clase Entero podría aumentarse con:
friend std::istream &operator>>(std::istream &is,
                                Entero &e);
                friend std::ostream &operator<<(std::ostream &os,
                                const Entero &e);
La implementación puede ser:
std::istream &operator>>(std::istream &is, Entero &e)
{
        return is >> e.e;
}

std::ostream &operator<<(std::ostream &os, const Entero &e)
{
        return os << e.e;
}
Al hacerlo ya podemos enviar y recibir Enteros de flujos como:
Entero r(5);
cout << r; // 5 a sálida estándar
cin >> r; // espera entero de entrada estándar
Pero también en otros flujos derivados de istream y ostream como pueden serlo archivos abiertos como fstream o la clase stringstream:
// requiere inclusión de <fstream>
fstream fs("numeros.txt", ios_base::out);  
y=-25;
fs << y; // 5 al archivo numeros.txt
fs.close();

fstream flee("numeros.txt", ios_base::in);
flee >> x; // 5 sale de numeros.txt hacía x
flee.close();

// requiere inclusión de <sstream>
stringstream sst(stringstream::in | stringstream::out);
sst << y << " " << 19; // -25 19 al flujo cadena sst
sst >> x; // -25 sale de sst y va a x
sst << y << " " << 19; // 19 -25 19 en sst
sst >> y; // 19 sale de ssy va a y en sst queda -25 19
assert(x==-25 && y==19);

sst >> y; // -25 a y en sst queda 19
sst >> x; // 19 a x sst queda vacío
assert(x==19 && y==25);
Note que usando un stringstream podemos convertir con facilidad datos de un tipo a otro; continuando el ejemplo anterior:
x=15;
sst << x; // 15 de x a sst
string cx;
sst >> cx; // 15 de sst a cx, sst queda vacío
sst << cx; // 15 de cx a sst, sst queda con 15
sst >> y; // 15 de sst a y, sst queda vacío
assert(x==15 && y==15);
Note que usamos el tipo string que es el remplazo en C++ para los vectores de caracteres, entre sus ventajas está que puede revisar que no se acceda posiciones fuera del vector y facilidad para crecer y disminuir dinámicamente.

Por otra parte pueden modificarse algunas características de un flujo con banderas (tomadas de ??):

Bandera Efecto si está activada
ios_base::boolalpha Presenta objetos bool como nombres (true, false).
ios_base::dec Formato de entrada/salida de enteros en decimal.
ios_base::fixed Valores flotantes y notación de punto fijo.
ios_base::hex Formato de entrada/salida de enteros en hexadecimal.
ios_base::internal Salida es llenada en el punto entero par a aumentarla hasta el ancho del campo field.
ios_base::left la salida es llenada al final para aumentarla hasta el ancho del campo field.
ios_base::oct Formato de entrada/salida en base octal.
ios_base::right la salida es llenada al comienzo para aumentarla hasta el ancho del campo field.
ios_base::scientific emitir valores de punto flotnate en notación científica.
ios_base::showbase emitir valores enteros precedidos por la base numérica.
ios_base::showpoint emitir valores de punto flotante incluyendo siempre el punto decimal.
ios_base::showpos emitir números no negativos precedidos de un signo más (+).
ios_base::skipws saltar espacios en blanco al comienzo en ciertas operaciones de ingreso.
ios_base::unitbuf descargar la salida después de cada operación de inserción.
ios_base::uppercase emitir letras en mayúsculas que remplacen letras ciertas letras mayúsculas.

Estas banderas se activan/desactiva con los métodos setf y unsetf por ejemplo:
cout.setf ( ios_base::hex, ios_base::basefield );  
cout.setf ( ios_base::showbase );                  
cout << 100 << endl;  //0x64
cout.unsetf ( ios_base::showbase );               
cout << 100 << endl; //64
cout.unsetf ( ios_base::hex); 
Aunque también hay algunas constantes que facilitan este tipo de cambios:
cout << 100 << endl << oct << 100 << endl;
endl emite '\n' y además descarga el buffer del stream --es decir envía todo caracter que pueda quedar en el buffer al stream. hex activa emición de números en hexadecimal.

También pueden controlarse características de la siguiente inserción como el ancho o el caracter de relleno bien con el método width (heredado de ios_base):
cout.width(10);
cout<<77;
o bien con la función manipuladora setw que requiere la inclusión de iomanip:
cout<<setw(10)<<77;
Los otros manipuladores son resetiosflags, setiosflags, setbase, setfill y setprecision.

Los operadores << y >> en un stringstream, lo hacen ser una estructura FIFO (First In First Out -- primero en llegar es primero en salir). Si tuviera cabeza y cola, los datos llegan por la cola y salen por la cabeza.

Dado que en flujos de entrada, el operador >> suele completar un dato al ingresar un espacio o retorno de carro, para ingresar cadenas es más apropiada la función getline que recibe el flujo de entrada y un dato tipo string:
string cadena;
cout << "Una cadena: ";
getline(cin, cadena);
La función getline junto con la clase stringstream permiten tener más control sobre el ingreso de información:
string num;
Entero me;
cout << "Un entero: ";
getline(cin, num);
istringstream ist(num);
ist >> me;
cout << "Fue "<< me << endl;

1.10.6  Excepciones

Permiten informar y manejar situaciones excepcionales, errores en tiempo de ejecución que pueden ocurrir durante la ejecución de una función, errores que la función no se compromete a manejar, sino pasa la responsabilidad a la función llamadora.

Una función puede generar una excepción con la palabra reservada throw seguida de una expresión de cualquier tipo. La excepción interrumpe flujo de ejecución hasta que sea atrapada en alguna función llamadora en medio de un bloque try en alguno de los casos catch.

Por ejemplo en la siguiente función que resuelve ecuaciones cuadráticas se genera una excepción cuando habría una división por cero.

Espacios de nombres

1.10.7  Lecturas recomendadas

1.10.8  Ejercicios de programación

  1. Implemente una clase complejo que represente números complejos y de manera que el siguiente código funcione:
    complejo c;  // Corresponde a 0.0 + 0.0i
    complejo x=3.0;  // Corresponde a 3.0 + 0.0i
    complejo y(4.0, 5.0);  // Corresponde a 4+5i
    complejo w(y);  // Corresponde a 4+5i
    x.suma(y);  //x debe quedar con la suma de x antes con y (i.e 7+5i)
    


  2. Implemente tantos operadores como le resulte posible para la clase Entero. El siguiente código debería funcionar:
    Entero x;  
    assert(x==0);
    x=8;
    assert(x<9 && x>7);
    


  3. A la clase complejo que implementó, agreguele operadores sobre flujos para ingreso y emición de complejos con la sintaxis: (5.3, 2.1). Sugerencias: (1) Identifique los elementos léxicos importantes `(' `,' `)'. (2) busquer las diversas operaciones que pueden implementarse sobre la clase string.

1
Aunque los 4 lenguajes tienen convenciones propias en cuanto a identificadores (por ejemplo Java soporta Unicode, mientras que PHP, C y C++ no), es común a todos considerar identificadores válidos cadenas alfanuméricas (i.e números y letras del alfabeto en inglés) iniciadas con letra o con _ . En PHP toda variable debe comenzar con el caracter `$'. Tal como lo sugiere [13] ``emplee nombres descriptivos para variables globales y nombres cortos para variables locales.''
2
En Java tras crear un arreglo no hay forma de cambiar el tamaño
3
Note que de acuerdo a [3] (sección 6.5.4) en C99 el tamaño de un arreglo que se reciba como parámetro de una función (bien con o sin tamaño) NO puede obtenerse con ayuda de sizeof --tiene que mantenerse en una variable o constante.
4
La dirección de retornó será la dirección que sigue a la instrucción en la que se hace la llamada.
5
static indica que la función puede llamarse sin crear objetos de la clase.
6
public indica que una función puede ser llamada desde otras clases.
7
A esta operación de conversión en inglés se le llama casting.
8
Los paréntesis permiten cambiar la precedencia o el orden de evaluación de una expresión.
9
Al escribir constantes en hexadecimal emplee los dígitos 0,1,2,3,4,5,6,7,8,9,A,B,C,D,E, F.
10
Al escribir constantes en octal emplee los dígitos 0,1,2,3,4,5,6,7.
11
En C y C++ también puede emplearse Unicode con el tipo wchar_t (definido en el encabezado stddef.h), los caracteres de este tipo llevan el prefijo L, por ejemplo L'\x192'.
12
En C y C++ puede ponerse como prefijo L para especificar una cadena de caracteres amplios (wchar_t).
13
Si el primer operando del operador de corrimiento a la derecha es negativo pueden llenarse los bits del comienzo con 0 con 1, el comportamiento en C y C++ depende de la plataforma.
14
Es una extensión descrita en el RFC 2109.
15
Como se explica más adelante una clase puede verse como un conjunto de variables y funciones.
16
Otras funciones que implementan parte de la funcionalidad de las presentadas son atof, atoi, atol y atoll.
17
Un objeto es una instancia (i.e memoria reservada) de una clase.
18
De hecho en C++ las estructuras son funcionalmente equivalentes a las clases, excepto porque en una clase por defecto todos los miembros son privados, mientras que en una estructura por defecto todos los miembros son públicos.
19
La conversión a cadena en el caso de objetos la realiza llamando al método toString.
20
La ubicación y el nombre del archivo de clases ha cambiado de una versión a otra de Java, la información citada es para JDK 1.3 en un sistema tipo Unix.
21
Idioma y formatos (e.g de fechas, números, cantidades monetarias) establecidos en variables de ambiente como LC_LANG, LC_MESSAGES. Para el caso de Colombia el valor de estas variables debería ser es_CO. Todas pueden ponerse simultaneamente con export LC_ALL=es_CO.
22
Al incluir un encabezado como stdio.h, los tipos, funciones y clases allì declaradas se agregan al espacio de nombres global.
23
El contenido de cstddef es el mismo del encabezado estándar de C stddef.h.
24
Las macros se definen con #define, pueden tener argumentos.
25
La forma de resolver inclusiones depende del compilador. Es usual que las inclusiones de la forma #include <archivo> se resuelvan buscando el archivo en la ruta de los archivos encabezados estándar. Las inclusiones de la forma #include "archivo" se resuelven buscando el archivo en la ruta donde están las fuentes compiladas y de no encontrarse en las rutas de encabezados estándar. De esta manera es posible definir encabezados que remplacen encabezados estándar.
26
Se trata de varias librerías de dominio público, en una de las cuales (libstdcrypt) hay algunas funciones de criptografía.
27
El comentario estaba desactualizado con respecto al código, que es un error común y que vale la pena reportar a los autores del programa.
28
En sistema OpenBSD para que la configuración funcione se requiere instalar bash2, findutils gmake y modificar toc/tests/gnu_find.sh cambiando toc_find find por toc_find gfind.
29
Por defecto los miembros de una clase son privados.
30
Un objeto es una instanciación de una clase, por ejemplo si Persona es una clase, la declaración Persona p declara un objeto p que es instancia de la clase Persona.
31
Un miembro estático de una clase (digamos s) de una clase X se declara con la palabra reservada static y puede usarse sin necesidad de crear objetos de la clase con X::s.

Capítulo 2  Vistazo a ingeniería de software y a algunas herramientas

En este capítulo procuraremos emplear los términos y definiciones de [6]. Dado que esa referencia es actualizada y se ha construido con esfuerzo de diversas organizaciones y personas reconocidas en el campo de ingeniería de software, la sección de teoría de cada guía es traducción de apartes de ese libro.

2.1  Ingeniería de Software y requerimientos

Indicadores de logro:

2.1.1  Teoría

De acuerdo a [6], ingeniería de software es
  1. "La aplicación de una aproximación sistemática, disciplinada y cuantificable al desarrollo, operación y mantenimiento de software; es decir, la aplicación de la ingeniería al software.
  2. El estudio de aproximaciones como las descritas en (1),"
Las áreas de conocimiento de la ingeniería de software son:

2.1.2  Requerimientos

Un requerimiento de software es una propiedad que un programa debe exhibir después de ser desarrollado o adaptado para resolver un problema particular.

Entre las propiedades de un requerimiento están: debe ser verificable, debe ser especificado sin ambigüedad y claramente de ser posible cuantitativamente (por ejemplo porcentaje de incremento en desempeño) y debe permitir estimar su costo.

Algunas clasificaciones usuales para requerimientos de software son: Las actividades relacionadas con requerimientos de software son: adquisición, análisis, especificación y validación. Todas ellas se interrelacionan en un proceso de requerimientos teniendo en cuenta que el entendimiento de los requerimientos evoluciona mientras procede el diseño y el desarrollo (así que una porción significativa de los requerimientos cambia, por lo cual el proceso de requerimientos se expande a todo el ciclo de vida del software).

Proceso de requerimientos

Como parte del proceso de requerimientos se configuran adquisición, análisis, especificación y validación de requerimientos de acuerdo al tipo de proyecto, de restricciones y de información disponible (e.g como estudios de viabilidad y mercadeo) en un Modelo del Proceso.

También se definen actores del proceso, es decir roles de quienes participan en el proceso además del especialista de requerimientos1. Podrían incluir: usuarios, clientes, analista de mercado, reguladores (e.g autoridades regulatorias) e ingenieros de software quienes podrían proponer restricciones sobre el proceso de desarrollo (e.g reutilización de componentes).

Como parte del proceso de requerimientos también se define el Proceso de soporte y administración que enlaza las actividades del proceso y aspectos de costos, recursos humanos, entrenamiento y herramientas.

Finalmente incluye un Proceso de calidad y mejoramiento que busca asegurar la calidad y el mejoramiento del proceso de requerimientos, orientándolo con estándares de calidad, modelos de mejoramiento del proceso, métricas y mediciones al proceso.

Adquisición de requerimientos

Es indispensable una buena comunicación entre los usuarios del software y los ingenieros de software, el especialista de requerimientos debe establecer el conducto para esta comunicación al comienzo de que comience el desarrollo.

Entre las fuentes de requerimientos pueden estar: Las metas, conocimiento del dominio de la aplicación, actores del proceso, ambiente operacional (i.e del ambiente en el que se ejecutará el programa), ambiente organizacionales.

Dada la diversidad y heterogeneidad de fuentes pueden emplearse como técnicas para adquirir requerimientos: entrevistas, escenarios (preguntas "que ocurriría si", caso de uso), prototipos (desde diagramas en papel a versiones de prueba), reuniones facilitadas (sinergia en un grupo, lluvia de ideas), observación (de procedimientos y tareas por inmersión en el ambiente de la organización)

Análisis de requerimientos

Los requerimientos se analizan para: detectar y resolver conflictos, descubrir limitaciones del software y como debe interactuar con el ambiente, elaborar requerimientos del sistema que permitan deducir requerimientos del software.

Pueden desarrollarse modelos conceptuales para ayudar a entender el problema (no para empezar a diseñar), por ejemplo: flujos de control, modelos de estado, trazas de eventos, interacción entre usuarios, modelos de objetos, modelos de datos y muchos otros. El tipo de modelo depende entre otros aspectos de: naturaleza del problema, expertisia del ingeniero de software, proceso de requerimientos del cliente, disponibilidad de métodos y herramientas.

Dado que el análisis y la elaboración de requerimientos demanda identificar componentes del software que deben satisfacerlos, el ingeniero de software en muchos casos debe hacer un diseño arquitectónico y una localización de requerimientos, empleando notaciones comunes al modelo conceptual y al diseño de la arquitectura (que hace parte de la etapa de diseño).

Finalmente como parte del análisis de requerimientos debe hacerse negociación de requerimientos en caso de conflicto entre dos actores que requieran características mutuamente incompatibles, entre requerimientos y recursos, entre requerimientos funcionales y no-funcionales. Para tomar una decisión es recomendable llegar a un consenso con el actor y hacer rastreable la decisión hasta el cliente.

Especificación de requerimientos

Se refiere a la producción de un documento (o su equivalente electrónico) que pueda ser revisado sistemáticamente, evaluado y aprobado. Dependiendo de la complejidad del sistema puede requerirse dividirlo en 3: La especificación puede hacerse en lenguaje natural o en una notación formal o semi-formal (buscando describir los requerimientos de la manera más precisa, concisa y natural posible).

La calidad de la especificación puede determinarse con indicadores tanto para requerimientos individuales (imperativos, directivas, frases débiles, opciones) como para el documento de especificación completo (tamaño, legibilidad, especificación, profundidad y estructura del texto).

Validación de requerimientos

Los documentos de requerimientos deben ser validados para asegurar que el ingeniero de software entendió los requerimientos. Deben revisarlo al menos representantes de los actores (clientes, desarrolladores) y debe estar sujeto a prácticas de manejo de configuraciones (i.e mantener versiones y traza de cambios).

Para la revisión puede servir proveer guías de que revisar en forma de listas de chequeo. También pueden emplearse prototipos, que aunque pueden resultar costosos pueden evitar desperdicio posterior de recursos invertidos en satisfacer requerimientos errados.

También deben validarse los modelos desarrollados, por ejemplo con chequeos estáticos en el caso de modelos de objetos o razonamiento formal (para probar propiedades) en caso de usar una notación de especificación formal.

Dado que los requerimientos deben ser validados en el producto final, es importante planear como se verificará cada requerimiento, diseñando una prueba de aceptación. En el caso de requerimientos no-funcionales deben ser analizados para llegar a expresarlos cuantitativamente.

2.1.3  Hacía la práctica: Programación extrema y requerimientos

Como se describe en [19], la programación extrema es una aproximación deliberada y disciplinada al desarrollo de software que da énfasis a la satisfacción del usuario (lo que necesita, cuando lo necesita). Potencia a los desarrolladores para responder a requerimientos cambiantes incluso en etapas avanzadas del ciclo de vida de un programa. Da énfasis al trabajo en equipo (entre desarrolladores y clientes) y reduce costos por ser una metodología liviana (i.e que requiere pocas reglas y prácticas que son fáciles de seguir) basada en re-examinar las prácticas de desarrollo.

Entre sus principales características están: Es especialmente apropiado para responder a problemas en dominios donde los requerimientos cambian, con pocos programadores (2 a 12) no necesariamente muy especializados.

Las etapas típicas de un proyecto de software desde el punto de vista de programación extrema son: planeación, diseño, programación, pruebas. Etapas que se iteran repetidas veces, hasta pasar todas las pruebas de una prueba de aceptación. Cada iteración dura de 1 a 3 semanas.

2.1.4  Requerimientos

La dificultad de especificar los requerimientos depende en buena medida del tipo de proyecto y de sus clientes/usuarios, por ejemplo en proyectos típicos los clientes no logran expresar todos los requerimientos que tienen en su mente o pueden cambiarlos con facilidad incluso en etapas avanzadas del ciclo de vida del proyecto.

Por esto es bien difícil hacer documentos con especificación de requerimientos, pues en rigor deberían especificar tanto lo que el sistema/software debe hacer como lo que no debe hacer. Esto tiende a generar documentos voluminosos, difíciles de leer y difíciles de mantener.

En programación extrema no hay un documento formal con la especificación de requerimientos, pero estos se mantienen implícitamente en forma de Además se busca asegurar que el software cumple las expectativas del usuario/cliente aumentando la comunicación con él/ella durante todas las etapas de desarrollo, preferiblemente cara a cara, para: establecer prioridades; acordar un plan de pequeñas entregas incrementales y su cronograma; que el cliente realice pruebas de funcionalidad.

La idea es reasignar y disminuir el tiempo ahorrado por el cliente/usuario al comienzo del proyecto por no escribir una especificación de requerimientos detallada.

Las Historias de usuarios son escritos muy cortos (e.g tres oraciones) realizados por el cliente indicando cosas que requiere que el sistema haga para ellos. Están escritas en términos propios del usuario (i.e no-técnico, sin entrar en detalles de tecnología, perfiles o algoritmos). Deben proveer suficiente detalle para que el programador haga un estimativo de bajo riesgo del tiempo que tomara implementar la historia entre 1 y 3 semanas2 (las historias que requieran más de 3 semanas deben dividirse).

Las historias de usuario, además de servir para elaborar un plan de entregas en una reunión con ese propósito, sirven para crear pruebas de aceptación (o pruebas de funcionalidad). Una historia de usuario puede tener una o más pruebas de aceptación que aseguran la funcionalidad esperada (son pruebas de caja negra que sólo cuando sean pasadas en su integridad permiten decir que se ha completado una historia de usuario). El cliente especifica escenarios que espera probar una vez esté implementada correctamente una historia. Durante las pruebas de aceptación el cliente revisa la corrección de las pruebas de aceptación y decide la prioridad para las pruebas fallidas. Las pruebas de aceptación deben ser automatizadas para ejecutarlas con frecuencia y su puntaje (establecido por el cliente) debe ser publicado para que el equipo planee tiempos de cada iteración y arregle los casos que fallen.

Los programadores deben completar tareas de programación cuestionando al usuario con respecto a las historias de usuario y a partir de pruebas fallidas. Estas tareas se producen al comienzo de cada iteración. Se escriben en tarjetas análogas a las de las historias de usuario, pero en el lenguaje de los desarrolladores y se constituyen en el plan detallado de la iteración. Los programadores escogen las tareas por realizar y estiman el tiempo que requieren para completar cada una en 1, 2 o 3 días ideales de programación, las tareas que requieran menos de un día se pueden agrupar y las que requieran más de 3 días se deben partir.

2.1.5  Lecturas recomendadas

Puede consultarse más sobre requerimientos en el capítulo 2 de [6].

Además de [19] recomendamos [4] y de allí en particular el artículo [17].

2.1.6  Ejercicios para afianzar teoría

  1. Dado que los requerimientos de software establecidos (por ejemplo en la especificación de requerimientos) llegan a servir como contrato entre cliente y desarrolladores y que en ciertos contextos es indispensable documentar todo el proceso de desarrollo de una forma estándar, pueden tener que escribirse extensos documentos, costosos de crear, leer y mantener siguiendo pautas. Un ejemplo de un estándar para especificar requerimientos del proceso empleado a manera de contrato y para consignar desarrollo del proceso de desarrollo, es el MIL-STD-498 empleado por agencias del Departamento de Defensa de EUA. Puede consultarse por ejemplo en: http://www.pogner.demon.co.uk/mil_498/.

    Analice este estándar e indique:
    1. En que condiciones y para que tipos de programas le parece aplicable
    2. Estime el tiempo que requiere sólo llenar las plantillas de este estándar para resolver un problema de software (sin elementos de hardware o de otra índole) de mediana complejidad. No tenga en cuenta el tiempo para llevar a cabo cada actividad, sólo para llenar las plantillas que este estándar exige para un proyecto de ese estilo.


  2. Busque una plantilla para el documento con la especificación de requerimientos de software, indique en que casos es aplicable y estime el tiempo que requiere completarla.

  3. Busque una notación formal para especificación de requerimientos y describa en que contexto es aplicable.

  4. En diversos sitios de Internet se emplean Wikis para mantener información de manera colaborativa y no controlada. Revise http://www.programacionextrema.org y describa como podría mejorar la página LosRoles para cambiar el término ``Coach'' por un equivalente apropiado en español.

2.2  Pruebas de Software

Indicadores de logro:

2.2.1  Teoría

De acuerdo a [6] las pruebas al software son ``la verificación dinámica del comportamiento esperado de un programa sobre un conjunto finito de casos, seleccionados de forma apropiada del dominio de ejecución infinito.''

``Actualmente se considera que la actitud correcta hacia la calidad es la de prevención: obviamente es mejor evitar problemas que corregirlos. Las pruebas deben verse, principalmente como un medio de chequear no sólo si la prevención ha sido efectiva, sino también para identificar fallas en aquellos casos donde, por alguna razón, no ha sido efectiva.''

Las principales áreas de pruebas de software son: Distinguimos entre la causa de un funcionamiento errado (a la que llamamos defecto en inglés fault) y el efecto observado que no se desea, al cual llamamos falla (en inglès failure). Las pruebas revelan fallas pero son los defectos los que deben ser eliminados.

Un criterio de selección permite elegir los casos de pruebas. Estos casos pueden ser evaluados con base en objetivos de pruebas.

Al hacer pruebas para identificar defectos una prueba exitosa es una que hace fallar al sistema.

Un oráculo es algún agente (humano o mecánico) que decide si un programa se comporta correctamente en una prueba dada.

Las pruebas pueden verse como una estrategia para manejar riesgo, pues no es posible hacer pruebas completas a software real: ``Las pruebas a los programas pueden usarse para mostrar la presencia de fallas, pero nunca para mostrar su ausencia´´ (Dijkstra).

Son rutas imposibles las rutas en el flujo de control que no pueden ser ejercitadas por entrada alguna.

Por posibilidad de prueba (del inglés testability) se entiende (1) la factibilidad de un programa de satisfacer un criterio de pruebas de cobertura (2) probabilidad que un programa presente una falla al probarlo.

2.2.2  Niveles de pruebas

El objeto de la prueba puede variar: ser un sólo módulo, un grupo de módulos o un sistema completo.

2.2.3  Objeto de la prueba

Con pruebas de unidades se verifica el funcionamiento aislado de piezas de software que por separado pueden probarse. Con pruebas de integración se verifica la interacción entre componentes de software, usualmente se prefieren estrategias de pruebas de integración incrementales, a poner todos los componentes juntos a la vez.

Las pruebas del sistema se consideran apropiado para comprobar requerimientos no funcionales del sistema como seguridad, velocidad, precisión y robustez. También se evalúan interfaces con otras aplicaciones, utilidades, dispositivos de hardware y el ambiente de operación.

2.2.4  Objetivos de las pruebas

Cuando los objetivos de las pruebas se establecen de forma precisa y cuantitativa es posible controlar el proceso de pruebas. Pueden establecerse como objetivo probar propiedades funcionales (pruebas de cumplimiento, pruebas de corrección, pruebas funcionales) así como propiedades no funcionales (seguridad, robustez), a diversos niveles (el objetivo de prueba depende del objeto de la prueba).

Las pruebas de aceptación chequean comportamiento del sistema contra los requerimientos del cliente. Las pruebas de instalación pueden verse como pruebas del sistema realizadas en hardware acorde con los requerimientos de configuración. Los procedimientos de instalación también pueden ser verificados.

Las pruebas alpha y beta se dan a un reducido grupo de usuarios potenciales bien internos (alpha) o externos (beta), quienes reporta problemas.

Las pruebas de cumplimiento o funcionales o de corrección validan si el comportamiento observado del software probado cumple con su especificación.

Las pruebas de logros de robustez ayudan a mejorar robustez (al buscar fallas) y a derivar medidas estadísticas de robustez.

Las pruebas de regresión son "la reiteración de pruebas de un sistema o componente para verificar que las modificaciones no han causado efectos no intencionados."

Las pruebas de desempeño verifican que el software cumpla requerimientos de desempeño (e.g capacidad y tiempo de respuesta). Una forma particular son las pruebas de volumen, en la que se prueban las limitaciones internas del programa o sistema.

Las pruebas de estrés ejercitan un programa en su carga de diseño máxima, así como más allá de esta.

En las pruebas espalda contra espalda se compara la misma prueba en dos versiones implementadas de un producto de software.

Las pruebas de recuperación buscar probar las capacidades de un programa para recuperarse después de un desastre.

Las pruebas de configuración analizan el software bajo diversas configuraciones (en los casos que el programa sirva a diversos tipos de usuarios).

Las pruebas de usabilidad evalúan que tan fácil es para los usuarios finales emplear el programa y la documentación, y que tanto apoya las labores del usuarios y su habilidad para recuperarse de errores del usuario.

2.2.5  Técnicas para probar

Dado que el objetivo de probar es identificar fallas para esto se emplean diversas técnicas que procuran identificar sistemáticamente un conjunto representativo de comportamientos del programa. Una clasificación posible es:

Basadas en intuición y experiencia del ingeniero de software

Las pruebas Ad Hoc se basan en la intuición y experiencia del ingeniero con programas similares, puede capturar pruebas especiales que no son capturadas con facilidad por técnicas formales.

Al aplicar pruebas exploratorias no se emplea un plan de pruebas, sino que estas se diseñan, ejecutan y modifican dinámicamente. Su efectividad depende del conocimiento el ingeniero de software.

Basadas en especificación

En las pruebas de particionamiento de equivalencia los datos de entrada se dividen en clase de equivalencia y se prueba un conjunto representativo de cada clase.

En las pruebas de análisis de valores de frontera, se toman casos sobre o cercanos a las fronteras del dominio de entrada de las variables (las pruebas de robustez son extensión de estas). .

En las pruebas de tablas de decisión, se representan relaciones entre entradas y salidas con tablas de decisión a partir de las cuales se derivan casos de pruebas de las posibles combinaciones de condiciones y acciones.

Las pruebas basadas en máquinas de estado finito se derivan para cubrir los estados y transiciones de un modelo del programa como máquina de estado finito.

A partir de una especificación formal (basada en modelo, o algebraica) es posible derivar automáticamente casos de prueba.

En las pruebas aleatorias se conoce bien el dominio de entradas y se derivan casos de pruebas de forma totalmente aleatoria.

Basadas en código

El criterios de cubrimiento del flujo de control procura cubrir todas las instrucciones o bloques de instrucciones en un programa o una combinación especificada de estos (por ejemplo cobertura de condicionales, cobertura de instrucciones). La forma más fuerte es pruebas de vías que generalmente no es practicable a causa de los ciclos.

En las pruebas basadas en el flujo de datos el grafo de flujo se anota de acuerdo al uso de variables (definición, acceso, eliminación).

Basadas en fallas

En tanteo de errores se diseñan casos de pruebas que buscan descubrir los defectos más plausibles de un programa. Una fuente de información es la historia de defectos de proyectos anteriores.

En las pruebas de mutación se generan un gran número de programas mutados (i.e cada uno con cambios sintácticos menores con respecto al programa que se prueba) y con un conjunto fijo de casos de prueba. Se busca eliminar programas mutados cuyo comportamiento difiera al del programa original (está técnica se concibió originalmente para poner a prueba los casos de prueba).

Basadas en uso

Perfil operacional: la idea es inferir de los resultados de los casos de pruebas la robustez futura del software cuando esté en uso real.

Las pruebas de ingeniería de robustez ocupan el proceso de desarrollo completo "se diseñan y guían por objetivos de robustez y por el uso esperado."

Basadas en la naturaleza de la aplicación

Entre otras se derivan pruebas en dominios particulares: pruebas orientadas a objetos, basadas en componentes, basadas en el web, pruebas a interfaces gráficas de usuario, a programas concurrentes, a sistemas de tiempo real, a sistemas críticos.

Selección y combinación de técnicas

Las técnicas basadas en la especificación y las basadas en el código (i.e Funcionales y estructurales) son complementarias, pues emplean fuentes diferentes y enfatizan en diferentes tipos de problemas.

Los casos de pruebas pueden determinarse de forma determinística o bien de forma aleatorias.

2.2.6  Medidas relacionadas con pruebas

Por una parte es posible realizar mediciones sobre el programa que se prueba: Por otra parte pueden hacerse mediciones a las pruebas realizadas:

2.2.7  Proceso de pruebas

Los conceptos, las estrategias, las técnicas y las medidas deben ser integradas en un proceso controlado y definido llevado a cabo por personas.

Consideraciones prácticas

En un proceso de pruebas es importante una actitud colaborativa hacia las pruebas y la calidad. El administrador debe promover una actitud sin egos (prevenir un concepto de propiedad en el código entre programadores).

Deben establecerse guías de pruebas por ejemplo basadas en riesgo (prioridades) o en escenarios (los casos de pruebas se especifican a partir de escenarios de uso del software).

Las actividades debe organizarse junto con personas, herramientas, políticas, medidas en un proceso bien definido y que haga parte integral del ciclo de vida.

La documentación es fundamental en las pruebas, entre los documentos pueden estar: Plan de pruebas, Especificación del diseño de pruebas, especificación del procedimiento de prueba, especificación de los casos de pruebas, bitácora de pruebas y reporte de incidentes de pruebas o problemas.

Como parte del proceso de pruebas también debe definirse el o los equipos de pruebas que puede ser bien interno o externo o ambos, dependiendo de los costos, horarios, niveles de madurez y prioridad de la aplicación.

Pueden emplearse medidas como número de casos ejecutados, aprobados y fallidos para evaluar la efectividad del proceso de pruebas.

También debe decidirse cuando terminar las pruebas por ejemplo con ayuda de cobertura de código, o completitud funcional así como riesgos y costos.

En lo posible para realizar pruebas o mantenimiento de forma efectiva en costos, deben reusarse los medios de pruebas. Los materiales de prueba deben estar bajo el control de un programa de administración de configuraciones.

Actividades de prueba

Las actividades depende del proceso de administración de configuraciones, estas actividades pueden ser:

2.2.8  Hacía la práctica: Pruebas de unidades

En programación extrema un lema es "programar un poco y probar", para esto se emplean pruebas de unidades, las cuales suelen agilizarse y automatizarse con herramientas.

Este es el fin de programas como check (para C), junit para Java, cppunit para C++ y OUnit para Ocaml entre otros.

check es una librería que se encadena a un módulo de prueba cuyas fuentes están en C. Para organizar las fuentes de un programa en C, estas pueden dividirse en módulos (i.e un módulo consta de una fuente en C y su encabezado con prototipos de funciones exportables). Cada uno de estos módulos debería tener una funcionalidad bien definida y limitada, por ejemplo en un programa que requiera usar tablas de hashing podría haber un módulo que maneje únicamente tablas de hashing.

Para probar un módulo (digamos hash.c y hash.h) con check debe hacerse un programa de prueba (digamos prueba_hash.c) que pruebe las funciones públicas/exportables de hash.c con diversos casos de prueba (se sugiere probar casos límites --como caso vacío, casos con entradas no válidas-- así como casos típicos de diversa complejidad).

Una vez creados los casos de prueba en prueba_hash.c y la función main en el mismo archivo, pueden compilarse y ejecutarse con:
cc -c hash.c
cc -c prueba_hash.c
cc -o prueba_hash hash.o prueba_hash.o /usr/local/lib/libcheck.a
remplazando la ruta de libcheck.a por la que corresponda en su plataforma.

2.2.9  Lecturas recomendadas

Puede consultarse más sobre pruebas en el capítulo 5 de [6].

Hay un listado de programas útiles para pruebas y programación extrema en http://www.xprogramming.com/software.htm

check está disponible en http://check.sourceforge.net/

junit está disponible en http://www.junit.org/index.htm

cppunit está disponible en http://cppunit.sourceforge.net/

OUnit está disponible en http://www.xs4all.nl/ mmzeeman/ocaml/

2.2.10  Ejercicios para afianzar teoría

  1. Revise la documentación bien de junit o bien de cppunit y describa como usaría una de estas herramientas para hacer pruebas de unidades.

  2. En sistemas tipos Unix y en el caso de programas escritos en C, es posible medir cubrimiento de pruebas sobre las fuentes usando la herramienta gcov y con los flags -fprofile-arcs -ftest-coverage del compilador gcc. Consulte la documentación de este programa y explique como puede usarlo para medir cobertura de las pruebas que se sugieren en el primer ejercicio de programación (ver más adelante).

2.2.11  Ejercicios de programación

  1. Es posible representar conjuntos de enteros usando bits de un entero. Esto se ha implementado en el módulo disponibles en
    http://structio.sourceforge.net/guias/ayudadesprog/conjuntos.c y http://structio.sourceforge.net/guias/ayudadesprog/conjuntos.h

    Descargue conjunto.h y conjunto.c de
    http://structio.sourceforge.net/guias/ayudadesprog/contrib/conjuntos.c

    Diseñe e implemente pruebas de unidad para ese módulo usando check.

2.3  Administración de configuraciones de Software

Indicadores de logro:

2.3.1  Teoría

``Un sistema puede verse como una colección de versiones de elementos de hardware, firmware o software, combinados de acuerdo al proceso de construcción para servir un fin particular. La administración de configuraciones del software (ACS) es la disciplina que identifica la configuración de un sistema en distinto puntos de tiempo con el propósito de controlar sistemáticamente los cambios a la configuración, mantener la integridad y rastreabilidad de la configuración durante el ciclo de vida del sistema.'' [6]

2.3.2  Manejo y planeación del proceso de ACS

Un bueno manejo de ACS requiere entender el contexto organizacional y las restricciones en el diseño e implementación del ACS.

Desde el punto de vista organizacional el ACS puede tener que ser consistente con procesos de administración de configuraciones de hardware y firmware, o con actividades de aseguramiento de calidad de la organización. La relación más cercana es con los equipos de desarrollo y mantenimiento. Frecuentemente las mismas herramientas apoyan el desarrollo, el mantenimiento y la ACS.

Las restricciones que afectan el manejo pueden ser: políticas y procedimientos puesto en otros niveles de la organización, el contrato entre quien adquiere y quien suple, cuerpos reguladores externos, el proceso del ciclo de vida del software escogido.

La planeación debe ser acorde con el contexto organizacional, las restricciones aplicables, guías comúnmente aceptadas y la naturaleza del proyecto. Tal planeación debe incluir: identificación de la configuración, control de la configuración, registro de estado de la configuración y manejo de las entregas. Aspectos como organización y responsabilidades3; recursos y cronogramas4; selección de herramienta e implementación5; control de vendedores y subcontratante y control de interfaces6 comúnmente son considerados y consignados junto con las actividades de planeación en un Plan de Administración de Configuración de Software, el cual típicamente está sujeto a una revisión y a auditoría de asegurameniento de calidad.

Puede requerirse algún grado de supervisión a la ACS, que puede estar a cargo de un autoridad que asegure la calidad del proceso. Para realizar esta supervisión pueden emplearse medidas a la ACS y auditorías a los productos de la implementación de la ACS.

2.3.3  Identificación de la configuración del software

En esta actividad se identifican elementos por controlar (elementos de la configuración del software), se establecen esquemas para los elementos y sus versiones, y se establecen las herramientas y técnicas que se usarán para adquirir y manejar elementos por controlar.

Entre los elementos de configuración del software (unidad controlada por el proceso de ACS), además de código puede haber: planes, especificaciones y diseños, documentación, material de prueba, herramientas de software, fuentes y código ejecutable, librerías de código, datos y diccionarios de datos y documentación. El proceso de selección de elementos de configuración de software debe buscar un balance entre dar visibilidad adecuada con propósitos de controlar el proyecto y dar un número manejable de elementos a controlar.

Una versión de un elemento de software es un elemento particular identificado y especificado (un estado de un elemento que evoluciona). Una revisión es una nueva versión de un elemento que remplaza una versión anterior. Una variante es una nueva versión de un elemento que se agrega a la configuración sin remplazar la versión anterior.

Una línea de base del software (en inglés baseline) es un conjunto de elementos de configuración formalmente designados y fijados en un tiempo específico durante el ciclo de vida del software.

Para agregar nuevos elementos de configuración debe seguirse un proceso de aprobación.

2.3.4  Control de la configuración del software

El control de configuración de software se refiere al manejo de cambios durante el ciclo de vida del software. Cubre el proceso de determinar que cambios hacer, la autoridad para aprobar cambios (el comité de control de configuración), soporte para la implementación de cambios, y el concepto formal de desviación de los requerimientos del proyecto.

Con un proceso para peticiones de cambio en el software se establece una autoridad y la forma de enviar y registrar peticiones de cambio, evaluar costos e impactos potenciales del cambio propuesto, y la forma de aceptar, modificar o rechazar cada cambio solicitado. Un enlace ente el proceso para peticiones de cambio y un sistema para reportar fallas puede facilitar el seguimiento de soluciones.

2.3.5  Registro del estado de la configuración del software

Se trata del registro y generación de reportes de información necesaria para el manejo efectivo de la configuración de software. El tipo de información incluye: identificación de configuraciones aprobadas, identificación y de estado de cambios, desviaciones y permisos.

La información reportada puede ser empleada por varios elementos de la organización y el proyecto: equipo de desarrollo, equipo de mantenimiento, administradores del proyecto y actividades de calidad. Esta información puede ser base de mediciones (e.g número de cambios requeridos por elemento de configuración de software, tiempo promedio requerido para implementar una petición de cambio).

2.3.6  Auditoría a la configuración de software

Una auditoría se realiza para evaluar el cumplimiento de productos de software y los procesos a las regulaciones, estándares, guías, planes y procedimientos aplicables. Cada auditoría se realiza de acuerdo a un proceso bien definido con varios roles y responsabilidades.

Puede hacerse una auditoría a la configuración funcional para asegurar que el elemento de software auditado es consistente con la especificación que lo gobierna, o una auditoría a la configuración física para asegurar que el diseño y la documentación de referencia es consistente con el producto de software como se construye.

Manejo y distribución de entregas de software

El termino entrega se refiere a la distribución de una configuración de software hacia afuera de la actividad de desarrollo.

Por manufactura de software se entiende la actividad de combinar las versiones apropiadas de los elementos de configuración, usando los datos de configuración apropiados en un programa ejecutable. Se manufactura usando versiones particulares de herramientas de apoyo como compiladores.

El manejo de las entregas de software incluye identifica, empacar y distribuir los elementos de un producto, por ejemplo ejecutable, documentación, notas de distribución7 y datos de configuración.

2.3.7  Hacía la práctica: Herramientas y CVS

Entre las herramientas sugeridas en [6] para el manejo de configuraciones están:

2.3.8  CVS

CVS es una herramienta fácil de usar e instalar, que permite controlar versiones, llevar varias líneas de desarrollo de un mismo proyecto y trabajar de manera distribuida.

La siguiente descripción de la forma de uso, una vez creado un repositorio es extraída de [16] :
  1. Hay un repositorio central donde está la copia ``oficial'' más actualizada. Usted ha configurado las variables de ambiente CVSROOT y CVS_RSH con la localización del repositorio y el programa para ingresar a la máquina donde está. Las posibilidades son:

  2. Cada desarrollador debe obtener una copia de ese repositorio y dejarla en su cuenta para trabajarla. Esto es hacer un checkout, por ejemplo para obtener una copia local del módulo quimica:
    cvs checkout quimica
    
    En lugar de checkout puede emplear co. Si lo requiere puede pasar opciones generales a CVS antes de checkout (como -z3 que indica transmitir información comprimida):
    cvs -z3 co quimica
    


  3. Cada desarrollador con alguna frecuencia debe actualizar su copia local con respecto a la del repositorio central, para mantener al día su copia con los cambios más recientes introducidos por otras personas. Esto es hacer un update (es importante que lo haga antes de comenzar a hacer cambios a su copia local). Por ejemplo puede usar:
    cvs -z3 update -Pd
    
    Note que en este comando la opción -z3 es una opción general de CVS, mientras que -Pd es particular a update (indica que no deben agregarse a la copia local directorios que estén vacíos en el repositorio).

  4. Cada desarrollador trabaja en su copia personal, y cuando completa una parte del trabajo (mejor asegurándose de no introducir errores), publica la actualización junto con un comentario (en el siguiente ejemplo es ``Ortografía corregida''):
    cvs commit -m "Ortografía corregida"
    
    Eventualmente al intentar esta operación, si otro usuario realizó una actualización primero que tiene alguna diferencia con respecto a los cambios que se quieren agregar, CVS detecta el conflicto, modifica el archivo para resaltar los conflictos y permite solucionar el conflicto antes de permitir esta operación. Por ejemplo un conflicto puede verse como:
    <<<<<<1.3                                                                  
    la presion interna de un gas es directamente proporcional a su temperatura.
     -------                                                                   
    La presión interna de un gas es inversamente proporcional a la temperatura
    >>>>>>
    
    Para solucionar el conflicto elimine las líneas que CVS agregó y deje la versión más precisa (o mézclelas), para este caso sólo quedaría:
    La presión interna de un gas es directamente proporcional a la temperatura.
    
La instalación de CVS y la configuración de los protocolos pserver y ssh se presenta por ejemplo en la sección `Servicio CVS' de [16]. Una vez instalado en un sistema pueden crearse repositorios como se describe en el siguiente extracto de la misma referencia:

Un usuario pepe emplearía una secuencia de comandos como la siguiente para crear un repositorio en /home/pepe/cvs:
cd ~
mkdir cvs
cd cvs
export CVS_RSH=""
export CVSROOT=/home/pepe/cvs
cvs init
pepe mismo o algún otro usuario del sistema que tenga permiso de escritura en ese directorio podrá leer (e.g update) y escribir (e.g commit) en el nuevo repositorio. Para eso basta que emplee:
export CVS_RSH=""
export CVSROOT=/home/pepe/cvs
También puede usarlo vía ssh (por ejemplo desde otro computador de la misma red) con CVS_RSH="ssh" . O si durante la instalación o reconfiguración de cvs el administrador activa el protocolo pserver puede emplearse CVSROOT=:pserver:pepe@purpura.micolegio.edu.co:/home/pepe/cvs, siendo pepe un usuario de la máquina donde está el repositorio con permisos para leer (o escribir) o un usuario para ese repositorio agregado en el archivo /home/pepe/cvs/CVSROOT/passwd9

Todo usuario con permiso de escritura en ese repositorio podrá ingresar un módulo nuevo. Por ejemplo un usuario con permiso de escritura y las variables CVS_RSH y CVSROOT con los valores recién presentados, puede ingresar todo el contenido del directorio  /quimica como módulo laquimica:
cd ~/quimica
cvs import laquimica start vendor
Tras lo cual el mismo usuario que importó el módulo, o cualquier otro con permiso de lectura, puede sacar su copia local y hacer otras operaciones usuales de CVS.

2.3.9  Manejo de configuraciones y programación extrema

Las doce mejores prácticas de programación extrema son: [4]
  1. Proceso de planeación: permite al cliente definir el valor de negocio de cada característica, y usa estimativos de costo dados por los programadores, para escoger lo que se necesita hacer y lo que necesita ser postergado.
  2. Pequeñas entregas: El equipo de PE (Programación extrema) pone un sistema simple en producción pronto, y lo actualiza con frecuencia en un corto ciclo.
  3. Metáforas: El equipo de PE usa un sistema de nombres común y un sistema común de descripción que guía el desarrollo y la comunicación.
  4. Diseño simple: Un programa desarrollado con PE debe ser el más simple que cumple con los requerimientos actuales. No hay mucha construcción ``para el futuro''. Obviamente es necesario asegurar un buen diseño, que en PE se buscan refactorizando.
  5. Probar: El equipo de PE se enfoca en la validación del software todo el tiempo. Los programadores desarrollan software escribiendo primero las pruebas, entonces el software que cumple los requerimientos reflejados en las pruebas. Los clientes proveen pruebas de aceptación que les permite asegurarse de que las características que necesita están.
  6. Refactorizar: El equipo de PE mejor el diseño del sistema durante todo el desarrollo. Esto se hace manteniendo el software limpio sin duplicar, con alta comunicación, simple y sin embargo completo.
  7. Programación en parejas: Los programadores PE escriben todo el código de producción en parejas, dos programadores trabajan juntos en una máquina.
  8. Propiedad colectiva: Todo el código pertenece a todos los programadores. Esto permite que el equipo vaya a velocidad total, porque cuando algo necesita ser cambiado, puede cambiarse sin demora.
  9. Integración continua: Los equipos de PE integran y manufacturan el sistema de software múltiples veces al día. Esto mantiene a todos los programadores en la misma página y permite un progreso rápido. Al integrar con más frecuencia se tiende a eliminar los problemas de integración que plagan equipo que integran con menos frecuencia.
  10. Semanas de 40 horas: Los programadores cansados cometen más errores. Los equipos PE no trabajan tiempo excesivo, manteniéndose frescos, saludables y efectivos.
  11. Cliente en el sitio: Un proyecto de PE es guiado por un individuo dedicado que puede determinar requerimientos, poner prioridades, contestar preguntas cuando los programadores las tienen. El efecto de estar ahí es que la comunicación mejora, con menos documentación --a menudo una de las partes más costosas de un proyecto de software.
  12. Estándar de programación: Para que un equipo trabaje efectivamente en parejas, y para compartir la propiedad del código, todos los programadores necesitan escribir el código de la misma forma, con reglas que aseguren que el código se comunica claramente.
Con respecto a administración de configuraciones por ejemplo apoyada por CVS se pueden poner en práctica: propiedad colectiva del código, integración continua, pequeñas entregas y refactorización (ver [8]).

2.3.10  Lecturas recomendadas

Pueden consultarse diversas herramientas de fuentes abiertas para seguimiento de defectos, mejoras y problemas en http://linas.org/linyx/pm.html

Para conocer más sobre CVS puede consultar [10].

Es posible emplear CVS desde sistemas Windows con WinCVS, una explicación puede consultarse en [11].

2.3.11  Ejercicios

  1. Revise la infraestructura de http://structio.sourceforge.net (se sugiere que abra una cuenta para que pueda explorarla mejor), identifique y describa las herramientas ayudas ofrecidas para realizar: (1) seguimiento de defectos, mejoras y problemas (2) control de versiones (3) manufactura, (4) liberación de entregas.

  2. Usando CVS saque la versión estable de Bugzilla del repositorio CVS. Vea la sección `Initial Checkout' en http://www.bugzilla.org/download/

  3. En un sistema tipo Unix cree un repositorio para CVS, importe un módulo y después extraigalo.

2.4  Calidad de software

Indicadores de logro:

2.4.1  Teoría

La definición de calidad depende de la organización y del proyecto, pero un punto claves es que los requerimientos del programa definan las características de calidad requeridas por el software10.

Se espera que en la cultura de los ingenieros de software la calidad del software y la ética jueguen roles fundamentales.

Los costos de la calidad dependen de las expectativas del cliente, se espera que el ingeniero de software puede presentar alternativas de calidades y sus costos.

Dentro los modelos de calidad pueden considerarse la calidad del proceso de ingeniería de software y la calidad del producto. Cómo modelo de calidad del proceso se han usado dos estándares complementarios: ISO9001 (junto con ISO9003) y CMMi, de estos el segundo da guías para mejorar procesos e incluye áreas específicas relacionadas con calidad de software (aseguramiento de calidad de proceso y producto, verificación del proceso y validación del proceso). Como modelo de calidad del producto se ha empleado el estándar ISO9126, el ingeniero de software debe descubrir requerimientos de calidad que no estén explícitos entre los requerimientos y discutir su importancia así como los niveles de dificultad requeridos para alcanzarlos y sus costos.

Es posible mejorar la calidad siguiendo un proceso continuo que requiere control de administración, coordinación y retroalimentación de diversos procesos concurrentes (e.g. ciclo de vida del software y el proceso de detección, eliminación y prevención de defectos). Para alcanzar objetivos en materia de calidad se pueden emplear aproximaciones como Total Quality Management (TQM) o Plan, Do, Check and Act (PDCA).

Procesos de administración de la calidad del software

Estos procesos constan de tareas y técnicas que indican como los planes de software (e.g administración, desarrollo, manejo de configuraciones) están siendo implementados y que tan bien están cumpliendo con los requerimientos los productos intermedios y finales. Algunos son:

Algunas actividades de este proceso encuentran defectos directamente mientras que otras indican donde puede ser valioso examinar más.

Deben definirse los procesos que se aplican, los propietarios de proceso y requerimientos para los procesos, métricas para los proceso, sus salidas y canales de retroalimentación.

2.4.2  Consideraciones prácticas

Deben tenerse en cuenta

2.4.3  Hacía la práctica

Ética

Uno de los aspectos éticos por tener en cuenta es el respeto por la propiedad intelectual. En la práctica es sencillo: es decir la verdad completa y clara sobre la procedencia de lo que se hace, respetando los términos de reproducción. A continuación se adapta parte de [18] para el caso de programas:
De basarse en fuentes ya existentes (bien sea copiando, modificando, resumiendo o inspirándose) recuerde dar los créditos para que otras personas puedan consultar lo que usted consultó o para no incurrir en plagio.

Para copiar o modificar porciones considerables de una fuente ya existente tenga en cuenta:
  1. Verifique que los términos de reproducción permitan la copia/modificación (normalmente las fuentes incluyen los términos de reproducción al comienzo de cada archivo o en un archivo separado e.g Derechos.txt, COPYING, RIGHTS).
  2. Cumpla con los requisitos de los términos de reproducción, por ejemplo si es el caso transcriba y mantenga los términos de reproducción de la fuente original en la suya.
  3. De el crédito al autor o autores.
  4. Para que otras personas puedan consultar la fuente, cite el sitio donde puede encontrarse (e.g un URL).
En el caso de documentos, si los términos de reproducción de un documento que desee copiar o modificar no permite copia o modificación, tiene opciones legalmente válidas: Si está haciendo un material en la que usted es el/la autor(a), sugerimos que escriba explícitamente que usted es el/la autor(a) y: Si tiempo después de haber creado o contribuido en una obra, detecta inconsistencia entre términos de reproducción o ausencia de créditos, es preferible corregirlos. Así evitará incurrir en plagio o infringir derechos de reproducción (consideramos que es errado oscurecer la verdad o mentir, además son ilegales el plagio e infringir derechos de reproducción).

Análisis estático: Verificación de programas y aserciones intermedias

Es posible verificar formalmente que un programa es correcto. Una técnica para lograrlo fue presentada por Hoare se basa en especificar lo que diversas porciones del programa deben hacer empleando aserciones en un lenguaje que usa el estado del programa (variables y sus valores), así como conectivos lógicos. Por ejemplo si en la siguiente porción de código se cumpliera al comienzo la aserción x>0 (i.e precondición), podríamos deducir11 que después de ejecutarse se cumple y==x (i.e poscondición):
if (x<0) {
        y=-x;
} 
else {
        y=x;
}
Esta técnica puede usarse para demostrar que son correctas porciones de código, la demostración puede hacerse bien manualmente o con ayuda de herramientas.

Esta verificación se facilita si las precondiciones y poscondiciones se escriben (al menos parcialmente) mientras se escribe el programa. Entre diversas posibilidades puede hacerse en el caso de C y C++ con la macro assert (definida en assert.h) y en Java con la palabra reservada assert (definida en Java 1.4), por ejemplo la porción anterior quedaría:
assert(x>0);
if (x<0) {
 y=-x;
} 
else {
 y=x;
}
assert(y==x);
Note que assert recibe una expresión booleana, en tiempo de ejecución continuará si la expresión es cierta y terminará el programa con un mensaje de error si la expresión es falsa. De esta forma assert facilita tanto un análisis estático del código dando parte de la especificación y por otra permite hacer pruebas mientras se ejecuta el programa. Por esto assert facilita encontrar fallas de programación. Use assert para detectar fallas de programación no para detectar información inválida dada por usuario.

En C y C++ si no desea que assert realice verificación de aserciones, defina al compilar el símbolo NDEBUG del preprocesador. Una forma fácil de lograrlo es pasando este símbolo al compilador/preprocesador o definiendo #define NDEBUG en las fuentes.

Análisis estático: Chequeadores de código fuente

Algunos compiladores realizan análisis de fuentes más profundos que otros para detectar errores no triviales (por ejemplo variables no inicializadas o porciones de código que no serán ejecutadas).

Existen herramientas que hacen análisis más profundos que los realizados por compiladores (los compiladores no pueden hacer análisis muy profundos por que deben producir código objeto rápidamente). Una de estas herramientas para C es splint y una para Java es jlint.

2.4.4  Lecturas recomendadas

Para conocer más sobre verificación de programas puede ver [9].

Puede ver ejemplos del uso de aserciones en Java en:
http://java.sun.com/j2se/1.4.2/docs/guide/lang/assert.html

2.4.5  Ejercicios

  1. Consulte la documentación de splint y use este programa para chequear uno de sus programas en C.

  2. Busque la documentación de jlint y úselo para chequear estáticamente uno de sus programas en Java. Opcional: Use antic con una fuente en C++ http://www.bugzilla.org/download/

  3. Escoja uno de los programas que ha desarrollado, uno que emplee funciones y agrega a cada función algunas precondiciones y algunas poscondiciones con assert.

2.5  Mantenimiento de software

Indicadores de logro:

2.5.1  Teoría

"El mantenimiento de software se define como la totalidad de actividades requeridas para dar soporte al software de forma efectiva en costos."12. Estas actividades se realizan tanto antes de la entrega como después de la entrega del software. Las actividades posteriores a la entrega incluyen: modificación, entrenamiento y operación o uso de un escritorio de ayuda.

Las solicitudes de modificaciones deben ser registradas y seguidas, el impacto de los cambios se determina, se modifican código y otros artefactos del software, se realizan pruebas y se libera una nueva versión del producto. Así mismo se da entrenamiento y soporte diario a los usuarios.

Se realizan modificaciones para: Se ha estimado que más del 80% del esfuerzo para mantener software es usado en acciones no correctivas. La inclusión de solicitudes de mejoras junto con solicitudes de corrección (usualmente juntadas por administradores de software) contribuyen en algunas de las falsas concepciones sobre el alto costo de las correcciones.

Dado que el software muestra un comportamiento regular y tendencias que pueden ser medidas. Se han desarrollado modelos predictivos que estiman el esfuerzo de mantenimiento que ha de hacerse.

Pueden clasificarse los tipos de mantenimiento en:

Aspectos claves en el mantenimiento del software

Entre los aspectos técnicos pueden resaltarse: Entre los aspectos administrativos: Para la estimación de costos se emplean modelos paramétricos y experiencia (en forma de juicios, analogías y descomposición estructural). Claramente la mejor aproximación es integrar datos empíricos (resultantes de mediciones) y experiencia.

Entre las medidas están: tamaño, esfuerzo, cronograma y calidad. También existen medidas para determinar la facilidad de analizar, la facilidad de modificar, la estabilidad y la posibilidad de realizar pruebas.

2.5.2  Proceso de mantenimiento

El proceso de mantenimiento descrito en el estándar IEEE 1219 es:
  1. Requerimiento de modificación
  2. Clasificación e identificación
  3. Análisis
  4. Diseño
  5. Implementación
  6. Pruebas del sistema
  7. Pruebas de aceptación
  8. Entrega
  9. Vuelve a 2.
Las actividades para implementar el proceso de mantenimiento de acuerdo al estándar ISO/IEC 14764 se basan en iteraciones de:
  1. Análisis del problema y de modificaciones
  2. Implementación de modificaciones
  3. Aceptación/revisión del mantenimiento
que en cualquier momento puede terminarse bien porque se retira el mantenimiento o porque se logra una migración.

Aunque varias actividades de mantenimiento son comunes a actividades de desarrollo (análisis, diseño, programación, pruebas y documentación), hay actividades propias del mantenimiento: Además quienes mantienen debe realizar actividades de soporte como planeamiento del mantenimiento, manejo de configuraciones, verificación y validación, aseguramiento de calidad, revisiones, auditorías y entrenamiento a usuarios.

Un plan de mantenimiento debe tener en cuenta: El control de configuraciones al mantenimiento se establece implementando un proceso de manejo de configuraciones (ver ??). El aseguramiento de calidad debe mantenerse y planearse para el mantenimiento (incluyendo validaciones y verificaciones, revisiones, auditorías).

2.5.3  Técnicas de mantenimiento

2.5.4  Lecturas recomendadas

Para conocer más sobre verificación de programas puede ver [9].

2.5.5  Ejercicios

  1. Busque y pruebe alguna herramientas para facilitar la comprensión de código.

  2. Busque y pruebe alguna herramientas para hacer ingeniería reversa.

2.6  Diseño de software

Indicadores de logro:

2.6.1  Teoría

En general diseño tiene relación con: metas, restricciones, alternativa, representaciones y soluciones.

El diseño de software es tanto "el proceso de definir la arquitectura componentes, interfaces y otras características de un sistemas o componente" como "el resultado de tal proceso"14. En esta etapa se analizan los requerimientos para producir una descripción de la estructura interna del software que servirá como base para su construcción. El resultado del diseño debe describir la arquitectura del software -- esto es, como se descompone y organiza en componentes-- y las interfaces entre estos componentes. También debe describir los componentes a un nivel de detalle que permita su construcción.

Durante esta etapa se producen varios modelos, se analizan y evalúan para determinar como cumplen los requerimientos, se evalúan soluciones alternas así como ventajas/desventajas. El modelo resultante sirve para planear las siguientes actividades.

El proceso de diseño suele constar de dos actividades: diseño arquitectónico y diseño detallado de software. En el primero se describe como se descompone y organiza el programa en componentes, en el segundo se describe el comportamiento específico de estos componentes.

Las técnicas empleadas como base para razonar sobre el diseño son:

Puntos claves en el diseño de software

Algunos puntos claves tienen que ver con calidad (e.g desempeño), algunos usualmente cortan a lo largo la funcionalidad del sistema, estos se han llamado ``aspectos''. Por ejemplo:

Estructura y arquitectura de software

En sentido estricto una arquitectura de software es "una descripción de los subsistemas y componentes de un sistema de software y las relaciones entre estos", pero el mismo termino se ha empleado para describir otras ideas relacionadas (en sistemas genéricos, patrones de diseño, familias de programas, o lineas de producción.

Una vista representa un aspecto parcial de una arquitectura de software que muestra propiedades específicas de un sistema de software, e.g la vista lógica (que satisface requerimientos funcionales, la vista de proceso (aspectos de concurrencia), la vista de desarrollo (como se divide el diseño en unidades de implementación). ``En resumen un diseño de software es un artefacto de múltiples facetas producido por el proceso de diseño y generalmente compuesto de vistas relativamente independientes y ortogonales.''

Un estilo arquitectónico es un conjunto de restricciones que definen un conjunto o familia de arquitecturas que las satisface. Por ejemplo: Mientras que los estilos pueden usarse para describir la organización a alto nivel (macroarquitectura) los patrones de diseño pueden usarse para describir detalles a un nivel más bajo y local (microarquitectura). Entre otros hay patrones de creación, patrones estructurales y patrones de comportamiento.

Para reutilizar diseños de software y componentes pueden diseñarse familias de software o líneas de producción de software. Que identifican puntos en común de tales familias y reusan y personalizan componentes teniendo en cuenta la variabilidad entre los miembros de la familia.

Evaluación y análisis de la calidad del diseño

Son atributos de buena calidad: mantenibilidad, portabilidad, facilidad de probar, rastreabilidad9, correcto, robusto, apropiado para el propósito.

Algunos son propios del tiempo de ejecución: desempeño, seguridad, disponibilidad, funcionalidad, usabilidad; y otros no: modificabilidad, portabilidad, reusabilidad, integrabilitad y facilidad para probar.

Entre las herramientas para asegurar calidad de un diseño están: Las medidas pueden clasificarse en:

Notaciones para diseño de software

Las siguientes notaciones describen la estructura (componentes e interconexión): Las notaciones de comportamiento (o vista dinámica) suelen usarse en el diseño detallado:

Estrategias y métodos para diseño de software

Las estrategias guían el proceso de diseño, los métodos son más específicos pues dan una notación, una descripción y un conjunto de lineamientos.

Estrategias: refinamiento y divide-y-conquista, estrategias arriba-abajo vs. abajo-arriba, abstracción de datos y encapsulamiento de información, uso de heurísticas, uso de patrones y lenguajes de patrones, uso de una aproximación iterativa e incremental.

El diseño orientado a funciones identifica las mayores funciones del programa, las elabora y refina. Produce entre otras diagramas de flujo, descripciones asociadas a procesos. Pueden usarse estrategias (análisis de transformaciones, análisis de transacciones) y heurísticas (fan-in/fan-out, alcance de efecto vs. alcance de control) para transformar un DFD en una arquitectura de software generalmente representada en un grafo de la estructura.

Para diseño orientado a objetos hay varios métodos que varían desde diseño OO basado en herencia y polimorfismo a diseño basado en componentes.

Los diseño centrado en estructuras de datos (como Jackson, Warnier-Orr) parten de las estructuras de datos de un programa (de entrada y de salida) y desarrolla las estructuras de control de estas.

El diseño basado en componentes busca proveer, desarrollar e integrar componentes15

También hay métodos formales de diseño y métodos transformacionales.



2.6.2  Lecturas recomendadas

Para conocer más sobre verificación de programas puede ver [9].

2.6.3  Ejercicios

  1. Consulte la documentación de splint y use este programa para chequear uno de sus programas en C.

  2. Busque la documentación de jlint y úselo para chequear estáticamente uno de sus programas en Java. Opcional: Use antic con una fuente en C++ http://www.bugzilla.org/download/

  3. Escoja uno de los programas que ha desarrollado, uno que emplee funciones y agrega a cada función algunas precondiciones y algunas poscondiciones con assert.

1
Es común que haya requerimientos contradictorios propuestos por diversos actores. El especialista de requerimientos buscará negociar requerimientos entre los diversos interesados teniendo en cuenta otras restricciones económicas, técnicas, regulatorias.
2
Suponiendo que en cada semana no hubiera distracciones, ni otras obligaciones y que supiera que hacer con exactitud.
3
Las responsabilidades específicas de un ACS necesitan ser asignadas a entes de la organización. Así mismo deben identificarse autoridad general y canales para reportar.
4
Los recursos y cronogramas deben estar relacionados con el cronograma del proyecto y las metas establecidas. Por ejemplo los requerimientos de entrenamiento para implementar el plan o para miembros nuevos debe especificarse.
5
Las herramientas pueden ser combinación de herramientas manuales y automáticas. Pueden permitir medir para mejorar el proceso. Pueden proveer soporte para: la librería de ACS, peticiones de cambios al software y procedimientos de aprobación, tareas de cambio en el código, reportes al estado de configuración y recolección de medidas, auditoría a la configuración de software, manejo y seguimiento a la documentación, realización de la manufactura del software, manejo y seguimiento de entregas y su distribución.
6
Cuando un elemento de software hace interfaz con otro software o hardware, un cambio en uno puede afectar al otro. En la planeación de ACS deben identificarse elementos interfaces y como se manejaran y comunicaran cambios en estos.
7
que típicamente describen nuevas capacidades, problemas conocidos y requerimientos de plataforma
8
Versiones para directorios, renombramientos y permisos. Más eficiente.
9
El archivo de claves para CVS cuando se usa pserver tiene 2 o 3 campos, el primero es el nombre con el que se conectará el usuario, el segundo la clave encriptada con DES y el tercer opcional es el nombre del usuario local a nombre del cual modificará el repositorio. Este archivo puede administrarse con htpasswd, para crear el archivo con un primer usuario: htpasswd -c pepe y posteriormente para agregar otros usuarios htpasswd agustin
10
La parte teórica de esta guía es traducción de apartes de [6].
11
Usando un cálculo apropiado (i.e cálculo de Hoare) con axiomas para cada posible sentencia del lenguaje.
12
Traducido así como otras partes de esta guía de [6].
13
Del inglés Service Level Agreements (SLA).
14
Traducido así como la mayoría de la sección teórica de esta guía de [6].
15
Un componente de software es una unidad independiente, con una interfaz y dependencias, puede componerse y entregarse de manera independiente.

Bibliografía

[1]
Javatm 2 platform std. ed. v1.3.1. http://java.sun.com/j2se/1.3/docs/api/index.html.

[2]
Working paper for draft proposed international standard for information systems-- programming language c++. http://ra.dkuug.dk/jtc1/sc22/open/n2356/, 1997.

[3]
Programming languages -- c. borrador del estándar c99. http://www.open-std.org/jtc1/sc22/open/n2794/n2794.txt, 1998.

[4]
Xprogramming.com: an extreme programming resource. http://www.xprogramming.com/, 2003.

[5]
The java tutorial. http://java.sun.com/docs/books/tutorial/, 2004.

[6]
Swebok: Guía al cuerpo de conocimiento en ingeniería de software. http://www.swebook.org, 2004.

[7]
Mehdi Achour, Friedhelm Betz, Antony Dovgal, and ... Manual de php. http://www.php.net/manual/es/.

[8]
Ulf Asklund, Lars Bendix, and Torbjörn Ekman. Configuration management for extreme programming. http://www.cs.lth.se/ bendix/Publications/ABE03/CM4XP.pdf.

[9]
Rodrigo Cardoso. Verificación y Desarrollo de Programas. 1993.

[10]
Per Cederqvist. Version management with cvs. https://www.cvshome.org/docs/manual/, 2004.

[11]
Jaime Irving Dávila. Wincvs: Un taller para comenzar. http://es.tldp.org/Tutoriales/WINCVS/html/, 2002.

[12]
James Gosling, Bill Joy, Guy L. Steele, and Gilad Bracha. The Java Language Specification. http://java.sun.com/docs/books/jls/, 2000.

[13]
Brian W. Kernighan and Rob Pike. La Práctica de la Programación. 2000.

[14]
Vladimir Támara Luis Alejandro Bernal. Curso gratuito y virtual de c. http://uvirtual.ean.edu.co/vtamara/cvirt2001/, 2001.

[15]
Vladimir Támara P. Apuntadores en c. http://structio.sourceforge.net/guias/ayudadesprog/apuntadores-c.sxw, 1997.

[16]
Vladimir Támara Patiño, Igor Tamara, Jaime Irving Dávila, and Pablo Chamorro. Aprendiendo a aprender linux. http://structio.sourceforge.net/guias/AA_Linux_colegio, 2003.

[17]
Wyatt Sutherland. An xp diary. http://www.xprogramming.com/xpmag/xpdiary.htm, 2002.

[18]
Igor Támara, Gustavo Ospina, Jahir Tocancipa, and Vladimir Támara. Manual de uso de repasa. http://structio.sourceforge.net/repasa/docusuario/index.html.

[19]
Don Wells. Extreme programming: A gentle introduction. http://extremeprogramming.org/, 2003.

Index


This document was translated from LATEX by HEVEA.