Con cadenas, podemos rellenar un objeto
string
sin saber cuanta memoria se va a
necesitar. El problema de introducir líneas de un fichero en
objetos string
es que se sabe cuántas
cadenas habrá - solamente lo sabemos cuando ya hemos leido el
fichero entero. Para resolver este problema necesitamos un nuevo
tipo de datos que pueda crecer automáticamente para contener las
cadenas que le vayamos introduciendo.
De hecho, ¿por qué limitarnos a manejar objetos
string
? Parece que este tipo de problema -
no saber la cantidad de cosas a manejar mientras está escribiendo
el problema - 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: las clases contenedor
(container). Las clases contenedor
son uno de los puntos fuertes del Estándar C++.
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ó las STL en la Librería Estándar de C++ haciendo un gran número de cambios. El desarrollo de las STL continúa en Silicon Graphics (SGI; ver www.sgi.com/Technology/STL). Las SGI STL divergen de la Librería Estándar de C++ en muchos detalles sutiles. Aunque es una creencia ampliamente generalizada, el C++ Estándar no "incluye" las 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 el SGI STL. En este libro, intentaré decir «la librería Estándar de C++» o «Librería Estándar de contenedores», 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 el
vector
y no saber cómo está implementado
(de nuevo, uno de los objetivos de la POO). Los programas que usan
vector
en estos primeros capítulos del
libro no son exactamente como los haría un programador
experimentado, como comprobará en el volumen 2. Aún así,
encontrará que en la mayoría de los casos el uso que se hace es
adecuado.
La clase vector
es una
plantilla, lo que significa que se puede
aplicar 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 un vector de «cualquier
clase». Para decirle al compilador con qué clase trabajará
(en este caso que va a manejar el vector), hay que poner el nombre
del tipo deseado entre «llaves angulares». 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 una 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 porque existen otros contenedores y
otros métodos para poner nuevos elementos en los contenedores. Por
ejemplo, hay un insert()
para poner algo en
medio de un contenedor. vector
la soporta
pero su uso es más complicado y no necesitamos explorarla 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 el elemento
será producido, 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 - lo dejamos para un capítulo posterior -
pero es útil que sea consciente 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
abierto 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 da un paso en 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 hay que admitir que tiene
un significado obvio) El último trozo, usa 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 un linea con el valor de
i
(mágicamente convertida 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 nos
proporciona endl
. Cuando lo compile y lo
ejecute verá el efecto de numeración de líneas del fichero.
Debido a que el operador >>
funciona con
iostreams
, se puede modificar fácilmente el
programa anterior para que convierta 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 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 el tipo entre las llaves angulares (el
argumento de las plantillas). Las plantillas y las librerías de
plantillas 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 (es decir, cambiar) cualquier
elemento del vector 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 haremos uso de él en los siguientes capítulos.