viernes, 15 de julio de 2016

Cuándo es correcto usar reserva dinámica de memoria

Todo libro, tutorial o curso, cubre de sobra el aspecto de reserva dinámica de memoria. Es una de las características principales del lenguaje C++ y la que le da la categoría de lenguaje de bajo nivel. Si bien es cierto que se explica la sintaxis para usarla, en los recursos que he utilizado a lo largo de los años nunca he encontrado nada que hable no sólo del cómo sino del cuando es correcto el uso de memoria dinámica.

Es cierto que con la experiencia uno va viendo a base de prueba-error cuando es útil realmente usar este tipo de reserva de memoria, y también es cierto que si el recurso con el que aprendiste el lenguaje es medianamente bueno, te harás una buena idea de cuando tiene sentido utilizar reserva dinámica de memoria y cuando no. En cualquier caso, aquí está mi opinión.

Podemos considerar que una de las propiedades que nos ayudarán a decidir cuando usar reserva dinámica o no, es la duración de almacenamiento de la variable en cuestión. Pongamos que tenemos una clase predefinida, y queremos obtener una instancia de la misma. Tenemos dos maneras de obtener dicha instancia:

MyClass obj;
MyClass *obj = new MyClass;

En la primera de ellas, el objeto obj se crea con una duración de almacenamiento que viene fijada por el propio compilador, es decir existirá hasta que se destruya, lo que ocurrirá cuando nos salgamos del scope donde este objeto ha sido declarado.

En cambio cuando instanciamos un objeto como en el segundo caso, el objeto tiene reserva dinámica de memoria lo que significa que existirá hasta que nosotros decidamos cuando con la palabra reservada delete. De aquí podemos deducir una norma no escrita en piedra que podremos utilizar para mantenernos cuerdos a la hora de utilizar variables, utiliza siempre que puedas la reserva automática de memoria.

Hay muchas situaciones en las que sin embargo necesitamos utilizar reserva dinámica de memoria:

  • Cuando trabajamos con variables que se encuentran fuera de nuestro scope y queremos acceder a ellas. En este caso en el que no queremos una copia, sino los valores originales ya que es posible que queramos cambiarlos, utilizaremos reserva dinámica de memoria.
  • Uno de los motivos principales es la necesidad de reservar una cantidad de memoria considerable que puede que llene la pila de memoria. Esto no debería ocurrir a no ser que estemos trabajando con sistemas un tanto diferentes y hay que tenerlo en cuenta.

En cualquier caso, a partir del estandar C++11 es aconsejable utilizar punteros inteligentes, std::unique_ptr, std::shared_ptr ya que ellos mismos manejan cuando se destruyen en función de cuando dejan de usarse haciendo que la memoria se maneje de una forma negligente. Mi opinión siempre es, plantear el puntero normal y cuando se liberará memoria (new y delete) y luego considerar si en algún momento uno de los punteros inteligentes puede aportar algo más.

REFERENCIAS

std::unique_ptr

std::shared_ptr

jueves, 14 de julio de 2016

Pasar argumentos por valor, referencia o puntero

Pasar por valor un argumento

Esta es la forma en la que los argumentos se pasan por defecto en C++. Lo que hacemos cada vez es que pasamos una copia a la función del argumento, lo que significa que el valor original no será  modificado.

void birthDate(int _day, int _month, int _year);

La principal desventaja que tiene, es que para valores de tamaño considerable no es una opción económica, y más si la función se llama varias veces. En la función anterior no habría problema, aunque no es la forma óptima de hacerlo. En cambio con la función siguiente la cosa cambia:

struct userData {
    int marks[100];
};
void calcAverage(userData _dataStruct);

Cada vez que se pase por argumento la estructura, se copiará un array de tamaño 100, que si se usa muchas veces, puede llegar a ser muy lento, y puesto que los datos de la estructura no van a cambiarse, debería de poder hacerse de otra manera.

Pasar por referencia un argumento:

Centrémonos en uno de los aspectos que más luz arroja acerca de lo que significa pasar un valor por referencia. Cuando pasamos un valor por copia como en el caso anterior, la única forma de devolver un valor es utilizando el return en la función, ya que al recibir una copia, seguramente tengamos que crear un elemento del mismo tipo que la función recibe para almacenar los cambios y después devolverlos.

Pero que sucede si lo que realmente necesito es cambiar el valor de una variable, y no hacer algo con ella. No tiene mucho sentido recibir una copia, hacer cambios y luego sobreescribir dicha variable por ejemplo de esta manera:

//main.cpp

struct Alum{
    double mark;
};
double updateMarksBonus(Alum _al, double _bonus){
    double newMark = _al.mark + _bonus;
    return newMark; 
}
int main() {
    Alum al;
    al.mark= 8.54;
    al.mark = updateMarksBonus(al, 1.4);
    cout << al.mark << endl;
    return 0;
}

Esto no tiene mucho sentido cuando pasando el valor por referencia puedo cambiar el valor de la estructura directamente de la siguiente manera:

struct Alum{
    double mark;
};
//----------------------------------------------------------
void updateMarksBonusByRef(Alum& _al, double _bonus){
    _al.mark+=_bonus;
}
//----------------------------------------------------------
int main() {
    Alum al;
    al.mark= 8.54;
    updateMarksBonusByRef(al, 1.4);
    cout << al.mark << endl;
    return 0;
}

Hay que tener cuidado, la capacidad de poder cambiar el valor original puede poner en peligro nuestro programa, ya que quizás ese valor no necesita ser cambiado, para poder utilizar valores pasados por referencia y asegurarnos de que ese valor está protegido y no va a ser cambiado, utilizaremos const references, que no es más que pasar el valor a la función y declara que ese valor que se pasa será constante:

void foo(const int& _x);

Pasar por dirección un argumento

En lugar de pasar una copia, o una referencia, pasamos un puntero. Cuando queremos acceder a grandes datos tipo arrays y queremos hacer consultas o incluso cambiar parámetros podemos utilizar esta forma de pasar valores:

void updateArray(int* _arrayPtr, int _size);

Esto es todo.