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ón f()

  • Un contenedor d local a la función g() que recibirá los valores de c

Aparentemente, esto supone que C++ copiará el contenido de c en d, y luego liberará la memoria ocupada por c.

alternate text

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.

alternate text

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:

alternate text