lunes, 10 de marzo de 2014

Hace unos meses que entregué unas prácticas finales de PE, mi profesora me preguntó: ¿tuvo problemas con alguna? –Sí, con las matrices, ya que no pude hacer una función que las devolviera, le contesté. –¿Y cómo lo resolvió?, dijo ella….
Yo esperaba con mi respuesta que la profesora me explicara un método para hacerlo, o que me dijera que luego me enviaría la solución, pero muy por el contrario ella muy segura respondió dando a entender que yo podría resolverlo “solo”. Y fue una de las más grandes lecciones de carrera que me han dado en mi vida. Desde entonces he buscado un ¿por qué mi código compilaba pero no era correcto su resultado?, y hallé artículos que abordaban (si no mi tema en cuestión) si ejemplos parecidos y eso me fue acercando a la solución, que ayer en un chispazo de conexión entre mis dos neuronas he obtenido. Se las presento.

¿Qué necesito saber para comprender este tutorial?
Manejo básico de punteros y referencias. Tener bien en claro lo que significa para el compilador cada uno ya que no es mi propósito explicarlo en este capítulo.
Nivel de dificultad: Básico Alto.

¿Es posible que una función devuelva una matriz?
Con datos estáticos (no creados en tiempo de ejecución) la respuesta es NO.

¿Por qué no, si es posible enviar un vector como parámetro a una función?
Enviar es posible ya sea por valor o referencia, pero esto no obliga a que se pueda devolver, es decir, la función es capaz de recibir un vector, operar con él y si uno quiere imprimirlo en pantalla, y puede regresarlo a la función llamadora. Pero no puede hacerlo igual con un arreglo de vectores, y eso es  una matriz en realidad.

Mostraré por qué.

Es importante tener muy en claro que las variables creadas en una función secundaria son de alcance solamente local y una vez que la operación del programa sale de la función estos espacios de memoria son liberados.

¿Por qué el siguiente código compila pero no funciona correctamente?

void main (void)
{
int **Asignamatriz(void);
int fil=4, col=4;
int **q;

//El puntero a puntero recibe lo devuelto por la función
q=Asignamatriz();

//Imprime la matriz que se recibe como argumento.
for (int i=0; i<fil; i++)
{
for(int j=0;j<col; j++)
cout<<q[i][j]<<" ";
cout <<endl;
}

}

int **Asignamatriz(void)
{
//Matriz 4x4
int matriz[4][4];
//Número de filas y columnas
int col=4, fil=4;
//Valor que asignaré a cada elemento de la matriz
int valor=0;
//Vector de punteros
int *p[4];
//Puntero a puntero
int **q;

//Lleno la matriz con valores
for (int i=0; i<fil; i++)
for(int j=0;j<col; j++)
matriz[i][j]=valor++; //int valor=0

//Cada elemento del vector "p" apunta a una fila de la matriz
for (int i=0; i<4;i++)
p[i]=matriz[i];

//El puntero "q" apunta al vector "p"
q=p;
return q;
}

Ya hemos dicho que las funciones sólo pueden retornar un valor a la vez, o un vector (uno sólo), pero no más. “q” apunta a un arreglo de vectores, por tanto “q” en su dirección de memoria asignada apunta al primer elemento de éstos, y puede acceder a los subsecuentes tres cambiándole de índice, lo que es válido para C++, ¿pero qué contienen en sus direcciones de memoria estos elementos subsecuentes? La respuesta son direcciones de memoria de los elementos de cada fila….  ¿ya notaste el “error”?
Cuando la función devuelve el puntero a un puntero (su tipo de retorno) lo hace correctamente, devuelve un puntero a  punteros de las direcciones de memoria pertenecientes a las filas, ¡y la función destruye (libera memoria) las demás variables (un puntero lo es también)! , por lo que ha liberado los espacios de memoria que contenían los elementos de cada fila…. Y a lo que apunta ahora el valor devuelto de la función es a lagunas de memoria, o “basura” como se le conoce.
Concluimos que no podemos devolver variables (estáticas) locales de una función, porque éstas se destruyen al salir de ella, algo que seguramente ya sabíamos pero que habíamos perdido de vista trabajando con matrices y punteros.
La memoria que es reservada dentro de una función secundaria (como la trabajada aquí) se libera pero no vuelve a ser usada, se “desperdicia”, por tanto tampoco es muy eficiente.
¿Has cachado ahora la solución? Reservar memoria en la función principal, y usar esos espacios de memoria para guardar y modificar la matriz.


¿Cómo hacerlo entonces?

Ahora sí, ¡hagámoslo!

-Tenemos una matriz de elementos que es de ámbito de función principal.
       int matriz[4][4];
Dividamos mentalmente esa matriz ya no en elementos sino en filas.

- Creamos un arreglo de punteros
       int *p[4];
Cada elemento del arreglo de punteros debe apuntar a cada fila de la matriz

                 for (int i=0; i<4;i++)
            p[i]=matriz[i];

-Creamos un puntero a puntero
       int **q;
Este puntero debe apuntar al puntero anterior.
       q=p;

-Declaramos la función prototipo que se encargará de operar la matriz, en este caso sumará en uno cada elemento.

       void Asignamatriz(int **&matriz,int fil, int col);

La función espera como primer argumento la referencia (dirección en memoria) de un puntero a puntero, ¿por qué? Porque crea un alias a una matriz ¡que ya existe en memoria!, en este caso asignada en la función principal (main). Entonces actuará directamente sobre ella, modificando los valores con alcance en la función llamadora. Además espera el número de filas y columnas de la matriz ya que C++ no lo determina automáticamente.

Este es el truco primordial. Analiza bien la sentencia.

-Defino la función Asignamatriz

void Asignamatriz(int **&matriz, int fil, int col)
{
for (int i=0; i<fil; i++)
for(int j=0;j<col; j++)
matriz[i][j]++;
}

matriz es un alias de una matriz que ya existe, por tanto lo que yo haga con sus elementos es de alcance no local sino de la función que la ha llamado, es decir en donde fue declarada la matriz pasada como argumento.

-Ahora así empleo la función Asignamatriz
      
Asignamatriz(q,fil,col);

¿Por qué paso “q” como argumento?
Porque es un puntero a un puntero, tal como lo he dicho en la función prototipo, y éste apunta a un vector de punteros, o dicho con otras palabras, “q” apunta a las filas de la matriz.

- Puedo crear otra función que utiliza esa matriz, pero que no modifica sus valores, sino que recibe una copia de ésta y solo ejecuta una operación como imprimirla en pantalla:

void Imprimematriz(int**matriz, int fil, int col)
{
//Imprime la matriz que se recibe como argumento.
for (int i=0; i<fil; i++)
{
for(int j=0;j<col; j++)
cout<<matriz[i][j]<<" ";
cout <<endl;
}
}

En este caso recibe, también, un puntero a un puntero pero sus valore son pasados como una “copia”, por tanto no son los originales y si yo modifico los elementos su alcance es local y no se reflejarán en la función llamadora.


VENTAJAS DE ESTE MÉTODO


Al emplearlo logramos que nuestras funciones estén diseñadas para realizar una operación a la vez. Ya no estamos obligados a operar la matriz y a imprimirla en pantalla o realizar una segunda operación en la misma función, ahora podemos separar responsabilidades, punto básico en la Programación Orientada a Objetos. Y si has comprendido la idea esencial de la solución te será muy fácil adaptarlo a un código POO.

ACLARACIONES

No es ésta la única forma de trabajar con funciones que realicen esta operación, ya que con matrices dinámicas esto también es posible, o usando variables de tipo “static” pero ya se los contaré en el siguiente capítulo.

Adjunto elcódigo .cpp (C++), y si tienen cualquier pregunta no duden en postearla en los comentarios, con todo gusto les responderé a la brevedad ( o buscaremos juntos la solución).

¡Los leo!




tags: ¿cómo devolver una matriz en C# C++ , Visual Studio, Dev C++, Borland? matrices con punteros y referencias, función que devuelve una matriz de numero enteros, como hacer una que una funcion devuelva matrices, arreglo de vectores, vectores con punteros.

8 comentarios :

  1. Respuestas
    1. Hola Karla =D
      Está en la parte final del post el código .cpp en el compilador Dev, por si te sirve.
      Cualquier cosa estoy para ayudarte, por cierto bienvenida a mi pequueño blog =)

      Eliminar
  2. Muchas gracias carlos, estaba estancado en este problema desde hace varias semanas, y ya habia recorrido muchos foros en ingles y en español, pero en ninguno de estos explicaban paso a paso el por que de las cosas.

    ResponderEliminar
  3. Muchas gracias amigo. Me sirvio mucho tu explicacion. Estaba buscando como ahcerlo para mi proyecto y aqui lo encontre. Muchas gracias.

    ResponderEliminar
  4. Yo uso Code Blocks, EXCELENTE EJEMPLO, me sirvió mucho! Muchisimas Gracias!

    ResponderEliminar
  5. excelente, muchas gracias. Estuve más de una hora tratando de aprender como usar el puntero que regreso con un return al terminar una función.

    ResponderEliminar
  6. y si quisiera sumar dos matrices y me devuelva una tercer matris ?

    ResponderEliminar
    Respuestas
    1. Hola
      Aquí está la solución para retornar una matriz, el otro problema que tienes es para sumar dos matrices, bueno suponiendo que ambas las recibas como matrices. En vez de un solo alias matriz, necesitas dos. Y luego sumar, bueno eso es cosa de niños solo recorre las matrices y las sumas guardando este resultado en una tercera matriz que puedes retornar.

      void Imprimematriz(int**matriz, int fil, int col)
      {
      //Imprime la matriz que se recibe como argumento.
      for (int i=0; i<fil; i++)
      {
      for(int j=0;j<col; j++)
      cout<<matriz[i][j]<<" ";
      cout <<endl;
      }
      }

      Eliminar