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".
Respostas:
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 usofoo_<function>(struct foo *foo, <parameters>);
. Isso permite que vocêstruct foo
seja 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 porcurl_easy_init()
.Existem abordagens ainda mais genéricas, consulte Programação orientada a objetos com ANSI-C
fonte
std::string
, você não poderiafoo::create
? Eu não uso C. Talvez isso seja apenas em C ++?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.
fonte
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
cleanup
atributo que destruirá adequadamente seus objetos. Se você declarar seu objeto assim: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, porqueC
não possui espaços para nome e é fácil colidir com outro nome de função.fonte
#define
usar seus nomes de tipo, você o__attribute__((cleanup(my_str_destructor)))
encontrará como implícito em todo o#define
escopo (ele será adicionado a todas as suas declarações de variáveis).#define
tipo d ou suas matrizes). Em resumo: não é padrão C e você paga com muita inflexibilidade em uso.__attribute__((cleanup()))
praticamente um quase-padrão. No entanto, b) e c) ainda estão de pé ...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:
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*p
e 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 sep
et
identificar 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:
No padrão C, no entanto, seria necessário usar:
Se
*p2
for mantido no armazenamento alocado e o ponteiro temporário não for mantido no armazenamento alocado, o tipo efetivo de*p2
se tornará o tipo do ponteiro temporário e o código que tenta usar*p2
como 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 .fonte
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.O próximo passo é ocultar a declaração struct. Você coloca isso no arquivo .h:
E então no arquivo .c:
fonte