Como você implementa uma classe em C? [fechadas]

139

Supondo que eu precise usar C (sem C ++ ou compiladores orientados a objeto) e não possua alocação dinâmica de memória, quais são algumas técnicas que posso usar para implementar uma classe ou uma boa aproximação de uma classe? É sempre uma boa idéia isolar a "classe" em um arquivo separado? Suponha que possamos pré-alocar a memória assumindo um número fixo de instâncias, ou mesmo definindo a referência a cada objeto como uma constante antes do tempo de compilação. Sinta-se livre para fazer suposições sobre qual conceito de POO eu precisarei implementar (ele variará) e sugerir o melhor método para cada um.

Restrições:

  • Eu tenho que usar C e não um OOP porque estou escrevendo o código para um sistema incorporado, e o compilador e a base de código preexistente estão em C.
  • Não há alocação dinâmica de memória porque não temos memória suficiente para supor razoavelmente que não ficaremos sem se começarmos a alocá-la dinamicamente.
  • Os compiladores com os quais trabalhamos não têm problemas com ponteiros de função
Ben Gartner
fonte
26
Pergunta obrigatória: você precisa escrever código orientado a objetos? Se você fizer por qualquer motivo, tudo bem, mas você estará lutando uma batalha bastante difícil. Provavelmente é o melhor se você evitar escrever código orientado a objetos em C. Certamente é possível - veja a excelente resposta do desenrolador - mas não é exatamente "fácil", e se você estiver trabalhando em um sistema incorporado com memória limitada, ele pode não ser viável. No entanto, posso estar errado - não estou tentando argumentar com você, apenas apresente alguns contrapontos que podem não ter sido apresentados.
22711 Chris Lutz
1
A rigor, não precisamos. No entanto, a complexidade do sistema tornou o código inalterável. Meu sentimento é que a melhor maneira de reduzir a complexidade é implementar alguns conceitos de POO. Obrigado por todos que responderem dentro de 3 minutos. Vocês são loucos e rápidos!
Ben Gartner
8
Esta é apenas minha humilde opinião, mas o OOP não torna o código instantaneamente sustentável. Isso pode facilitar o gerenciamento, mas não necessariamente mais sustentável. Você pode ter "namespaces" em C (o Apache Portable Runtime prefixa todos os símbolos globais com apr_GLib e os prefixos g_para criar um namespace) e outros fatores organizadores sem OOP. Se você estiver reestruturando o aplicativo, considere gastar algum tempo tentando criar uma estrutura processual mais sustentável.
22720 Chris Lutz
isso foi discutido inúmeras vezes antes - você olhou para alguma das respostas anteriores?
Larry Watanabe
Esta fonte, que estava em uma resposta excluída da minha, também pode ser útil : planetpdf.com/codecuts/pdfs/ooc.pdf Ela descreve uma abordagem completa para fazer OO em C.
Ruben Steins

Respostas:

86

Isso depende do conjunto de recursos "orientado a objetos" exato que você deseja ter. Se você precisar de coisas como sobrecarga e / ou métodos virtuais, provavelmente precisará incluir ponteiros de função nas estruturas:

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

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

Isso permitiria implementar uma classe, "herdando" a classe base e implementando uma função adequada:

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;
}

É claro que isso exige que você também implemente um construtor, que garanta que o ponteiro da função esteja configurado corretamente. Normalmente, você alocaria memória dinamicamente para a instância, mas também pode permitir que o chamador faça isso:

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

Se você deseja vários construtores diferentes, precisará "decorar" os nomes das funções, não poderá ter mais de uma rectangle_new()função:

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

Aqui está um exemplo básico mostrando o 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 isso lhe dê algumas idéias, pelo menos. Para uma estrutura orientada a objetos bem-sucedida e rica em C, consulte a biblioteca GObject da glib .

Observe também que não há nenhuma "classe" explícita modelada acima, cada objeto tem seus próprios ponteiros de método, que são um pouco mais flexíveis do que você normalmente encontraria no C ++. Além disso, custa memória. Você pode evitar isso colocando os ponteiros do método em uma classestrutura e inventar uma maneira de cada instância de objeto fazer referência a uma classe.

descontrair
fonte
Como não foi necessário escrever C orientado a objetos, é melhor criar funções que tomam const ShapeClass *ou const void *como argumentos? Parece que este último pode ser um agradável pouco sobre herança, mas eu posso ver argumentos para os dois lados ...
Chris Lutz
1
@ Chris: Sim, essa é uma pergunta difícil. : | GTK + (que usa GObject) usa a classe apropriada, ou seja, RectangleClass *. Isso significa que você muitas vezes precisa fazer projeções, mas elas fornecem macros úteis para ajudar nisso; portanto, você sempre pode converter BASECLASS * p em SUBCLASS * usando apenas SUBCLASS (p).
descontraia
1
Meu compilador falha na segunda linha de código: float (*computeArea)(const ShapeClass *shape);dizer que ShapeClassé um tipo desconhecido.
precisa saber é o seguinte
@DanielSank devido à falta de declaração de encaminhamento exigida pela 'typedef struct' (não mostrada no exemplo fornecido). Como as structpróprias referências, ela requer uma declaração antes de ser definida. Isso é explicado com um exemplo aqui na resposta de Lundin . Modificar o exemplo para incluir a declaração de encaminhamento deve resolver seu problema; typedef struct ShapeClass ShapeClass; struct ShapeClass { float (*computeArea)(const ShapeClass *shape); };
S. Whittaker
O que acontece quando o Retângulo tem uma função que nem todas as Formas fazem. Por exemplo, get_corners (). Um círculo não implementaria isso, mas um retângulo poderia. Como você acessa uma função que não faz parte da classe pai da qual você herdou?
Otus
24

Eu tive que fazer isso uma vez também para uma lição de casa. Eu segui essa abordagem:

  1. Defina seus membros de dados em uma estrutura.
  2. Defina os membros da função que apontam para sua estrutura como primeiro argumento.
  3. Faça isso em um cabeçalho e um c. Cabeçalho para definição de estrutura e declarações de função, c para implementações.

Um exemplo simples seria o seguinte:

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

void push(Queue* q, int element);
void pop(Queue* q);
// etc.
/// 
erelender
fonte
Isso é o que eu fiz no passado, mas com a adição do escopo falso, colocando protótipos de função no arquivo .c ou .h, conforme necessário (como mencionei na minha resposta).
Taylor Leese
Eu gosto disso, a declaração struct aloca toda a memória. Por alguma razão, esqueci que isso funcionaria bem.
Ben Gartner
Eu acho que você precisa de um typedef struct Queue Queue;lá.
22711 Craig McQueen
3
Ou apenas typedef struct {/ * members * /} Fila;
Brooks Moses
#Craig: Obrigado pelo lembrete, atualizado.
erelender
12

Se você deseja apenas uma classe, use uma matriz de structs como dados dos "objetos" e passe ponteiros para eles para as funções "membro". Você pode usar typedef struct _whatever Whateverantes de declarar struct _whateverpara ocultar a implementação do código do cliente. Não há diferença entre esse "objeto" e o FILEobjeto de biblioteca padrão C.

Se você deseja mais de uma classe com herança e funções virtuais, é comum ter ponteiros para as funções como membros da estrutura ou um ponteiro compartilhado para uma tabela de funções virtuais. A biblioteca GObject usa esse e o truque typedef e é amplamente usada.

Há também um livro sobre técnicas para esta disponível on-line - Programação Orientada a Objetos com ANSI C .

Pete Kirkham
fonte
1
Legal! Alguma outra recomendação para livros sobre POO em C? Ou qualquer outra técnica de design moderno em C? (ou sistemas embarcados?)
Ben Gartner
7

você pode dar uma olhada no GOBject. é uma biblioteca de SO que oferece uma maneira detalhada de criar um objeto.

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

Alex
fonte
1
Muito interessado. Alguém sabe sobre o licenciamento? Para meus propósitos no trabalho, colocar uma biblioteca de código aberto em um projeto provavelmente não funcionará do ponto de vista jurídico.
Ben Gartner
O GTK + e todas as bibliotecas que fazem parte desse projeto (incluindo GObject) são licenciadas sob a GNU LGPL, o que significa que você pode vincular a elas a partir de software proprietário. Mas não sei se isso será viável para o trabalho incorporado.
31711 Chris Lutz
7

Interfaces e implementações C: Técnicas para criar software reutilizável , David R. Hanson

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

Este livro faz um excelente trabalho ao abordar sua pergunta. Está na série Addison Wesley Professional Computing.

O paradigma básico é algo como isto:

/* for data structure foo */

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

Vou dar um exemplo simples de como o POO deve ser feito em C. Sei que esse tópico é de 2009, mas gostaria de acrescentar isso de qualquer maneira.

/// 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;
}

O conceito básico envolve colocar a 'classe herdada' no topo da estrutura. Dessa forma, o acesso aos 4 primeiros bytes na estrutura também acessa os 4 primeiros bytes na 'classe herdada' (otimizações não-loucas). Agora, quando o ponteiro da estrutura é convertido para a 'classe herdada', a 'classe herdada' pode acessar os 'valores herdados' da mesma maneira que acessaria seus membros normalmente.

Esta e algumas convenções de nomenclatura para construtores, destruidores, funções de alocação e desalocação (eu recomendo init, clean, new, free) irão lhe proporcionar um longo caminho.

Quanto às funções virtuais, use ponteiros de função na estrutura, possivelmente com Class_func (...); invólucro também. Quanto aos modelos (simples), adicione um parâmetro size_t para determinar o tamanho, exija um ponteiro nulo * ou exija um tipo de 'classe' com apenas a funcionalidade de sua preferência. (por exemplo, int GetUUID (Object * self); GetUUID (& p);)

yyny
fonte
Disclaimer: Todo o código escrito no smartphone. Adicione verificações de erro onde necessário. Verifique se há bugs.
yyny 21/05
4

Use a structpara simular os membros de dados de uma classe. Em termos de escopo do método, você pode simular métodos privados colocando os protótipos de função privada no arquivo .c e as funções públicas no arquivo .h.

Taylor Leese
fonte
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
fonte
3

No seu caso, a boa aproximação da classe pode ser o ADT . Mas ainda assim não será o mesmo.

Artem Barger
fonte
1
Alguém pode fazer uma breve diferença entre um tipo de dado abstrato e uma classe? Eu sempre tenho os dois conceitos tão intimamente ligados.
Ben Gartner
Eles são de fato intimamente relacionados. Uma classe pode ser vista como uma implementação de um ADT, pois (supostamente) pode ser substituída por outra implementação que satisfaça a mesma interface. Eu acho que é difícil dar uma diferença exata, pois os conceitos não estão claramente definidos.
Jørgen Fogh
3

Minha estratégia é:

  • Defina todo o código da classe em um arquivo separado
  • Definir todas as interfaces para a classe em um arquivo de cabeçalho separado
  • Todas as funções de membro recebem um "ClassHandle" que substitui o nome da instância (em vez de o.foo (), chame foo (oHandle)
  • O construtor é substituído por uma função void ClassInit (ClassHandle h, int x, int y, ...) OR ClassHandle ClassInit (int x, int y, ...) dependendo da estratégia de alocação de memória
  • Todas as variáveis ​​de membro são armazenadas como membro de uma estrutura estática no arquivo de classe, encapsulando-o no arquivo, impedindo que arquivos externos o acessem
  • Os objetos são armazenados em uma matriz da estrutura estática acima, com identificadores predefinidos (visíveis na interface) ou um limite fixo de objetos que podem ser instanciados
  • Se útil, a classe pode conter funções públicas que percorrerão a matriz e chamarão as funções de todos os objetos instanciados (RunAll () chama cada Run (oHandle)
  • Uma função Deinit (ClassHandle h) libera a memória alocada (índice de matriz) na estratégia de alocação dinâmica

Alguém vê problemas, buracos, armadilhas potenciais ou benefícios / desvantagens ocultos de qualquer variação dessa abordagem? Se estou reinventando um método de design (e suponho que seja), você pode me indicar o nome dele?

Ben Gartner
fonte
Por uma questão de estilo, se você tiver informações para adicionar à sua pergunta, edite-a para incluir essas informações.
Chris Lutz
Parece que você mudou do malloc alocando dinamicamente de um heap grande para ClassInit () selecionando dinamicamente de um pool de tamanho fixo, em vez de realmente fazer algo sobre o que acontecerá quando você solicitar outro objeto e não tiver os recursos para fornecer um .
Pete Kirkham
Sim, a carga de gerenciamento de memória é transferida para o código que chama ClassInit () para verificar se o identificador retornado é válido. Essencialmente, criamos nossa própria pilha dedicada para a classe. Não tenho certeza de que vejo uma maneira de evitar isso se quisermos fazer uma alocação dinâmica, a menos que implementemos um heap de uso geral. Eu preferiria isolar o risco herdado no heap para uma classe.
Ben Gartner
3

Veja também esta resposta e esta

É possível. Sempre parece uma boa idéia na época, mas depois se torna um pesadelo de manutenção. Seu código fica cheio de pedaços de código que unem tudo. Um novo programador terá muitos problemas para ler e entender o código se você usar ponteiros de função, uma vez que não será óbvio que funções são chamadas.

A ocultação de dados com funções get / set é fácil de implementar em C, mas pára por aí. Eu já vi várias tentativas disso no ambiente incorporado e, no final, é sempre um problema de manutenção.

Como todos vocês têm problemas de manutenção, eu evito.

Gerhard
fonte
2

Minha abordagem seria mover as funções associadas astruct todas as funções associadas principalmente a um (s) arquivo (s) de origem separado, para que ele possa ser usado "de forma portável".

Dependendo do seu compilador, você poderá incluir funções no struct, mas essa é uma extensão muito específica do compilador e não tem nada a ver com a última versão do padrão que eu usei rotineiramente :)

Warren
fonte
2
Ponteiros de função são todos bons. Tendemos a usá-los para substituir grandes instruções de chave por uma tabela de pesquisa.
Ben Gartner
2

O primeiro compilador c ++ foi na verdade um pré-processador que traduziu o código C ++ em C.

Portanto, é muito possível ter classes em C. Você pode tentar desenterrar um pré-processador C ++ antigo e ver que tipo de soluções ele cria.

Sapo
fonte
Isso seria cfront; teve problemas quando exceções foram adicionadas ao C ++ - o tratamento de exceções não é trivial.
11139 Jonathan Leffler
2

O GTK é construído inteiramente em C e usa muitos conceitos de POO. Eu li o código fonte do GTK e é bastante impressionante e definitivamente mais fácil de ler. O conceito básico é que cada "classe" é simplesmente uma estrutura e funções estáticas associadas. Todas as funções estáticas aceitam a estrutura "instance" como parâmetro, fazem o que for necessário e retornam resultados, se necessário. Por exemplo, você pode ter uma função "GetPosition (CircleStruct obj)". A função simplesmente vasculharia a estrutura, extrairia os números de posição, provavelmente criaria um novo objeto PositionStruct, colocaria xey no novo PositionStruct e o retornaria. O GTK ainda implementa herança dessa maneira incorporando estruturas dentro de estruturas. bem esperto.

rocketsarefast
fonte
1

Você quer métodos virtuais?

Caso contrário, basta definir um conjunto de ponteiros de função na própria estrutura. Se você atribuir todos os ponteiros de função às funções C padrão, poderá chamar funções de C em sintaxe muito semelhante à que você faria em C ++.

Se você deseja ter métodos virtuais, fica mais complicado. Basicamente, você precisará implementar sua própria VTable para cada estrutura e atribuir ponteiros de função à VTable, dependendo de qual função é chamada. Você precisaria de um conjunto de ponteiros de função na estrutura que, por sua vez, chama o ponteiro de função na VTable. Isso é, essencialmente, o que o C ++ faz.

TBH embora ... se você quiser o último, provavelmente está melhor apenas encontrando um compilador C ++, pode usar e recompilar o projeto. Eu nunca entendi a obsessão pelo C ++ não ser utilizável no incorporado. Eu o usei muitas vezes e funciona rápido e não tem problemas de memória. Claro que você precisa ter um pouco mais de cuidado com o que faz, mas não é tão complicado assim.

Goz
fonte
Eu já disse e falarei novamente, mas dirá novamente: Você não precisa de ponteiros de função ou a capacidade de chamar funções de estruturas do estilo C ++ para criar OOP em C, OOP é principalmente sobre herança de funcionalidade e variáveis (conteúdo), ambos os quais podem ser obtidos em C sem ponteiros de função ou código duplicado.
yyny 20/05
0

C não é uma linguagem OOP, como você aponta corretamente, então não há uma maneira integrada de escrever uma classe verdadeira. Sua melhor aposta é olhar para estruturas e indicadores de função , isso permitirá que você construa uma aproximação de uma classe. No entanto, como C é procedural, você pode considerar escrever um código mais semelhante ao C (ou seja, sem tentar usar classes).

Além disso, se você pode usar C, provavelmente pode usar C ++ e obter classes.

Benjamin
fonte
4
Não vou diminuir o voto, mas, para sua informação, ponteiros de função ou a capacidade de chamar funções de estruturas (que suponho que seja sua intenção) não tem nada a ver com OOP. OOP é principalmente sobre herança de funcionalidade e variáveis, as quais podem ser obtidas em C sem ponteiros de função ou duplicatas.
yyny