Aula Macedonia


Curso de Programación Orientada a Objetos en C++


Artículo realizado por
Fernando Rodríguez.







Capítulo 4.
La piedra angular del C++: las clases.

Con este tercer capítulo del curso sobre C++, empezaremos a ver las clases, auténticas piedras angulares para la programación en C++. Mediante las clases, como veremos más adelante, podremos valernos de una de las propiedades más importantes y fundamental de toda programación orientada a objetos, esto es, la encapsulación. Recordemos que la encapsulación hacía referencia a la capacidad de mezclar datos (variables, constantes, etc) junto a funciones (a partir de ahora, llamaremos a estas funciones métodos), de tal forma que los métodos trabajen con los datos.

Introducción: creando una clase

Una clase la podemos entender como una estructura en la que, además de poder incorporar variables, también se puede hacer lo propio con funciones. Supongamos la siguiente estructura que utiliza el mecanismo de la encapsulación:


struct Guerrero {
	char *m_nombre;
	int m_fuerza;
	int m_resistencia;
	int m_experiencia;
	void AsignaNombre (char *nombre);
	void IncrementaExperiencia (int tipo_atributo);
	void MuestraDatosPersonaje (void);
};

Como podéis observar, la estructura guerrero sigue las normas básicas de toda estructura que conocemos del C tradicional salvo, claro está, esas tres funciones. Sin embargo, pese a que es posible definir estructuras de este modo en C++ y comenzar a trabajar con objetos, en la práctica no se utiliza struct de este modo, es decir, para definir estructuras como las de arriba en las que se mezclan variables con funciones de miembro se utilizan las clases. Para definir una clase no tenemos que ir muy lejos, basta con cambiar la palabra struct por class y ya lo habremos logrado, mirad:


class Guerrero {
	char *m_nombre;
	int m_fuerza;
	int m_resistencia;
	int m_experiencia;
	void AsignaNombre (char *nombre);
	void IncrementaExperiencia (int tipo_atributo);
	void MuestraDatosPersonaje (void);
};

El uso de clases frente a estructuras radica en que con las primeras podemos utilizar mecanismos tan vitales y fundamentales como la herencia y otras importantes propiedades de la orientación a objetos.

Pasando a analizar más de cerca la clase guerrero definida más arriba, veremos enseguida que hay un total de cuatro variables de miembro. Dichas variables de miembro llevan todas una "m" seguida de un subrayado esto es así porque se suele utilizar este convenio para distinguir variables que son de miembro, como es el caso, de variables que no pertenecen a una clase (por ejemplo una variable cualquiera definida en el main). Puede que todos estos detalles os sean molestos. Podéis prescindir perfectamente de ellos pero yo os recomiendo que seáis siempre ordenados. Pensad que si no estáis aún metidos en la programación en Windows, la colaboración para realizar programas en equipo no la habéis vivido muy de cerca pero, tarde o temprano, os tocará tener que trabajar con un equipo estable de personas o código que no es vuestro, por eso, es muy importante aprender a construir código que no sólo funcione (y además lo haga bien) sino que sea entendible por otros.

Volviendo al análisis de la clase guerrero, veíamos como definíamos cuatro variables pero, además, también insertábamos la definición de tres métodos que eran:

Como veremos más adelante será con estos métodos con los que realizaremos el trabajo de nuestras variables anteriormente creadas. Se acabó eso de tratar las variables por separado, ahora, para una simple inserción deberemos de utilizar una función que lo haga.

Cuando declaramos una clase no se reservará memoria hasta que creemos un objeto de la clase, la creación de un objeto de una clase se llamará instanciar un objeto y, a su vez, un objeto creado de una clase se denomina instancia de esa clase.

Releer si no habéis captado todo porque será importante tener las cosas más o menos claras a partir de ahora.

Protegiendo el acceso a la clase

Un aspecto muy importante de la programación orientada a objetos es el tema de la seguridad y el control de acceso a los miembros de datos de la clase. Para poder definir en qué medida las variables y funciones miembro pueden ser accedidas, vamos a poder establecer unas prioridades de acceso. Con este aspecto vamos a erradicar, entre otras cosas, los problemas que nos solían ocasionar las variables globales del C tradicional, esto es, efectos laterales, cambios inadvertidos de valor, etc.

Existen tres especificadores de acceso posibles que son public, protected y private. Antes de ver un ejemplo de cada uno de ellos, citaremos que cuando se declara público (public) un miembro de una clase, tanto si es función como si es variable, este se podrá acceder desde dentro de la propia clase como desde fuera de ella. Cuando declaramos miembros protegidos (protected) nos aseguramos que sólo serán accesibles desde las funciones de miembro de la clase siendo imposible el acceso tanto desde otras clases como desde cualquier otra parte del programa. Finalmente, un especificador privado (private), no permite que las clases sean accedidas ni desde otras clases, ni desde otras partes del programa ni desde clases derivadas (que ya las veremos más adelante).

Funciones de miembro intercaladas o en línea.

Antes de pasar al primer "gran" ejemplo de este capítulo, veremos qué son las funciones de miembro intercaladas o en línea. Las funciones intercaladas, no son más que las funciones de miembro definidas en la clase y que, además, también contienen su código en la misma clase. Estas funciones se caracterizan porque ocupan muy pocas líneas de código de tal manera que, para evitar la sobrecarga computacional que puede ser la llamada a una función cuando realmente el código a ejecutar es mínimo, caso de estas funciones, se permite que dichas funciones o métodos se "inserten" en todos aquellos lugares en los que son necesitadas, creándose una copia de ellas mismas dentro del código en las zonas en las que se necesitan. Con esto ganamos en rapidez pero también tamaño en nuestros programas ejecutables ya que vamos a tener repetidas copias en el ejecutable de la función en cuestión. De todos modos, merece la pena hacer el ejecutable final un poco más grande si con ello ganamos en rapidez, por ello, es aceptable utilizar las funciones intercaladas.

Un típico ejemplo de función intercalada podría ser aquella que en una clase se dedica a dar valor a una variable de miembro, es decir, aquella que se limita a realizar una asignación. Recordando el ejemplo de la clase guerrero podríamos definir la función AsignaNombre de forma intercalada. Para llevar esto a cabo existen dos métodos. El primero consiste en realizar la definición y cuerpo de la función en la misma línea, dentro de la definición de la clase. La otra sería la utilización de la palabra reservada inline que no veremos hasta más adelante del curso. Por ahora nos bastará ver un breve ejemplo de cómo podría quedara nuestra clase guerrero si definiéramos como intercalada la función encargada de asignar un valor a la variable nombre:


class Guerrero {
   char *m_nombre;
   int m_fuerza;
   int m_resistencia;
   int m_experiencia;
   void AsignaNombre (char *nombre) {m_nombre = nombre;}
   void IncrementaExperiencia (int tipo_atributo);
   void MuestraDatosPersonaje (void);
};

Como podéis observar la función intercalada sigue todas las normas de definición de las funciones tradicionales, esto es, cabecera de la función y, a continuación, cuerpo de la misma encerrada entre llaves.

De este modo, ya vemos y confirmamos una serie de cosas muy importantes:

  • Las clases nos permiten encapsular datos (variables de miembro) y funciones para trabajar con esos datos (métodos).

  • Podemos definir métodos intercalados cuando el trabajo de los mismos es mínimo. Una forma de hacerlo consiste en escribir la cabecera y el cuerpo en la misma línea y dentro de la clase.

  • Debemos de trabajar con las variables de miembro mediante los métodos que creemos en la clase, es decir, hay que evitar hacer Nombre_Objeto.m_nombre = nombre, es decir, a menos que no estemos "ensayando" el acceso directo a las variables de una clase deberán de realizarse mediante los métodos adecuados para ello. Lo correcto, como digo, es definirnos una función de miembro que se encargue de esa tarea. En nuestro ejemplo de clase el método AsignaNombre trabaja con la variable de miembro m_nombre asignándole un valor.

Vamos a repasar todo con un ejemplo.

Antes de continuar escribiendo, vamos a realizar un ejemplo de implementación de una clase, creación de una instancia a dicha clase (creación de un objeto) y, por último, utilización de la clase a través del objeto creado.

El ejemplo será bien sencillo. Construiremos una clase guerrero en la que, como si se tratará de un juego de Rol (infinitamente primitivo...), seremos capaces de ir asignando valores a datos como la edad, la fuerza, la destreza, la energía o el nombre. Por supuesto, podremos ser capaces de volcar dicha información a pantalla. Después, podremos crear tantas instancias u objetos como queramos de ese tipo, tantos como guerreros queramos manejar... ¡Allá vamos!.


#include <iostream.h>

class Guerrero {
private:
 char *m_nombre;
 int m_edad;
 int m_fuerza;
 int m_destreza;
 int m_energía;

public:
 void MuestraDatos(void);
 void PonerNombre (char *nombre) {m_nombre = nombre;} 
 void PonerEdad (int edad) {m_edad = edad;} 
 void PonerFuerza (int fuerza) {m_fuerza = fuerza;} 
 void PonerDestreza(int destreza){m_destreza=destreza;} 
 void PonerEnergia (int energia) {m_energia = energia;} 
 char * DevuelveNombre (void) {return m_nombre;} 
 int DevuelveEdad (void) {return m_edad;} 
 int DevuelveFuerza(void) {return m_fuerza;} 
 int DevuelveDestreza(void) {return m_destreza;} 
 int DevuelveEnergia (void) {m_energia = energia;} 
 }; 
  
 void Guerrero::MuestraDatos(void) 
 { 
  cout << "Nombre del personaje: " << m_nombre << ‘\n’; 
  cout << "Edad: " << m_edad << ‘\n’; 
  cout << "Fuerza: " << m_fuerza << ‘\n’; 
  cout << "Destreza: " << m_nombre << ‘\n’; 
  cout << "Energía: " << m_energia << ‘\n’; 
 } 
  
 void main (void) 
 { 
   // Creamos un objeto o instancia llamado Conan  
   Guerrero Conan; 
  
   // Ahora le vamos dando valores al personaje 
  
   Conan.PonerNombre ("Conan"); 
   Conan.PonerEdad (23); 
   Conan.PonerFuerza (40); 
   Conan.PonerDestreza (20); 
   Conan.PonerEnergia(150); 
  
   // Y ahora vemos por pantalla el resultado 
  
   Conan.MostrarDatos(); 
 } 
 

En este corto ejemplo podemos ver ya el funcionamiento de la programación orientada a objetos desde su nivel más básico. En un primer lugar podemos distinguir la creación de la clase Guerrero. Apreciamos una parte privada en donde definimos las variables que sólo podrán ser accesibles por las funciones de la clase y una parte pública a la que sí se podrá acceder desde fuera (desde el main en este caso). Así, para poder asignar un valor no se nos permitirá hacer libremente algo como:

Conan.m_nombre = "Conan";

Si hacemos esto el compilador mostrará un error y no dejará que se acabe la compilación del ejemplo.

Para hacer tales cosas, disponemos de funciones definidas como intercaladas o en línea dentro de la propia clase. Esto es así porque como vimos en una sección más arriba, las funciones de asignación de datos a las variables, en este caso, son de una sencillez tal que merece la pena utilizar funciones en línea. Por otro lado, están declaradas como públicas con lo que, al llamarlas desde el main, no producen error alguno en tiempo de compilación. Otras funciones que también se definen pero no se utilizan son las referentes a la devolución de los datos de las variables de miembro. Realmente las he puesto para recalcar, aún más, que TODAS las operaciones con variables hay que realizarlas a través de funciones de miembro y que si, por ejemplo, sólo quisiera mostrar por pantalla la edad del guerrero creado pues debería de obtenerla utilizando la función creada a tal efecto (en este caso en concreto sería Conan.DevuelveEdad(); ).

En lo referente a la función void MostrarDatos(void); pues nos encontramos con el caso más usual, la declaración de funciones FUERA de la clase. Estas funciones serán las que más implementemos. Para su utilización debemos de seguir unos pasos muy sencillos. Sólo debemos de poner el nombre de la clase a la que pertenecen (en el ejemplo es la clase Guerrero) seguido y sin espacios de dos puntos :: . Esta pareja de dos puntos recibe el nombre de operador de resolución de ámbito y es la manera más usual para definir funciones de miembro fuera de la clase. Con ellos, el compilador sabe a qué clase corresponde la función que vamos a definir. Y ya para finalizar, después del operador de resolución de ámbito, pondremos el nombre de la función de miembro. Tenéis que tener en cuenta que, pese a que la función se implementa fuera de la clase, ésta ya ha sido definida dentro de ella (tal y como haríamos en C tradicional para definir funciones que no son la main. Recordemos que primero se definen encima del main, para implementarse después, a continuación de la función main).

Una vez que ya tenemos definida la clase, sólo hay que pasar a la función main y comenzar a trabajar con ella. Lo primero es crearnos un objeto o instancia de esa clase. Dicho objeto puede tener cualquier nombre y se crea como si la clase anteriormente definida fuera un tipo de dato, es decir, ponemos el nombre de la clase y después el nombre del objeto que queremos crear de ese tipo de clase. En nuestro caso al objeto en cuestión le hemos llamado Conan (original sí señor...).

Una vez creado el objeto Conan procedemos a trabajar con él. Cómo es lógico, tendremos que inicializar sus valores. Para ello bastará con ir utilizando los métodos de los que dispone. Así, utilizamos PonerNombre, PonerEdad, etc hasta haber completado todos y cada uno de los atributos que debemos de inicializar. El resto es bien fácil pues sólo tendremos que llamar al método para mostrar los datos y así verlos en pantalla.

Cabe destacar que, para poder acceder a los métodos del objeto, deberemos de utilizar un punto entre el nombre del objeto y el método en cuestión, es decir, como si Conan fuera una estructura de C.

Esta sería la descripción de un modo exhaustivo del ejemplo. Espero que se os hayan aclarado bastantes cosas sobre lo que hemos venido explicando con anterioridad. Por supuesto, aún no hemos puesto sobre la mesa todo lo que hemos visto. Por ejemplo, aún no hemos hecho un ejemplo real que implemente herencia.

Constructores y destructores

Los constructores y destructores son una forma efectiva de inicializar y dar por concluida, respectivamente, la utilización de una instancia u objeto que hemos estado utilizando hasta ahora. Un constructor se puede tomar como un método que se llama nada más que creamos un objeto o instancia y que se encarga de "preparar", por así decirlo, la utilización del mismo ya sea inicializando valores básicos, asignando memoria para trabajar, etc. En cuanto a los destructores, pues podemos entenderlos como los métodos que se llaman automáticamente cuando deseamos eliminar un objeto creado con anterioridad (por ejemplo, al acabar un programa todos los objetos que hemos creado al comienzo del mismo se eliminarán) encargándose de liberar todos los posibles recursos asignados al mismo como puede ser memoria.

Los Constructores siempre se llaman nada más realizar una instancia y cumplen la propiedad de llevar el mismo nombre de la clase a la que pertenecen, es decir, un constructor de la clase casa tendría el mismo nombre, esto es, se llamaría casa también. Esto podría ser algo así:


class Casa { 
 public: 
  Casa(); // Constructor 
 ........ 
  }  

Si creáramos un objeto chalet de tipo Casa en nuestro programa:

Casa chalet;

El método constructor se llamaría automáticamente si necesidad de que nosotros hiciéramos chalet.Casa(); . Hay que añadir, finalmente, que dentro de una clase sólo puede existir un constructor.

El tema de los destructores es idéntico en funcionamiento sólo que, en lugar de ser llamados automáticamente al crear el objeto, son llamados cuando éste se destruye. Un objeto se puede destruir si es dinámico o, de forma más fácil, si llegamos al final del main dentro del programa en el que lo habíamos utilizando. Los destructores se definen con el mismo nombre de la clase, al igual que los constructores, pero con la notable diferencia de llevar delante del nombre el símbolo ~ (ALT - 126), sería algo así (continuamos con el mismo ejemplo de clase):


 class Casa { 
 public: 
  Casa(); // Constructor 
  ~Casa(); // Destructor 
 ........ 
 } 
 

Las características que sí distinguen a un destructor de un constructor son dos: un destructor nunca devuelve nada y tampoco puede recibir parámetro alguno.

La implementación tanto de constructores como de destructores sigue las normas ya vistas más arriba para definir una función de miembro de una clase. Podemos optar por realizar implementaciones intercaladas o in line o bien, podemos definirlas fuera del cuerpo de la clase utilizando para ello el operador de resolución de ámbito (::).

Por ejemplo, el esquema de la implementación del constructor de la clase anterior utilizando el operador de resolución de ámbito sería:


 Casa::Casa()  
 { 
  // Aquí pondríamos el 
  // cuerpo de la función constructor 
 } 

Y en el caso del destructor:


 Casa::~Casa()  
 { 
  // Aquí pondríamos el 
  // cuerpo de la función destructor 
 } 

Los constructores y destructores siempre se encuentran dentro de una clase aunque nosotros no los hayamos definido, es decir, en nuestro ejemplo de más arriba, en el que implementamos una clase llamada guerrero, no definimos ningún constructor ni tampoco ningún destructor sin embargo, sí que existían. Cuando nosotros creamos una instancia a un objeto, el compilador siempre llama a un constructor. En el caso de que no exista, como pasa en el ejemplo de arriba, el compilador crea un sencillo constructor con el que instanciar nuestro objeto. Por tanto, al crear siempre el compilador un constructor por defecto en caso de no encontrar el nuestro, nosotros podemos "pasar olímpicamente" de molestarnos en crearnos uno, como vimos más arriba. Esto mismo es aplicable a los destructores, es decir, cuando el objeto deja de "existir" y se elimina, bien por la finalización del programa o bien porque se trataba de un objeto dinámico y hemos decidido eliminarlo en el transcurso de ejecución de nuestro programa, el compilador "irá" en la búsqueda del destructor que hayamos definido en la clase. Si no lo encuentra, no tendrá más remedio que utilizar el que creó automáticamente cuando instanciamos el objeto por primera vez (el compilador cuando creamos el objeto se encarga siempre de comprobar la existencia de un destructor por eso, realmente cuando eliminamos un objeto ya sabe si lo que tiene que hacer es llamar al destructor que hemos creado o bien utilizar el que él mismo ha producido porque nosotros no teníamos ninguno listo).

Puede parecer que la utilización de constructores y destructores es un "rollo" para el programador pero eso no es en absoluto cierto, es más, llegan a ser en casi todas las ocasiones imprescindibles ya que nos posibilitan entre otras cosas, tareas tan importantes y vitales como la inicialización de las variables del objeto nada más instanciarlo. Recordemos que cuando creamos una clase y las variables aún no han sido inicializadas (por ejemplo una variable entera no ha sido puesta a su valor inicial para que el objeto trabaje bien con ella) siempre contendrá "valores basura". No me extenderé más aquí porque es algo que ya supongo que conoceréis de sobra. El caso es que, volviendo sobre el tema, los constructores, en particular, pueden ser muy útiles para estos casos de inicialización de valores. También hay otras razones más "persuasivas". Un buen programador deberá siempre suponerse lo peor (y más acertado) y eso significa no confiar en que los compiladores nos generen... buenos constructores.

Argumentos por defecto en funciones

Esta posibilidad resulta extremadamente útil para aplicarla en la realización de constructores para nuestras clases. La creación de funciones que incluyan argumentos por defecto nos permiten el poder invocar a una función sin necesidad de pasarle todos los parámetros que esta incluye ya que, en su defecto o ausencia, la función es capaz de asignar un valor ya definido a esa variable.

Nos podemos imaginar que tenemos una función que dibuja círculos y que, en la casi totalidad de las ocasiones, dichos círculos comienzan en la posición (10,10). Resulta muy útil el poder llamar a la función y que, dependiendo de si ésta detecta que recibe argumentos o no, sea capaz de asignar el valor que le pasamos a las coordenadas x e y o bien, darlas los valores (x=10,y=10).

Ahora imaginémonos que esto se aplica al constructor de una clase. Vamos a suponer que la clase se llama círculo, para seguir con el mismo ejemplo. Cuando creemos un objeto o instancia de dicha clase, puede que nos interese que comience dibujando el círculo en una posición distinta a (10,10) pero sabemos de antemano que en la práctica totalidad de las ocasiones, nada más que creamos la instancia, queremos que, internamente, los valores de x e y queden inicializados a dichos valores.

Para poder definir funciones que toman valores por defecto tan sólo tenemos que hacer y poniendo el ejemplo del círculo:

void SituaCirculo (int x = 10, int y = 10);

En el caso de que no pasemos el parámetro en la zona de las x, por ejemplo, la función asignará a esta variable el valor 10. Aún más, si no pasamos ningún argumento, las variables x e y quedarán inicializadas a x=10 e y=10.

Veamos ahora un sencillo ejemplo en el que definimos una clase llamada Circulo (totalmente incompleta) que contiene una función de miembro para decirnos en qué coordenadas hemos situado el círculo utilizando las variables x e y. El constructor de dicha clase se encarga de dar valores por defecto a x e y en caso de no recibir ninguno cuando creemos la instancia:


 #include <iostream.h> 
  
 class Circulo { 
  
 private: 
  int m_x, m_y; 
  
 public: 
  // Constructor 
 Circulo (int xtemp = 10, int ytemp = 10) 
  { m_x = xtemp; m_y = ytemp; } 
 ~Circulo (){} // Destructor 
 int DevuelvePosx (void) { return m_x; } 
 int DevuelvePosy (void) { return m_y; } 
 } 
  
 void main (void) 
 { 
  // Aquí creamos un objeto con paso de parámetros 
  Circulo circulo1(12,30); 
  // Aquí acabamos de crear la instancia 
  Circulo circulo2;    

  cout << "La coordenada X de circulo 1 es " 
  << circulo1.DevuelvePosx << ‘\n’; 
  cout << "La coordenada Y de circulo 1 es " 
  << circulo1.DevuelvePosy << ‘\n’; 
  
  cout << ‘/n’ ; 
  
  cout << "La coordenada X de circulo 2 es " 
  << circulo2.DevuelvePosx << ‘\n’; 
  cout << "La coordenada Y de circulo 2 es " 
  << circulo2.DevuelvePosy << ‘\n’; 
  } 
 

Si ejecutamos el programa, este mostraría por pantalla:

La coordenada X del circulo 1 es 12
La coordenada Y del circulo 1 es 30

La coordenada X del circulo 2 es 10
La coordenada Y del circulo 2 es 10

Para entender todo mejor he creado dos objetos. La instancia circulo1 se caracteriza por pasar al constructor unos valores alternativos mientras que circulo2 lo que hace es utilizar los valores por defecto. En fin, creo que no tiene mayor problema ¿no?. Pues a lo siguiente

¿Qué es el puntero this?

El puntero this es una de las múltiples prestaciones que incorpora el C++ y que, además, es ampliamente utilizada. Todo objeto que nosotros creamos en C++ posee un puntero llamado puntero this que tiene la particularidad de apuntar al objeto al que va ligado. Así el objeto se va a encontrar "apuntado así mismo" mediante el puntero this. Además, siempre que dentro del programa llamamos a una función de miembro, el puntero this se pasa en la llamada a la función. Con esto la función puede utilizar el puntero this para acceder a otros miembros (funciones o variables) de su misma clase.

Como las funciones de miembro suelen utilizar el puntero this como un argumento al llamar a otras funciones, la función llamada puede, a su vez, utilizar el puntero this para acceder al conjunto de miembros del objeto de la función que la ha llamado.... ¡qué lío!...

Quizás lo mejor para poder entender el puntero this es ir "soltándolo" a medida que avancemos en el curso mediante la inclusión del mismo en ejemplos. De todos modos, baste el listado que voy a poner a continuación para que os hagáis una idea de lo que es el puntero this:


 #include <iostream.h> 
  
 class Puntero_this{ 
 public: 
  // Constructor 
  Puntero_this(void) {} 
  // Destructor 
  ~Puntero_this() {} 
  // Devuelve el puntero this 
  void* Devuelve_this(void) {return this;} 
 }; 

 void main (void) 
 { 
  // Puntero sin tipo 
  void *puntero; 
  // Creamos una instancia u objeto 
  Puntero_this obj_this; 

  // En puntero está el puntero this 
  puntero = obj_this.Devuelve_this(); 
    
  cout << "El valor de puntero es el del puntero 
  this y vale " << puntero << ‘\n’; 
 } 
  

El ejemplo es muy sencillo e ilustra cómo todas las clases que creamos tienen su propio puntero this. Así, la clase Puntero_this tiene su propio puntero this que es devuelto mediante el método intercalado llamado void* Devuelve_this (void); Como vemos en el ejemplo, nos creamos un puntero void llamado "puntero" para contener el valor que nos devuelve la función anteriormente citada para después mostrarlo en pantalla mediante un sencillo cout.

Y en el próximo número....

Pues en el próximo número y al disponer de más tiempo espero poder darle un gran empujón a este curso realizando dos capítulos más. Creo que lo más importante es ofrecer la información lo más extensamente posible y, aunque para un fanzine como Macedonia, le viene bien ir poco a poco para mantener contenidos siempre, no lo haremos... ¡Así que en el próximo número mucho más!





AULA MACEDONIA
a
MACEDONIA Magazine