¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí
¡Acceso ilimitado 24/7 a todos nuestros libros y vídeos! Descubra la Biblioteca Online ENI. Pulse aquí
  1. Libros
  2. C++
  3. La librería Standard Template Library
Extrait - C++ De los fundamentos del lenguaje a las aplicaciones
Extractos del libro
C++ De los fundamentos del lenguaje a las aplicaciones Volver a la página de compra del libro

La librería Standard Template Library

Introducción

El inventor de C ++, Bjarne Stroustrup, trabajó en proyectos de desarrollo muy importantes. Las primeras aplicaciones de C++ se llevaron a cabo en el campo de las telecomunicaciones, campo que requiere herramientas de alto nivel. Por supuesto, las clases promueven la abstracción, pero un programa no se compone únicamente de interfaces y de implementaciones.

Al diseñar el software, el desarrollador debe determinar el límite de reutilización de lo desarrollado previamente. Detrás del término reutilización, a menudo nos referimos al hecho de copiar y pegar ciertas partes del código fuente. ¿Cuáles son los elementos que se prestan mejor a esta operación? Las clases, y por supuesto el modelo orientado a objetos construido en torno al concepto de reutilización. Pero también encontramos estructuras de datos, funciones especializadas y piezas algorítmicas de diversa naturaleza, que aún no han alcanzado la madurez necesaria para la formación de clases.

Los módulos describen un desglose bastante físico de los programas. Por lo tanto, un archivo de código fuente .h o .cpp se puede considerar como un módulo. Sin embargo, el montaje de módulos no es cosa fácil, sobre todo cuando proceden de desarrollos anteriores. Pueden aparecer conflictos de nombres, funciones, diferencias de representación...

Organización de los programas

1. Espacios de nombres

El lenguaje C solo conoce dos niveles de ámbito: el nivel global, al que pertenece la función main(), y el nivel local, destinado a instrucciones y variables locales. Con la aparición de las clases, apareció un nivel adicional, destinado al registro de campos y métodos. Luego, la introducción de la derivación (herencia) y de los miembros estáticos terminó de completar la gama de los niveles de alcance.

Por las razones mencionadas en la introducción, se hizo necesario estructurar el espacio global. Para mantener solo uno, el espacio global es demasiado arriesgado para almacenar variables y funciones de programas antiguos. Los conflictos son inevitables.

Por lo tanto, podemos particionar este espacio global usando espacios de nombres:

namespace Edificio 
{ 
  double longitud; 
 
  void medir() 
  { 
    longitud=50.3; 
  } 
} ; 
 
namespace Cadenas 
{ 
  int longitud; 
 
  void calculo_longitud(char*s) 
  { 
    longitud=strlen(s); 
  } 
} ; 

Ambos espacios de nombres, Edificio y Cadenas, contienen una variable llamada longitud, que es de un tipo diferente. Las funciones medir() y calculo_longitud() siempre usan la versión correcta, porque la regla de accesibilidad también se verifica en los espacios de nombres: el compilador siempre busca la versión más cercana.

Para usar cualquiera de estas funciones, la función main() debe usar la operación de resolución de alcance :: o una instrucción using:

int main(int argc, char* argv[]) 
{ 
  Edificio::medir(); 
  printf("La longitud del edificio es %g\n",Edificio::longitud); 
 
  using Cadenas::longitud; 
  Cadenas::calculo_longitud("hola"); 
  printf("La longitud de la cadena es %d\n",longitud); 
  return 0; 
} 

Observamos que llamar a una función declarada dentro de un espacio de nombres se parece mucho a llamar a un método estático. Esta analogía continúa para el acceso a una variable...

Flujo C++ (entradas-salidas)

La STL gestiona muchos aspectos de las entradas-salidas. Inaugura una forma de programación para que persistan los nuevos tipos definidos utilizando el lenguaje C++.

El equipo que la diseñó a finales de la década de 1980, se cuidó de ajustarse a las técnicas vigentes y de producir un trabajo que resistiera el paso de los años.

Se debe reconocer que la administración de archivos ha evolucionado enormemente, desde la introducción de la librería estándar: las bases de datos relacionales han reemplazado a los archivos estructurados y las interfaces gráficas han prevalecido frente a las consolas orientadas a caracteres.

Sin embargo, los ejes tomados para el desarrollo de la STL fueron los correctos. Si el uso de los flujos ha caído un poco en desuso, su estudio permite ver con mayor claridad para producir una nueva generación de entradas-salidas. Además, sigue existiendo el terminal en modo cadena de caracteres. La vitalidad de los sistemas Linux es prueba de ello.

1. Generalidades

Para comenzar a aprender las entradas-salidas correctamente, debe diferenciar entre archivo y flujo. Un archivo se caracteriza por un nombre, una ubicación, derechos de acceso y, algunas veces, también un periférico. Un flujo (stream en inglés) es un contenido, una información que el programa lee o escribe. Esta información puede ser de alto o bajo nivel. En la base, naturalmente, encontramos el byte, pero luego éste se especializa en datos de tipo entero, decimal, booleano, string, etc. Finalmente, podemos crear registros compuestos por información muy diversa. Es bastante lógico considerar que la forma de estos registros se corresponde con la formación de una clase, es decir, un tipo en sentido C++.

Los flujos de C++ se organizan en tres niveles; el primero, el más abstracto, agrupa los ios_base, un formato de entrada-salida independiente del estado y del formato. Luego encontramos el nivel basic_ios, versión que integra la noción de parámetro regional (locale en inglés).

Finalmente, encontramos el nivel basic_iostream, un grupo de modelos de clase destinados a soportar el formateo de todos los tipos base conocidos por el lenguaje. Trabajaremos en este nivel.

La circulación de información se realiza...

Clase string para la representación de las cadenas de caracteres

Sorprendentemente, la mayoría de los tratados sobre algoritmia no estudian las cadenas como tales. La estructura de datos más cercana sigue siendo la tabla, para la que hemos imaginado una gran cantidad de problemas y soluciones. 

El lenguaje C se ha mantenido fiel a este enfoque y considera que las cadenas son arrays de caracteres. Sus diseñadores tomaron dos decisiones importantes: la longitud de una cadena se limita a la asignada por el array y la codificación es la de los caracteres de C, utilizando la tabla ASCII. En la medida en que no existe medio de determinar el tamaño de un array que no sea mediante el uso de una variable adicional, los diseñadores del lenguaje C han imaginado terminar sus cadenas con un carácter especial, de valor nulo. Es cierto que este carácter no tiene función en la tabla ASCII, pero las cadenas de C se han vuelto muy especializadas, muy lejos del algoritmo general.

El autor de C++, Bjarne Stroustrup, quería que su lenguaje fuera compatible con el lenguaje C, pero también una mejora en la codificación teniendo en cuenta diferentes formatos de codificación, ASCII o no.

1. Representación de las cadenas en la STL

Para la STL, una cadena es un conjunto ordenado de caracteres. Por lo tanto, una cadena se parece mucho al vector, una clase que también está presente en la librería. Sin embargo, la cadena desarrolla accesos y procesamientos que le son específicos, por lo que soporta mejor los algoritmos traducidos a C++.

Las cadenas de la librería estándar utilizan una clase de caracteres para omitir la codificación. La STL proporciona soporte para caracteres ASCII (char) y para caracteres extendidos (wchar_t), pero uno podría considerar desarrollar otros formatos destinados a algoritmos basados en cadenas. La ingeniería genética utiliza cadenas formadas por caracteres específicos, A, C, G, T. Por lo tanto, codificar con un char es muy costoso en términos de espacio, ya que dos bits son suficientes para expresar dicho vocabulario, tanto más cuanto las secuencias de genes pueden afectar a varios cientos de miles de bases. También puede especificar caracteres adecuados para alfabetos no latinos, para los cuales la tabla ASCII no es eficaz.

La clase basic_string...

Contenedores dinámicos

Una función esencial de la librería estándar, es proporcionar mecanismos para soportar los algoritmos con la mayor eficiencia posible. Esta declaración tiene varios objetivos contradictorios. Los algoritmos preferiblemente deben ser genéricos, es decir, métodos de trabajo independientes del tipo de datos a manejar. El lenguaje C utiliza los punteros void* para garantizar la generalidad, pero este enfoque implica una pérdida significativa de eficiencia en el control de tipos, complicación del código y, en última instancia, un rendimiento deficiente. La eficiencia demandada por la STL se obtiene al precio de un diseño cuidadoso y controles sutiles de los tipos. Es cierto que el resultado es un compromiso entre expectativas a veces opuestas, pero es lo suficientemente convincente como para ser utilizado en el desarrollo de aplicaciones cuya seguridad operativa es imperativa.

Los diseñadores de la STL, utilizaron los modelos de clases para desarrollar la generalidad. Los modelos de clases y funciones se analizan en detalle en el capítulo Programación orientada a objetos, y su uso es bastante sencillo. Una clase se instancia a partir de su modelo, proporcionando los parámetros esperados, generalmente el tipo de datos que realmente se tiene en cuenta para la implementación de la clase. Es absolutamente necesario diferenciar este enfoque del uso de macros (#define), que provoca resultados inesperados. Estas macros resultan muy poco seguras de usar.

Una idea original en la construcción de la librería estándar es la correlación entre los contenedores de datos y los algoritmos que se aplican a estos contenedores. Una lectura comparativa de diferentes manuales de algoritmia, lleva a la conclusión de que las estructuras de datos son siempre bastante parecidas, así como los algoritmos que se aplican a estas estructuras. Por tanto, no era oportuno diseñar estructuras aisladas, como una pila o una lista sin pensar en el después y en la aplicación de algoritmos menos específicos. Los diseñadores de la STL pudieron evitar este escollo.

1. Contenedores

Los contenedores son estructuras de datos para almacenar objetos de varios tipos. Los contenedores STL respetan las principales construcciones...

Trabajos prácticos

El intérprete tiny-lisp se basa en gran medida en la STL. Aquí hay detalles sobre la clase Variant implementada con gran soporte de objetos de la librería estándar.

1. La clase Variant

Variant es el tipo de datos universal de tiny-lisp. Puede ser un símbolo, un número, una lista (de Variant), un procedimiento.

images/cap4-pag86.png

En tiny-lisp, el objeto Variant forma parte de un entorno, un contenedor dotado de una tabla de símbolos. Esta estructura es necesaria para la ejecución de las funciones y de las expresiones lambda LISP, para pasar los parámetros y crear variables locales.

enum variant_type 
{ 
   Symbol, Number, List, Proc, Lambda, Cadena 
}; 
 
// definición por venir; Variant y Entorno se referencian mutuamente 
struct Entorno; 
 
// un Variant representa cualquier tipo de valor Lisp 
class Variant { 
public: 
 
   // función que devuelve Variant y que recibe como argumento variants 
 
   typedef Variant(*proc_type) ( const std::vector<Variant>& ); 
 
   typedef std::vector<Variant>::const_iterator iter; 
 
   typedef std::map<std::string, Variant> map; 
 
   // tipos tomados de la enumeración: symbol, number, list, proc o lamda 
   variant_type type; 
 
   // valor escalar 
   std::string val; 
 
   // valor list 
   std::vector<Variant> list; 
 
   // valor lambda 
   proc_type proc; 
 
   // entorno 
   Entorno* env; 
 
   // constructores 
   Variant(variant_type type = Symbol) : type(type) , env(0), proc(0) { 
 
   } 
 
   Variant(variant_type type, const std::string& val) : 
type(type), val(val) , env(0) , proc(0) { 
 
   } 
 
   Variant(proc_type proc) : type(Proc), proc(proc) , env(0) { 
 
   } 
 
   std::string to_string(); 
   std::string to_json_string(); ...