Serie ficheros virtuales

 

 

 

 

 

C Ficheros virtuales

 

 

 

 

 

 

A) De C ANSI a Visual C

 

 

Esta zona se dedica al desarrollo del producto en C ANSI y su utilización bajo interfaces Visual C.

 

También se hará referencia al uso en iSeries bajo RPG.

 

 

 

A.I Desarrollo y fundamentos

 

A.I.1 Punteros e instrucciones de asignación dinámica de memoria en C ANSI

 

 A.I.1.1 Introducción a los punteros

 A.I.1.2 Punteros genéricos (void *)

 A.I.1.3 Punteros unidimensionales

 A.I.1.4 Algunas consideraciones prácticas

       - Sobre los tipos de puntero utilizados en los parámetros

       - Sobre la reserva de memoria y su liberación

       - Sobre los errores de asignación de memoria

       - Sobre el ámbito de la memoria

 

 A.I.1.5 Punteros bidimensionales

 A.I.1.6 Dimensiones superiores

 

 

El funcionamiento de la aplicación de soporte de ficheros virtuales hace un uso intensivo de la asignación dinámica de memoria, que organiza en registros y ficheros ligados por vías de acceso, por tanto conviene dedicar algo de tiempo a describir las instrucciones sobre punteros y a la asignación dinámica de memoria.

 

                                                                                    _________

 

 

A.I.1.1 Introducción a los punteros

 

Los punteros son variables que sirven para guardar las direcciones de otras variables u objetos.

 

Se pueden señalar varias categorías para C ANSI. Pero básicamente consisten en:

 

·         Punteros a variables, que permiten archivar la dirección de otra variable,

·         Punteros a estructuras, que apuntan a estructuras y para los que se introducen operadores de recuperación de sus miembros, y

·         Punteros a procedimientos, que permiten archivar la dirección de un programa o de un procedimiento.

 

En este libro se utilizarán todos ellos, los de variables y estructuras porque sustentan el producto y los de procedimientos porque se introducen en varios ejemplos de cálculo de funciones inversas que aparecerán en la segunda parte, concretamente en la calculadora hipotecaria y en la calculadora de precisión extendida.

 

Adicionalmente, hay que indicar que el sistema operativo concreto con el que trabajemos puede admitir una tipología más amplia de punteros, extendiéndolos a distintos tipos de objetos, que puede necesitar una traducción interna genérica a void* ó a tipos extendidos definidos en cabeceras y directivas #pragma particulares de la versión de C utilizada. En este libro no usaremos estos tipos.

 

 

                                                                                    _________

 

 

 

Los punteros a variables se reconocen porque utilizan una sintaxis específica, que vamos a resumir a continuación de forma sucinta.

 

 

En cuanto a la definición, se reconocen porque se antepone * a su nombre:

 

Así              int i, j;     es una definición de las variables enteras i, j

Mientras que     int *k;       define un puntero a la variable entera k

 

 

En la asignación, su reconocimiento se produce porque las variables puntero contienen direcciones y utilizan el operador dirección &:

 

Así             i = 5;      es una asignación para una variable entera

Mientras que   k = &j;      es una asignación para un puntero a variable entera

 

 


El operador  *  también se utiliza para acceder al valor de la variable apuntada, como operador indirección.


  Así, podemos escribir el siguiente fragmento de código:


     // Definiciones

     int i,j;
     int *k;

           
      // Asignaciones

      i = 5;

      k = &i;
 
      j = *k;       Entonces  k = 5.



En el paso de parámetros el juego es más contextual:



 Una definición como

 double G(double d)  indica un paso por valor, los cambios en d no se reflejarán en el nivel anterior.


 Por el contrario,

double G(double *d) es un paso por referencia, entonces al pasarse la dirección de la variable el valor de la

                    misma puede cambiarse


Así, podríamos tener

   double F(double *d);
   double G(double *d, double *r);

   double F(double *d)
   {
    double r = 0;
 
    return G(d, &r); // Pasa d que es una dirección soportada por un puntero y &r, la dirección de r
                     // Tanto el valor apuntado por d como el propio valor de r pueden cambiarse en G
                     // y sus cambios se transportarán a las siguientes instrucciones de F
    }
   
                                                                                    _________

 


Las variables contiguas a las apuntadas por un puntero son accesibles por aritmética de punteros, lo que es especialmente utilizado en el manejo de cadenas, que en C son series de caracteres acabadas con el terminador nulo ‘\x00’.

En la aritmética de punteros se accede a una variable desde una dirección de partida a la que se suma o resta el número de elementos necesario.

 

Veámoslo en un ejemplo:



 Supuesta la definición

   char c[] = “123”; // Definición de una cadena de caracteres de forma directa.
                     // Al definirse así el sistema crea una serie de tamaño 4 y contenido 123\0.

   char *s = c;      // Puntero definido con la dirección de la cadena pues el nombre de una
                     // también es sinónimo de su dirección, aunque como puntero constante,
                     // c no puede reasignarse a otra dirección.

Entonces, tanto
 
   memset(c+1, ‘b’, 1);      como      *(s+1) = ‘b’; // “La aritmética de punteros”

   cambiarían el valor de c de “123” a “1b3”.


Cabe señalar que las cadenas una vez definidas ya sólo se pueden cambiar de forma indirecta, bien utilizando un puntero auxiliar, al igual que s con c del mediante *(s+i), o bien empleando las familias de funciones mem ó str, tal como el memset del mismo ejemplo.

Normalmente prefiero estas últimas porque necesariamente obligan a especificar qué se está haciendo y son las que se emplean en el núcleo del sistema de los ficheros virtuales, pero * es la alternativa más concisa y popular.


Siguiendo esta línea, vamos a introducir un par de rutinas para disponer de unos ejemplos más elaborados del uso de esta notación.

Primero veremos la codificación de la rutina depuradora de blancos intermedios del analizador de expresiones de la calculadora en aritmética extendida, que se presentará en los capítulos de ejemplos, y en donde se han resaltado las instrucciones bajo aritmética de punteros y además se han acompañado del seguimiento de un caso particular entre corchetes para explicitar su funcionamiento:

                                                                                    _________

 

 

//-----------------------------------------------------------------------------------

// Función: SRRCAL_DepuraBlanks0

//

// Descripción: Auxiliar de depuración de blancos intermedios de una

//           expresión de entrada. Con recorte de longitud.

//

// Parámetros:

// (IO) cValorA: Valor de expresión a depurar

//

// Retorno: Nºde blancos depurados

//-----------------------------------------------------------------------------------

int SRRCAL_DepuraBlanks0(char *cValorA)

{

 static char cValor[32767]; // Copia Wk de cValorA para salida

 char *cValorW = cValor;    // Auxiliar para recorrido de cValor de salida

 

 char *cValorAW = cValorA;  // Recorrido de cValorA de entrada

 

 int iNblank = 0;           // Valor de retorno. Contador de blancos depurados

 

 

 

 // Inicializa y copia a variables temporales de proceso

 

 memset(cValor, ' ', 32766);

 memset(cValor + 32767, '\x00', 1);

 

 

 // Copia a variable temporal de proceso

 

 strcpy(cValor, cValorA); [ supongamos “ 1 + 2 + 3 ”]

 

 

 

 // Bucle de depuración de blancos

 

 while (*cValorAW)

 {

  if ( *cValorAW == ' ' )

  {

   iNblank++;

   cValorAW++;

  }

  else *cValorW++ = *cValorAW++; // Instrucción de paso central

 }


 

 

 // Filtro

 

 if (!iNblank) return iNblank;

 

 

 // Cierra la cadena, recortando su longitud a la de la cadena depurada

 

 memset(cValorW, '\x00', 1);

 

 

 

 

[ En el ejemplo, se seguiría la evolución

 

 

Entrada “ 1 + 2 + 3 ”

 

 

Posición  0   1   2   3   4   5   6   7   8   9   10  11  12 (13)

 

Valor    “ ” “ ” “1” “ ” “+” “ ” “2” “ ” “+” “ ” “3” “ ” “ ” \00

 

iNblank   1   2   2   3   3   4   4   5   5   6   6   7   8

 

 

Salida “1+2+3”

 

Posición  0   1   2   3   4  (5)


 

Valor    “1” “+” “2” “+” “3” \00

 

 

Como se utiliza la notación postincremento++, cada vez que se asigna un valor, el puntero queda apuntado a continuación a la siguiente posición.

 

Entonces, la última asignación del terminador ‘\00’, ya fuera del bucle, cierra la cadena de salida recortando su longitud efectiva a la establecida por la última operación de asignación llevada a término]

 

 

 

 

 // Devuelve con la copia depurada de blancos intermedios y tamaño recortado

 

 strcpy(cValorA, cValor);

 

 return iNblank;

}

                                                                                    _________

 

 

Otro ejemplo de uso de aritmética de punteros lo proporciona el servicio “Trim” del mismo programa auxiliar SRRCAL.

 

Trim es el depurador de blancos de cola que se aplica a los campos de entrada en los cuadros de dialogo alfanuméricos de la Multiagenda que se presenta en los capítulos de ejemplos. Como ofrece la variante de recorrer al revés la cadena recibida apoyándose en strrev y utilizar la diferencia aritmética de punteros, también se incluye ahora al completo:

 

//-----------------------------------------------------------------------

// Función: SRRCAL_Trim

//

// Descripción: Auxiliar de depuración de blancos de cola de una

//           expresión de entrada. Con recorte de longitud.

//

// Parámetros:

// (IO) cText: Valor de expresión a depurar

//

// Retorno: Longitud tras la depuración

//------------------------------------------------------------------------

unsigned long SRRCAL_Trim(char *cText)

{

 static char cWork[32767]; // Paso auxiliar del texto

 

 char *cWorkW = cWork;     // Recorrido de cWork

 

 int iNctrl = 0;           // Control resultados SRRCAL_DepuraCtrl

 int iNblanks=0;           // Control resultados SRRCAL_DepuraBlanks

 

 unsigned long lResul = 0; // Aux.Cálculo última posición significativa

 unsigned long lLong = 0;  // Aux.Cálculo última posición significativa

 

 

 

 // Inicializa, depura caracteres de control y copia a variable de trabajo

 

 memset(cWorkW, ' ', 32766);

 memset(cWorkW + 32767, '\x00', 1);

 

 iNctrl = SRRCAL_DepuraCtrl(cText); // Similar a DepuraBlanks (Para caracteres de control introducidos por error)

 

 strcpy(cWorkW, cText);

 

 

 // Longitud de partida

 

 lLong = strlen(cText);

 

 

 // Si no hay blancos que depurar, termina

 

 iNblanks = SRRCAL_DepuraBlanks(cWorkW);

 if (!iNblanks) return lLong;

 

 

 // Busca último carácter significativo

 

 strrev(cWorkW); // Revierte la variable de trabajo para su lectura inversa directa

 

 while (*cWorkW)

 {

  if ( *cWorkW == ' ' ) cWorkW++;

  else break;

 }

 

 lResul = cWorkW - cWork;

 

 

 

 // Recorta longitud al último carácter significativo

 

 memset(cText + lLong - lResul + 1, '\00', 1);

 

 lLong = strlen(cText);

 

 return lLong;

}

 

 

[ Si volvemos a tomar el mismo ejemplo de partida, se seguiría la evolución siguiente

 

 

Entrada “ 1 + 2 + 3 ” lLong = (strlen) = 12

Reverso “ 3 + 2 + 1 ”

 

 

Posición  0   1   2

 

Valor    “ ” “ ” “3” break en (“3” != “ ”)

 

lResul = (cWorkW = cWork + 2) – (cWork) = 2

 

 

=> Terminador ‘\00’ en lLonglResul + 1 = 12 – 2 + 1 = 11, esto es:

 

 

Posición  0   1   2   3   4   5   6   7   8   9   10 (11)

 

Valor    “ ” “ ” “1” “ ” “+” “ ” “2” “ ” “+” “ ” “3” \00

 

=> Salida final “ 1 + 2 + 3” {=> (strlen) = 10} ]

 

 

                                                                                    _________

 

 

Anticipamos ahora que los punteros a estructuras tienen una definición de espíritu muy similar a la definición de punteros a variables que acabamos de ver, del estilo que sigue:

 

 

struct sEstructura *p_sEstructura;

 

 

y serán el centro de atención del siguiente capítulo del libro.

 

                                                                                    _________

 

 

 

 

En cuanto a la sintaxis de los punteros a procedimientos, su definición formal es

 

 

tipo (*F) (tipo parámetro1, tipo parámetro 2, ...)

 

 

Si bien no son objeto de uso directo en la aplicación, sí se utilizan en los ejemplos de los capítulos que veremos en la segunda parte de esta sección del libro y también en el anexo dedicado al método de resolución de ecuaciones siguiendo el algoritmo parabólico Müller.

 

                                                                                    _________

 

 

A.I.1.2 Punteros genéricos (void *)


Hemos visto definiciones de punteros concretas, de tipo *int k, pero hay un formato especial sintético que precisamente es básico en la aplicación y que permite implementar prototipos como los que siguen, asociados a la escritura y lectura secuencial en ficheros virtuales:

 

     long SRRCW_WRITE(const char *cFILE, const void *vDato);

     short int SRRCW_READ(const char *cFILE, long IndiceDeLectura, void *vDato);

 

 

Aquí aparecen dos nuevas especificaciones:

 

const” es un modificador que indica que el puntero se tomará como constante.

 

Por un abuso del lenguaje que cometemos todos, se utiliza generalmente en el sentido algo diferente de que el puntero apunta a un dato del que indicamos que debería permanecer constante.

 

Esto se consigue en la práctica porque bajo “constcFILE no podrá utilizarse directamente en funciones con prototipos que los empleen como punteros (de salida) “char *” como strcpy ó memcpy; esto es, el compilador no aceptará una instrucción del tipo strcpy(cFILE, cNOMBRE);

 

Pero debe tenerse en cuenta que el compilador si aceptará un juego de instrucciones como el siguiente:

 

 void Procedimiento(const char *cValorA)

{

 char *cValorAW = (char *) cValorA; // Copia Wk de cValorA

 

 ...

 

[ cValorAW ahora es de tipo char * y puede utilizarse en strcpy ]

 

 

 

Por tanto, lo que realmente garantiza aquí “const” es que es el propio puntero el que permanecerá constante.

 

                                                                                    _________

 

 

Y “void *” que indica por su parte que se recibe un puntero de tipo genérico que la implementación anterior y/o posterior a su utilización deberá concretar con un tipado implícito contextual o uno explícito (comúnmente denominado “casting”) como el siguiente:

 

      cDato = (char *) vDato;

 

 

en donde el tipo concreto del dato recibido se utilizará entonces en cDato como char *.

 

 

Un tipado (char *) siempre se admite con punteros de datos en C ANSI, pero cualquier otro tipo de conversión que no sea a la del tipo original de partida puede causar un error en tiempo de ejecución al utilizar las variables asociadas, debido a la discrepancia automática de espacio de almacenamiento que se produciría. Es de señalar que esta facilidad de tipado (char *) proporciona un método de exploración bajo entorno de depuración en ciertos sistemas, como el iSeries.

 

 

void *” puede utilizarse para recibir cualquier puntero de tipo de datos (esto es, a variables y estructuras, aunque no a funciones), y de hecho se verá en los ejemplos cómo en los mismos procedimientos *vDato se utilizará como soporte de paso de tipos long*, char* ó, y es el uso más interesante, (tipo struct) * donde (tipo struct) denota un puntero a estructura.

Más adelante veremos al presentar la función que desarrolla este prototipo en el capítulo dedicado a SRRCW como *vDato no llegará a resolverse internamente (Salvo para propósitos de inspección bajo debug como char * con cDato [cDato = (char *) vDato;]) sino que simplemente hará de puente de recepción y paso entre procesos.

 

Nos encontraremos al final con un establecimiento previo al WRITE y una restauración posterior con un READ o un CHAIN como en el siguiente ejemplo, en donde en el fichero virtual “PRUEBA” en un cierto momento se guarda el dato lDato que se recupera posteriormente:

       long lDato = 12478;

     ...

                SRRCW_WRITE("PRUEBA", &lDato);

           ...

       SRRCW_READ("PRUEBA", 1, &lDato);

                                                                                    _________

 

 

Un apunte adicional. En los prototipos siguientes utilizamos la partícula void en dos sentidos diferentes:

 

       void  ProcedimientoA(void *v);

               void *ProcedimientoB(void);

 

 

En el prototipo A admitimos un puntero genérico v, sin valor de retorno, mientras que en el prototipo B no se recibe ningún parámetro pero se devuelve uno de tipo genérico.

 

                                                                                    _________

 

 

Hasta ahora en los ejemplos se ha obviado el tema de la asignación del espacio apuntado por los punteros, que es precisamente el uso implícito principal de los mismos dentro del sistema de ficheros virtuales.

 

 

Es el momento de acometerlo, abordándolo según la dimensión del espacio a direccionar (Vector lineal, matriz bidimensional, cubo tridimensional o hipercubo n-dimensional).

 

Empecemos, pues, con los vectores lineales dinámicos (Unidimensionales).

 

 

                                                                                    _________

 

 

A.I.1.3 Punteros unidimensionales

 

Cuando se define un puntero, se define una serie una serie de dimensión dinámica cuya dimensión se concreta al asignar memoria suficiente para manejar los elementos deseados.

 

Como se apuntado antes, los punteros se pueden considerar bajo otras perspectivas, pero ésta es la forma de verlos que tiene la aplicación en cuanto al soporte de ficheros virtuales.

 

Por otro lado, la diferencia entre las series dinámicas y las ordinarias consiste en que éstas son punteros constantes y por tanto no pueden ser objeto de operaciones dinámicas, esto es, no pueden cambiar para apuntar a otra serie ni redimensionarse.

Aunque sí pueden utilizarse punteros auxiliares utilizando un casting explícito para recorrerlas en aritmética de punteros, como en el ejemplo SRRCAL_DepuraBlanks0 visto anteriormente, donde se definía una serie cValor y un puntero auxiliar  char *cValorW = cValor;  para poder así avanzar cValor como cValorW++, resultando en la práctica como si ejecutáramos directamente cValor++.

 

 

La instrucción base de asignación dinámica de memoria es “malloc” cuya definición formal viene dada como

 

        void *malloc(size_t tam)

 

 

donde el tipo size_t puede considerarse en la práctica como un entero sin signo.

 

 

 

Ejemplo:

 

short int e1_1(int iN)

{

 short int NO_ERRO = 0; // Marca de terminación "satisfactoria"

 int i = 0;             // Contador de for

 double *dS;            // Define la serie dinámica dS, dimensión indefinida

 

 

 // Si se hace un debug de dS ahora se verá como void no evaluable

 

 

 // Se concreta dinámicamente la dimensión de dS como sigue

 

 dS = (double *) malloc(iN * sizeof(double));

 

 if (!dS) return 1; // Error de asignación, que siempre conviene controlar

 

 

 // Ahora la serie tiene dimensión definida, pero contenido indefinido a inicializar

 

 memset(dS, '\x00', iN * sizeof(double)); // Inicializa a ceros

 

 

 // ...

 

 proceso

 

 // ...

 

 

 // Siempre que se utilice malloc, debe liberarse explícitamente la memoria consumida

 

 // pues el sistema libera la memoria de las variables locales pero no la de malloc

 

 // ya que puede interesar para propósitos de persistencia como explota exhaustivamente la aplicación de ficheros virtuales.

 

 

 free(dS);

 

 

 

 // A continuación, y si ya no va utilizarse el puntero de nuevo, es conveniente asignar a nulo para disponer de una manera de distinguir

 // entre punteros vacíos, y con esta práctica se incluirían también los agotados, que son directamente inválidos, de los efectivamente en uso

 // puesto que tras emitir free, y según en que sistema, los punteros pueden conservan aún una referencia a una dirección, aunque esta

 // ya carezca de soporte de contenido.

 

 dS = NULL;

 

 

 return NO_ERRO;

}

 

 

Todos los ejemplos que se presentan en esta primera parte del libro se encuentran en el programa PruebasW, implementados en el botón “Pruebas del libro” para poder seguirlos bajo debug.

 

(Nota: En la parte NET se presentan ejemplos similares, incluso más detallados, que pueden seguirse directamente en pantalla sin necesidad del mecanismo del debug)

 

                                                                                    _________

 

 

Veamos ejemplos de un debug de seguimiento:

 

 

 

 

 

 

 

 

 

Como el uso de malloc de esta manera es muy común, se dispone de una función alternativa

que explicita este tipo de uso: calloc, cuya definición formal es

 

             void *calloc(size_t num, size_t tam)

 

 

Además, calloc inicializa a ceros la memoria reservada, cosa que no sucede con malloc.

 

 

Entonces, la instrucción anterior podría haberse escrito como

 

 

                // ...

 

                dS = (double *) calloc(iN, sizeof(double));

 

                if (!dS) return 1;

 

                // ...

 

 

Lo mejor de las series dinámicas es que pueden redimensionarse a voluntad, lo peor es que precisamente es muy fácil incurrir en errores de asignación y direccionamiento.

 

 

[Incidentalmente, hay que señalar una ventaja adicional de la aplicación. Y es que, paradójicamente, su uso permite obviar este tema, pues ella misma se encarga de asignar, reasignar y liberar la memoria que consumen las bases de datos que se generen sin necesidad de acudir a las sentencias que estamos presentando.]

 

 

Así, si necesitamos ampliar la serie definida, simplemente hay que utilizar la instrucción

 

 

           void *realloc(void *p_origen, size_t tam)

 

 

Veamos como ejemplo la instrucción concreta de la codificación del sistema de ficheros virtuales, que se encarga de ir disponiendo del espacio de trabajo necesario para direccionar el número máximo de los ítems del bloque en curso, y que veremos en su contexto en el capítulo A.I.5:

 

 

           // Espacio que se reserva para la zona de datos al ampliar un fichero virtual

 

           cDatoW = (char *) realloc(cDatoW, lMaxSize*sizeof(char));

 

 

 

La reasignación a un tamaño inferior no plantea ningún problema, siempre que no intentemos volver a utilizar el fragmento de serie liberada.

                                                                                    _________

 

 

La memoria asignada por malloc permanece reservada hasta liberarla explícitamente con la instrucción free:

 

    void free(void *puntero_a_espacio_a_liberar)

 

 

que ya ha aparecido en el ejemplo 1.

 

 

 

Ni que decir tiene que si se invoca free sobre una serie ya liberada nos encontraremos con un error de uso de puntero, conviene por eso utilizar la asignación a NULL tras usar free para evitar confundir los punteros asignados de los verdaderamente vacíos.

 

                                                                                    _________

 

 

Una nota importante

 

Las instrucciones que se citan son de C ANSI, para C++ se introducen las instrucciones  new y delete  para asignar y liberar memoria, que en su versión para series dinámicas se codificarían como sigue

 

int *pInt = new(nothrow) int[100]; // Se crea un vector dinámico de enteros de 100 ítems

 

// Se indica nothrow para que si hay un error de asignación se devuelva un puntero nulo en vez de

// emitir una excepción

...

 

delete [] pInt; // Libera la memoria utilizada

 

 

No hay una alternativa a directa realloc, aunque puede escribirse una rutina que la emule utilizando la instrucción copy, alternativa a memcpy para C++, mediante una combinación de new, copy y delete.

 

 

Con la aparición de Visual C NET se produce un nuevo cambio en los objetos nativos, que pasan a ser de tipo administrado __gcgarbage collector” lo que significa que tras su uso se recolectan automáticamente de una forma similar a como se hace en Java.

 

Entonces, de un lado ya no se precisa delete porque los objetos se destruyen por sí mismos cuando salen de su ámbito de funcionamiento.

 

Por otro, se fuerza la sustitución de las series por contenedores de tipo vector que se operan con iteradores.

 

 

En cualquier caso, veremos en la segunda parte del libro como crear clases contenedoras que permitan utilizar el producto en la plataforma NET y más adelante una solución NET nativa.

                                                                                    _________

 

A.I.1.4 Algunas consideraciones prácticas

Se va a presentar un edificio construido por capas, empezando por el módulo SRRCM que por sí mismo ya sirve como sistema de soporte para caché y ficheros virtuales básicos, aunque sin vías de acceso adicionales ni operaciones de lectura por clave igual.

 

Desde este primer módulo surgen algunas consideraciones prácticas:

 

- Sobre los tipos de puntero utilizados en los parámetros

En su primera versión, desarrollada en iSeries en C ANSI para uso en RPG, los parámetros se definieron como punteros char *. Posteriormente, para ganar generalidad y poder usar estructuras en PC, se cambiaron a los void* citados anteriormente, lo que pudo hacerse gracias a que este cambio es irrelevante para el uso en RPG, de hecho no precisó cambiar los prototipos de enlace.

Sin embargo, hubo una pérdida: el depurador iSeries ya no presentaba el valor de las variables asociadas, ni aún forzando a visualización hexadecimal; por ello se verá en las rutinas críticas el uso de variables char* que se igualan a las variables recibidas con el propósito de mejorar el seguimiento bajo depuración.

Depuración que llegó un momento en que ya no se utilizaba para evaluar el funcionamiento del sistema, muy trillado en su algorítmica y pulido por su uso continuo, sino que ha llegado más bien a servir para dilucidar en que pueden fallar los sistemas externos que lo utilizan, cuando se lleva a cabo una implementación que no responde a lo previsto.

                                                                                    _________

 

- Sobre la reserva de memoria y su liberación

En su primera versión para PC, el sistema de ficheros virtuales se desarrolló en turbo C y bajo éste compilador sí llegue a encontrar respuesta nula en malloc/realloc en pruebas de tamaño normal de alrededor de varios centenares de registros.

Sin embargo, en el iSeries, compilando con teraespacio habilitado, esta situación no se me ha presentado ni en las pruebas más extremas, en que se solicitaban millones de registros, pues el sistema asignaba espacio virtual o físico según precisara. Hay que señalar que si por el contrario no se habilita la opción de teraespacio, que es la opción por defecto al compilar módulos RPG pero no lo es para C por lo que es necesario especificarla explícitamente, el sistema sólo direccionará cuatro megabytes de memoria antes de alcanzar el tamaño máximo de espacio dinámico.

Asimismo, en MsC++ tampoco he obtenido respuesta nula a malloc a niveles respetables, de centenares de miles de registros (y en un pentium III con tan sólo 64 Mb de memoria).

 

En cualquier caso el producto es flexible, si se obtiene una respuesta nula en la ampliación por agotamiento del espacio dinámico, simplemente deben liberarse datos con las funciones CLRF o ERASE o CLOSE para el cierre final.

 

La memoria asignada con malloc debería liberarse automáticamente cuando se cierra el programa en MsC++ ó cuando se cierra el grupo de activación en el iSeries.

Se observa en la práctica que sucede así instantáneamente en el iSeries, pero en MsC++ la liberación automática se realiza en segundo plano y puede llevar su tiempo, durante el cual el sistema incurre en riesgo de utilización de memoria caducada.

Es preferible, porque nos deja el control en nuestras manos y además es de respuesta inmediata, invocar a una función de cierre tal como SRRCW_CLOSE para ejecutar explícitamente los free contrarios a los malloc utilizados.

 

El depurador de MsC++ dispone de una útil herramienta de aviso para depurar malloc sin cerrar. Cuando se cierra la prueba de un programa, si el depurador detecta que hay situaciones de memoria abierta sin aplicar free lo indica con el mensaje “memory leaks!”. Por tanto, en un diseño de mínimo incremental, esta herramienta nos permite eludir las situaciones abiertas vigilando que en las pruebas de cada nueva rutina añadida que utilice el juego malloc/free no se presente este aviso.

                                                                                    _________

 

 

 

- Sobre los errores de asignación de memoria

 

Si es malo olvidarse de utilizar malloc cuando se precise, aún es peor hacerlo de manera insuficiente. En el primer caso el error catastrófico que se produce orienta perfectamente del problema. En el segundo, careceremos de esa oportunidad; si tenemos suerte, detectaremos el problema cerca de donde se originó, con algo menos, nos alertará un ASSERT FAILURE del MsC++. En el peor, pensaremos que está todo mal porque el funcionamiento será, a efectos prácticos, al azar, pasando de bueno a malo o regular sin causa aparente. Y el asunto puede llegar más lejos, puede llegar a que rutinas absolutamente inocentes aparezcan inesperadamente como rematadamente culpables.

 

Una alternativa menos elegante pero más segura es definir una variable tampón estática de longitud máxima a la que utilizar como espacio de trabajo recurrente a recorrer por aritmética de punteros, controlando que no se agote la longitud asignada, emulando el malloc sobre ese espacio controlado y reservado. Aún así, si en ese espacio se define una cadena, esto es, una serie de caracteres + el terminador nulo, y se usa el espacio posterior al terminador apoyándose en la variable de soporte de la cadena con aritmética de punteros, el MsC++ también nos dará un ASSERT FAILURE.

 

En resumen, malloc es una poderosa herramienta, pero para garantizar el éxito con ella es conveniente ser minucioso al extremo en la contabilidad de las asignaciones y descartes correspondientes a su uso, amén de conjugarlo con un riguroso desarrollo de mínimo incremental, es decir, ir agregando a las zonas probadas todo lo más una rutina en cada paso, que a su vez debe probarse hasta alcanzar el estado de verificada como producto estable y dedicar tiempo a reservar cada versión probada a fin de evitar errores acumulativos de difícil detección.

 

Si no es posible seguir las recomendaciones anteriores o nos hallamos ante una situación heredada a la que debe darse solución inmediata, a veces es eficaz, aunque es verdaderamente sumamente heterodoxo, y desde luego debe ser siempre provisional, centrar la zona sintomática e incluir variables auxiliares tampón, sin más uso efectivo que el de recoger el impacto del fallo de direccionamiento al que dedicar tiempo para una corrección adecuada posterior.

 

Las consideraciones anteriores sobre la reserva de memoria desaparecen en el modelo NET nativo, en donde la asignación dinámica de memoria está asociada a cada objeto, es supervisada por el sistema y se recolecta automáticamente, pero con un estilo y forma de programación radicalmente diferente.

 

Tampoco aparecerá este problema en las otras versiones de la serie dedicadas a visual basic para aplicaciones o a javascript porque la asignación y descarte de la memoria consumida por los vectores de soporte utiliza un mecanismo interno automático del que no hay que preocuparse.

 

                                                                                    _________

 

- Sobre el ámbito de la memoria

Los punteros son variables que apuntan a otras variables. Debe tenerse en cuenta el ámbito de las variables apuntadas.

Este puede ser automático, también llamado local, para las variables temporales que se definen localmente en un procedimiento. Su vigencia se circunscribe a la vida del procedimiento. El intento de usar direcciones locales fuera de su ámbito dará origen a un error.

Ilustremos el caso:

char* BufferZeros()

{

 char cBuffer[] = "0000000000";

 

 return cBuffer;

}

 

El uso de esta función dará lugar a un error en el retorno porque la vigencia de la variable habrá caducado.

 

 

Esto nos lleva a las variables estáticas, estas tienen vigencia mientras dure la vida del módulo de pertenencia. El mismo ejemplo anterior puede funcionar cambiándolo por

 

static char cBuffer[] = "0000000000";

 

char* BufferZeros()

{

 return cBuffer;

}

 

Ahora hay que tener en cuenta que cBuffer es una variable global del módulo en que radica la función BufferZeros y que mantendrá su permanencia mientras esté vigente el programa raíz del proceso.

 

 

Por último nos encontramos con las variables dinámicas que reciben la asignación de memoria con malloc y se liberan con free y que son a las que principalmente nos referiremos en este libro.

 

 

Es importante tener en cuenta que aunque siempre podemos usar el grupo de operaciones mem (como memcpy) para copiar contenidos en las variables que utilicemos mientras estas estén vigentes, un casting seguro sólo se garantiza para punteros que apunten a variables del mismo ámbito, si no contemplamos esta salvaguarda, el sistema puede rechazar la operación con un error que puede traducirse finalmente como del tipo "intento de usar direccionamiento fuera de ámbito".

 

 

                                                                                    _________

 

 

A.I.1.5 Punteros bidimensionales

 

Las asignaciones más interesantes son las de dimensión doble porque permiten serializar objetos y son la base de la emulación virtual de ficheros de la que trata específicamente este libro.

 

 

Un puntero doble también se puede utilizar para indicar que en una función va a cambiarse el propio valor de puntero que se pasa, como en el prototipo de la función de lectura siguiente:

 

 

short int SRRCM_VREAD(long lNIDp, long lINDIp, void **vDato)

 

 

en donde se devuelve la nueva dirección de los datos leídos en *vDato, lo que se indica formalmente especificando en el prototipo **vDato.

 

 

 

Sin embargo, la forma principal que tiene la aplicación de ver la definición de un puntero doble es como una serie de doble dimensión dinámica, en donde las dimensiones se concretan al asignar memoria suficiente para manejar los elementos que se deseen.

 

La serie doble puede verse como una serie compuesta de elementos de tipo vector, por ello, para concretar las dimensiones, debe usarse malloc en dos etapas de uso distinto.

 

Primero se concreta la dimensión izquierda, donde reservamos espacio para elementos de tipo vector, y a continuación se reserva el espacio que se desea contenga cada vector.

 

Es interesante observar que la segunda dimensión puede ser distinta para cada vector reservado, lo que es una extensión de la definición de series dobles ordinaria, y es otro de los principios básicos de la aplicación de ficheros virtuales.

 

Ejemplo:

 

short int e1_2(long lM, long lN)

{

 short int NO_ERRO = 0; // Marca de terminación "satisfactoria"

 long li = 0;           // Índice de nodo i

 long lj = 0;           // Índice de nodo j

 double **dS;           // Define la serie doble dinámica dS, dimensiones indefinidas

 

 

 

 // Si se hace un debug de dS ahora se verá como void no evaluable

 

 

 // Se concreta dinámicamente la dimensión izquierda de dS[lM][] como sigue

 

 dS = (double **) malloc(lM * sizeof(double *));

 

 if (!dS) return 1; // Error de asignación, que siempre conviene controlar

 

 

 // Se ha concretado la primera dimensión, pero si ahora se hace un debug de dS[x] se

 // verá como void no evaluable, hay que continuar la definición

 

 

 // Se concreta la dimensión derecha de dS[lM][lN] como sigue

 

 for (li=0;li<lM;++li)

 {

  dS[li] = (double *) malloc(lN * sizeof(double));

 

  if (!dS[li]) return 1; // Error de asignación, que siempre conviene controlar

 }

 

 

 

 // Ahora la serie tiene dimensiones definidas, pero contenido indefinido a

 // inicializar, por ejemplo con el valor de su posición en la serie

 

 for (li=0;li<lM;++li) for (lj=0;lj<lN;++lj) dS[li][lj] = (double) li * lM + lj;

 

 

 // ...

 

 

 // Libera la memoria consumida, debe hacerse explícitamente siempre que se utilice

 //

 // malloc, pues el sistema libera la memoria de las variables locales pero no el de

 //

 // malloc ya que puede interesar para propósitos de persistencia.

 //

 // La liberación ha de hacerse de forma inversa a la asignación, como sigue

 

 for (li=0;li<lM;++li)

 {

  free (dS[li]);

  dS[li] = NULL;

 }

 

 free(dS);

 

 dS = NULL;

 

 return NO_ERRO;

}

 

 

Veamos ahora el resultado del debug del código paso a paso:

 

 

Antes de ejecutar malloc

 

 

 

 

 

Tras malloc, pero antes de proceder a inicializar

 

 

 


 

Y ya por último después de inicializar

 

 

 

 

                                                                                   _________

 

 

A.I.1.6 Dimensiones superiores

 

Para considerar dimensiones superiores es aconsejable tener presente un esquema que permita su seguimiento pues es fácil perderse. Así, suponiendo que manejemos variables char, podemos considerar el siguiente esquema:

 

 

0  c Carácter

 

 

1 *c Una serie, es el salto cualitativo más importante.

 

     Puede direccionar palabras, frases, hojas, capítulos, hasta llegar a imaginar un libro.

 

     En una perspectiva distinta, en una lista de parámetros, nos serviría para indicar paso por referencia y por tanto de valor

     modificable.

 

     Esto puede aplicarse a cada nivel. En una lista de parámetros *[**..] indica un paso por referencia y por tanto que el valor del

     nivel inferior apuntado [**..] puede devolverse modificado.

 

 

 

2 **c Podemos imaginarlo como una estantería donde se colocan los libros, para esta presentación es el nivel más interesante puesto que

          la aplicación de ficheros virtuales alcanza hasta ésta profundidad.

 

      Obsérvese que cada uno de los libros direccionados puede ser de distinto tamaño.

 

      Esta versatilidad también se utiliza en la aplicación, puesto que cada fichero (o libro en la estantería de trabajo, según el

      lenguaje que se está empleando aquí) tiene su propio número de elementos.

 

 

 

3 ***c    Podemos imaginarlo como un mueble para las estanterías, y cada una de ellas puede ser de distinta amplitud.

 

4 ****c   Podemos imaginarlo como un pasillo para los muebles, y cada uno de ellos puede ser de distinta longitud.

 

5 *****c  Podemos imaginarlo como una planta para los pasillos, cada cual de distinta capacidad.

 

6 ******c Podemos imaginarlo como una sección de plantas, cada una de ellas de distinta anchura.

 

etc.

 

 

 

Algunos comentarios finales:

  Las series pueden utilizarse en formato dimensional o en formato de aritmética de punteros, que se presupone más rápido, pero mientras que en el caso unidimensional es muy fácil pasar de uno a otro y usar dS[i] ó *(dS + i) según convenga, en el caso multidimensional el uso de la aritmética de punteros debe hacerse con mucho cuidado pues la confusión de índices y tipos es muy fácil.

 Por eso, en códigos multidimensionales prefiero utilizar la notación más explícita posible y hacer uso de las funciones mem como en el fragmento siguiente

                memcpy(vDato, cDATOS[lNIDc]+lIndic*lSDimD[lNIDc], lSDimD[lNIDc]);

                       salida  fichero(lNIDc),posición(lIndic)         tamaño de cada registro lNIDc

 

que pertenece al núcleo de extracción de datos de la función SRRCM_CHAIN y sobre el que volveremos en el capítulo correspondiente

                                                                                    _________