Wie man als C-Programmierer denkt, nachdem man mit der OOP-Sprache befasst ist? [geschlossen]

38

Bisher habe ich nur objektorientierte Programmiersprachen (C ++, Ruby, Python, PHP) verwendet und lerne jetzt C. Ich finde es schwierig, die richtige Art und Weise zu finden, Dinge in einer Sprache zu tun, für die es kein Konzept gibt 'Objekt'. Mir ist klar, dass es möglich ist, OOP-Paradigmen in C zu verwenden, aber ich würde gerne die C-Idiomatik lernen.

Wenn ich ein Programmierproblem löse, stelle ich mir zuerst ein Objekt vor, das das Problem löst. Durch welche Schritte ersetze ich dies, wenn ich ein Paradigma für die imperative Programmierung ohne OOP verwende?

Mas Bagol
quelle
15
Ich habe noch keine Sprache gefunden, die meiner Denkweise am nächsten kommt, daher muss ich meine Gedanken für jede Sprache, die ich verwende, „zusammenstellen“. Ein Konzept, das ich für nützlich befunden habe, ist das einer „Code-Einheit“, unabhängig davon, ob es sich um eine Bezeichnung, eine Unterroutine, eine Funktion, ein Objekt, ein Modul oder ein Framework handelt. Wenn Sie einen Ansatz von oben nach unten auf Objektebene verwenden, können Sie in C zunächst eine Reihe von Funktionen erstellen, die sich so verhalten, als ob das Problem gelöst worden wäre. Gut gestaltete C-APIs sehen oft wie OOP aus, werden aber qux = foo.bar(baz)zu qux = Foo_bar(foo, baz).
amon
Um amon wiederzugeben , konzentrieren Sie sich auf Folgendes: grafische Datenstruktur, Zeiger, Algorithmen, Ausführung (Kontrollfluss) von Code (Funktionen), Funktionszeiger.
rwong
1
LibTiff (Quellcode auf Github) ist ein Beispiel für die Organisation großer C-Programme.
rwong
1
Als C # -Programmierer würde ich Delegaten (Funktionszeiger mit einem gebundenen Parameter) weitaus mehr vermissen als Objekte.
CodesInChaos
Ich persönlich fand den größten Teil von C einfach und unkompliziert, mit Ausnahme des Vorprozessors. Wenn ich C neu lernen müsste, wäre dies ein Bereich, auf den ich mich sehr konzentrieren würde.
Biziclop

Antworten:

53
  • AC-Programm ist eine Sammlung von Funktionen.
  • Eine Funktion ist eine Sammlung von Anweisungen.
  • Sie können Daten mit einem kapseln struct.

Das ist es.

Wie hast du eine Klasse geschrieben? So schreiben Sie eine .C-Datei. Zugegeben, Sie erhalten keine Methodenpolymorphie und Vererbung, aber Sie können solche mit unterschiedlichen Funktionsnamen und -zusammensetzungen trotzdem simulieren .

Studieren Sie die Funktionale Programmierung, um den Weg zu ebnen . Es ist wirklich erstaunlich, was Sie ohne Unterricht tun können , und einige Dinge funktionieren tatsächlich besser, ohne den Aufwand für Unterricht.

Weitere Lektüre
Objektorientierung in ANSI C

Robert Harvey
quelle
9
Sie können auch typedefdas structund machen etwas klassen wie . und typedef-ed-Typen können in andere Typen eingeschlossen werden struct, die selbst typedef-ed sein können. Was Sie mit C nicht bekommen, ist das Überladen von Operatoren und die oberflächlich einfache Vererbung von Klassen und den darin enthaltenen Elementen in C ++. und Sie bekommen nicht viel seltsame und unnatürliche Syntax, die Sie mit C ++ bekommen. Ich liebe das Konzept von OOP wirklich, aber ich denke, C ++ ist eine hässliche Realisierung von OOP. i wie C , weil es ist eine kleinere Sprache und Blätter aus Syntax der Sprache, die am besten zu Funktionen verlassen wird.
Robert Bristow-Johnson
22
Als jemand, dessen Muttersprache C ist, würde ich es wagen, das zu sagen . a lot of things actually work better without the overhead of classes
Haneefmubarak
1
Um zu erweitern, wurden viele Dinge ohne OOP entwickelt: Betriebssysteme, Protokollserver, Bootloader, Browser und so weiter. Computer denken nicht in Objekten und müssen es auch nicht. Tatsächlich ist es für sie oft ziemlich langsam , dies zu erzwingen.
Edmz
Kontrapunkt : a lot of things actually work better with addition of class-based OOP. Quelle: TypeScript, Dart, CoffeeScript und alle anderen Methoden, mit denen die Industrie versucht, sich von einer funktionalen / prototypischen OOP-Sprache zu lösen.
Den
Um zu expandieren, wurden mit OOP viele Dinge entwickelt : alles andere. Der Mensch denkt natürlich in Objekten, und Programme sind so geschrieben, dass andere Menschen sie lesen und verstehen können.
Den
18

Lesen Sie SICP und lernen Sie Schema und die praktische Idee der abstrakten Datentypen . Dann ist das Codieren in C einfach (da mit SICP, ein bisschen C und ein bisschen PHP, Ruby usw. Ihre Überlegungen erweitert würden und Sie verstehen würden, dass objektorientierte Programmierung möglicherweise nicht der beste Stil in C ist in allen Fällen, aber nur für bestimmte Programme). Seien Sie vorsichtig mit der dynamischen Speicherzuweisung in C , was wahrscheinlich der schwierigste Teil ist. Der Programmiersprachenstandard C99 oder C11 und seine C-Standardbibliothek sind eigentlich ziemlich schlecht (TCP oder Verzeichnisse sind ihm nicht bekannt!), Und Sie benötigen häufig einige externe Bibliotheken oder Schnittstellen (z. B.POSIX , libcurl für die HTTP-Client-Bibliothek, libonion für die HTTP-Server-Bibliothek, GMPlib für Bignums, einige Bibliotheken wie libunistring für UTF-8 usw.).

Ihre "Objekte" sind häufig in C verwandte struct-s, und Sie definieren die Menge der Funktionen, die auf sie angewendet werden. Für kurze oder sehr einfache Funktionen sollten Sie sie mit der entsprechenden Definition structwie static inlinein einer Header-Datei definieren foo.h, die an #includeanderer Stelle eingefügt werden soll.

Beachten Sie, dass objektorientierte Programmierung nicht das einzige Programmierparadigma ist . In einigen Fällen lohnen sich andere Paradigmen ( funktionale Programmierung à la Ocaml oder Haskell oder sogar Schema oder Commmon Lisp, logische Programmierung à la Prolog usw. usw.). Lesen Sie auch J.Pitrats Blog über deklarative künstliche Intelligenz. Siehe Scotts Buch: Programming Language Pragmatics

Tatsächlich möchte ein Programmierer in C oder Ocaml normalerweise nicht in einem objektorientierten Programmierstil programmieren. Es gibt keinen Grund, sich dazu zu zwingen, an Gegenstände zu denken, wenn dies nicht nützlich ist.

Sie definieren einige structund die Funktionen, die auf ihnen ausgeführt werden (häufig durch Zeiger). Möglicherweise benötigen Sie einige getaggte Gewerkschaften (häufig eine structmit einem Tag-Mitglied, häufig einige enumund einige im unionInneren), und möglicherweise ist es hilfreich, am Ende einiger Ihrer -s ein flexibles Array-Mitglied zu haben struct.

Schauen Sie sich den Quellcode einiger freier Software in C an (siehe github & sourceforge , um einige zu finden). Wahrscheinlich wäre die Installation und Verwendung einer Linux-Distribution nützlich: Sie besteht fast nur aus freier Software, enthält großartige C-Compiler für freie Software ( GCC , Clang / LLVM ) und Entwicklungstools. Siehe auch Erweiterte Linux-Programmierung, wenn Sie für Linux entwickeln möchten.

Vergessen Sie nicht , mit allen Warnungen und Debug - Informationen, zum Beispiel zu kompilieren gcc -Wall -Wextra -g-notably bei der Entwicklung und Debugging phases- und lernen einige Werkzeuge zu verwenden, zB valgrind zu jagen Speicherlecks , der gdbDebugger usw. Achten Sie auf gut zu verstehen , was nicht definiert Verhalten und stark vermeiden Sie es (denken Sie daran, dass ein Programm einige UB haben könnte und manchmal zu "arbeiten" scheint).

Wenn Sie wirklich objektorientierte Konstrukte (insbesondere Vererbung ) benötigen , können Sie Zeiger auf verwandte Strukturen und auf Funktionen verwenden. Sie könnten Ihre eigene vtable- Maschinerie haben, wobei jedes "Objekt" mit einem Zeiger auf einen structenthaltenden Funktionszeiger beginnt . Sie profitieren von der Möglichkeit, einen Zeigertyp in einen anderen Zeigertyp umzuwandeln (und von der Tatsache, dass Sie einen struct super_stZeigertyp umwandeln können , der dieselben Feldtypen enthält wie diejenigen, die mit a beginnen struct sub_st, um die Vererbung zu emulieren). Beachten Sie, dass C ausreicht, um ziemlich ausgefeilte Objektsysteme zu implementieren - insbesondere unter Einhaltung einiger Konventionen -, wie GObject (von GTK / Gnome) demonstriert.

Wenn Sie wirklich brauchen , Verschlüsse , werden Sie oft emulieren sie mit Rückrufen , mit der Konvention , dass jede Funktion eines Rückruf mit beiden Zeigern einer Funktion übergeben wird und einige Client - Daten (von dem Funktionszeiger verbraucht wird, wenn es das nennt). Sie könnten auch (konventionell) Ihre eigenen schließungsähnlichen struct-s haben (die einen Funktionszeiger und die geschlossenen Werte enthalten).

Da C eine sehr einfache Sprache ist, ist es wichtig, eigene Konventionen zu definieren und zu dokumentieren (inspiriert von der Praxis in anderen C-Programmen), insbesondere in Bezug auf die Speicherverwaltung und möglicherweise auch einige Namenskonventionen. Es ist nützlich, eine Vorstellung von der Befehlssatzarchitektur zu haben . Vergessen Sie nicht, dass ein C- Compiler möglicherweise viele Optimierungen an Ihrem Code vornimmt (wenn Sie ihn dazu auffordern). Überlassen Sie die Mikrooptimierung also Ihrem Compiler ( gcc -Wall -O2für eine optimierte Kompilierung der veröffentlichten Versionen) Software). Wenn Sie sich für Benchmarking und Raw-Leistung interessieren, sollten Sie Optimierungen aktivieren (sobald Ihr Programm debuggt wurde).

Vergessen Sie nicht, dass Metaprogrammierung manchmal nützlich ist . In C geschriebene große Software enthält häufig Skripte oder Ad-hoc-Programme, um C-Code zu generieren, der an anderer Stelle verwendet wird (und möglicherweise spielen Sie auch einige schmutzige C-Präprozessor- Tricks ab, z. B. X-Makros ). Es gibt einige nützliche C-Programmgeneratoren (z. B. yacc oder gnu bison zum Generieren von Parsern, gperf zum Generieren perfekter Hash-Funktionen usw.). Auf einigen Systemen (insbesondere Linux und POSIX) können Sie sogar zur Laufzeit C-Code in einer generated-001.cDatei generieren , ihn zu einem gemeinsam genutzten Objekt kompilieren, indem Sie zur Laufzeit einen Befehl (z. B. gcc -O -Wall -shared -fPIC generated-001.c -o generated-001.so) ausführen und dieses gemeinsam genutzte Objekt dynamisch mit dlopen laden& Mit dlsym einen Funktionszeiger von einem Namen abrufen . Ich mache solche Tricks in MELT (eine Lisp-ähnliche domänenspezifische Sprache, die für Sie nützlich sein könnte, da sie die Anpassung des GCC- Compilers ermöglicht).

Seien Sie sich bewusst von der Garbage Collection Konzepte und Techniken ( Referenzzählung ist oft eine Technik zu verwalten Speicher in C, und es ist meiner Meinung nach eine schlechte Form der Garbage Collection , die nicht gut behandeln zirkuläre Referenzen , Sie haben könnten schwache Zeiger zu helfen darüber, aber es könnte schwierig sein). In einigen Fällen könnten Sie in Betracht ziehen, den konservativen Müllmann von Boehm zu verwenden .

Basile Starynkevitch
quelle
7
Unabhängig von dieser Frage ist das Lesen von SICP zweifellos ein guter Rat, aber für das OP wird dies wahrscheinlich zur nächsten Frage führen: "Wie kann man als C-Programmierer denken, nachdem man mit SICP voreingenommen ist?".
Doc Brown
1
Nein, weil das Schema von SICP & PHP (oder Ruby oder Python) so unterschiedlich ist, dass das OP ein viel breiteres Denken bekommen würde; und SICP erklären recht gut, was in der Praxis ein abstrakter Datentyp ist, und das ist sehr nützlich zu verstehen, insbesondere für die Codierung in C.
Basile Starynkevitch
1
SICP ist ein seltsamer Vorschlag. Schema unterscheidet sich stark von C.
Brian Gordon
Aber SICP lehrt viele gute Gewohnheiten, und das Wissen um Scheme hilft beim Codieren in C (für die Konzepte von Closures, abstrakten Datentypen usw.)
Basile Starynkevitch
5

Die Art und Weise, wie das Programm erstellt wird, definiert im Wesentlichen, welche Aktionen (Funktionen) ausgeführt werden müssen, um das Problem zu lösen (daher wird es als prozedurale Sprache bezeichnet). Jede Aktion entspricht einer Funktion. Anschließend müssen Sie definieren, welche Art von Informationen die einzelnen Funktionen erhalten und welche Informationen sie zurückgeben müssen.

Das Programm ist normalerweise in Dateien (Module) unterteilt. Jede Datei hat normalerweise eine Gruppe von Funktionen, die miteinander verbunden sind. Am Anfang jeder Datei deklarieren Sie (außerhalb einer Funktion) Variablen, die von allen Funktionen in dieser Datei verwendet werden. Wenn Sie das Qualifikationsmerkmal "static" verwenden, werden diese Variablen nur in dieser Datei angezeigt (jedoch nicht in anderen Dateien). Wenn Sie das Qualifikationsmerkmal "static" nicht für Variablen verwenden, die außerhalb von Funktionen definiert wurden, können Sie auch über andere Dateien darauf zugreifen. Diese anderen Dateien sollten die Variable als "extern" deklarieren (aber nicht definieren), damit der Compiler nach ihnen sucht in anderen Dateien.

Kurz gesagt, Sie denken zuerst über die Prozeduren (Funktionen) nach und stellen dann sicher, dass alle Funktionen Zugriff auf die benötigten Informationen haben.

Mandrill
quelle
3

C-APIs haben oft - vielleicht sogar normalerweise - eine im Wesentlichen objektorientierte Schnittstelle, wenn Sie sie richtig betrachten.

In C ++:

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

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

In C:

typedef struct {
    int x;
} foo;

void bar (foo*, int param);

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

Wie Sie vielleicht wissen, verwendet in C ++ und verschiedenen anderen formalen OO-Sprachen eine Objektmethode unter der Haube ein erstes Argument, das ein Zeiger auf das Objekt ist, ähnlich wie die C-Version von bar()oben. Ein Beispiel dafür, wo dies in C ++ auftaucht, ist, wie std::bindObjektmethoden an Funktionssignaturen angepasst werden können:

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

Wie andere betont haben, besteht der wirkliche Unterschied darin, dass formale OO-Sprachen Polymorphismus, Zugriffskontrolle und verschiedene andere nützliche Funktionen implementieren können. Das Wesen der objektorientierten Programmierung, die Erzeugung und Manipulation diskreter, komplexer Datenstrukturen, ist jedoch bereits eine grundlegende Praxis in C.

Goldlöckchen
quelle
2

Einer der Hauptgründe, warum Menschen ermutigt werden, C zu lernen, ist, dass es eine der niedrigsten Programmiersprachen auf hoher Ebene ist. OOP-Sprachen erleichtern das Nachdenken über Datenmodelle und das Übermitteln von Code- und Nachrichtenvorlagen. Am Ende des Tages führt ein Mikroprozessor Code schrittweise aus, springt in Codeblöcke hinein und aus ihnen heraus (Funktionen in C) und bewegt sich Verweise auf Variablen (Zeiger in C), damit verschiedene Teile eines Programms Daten gemeinsam nutzen können. Stellen Sie sich C als Assemblersprache in Englisch vor - und geben Sie Schritt für Schritt Anweisungen für den Mikroprozessor Ihres Computers - und Sie werden nicht viel falsch machen. Als Bonus funktionieren die meisten Betriebssystemschnittstellen eher wie C-Funktionsaufrufe als wie OOP-Paradigmen.

Gaurav
quelle
2
IMHO C ist eine Low-Level-Sprache, aber viel höher als Assembler- oder Maschinencode, da der C-Compiler viele Low-Level-Optimierungen durchführen könnte.
Basile Starynkevitch
C-Compiler bewegen sich im Namen der "Optimierung" auch in Richtung eines abstrakten Maschinenmodells, das die Gesetze der Zeit und der Kausalität negieren kann, wenn Eingaben vorgenommen werden, die Undefiniertes Verhalten verursachen würden, selbst wenn das natürliche Verhalten des Codes auf der Maschine, auf der es sich befindet wird ausgeführt, würde sonst den Anforderungen genügen. Beispielsweise funktioniert die Funktion uint16_t blah(uint16_t x) {return x*x;}auf Computern mit unsigned int16 Bit oder 33 Bit oder mehr identisch . Einige Compiler für Computer mit unsigned int17 bis 32 Bit können jedoch einen Aufruf dieser Methode in
Betracht ziehen
... als Erlaubnis für den Compiler zu schließen, dass möglicherweise keine Kette von Ereignissen auftreten kann, die dazu führen würde, dass der Methode ein Wert von mehr als 46340 zugewiesen wird. Auch wenn das Multiplizieren von 65533u * 65533u auf einer beliebigen Plattform einen Wert ergeben würde, der beim Umwandeln in uint16_t9 den Wert ergibt, schreibt der Standard kein solches Verhalten vor, wenn Werte vom Typ uint16_tauf 17- bis 32-Bit-Plattformen multipliziert werden .
Supercat
-1

Ich bin auch ein OO-Eingeborener (C ++ allgemein), der manchmal in einer Welt von C überleben muss. Für mich ist die grundlegend größte Hürde die Fehlerbehandlung und das Ressourcenmanagement.

In C ++ müssen wir einen Fehler von dort, wo er auftritt, an die oberste Ebene zurückgeben, wo wir damit umgehen können, und wir haben Destruktoren, um unseren Speicher und andere Ressourcen automatisch freizugeben.

Möglicherweise stellen Sie fest, dass viele C-APIs eine Init-Funktion enthalten, mit der Sie eine typisierte Leere * erhalten, die wirklich ein Zeiger auf eine Struktur ist. Dann übergeben Sie dies als erstes Argument für jeden API-Aufruf. Im Wesentlichen wird dies Ihr "dieser" Zeiger aus C ++. Es wird für alle internen Datenstrukturen verwendet, die versteckt sind (ein sehr OO-Konzept). Sie können es auch zum Verwalten des Speichers verwenden, z. B. eine Funktion namens myapiMalloc, die Ihren Speicher malloc und den malloc in Ihrer C-Version dieses Zeigers aufzeichnet, damit Sie sicherstellen können, dass er freigegeben wird, wenn Ihre API zurückkehrt. Wie ich kürzlich herausgefunden habe, können Sie damit Fehlercodes speichern und setjmp und longjmp verwenden, um ein Verhalten zu erzielen, das dem von throw catch sehr ähnlich ist. Durch die Kombination beider Konzepte erhalten Sie einen Großteil der Funktionalität eines C ++ - Programms.

Jetzt hast du gesagt, dass du nicht lernen willst, C in C ++ zu zwingen. Das ist nicht wirklich das, was ich beschreibe (zumindest nicht absichtlich). Dies ist einfach eine (hoffentlich) gut konzipierte Methode, um die C-Funktionalität zu nutzen. Es hat sich herausgestellt, dass es einige OO-Varianten gibt - vielleicht haben OO-Sprachen deswegen entwickelt. Sie waren eine Möglichkeit, Konzepte zu formalisieren, durchzusetzen und zu erleichtern, die sich bei manchen als Best Practice herausgestellt haben.

Wenn Sie der Meinung sind, dass dies für Sie ein OO-Gefühl ist, besteht die Alternative darin, dass praktisch jede Funktion einen Fehlercode zurückgibt, den Sie nach jedem Funktionsaufruf überprüfen und den Aufrufstapel weitergeben müssen. Sie müssen sicherstellen, dass alle Ressourcen nicht nur am Ende jeder Funktion, sondern auch an jedem Rückkehrpunkt freigegeben werden (möglicherweise nach jedem Funktionsaufruf, der einen Fehler zurückgeben kann, der darauf hinweist, dass Sie nicht fortfahren können). Es kann sehr mühsam werden und führt dazu, dass Sie denken, dass ich mich wahrscheinlich nicht mit diesem potenziellen Speicherzuweisungsfehler (oder dem Lesen von Dateien oder dem Herstellen einer Portverbindung ...) befassen muss. Ich gehe einfach davon aus, dass es funktionieren wird, oder ich Schreibe jetzt den "interessanten" Code und kehre zurück und kümmere dich um die Fehlerbehandlung - was niemals passiert.

Phil Rosenberg
quelle