Laburo España: 250.000 ofertas de empleo

Miércoles, 24 de mayo de 2006

C++ Orientado a objetos II

Para continuar con el anterior artículo explicaremos:

  1. - Sobrecarga de los operadores new, delete y []
  2. - Clases abstractas
  3. - Conclusiones sobre la POO


Antes de continuar con la segunda parte de esta serie. Comentaré que perfectamente conozco manuales como el de C con clase, y lo recomiendo a todo el mundo que esté empezando en esto de la programación (yo ya lo leí). Estos trabajos simplemente son un apoyo a aspectos difíciles del lenguaje, un material de apoyo, y un intento de documentar temas superficialmente tocados en Internet.

1.- SOBRECARGA DE OPERADORES:

Se explicará a continuación la sobrecarga de los operadores más difíciles de encontrar en otros tutoriales o libros.

1- Operador new:
Antes de nada, comentar que estos prototipos son inamovibles, lo he comprobado. Su prototipo para memoria dinámica de tipos simples:

void* operator new(size_t tamano);

Para arrays:

void* operator new[](size_t tamano)

Como podemos sobrecargalo para que haga muchas cosas diferentes, veremos una manera que yo uso, no sé si es del todo correcta, es:

Ejemplo 1
void* operator new(size_t tamano)
{
void *puntero = NULL;
puntero = new "tipo";
return puntero;
}

Ejemplo2
void* operator new(size_t tamano)
{
void *puntero = NULL;
puntero = new("tipo"); //No entra en recursivo
return puntero;
}

Ejemplo3
void* operator new[](size_t tamano)
{
void *puntero = NULL;
puntero = new "tipo"[tamano];
return puntero;
}

- Explicación:
En el ejemplo1, creamos un puntero genérico del tipo void apuntado a NULL. Lo que me recuerda, un pequeño consejo sobre NULL extraido de Ceklog.

Después utilizamos memoria dinámica normal, y reservamos memoria para nuestro "tipo". El tipo puede ser el que nos interese en nuestro caso, puede ser un int, float, una clase...
Estaréis pensando que claro, eso no es nada dinámico, reservar siempre espacio para el mismo tipo, estoy de acuerdo. Pero lo cierto es que no he dado con otra forma de hacerlo. Teóricamente debería ser new(tamano) lo que hiciesemos, sin embargo esto a nuestro compilador no le va a gustar demasiado, podéis comprobarlo.

- LLamar a estos operadores:
Estos operadores se llaman en un posible main del siguiente modo:
objeto.operator new(parámetros);

Sobre este operador en Internet, siempre he encontrado información poco detallada, nunca un ejemplo clarificador. Incluso estoy seguro de que algunos que lo mencionan, realmente no sabrían usarlo.
El mejor ejemplo para C++ lo encontré en la web de Chuidiang , y aunque se explica para C++, no me convence del todo, porque dentro del operador utiliza malloc() y free() funciones propias de C para la gestión dinámica de memoria.
Pero esta sería una opción para lograr reservar memoria según el tamaño que queramos. Su código es:

Ejemplo4
void* operator new(size_t tamano)
{
void *puntero = NULL;
puntero = malloc(tamano);
return puntero;
}

Así que dejo aquí planteada la cuestión por si alguien quiere aclararla, de cómo sobrecargar new usando solamente memoria de C++, yo por mucho que he buscado no encontré nada al respecto.

2- Operador delete:
Su prototipo de nuevo inalterable es:

void operator delete(void *p);

Este resulta más sencillo y evidente en su uso. Un ejemplo sería:

void operator delete(void *puntero)
{
delete puntero;
return;
}

Le pasamos un puntero. En el operador liberamos la memoria. Se suele usar para tracear errores de memoria dinámica. Del siguiente modo, poniendo un contador de punteros creados y uno de punteros liberados, si no coinciden, ¡mal asunto!

Para arrays el prototipo es:

void operator delete[](void *p);
{
delete []puntero;
return;
}

3- Sobrecarga del operador []:
Este operador conocido como de subindexación de arrays, solo puede sobrecargarse como un método miembro (nunca friend, prestad atención a esto, nunca friend). Su prototipo:

tipo& nombre_clase::operator [](int indice )

Un ejemplo básico, aunque quizás demasiado enrevesado, de su uso es crear un array seguro. Veamos el código:

class Arrayseguro
{
public:
int array[5];
Arrayseguro();
int & operator [](int indice);
};

Arrayseguro::Arrayseguro()
{
for(int i=0;i<5;i++)
array[i]=i;
}

int & Arrayseguro::operator [](int indice)
{
if(i<0 || i > 4)
exit(1);

return array[i];
}

int main()
{
Arrayseguro objeto;
objeto.array[3] = 8; //FUNCIONA
//objeto.array[5] = 9; ABORTA

return 0;
}

- Explicación:
Creamos una clase que tiene por atributo un array y sobrecargamos el operador [] para que si se intenta acceder a posiciones que se encuentran fuera del array el programa aborte con código de error 1. Este ejemplo perfectamente se puede hacer con memoria dinámica, resultando mucho más útil.
Esta es una interesante medida de seguridad para nuestros programas.

2.- CLASES ABSTRACTAS:

El otro día me olvidé de comentar lo que son las clases abstractas, el segundo uso de la palabra reservada virtual.
Cuando en una clase definimos un método como virtual y lo igualamos a cero, se dice que es virtual puro. Una clase que contenga uno solo de estos métodos, se considera abstracta.
No se pueden generar objetos de las clases abstractas, ya que son una simple base para heredar. Lo vemos mejor con un ejemplo:

class Personalidad
{
protected:
int sociabilidad;
int gracioso;
public:
virtual void mostrar() = 0;
virtual int get_gracioso() = 0;
};

class Persona : public Personalidad
{
protected:
char *nombre;
int edad;
public:
void mostrar();
int get_gracioso();
bool mayor_de_edad();
};

void Persona::mostrar()
{
cout << "La persona es: " << nombre << endl;
return;
}

int Persona::get_gracioso()
{
return gracioso;
}

bool mayor_de_edad()
{
if (edad >= 18)
return true;
return false;
}

int main()
{
Persona Miguel;
return 0;
}

- Explicación:
Se observa que no tendría mucha lógica crear un objeto de tipo personalidad según la jerarquí de herencia. Por ello definimos la clase Personalidad como abstracta.

La segunda consideración a tener en cuenta, es que los métodos virtuales puros, siempre se deben redeclarar en las hijas, digamos que son un esquema o molde, y de este modo, si nos olvidamos el compilador nos dará error, evitando que el asunto vaya a más.

3.- CONCLUSIONES SOBRE LA POO:

Aquí termina esta segunda parte, los operadores tratados son bastante avanzados, igual que las clases abstractas. Deben utilizarse simplemente cuando se estime necesario. La programación orientada a objetos existe, no podemos negarlo, y por lo tanto es importante conocer sus ventajas e inconvenientes.
Son muchos sus detractores, habitualmente se argumenta que de ser una idea tan genial C habría dejado de usarse hace años, sin embargo no ha ocurrido. No existe todavía una técnica inmejorable.
Desde mi punto de vista, los objetos pueden brindarnos conjugados con la programación estructurada un buen arma de ataque.
Se debe sobrecargar solamente los operadores estrictamente necesarios, y tener una estructura hecha de las clases, antes de ponernos manos a la obra. Al principio pensar orientado a objetos es difícil, pero nos acostumbraremos.

En próximos fascículos, dentro ya de algún tiempo, quizás comente algo sobre programación explícitamente en Linux. Insisto que si os parece bien, dejéis algún que otro comentario.

Un saludo


Por: Miguel Araujo | Programación | Comentarios (6) | Referencias (0)

Comentarios

Para usar la gestión de memoria "tipo C++" hay que llamar al new no sobreescrito hay que usar el operador :: (resolución de ámbito o "scope") como quieres llamar al new "global" no tienes que especificar nombre de clase ni namespace (ver el ejemplo)

Por otro lado está bien saber como se sobreescribir operadores, pero hay que tener una buena razón para ello, porque a alguein que no conozca el código puede ser ilegible (en particular esos operadores new, delete que tienen un comportamiento bien definido si no están sobreescritos... )

Un saludo y gracias por el esfurzo en tu artículo


ejemplo:

#include <stdio.h>
#include <iostream>
using namespace std;
class A
{
public:
void* operator new(size_t)
{
cout<<"constryendo uno (sobreescrito)"< return ::new A;
}


void* operator new[] (size_t siz)
{
cout<<"constryendo vector (sobreescrito), tamaño "<< siz < return ::new A[siz];
}

};
int main ()
{
A* a= new A[25];
A* b= new A;
}

mig21 | 25-05-2006 12:51:03

me temo que la info de mi comentario anterior no es del todo válida, voy a investigar un poquito... :)

mig21 | 25-05-2006 13:21:59

En el ejemplo anterior el problema es que se llamaba al operador global dentro del operador sobreescrito, con lo que se pasaba dos veces por el constructor (y no estoy seguro que no se pierda memoria, habría que comprobarlo...)

eso si se puede evitar usar el gestor de memoria sobreescrito si se llama a ::new y ::delete (para más dificultad de uso aún :) )

En general sobrecargar new y delete sirve para hacer gestion distinta a la normal del heap, por ejemplo hacer un gestor que coja mucha memoria al pricipio y vaya repartiéndola a los que la vayan pidiendo. Le veo poca utilidad en la mayoría de los casos...
(por aquí hay alguien que sabe de que va y no le gusta mucho...: http://www.scs.stanford.edu/~dm/home/papers/c++-ne...)

hacer un gestor de memoria de una clase con new y delete (sencillo y muy poco útil) sería:

#include <stdio.h>
#include <iostream>
using namespace std;
class A
{
public:
static int i;
int ii;
A()
{
ii=i++;
cout<<"construyendo... "<< i<<" "< }
~A()
{
ii=i--;
cout<<"destruyendo... "<< i<<" "< }

static void* operator new(size_t t)
{
cout<<"constryendo uno (sobreescrito)"< return new char[t];
}


static void* operator new[] (size_t siz)
{
cout<<"constryendo vector (sobreescrito), tamaño "<< siz < return ::new char[siz];
}
static void operator delete(void *point)
{
cout<<"destryendo uno (sobreescrito)"< ::delete (char*)point ;
}


void operator delete[] (void * point)
{
cout<<"destryendo vector (sobreescrito)"< ::delete [] (char*)point;
}




};
int A::i=0;
int main ()
{
A* a= new A[10];
A* b= new A;
A* c= ::new A;
for(int i=0;i<10;++i)
cout< delete []a;
delete b;
::delete c;

return 0;
}

mig21 | 25-05-2006 14:02:21

>Insisto que si os parece bien, dejéis algún que >otro comentario.

A mi me parece de puta madre ;)

Carlos | 25-05-2006 16:04:47

Por último, y como curiosidad

en la primera versión hacía -> return ::new char[siz];

algo que yo no sabía es que en el parámetro tipo size_t del new te viene el tamaño en bytes de lo que se va a alojar, con lo que

A* a= new A[25];

le pasaba un siz al new que es el número de bytes. Total que debería poner (para que el número sea el mismo)
return ::new char[siz/sizeof(A)];

de todos modos pasaría dos veces por el constructor, porque (no lo sabía) el cada new significa pasar por el constructor. Si se tienen datos de la clase (estáticos) y se cambian en las construcción (una cuenta de referencias, como en el ii del segundo ejemplo) puede dar comportamientos inesperados. Pero no perdía memoria...

Como viene a decir el artículo que menciono en el tercer comentario, una cosa es la gestion de memoria y otra la inialización (o destrucción) cosa que en C++ está un poco ligada...

Creo que he liado más que ayudado, lo siento O:)

mig21 | 25-05-2006 18:02:40

Hola:

Yo he usado la sobrecarga de new y delete globales para hacer contabilidad de memoria reservada y liberada, con la intención de ver en qué partes del ćodigo se olvida hacer los delete.

Simplemente se hace una pequeña librería compilada con estas dos funciones. Si enlazas tu ejecutable con ella, tienes contabilidad de punteros. Si no la enlazas, no la tienes. Es una forma cómoda y rápida de sin tener que tocar en absoluto código -sólo hay que linkar-, ver dónde se pierde esa memoria.

En cuanto a usar malloc(), lo hago porque estoy redefiniendo new. Si uso new dentro de new, tengo recursión infinita. No he probado si llamando a ::new en vez de new se evita esa recursión, pero ... ¿no es lo mismo ::new que new a secas?.

Cada clase puede sobreescribir su propio new y delete, pero eso no me sirve para contabilidad global de punteros, además de que si quieres poner o quitar, tienes que tocar el código. La contabilidad de punteros es una cosa temporal y mientras se está desarrollando.

Se bueno.

chuidiang | 07-11-2007 23:09:36

Comentar


Recordar datos

Búsqueda

Acerca de InfoLinux


Tu punto de encuentro GNU/Linux: Manuales, anécdotas, curiosidades, consejos y trucos, ¡sácale partido a tu ordenador!


Linuxeros Online:

Blogs que leo

Sindicación

Añadir a Feedness
RDF XML ATOM

Créditos

Diseñado por Studio.st
Online gracias a Bitacoras.com