É ruim escrever C orientado a objeto? [fechadas]

14

Eu sempre pareço escrever código em C que é principalmente orientado a objetos, então, digamos que eu tinha um arquivo de origem ou algo que eu criaria uma estrutura e, em seguida, passaria o ponteiro dessa estrutura para funções (métodos) pertencentes a essa estrutura:

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

void destroyFoo(struct foo* foo); // frees foo and its things

Isso é uma prática ruim? Como aprendo a escrever C da "maneira correta".

mosmo
fonte
10
Grande parte do Linux (o kernel) é escrito dessa maneira, na verdade, até emula ainda mais conceitos semelhantes a OO, como o envio de método virtual. Eu considero isso bastante apropriado.
precisa saber é o seguinte
13
" [Ele] determinou que o Real Programmer pode escrever programas FORTRAN em qualquer idioma. " - Ed Post, 1983
Ross Patterson
4
Existe alguma razão para você não querer mudar para C ++? Você não precisa usar as partes que não gosta.
svick
5
Isso realmente levanta a questão de "O que é 'orientado a objetos'?" Eu não chamaria isso de orientado a objetos. Eu diria que é processual. (Você não tem herança, polimorfismo, encapsulamento / capacidade de ocultar o estado e provavelmente está perdendo outras características do OO que não saem do topo da minha cabeça.) Se é uma boa ou má prática, não depende dessa semântica. , Apesar.
Jpmc26 28/01
3
@ jpmc26: Se você é um prescritivista linguístico, deve ouvir Alan Kay, ele inventou o termo, ele começa a dizer o que significa e diz que OOP é tudo sobre mensagens . Se você é um descritivista linguístico, pesquisaria o uso do termo na comunidade de desenvolvimento de software. Cook fez exatamente isso, analisou os recursos de idiomas que afirmam ou são considerados OO e descobriu que eles tinham uma coisa em comum: o sistema de mensagens .
Jörg W Mittag

Respostas:

24

Não, isso não é uma prática ruim, é até encorajado a fazê-lo, embora alguém possa usar convenções como struct foo *foo_new();evoid foo_free(struct foo *foo);

Obviamente, como diz um comentário, faça isso apenas quando apropriado. Não faz sentido usar um construtor para um int.

O prefixo foo_é uma convenção seguida por muitas bibliotecas, porque evita o conflito com a nomeação de outras bibliotecas. Outras funções geralmente têm a convenção de uso foo_<function>(struct foo *foo, <parameters>);. Isso permite que você struct fooseja do tipo opaco.

Dê uma olhada na documentação libcurl para a convenção, especialmente com "subnamespaces", para que chamar uma função curl_multi_*pareça errado à primeira vista quando o primeiro parâmetro foi retornado por curl_easy_init().

Existem abordagens ainda mais genéricas, consulte Programação orientada a objetos com ANSI-C

Resíduo
fonte
11
Sempre com a ressalva "onde apropriado". OOP não é uma bala de prata.
Deduplicator
C não possui namespaces nos quais você pode declarar essas funções? Semelhante a std::string, você não poderia foo::create? Eu não uso C. Talvez isso seja apenas em C ++?
precisa saber é o seguinte
@ChrisCirefice Não há namespaces em C, é por isso que muitos autores de bibliotecas usam prefixos para suas funções.
Residuum
2

Não é ruim, é excelente. A programação orientada a objetos é uma coisa boa (a menos que você se empolgue, pode ter muita coisa boa). C não é o idioma mais adequado para OOP, mas isso não deve impedir você de tirar o melhor proveito dele.

gnasher729
fonte
4
Não que eu possa discordar, mas sua opinião realmente deve ser apoiada por alguma elaboração.
Deduplicator
1

Não é ruim. Ele recomenda o uso do RAII, que evita muitos erros (vazamentos de memória, uso de variáveis ​​não inicializadas, uso após liberação etc., o que pode causar problemas de segurança).

Portanto, se você deseja compilar seu código apenas com GCC ou Clang (e não com o compilador MS), você pode usar o cleanupatributo que destruirá adequadamente seus objetos. Se você declarar seu objeto assim:

my_str __attribute__((cleanup(my_str_destructor))) ptr;

Em seguida my_str_destructor(ptr), será executado quando o ptr sair do escopo. Lembre-se de que ele não pode ser usado com argumentos de função .

Além disso, lembre-se de usar my_str_nos nomes dos métodos, porque Cnão possui espaços para nome e é fácil colidir com outro nome de função.

Marqin
fonte
2
Afaik, RAII é sobre o uso da chamada implícita de destruidores de objetos em C ++ para garantir a limpeza, evitando a necessidade de adicionar chamadas explícitas de liberação de recursos. Portanto, se não me engano, RAII e C são mutuamente exclusivos.
cmaster - restabelece monica
@cmaster Se você #defineusar seus nomes de tipo, você o __attribute__((cleanup(my_str_destructor)))encontrará como implícito em todo o #defineescopo (ele será adicionado a todas as suas declarações de variáveis).
Marqin
Isso funciona se a) você usar gcc, b) se você usar o tipo apenas em variáveis ​​locais da função ec) se você usar o tipo apenas em uma versão simples (sem ponteiros para o #definetipo d ou suas matrizes). Em resumo: não é padrão C e você paga com muita inflexibilidade em uso.
cmaster - reinstala monica em
Como mencionado na minha resposta, isso também funciona em clang.
Marqin
Ah, eu não percebi isso. Isso de fato torna o requisito a) significativamente menos severo, uma vez que torna __attribute__((cleanup()))praticamente um quase-padrão. No entanto, b) e c) ainda estão de pé ...
cmaster - Reintegrar monica
-2

Pode haver muitas vantagens nesse código, mas infelizmente o Padrão C não foi escrito para facilitar. Historicamente, os compiladores ofereceram garantias comportamentais efetivas além do exigido pelo Padrão, que tornou possível escrever esse código de maneira muito mais limpa do que é possível no Padrão C, mas os compiladores começaram recentemente a revogar essas garantias em nome da otimização.

Mais notavelmente, muitos compiladores C garantem historicamente (por design, se não documentação) que, se dois tipos de estrutura contiverem a mesma sequência inicial, um ponteiro para qualquer tipo poderá ser usado para acessar membros dessa sequência comum, mesmo que os tipos não sejam relacionados, e ainda que, para fins de estabelecer uma sequência inicial comum, todos os indicadores de estruturas são equivalentes. O código que utiliza esse comportamento pode ser muito mais limpo e seguro para o tipo do que o código que não possui, mas, infelizmente, embora o Padrão exija que estruturas que compartilham uma sequência inicial comum sejam dispostas da mesma maneira, ele proíbe o código de realmente usar um ponteiro de um tipo para acessar a sequência inicial de outro.

Consequentemente, se você deseja escrever código orientado a objetos em C, terá que decidir (e deve tomar essa decisão desde o início) saltar por muitos obstáculos para cumprir as regras do tipo ponteiro de C e estar preparado para ter Os compiladores modernos geram código sem sentido, se alguém escorregar, mesmo que os compiladores mais antigos tenham gerado código que funcione conforme o planejado, ou documentem um requisito de que o código só possa ser usado com compiladores configurados para suportar o comportamento do ponteiro no estilo antigo (por exemplo, usando um "-fno-strict-aliasing") Algumas pessoas consideram "-fno-strict-aliasing" como mau, mas eu sugiro que seja mais útil pensar em "-fno-strict-aliasing" C como uma linguagem que oferece maior poder semântico para alguns propósitos do que C "padrão",mas à custa de otimizações que podem ser importantes para outros fins.

Por exemplo, em compiladores tradicionais, os compiladores históricos interpretariam o seguinte código:

struct pair { int i1,i2; };
struct trio { int i1,i2,i3; };

void hey(struct pair *p, struct trio *t)
{
  p->i1++;
  t->i1^=1;
  p->i1--;
  t->i1^=1;
}

executando as seguintes etapas na ordem: incremente o primeiro membro de *p, complemente o bit mais baixo do primeiro membro de *t, em seguida, diminua o primeiro membro de *pe complemente o bit mais baixo do primeiro membro de *t. Compiladores modernos reorganizarão a sequência de operações de uma maneira que codifique qual será mais eficiente se pe tidentificar objetos diferentes, mas mudará o comportamento se não o fizerem.

É claro que este exemplo é deliberadamente artificial e, na prática, o código que usa um ponteiro de um tipo para acessar membros que fazem parte da sequência inicial comum de outro tipo geralmente funciona, mas infelizmente, já que não há como saber quando esse código pode falhar não é possível usá-lo com segurança, exceto desativando a análise de aliasing baseada em tipo.

Um exemplo um pouco menos artificial poderia ocorrer se alguém quisesse escrever uma função para fazer algo como trocar dois ponteiros por tipos arbitrários. Na grande maioria dos compiladores "C da década de 90", isso poderia ser realizado através de:

void swap_pointers(void **p1, void **p2)
{
  void *temp = *p1;
  *p1 = *p2;
  *p2 = temp;
}

No padrão C, no entanto, seria necessário usar:

#include "string.h"
#include "stdlib.h"
void swap_pointers2(void **p1, void **p2)
{
  void **temp = malloc(sizeof (void*));
  memcpy(temp, p1, sizeof (void*));
  memcpy(p1, p2, sizeof (void*));
  memcpy(p2, temp, sizeof (void*));
  free(temp);
}

Se *p2for mantido no armazenamento alocado e o ponteiro temporário não for mantido no armazenamento alocado, o tipo efetivo de *p2se tornará o tipo do ponteiro temporário e o código que tenta usar *p2como qualquer tipo que não corresponda ao ponteiro temporário type invocará o comportamento indefinido. É extremamente improvável que um compilador note isso, mas como a filosofia moderna do compilador exige que os programadores evitem o comportamento indefinido a todo custo, não consigo pensar em outro meio seguro de escrever o código acima sem usar o armazenamento alocado .

supercat
fonte
Downvoters: Gostaria de comentar? Um aspecto fundamental da programação orientada a objetos é a capacidade de vários tipos compartilharem aspectos comuns e fazer com que um ponteiro para qualquer tipo seja utilizável para acessar esses aspectos comuns. O exemplo do OP não faz isso, mas mal arranha a superfície de ser "orientado a objetos". Os compiladores históricos de C permitiriam que o código polimórfico fosse escrito de uma maneira muito mais limpa do que é possível no padrão C. de hoje. O design de código orientado a objetos em C exige, portanto, que se determine qual idioma exato está sendo direcionado. Com que aspecto as pessoas discordam?
Supercat
Hm ... lembre-se de mostrar como as garantias que o padrão oferece não permitem que você acesse com clareza os membros da subseqüência inicial comum? Porque eu acho que é isso que você quer dizer sobre os males da ousadia de otimizar dentro dos limites do comportamento contratual que depende dessa época? (Essa é a minha adivinhar o que os dois downvoters encontrado censurável.)
Deduplicator
OOP não requer necessariamente herança, portanto, a compatibilidade entre duas estruturas não é um grande problema na prática. Posso obter OO real colocando ponteiros de função em uma estrutura e invocando essas funções de uma maneira específica. Obviamente, esse foo_function(foo*, ...)pseudo-OO em C é apenas um estilo de API específico que se parece com classes, mas deveria ser chamado de programação modular com tipos de dados abstratos.
amon
@Duplicador: Veja o exemplo indicado. O campo "i1" é um membro da sequência inicial comum de ambas as estruturas, mas os compiladores modernos falharão se o código tentar usar um "par de estruturas *" para acessar o membro inicial de um "trio de estruturas".
Supercat
Qual compilador C moderno falha nesse exemplo? Alguma opção interessante é necessária?
Deduplicator
-3

O próximo passo é ocultar a declaração struct. Você coloca isso no arquivo .h:

typedef struct foo_s foo_t;

foo_t * foo_new(...);
void foo_destroy(foo_t *foo);
some_type foo_whatever(foo_t *foo, ...);
...

E então no arquivo .c:

struct foo_s {
    ...
};
Salomão Lento
fonte
6
Esse pode ser o próximo passo, dependendo do objetivo. Seja ou não, isso nem remotamente tenta responder à pergunta.
Deduplicator