¿Cómo implementas una clase en C? [cerrado]

139

Suponiendo que tengo que usar C (no C ++ ni compiladores orientados a objetos) y no tengo una asignación de memoria dinámica, ¿cuáles son algunas técnicas que puedo usar para implementar una clase o una buena aproximación de una clase? ¿Es siempre una buena idea aislar la "clase" en un archivo separado? Suponga que podemos preasignar la memoria asumiendo un número fijo de instancias, o incluso definiendo la referencia a cada objeto como una constante antes del tiempo de compilación. Siéntase libre de hacer suposiciones sobre qué concepto de OOP necesitaré implementar (variará) y sugerir el mejor método para cada uno.

Restricciones:

  • Tengo que usar C y no una OOP porque estoy escribiendo código para un sistema incrustado, y el compilador y la base de código preexistente está en C.
  • No hay asignación de memoria dinámica porque no tenemos suficiente memoria para asumir razonablemente que no se acabará si comenzamos a asignarla dinámicamente.
  • Los compiladores con los que trabajamos no tienen problemas con los punteros de función
Ben Gartner
fuente
26
Pregunta obligatoria: ¿tiene que escribir código orientado a objetos? Si lo haces por cualquier razón, está bien, pero estarás luchando una batalla bastante cuesta arriba. Probablemente sea lo mejor si evita intentar escribir código orientado a objetos en C. Sin duda es posible, vea la excelente respuesta de desenrollar, pero no es exactamente "fácil", y si está trabajando en un sistema integrado con memoria limitada, puede no ser factible Sin embargo, puedo estar equivocado: no estoy tratando de discutirlo, solo presento algunos contrapuntos que pueden no haber sido presentados.
Chris Lutz
1
Estrictamente hablando, no tenemos que hacerlo. Sin embargo, la complejidad del sistema ha hecho que el código no se pueda mantener. Creo que la mejor manera de reducir la complejidad es implementar algunos conceptos de OOP. Gracias por todos los que respondieron en 3 minutos. ¡Ustedes están locos y rápidos!
Ben Gartner
8
Esta es solo mi humilde opinión, pero OOP no hace que el código se pueda mantener al instante. Puede que sea más fácil de administrar, pero no necesariamente más fácil de mantener. Puede tener "espacios de nombres" en C (el Apache Portable Runtime prefija todos los símbolos globales con apr_y GLib los prefija g_para crear un espacio de nombres) y otros factores de organización sin OOP. Si va a reestructurar la aplicación de todos modos, consideraría pasar algún tiempo tratando de encontrar una estructura de procedimiento más sostenible.
Chris Lutz
esto se ha discutido interminablemente antes: ¿miró alguna de las respuestas anteriores?
Larry Watanabe
Esta fuente, que estaba en una respuesta mía eliminada, también puede ser de ayuda: planetpdf.com/codecuts/pdfs/ooc.pdf Describe un enfoque completo para hacer OO en C.
Ruben Steins

Respuestas:

86

Eso depende del conjunto de características "orientado a objetos" que desea tener. Si necesita cosas como sobrecarga y / o métodos virtuales, probablemente necesite incluir punteros de función en las estructuras:

typedef struct {
  float (*computeArea)(const ShapeClass *shape);
} ShapeClass;

float shape_computeArea(const ShapeClass *shape)
{
  return shape->computeArea(shape);
}

Esto le permitiría implementar una clase, "heredando" la clase base e implementando una función adecuada:

typedef struct {
  ShapeClass shape;
  float width, height;
} RectangleClass;

static float rectangle_computeArea(const ShapeClass *shape)
{
  const RectangleClass *rect = (const RectangleClass *) shape;
  return rect->width * rect->height;
}

Por supuesto, esto requiere que también implemente un constructor, que se asegura de que el puntero de la función esté configurado correctamente. Normalmente asignaría memoria dinámicamente para la instancia, pero también puede dejar que la persona que llama haga eso:

void rectangle_new(RectangleClass *rect)
{
  rect->width = rect->height = 0.f;
  rect->shape.computeArea = rectangle_computeArea;
}

Si desea varios constructores diferentes, tendrá que "decorar" los nombres de las funciones, no puede tener más de una rectangle_new()función:

void rectangle_new_with_lengths(RectangleClass *rect, float width, float height)
{
  rectangle_new(rect);
  rect->width = width;
  rect->height = height;
}

Aquí hay un ejemplo básico que muestra el uso:

int main(void)
{
  RectangleClass r1;

  rectangle_new_with_lengths(&r1, 4.f, 5.f);
  printf("rectangle r1's area is %f units square\n", shape_computeArea(&r1));
  return 0;
}

Espero que esto te dé algunas ideas, al menos. Para un marco orientado a objetos exitoso y rico en C, busque en la biblioteca GObject de glib .

También tenga en cuenta que no hay una "clase" explícita modelada anteriormente, cada objeto tiene sus propios punteros de método, que es un poco más flexible de lo que normalmente encontraría en C ++. Además, cuesta memoria. Puede alejarse de eso rellenando los punteros del método en una classestructura e inventando una forma para que cada instancia de objeto haga referencia a una clase.

relajarse
fuente
No habiendo tenido que intentar escribir C orientado a objetos, ¿es generalmente mejor hacer funciones que tomen const ShapeClass *o const void *como argumentos? Parecería que esto último podría ser un poco más amable con la herencia, pero puedo ver argumentos en ambos sentidos ...
Chris Lutz
1
@ Chris: Sí, esa es una pregunta difícil. : | GTK + (que usa GObject) usa la clase adecuada, es decir, RectangleClass *. Esto significa que a menudo tiene que hacer lanzamientos, pero proporcionan macros útiles que ayudan con eso, por lo que siempre puede lanzar BASECLASS * p a SUBCLASS * usando solo SUBCLASS (p).
relajarse
1
Mi compilador falla en la segunda línea de código: float (*computeArea)(const ShapeClass *shape);decir que ShapeClasses un tipo desconocido.
DanielSank
@DanielSank debido a la falta de declaración de avance requerida por la 'estructura typedef' (no se muestra en el ejemplo dado). Debido a las structreferencias en sí, requiere una declaración antes de que se defina. Esto se explica con un ejemplo aquí en la respuesta de Lundin . La modificación del ejemplo para incluir la declaración directa debería resolver su problema; typedef struct ShapeClass ShapeClass; struct ShapeClass { float (*computeArea)(const ShapeClass *shape); };
S. Whittaker
Qué sucede cuando Rectángulo tiene una función que no todas las Formas tienen. Por ejemplo, get_corners (). Un círculo no implementaría esto, pero un rectángulo sí. ¿Cómo se accede a una función que no forma parte de la clase principal de la que heredó?
Otus
24

También tuve que hacerlo una vez para hacer la tarea. Seguí este enfoque:

  1. Defina sus miembros de datos en una estructura.
  2. Defina los miembros de su función que toman un puntero a su estructura como primer argumento.
  3. Haga esto en un encabezado y uno c. Encabezado para definición de estructura y declaraciones de funciones, c para implementaciones.

Un ejemplo simple sería este:

/// Queue.h
struct Queue
{
    /// members
}
typedef struct Queue Queue;

void push(Queue* q, int element);
void pop(Queue* q);
// etc.
/// 
erelender
fuente
Esto es lo que he hecho en el pasado, pero con la adición de un alcance falso al colocar prototipos de funciones en el archivo .c o .h según sea necesario (como mencioné en mi respuesta).
Taylor Leese
Me gusta esto, la declaración de estructura asigna toda la memoria. Por alguna razón olvidé que esto funcionaría bien.
Ben Gartner
Creo que necesitas un typedef struct Queue Queue;allí.
Craig McQueen
3
O simplemente typedef struct {/ * members * /} Queue;
Brooks Moses, el
#Craig: Gracias por el recordatorio, actualizado.
erelender
12

Si solo desea una clase, use una matriz de structs como datos de "objetos" y páselos a las funciones "miembro". Puede usar typedef struct _whatever Whateverantes de declarar struct _whateverocultar la implementación del código del cliente. No hay diferencia entre tal "objeto" y el FILEobjeto de biblioteca estándar C.

Si desea más de una clase con herencia y funciones virtuales, es común tener punteros a las funciones como miembros de la estructura, o un puntero compartido a una tabla de funciones virtuales. La biblioteca GObject usa tanto este como el truco typedef, y es ampliamente utilizado.

También hay un libro sobre técnicas de este disponible en línea - Programación Orientada a Objetos con la norma ANSI C .

Pete Kirkham
fuente
1
¡Frio! ¿Alguna otra recomendación para libros sobre OOP en C? ¿O alguna otra técnica de diseño moderno en C? (¿o sistemas integrados?)
Ben Gartner
7

puedes echar un vistazo a GOBject. Es una biblioteca de sistema operativo que le ofrece una forma detallada de hacer un objeto.

http://library.gnome.org/devel/gobject/stable/

Alex
fuente
1
Muy interesado. Alguien sabe acerca de la licencia? Para mis propósitos en el trabajo, colocar una biblioteca de código abierto en un proyecto probablemente no va a funcionar desde un punto de vista legal.
Ben Gartner
GTK +, y todas las bibliotecas que forman parte de ese proyecto (incluido GObject), tienen licencia bajo GNU LGPL, lo que significa que puede vincularlas desde un software propietario. Sin embargo, no sé si eso será factible para el trabajo integrado.
Chris Lutz
7

C Interfaces e implementaciones: técnicas para crear software reutilizable , David R. Hanson

http://www.informit.com/store/product.aspx?isbn=0201498413

Este libro hace un excelente trabajo al cubrir su pregunta. Está en la serie Addison Wesley Professional Computing.

El paradigma básico es algo como esto:

/* for data structure foo */

FOO *myfoo;
myfoo = foo_create(...);
foo_something(myfoo, ...);
myfoo = foo_append(myfoo, ...);
foo_delete(myfoo);
Mark Harrison
fuente
5

Daré un ejemplo simple de cómo se debe hacer OOP en C. Me doy cuenta de que esta película es de 2009, pero me gustaría agregar esto de todos modos.

/// Object.h
typedef struct Object {
    uuid_t uuid;
} Object;

int Object_init(Object *self);
uuid_t Object_get_uuid(Object *self);
int Object_clean(Object *self);

/// Person.h
typedef struct Person {
    Object obj;
    char *name;
} Person;

int Person_init(Person *self, char *name);
int Person_greet(Person *self);
int Person_clean(Person *self);

/// Object.c
#include "object.h"

int Object_init(Object *self)
{
    self->uuid = uuid_new();

    return 0;
}
uuid_t Object_get_uuid(Object *self)
{ // Don't actually create getters in C...
    return self->uuid;
}
int Object_clean(Object *self)
{
    uuid_free(self->uuid);

    return 0;
}

/// Person.c
#include "person.h"

int Person_init(Person *self, char *name)
{
    Object_init(&self->obj); // Or just Object_init(&self);
    self->name = strdup(name);

    return 0;
}
int Person_greet(Person *self)
{
    printf("Hello, %s", self->name);

    return 0;
}
int Person_clean(Person *self)
{
    free(self->name);
    Object_clean(self);

    return 0;
}

/// main.c
int main(void)
{
    Person p;

    Person_init(&p, "John");
    Person_greet(&p);
    Object_get_uuid(&p); // Inherited function
    Person_clean(&p);

    return 0;
}

El concepto básico implica colocar la 'clase heredada' en la parte superior de la estructura. De esta manera, el acceso a los primeros 4 bytes en la estructura también accede a los primeros 4 bytes en la 'clase heredada' (Asumiendo optimizaciones no locas). Ahora, cuando el puntero de la estructura se lanza a la 'clase heredada', la 'clase heredada' puede acceder a los 'valores heredados' de la misma manera que accedería a sus miembros normalmente.

Esto y algunas convenciones de nomenclatura para constructores, destructores, funciones de asignación y reparto (recomiendo init, clean, new, free) lo ayudarán en gran medida.

En cuanto a las funciones virtuales, use punteros de función en la estructura, posiblemente con Class_func (...); envoltorio también. En cuanto a las plantillas (simples), agregue un parámetro size_t para determinar el tamaño, requiera un puntero nulo * o requiera un tipo 'clase' con solo la funcionalidad que le interesa. (por ejemplo, int GetUUID (Object * self); GetUUID (& p);)

yyny
fuente
Descargo de responsabilidad: todo el código escrito en el teléfono inteligente. Agregue verificaciones de error donde sea necesario. Verifica si hay errores.
yyny
4

Use a structpara simular los miembros de datos de una clase. En términos del alcance del método, puede simular métodos privados colocando los prototipos de funciones privadas en el archivo .c y las funciones públicas en el archivo .h.

Taylor Leese
fuente
4
#include <stdio.h>
#include <math.h>
#include <string.h>
#include <uchar.h>

/**
 * Define Shape class
 */
typedef struct Shape Shape;
struct Shape {
    /**
     * Variables header...
     */
    double width, height;

    /**
     * Functions header...
     */
    double (*area)(Shape *shape);
};

/**
 * Functions
 */
double calc(Shape *shape) {
        return shape->width * shape->height;
}

/**
 * Constructor
 */
Shape _Shape() {
    Shape s;

    s.width = 1;
    s.height = 1;

    s.area = calc;

    return s;
}

/********************************************/

int main() {
    Shape s1 = _Shape();
    s1.width = 5.35;
    s1.height = 12.5462;

    printf("Hello World\n\n");

    printf("User.width = %f\n", s1.width);
    printf("User.height = %f\n", s1.height);
    printf("User.area = %f\n\n", s1.area(&s1));

    printf("Made with \xe2\x99\xa5 \n");

    return 0;
};
Pierozi
fuente
3

En su caso, la buena aproximación de la clase podría ser un ADT . Pero aún así no será lo mismo.

Artem Barger
fuente
1
¿Alguien puede dar una breve diferencia entre un tipo de datos abstractos y una clase? Siempre tengo los dos conceptos tan estrechamente vinculados.
Ben Gartner
De hecho, están estrechamente relacionados. Una clase puede verse como una implementación de un ADT, ya que (supuestamente) podría ser reemplazada por otra implementación que satisfaga la misma interfaz. Sin embargo, creo que es difícil dar una diferencia exacta, ya que los conceptos no están claramente definidos.
Jørgen Fogh
3

Mi estrategia es:

  • Defina todo el código para la clase en un archivo separado
  • Defina todas las interfaces para la clase en un archivo de encabezado separado
  • Todas las funciones miembro toman un "ClassHandle" que sustituye el nombre de la instancia (en lugar de o.foo (), llame a foo (oHandle)
  • El constructor se reemplaza con una función void ClassInit (ClassHandle h, int x, int y, ...) O ClassHandle ClassInit (int x, int y, ...) dependiendo de la estrategia de asignación de memoria
  • Todas las variables miembro se almacenan como miembro de una estructura estática en el archivo de clase, encapsulando en el archivo, evitando que los archivos externos accedan a él.
  • Los objetos se almacenan en una matriz de la estructura estática anterior, con identificadores predefinidos (visibles en la interfaz) o un límite fijo de objetos que se pueden instanciar
  • Si es útil, la clase puede contener funciones públicas que recorrerán la matriz y llamarán a las funciones de todos los objetos instanciados (RunAll () llama a cada Run (oHandle)
  • Una función Deinit (ClassHandle h) libera la memoria asignada (índice de matriz) en la estrategia de asignación dinámica

¿Alguien ve algún problema, agujeros, posibles dificultades o beneficios / inconvenientes ocultos para cualquiera de las variaciones de este enfoque? Si estoy reinventando un método de diseño (y supongo que debo hacerlo), ¿puede indicarme el nombre?

Ben Gartner
fuente
Como cuestión de estilo, si tiene información para agregar a su pregunta, debe editar su pregunta para incluir esta información.
Chris Lutz
Parece que ha pasado de malloc asignando dinámicamente de un montón grande a ClassInit () seleccionando dinámicamente de un grupo de tamaño fijo, en lugar de hacer algo sobre lo que sucederá cuando solicite otro objeto y no tenga los recursos para proporcionar uno .
Pete Kirkham
Sí, la carga de administración de memoria se transfiere al código que llama a ClassInit () para verificar que el identificador devuelto sea válido. Esencialmente hemos creado nuestro propio montón dedicado para la clase. No estoy seguro de ver una manera de evitar esto si queremos hacer una asignación dinámica, a menos que implementemos un montón de propósito general. Prefiero aislar el riesgo de heredar en el montón a una clase.
Ben Gartner
3

También vea esta respuesta y esta

Es posible. Siempre parece una buena idea en ese momento, pero luego se convierte en una pesadilla de mantenimiento. Su código se llena de fragmentos de código que unen todo. Un nuevo programador tendrá muchos problemas para leer y comprender el código si usa punteros de función, ya que no será obvio qué funciones se llaman.

La ocultación de datos con funciones get / set es fácil de implementar en C, pero se detiene allí. He visto múltiples intentos de esto en el entorno embebido y al final siempre es un problema de mantenimiento.

Como todos ustedes tienen problemas de mantenimiento, me mantendría al margen.

Gerhard
fuente
2

Mi enfoque sería mover las structy todas las funciones asociadas principalmente a un archivo (s) de origen separado para que pueda usarse "de forma portátil".

Dependiendo de su compilador, es posible que pueda incluir funciones en el struct, pero esa es una extensión muy específica del compilador, y no tiene nada que ver con la última versión del estándar que usé habitualmente :)

madriguera
fuente
2
Los punteros de función son todos buenos. Tendemos a usarlos para reemplazar grandes declaraciones de cambio con una tabla de búsqueda.
Ben Gartner
2

El primer compilador de C ++ en realidad fue un preprocesador que tradujo el código de C ++ a C.

Por lo tanto, es muy posible tener clases en C. Puede intentar desenterrar un antiguo preprocesador de C ++ y ver qué tipo de soluciones crea.

Sapo
fuente
Eso seria cfront; se topó con problemas cuando se agregaron excepciones a C ++; el manejo de excepciones no es trivial.
Jonathan Leffler
2

GTK está construido completamente en C y utiliza muchos conceptos OOP. He leído el código fuente de GTK y es bastante impresionante, y definitivamente más fácil de leer. El concepto básico es que cada "clase" es simplemente una estructura y funciones estáticas asociadas. Todas las funciones estáticas aceptan la estructura de "instancia" como parámetro, hacen lo que sea necesario y devuelven resultados si es necesario. Por ejemplo, puede tener una función "GetPosition (CircleStruct obj)". La función simplemente cavaría a través de la estructura, extraería los números de posición, probablemente construiría un nuevo objeto PositionStruct, pegaría las x e y en la nueva PositionStruct y lo devolvería. GTK incluso implementa la herencia de esta manera al incorporar estructuras dentro de las estructuras. muy inteligente.

cohetes
fuente
1

¿Quieres métodos virtuales?

Si no es así, simplemente define un conjunto de punteros de función en la estructura misma. Si asigna todos los punteros de función a funciones C estándar, podrá invocar funciones desde C en una sintaxis muy similar a como lo haría en C ++.

Si quieres tener métodos virtuales, se vuelve más complicado. Básicamente, necesitará implementar su propia VTable en cada estructura y asignar punteros de función a la VTable dependiendo de la función que se llame. Entonces necesitaría un conjunto de punteros de función en la estructura misma que a su vez llama al puntero de función en la VTable. Esto es, esencialmente, lo que hace C ++.

Sin embargo, TBH ... si quieres lo último, entonces probablemente sea mejor solo encontrar un compilador de C ++ que puedas usar y volver a compilar el proyecto. Nunca he entendido que la obsesión con C ++ no sea utilizable en embebido. Lo he usado muchas veces y funciona es rápido y no tiene problemas de memoria. Seguro que tienes que ser un poco más cuidadoso con lo que haces, pero en realidad no es tan complicado.

Goz
fuente
Ya lo dije y lo diré nuevamente, pero lo diré nuevamente: no necesita punteros de función o la capacidad de llamar funciones desde estructuras estilo C ++ para crear OOP en C, OOP se trata principalmente de herencia de funcionalidad y variables (contenido), que se pueden lograr en C sin punteros de función o código duplicado.
yyny
0

C no es un lenguaje OOP, como lo señala correctamente, por lo que no hay una forma integrada de escribir una clase verdadera. Su mejor opción es mirar estructuras y punteros de función , estos le permitirán construir una aproximación de una clase. Sin embargo, como C es de procedimiento, es posible que desee considerar escribir más código similar a C (es decir, sin tratar de usar clases).

Además, si puede usar C, puede usar probablemente C ++ y obtener clases.

Benjamín
fuente
44
No votaré negativamente, pero para su información, los punteros de función o la capacidad de llamar funciones desde estructuras (lo cual supongo que es su intención) no tiene nada que ver con OOP. OOP se trata principalmente de la herencia de la funcionalidad y las variables, las cuales se pueden lograr en C sin punteros de función o duplicaciones.
yyny