Il me semble toujours écrire du code en C qui est principalement orienté objet, alors disons que j'avais un fichier source ou quelque chose que je créerais une structure puis passerais le pointeur vers cette structure aux fonctions (méthodes) appartenant à cette structure:
struct foo {
int x;
};
struct foo* createFoo(); // mallocs foo
void destroyFoo(struct foo* foo); // frees foo and its things
Est-ce une mauvaise pratique? Comment puis-je apprendre à écrire C de la "bonne façon".
Réponses:
Non, ce n'est pas une mauvaise pratique, c'est même encouragé à le faire, même si on pourrait même utiliser des conventions comme
struct foo *foo_new();
etvoid foo_free(struct foo *foo);
Bien sûr, comme le dit un commentaire, ne faites cela que lorsque cela est approprié. Cela n'a aucun sens d'utiliser un constructeur pour un
int
.Le préfixe
foo_
est une convention suivie par de nombreuses bibliothèques, car il protège contre les conflits avec la dénomination d'autres bibliothèques. D'autres fonctions ont souvent la convention à utiliserfoo_<function>(struct foo *foo, <parameters>);
. Cela vous permetstruct foo
d'être un type opaque.Jetez un œil à la documentation de libcurl pour la convention, en particulier avec les "subnamespaces", de sorte que l'appel d'une fonction
curl_multi_*
semble incorrect à première vue lorsque le premier paramètre a été renvoyé parcurl_easy_init()
.Il existe des approches encore plus génériques, voir Programmation orientée objet avec ANSI-C
la source
std::string
, ne pourriez-vous pas avoirfoo::create
? Je n'utilise pas C. Peut-être que c'est seulement en C ++?Ce n'est pas mal, c'est excellent. La programmation orientée objet est une bonne chose (à moins que vous ne vous emportiez, vous pouvez avoir trop d'une bonne chose). C n'est pas le langage le plus approprié pour la POO, mais cela ne devrait pas vous empêcher d'en tirer le meilleur parti.
la source
Ce n'est pas mauvais. Il approuve l'utilisation de RAII qui empêche de nombreux bugs (fuites de mémoire, utilisation de variables non initialisées, utilisation après libération, etc., ce qui peut entraîner des problèmes de sécurité).
Donc, si vous souhaitez compiler votre code uniquement avec GCC ou Clang (et non avec le compilateur MS), vous pouvez utiliser un
cleanup
attribut, qui détruira correctement vos objets. Si vous déclarez votre objet comme ça:Puis
my_str_destructor(ptr)
sera exécuté lorsque ptr sortira du cadre. Gardez juste à l'esprit qu'il ne peut pas être utilisé avec des arguments de fonction .N'oubliez pas non plus d'utiliser
my_str_
dans vos noms de méthode, car ilC
n'a pas d'espaces de noms, et il est facile d'entrer en collision avec un autre nom de fonction.la source
#define
utilisez vos noms de caractères,__attribute__((cleanup(my_str_destructor)))
vous les obtiendrez comme implicites dans toute la#define
portée (ils seront ajoutés à toutes vos déclarations de variables).#define
type 'd ou de tableaux de celui-ci). En bref: ce n'est pas du C standard, et vous payez avec beaucoup de rigidité à l'usage.__attribute__((cleanup()))
peu près une quasi-norme. Cependant, b) et c) sont toujoursIl peut y avoir de nombreux avantages à ce code, mais malheureusement, la norme C n'a pas été écrite pour le faciliter. Les compilateurs ont historiquement offert des garanties de comportement efficaces au-delà de ce que la norme exigeait qui permettait d'écrire un tel code beaucoup plus proprement que ce qui est possible dans la norme C, mais les compilateurs ont récemment commencé à révoquer ces garanties au nom de l'optimisation.
Plus particulièrement, de nombreux compilateurs C ont historiquement garanti (par conception sinon documentation) que si deux types de structure contiennent la même séquence initiale, un pointeur vers l'un ou l'autre type peut être utilisé pour accéder aux membres de cette séquence commune, même si les types ne sont pas liés, et en outre qu'aux fins de l'établissement d'une séquence initiale commune, tous les pointeurs vers les structures sont équivalents. Le code qui utilise un tel comportement peut être beaucoup plus propre et plus sûr que le code qui ne le fait pas, mais malheureusement, même si la norme exige que les structures partageant une séquence initiale commune doivent être disposées de la même manière, il interdit au code d'utiliser réellement un pointeur d'un type pour accéder à la séquence initiale d'un autre.
Par conséquent, si vous voulez écrire du code orienté objet en C, vous devrez décider (et devriez prendre cette décision tôt) de sauter à travers un grand nombre de cerceaux pour respecter les règles de type pointeur de C et être prêt à avoir les compilateurs modernes génèrent du code insensé si l'un d'eux glisse, même si des compilateurs plus anciens auraient généré du code qui fonctionne comme prévu, ou bien documentent une exigence selon laquelle le code ne sera utilisable qu'avec des compilateurs configurés pour prendre en charge le comportement de pointeur à l'ancienne (par exemple en utilisant un "-fno-strict-aliasing") Certaines personnes considèrent "-fno-strict-aliasing" comme mauvais, mais je dirais qu'il est plus utile de penser à "-fno-strict-aliasing" C comme étant un langage qui offre une puissance sémantique supérieure à certaines fins que le C "standard",mais au détriment des optimisations qui pourraient être importantes à d'autres fins.
À titre d'exemple, sur les compilateurs traditionnels, les compilateurs historiques interpréteraient le code suivant:
comme effectuer les étapes suivantes dans l'ordre: incrémenter le premier membre de
*p
, compléter le bit le plus bas du premier membre de*t
, puis décrémenter le premier membre de*p
, et compléter le bit le plus bas du premier membre de*t
. Les compilateurs modernes réorganiseront la séquence des opérations de manière à coder qui sera plus efficacep
ett
identifiera les différents objets, mais changeront le comportement s'ils ne le font pas.Cet exemple est bien sûr délibérément conçu, et dans la pratique, le code qui utilise un pointeur d'un type pour accéder aux membres qui font partie de la séquence initiale commune d'un autre type fonctionnera généralement , mais malheureusement puisqu'il n'y a aucun moyen de savoir quand un tel code pourrait échouer. il n'est pas possible de l'utiliser en toute sécurité, sauf en désactivant l'analyse d'alias basée sur le type.
Un exemple un peu moins artificiel se produirait si l'on voulait écrire une fonction pour faire quelque chose comme échanger deux pointeurs vers des types arbitraires. Dans la grande majorité des compilateurs «C des années 90», cela pourrait être accompli via:
Dans la norme C, cependant, il faudrait utiliser:
Si
*p2
est conservé dans le stockage alloué et que le pointeur temporaire n'est pas conservé dans le stockage alloué, le type effectif de*p2
deviendra le type du pointeur temporaire et le code qui tentera de l'utiliser*p2
comme n'importe quel type ne correspondant pas au pointeur temporaire type invoquera le comportement indéfini. Il est certain qu'il est extrêmement improbable qu'un compilateur remarque une telle chose, mais comme la philosophie moderne du compilateur exige que les programmeurs évitent à tout prix les comportements indéfinis, je ne peux penser à aucun autre moyen sûr d'écrire le code ci-dessus sans utiliser le stockage alloué. .la source
foo_function(foo*, ...)
pseudo-OO en C n'est qu'un style d'API particulier qui ressemble à des classes, mais il devrait être plus correctement appelé programmation modulaire avec des types de données abstraits.L'étape suivante consiste à masquer la déclaration struct. Vous mettez ceci dans le fichier .h:
Et puis dans le fichier .c:
la source