Comment penser en tant que programmeur C après avoir biaisé avec le langage POO? [fermé]

38

Auparavant, je n'utilisais que des langages de programmation orientés objet (C ++, Ruby, Python, PHP) et j'apprends maintenant le C. J'ai du mal à trouver la bonne façon de faire les choses dans un langage sans concept 'Objet'. Je me rends compte qu'il est possible d'utiliser des paradigmes de POO en C, mais j'aimerais apprendre la méthode C-idiomatique.

Lors de la résolution d'un problème de programmation, la première chose que je fais est d'imaginer un objet qui résoudra le problème. Quelles étapes dois-je remplacer par ceci lors de l’utilisation d’un paradigme de programmation impérative non-POO?

Mas Bagol
la source
15
Je n'ai pas encore trouvé de langage qui corresponde parfaitement à ma façon de penser. Je dois donc «compiler» mes idées pour chaque langage que j'utilise. Un concept que j'ai trouvé utile est celui d'une «unité de code», qu'il s'agisse d'une étiquette, d'un sous-programme, d'une fonction, d'un objet, d'un module ou d'un framework: chacun d'entre eux doit être encapsulé et présenter une interface bien définie. Si vous utilisez une approche descendante au niveau des objets, vous pouvez commencer par créer un ensemble de fonctions qui se comportent comme si le problème avait été résolu. Souvent, des API C bien conçues ressemblent à la POO, mais qux = foo.bar(baz)deviennent qux = Foo_bar(foo, baz).
amon
Pour écho amon , mettant l' accent sur les points suivants: le graphique en forme de structure de données, des pointeurs, des algorithmes, l'exécution (flux de contrôle) d' un code (fonctions), des pointeurs de fonction.
Juin
1
LibTiff (code source sur github) est un exemple d’organisation de gros programmes C.
Rwong
1
En tant que programmeur C #, je manquerais beaucoup plus de délégués (indicateurs de fonction avec un paramètre lié) que d'objets.
CodesInChaos
J'ai personnellement trouvé l'essentiel de C facile et simple, à l'exception notable du pré-processeur. Si je devais réapprendre le C, ce serait un domaine sur lequel je concentrerais beaucoup d'efforts.
biziclop

Réponses:

53
  • Programme AC est un ensemble de fonctions.
  • Une fonction est un ensemble d'énoncés.
  • Vous pouvez encapsuler des données avec un fichier struct.

C'est ça.

Comment as-tu écrit un cours? C'est à peu près comment vous écrivez un fichier .C. Certes, vous n'obtenez pas des éléments comme le polymorphisme de méthode et l'héritage, mais vous pouvez quand même simuler ceux avec des noms de fonction et une composition différents .

Pour ouvrir la voie, étudiez la programmation fonctionnelle. C'est vraiment étonnant ce que l'on peut faire sans cours, et certaines choses fonctionnent mieux sans les frais généraux des cours.

Lectures
suggérées Orientation objet en ANSI C

Robert Harvey
la source
9
vous pouvez aussi faire typedefça structet faire quelque chose de classe . Les typedeftypes et -ed peuvent être inclus dans d'autres structs qui eux-mêmes peuvent être typedef-ed. Ce que vous n’obtenez pas avec C, c’est la surcharge d’opérateurs et l’héritage superficiel et simple des classes et des membres qu’il contient en C ++. et vous n'obtenez pas beaucoup de syntaxe étrange et contre nature que vous obtenez avec C ++. J'aime vraiment le concept de la programmation orientée objet, mais je pense que C ++ est une réalisation laide de la programmation orientée objet. i comme C , car il est une langue plus petite et les feuilles sur la syntaxe de la langue qui est préférable de laisser aux fonctions.
robert bristow-johnson
22
En tant que personne dont la langue maternelle était / est C, je me permettrais de le dire . a lot of things actually work better without the overhead of classes
haneefmubarak
1
Pour se développer, beaucoup de choses ont été développées sans POO: systèmes d'exploitation, serveurs de protocoles, chargeurs de démarrage, navigateurs, etc. Les ordinateurs ne pensent pas en termes d'objets et n'en ont pas non plus besoin. En effet, il leur est souvent très lent de le forcer.
Edmz
Contrepoint: a lot of things actually work better with addition of class-based OOP. Source: TypeScript, Dart, CoffeeScript et toutes les autres méthodes utilisées par l’industrie pour s’éloigner du langage OOP fonctionnel / prototype.
Den
Pour se développer, beaucoup de choses ont été développées avec OOP: tout le reste. Les humains pensent naturellement en termes d’objets et les programmes sont écrits pour que les autres humains les lisent et les comprennent.
Den
18

Lisez SICP et découvrez Scheme, ainsi que l’ idée pratique des types de données abstraits . Il est alors facile de coder en C (car avec SICP, un peu de C, et un peu de PHP, Ruby, etc ...), votre réflexion serait suffisamment élargie et vous comprendriez que la programmation orientée objet peut ne pas être le meilleur style en tous les cas, mais seulement pour certains types de programmes). Faites attention à l'allocation de mémoire dynamique en C , qui est probablement la partie la plus difficile. Le langage de programmation standard C99 ou C11 et sa bibliothèque standard C sont en fait assez médiocres (on ne connaît pas le protocole TCP ni les répertoires!), Et vous aurez souvent besoin de bibliothèques externes ou d’interfaces (par exemplePOSIX , libcurl pour la bibliothèque client HTTP, libonion pour la bibliothèque de serveur HTTP, GMPlib pour les fichiers bignums, une bibliothèque comme libunistring pour UTF-8, etc ...).

Vos "objets" sont souvent en C, des struct-s liés , et vous définissez l’ensemble des fonctions qui les exploitent. Pour des fonctions courtes ou très simples, pensez à les définir, avec les éléments pertinents struct, comme static inlinedans certains fichiers d’en-tête qui foo.hdoivent être #include-d ailleurs.

Notez que la programmation orientée objet n'est pas le seul paradigme de programmation . Dans certains cas, d'autres paradigmes sont intéressants ( programmation fonctionnelle à la Ocaml ou Haskell ou encore Scheme ou Commmon Lisp, programmation logique à la Prolog, etc. etc ... Lisez également le blog de J.Pitrat sur l'intelligence artificielle déclarative). Voir le livre de Scott: Programming Language Pragmatique

En réalité, un programmeur en C ou en Ocaml ne souhaite généralement pas coder dans un style de programmation orienté objet. Il n'y a aucune raison de vous forcer à penser à des objets quand cela ne sert à rien.

Vous en définirez certaines structet leurs fonctions (souvent par le biais de pointeurs). Vous pourriez avoir besoin de quelques unions étiquetées (souvent, un structmembre avec un tag, souvent enum, et d’autres à l’ unionintérieur), et il pourrait être utile d’avoir un membre de tableau flexible à la fin de certains de vos struct-s.

Regardez à l'intérieur du code source de certains logiciels libres existants en C (voir github & sourceforge pour en trouver). Il est probablement utile d’installer et d’utiliser une distribution Linux: celle-ci n’est constituée que de logiciels libres, elle dispose d’excellents compilateurs de logiciels libres ( GCC , Clang / LLVM ) et d’outils de développement. Voir aussi Programmation Linux avancée si vous souhaitez développer pour Linux.

N'oubliez pas de compiler avec tous les avertissements et informations de débogage, par exemple, gcc -Wall -Wextra -gnotamment pendant les phases de développement et de débogage, et d'apprendre à utiliser certains outils, par exemple, valgrind pour chasser les fuites de mémoire , le gdbdébogueur, etc. Veillez à bien comprendre ce qui n'est pas défini comportement et évitez-le fortement (rappelez-vous qu'un programme peut avoir un UB et parfois sembler "fonctionner").

Lorsque vous avez réellement besoin de constructions orientées objet (notamment l' héritage ), vous pouvez utiliser des pointeurs sur des structures et des fonctions associées. Vous pouvez avoir votre propre machine vtable , chaque objet commençant par un pointeur sur un structpointeur de fonction contenant. Vous tirez parti de la possibilité de convertir un type de pointeur en un autre type de pointeur (et du fait que vous pouvez convertir à partir d'un struct super_stcontenant les mêmes types de champs que ceux commençant par struct sub_stémuler un héritage). Notez que C est suffisant pour implémenter des systèmes d’objets assez sophistiqués - en particulier en respectant certaines conventions -, comme le montre GObject (de GTK / Gnome).

Lorsque vous avez réellement besoin de fermetures , vous les imitez souvent avec des rappels , avec la convention selon laquelle chaque fonction utilisant un rappel est transmise à la fois à un pointeur de fonction et à certaines données client (utilisées par le pointeur de la fonction lorsqu'il appelle cela). Vous pourriez aussi avoir (de manière conventionnelle) vos propres struct-s de type fermeture (contenant un pointeur de fonction et les valeurs fermées).

Comme le langage C est un langage de très bas niveau, il est important de définir et de documenter vos propres conventions (inspirées de la pratique des autres programmes C), en particulier en ce qui concerne la gestion de la mémoire, et probablement aussi certaines conventions de dénomination. Il est utile d’avoir une idée de l’ architecture des jeux d’instructions . Ne pas oublier que un C compilateur peut faire beaucoup d' optimisations sur votre code (si vous lui demandez), il ne se soucie pas trop de faire des micro-optimisations à la main, que l' autorisation à votre compilateur ( gcc -Wall -O2pour la compilation optimisée de sortie Logiciel). Si vous vous souciez de l'analyse comparative et des performances brutes, vous devez activer les optimisations (une fois que votre programme a été débogué).

N'oubliez pas que parfois, la métaprogrammation est utile . Assez souvent, les gros logiciels écrits en C contiennent des scripts ou des programmes ad-hoc pour générer du code C utilisé ailleurs (et vous pouvez également jouer à des astuces de préprocesseur sales , par exemple X-macros ). Il existe des générateurs de programmes C utiles (par exemple, yacc ou gnu bison pour générer des analyseurs syntaxiques, gperf pour générer des fonctions de hachage parfait, etc.). Sur certains systèmes (notamment Linux et POSIX), vous pouvez même générer du code C à l'exécution generated-001.c, le compiler en un objet partagé en exécutant une commande (comme gcc -O -Wall -shared -fPIC generated-001.c -o generated-001.so) à l'exécution, charger cet objet partagé de manière dynamique à l'aide de dlopen.& récupère un pointeur de fonction à partir d’un nom utilisant dlsym . Je fais de telles astuces dans MELT (un langage spécifique au domaine, similaire à Lisp, qui pourrait vous être utile, car il permet la personnalisation du compilateur GCC ).

Soyez conscient des concepts et des techniques de collecte des déchets (le comptage de références est souvent une technique de gestion de la mémoire en C, et IMHO est une forme de collecte des déchets médiocre qui ne traite pas bien les références circulaires ; vous pourriez avoir des pointeurs faibles pour vous aider, mais cela peut être délicat). À certaines occasions, vous pourriez envisager d'utiliser le récupérateur de déchets conservateur de Boehm .

Basile Starynkevitch
la source
7
Honnêtement, indépendamment de cette question, la lecture de SICP est sans aucun doute un bon conseil, mais pour le PO, cela mènera probablement à la question suivante: "Comment penser en tant que programmeur C après avoir privilégié le SICP".
Doc Brown
1
Non, car les schémas de SICP et PHP (ou Ruby ou Python) sont si différents que le PO aurait une réflexion beaucoup plus large. et SICP explique assez bien ce qu'est le type de données abstrait en pratique, ce qui est très utile pour comprendre, en particulier pour le codage en C.
Basile Starynkevitch
1
SICP est une suggestion étrange. Scheme est très différent de C.
Brian Gordon
Mais SICP enseigne beaucoup de bonnes habitudes et savoir que Scheme aide à coder en C (pour les concepts de fermeture, de types de données abstraits, etc.)
Basile Starynkevitch
5

La façon dont le programme est construit consiste essentiellement à définir quelles actions (fonctions) doivent être exécutées afin de résoudre le problème (c'est pourquoi il est appelé langage procédural). Chaque action correspondra à une fonction. Vous devez ensuite définir le type d’informations que chaque fonction recevra et les informations qu’elles doivent renvoyer.

Le programme est normalement séparé en fichiers (modules). Chaque fichier aura normalement un groupe de fonctions liées. Au début de chaque fichier, vous déclarez (en dehors de toute fonction) les variables qui seront utilisées par toutes les fonctions de ce fichier. Si vous utilisez le qualificatif "statique", ces variables ne seront visibles qu'à l'intérieur de ce fichier (mais pas à partir d'autres fichiers). Si vous n'utilisez pas de qualificatif "statique" sur les variables définies en dehors des fonctions, ils seront également accessibles à partir d'autres fichiers. Ces autres fichiers doivent déclarer la variable comme "extern" (mais pas la définir) afin que le compilateur les recherche. dans d'autres fichiers.

En bref, vous pensez d’abord aux procédures (fonctions), puis vous vous assurez que toutes les fonctions ont accès aux informations dont elles ont besoin.

Mandrill
la source
3

Les API C ont souvent - peut-être même, généralement - une interface essentiellement orientée objet si vous les regardez correctement.

En C ++:

class foo {
    public:
        foo (int x);
        void bar (int param);
    private:
        int x;
};

// Example use:
foo f(42);
f.bar(23);

En C:

typedef struct {
    int x;
} foo;

void bar (foo*, int param);

// Example use:
foo f = { .x = 42 };
bar(&f, 23);

Comme vous le savez peut-être, en C ++ et dans divers autres langages OO formels, une méthode objet prend sous le capot un premier argument qui est un pointeur sur l'objet, un peu comme la version C bar()ci - dessus. Pour un exemple de ce qui apparaît à la surface en C ++, voyez comment std::bindutiliser les méthodes d'objet pour ajuster les signatures de fonction:

new function<void(int)> (
    bind(&foo::bar, this, placeholders::_1)
//                  ^^^^ object pointer as first arg
);

Comme d'autres l'ont fait remarquer, la vraie différence réside dans le fait que les langages OO formels peuvent implémenter un polymorphisme, un contrôle d'accès et diverses autres fonctionnalités astucieuses. Mais l'essence de la programmation orientée objet, la création et la manipulation de structures de données discrètes et complexes, est déjà une pratique fondamentale en C.

boucle d'or
la source
2

L'une des principales raisons pour lesquelles les gens sont encouragés à apprendre le C est que c'est l'un des langages de programmation de haut niveau les plus bas. Les langages POO facilitent la réflexion sur les modèles de données et la modélisation du passage du code et des messages, mais à la fin de la journée, un microprocesseur exécute le code pas à pas, sautant dans des blocs de code (fonctions en C) et se déplaçant. des références à des variables (pointeurs en C) autour afin que différentes parties d’un programme puissent partager des données. Pensez à C comme langage d'assemblage en anglais - fournissant des instructions étape par étape au microprocesseur de votre ordinateur - et vous ne vous tromperez pas. En prime, la plupart des interfaces de système d’exploitation fonctionnent comme des appels de fonction C plutôt que comme des paradigmes de POO,

Gaurav
la source
2
IMHO C est un langage de bas niveau, mais beaucoup plus élevé que l'assembleur ou le code machine, car le compilateur C pourrait faire beaucoup d'optimisations de bas niveau.
Basile Starynkevitch
Les compilateurs C s’orientent également, au nom de "l’optimisation", vers un modèle de machine abstrait qui pourrait annuler les lois du temps et de la causalité lorsqu’une entrée serait causée par un comportement indéfini, même si le comportement naturel du code sur la machine est exécuté répondrait autrement aux exigences. Par exemple, la fonction uint16_t blah(uint16_t x) {return x*x;}fonctionnera de manière identique sur les machines de unsigned int16 bits ou de 33 bits ou plus. Cependant, certains compilateurs pour des machines de unsigned int17 à 32 bits peuvent considérer un appel à cette méthode ...
Supercat
... comme autoriser le compilateur à déduire qu'aucune chaîne d'événements susceptibles de donner à la méthode une valeur supérieure à 46340 ne pourrait éventuellement se produire. Même si la multiplication de 65533u * 65533u sur n’importe quelle plate-forme génèrerait une valeur égale à uint16_t9, le Standard n’impose pas de tels comportements lors de la multiplication de valeurs de type uint16_tsur des plates-formes de 17 à 32 bits.
Supercat
-1

Je suis aussi un natif OO (C ++ en général) qui doit parfois survivre dans un monde de C. Pour moi, le plus gros obstacle consiste fondamentalement à gérer les erreurs et à gérer les ressources.

En C ++, nous devons jeter une erreur d'où cela arrive jusqu'au niveau supérieur où nous pouvons le traiter et nous avons des destructeurs pour libérer automatiquement notre mémoire et d'autres ressources.

Vous remarquerez peut-être que de nombreuses API C incluent une fonction init qui vous fournit un typedef'd void *, qui est en réalité un pointeur sur une structure. Ensuite, vous transmettez ceci en tant que premier argument pour chaque appel d'API. Cela devient essentiellement votre pointeur "ceci" de C ++. Il est utilisé pour toutes les structures de données internes qui sont cachées (un concept très orienté objet). Vous pouvez également l'utiliser pour gérer la mémoire, par exemple avoir une fonction appelée myapiMalloc qui malloque votre mémoire et enregistre le malloc dans votre version C d'un pointeur this afin que vous puissiez vous assurer qu'il est libéré lors du retour de votre API. En outre, comme je l’ai récemment découvert, vous pouvez l’utiliser pour stocker des codes d’erreur et utiliser setjmp et longjmp pour vous donner un comportement très similaire à celui de la capture. La combinaison des deux concepts vous donne une grande partie des fonctionnalités d'un programme C ++.

Maintenant, vous avez dit que vous ne vouliez pas apprendre à forcer C en C ++. Ce n'est pas vraiment ce que je décris (du moins pas délibérément). C'est simplement une méthode (espérons-le) bien conçue pour exploiter la fonctionnalité C. Certaines versions de OO ont peut-être été utilisées. C'est peut-être pour cette raison que les langages OO ont été développés. Ils étaient un moyen de formaliser / appliquer / faciliter les concepts que certaines personnes trouvaient être la meilleure pratique.

Si vous pensez que cela vous fait peur, l’alternative est d’avoir à peu près toutes les fonctions qui renvoient un code d’erreur que vous devez vous assurer religieusement de vérifier après chaque appel de fonction et de propager la pile d’appels. Vous devez vous assurer que toutes les ressources sont libérées non seulement à la fin de chaque fonction, mais à chaque point de retour (éventuellement après tout appel de fonction pouvant renvoyer une erreur indiquant que vous ne pouvez pas continuer). Cela peut devenir très fastidieux et vous faire penser que je n’ai probablement pas besoin de faire face à cette défaillance potentielle d’allocation de mémoire (ou de lecture de fichier ou de connexion de port ...), je suppose simplement que cela fonctionnera ou que je " ll écrira le code "intéressant" maintenant et reviendra et traitera le traitement des erreurs - ce qui ne se produit jamais.

Phil Rosenberg
la source