Ist es schlecht, objektorientiertes C zu schreiben? [geschlossen]

14

Ich scheine immer Code in C zu schreiben, der hauptsächlich objektorientiert ist. Sagen wir also, ich hätte eine Quelldatei oder etwas, das ich erstellen würde. Dann übergebe ich den Zeiger auf diese Struktur an Funktionen (Methoden), die zu dieser Struktur gehören:

struct foo {
    int x;
};

struct foo* createFoo(); // mallocs foo

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

Ist das schlechte Praxis? Wie lerne ich, C "richtig" zu schreiben?

mosmo
quelle
10
Ein Großteil von Linux (dem Kernel) ist so geschrieben, in der Tat emuliert es sogar noch mehr OO-ähnliche Konzepte wie das Versenden virtueller Methoden. Ich halte das für ziemlich richtig.
Kilian Foth
13
" [D] er entschied, dass Real Programmer FORTRAN-Programme in jeder Sprache schreiben kann. " - Ed Post, 1983
Ross Patterson,
4
Gibt es einen Grund, warum Sie nicht zu C ++ wechseln möchten? Sie müssen nicht die Teile verwenden, die Sie nicht mögen.
Svick
5
Dies wirft wirklich die Frage auf: "Was ist objektorientiert?" Ich würde das nicht objektorientiert nennen. Ich würde sagen, es ist prozedural. (Sie haben keine Vererbung, keinen Polymorphismus, keine Einkapselung / Fähigkeit, den Zustand zu verbergen, und es fehlen wahrscheinlich andere Kennzeichen von OO, die mir nicht auf den Kopf kommen.) Ob es sich um eine gute oder eine schlechte Praxis handelt, hängt nicht von dieser Semantik ab obwohl.
jpmc26
3
@ jpmc26: Wenn Sie ein Sprachwissenschaftler sind, sollten Sie Alan Kay zuhören, er hat den Begriff erfunden, er kann sagen, was er bedeutet, und er sagt, bei OOP dreht sich alles um Messaging . Wenn Sie ein linguistischer Deskriptivist sind, würden Sie die Verwendung des Begriffs in der Softwareentwicklungs-Community untersuchen. Cook hat genau das getan, er hat die Merkmale von Sprachen analysiert, die entweder behaupten oder als OO gelten, und er hat festgestellt, dass sie eines gemeinsam haben: Messaging .
Jörg W Mittag

Antworten:

24

Nein, dies ist keine schlechte Praxis, es wird sogar empfohlen, dies zu tun, obwohl man sogar Konventionen wie struct foo *foo_new();und verwenden könntevoid foo_free(struct foo *foo);

Natürlich, wie es in einem Kommentar heißt, tun Sie dies nur, wo es angebracht ist. Es hat keinen Sinn, einen Konstruktor für ein zu verwenden int.

Das Präfix foo_ist eine Konvention, die von vielen Bibliotheken befolgt wird, da es vor Konflikten mit der Benennung anderer Bibliotheken schützt. Andere Funktionen haben oft die Konvention zu verwenden foo_<function>(struct foo *foo, <parameters>);. Dies ermöglicht es Ihnen struct foo, ein undurchsichtiger Typ zu sein.

Werfen Sie einen Blick auf die libcurl-Dokumentation der Konvention, insbesondere mit "Subnamespaces", damit der Aufruf einer Funktion curl_multi_*auf den ersten Blick falsch aussieht, als der erste Parameter von zurückgegeben wurde curl_easy_init().

Es gibt noch allgemeinere Ansätze, siehe Objektorientierte Programmierung mit ANSI-C

Residuum
quelle
11
Immer mit der Einschränkung "wo angebracht". OOP ist keine Wunderwaffe.
Deduplikator
Hat C keine Namespaces, in denen Sie diese Funktionen deklarieren könnten? Ähnlich wie std::string, konntest du nicht foo::create? Ich benutze kein C. Vielleicht ist das nur in C ++?
Chris Cirefice
@ChrisCirefice In C gibt es keine Namespaces. Deshalb verwenden viele Bibliotheksautoren Präfixe für ihre Funktionen.
Residuum
2

Es ist nicht schlecht, es ist ausgezeichnet. Objektorientierte Programmierung ist eine gute Sache (es sei denn , Sie mitreißen, Sie können zu viel von einer guten Sache haben). C ist nicht die am besten geeignete Sprache für OOP, aber das sollte Sie nicht davon abhalten, das Beste daraus zu machen.

gnasher729
quelle
4
Nicht, dass ich dem widersprechen könnte, aber Ihre Meinung sollte wirklich durch eine Ausarbeitung gestützt werden .
Deduplizierer
1

Es ist nicht schlecht. Es wird die Verwendung von RAII empfohlen , wodurch viele Fehler vermieden werden (Speicherverluste, Verwendung nicht initialisierter Variablen, Verwendung nach dem Freischalten usw., die Sicherheitsprobleme verursachen können).

Wenn Sie also Ihren Code nur mit GCC oder Clang kompilieren möchten (und nicht mit MS Compiler), können Sie cleanupAttribute verwenden, die Ihre Objekte ordnungsgemäß zerstören. Wenn Sie Ihr Objekt so deklarieren:

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

Dann my_str_destructor(ptr)wird ausgeführt, wenn ptr Spielraum erlischt. Denken Sie daran, dass es nicht mit Funktionsargumenten verwendet werden kann .

Denken Sie auch daran, my_str_in Ihren Methodennamen zu verwenden, da Ces keine Namespaces gibt und es leicht ist, mit einem anderen Funktionsnamen zu kollidieren.

Marqin
quelle
2
In Afaik, RAII geht es darum, den impliziten Aufruf von Destruktoren für Objekte in C ++ zu verwenden, um die Bereinigung zu gewährleisten und das Hinzufügen expliziter Aufrufe zur Ressourcenfreigabe zu vermeiden. Wenn ich mich also nicht sehr irre, schließen sich RAII und C gegenseitig aus.
cmaster
@cmaster Wenn Sie #defineIhre Typnamen verwenden möchten, erhalten __attribute__((cleanup(my_str_destructor)))Sie diese implizit im gesamten #defineGültigkeitsbereich (sie werden allen Variablendeklarationen hinzugefügt).
Marqin
Das funktioniert, wenn a) Sie gcc verwenden, b) wenn Sie den Typ nur in lokalen Funktionsvariablen verwenden, und c) wenn Sie den Typ nur in einer nackten Version verwenden (keine Zeiger auf den #defineTyp 'd oder Arrays davon). Kurzum: Es ist kein Standard-C, und Sie zahlen mit viel Inflexibilität im Einsatz.
cmaster
Wie in meiner Antwort erwähnt, funktioniert das auch in Klänge.
Marqin
Ah, das habe ich nicht bemerkt. Das macht die Anforderung a) in der Tat deutlich weniger streng, da dies so __attribute__((cleanup()))ziemlich einen Quasi-Standard darstellt. Allerdings stehen b) und c) noch ...
cmaster - monica
-2

Ein solcher Code kann viele Vorteile haben, aber leider wurde der C-Standard nicht geschrieben, um ihn zu vereinfachen. Compiler haben in der Vergangenheit effektive Verhaltensgarantien geboten, die über die Anforderungen des Standards hinausgehen und es ermöglichten, solchen Code viel sauberer zu schreiben als dies in Standard C möglich ist. In letzter Zeit widerrufen Compiler solche Garantien jedoch im Namen der Optimierung.

Insbesondere haben viele C-Compiler in der Vergangenheit garantiert (von Entwurf bis Dokumentation), dass, wenn zwei Strukturtypen dieselbe anfängliche Sequenz enthalten, ein Zeiger auf einen der beiden Typen verwendet werden kann, um auf Mitglieder dieser gemeinsamen Sequenz zuzugreifen, auch wenn die Typen nicht in Beziehung stehen. und weiter, dass zum Zwecke des Erstellens einer gemeinsamen Anfangssequenz alle Zeiger auf Strukturen äquivalent sind. Code, der von einem solchen Verhalten Gebrauch macht, kann viel sauberer und typsicherer sein als Code, der dies nicht tut. Obwohl der Standard jedoch vorschreibt, dass Strukturen, die eine gemeinsame Anfangssequenz haben, auf die gleiche Weise angelegt werden müssen, verbietet er die tatsächliche Verwendung von Code Ein Zeiger eines Typs, um auf die ursprüngliche Sequenz eines anderen Typs zuzugreifen.

Wenn Sie objektorientierten Code in C schreiben möchten, müssen Sie sich (und sollten Sie diese Entscheidung frühzeitig treffen) entscheiden, entweder durch viele Rahmen zu springen, um die Zeigerregeln von C einzuhalten, und sich darauf vorzubereiten moderne Compiler generieren unsinnigen Code, wenn man aus dem Ruder läuft, auch wenn ältere Compiler Code generiert hätten, der wie vorgesehen funktioniert, oder dokumentieren eine Anforderung, dass der Code nur mit Compilern verwendet werden kann, die so konfiguriert sind, dass sie das Verhalten von Zeigern im alten Stil unterstützen (z. B. mit ein "-fno-strict-aliasing") Einige Leute betrachten "-fno-strict-aliasing" als böse, aber ich würde vorschlagen, dass es hilfreicher ist, sich "-fno-strict-aliasing" C als eine Sprache vorzustellen, die bietet für einige Zwecke eine größere semantische Kraft als "Standard" C,aber auf Kosten von Optimierungen, die für andere Zwecke wichtig sein könnten.

Bei herkömmlichen Compilern interpretieren historische Compiler beispielsweise den folgenden Code:

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

B. Ausführen der folgenden Schritte in der Reihenfolge: Inkrementieren des ersten Elements von *p, Ergänzen des niedrigsten Bits des ersten Elements von *t, Dekrementieren des ersten Elements von *pund Ergänzen des niedrigsten Bits des ersten Elements von *t. Moderne Compiler ordnen die Abfolge der Operationen auf eine Weise neu, die effizienter ist, wenn pund tverschiedene Objekte identifiziert werden, ändern jedoch das Verhalten, wenn dies nicht der Fall ist .

Dieses Beispiel wurde natürlich absichtlich erfunden, und in der Praxis funktioniert Code, der einen Zeiger eines Typs verwendet, um auf Mitglieder zuzugreifen, die Teil der allgemeinen Anfangssequenz eines anderen Typs sind, normalerweise , aber leider gibt es keine Möglichkeit zu wissen, wann ein solcher Code fehlschlagen könnte Die sichere Verwendung ist nur durch Deaktivieren der typbasierten Aliasing-Analyse möglich.

Ein etwas weniger ausgeklügeltes Beispiel würde auftreten, wenn man eine Funktion schreiben möchte, um so etwas wie zwei Zeiger auf beliebige Typen zu tauschen. In der überwiegenden Mehrheit der "1990er C" -Compiler konnte dies erreicht werden durch:

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

In Standard C müsste man jedoch verwenden:

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

Wenn *p2der temporäre Zeiger im zugewiesenen Speicher verbleibt und nicht im zugewiesenen Speicher, wird der effektive Typ von *p2zum Typ des temporären Zeigers und der Code, der versucht, ihn *p2als einen beliebigen Typ zu verwenden, der nicht mit dem temporären Zeiger übereinstimmt type ruft Undefiniertes Verhalten auf. Es ist zwar äußerst unwahrscheinlich, dass ein Compiler so etwas bemerkt, aber da die moderne Compilerphilosophie verlangt, dass Programmierer um jeden Preis Undefiniertes Verhalten vermeiden, kann ich mir keine andere sichere Methode zum Schreiben des obigen Codes vorstellen, ohne zugewiesenen Speicher zu verwenden .

Superkatze
quelle
Downvoters: Möchtest du einen Kommentar abgeben? Ein Schlüsselaspekt der objektorientierten Programmierung ist die Fähigkeit, dass mehrere Typen gemeinsame Aspekte haben und dass ein Zeiger auf einen solchen Typ verwendet werden kann, um auf diese gemeinsamen Aspekte zuzugreifen. Das Beispiel des OP macht das nicht, aber es kratzt kaum an der Oberfläche, "objektorientiert" zu sein. Historische C-Compiler würden es ermöglichen, polymorphen Code auf eine viel sauberere Art und Weise zu schreiben, als dies im heutigen Standard C möglich ist. Um objektorientierten Code in C zu entwerfen, muss daher bestimmt werden, auf welche Sprache genau man abzielt. Mit welchem ​​Aspekt stimmen die Menschen nicht überein?
Supercat
Hm ... möchten Sie vielleicht zeigen, wie die Garantien, die der Standard gibt, es Ihnen nicht erlauben, sauber auf Mitglieder der gemeinsamen anfänglichen Teilsequenz zuzugreifen? Weil ich denke, das ist es, worüber Sie sich schimpfen, wenn Sie es wagen, im Rahmen des vertraglichen Verhaltens zu optimieren, was diesmal ausschlaggebend ist? (Das ist meine Vermutung, was die beiden Downvoter als anstößig empfanden.)
Deduplizierer
OOP erfordert nicht unbedingt eine Vererbung, daher ist die Kompatibilität zwischen zwei Strukturen in der Praxis kein großes Problem. Ich kann echte OO erhalten, indem ich Funktionszeiger in eine Struktur setze und diese Funktionen auf eine bestimmte Weise aufrufe. Natürlich ist dieses foo_function(foo*, ...)Pseudo-OO in C nur ein bestimmter API-Stil, der wie Klassen aussieht, aber es sollte besser als modulare Programmierung mit abstrakten Datentypen bezeichnet werden.
Am
@ Deduplicator: Siehe das angegebene Beispiel. Feld "i1" ist ein Mitglied der gemeinsamen Anfangssequenz beider Strukturen, aber moderne Compiler schlagen fehl, wenn der Code versucht, mit einem "Strukturpaar *" auf das Anfangsmitglied eines "Strukturtrios" zuzugreifen.
Superkatze
Welcher moderne C-Compiler verfehlt dieses Beispiel? Interessante Optionen benötigt?
Deduplizierer
-3

Der nächste Schritt ist das Ausblenden der Strukturdeklaration. Sie fügen dies in die .h-Datei ein:

typedef struct foo_s foo_t;

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

Und dann in der .c-Datei:

struct foo_s {
    ...
};
Solomon Slow
quelle
6
Das könnte je nach Ziel der nächste Schritt sein. Unabhängig davon, ob dies der Fall ist oder nicht, versucht dies nicht einmal, die Frage remote zu beantworten.
Deduplizierer