Practica4

=Práctica 4: Memoria dinámica en C++=

**Introducción: La clase vector**
Ahora vamos crear una clase que llamaremos vector. La clase vector representa un vector genérico de elementos que será creado usando plantillas. El vector pertenecerá al espacio de nombres storage.

Veamos el codigo vector.h

code format="cpp" using namespace std; namespace storage { template class Vector { public: /**Empty constructor */   inline Vector; /**Parametrized constructor */   inline Vector(unsigned int size); /**Copy constructor */   inline Vector(const Vector & V); /**Destructor */   inline ~Vector; /**Assign operator */   inline Vector  & operator=(const Vector &V);
 * 1) ifndef _Vector_H_
 * 2) define _Vector_H_
 * 3) include
 * 4) include

/**Resizes the vector */   inline void resize(unsigned int size); /**Returns the vector size */   inline int size const; /**Returns the i-th element */   inline Type & operator[](unsigned int i);

private: Type *_data; unsigned int _size; }; ////////////////////////// // ////////////////////////// template Vector::Vector { _data=NULL; _size=0; }

////////////////////////// // ////////////////////////// template Vector::Vector(unsigned int size){ _size=size; _data=new Type[size]; } ////////////////////////// // ////////////////////////// template Vector::Vector(const Vector & V){ _size=V.size; _data=new Type [_size]; for(unsigned int i=0;i<_size;i++) _data[i]=V._data[i]; }

////////////////////////// // ////////////////////////// template Vector::~Vector{ if (_data!=NULL) delete []_data; }

////////////////////////// // ////////////////////////// template Vector & Vector::operator=(const Vector &V) { if (_data!=NULL) delete []_data; _size=V.size; _data=new Type[_size]; for(unsigned int i=0;i<_size;i++) _data[i]=V._data[i]; return *this; }

////////////////////////// // ////////////////////////// template void Vector::resize(unsigned int size) { if (_data!=NULL) delete [] _data; _size=size; _data=new Type[_size]; } ////////////////////////// // ////////////////////////// template Type & Vector::operator[](unsigned int i) { assert(i>=0 && i<_size); return _data[i]; }

////////////////////////// // ////////////////////////// template<class Type> int Vector<Type>::size const { return _size; } };

code
 * 1) endif

Veamos que hay aquí. Lo primero es que tenemos que la clase tiene 2 variables privadas. La primera es _size que indica el número de elementos del vector. Después tenemos _data, que será el puntero a la zona de memoria donde se almacenan los datos. la clase tiene los tres constructores típicos. Fijaros que en caso del constructor vacío, el puntero se pone a NULL.

Lo primero que salta a la vista es la directiva //new//. Bien, esta es la forma en que se reserva memoria dinámica en C++. Fijaros bien en la sintaxis:

code format="cpp" tipo_dato *var=new tipo_de_dato [ numero_elementos] ;

code

Ejemplos:

code format="cpp" int *v=new int [10];// vector de 10 elementos v[0]=v[1]=1; float *f=new float[30]; //vector float de 30 elementos f[0]=f[1]=12;

code

Lo bueno de //new//, es que el calcula el tamaño de la memoria por nosotros. En realidad, el código de arriba se traduce en C en:

code format="cpp" int *v=(int*)malloc(sizeof(int)*10); //vector de 10 elementos v[0]=v[1]=1; float *f=(float*)malloc(sizeof(float)*30);// vector float de 30 elementos f[0]=f[1]=12; code

En el caso de nuestro vector:

code format="cpp" _data=new Type[n]; code

la palabra Type será sustituida por el tipo correspondiente en tiempo de compilación.

Bueno, tenemos algo nuevo ~Vector. Esto es lo que se llama un destructor. El destructor es la función a la que se llama cuando se debe destruir el objeto. Es decir, la función a la que se llama cuando ya no se va a usar el objeto nunca más. Claro, el objeto tendrá entonces que liberar la memoria que ha creado. El destructor es una función a la que llama el compilador por nosotros cuando detecta que el objeto no se va a usar más. **No tenemos que llamar al destructor, el compilador lo hace por nosotros.** Daros cuenta que en la clase Vector, el destructor lo único que hace es llamar a //delete//**.** La función //delete// es la forma en que se libera la memoria que ha sido creada con //new//. Es por tanto el equivalente al //free// de C.

Otra cosa nueva, tenemos un nuevo operador, el operador []. Fijaros que su uso es como el de los otros operadores que hemos visto. En este caso, el operador hemos dicho que devuelve un Type &, es decir devuelve la referencia al i-ésimo elemento. Por ello, podemos usar este operador para modificar los elementos del vector.

Y ahí no acaba la cosa. Quiero que os fijéis en la implementación de las funciones. Como dijimos en el capitulo anterior, teníamos programación en linea y programación en linea implícita. En este caso hemos optado por la primera opción. En el caso de usar plantillas, la sintaxis se complica un poco. Hay que poner //template<class Type>// al principio de la función, y después, cada vez que aparezca el nombre de la clase, hay que poner //<Type>//, es decir Vector//<Type>//. Si hubiésemos elegido la programación en línea implícita, hubiese sido mas fácil (pero no hubiésemos aprendido esto :))

Por último, vemos que la función size tiene el cualificador const. Esto sirve para indicar que la llamada a la función no modifica al objeto. Esto tendrá la utilidad de poder llamar a esta función cuando el objeto sea pasado como constante. Ejemplo: code format="cpp" class Test { Test(const Test &T) {   _size=getSize; //Error _size=getSize2; //Ok }

void getSize{return _size;}

void getSize2 const {return _size;}

private: int _size; }; code En el caso de llamar a getSize, el compilador dará un error indicando que la función no es constante. Eso es porque T se ha declarado como const, por tanto, solo se podrá llamar a funciones que se hayan declarado como constantes.

Finalmente vamos a ver un ejemplo de funcionamiento. Creamos el main.cpp

code format="cpp" using namespace std;
 * 1) include <vector.h>
 * 2) include
 * 3) include

int main {

storage::Vector v; v.resize(10); for(unsigned int i=0;i<v.size;i++) v[i]=i;

storage::Vector v2=v; for(unsigned int i=0;i<v.size;i++) assert(v[i]==v2[i]);

cout<<"Perfect"<<endl;

v.resize(9); cout<<"Now, it should abort the program"<<endl; v[9]=10; cout<<"If you are here, then there is a problem in the code"<<endl; } code

Una cosa que nos llama la atención aquí, es que no hemos declarado el espacio de nombres storage como //using namespace storage//. En lugar de eso, le hemos indicado al compilador al declarar la clase que el objeto pertenece al espacio de nombres como //storage::Vector //. De esta forma, estamos aclarándole al compilador dónde esta la clase.


 * 1.2 Rizando el rizo**

Bueno, ahora un poquito más. Coged la clase Float de la práctica anterior y usad el siguiente código: code format="cpp" using namespace values; using namespace std;
 * 1) include <vector.h>
 * 2) include
 * 3) include
 * 4) include <number.h>

int main {

storage::Vector<Float> v; v.resize(10); for(unsigned int i=0;i<v.size;i++) v[i]=i;

storage::Vector<Float> v2=v; for(unsigned int i=0;i<v.size;i++) assert(v[i]==v2[i]);

cout<<"Perfect"<<endl;

v.resize(9); cout<<"Now, it should abort the program"<<endl; v[9]=10; cout<<"If you are here, then there is any problem in the code"<<endl; } code

Vaya lio! Pues resulta que el tipo puede ser a su vez una clase. Pero aun podemos complicarlo más. Coged la clase Number y compilar este código:

code format="cpp" using namespace values; using namespace std;
 * 1) include <vector.h>
 * 2) include
 * 3) include
 * 4) include <number.h>

int main {

storage::Vector<Number > v; v.resize(10); for(unsigned int i=0;i<v.size;i++) v[i]=i;

storage::Vector<Number > v2=v; for(unsigned int i=0;i<v.size;i++) assert(v[i]==v2[i]);

cout<<"Perfect"<<endl;

v.resize(9); cout<<"Now, it should abort the program"<<endl; v[9]=10; cout<<"If you are here, then there is any problem in the code"<<endl; } code

Fijaros que si vamos a usar un template que usa otro template, se pone un espacio > entre los >. Es decir storage::Vector<Number >.

**2.1 Creación de objetos**
Hasta el momento hemos creado objetos siempre de forma estática. Es decir, declaraciones del tipo

code format="cpp" main{ Integer I; } code

Pero hay ocasiones en que yo puedo necesitar manejar memoria dinámica. Vea el siguiente ejemplo code format="cpp" main{ Float *F=new Float; } code

Con esto, estoy creado un de forma dinámica un objeto de tipo Float. El operador //new// se encargará de llamar al constructor. En este caso al vacío. Aunque también podríamos haber escrito. code format="cpp" main { Float *F=new Float(10); }

code

para llamar al parametrizado.

Ahora bien, cuando se crea una objeto de forma dinámica, hay cambios a la hora de llamar a sus funciones miembro. code format="cpp" main { Float *F=new Float(10); cout<<F->getValue<<endl; } code

En lugar de usar el operador (.), cuando lo que tenemos es un puntero a un objeto (Float*), usaremos el operador ->. Esta es una de las novedades de C++ y que ya veremos que nos dará una gran potencia de rehusabilidad.

Ahora bien, no debemos confundir estas dos cosas code format="cpp" main { Float *F=new Float(10); cout<<F->getValue<<endl; Float *VF=new Float[10]; for(unsigned int i=0;i<10;i++) cout<<VF[i].getValue<<endl; } code

En el primer caso, F es un puntero que apunta a un único objeto, y por tanto se usa el operador ->. En el segundo caso VF es un puntero que apunta a un array de objetos de tipo Float. Por tanto, para acceder al i-esimo elemento usando el operador []. Por tanto VF[i] ya no es un puntero si no el objeto en si mismo, por ello, se usa el operador (.). Es decir, VF[i].getValue.

Pero fijémonos en otra cosa, cuando yo hago new Float[10]. Estoy creando un vector de 10 elementos. Por defecto, el compilador llama al constructor vacío. El problema es que no podremos llamar a ningún otro constructor.

Pero en realidad F y VF no son tan distintos, podríamos hacer lo siguiente code format="cpp" main { Float *F=new Float(10); cout<<F->getValue<<endl; cout<<F[0].getValue<<endl; cout<<(*F).getValue<<endl; } code

En el primer caso usamos la notación ->. En el segundo, accedemos al primer elemento apuntado por F, es decir, el único. En el tercer caso, *F convierte un Float * en un Float, y por tanto podemos usar el operador (.).


 * 2.2 Paso de objetos dinámicos a funciones**

Una vez que tengo creado mi objeto de forma dinámica como lo paso a otra función. Muy simple

code format="cpp" void modifica (Float *F,float v) { F->setValue(v); }

main { Float *F=new Float(10); modifica(F,11); cout<<F->getValue<<endl; delete []F; }

code

En el ejemplo, la función modifica recibe el puntero a un objeto de la clase y lo modifica en el interior. Otra forma alternativa de escribir la función hubiese sido code format="cpp" void modifica (Float *F,float v) { (*F).setValue(v); } code

usando el operador (.) en lugar del ->.

Pero vamos un poco mas allá, podríamos tener la función code format="cpp" void modifica (Float &F,float v) { F.setValue(v); }

code

en este caso, se esta usando la notación de paso por referencia de C++. Como vemos, es posible intercambiar las notaciones de C y C++. Para poder llamar a la función, tendríamos que haber escrito main code format="cpp" { Float *F=new Float(10); modifica(*F,11); cout<<F->getValue<<endl; } code

y obtendremos el mismo resultado.

**2.3 Destrucción de objetos dinámicos**
__**Cuando acabemos de usar nuestro objeto dinámico, no debemos olvidarnos de destruirlo**__. Para ello, usaremos delete que se encargara de llamar automáticamente al destructor en caso de estar definido. Es importante indicar que si el constructor no esta definido, no se le podrá llamar.

Veamos con un ejemplo este tema

code format="cpp" using namespace values; using namespace std;
 * 1) include <vector.h>
 * 2) include
 * 3) include
 * 4) include <number.h>

int main {

storage::Vector<Number > *v=new storage::Vector<Number >; v->resize(10);

for(unsigned int i=0;i<v->size;i++) (*v)[i]=i; delete [] v; } code

En el ejemplo, creamos usando new un objeto de la clase vector. Después, lo recorremos para rellenar sus valores. El caso es que para poder usar los operadores, no podemos hacerlo con punteros. Es decir, v->[] no es algo valido. Por tanto, con (*v) es un Number y ya puedo usarlo.

Finalmente, uso //delete// para borrar el objeto. Éste a su vez llama al destructor que libera la memoria del vector.

**3. Ejercicio**
Como ejercicio, debéis crear la clase Matrix. La clase Matrix será una plantilla. Tendrá los siguientes métodos: a) constructor vacío b) constructor de copia. c) constructor parametrizado. Recibe dos enteros sin signo, número de filas (nRows) y número de columnas (nCols). d) resize. Función que cambia el tamaño de la matriz. Esta función recibe dos enteros sin signo (nRows y nCols). Os aconsejo, que no repitáis código y que el constructor parametrizado llame a esta función. e) unsigned int getNRows indica el numero de filas f) iunsigned nt getNCols indica el numero de columnas g) Type & get(unsigned int r,unsigned int c); Función de acceso que retorna el elemento (r,c) de la matriz. En este caso, no podremos usar el operador []. Llamaremos a esta función. La función comprobará si el elemento existe. En caso negativo, aborta el programa. h) void setIdentity; Esta función hace que la matriz sea la identidad. es decir, todo a cero excepto los elementos de la diagonal principal que estarán a uno. i) Operador de asignación. j) Operador de suma. Realiza la suma de dos matrices y retorna el resultado. La función se encargará de comprobar que es posible realizar la operación. En caso de no ser posible se abortará el programa. k) destructor

Ayuda: Os voy a indicar aquí cómo se reserva memoria para una matriz y cómo se elimina.

Crear una matriz dinámica de nxm

code format="cpp" double **matrix=new double*[n]; for(int i=0;i<n;i++) matrix[i]=new double[m]; code

Eliminar la matriz se hace al revés:

code format="cpp" for(int i=0;i<n;i++) delete [] matrix[i]; delete [] matrix; code

Para probar el código, debéis hacer funcionar con éxito el siguiente programa.

code format="cpp" using namespace std; using namespace storage;
 * 1) include <matrix.h>
 * 2) include

//template Function that prints a matrix

template<class Type> void print(Matrix<Type> &M) { for(unsigned int i=0;i<M.getNRows;i++){ for(unsigned int j=0;j<M.getNCols;j++) cout<<M.get(i,j)<< " "; cout<<endl; } }

int main(int argc,char **argv) { Matrix M; M.resize(10,10); M.setIdentity; print(M); Matrix M2(M); print(M2); Matrix M3(10,10); M3=M+M2; print(M3); //check the result for(unsigned int i=0;i<M3.getNRows;i++){ for(unsigned int j=0;j<M.getNCols;j++) if (i!=j) assert(M3.get(i,j)==0); else assert(M3.get(i,j)==2); } cout<<"Perfect"<<endl; } code