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
-
LOGRO: Repasa conceptos de lenguajes imperativos
- LOGRO: Realiza programas en C, C++, PHP o Java
1.1 Introducción a tipos, expresiones y asignaciones
Indicadores de logro:
-
INDICADOR: Define estado, conoce el esquema de ejecución de un programa en un lenguaje imperativo
- INDICADOR: Repasa tipos en C, C++, PHP y Java
- INDICADOR: Repasa expresiones
- INDICADOR: Repasa asignación
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
-
Construya tablas de precedencia de operadores para tipos enteros en
los 4 lenguajes (los de C y C++ son los mismos).
-
Construya tablas de precedencia de operadores para tipos booleanos en
los 4 lenguajes.
-
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
-
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:
-
INDICADOR: Maneja condicionales
en C, C++, PHP y Java
- INDICADOR: Emplea y entiende ciclos en
C, C++, PHP y Java
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) {
bloque1
}
else {
bloque2
}
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) :
bloque1
else :
bloque2
endif
En PHP puede usarse elseif para hacer un condicional
con diversos casos, en la sintaxis alterna quedaría:
if (exprb1) :
bloque1
elseif (exprb2) :
bloque2
else :
bloque3
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 const1:
bloque1
case const2:
bloque2
···
case constn:
bloquen
default:
bloquen+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.
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:
-
break que obliga una salida del ciclo.
- continue que obliga a un salto a la guarda del ciclo. En PHP a
continuación de continue puede ponerse un número que indica la
cantidad de ciclos que deben saltarse.
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
-
Que imprimirá cada una de las siguientes porciones de código cuando se
ejecuten:
-
while (1)
printf("B");
printf("C");
while (1) {
printf("B");
printf("C"); }
while (0)
printf("B");
printf("C");
while (0) {
printf("B");
printf("C"); }
1.2.5 Ejercicios de programación
-
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).
-
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).
-
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:
-
INDICADOR: Emplea vectores y apuntadores
- INDICADOR: Emplea y define funciones
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:
-
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,
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.
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
-
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 ?
-
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
-
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.
-
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:
-
INDICADOR: Reconoce y emplea expresiones
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]:
-
Usar espacios antes y después de operandos.
- Usar una forma natural para las expresiones.
- Emplear paréntesis para resolver ambigüedad.
- Dividir expresiones complejas.
- Tener cuidado con los efectos laterales (en expresiones con
operadores de asignación o llamadas a funciones).
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):
-
-e1 Inverso aditivo.
- e1 * e2 Multiplicación.
- e1 / e2 División de e1 entre e2. Si ambos operandos
son enteros da el cociente entero, si alguno es flotante da la división
en flotantes. Si e2 es cero en C y C++ el comportamiento varia
de una plataforma a otra, en Java si es división entera genera la excepción
ArithmeticException, pero si es división flotante puede dar
NaN o infinito (bien positivo o bien negativo).
- e1 % e2 Residuo de la división entre e1 y e2, en
el caso de C y C++ ambos deben ser enteros y el comportamiento si
e2 es 0 depende
de la plataforma. Debe darse igualdad entre e1 /e2*e2 + e1% e2 y
e1. En Java si e1 y e2 son enteros y e2 es cero genera
la excepción ArithmeticException, en Java también opera entre
flotantes.
- e1 + e2 Suma.
- e1 - e2 Resta.
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):
-
~
e1 negación de los bits de e1, por ejemplo
la negación del número binario 110010 es 001101.
- e1
<<
e2 corre bits de e1 a la
izquierda e2 posiciones. Introduce ceros. Corresponde a multiplicar
e1 por 2e2. Por ejemplo 39 <<
2 equivale a correr 100111 2 bits
a la izquierda para obtener 10011100 que en decimal es 156.
- e1
>>
e2 corre bits de e1 a la
derecha e2 posiciones. Si e1 es positivo corresponde al cociente
de dividir e1 entre 2e213. Por
ejemplo (unsigend char)39 >>
2 corresponde a correr 00100111 dos
bits a la derecha es decir 00001001 que en decimal es 9.
- e1 & e2 realiza conjunción (y) entre bits de e1 y
de e2. Por ejemplo la conjunción entre los números binarios
100111 y 110010 es 100010.
- e1
^
e2 realiza disyunción exclusiva (o
exclusivo, xor) entre bits de e1 y de e2. Por ejemplo la
disyunción exclusiva entre los números binarios 100111 y 110010 es 010101.
- e1 | e2 realiza disyunción (o) entre bits de e1 y
de e2. Por ejemplo la disyunción entre los números binarios
100111 y 110010 es 110111.
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:
-
$_POST Arreglo de datos recibidos por método POST del
protocolo HTTP desde un formulario.
- $_GET Arreglo de datos recibidos por método GET del
protocolo HTTP desde un formulario.
- $_GLOBALS Arreglo de variables globales. Sus llaves son
los nombres de variables globales y sus valores los valores de cada
variable.
- $_SERVER Establecida por el servidor web, incluye información
sobre el ambiente de ejecución.
- $_COOKIE Variables establecidas por el programa con
cookies de HTTP.
- $_FILES Información dada por el protocolo HTTP cuando en
un formulario se envian archivos.
- $_ENV Variables de ambiente del usuario que inició el
interprete (si se trata del interprete del servidor web, será el
ambiente del servidor web).
- $_REQUEST Variables recibidas por el protocolo HTTP por
cualquiera de los métodos GET, POST y COOKIE.
- $_SESSION Variables registradas como variables de sesión.
Estas permiten mantener el estado entre la ejecución de un script y la de
otro tras peticiones de cambio de script (o de volver a cargar) por parte
de un usuario de un navegador.
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:
-
Cookies: que mantienen el estado en el computador del cliente.
Se trata de una característica del protocolo HTTP14 que permite al servidor enviar al
cliente en el encabezado de una página una solicitud para almacenar o
devolver un valor asociado a un nombre. Al almacenar un valor debe
especificarse en cuanto tiempo expira. El navegador va llevando el
estado de cookies y lo salva una vez se cierra dejando toda la información
de cookies en un archivo --en el caso de mozilla y firefox es un
archivo con nombre cookies.txt en el directorio .firefox o
mozilla del usuario.
- Variables de sesión: que tipicamente mantiene parte del estado
en el servidor y emplean una identificación única por usuario (almacenada
normalmente en un cookie). El estado es salvado en un archivo temporal
(usualmente en el directorio /tmp.)
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
-
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).
-
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.
-
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
-
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.
-
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:
-
INDICADOR: Emplea apuntadores en C y C++, referencias en C++ y PHP y tipos referencia en Java
1.5.1 Apuntadores y referencias
Un programa puede tener diversos tipos de memoria:
-
Memoria del programa: donde se mantienen las instrucciones del programa
- Memoria de datos: donde se mantienen variables, objetos y datos localizados dinámicamente
- Pila: donde se mantiene temporalmente información de funciones
- Memoria de datos constantes: donde se mantienen constantes definidas
del programa
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++:
-
*apuntador que permite usar y modificar la información
de la dirección de memoria indicada por el apuntador.
- &lvalue que en este caso es la dirección en memoria
del lvalue.
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:
-
Pasar una variable por referencia a una función para que una variable
de la función llamadora sea modificada por la función llamada.
- Localizar memoria dinámica. Por ejemplo localizar vectores
en tiempo de ejecución.
- Iterar sobre elementos de vectores (con los operadores aritméticos
+ y -)
- Calcular ``distancia'' entre elementos de un vector.
- Vectores de apuntadores (por ejemplo el parámetro
argv de la función main).
- Enlazar estructuras de datos como listas, árboles y
grafos.
- Hacer funciones más genéricas empleando apuntadores a funciones
y apuntadores a void.
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
-
Tras revisar [3], enumere y describa los especificadores de
formato de la función printf (Ayuda: son los mismos de la
función fprintf).
-
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).
-
¿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
-
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:
-
INDICADOR: Declara variables y constantes
- INDICADOR: Emplea TADs y clases
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";
?>
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
-
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.
-
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
-
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).
-
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;
}
}
-
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:
-
INDICADOR: Emplea cadenas
- INDICADOR: Emplea estructuras
- INDICADOR: Emplea memoria dinámica
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:
-
double strtof(const char *nptr, char **endptr) que convierte
la cadena apuntada por nptr a flotante. Salta espacios que pueda
haber al comienzo y retorna en endptr (si este no es NULL) el
resto de cadena que
sigue al flotante reconocido16. Esta función reconoce
signo al comienzo, puede reconocer números en hexadecimal si comienzan con
0x y las cadenas especiales NaN, INF e INFINITY (ver 1.4.3).
Otras funciones análogas pero que retornar double y long double
son strtod y strtold respectivamente.
- long strtol(const char *nptr, char **endptr, int base) que
convierte la cadena apuntada por nptr a entero largo, salta espacios
al comienzo, supone que el número está escrito en la base indicada
(entre 2 y 36) y retorno el resto de la cadena que sigue al entero en
endptr (a menos que sea NULL.) Son funciones análogas pero
para otros tipos: strtoll, strtoul y strtoull.
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.
-
void *calloc(size_t nmemb, size_t size) localiza un vector
de tamaño size, donde cada elemento es de nmemb bytes, e inicializa
todos los bytes en 0.
- void *malloc(size_t size) localiza un área de memoria de
size bytes, cada byte con un valor inicial arbitrario.
- void *realloc(void *ptr, size_t size) reasigna área de memoria
dinámica apuntad por ptr para que su nuevo tamaño sea size bytes.
ptr debe ser NULL (y realloc se comporta como malloc) o
un apuntador retornado por alguna de estas 3 funciones para localizar memoria.
Si el nuevo tamaño es mayor que el anterior, copia la información al comienzo
de la nueva área. Si el nuevo tamaño es menor que el anterior, copia sólo
los primeros size bytes.
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
-
Consulte funciones para operar con cadenas en C (Ayuda: string.h).
-
Consulte operadores y métodos para operar con cadenas usando el tipo
string en C++.
-
Consulte métodos para operar con cadenas en Java.
-
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
-
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.
-
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).
-
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:
-
INDICADOR: Emplea bibliotecas estándares
- INDICADOR: Maneja fuentes organizadas en diversos archivos
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:
-
java.applet para hacer applets (binarios de Java ejeuctables
desde un navegador).
- java.awt que permite hacer aplicaciones gráficas.
- java.beans que facilita creación de componentes.
- java.io que maneja entrad y salida.
- java.lang con clases básicas (e.g String).
- java.math permite hacer aritmética entera de precisión arbitraria.
- java.net que permite operar con sockets y en redes.
- java.rmi para hacer llamadas a métodos remotos.
- java.security con funciones relacionadas con seguridad.
- java.sql API para acceso a bases de datos SQL.
- java.text Claes e interfaces para manejar textos, fechas,
números y mensajes independiente del lenguaje.
- java.util Compresores, descompresores, arreglos que pueden
cambiar de tamaño, pila, lista, calendario.
- org.omg.CORBA implementación de Corba para Java (Corba permite
intercomunicar en red programas, independientemente del lenguaje de
programación de cada programa).
- javax.accessibility
- javax.naming permite acceder servicios de nombres.
- javax.swing Componentes gráficos livianos que funcionan igual
en todas las plataformas.
- javax.sound.midi secuenciador y sintetizador MIDI.
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
-
dar el nombre de la clase precedido del paquete, por ejemplo
java.lang.System, o más en concreto:
java.lang.System.println("gracias");
- 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.+;
- 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:
-
<assert.h> Para chequear aserciones con assert cuando
no está definido el símbolo NDEBUG.
- <complex.h> Aritmética de números complejos. Puede usarse
los tipos double complex, float complex y long double complex.
y funciones como creal, conj, cimag, carg,
csqrt, cpow, cabs, clog, cexp, ctanh,
csinh, ccosh, catanh, casinh, cacosh,
ctan, csin, ccos, catan, casin,
cacos .
Cada una de estas funciones recibe y/o retorna complex double o
double,
también hay funciones análogas que operan con float y
long double, con los mismos nombres de estas pero con los
sufijos f y l respectivamente).
- <ctype.h> Con funciones para manejo de caracteres que dependen
de la localización21. Por ejemplo para decidir
si un caracter pertenece a un grupo: isalnum, isalpha,
iscntrl, isdigit, isgraph, islower,
isupper, isprint, ispunct, isspace,
isxdigit; y para hacer conversiones
tolower, toupper.
- <errno.h> Macros relacionados con reporte de errores, así
como la variable errno que es usada por diversas funciones de la
librería estándar para reportar errores. Si después de llamar una función
su valor es 0 no hubo error, otros códigos representa errores. En el
caso de OpenBSD algunos códigos y símbolos definidos son:
1 EPERM operación no permitida, 2 ENOENT no existe archivo o
directorio, 3 ESRCH No existe el proceso, 4 EINTR Llamada a
función interrumpida.
- <float.h> características de la implementación de números
flotantes.
- <iso646.h> macros que expanden a secuencias de caracteres
reservadas en C (utiles por ejemplo si no logran generarse ciertos
caracteres con el teclado):
Símbolo |
Secuencia de caracteres |
and |
&& |
and_eq |
&&= |
bitand |
& |
bitor |
| |
compl |
| ~ |
not |
| ! |
not_eq |
| != |
or |
| || |
or_eq |
| |= |
xor |
| ^ |
xor_eq |
| ^ |
- <limits.h> que define límites y parámetros de los tipos
enteros.
- <locale.h> para manejar localización. La localización
la conforman detalles de un idioma o una cultura como: símbolo del punto
decimal, separador de miles, símbolo monetario, formato de fechas. Se
establece con la función setlocale cuyo primer parámetro puede
ser el valor de uno de los símbolos: LC_ALL, LC_COLLATE,
LC_CTYPE, LC_MONETARY, LC_NUMERIC, LC_TIME,
y el segundo puede ser la cadena "C" para indicar una localización
por defecto, "" para indicar la localización del sistema o una
cadena como "es_CO" para indicar localización de Colombia.
- <math.h> que declara tipos y funciones matemáticas.
isfinite, isinf, isnan, isnormal,
signbit, acos, asin, atan, atan2,
cos, sin, tan, acosh, asinh,
atanh, cosh, sinh, tanh, exp, exp2, expm1, frexp, ilogb, ldexp,
log, log10, log1p, log2, logb,
modf, scalbn, cbrt, fabs, hypot,
pow, sqrt, erf, erfc, lgamma,
tgamma, ceil, floor, nearbyint,
rint, round, lround, trunc, fmod,
remainder, remquo, copysign, nan,
nextafter, fdim, fmax, fmin, fma.
- <setjmp.h> Saltos no locales, con setjmp puede
``marcarse'' un sitio del programa al cual puede después saltarse
con longjmp (incluso desde otra función, pero cuidando que
la pila podría ser modificada).
- <signal.h> para manejar señales (en sistemas tipo Unix).
- <stdarg.h> para manejar argumentos en funciones cuyo
número de parámetros es variable (e.g printf).
- <stddef.h> tipos y constantes básicas (e.g NULL.
- <stdio.h> Tipos , macros, variables y funciones de
entrada/salida. Entre las variables están stderr, stdin
y stdout que son archivos que representan error estándar, entrada estándar
y salida estándar. Entre las funciones definidas hay algunas
para manejar archivos a alto nivel
remove, rename, tmpfile, tmpnam, otras
para acceder a archivos fclose, fflush, fopen,
freopen,
setbuf, setvbuf; operaciones para manejar archivos tipo texto
fprintf,
fscanf, printf, scanf, snprintf, sprintf,
sscanf, vfprintf, vfscanf, vprintf,
vscanf, vsnprintf, vsprintf, vsscanf,
fgetc, fgets, fputc, fputs, getc,
getchar, gets, putc, putchar, puts,
ungetc; para manejar archivos binarios
fread, fwrite, fgetpos, fseek, fsetpos,
ftell, rewind; para manejar errores clearerr,
feof, ferror, perror.
- <stdlib.h> utilidades generales como las funciones
de conversión (ver 1.7.1); generación de números seudo-aleatorios
rand, srand; manejo de memoria calloc, free,
malloc, realloc; comunicación con ambiente abort,
atexit, exit, getenv, system; funciones para
buscar y ordenar bsearch, qsort; funciones de aritmética
entera abs, labs, llabs, div, ldiv,
lldiv; manejo de caracteres multibyte mblen, mbtowc,
wctomb, mbstowcs, wcstombs.
- <string.h> para manejo de cadenas y zonas de memoria con
operaciones para copiar y mover memcpy, memmove, strcpy,
strncpy; concatenar strcat, strncat; comparar memcmp,
strcmp, strcoll, strncmp, strxfrm; búsqueda
memchr, strchr, strcspn, strpbrk, strrchr,
strspn, strstr, strtok, memset, strerror,
strlen.
- <tgmath.h> que define las mismas funciones de math.h
y complex.h pero haciéndolas genéricas para los tipos float,
double, long double y sus análogos complex.
- <time.h> con estructuras, tipos, macros y funciones
para manipular tiempo clock, difftime, mktime,
mkxtime, time; para convertir asctime, ctime,
gmtime, localtime, strftime, strfxtime,
zonetime.
- <wchar.h> que facilita el uso de cadenas de caracteres amplios
y multibyte fwprintf, fwscanf, swprintf, swscanf,
vfwprintf, vfwscanf, vswprintf, vswscanf,
vwprintf, vwscanf, wprintf, wscanf; así como
manejo de archivos fgetwc, fgetws, fputwc, fputws,
fwide, getwc, getwchar, putwc, putwchar,
ungetwc; funciones de conversión wcstod, wcstof,
wcstold, funciones sobre cadenas wcscpy, wcsncpy,
wcscat, wcsncat, wcscmp, wcscoll, wcsncmp,
wcsxfrm, wcschr, wcscspm, wcslen, wcspbrk,
wcsrchr, wcsspn, wcsstr, scstok, wmemchr,
wmemcmp, wmemcpy, wmemmove, wmemset; para
conversión de tiempo wcsftime, wcsfxtime; para interoperar
con multibytes (teniendo en cuenta el estado de cadenas multybyte)
btowc, wctob, mbsinit, mbrlen,
mbrtowc, wcrtomb, mbsrtowcs, wcsrtombs.
- <wctype.h> para clasificar y convertir caracteres amplios
iswalnum, iswalpha, iswcntrl, iswdigit, iswgraph,
iswlower, iswprint, iswpunct, iswspace, iswupper,
iswxdigit, iswctype, wctype, tpwlower,
towupper, towctrans, wctrans.
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:
-
std::printf que hace explicito que se desea llamar la función
printf de la biblioteca estándar de C++.
- agregar al comienzo del programa using namespace std, que hace
que en el espacio de nombres globales se incluyan los nombres del
espacio std (ver 1.7.1).
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:
-
Soporte al lenguaje
-
Tipos <cstddef>
- Propiedades de la implementación <limits> , <climits> ,
<cfloat>
- Inicio y terminación <cstdlib>
- Manejo de memoria dinámica <new>
- Identificación de tipos <typeinfo>
- Manejo de excepciones <exception>
- Otro soporte en tiempo de ejecución <cstdarg> ,
<csetjmp> , <ctime> , <csignal> , <cstdlib> .
- Diagnósticos
-
Clases de excepciones <stdexcept>
- Aserciones <cassert>
- Códigos de error <cerrno>
- Utilidades generales
-
Utilidades para componentes <utility> (comparaciones, parejas)
- Funciones <functional> que define algunos operadores
como funciones para facilitar el uso de iteradores.
- Memoria <memory> , para definir localizadores de memoria.
- Funciones de memoria de <cstdlib>
- Fechas y tiempo <ctime>
- Cadenas
-
Plantilla para cadenas de cualquier tipo <string> ,
las instancia en particular para char (ver 1.7.1) y
para wchar_t.
- Secuencias terminadas en el caracter nulo ('\0' ) <cctype>
<cwctype> , <cstring> , <cwchar> , <cstdlib> .
- Internacionalización
- Contenedores
-
Secuencias <deque> , <list> , <queue> ,
<stack> , <vector> .
- Cotenedores asociativos <map> , <set> , <bitset> .
- Iteradores
-
<iterator> que permite usar o definir iteradores para diversas
clases usando por ejemplo operadores (los hay de acceso aleatoria,
bidireccionales y hacia adelante).
- Algoritmos
-
<algorithm> que implementa operaciones que no modifican
secuencias (e.g find, for_each, find_if,
find_end, count, equal, search)
, algunas que modifican (copy, transform, swap,
replace, fill, generate, remove, unique,
reverse, rotate, random_shuffle, partition) y
operaciones relacionadas con ordenamiento (sort, stable_sort,
partial_sort, nth_element, binary_search, merge),
conjuntos (includes, set_union, set_intersection,
set_difference), montículos (push_heap, pop_heap,
make_heap, sort_heap), mínimos y máximos (min,
max, min_element, max_element,
lexicographical_compare) y permutaciones (next_permutation,
prev_permutation).
- <cstdlib>
- Biblioteca numérica
-
<complex> números complejos
- <numeric> operaciones numéricas generalizadas
- <cmath> , <cstdlib> funciones mátematicas heredadas de C
- Entrada/Salida
-
<iostream> Objetos de entrada/salida estándar
- <ios> Clases básicas de e/s
- <streambuf> buffers para flujos
- <istream> , <ostream> , <iomanip> manipuladores
y formateadores.
- <sstream> , <cstdlib> flujos a cadenas
- <fstream> , <cstdio> , cwchar flujos a archivos
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:
-
Cada archivo fuente es pasado por el preprocesador, el preprocesador
remplaza las macros24 por su valor;
incluye los archivos indicados con
#include
25, deja sólo
las porciones apropiadas de los #if
que pueda haber, de requerirlo
imprime mensajes de error especificados con #error
y hace cambios
a la forma de compilación de acuerdo a los #pragma
que haya.
- Cada archivo fuente después de preprocesado es compilado, es decir
es traducido a código objeto.
- Todos los códigos objetos que conforman el programa son encadenados u
junto con las librerías apropiadas, para generar un ejecutable. Debe haber
un sólo código objeto (así como fuente) en el que se defina la función
main.
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:
-
-o ejmd5 que indica el archivo de salida.
- -I prog/skalibs/include que indica una ruta donde buscar archivos
encabezados (por defecto busca en /usr/include).
- -L prog/skalibs/library que indica una ruta donde buscar
librerías
- -lstdcrypto y -lstddjb que indican encadenar con las
bibliotecas libstdcrypto.a (ubicada en prog/skalibs/library).
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:
-
En el caso de Java puede encontrar la documentación
de las bibliotecas para cada versión de Java en
http://java.sun.com/reference/api/index.html.
- En el caso de C++ puede consultar el borrador de la especificación
[borrcp] o en Internet alguna de las guías disponibles como
En el caso de C pueden consultarse las mismas referencias de C++ (pues
la librería estándar de C está incluida en la de C++) o el borrador
del estándar [3] o en Internet
http://www-ccs.ucsd.edu/c/ o más fácil en un sistema tipo Unix
con man (e.g man stdio). Pueden verse los códigos de error
con man errno.
1.8.4 Ejercicios para afianzar teoría
-
Traduzca la documentación de la clase Double de java.lang
1.8.5 Ejercicios de programación
-
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).
-
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:
-
INDICADOR: Define y usa clases que heredan de otras clases
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:
-
En Java al pasar un objeto como argumento de una función o al
retornarlo el objeto pasa por referencia.
- En Java cada clase puede heredar a lo sumo de una clase, aunque
puede implementar una o más interfaces. Una interfaz corresponde a una
clase abstracta en la que todos los métodos son virtuales puros.
- En Java cada miembro público debe precederse en su declaración con
la palabra reservada public. Las clases e interfaces pueden ser
públicas o no (igualmente precediéndolas con public), lo que indica
que pueden usarse desde otros archivos (todas los paquetes son públicos).
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
-
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.
-
Investigue el uso de interfaces en Java y escriba un programa
corto que las use.
1.10 Lo propio de C++
Indicadores de logro:
-
INDICADOR: Define y usa constructores y destructores
- INDICADOR: Emplea sobrecarga de funciones y de operadores
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;
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.
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).
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;
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
-
Secciones ``Special member functions'' y ``Overloading'' del borrador
del estándar de C++ ??.
- Secciones `Constructors,' `Destructors,' `Assignment operators,'
`Operator overloading,' `Friends,' `Input/output via <iostream> and <cstdio>,'
`Exceptions and error handling' y
`Const correctness' de ??.
- Sección sobre Entrada y Salida de ??.
1.10.8 Ejercicios de programación
-
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)
-
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);
-
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
-
LOGRO: Conoce áreas y etapas típicas de la ingeniería de software
- LOGRO: Conoce forma de desarrollo sugerida por Programación Extrema
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:
-
INDICADOR: Define ingeniería de software y algunas de sus áreas
- INDICADOR: Define requerimientos de software y áreas de conocimiento en ese tema
- INDICADOR: Define programación extrema y puede recolectar historias de usuario y tareas de programación
De acuerdo a [6], ingeniería de software es
-
"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.
- El estudio de aproximaciones como las descritas en (1),"
Las áreas de conocimiento de la ingeniería de software son:
-
Requerimientos
- Diseño
- Construcción
- Pruebas
- Mantenimiento
- Administración de configuraciones
- Administración de ingeniería del software
- Proceso de ingeniería del software
- Herramientas y métodos de ingeniería
- Calidad de software
- Disciplinas relacionadas
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:
-
Requerimientos de productos y de procesos: los primeros se refieren a
requerimientos del software por desarrollar, los segundos a restricciones
en el desarrollo (e.g lenguaje de programación).
- Requerimientos funcionales y no funcionales: Los primeros describen una
función que el programa debe ejecutar, también se llaman capacidades. Los
segundos son restricciones o requerimientos de calidad e.g. desempeño,
mantenibilidad, seguridad, robustez.
- Propiedades emergentes: se refiere a requerimientos que no pueden
ser atribuidos a un sólo componente de un programa, sino que depende de la
interrelación de todos los componentes y por tanto dependen de la arquitectura
del sistema (e.g. desempeño de un sistema completo).
- Requerimientos del sistema y del software: los primeros se aplican a
un sistema completo del cual el software hace una parte (por ejemplo puede
haber hardware, firmware, información, personas, técnicas, servicios).
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:
-
Documento de definición del sistema: define los requerimientos del
sistema de alto-nivel desde la perspectiva del dominio. Sus lectores
incluyen representantes de los usuarios/clientes del sistema. Lista
requerimientos con información del marco, ambiente de operación, restricciones,
suposiciones y requerimientos no funcionales.
- Especificación de requerimientos del sistema: en el caso de sistemas
complejos con porciones importantes que no son software, se especifican los
requerimientos del sistema completo (empleando técnicas de ingeniería) para
derivar de allí los requerimientos de software.
- Especificación de requerimientos del software: establece los acuerdo
entre cliente y contratante sobre los que hace y lo que no hace el producto de
software
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:
-
Comunicación: los programadores se comunican directamente con
los clientes, negocian alcances y cronograma.
- Simplicidad: Mantienen diseños simples y limpios,
- Retroalimentación: obtenida de pruebas que se realizan desde el
primer día (el programador debe asegurar que los requerimientos y
el diseño son susceptibles de ser puestos a prueba y debe crear pruebas
automáticas de unidades y funcionales).
También de usuarios a quienes se les entrega el sistema tan
pronto como es posible, para implementar los cambios que sugieran.
- Coraje: Con las características presentas los programadores
pueden responder con valor a requerimientos y tecnología cambiante.
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
-
Historias de usuarios: escritas por los clientes durante la
planeación
- Tareas de programación: identificadas y asignadas por los programadores
con base en las historias de usuario por implementar.
- Pruebas de aceptación: que son ejecutadas y refinadas con el
cliente/usuario.
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
-
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:
-
En que condiciones y para que tipos de programas le parece aplicable
- 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.
-
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.
-
Busque una notación formal para especificación de requerimientos y describa
en que contexto es aplicable.
-
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:
-
INDICADOR: Identifica metodologías para probar software y la terminología del área
- INDICADOR: Conoce ambientes para realizar pruebas de unidades útiles en el marco de programación extrema.
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:
-
Niveles de pruebas
- Técnicas para probar
- Medidas relacionadas con pruebas
- Proceso de pruebas
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:
-
A partir de medidas basadas en el tamaño del programa (e.g número de
líneas del código fuente) o en su estructura (e.g complejidad) pueden
guiarse las pruebas.
- Para hacer pruebas más efectivas puede clasificarse el tipo de
defectos
y compararse con información histórica para hacer predicciones de calidad
de posibles defectos. Para cada clase de falla la densidad se mide como
el radio entre el número de fallas encontradas y el tamaño del programa.
- Para determinar cuando detener las pruebas puede estimarse
estadísticamente la robustez del software. Existen modelos de crecimiento
de robustez basados en cuenta de fallas y en tiempo entre fallas.
Por otra parte pueden hacerse mediciones a las pruebas realizadas:
-
Puede medirse el radio entre los elementos cubiertos con pruebas y
el total de elementos (e.g porcentaje de caminos cubiertos del grafo de
flujo).
- Es posible insertar fallas artificiales (semillas de fallas) buscando
que estas ayuden a descubrir fallas reales. Con este procedimiento se puede
evaluar la efectividad de las pruebas.
- En las pruebas de mutación el radio entre mutantes eliminados sobre
total de mutantes generados puede medir la efectividad de las pruebas.
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:
-
Planeación: Incluye coordinación de personal, administración de
facilidades y equipos de prueba, planeación de resultados indeseables.
- Generación de casos de prueba: depende del nivel de pruebas, y las
técnicas de pruebas. Los casos de prueba deberían estar bajo el
control de un software para administración de configuraciones e incluir
el resultado esperado de cada prueba.
- Desarrollo del ambiente de pruebas: El ambiente debe facilitar
desarrollo y control de casos de prueba, registro y recuperación
de resultados esperados, scripts y otros materiales de prueba.
- Ejecución: Todo lo realizado durante las pruebas debe
ser documentado. Las pruebas deben hacerse de acuerdo a procedimientos
documentados usando una versión claramente definida del programa a
probar.
- Evaluación de los resultados de pruebas: Podría ser realizada
por un equipo evaluador formal.
- Reporte de problemas/registro de pruebas: Las actividades pueden
registrarse en una bitácora que identifique cuando se realizó la
prueba, quien la realizó, cual fue la base y otros datos. Los resultados
no esperados pueden ser registrados en un sistema para reportar problemas.
- Seguimiento de defectos: Los defectos pueden analizarse para determinar
cuando fueron introducidos, que error los causo y cuando pudieron ser
observados por primera vez. El seguimiento de esta información
se usa para determinar los aspecto de la ingeniería de software que
necesitan mejorar y para medir la efectividad de análisis y pruebas
previas.
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
-
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.
-
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
-
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:
-
INDICADOR: Conoce términos y definiciones relacionados con administración de configuraciones
- INDICADOR: Conoce y emplea CVS para controlar versiones.
``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:
-
Herramientas para seguir defectos, mejoras y problemas. Algunos
ejemplos de este tipo de herramientas son: Bugzilla
http://www.bugzilla.org/ y Scarab
http://scarab.tigris.org/.
- Herramientas para el control de versiones. RCS es una herramienta
para control de versiones de archivos (típicamente incluida en
sistemas tipo Unix); CVS permite controlar versiones de jerarquías de
directorios y sus archivos de forma distribuida como se explica más
adelante; subversion busca dar la misma funcionalidad de CVS con
algunas ventajas8
- Herramientas para la construcción y liberación de entregas.
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] :
-
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:
-
Si el repositorio está en la misma máquina en la que usted está
trabajando por ejemplo en el directorio /var/cvs:
export CVS_RSH=""
export CVSROOT=/var/cvs
- Si el repositorio está en un directorio en otra máquina, y la otra
máquina tiene un servidor ssh o rsh y usted tiene cuenta
en esa máquina (o su clave pública ssh está autorizada en alguna
cuenta de esa máquina). Suponiendo que la otra máquina es
purpura.micolegio.edu.co, que cuenta con ssh , que el repositorio
está en el directorio /home/pepe/cvs, y que en esa máquina usted
puede usar con ssh la cuenta pepe:
export CVS_RSH=ssh
export CVSROOT=pepe@purpura.micolegio.edu.co:/home/pepe/cvs
- Si el repositorio está en un directorio de otra máquina, el
protocolo pserver de CVS funciona en esa máquina y usted tiene una
cuenta en esa máquina o está autorizado para usar pserver en el
repositorio:
export CVS_RSH=""
export CVSROOT=:pserver:pepe@purpura.micolegio.edu.co:/home/pepe/cvs
cvs login
- 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
- 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).
- 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]
-
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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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.
- Semanas de 40 horas: Los programadores cansados cometen más errores.
Los equipos PE no trabajan tiempo excesivo, manteniéndose frescos,
saludables y efectivos.
- 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.
- 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].
-
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.
-
Usando CVS saque la versión estable de Bugzilla del repositorio CVS.
Vea la sección `Initial Checkout' en
http://www.bugzilla.org/download/
-
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:
-
INDICADOR: Conoce términos y definiciones relacionados con calidad de software
- INDICADOR: Conoce y emplea técnicas para asegurar calidad del código (assert, chequeadores estáticos de código).
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.
-
Proceso de aseguramiento de calidad: Aseguran que el problema esta
planteado de forma clara y adecuada y que los requerimientos están
definidos y expresados apropiadamente. El plan de aseguramiento de calidad define
los medios usados para asegurar que el software desarrollado satisface los
requerimientos del usuario y que es de la más alta calidad posible
dentro de las restricciones del proyecto. Este plan identifica
documentos, estándares, prácticas y convenciones que gobiernan el proyecto
y la forma como son chequeados y monitoreados para asegurar que
son apropiados y que se cumplen.
- Proceso de verificación y validación: La verificación intenta
asegurar que el producto se construye correctamente, en el sentido que
los productos de salida de una actividad cumplan las especificaciones
impuestas en actividades previas. La validación intenta asegurar que
se construye el producto correcto. Un plan de V&V describe los recursos y
sus roles y actividades, así como las técnicas y herramientas por usar.
- Procesos de revisión y auditorías
-
Revisiones de administración:
Monitorear progreso, determinar estado de planes y cronogramas,
confirmar requerimientos y su localización en el sistema, o evaluar
la efectividad de las aproximaciones administrativas. Apoyan la toma
de decisiones sobre cambios y acciones correctivas. Determinan
lo adecuados que son los planes, cronogramas y requerimientos. Pueden
hacerse sobre productos como reportes de auditorías, de progreso, reportes
de V&V y sobre diversos tipos de planes incluyendo manejo de riesgo,
administración del proyecto, administración de configuraciones de software.
- Revisiones técnicas: Evalúa un producto de software para
determinar lo apropiado que resulta para el uso que se le espera dar.
Busca identificar discrepancias entre especificaciones aprobadas y
estándares. Deben establecerse roles específicos: quien toma decisión,
líder de revisiones, quien lleve registros, apoyo técnico para apoyar las
actividades de revisión. Las siguientes entradas deben ser establecidas:
establecimiento de objetivos, un producto de software específico,
los planes de administración específicos, la lista de reportes asociados
con el producto, el procedimiento de revisión técnica.
- Inspecciones: su propósito es detectar e identificar anomalías
en un producto de software. A diferencia de las revisiones son de unas
pocas horas y centradas en porciones específicas. Incluyen al autor de un
producto final o intermedio, un líder de inspección, alguien que lleve el
registro, alguien
que lea, y unos pocos inspectores (2 a 5). Cada miembro del grupo
examina el producto de software y otros insumos para aplicar alguna
técnica analítica a una pequeña porción del producto, o al producto
entero enfocándose sólo en un aspecto. Cualquier anomalía encontrada
es documentada y enviada al líder de inspecciones. La lista de chequeo
resultante de la inspección clasifica las anomalías y es revisada por
el equipo. Estas inspecciones arrojan como resultado uno de estos criterios:
aceptado sin o con mínimo trabajo adicional; aceptado con verificación de
trabajo adicional; más inspección es requerida.
- Recorridos: Evalúan un producto de software, pueden hacerse para educar
una audiencia en un producto de software. Buscan encontrar anomalías,
mejorar producto de software, considerar implementaciones alternativas,
evaluar cumplimiento de estándares y especificaciones. Es principales
organizada por el ingeniero de software para dar a sus compañeros de equipo
la oportunidad de revisar su trabajo, como técnica de aseguramiento.
- Auditorías: Dan una evaluación independiente del cumplimiento de
productos de software y procesos a las regulaciones, estándares, lineamientos,
planes y procedimientos aplicables. La auditoría identifica instancias que
no cumplen requerimientos y produce un informe el cual requiere del equipo
acciones correctivas. Pueden hacerse a casi cualquier producto en cualquier
etapa de desarrollo o mantenimiento.
2.4.2 Consideraciones prácticas
Deben tenerse en cuenta
-
Para requerimientos de calidad de software: factores que influyen
(e.g dominio del sistema, requerimientos de software y sistema,
componentes comerciales o estándares por usar, etc.); robustez, niveles
de integridad del software
- Caracterización de defectos
- Técnicas para manejar calidad de software
-
Técnicas estáticas: examinan documentación del proyecto y software
sin ejecutarlo. Pueden hacerse con o sin asistencia de herramientas
que automaticen. Lectura de código.
- Técnicas intensivas en personas: Incluyen revisiones y auditorías y
otras técnicas en grupos de dos o más personas. Requieren preparación
antes de reuniones, y puede hacer uso de listas de chequeo y
resultados de técnicas analíticas o de pruebas.
- Técnicas analíticas: incluyen análisis de complejidad, análisis
de control de flujo y análisis de algoritmos. Métodos formales para
verificar requerimientos de software y de diseños. Pruebas de corrección.
- Técnicas dinámicas: Usualmente son técnicas para probar (aunque
puede incluirse simulación, model checking y ejecución simbólica).
- Pruebas: una organización independiente de V&V podría monitorear
el proceso de pruebas. También podría realizar ``pruebas por terceros.''
- Medidas de la calidad de software. Entre las posibles están:
-
Basadas en estadísticas: permiten mostrar áreas problemáticas. Las
gráficas resultantes y las ayudas visuales ayudan a quienes toman decisiones.
- Predicción: ayuda a planear tiempo de pruebas y a predecir fallas.
- Análisis de tendencias: indican cuando un cronograma no se ha
respetado, o que cierto tipo de fallas serán
más intensas a menos que se tomen acciones correctivas.
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:
-
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).
- 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.
- De el crédito al autor o autores.
- 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:
-
Copiar textualmente porciones minoritarias no consecutivas[4] a
manera de citas, eventualmente mejorando ortografía y anotando autores
y procedencia (no se es autor de lo que se copia o se corrige
ortograficamente).
- Emplear el material como referencia junto con otras fuentes para
crear un documento propio del cual usted será el/la autor(a) y que
tendrá como bibliografía las fuentes en las que se base.
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:
-
Esfuércese por dar crédito a las fuentes en las que se base.
No copie textualmente ni haga simplemente cambios a otra fuente sin
dar crédito o incurriría en plagio. El nuevo material debe ser
resultado del estudio de diversas fuentes (cuyo crédito debe darse),
de la organización y síntesis sistemática y de su consecuente inspiración.
- Si hace la obra bajo un contrato de trabajo, la organización
contratante será propietaria de los derechos de reproducción (a menos que
indique explícitamente lo contrario en un documento público). Será por
tanto la organización la que pueda asignar o cambiar los derechos de
reproducción de la obra. Eventualmente puede sugerir a su
organización emplear términos de reproducción que promuevan dar
desinteresadamente (como ceder al dominio público). Revise que no haya
inconsistencias entre los derechos de reproducción, caso que podría
darse si algunas porciones tienen derechos de reproducción
particulares, por ejemplo como consecuencia de copiarlas de fuentes
que permiten libre copia o modificación.
- Invitamos a dar desinteresadamente. A los propietarios de
derechos de reproducción de obras los invitamos a emplear términos de
reproducción que promuevan este valor (por ejemplo dominio público
licencias BSD y MIT). Así mismo lo invitamos a difundir sus obras para
que puedan llegar a quienes las necesitan por ejemplo publicándolas en
Internet, y anunciándolas en diversos medios. También invitamos a realizar
campañas y educar en la promoción de la ética, de la solidaridad y del
respeto por los derechos morales y patrimoniales de las obras intelectuales.
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
-
Consulte la documentación de splint y use este programa para chequear
uno de sus programas en C.
-
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/
-
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:
-
INDICADOR: Conoce términos y definiciones relacionados con mantenimiento de software
- INDICADOR: Conoce y emplea técnicas para facilitar mantenimiento de software (assert, chequeadores estáticos de código).
"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:
-
Corregir fallas
- Mejorar el diseño
- Implementar mejoras
- Hacer interfaces con otros sistemas
- Adaptar el programa para pode usar diferentes facilidades de
hardware, software, sistema y telecomunicaciones.
- Migrar software legado
- Retirar software
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:
-
Mantenimiento correctivo: modificaciones por reacción a
productos de software, realizadas después de la entrega para corregir
problemas corregidos.
- Mantenimiento adaptativo: modificación después de la entregado
para mantenerlo utilizable en un ambiente cambiante.
- Mantenimiento perfectivo: modificación después de la entrega
para mejorar desempeño o mantenibilidad.
- Mantenimiento preventivo: modificación después de la entrega para
detectar o corregir fallas latentes antes de que se conviertan en
fallas efectivas.
Aspectos claves en el mantenimiento del software
Entre los aspectos técnicos pueden resaltarse:
-
Entendimiento limitado: Se refiere a la dificultad de hacer
cambios a software no desarrollados por quien hace el cambio. Se
ha estimado que entre el 40% y 60% del esfuerzo de mantener se
dedica a entender el software por modificar.
- Pruebas: El costo de repetir pruebas en una pieza de software
grande puede ser significativo en tiempo y dinero. Son muy importantes
las pruebas de regresión en la etapa de mantenimiento para asegurar
que las modificaciones no han causado efectos no intencionales.
- Análisis de impacto:
Quien mantiene un programa debe tener un conocimiento de la estructura y
contenido del software para realizar un análisis de impactos que
identifique los sistemas y productos de software afectados por una
solicitud de cambio y estimar los recursos necesitados para
efectuar el cambio. La solicitud de cambio (llamada requerimiento de
modificación o reporte de problema), debe ser analizada y traducida
a términos de software. Los objetivos del anàlisis son:
-
Determinar el alcance de un cambio para planear e implementar el trabajo.
- Desarrollar estimativos precisos sobre los recursos requeridos para
realizar el trabajo.
- Analizar el costo/beneficio del cambio requerido.
- Comunicar a otros la complejidad de un cambio.
Deben darse diversas soluciones potenciales y hacerse una
recomendación sobre el mejor curso de acción.
El software desarrollado con mantenibilidad en mente facilita el análisis
de impacto.
- Mantenibilidad: es definida como un característica de calidad. Una
de las causas principales de su ausencia es mala documentación que
conlleva a la dificultad de comprensión.
Entre los aspectos administrativos:
-
Alineamiento con objetivos organizacionales: entre los objetivos
organizacionales debe estar, demostrar el retorno de inversión del
mantenimiento de software.
- Equipo: se refiere a como atraer y mantener al equipo que hace
mantenimiento.
- Proceso: las actividades requeridas para hacer mantenimiento son
un reto administrativo.
- Aspectos organizacionales: organización o función responsable
del mantenimiento (no necesariamente son quienes lo desarrollan).
- Contratación externa (outsourcing): es una opción elegida para
software de misión no crítica (pues las compañías no desean perder el
control del software que usan como núcleo de su negocio).
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:
-
Requerimiento de modificación
- Clasificación e identificación
- Análisis
- Diseño
- Implementación
- Pruebas del sistema
- Pruebas de aceptación
- Entrega
- Vuelve a 2.
Las actividades para implementar el
proceso de mantenimiento de acuerdo al estándar ISO/IEC 14764 se basan
en iteraciones de:
-
Análisis del problema y de modificaciones
- Implementación de modificaciones
- 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:
-
Transición
- Modificación
- Escritorio de ayuda para Requerimiento de modificación y
reporte de problemas.
- Análisis de impacto
- Soporte de software (ayuda y recomendación a usuarios).
- Acuerdos a Nivel de Servicios13 y contratos de
mantenimiento específicos.
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:
-
Plan del negocio (organizacional)
- Plan de mantenimiento (transición): como el mantenimiento
puede durar años, primero debe hacerse un documento de conceptos con
-
El alcance del mantenimiento
- La adaptación del proceso de mantenimiento
- La identificación de la organización que hace el mantenimiento
- Un estimado de los costos de mantenimiento
El plan de mantenimiento debe especificar como
los usuarios realizarán requerimientos de modificación o
como reportaran problemas.
- Plan de versiones/entregas (programa): requiere de quien hace
el mantenimiento:
-
Coleccionar fechas de disponibilidad de requerimientos individuales
- Acordar con usuario el contenido de las siguientes entregas/versiones
- Identificar conflictos potenciales y desarrollar alternativas
- Medir el riesgo de una entrega y desarrollar un plan de emergencia
en caso de que se presenten problemas
- Informar a los inversionistas
- Plan de requerimientos de cambio individuales (nivel de requerimientos):
s realiza durante el análisis de impactos.
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
-
Los navegadores de código ayudan a comprender el código, así como
una documentación clara y concisa.
- La reingeniería se define como el examen y alteración de
software para reconstituirlo de una manera nueva. Usualmente
no se lleva a cabo para mejorar la mantenibilidad sino para
remplazar software legado.
- Ingeniería reversa es el proceso de analizar software para
identificar sus componentes e interrelaciones y para
crear representaciones del software de otras maneras con niveles
más altos de abstracción. Es una técnica pasiva que no modifica
el software, ni que resulta en nuevo software. Los esfuerzos
de este tipo de ingeniería producen: grafos de llamados, grafos
de flujo de control. Otro tipo de ingeniería reversa es
re-documentación, otro es recuperación del diseño. Refactorizar
es una transformación a un programa para reorganizarlo sin cambiar
su comportamiento, es una forma de ingeniería reversa que busca
mejorar la estructura del programa. La ingeniería reversa de datos
buscar recuperar esquemas lógicos a partir de bases de datos físicas.
2.5.4 Lecturas recomendadas
Para conocer más sobre verificación de programas puede ver
[9].
2.5.5 Ejercicios
-
Busque y pruebe alguna herramientas para facilitar la comprensión de código.
-
Busque y pruebe alguna herramientas para hacer ingeniería reversa.
2.6 Diseño de software
Indicadores de logro:
-
INDICADOR: Conoce términos y definiciones relacionados con diseño de software
- INDICADOR: Conoce y emplea técnicas y herramientas para facilitar diseño de software.
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:
-
Abstracción: proceso de olvidar información para que cosas diferentes
puedan ser tratadas como iguales. En diseño dos formas claves son
parametrización y especificación.
- Acoplamiento y cohesión: Acoplamiento es fuerza de vínculos entre
módulos, cohesión es la relación entre los elementos que forman un módulo.
- Descomposición y modularización: descompone un software grande en
partes independientes poniendo funcionalidades o responsabilidades en
cada parte.
- Encapsular/esconder información: significa agrupar y empaquetar
los elementos y detalles internos de una implementación para hacerlos
inaccequibles y mejorar abstracción.
- Separación de interfaces e implementación: se trata de definir
un componente especificando una interfaz pública que puede ser usada
por componentes clientes, los cuales no deben preocuparse por detalles
de la implementación del componente que usan sino sólo por cumplir
requisitos de la interfaz.
- Suficiencia, completitud y primitivas: significa asegurar
que el componente captura todas las características importantes de
una abstracción y nada más.
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:
-
Concurrencia:
Como descomponer el software en procesos, tareas e hilos de ejecución.
- Control y manejo de eventos: Como organizar datos y flujo de control,
como manejar eventos temporales y reactivos con mecanismos como
invocación implícita y call-backs.
- Distribución de componentes: Como distribuir software sobre el hardware,
como se comunican los componentes.
- Manejo de errores y excepciones y tolerancia a fallas: Como
prevenir y tolerar fallas y manejar condiciones excepcionales.
- Interacción y presentación: Como estructurar y organizar la
interacción con usuarios y la presentación de información. No es
especificar interfaz de usuario.
- Persistencia de datos: Como manejar información que debe
perdurar.
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:
-
Estructura general (capas, tuberías y filtros, tablero)
- Sistemas distribuidos (cliente-servidor, tres capas, broker)
- Sistemas interactivos (Controlador modelo-vista, Control
de presentación-abstracción)
- Sistemas adaptables (micro-kernel, reflección)
- Otros (por ejemplo, proceso en lotes, interpretes, control de
procesos, basados en reglas)
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:
-
Revisiones de diseño de software: en grupo, informales o
semiformales aseguran calidad de artefactos de software (revisión
de arquitectura, revisiones e inspecciones de diseño, técnicas basadas
en escenarios, seguimiento de requerimientos).
- Análisis estático: formal o semiformal para evaluar un diseño
(análisis libre de fallas, chequeo cruzado automático).
- Simulación y prototipos: técnicas dinámicas (simulación de
desempeño o prototipo de viabilidad),
Las medidas pueden clasificarse en:
-
Medidas orientadas a la función del diseño: se trata
de medidas que pueden computarse sobre una descomposición funcional
de un diseño.
- Orientadas a objetos: pueden computarse métricas sobre un diseño
representado como diagrama de clases. También hay métricas sobre el
contenido interno de cada clase.
Notaciones para diseño de software
Las siguientes notaciones describen la estructura (componentes e interconexión):
-
Lenguajes de descripción de arquitectura: son textos formales, describen en términos de componentes y conectores
- Diagrama de clases y objetos: conjunto de clases y sus relaciones.
- Diagrama de componentes: representa conjunto de componentes, un
componente es "una parte física y reemplazable de un sistemas que provee
y cumple la realización de un conjunto de interfaces."
- Tarjetas de colaboración en responsabilidades: se usan para
denotar los nombres de componentes (clases), sus responsabilidades, y
los nombres de componentes que colaboran.
- Diagrama de entrega: representa nodos físico y sus relaciones, para
modelar aspectos físicos de un sistema.
- Diagramas entidad-relación: se usan para representar modelos
conceptuales de datos almacenados en sistemas de información.
- Lenguajes de descripción de interfaces: lenguajes como de
programación usados para definir interfaces de componentes.
- Diagramas de estructuras de Jackson: describe estructuras
de datos en términos de secuencia, selección e iteración.
- Gráficas de estructura: describen estructura de llamados de
un programa (que módulo llama a cual).
Las notaciones de comportamiento (o vista dinámica) suelen usarse en
el diseño detallado:
-
Diagramas de actividades: muestra el flujo de control de una
actividad a otra.
- Diagramas de colaboración: interacciones que ocurren entre un grupo
de objetos, donde el énfasis está en los objetos, sus enlaces y los
mensajes que intercambian.
- Diagrama de flujo de datos: muestra flujo de datos entre un
conjunto de procesos.
- Diagramas y tablas de decisión: representan combinaciones
complejas de condiciones y acciones.
- Diagramas de flujo: representa flujo de control y las acciones
asociadas por realizar.
- Diagramas de secuencia: muestran interacción entre un grupo de
objetos, con énfasis en el ordenamiento de mensajes en el tiempo.
- Diagramación de transición de estados: muestran control de
flujo de un estado a otro en una máquina de estados.
- Lenguajes para especificación formal: lenguajes textuales
y formales para definir y abstraer rigurosamente interfaces
de componentes y comportamiento, usualmente en términos de precondición
y poscondición.
- Pseudo-código y lenguajes para diseño de programas: lenguajes
como de programación, estructurales para describir comportamiento de
un procedimiento o método.
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
-
Consulte la documentación de splint y use este programa para chequear
uno de sus programas en C.
-
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/
-
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.