2.7. Introducción a los vectores

Con las cadenas, se puede rellenar un objeto string sin saber cuánta memoria se va a necesitar. El problema de introducir líneas de un fichero en objetos string es que no se sabe de antemano cuántas cadenas habrá - solamente se sabe cuando ya se ha leído el fichero entero. Para resolver este problema hace falta algún tipo de contenedor que pueda crecer automáticamente para contener todas las cadenas que se le quieran introducir.

De hecho, ¿por qué limitarse a manejar objetos string? Resulta que este tipo de problema - no saber la cantidad de cosas a manejar mientras está escribiendo el programa - ocurre a menudo. Y este objeto «contenedor» podría resultar más útil si pudiera manejar cualquier clase de objeto. Afortunadamente, la Librería Estándar de C++ tiene una solución lista para usar: las clases contenedor (container). Las clases contenedor son uno de los puntos fuertes del C++ Estándar.

A menudo existe un poco de confusión entre los contenedores y los algoritmos en la Librería Estándar de C++, y la STL. La Standard Template Library fue el nombre que usó Alex Stepanov (que en aquella época estaba trabajando en Hewlett-Packard) cuando presentó su librería al Comité del Estándar C++ en el encuentro en San Diego, California, en la primavera de 1994. El nombre sobrevivió, especialmente después de que HP decidiera dejarlo disponible para la descarga pública. Posteriormente el comité integró la STL en la Librería Estándar de C++ haciendo un gran número de cambios. El desarrollo de la STL continúa en Silicon Graphics (SGI; ver www.sgi.com/Technology/STL). La STL de SGI diverge de la Librería Estándar de C++ en muchos detalles sutiles. Así que, aunque sea una idea equivocada muy extendida, el C++ Estándar no «incluye» la STL. Puede ser confuso debido a que los contenedores y los algoritmos en el C++ Estándar tienen la misma raíz (y a menudo el mismo nombre) que en la STL de SGI. En este libro, intentaré decir «la Librería Estándar de C++» o «los contenedores de la Librería Estándar», o algo similar y eludiré usar el término STL.

A pesar de que la implementación de los contenedores y algoritmos de la Librería Estándar de C++ usa algunos conceptos avanzados, que se cubren ampliamente en dos largos capítulos en el segundo volumen de este libro, esta librería también puede ser potente sin saber mucho sobre ella. Es tan útil que el más básico de los contenedores estándar, el vector, se introduce en este capítulo y se usará a lo largo de todo el libro. Verá que puede hacer muchas cosas con lo básico de vector sin preocuparse de cómo está implementado (de nuevo, uno de los objetivos importantes de la POO). Como aprenderá mucho más sobre éste y otros contenedores cuando llegue a los capítulos de la Librería Estándar del Volumen 2, parece perdonable que los programas que usan vector en estos primeros capítulos del libro no sean exactamente como los haría un programador experimentado de C++. Comprobará que en la mayoría de los casos el uso que se hace aquí es adecuado.

La clase vector es una plantilla, lo que significa que se puede aplicar de forma eficiente a tipos de datos diferentes. Es decir, se puede crear un vector de figuras, un vector de gatos, un vector de strings, etc. Básicamente, con una plantilla se puede crear una «clase de cualquier cosa». Para decirle al compilador con qué trabajará la clase (en este caso, qué va a contener el vector), hay que poner el nombre del tipo deseado entre «paréntesis angulares», es decir, < y >. Por lo que un vector de string se denota como vector<string>. Con eso, se crea un vector a medida que solamente contendrá objetos string, y recibirá un mensaje de error del compilador si intenta poner otra cosa en él.

Como el vector expresa el concepto de «contenedor», debe existir una manera de meter cosas en él y sacar cosas de él. Para añadir un nuevo elemento al final del vector, se usa el método push_back(). Recuerde que, como es un método, hay que usar un . para invocarlo desde un objeto particular. La razón de que el nombre de la función parezca un poco verboso - push_back() en vez de algo más simple como put - es que existen otros contenedores y otros métodos para poner nuevos elementos en los contenedores. Por ejemplo, hay un método insert() para poner algo en medio de un contenedor. vector lo soporta pero su uso es más complicado y no será necesario explorarlo hasta el segundo volumen del libro. También hay un push_front() (que no es parte de vector) para poner cosas al principio. Hay muchas más funciones miembro en vector y muchos más contenedores en la Librería Estándar, pero le sorprenderá ver la de cosas que se pueden hacer con sólo un par de características básicas.

Así que se pueden introducir elementos en un vector con push_back() pero ¿cómo puede sacar esos elementos? La solución es inteligente y elegante: se usa la sobrecarga de operadores para que el vector se parezca a un array. El array (que será descrito de forma más completa en el siguiente capítulo) es un tipo de datos que está disponible prácticamente en cualquier lenguaje de programación por lo que debería estar familiarizado con él. Los arrays son agregados, lo que significa que consisten en un número de elementos agrupados. La característica distintiva de un array es que estos elementos tienen el mismo tamaño y están organizados uno junto a otro. Y todavía más importante, que se pueden seleccionar mediante un índice, lo que significa que puede decir: «Quiero el elemento número n» y obtendrá ese elemento, normalmente de forma rápida. A pesar de que existen excepciones en los lenguajes de programación, normalmente se indica la «indexación» mediante corchetes, de tal forma que si se tiene un array a y quiere obtener el quinto elemento, sólo tiene que escribir a[4] (fíjese en que la indexación siempre empieza en cero).

Esta forma compacta y poderosa de notación indexada se ha incorporado al vector mediante la sobrecarga de operadores como el << y el >> de los iostreams. De nuevo, no hay que saber cómo se ha implementado la sobrecarga de operadores - eso queda para un capítulo posterior - pero es útil que sea consciente de que hay algo de magia detrás de todo esto para conseguir que los corchetes funcionen con el vector.

Con todo esto en mente, ya puede ver un programa que usa la clase vector. Para usar un vector, hay que incluir el fichero de cabecera <vector>:

//: C02:Fillvector.cpp
// Copy an entire file into a vector of string
#include <string>
#include <iostream>
#include <fstream>
#include <vector>
using namespace std;

int main() {
  vector<string> v;
  ifstream in("Fillvector.cpp");
  string line;
  while(getline(in, line))
    v.push_back(line); // Add the line to the end
  // Add line numbers:
  for(int i = 0; i < v.size(); i++)
    cout << i << ": " << v[i] << endl;
} ///:~

Listado 2.10. C02/Fillvector.cpp


Casi todo este programa es similar al anterior; se abre un fichero y se leen las líneas en objetos string (uno cada vez). Sin embargo, estos objetos string se introducen al final del vector v. Una vez que el bucle while ha terminado, el fichero entero se encuentra en memoria dentro de v.

La siguiente sentencia en el programa es un bucle for. Es parecido a un bucle while aunque añade un control extra. Como en el bucle while, en el for hay una «expresión de control» dentro del paréntesis. Sin embargo, esta expresión está dividida en tres partes: una parte que inicializa, una que comprueba si hay que salir del bucle, y otra que cambia algo, normalmente para avanzar por una secuencia de elementos. Este programa muestra el bucle for de la manera más habitual: la parte de inicialización int i = 0 crea un entero i para usarlo como contador y le da el valor inicial de cero. La comprobación consiste en ver si i es menor que el número de elementos del vector v. (Esto se consigue usando la función miembro size() -tamaño-, que he colado aquí sin avisar, pero hay que admitir que tiene un significado bastante obvio). La última parte usa una notación abreviada de C y C++, el operador de «autoincremento», para aumentar en uno el valor de i. Efectivamente, i++ dice «coge el valor de i, añádele uno y guarda el resultado en i». Conclusión: el efecto del bucle for es aumentar la variable i desde cero hasta el tamaño del vector menos uno. Por cada nuevo valor de i se ejecuta la sentencia del cout, que construye una línea con el valor de i (mágicamente convertido a un array de caracteres por cout), dos puntos, un espacio, la línea del fichero y el carácter de nueva línea que proporciona endl. Cuando lo compile y lo ejecute verá que el efecto es añadir números de línea al fichero.

Debido a la forma en que funciona el operador >> con iostreams, se puede modificar fácilmente el programa anterior para que divida la entrada en palabras separadas por espacios, en vez de líneas:

//: C02:GetWords.cpp
// Break a file into whitespace-separated words
#include <string>
#include <iostream>
#include <fstream>
#include <vector>
using namespace std;

int main() {
  vector<string> words;
  ifstream in("GetWords.cpp");
  string word;
  while(in >> word)
    words.push_back(word); 
  for(int i = 0; i < words.size(); i++)
    cout << words[i] << endl;
} ///:~

Listado 2.11. C02/GetWords.cpp


La expresión:

while (in >> word)

es la que consigue que se lea una «palabra» cada vez, y cuando la expresión se evalúa como «falsa» significa que se ha llegado al final del fichero. De acuerdo, delimitar una palabra mediante caracteres en blanco es un poco tosco, pero sirve como ejemplo sencillo. Más tarde, en este libro, verá ejemplos más sofisticados que le permiten dividir la entrada de la forma que quiera.

Para demostrar lo fácil que es usar un vector con cualquier tipo, aquí tiene un ejemplo que crea un vector de enteros:

//: C02:Intvector.cpp
// Creating a vector that holds integers
#include <iostream>
#include <vector>
using namespace std;

int main() {
  vector<int> v;
  for(int i = 0; i < 10; i++)
    v.push_back(i);
  for(int i = 0; i < v.size(); i++)
    cout << v[i] << ", ";
  cout << endl;
  for(int i = 0; i < v.size(); i++)
    v[i] = v[i] * 10; // Assignment  
  for(int i = 0; i < v.size(); i++)
    cout << v[i] << ", ";
  cout << endl;
} ///:~

Listado 2.12. C02/Intvector.cpp


Para crear un vector que maneje un tipo diferente basta con poner ese tipo como argumento de la plantilla (el argumento entre paréntesis angulares). Las plantillas y las librerías de plantillas bien diseñadas pretenden ofrecer precisamente esta facilidad de uso.

Además este ejemplo demuestra otra característica esencial del vector en la expresión

v[i] = v[i] * 10;

Puede observar que el vector no está limitado a meter cosas y sacarlas. También puede asignar (y, por tanto, cambiar) cualquier elemento del vector, también mediante el uso de los corchetes. Eso significa que el vector es un objeto útil, flexible y de propósito general para trabajar con colecciones de objetos, y sin duda se usará en los siguientes capítulos.