Bienvenidos sean a este post, hoy veremos la conducta de los contenedores en la memoria.
Hasta ahora sabemos que todo lo que existe en nuestro codigo se ubica en memoria. Tambien sabemos que todos estos se ubican entre el heap y el stack, veamos la siguiente declaracion:
struct Email
{
std::string asunto;
std::string cuerpo;
std::string desde;
std::chrono::time_point datetime;
};
int main()
{
Email obj;
Email* ptr;
}
Tenemos un struct que representa a un email con sus respectivas propiedades para almacenar informacion. En el main, crearemos un objeto y un puntero de este struct y ambos seran creados en el stack. Si bien por lo general se considera que el puntero va en el heap, esto no es asi. Porque el puntero puede apuntar a una direccion de memoria en el heap pero estos se almacenan siempre en el stack. Esto es fundamental que lo entendamos y recordemos antes ed continuar con los proximos temas de vector y listas.
Si bien ya vimos como trabaja vector en este post, una cosa que podemos deducir de su manera de trabajar es encapsulando al puntero en un buffer interno que representa un array de elementos del tipo especificado. Cuando se declara un objeto de vector, este toma la cantidad necesaria de memoria de stack para almacenar los datos miembros. Para ello, la clase vector tiene los siguientes tres propiedades:
template <typename T>
class Vector
{
public:
// codigo omitido
private:
int capacity_;
int size_;
T* buffer_;
};
El primero representa a la capacidad del vector, el segundo al tamaño que representa, y cuando estos son iguales se procede a aumentar la capacidad del vector, pero de eso hablamos en ese post, y por ultimo tenemos la propiedad que es el buffer y tiene toda la informacion. Vamos a suponer que creamos un objeto de la siguiente manera:
int main()
{
Vector<int> v;
}
Suponiendo que un entero toma 4 bytes, el puntero toma 8 bytes y la declaracion anterior toma al menos 16 bytes del stack. Cuando comencemos a ingresar elementos al vector, en el stack se mantendra de la misma forma pero entrara en accion el heap. Aqui el array buffer_ apunta a una direccion de memoria usando la funcion new[]. Con esto comentado vamos a ejecutar las siguientes funciones:
v.push_back(17);
v.push_back(21);
v.push_back(74);
Como mencionamos anteriormente, estos valores se iran agregando secuencialmente en el heap. Por esta razon, muchas se lo considera como un contenedor cache-friendly.
Cuando creamos una lista enlazada, esta tambien utiliza espacio en el stack para miembros de datos. Implementemos un objeto de tipo lista enlazada de la siguiente manera:
int main()
{
LinkedList<int> lista;
}
Esta clase no existe como tal pero la creamos en este post para poder generar las listas. Esta simple implementacion solo genera un puntero a head_ y tomara al menos 8 bytes de la memoria en stack. Pasemos a agregar un nuevo valor a esta lista:
lista.agregar(19);
Esta funcion se encarga de agregar un nuevo nodo en el heap, sobre esta funcion hablamos en este post, y en este nodo pasaremos los datos. Cada nodo al ser un struct se compone de tres propiedades, siendo uno para el valor, otro para el nodo siguiente y otro para el nodo previo. Si agregamos otro nodo, se hara exactamente lo mismo pero ahora tenemos algunos cambios. El primer nodo ahora en su campo relacionado al siguiente item apuntara al siguiente elemento, y en el segundo nodo el campo relacionado al previo elemento apuntara al valor del primer nodo. Y a medida, que vayamos agregando mas nodos o valores estos se iran interconectando de esa manera. Pero una cosa interesante sucede si hacemos lo siguiente:
int main()
{
LinkedList<int> lista;
lista.agregar(19);
int* otro = new int(129);
lista.agregar(22);
}
Si agregamos un nuevo objeto con un valor antes de agregar un nuevo nodo a la lista. Este quedara asignado entre los dos nodos. Si bien esto no afectara a la operabilidad de la lista, si afecta a la memoria cache al no tener una secuencialidad, y por ende se lo denomina como not-cache-friendly.
Si bien podemos implementar herramientas para solucionarlo, eso nos hace perder el horizonte de la implementacion. Por eso se pone mucho enfasis en la seleccion correcta de una estructura de datos para la tarea que debe realizar esa seccion del codigo. Pero en la vida real, es muy raro que desarrollemos nuestros propios vector y listas enlazadas sino mas bien que se tienden a utilizar librerias estandar y bien testeadas para C++ sobre estos, como se dice habitualmente no es necesario reinventar la polvora.
En resumen, hoy hemos visto la teoria sobre como actua el contenedor en memoria, sus particularidades, ,los pros y contra de vector y listas enlazadas, asi como tambien se comportan en memoria. Espero les haya resultado de utilidad sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.


Donatión
It’s for site maintenance, thanks!
$1.50
