Uso del contenedor vector
con funciones¶
vector
como argumento de una función¶
Al igual que cualquier otro objeto, un contenedor vector
puede pasarse
por valor o por referencia.
En general, el paso por referencia será la opción preferida dado que en el paso por valor debemos realizar una copia de todo el contenedor.
Si éste contiene muchos elementos el proceso será ineficiente computacionalmente.
Solo si la función necesita en cualquier caso realizar una copia del contenedor original, entonces tendrá sentido pasar por valor.
En muchas ocasiones se pasa un objeto por referencia que no se va a modificar. Un peligro de pasar por referencia es que, inadvertidamente, al programar podemos alterarlo.
Por ello, una excelente práctica de programación es calificar como de
tipo const
aquellos argumentos que sean referencias pero que no deseemos
modificar:
Si inadvertidamente modificamos la referencia el compilador nos avisará con un error.
Frente a terceros, queda claro que la función no modificará ese parámetro.
En el ejemplo que vamos a analizar a continuación veremos diferentes versiones de una función que calcula el mínimo de un vector.
La función minimo_vector()
utiliza programación defensiva, de tal forma
que devuelve una variable boolena advirtiendo de que la operación se ha
realizado correctamente (true
) o, por el contrario, el vector estaba
vacío (false
). Esta operación podría hacerse de manera más elegante
lanzando una
excepción,
pero consideramos este tema fuera del ámbito de un curso introductorio.
Función minimo_vector()
con vectores estilo C¶
#include <iostream>
using namespace std;
bool minimo_vector_v1(int [], int, int&);
int main()
{
int v[]{4,1,5,3};
int minimo;
if (minimo_vector_v1(v, 4, minimo))
cout << "El mínimo es " << minimo << endl;
}
bool minimo_vector_v1(int v[], int num, int& minimo)
{
if (num <= 0)
return false;
minimo = v[0];
for (int i = 1; i < num; ++i)
if (v[i] < minimo)
minimo = v[i];
return true;
}
Edita, compila y ejecuta el código
Recalcamos de nuevo el hecho de que la función debe recibir el tamaño
del vector para poder ser programada. La variable minimo
se pasa por
referencia, ya que el valor obtenido dentro de la función deseamos conservarlo
en el ámbito de la función que invoca, en este caso main()
.
Función minimo_vector()
con vector
y paso por valor¶
#include <iostream>
#include <vector>
using namespace std;
bool minimo_vector_v2(vector<int> v, int& minimo);
int main()
{
vector<int>v{4,1,5,3};
int minimo;
if (minimo_vector_v2(v, minimo))
cout << "El mínimo es " << minimo << endl;
}
bool minimo_vector_v2(vector<int> v, int& minimo)
{
if(v.empty())
return false;
minimo = v[0];
for (size_t i = 1; i < v.size(); ++i)
if (v[i] < minimo)
minimo = v[i];
return true;
}
Edita, compila y ejecuta el código
En esta versión ya no es necesario pasar el tamaño del vector. Es un atributo
del objeto vector
. Por lo demás, el código es muy similar.
Función minimo_vector()
con vector
y paso por referencia¶
#include <iostream>
#include <vector>
using namespace std;
bool minimo_vector_v3(vector<int>& v, int& minimo);
int main()
{
vector<int>v{4,1,5,3};
int minimo;
if (minimo_vector_v3(v, minimo))
cout << "El mínimo es " << minimo << endl;
}
bool minimo_vector_v3(vector<int>& v, int& minimo)
{
if(v.empty())
return false;
minimo = v[0];
for (size_t i = 1; i < v.size(); ++i)
if (v[i] < minimo)
minimo = v[i];
return true;
}
Edita, compila y ejecuta el código
Nótese que la única (pero significativa) diferencia es que ahora el vector se pasa por referencia. Es una gran ventaja pues evitamos copiar el vector que, eventualmente, puede tener un número elevado de elementos.
Función minimo_vector()
con vector
y paso por referencia constante¶
#include <iostream>
#include <vector>
using namespace std;
bool minimo_vector_v4(const vector<int>& v, int& minimo);
int main()
{
vector<int>v{4,1,5,3};
int minimo;
if (minimo_vector_v4(v, minimo))
cout << "El mínimo es " << minimo << endl;
}
bool minimo_vector_v4(const vector<int>& v, int& minimo)
{
if(v.empty())
return false;
// v.push_back(0); //Descomenta esta línea ¿Qué ocurre?
minimo = v[0];
for (size_t i = 1; i < v.size(); ++i)
if (v[i] < minimo)
minimo = v[i];
return true;
}
Edita, compila y ejecuta el código
Esta versión es la más eficiente y segura de todas. Mediante el especificador
const
garantizamos que el vector no va a ser alterado por error y, además,
un usuario de nuestra función tiene garantizado que, al enviar el vector a la
función, cuando esta termine no habrá sufrido ninguna alteración.
Descomentando la línea v.push_back(0);
puede observarse el beneficioso
efecto de usar const
.
vector
como valor de retorno de una función¶
En C++ las funciones devuelven objetos y una variable tipo vector
es un objeto.
Esto supone una gran ventaja sobre los vectores al estilo C, que no pueden ser devueltos.
Semántica de movimiento¶
Cuando devolvemos un contenedor c
desde una función f()
que fue invocada por una función g()
tenemos:
Un contenedor
c
local a la funciónf()
Un contenedor
d
local a la funcióng()
que recibirá los valores dec
Aparentemente, esto supone que C++ copiará el contenido de
c
en d
, y luego liberará la memoria ocupada por c
.
Esto puede ser muy ineficiente si el contenedor es grande y es el razonamiento
que hemos esgrimido para pasar los contenedores vector
por referencia.
Afortunadamente, en general, C++ no realiza una copia, sino que
asocia (bind)
el identificador del contenedor d
a la zona de memoria del contenedor
c
, y libera la memoria ocupada previamente por d
.
Esta característica se denomina semántica de movimiento.
En el siguiente ejemplo vamos a ver dos posibles opciones para cargar desde teclado los elementos de un vector:
pasando el vector por referencia
devolviendo vía
return
el vector
#include <iostream>
#include <vector>
using namespace std;
void carga_vector_v1(vector<double>&);
vector<double> carga_vector_v2(void);
void muestra_vector(const vector<double>&);
int main ()
{
vector<double> v;
carga_vector_v1(v);
muestra_vector(v);
v=carga_vector_v2();
muestra_vector(v);
}
void muestra_vector(const vector<double>& v)
{
for (size_t i = 0; i < v.size(); ++i)
cout << v[i] << endl;
}
Versión que pasa el vector por referencia
void carga_vector_v1(vector<double>& v)
{
int num_elementos;
cout << "Introduce número de elementos: ";
cin >> num_elementos;
v.clear(); //Borramos el vector por si v no está vacío
for (int i = 0; i < num_elementos; ++i)
{
double valor;
cout << "\nIntroduce elemento " << i+1 << ": ";
cin >> valor;
v.push_back(valor);
}
}
Versión que devuelve vector vía return
vector<double> carga_vector_v2()
{
int num_elementos;
cout << "Introduce número de elementos: ";
cin >> num_elementos;
vector<double> v;
v.reserve(num_elementos); // Creamos al menos num_elementos de capacidad
for (int i = 0; i < num_elementos; ++i)
{
double valor;
cout << "\nIntroduce elemento " << i+1 << ": ";
cin >> valor;
v.push_back(valor);
}
return v;
}
Edita, compila y ejecuta el código
Entre las dos opciones es preferible utilizar la segunda variante. Se adapta
mejor a lo que habitualmente esperamos de una función, que es devolver vía
return
un valor.
Algunas ventajas pueden también vislumbrarse del análisis del código de ambas versiones que hacemos a continuación.
clear()¶
La versión void carga_vector_v1(vector<double>&)
pasa un vector existente
por referencia. El aspecto relevante en este caso es que debemos ser conscientes,
en el diseño de cualquier función que recibe un vector, que este puede
no estar vacío. Por ello,
utilizamos la función miembro clear()
, que vacía el contenido del vector,
es decir, modifica su tamaño al valor 0
,
pero manteniendo intacta su capacidad. De no vaciar el vector, la función
lo que haría realmente es añadir elementos a los ya existentes previamente en
el vector.
El término vaciar puede llevar a equívoco y hacernos pensar que la
función clear()
realiza una suerte de eliminación elemento a elemento
del vector. ¡Nada de eso! Ni siquiera se altera la capacidad del vector. Es
una función tan simple como poner a 0
el atributo interno del vector que
almacena el tamaño.
reserve()¶
La versión vector<double> carga_vector_v2()
por el contrario, crea en su
interior el nuevo vector y, mediante la semántica de movimiento, de forma
transparente al programador, lo transfiere al vector de la función invocante.
Nótese en esta función la presencia optativa de la función miembro reserve()
.
La función reserve aumenta la capacidad de un vector al valor indicado por su
argumento. Su utilización es optativa, pero aconsejable, especialmente para
aquellas funciones que crean vectores desde 0 y alcanzan un elevado número
de elementos. De esta forma, el Sistema Operativo es invocado una única vez
solicitando la memoria justa necesaria para almacenar el vector.
Para ver el efecto beneficioso del uso de reserve()
, en el siguiente ejemplo
se muestra como evoluciona la capacidad de un vector de 100
elementos
sin y con el uso de reserve()
. Nótese que cada vez que se actualiza
la capacidad, se solicita al Sistema Operativo una nueva área de memoria y
se realiza una copia de los elementos del vector desde la antigua a la nueva
zona.
#include <iostream>
#include <vector>
using namespace std;
int main()
{
vector<int> v1;
size_t capacidad_actual = v1.capacity();
cout << "Evolucion de la capacidad sin reserve()\n\n";
for (int i = 0; i < 100; ++i)
{
v1.push_back(i);
if (capacidad_actual != v1.capacity())
{
capacidad_actual = v1.capacity();
cout << "Iteración " << i << ". Nueva capacidad: " << capacidad_actual << endl;
}
}
vector<int> v2;
capacidad_actual = v2.capacity();
v2.reserve(100); // Única diferencia con el código superior
cout << "\nEvolucion de la capacidad con reserve()\n\n";
for (int i = 0; i < 100; ++i)
{
v2.push_back(i);
if (capacidad_actual < v2.capacity())
{
capacidad_actual = v2.capacity();
cout << "Iteración " << i << ". Nueva capacidad: " << capacidad_actual << endl;
}
}
}
Edita, compila y ejecuta el código
La salida por pantalla del ejemplo anterior es: