Joel Spolsky charakterisierte C ++ als "genug Seil, um sich aufzuhängen" . Eigentlich fasste er "Effective C ++" von Scott Meyers zusammen:
Es ist ein Buch, das im Grunde sagt, C ++ ist genug Seil, um sich aufzuhängen, und dann ein paar zusätzliche Meilen Seil und dann ein paar Selbstmordpillen, die als M & Ms getarnt sind ...
Ich habe keine Kopie des Buches, aber es gibt Hinweise darauf, dass sich ein Großteil des Buches auf Fallstricke bei der Speicherverwaltung bezieht, die in C # offenbar in Frage gestellt werden, da die Laufzeitumgebung diese Probleme für Sie verwaltet.
Hier sind meine Fragen:
- Vermeidet C # Fallstricke, die in C ++ nur durch sorgfältige Programmierung vermieden werden? Wenn ja, in welchem Maße und wie werden sie vermieden?
- Gibt es neue, unterschiedliche Fallstricke in C #, die ein neuer C # -Programmierer kennen sollte? Wenn ja, warum konnten sie durch das Design von C # nicht vermieden werden?
c#
programming-languages
c++
alx9r
quelle
quelle
Your questions should be reasonably scoped. If you can imagine an entire book that answers your question, you’re asking too much.
. Ich glaube, dies ist eine solche Frage ...Antworten:
Der grundlegende Unterschied zwischen C ++ und C # beruht auf undefiniertem Verhalten .
Es hat nichts mit der manuellen Speicherverwaltung zu tun. In beiden Fällen ist das ein gelöstes Problem.
C / C ++:
Wenn Sie in C ++ einen Fehler machen, ist das Ergebnis undefiniert.
Wenn Sie versuchen, bestimmte Annahmen über das System zu treffen (z. B. vorzeichenbehafteter Integer-Überlauf), ist Ihr Programm möglicherweise nicht definiert.
Vielleicht lesen Sie diese 3-teilige Serie über undefiniertes Verhalten.
Das macht C ++ so schnell - der Compiler muss sich keine Gedanken darüber machen, was passiert, wenn etwas schief geht, und kann so die Überprüfung auf Richtigkeit vermeiden.
C #, Java usw.
In C # wird garantiert, dass viele Fehler als Ausnahmen auftauchen, und es wird viel mehr über das zugrunde liegende System garantiert .
Dies ist eine grundlegende Barriere, um C # so schnell wie C ++ zu machen, aber es ist auch eine grundlegende Barriere, um C ++ sicherer zu machen, und es erleichtert die Arbeit mit und das Debuggen von C #.
Alles andere ist nur Soße.
quelle
Das meiste tut es, manche tut es nicht. Und natürlich macht es einige neue.
Undefiniertes Verhalten - Die größte Gefahr bei C ++ besteht darin, dass ein Großteil der Sprache undefiniert ist. Der Compiler kann das Universum buchstäblich sprengen, wenn Sie diese Dinge tun, und es wird in Ordnung sein. Dies ist natürlich ungewöhnlich, aber es ist ziemlich üblich, dass Ihr Programm auf einem Computer einwandfrei funktioniert und auf einem anderen aus keinem guten Grund. Oder schlimmer noch, auf subtile Weise anders handeln. C # weist in seiner Spezifikation einige Fälle von undefiniertem Verhalten auf, die jedoch selten sind und in Bereichen der Sprache vorkommen, die nur selten bereist werden. C ++ hat die Möglichkeit, bei jeder Aussage auf undefiniertes Verhalten zu stoßen.
Speicherlecks - Dies ist für modernes C ++ weniger wichtig, aber für Anfänger und während etwa der Hälfte seiner Lebensdauer machte es C ++ sehr einfach, Speicher zu verlieren. Wirksames C ++ kam gleich um die Entwicklung von Praktiken, um dieses Problem zu beseitigen. Das heißt, C # kann immer noch Speicher verlieren. Der häufigste Fall, in den Menschen geraten, ist die Erfassung von Ereignissen. Wenn Sie über ein Objekt verfügen und eine seiner Methoden als Handler für ein Ereignis verwenden, muss der Eigentümer dieses Ereignisses über einen GC verfügen, damit das Objekt stirbt. Den meisten Anfängern ist nicht klar, dass der Event-Handler als Referenz gilt. Es gibt auch Probleme mit der Nichtverfügbarkeit von verfügbaren Ressourcen, die Speicherverluste verursachen können, aber diese sind bei weitem nicht so häufig wie Zeiger in C ++ vor Effective.
Kompilierung - C ++ hat ein verzögertes Kompilierungsmodell. Dies führt zu einer Reihe von Tricks, um gut damit zu spielen und die Kompilierzeiten niedrig zu halten.
Strings - Modernes C ++ verbessert dies ein wenig, ist jedoch
char*
für ca. 95% aller Sicherheitsverletzungen vor dem Jahr 2000 verantwortlich. Erfahrene Programmierer konzentrieren sich daraufstd::string
, aber es ist immer noch etwas zu vermeiden und ein Problem in älteren / schlechteren Bibliotheken . Und das betet, dass Sie keine Unicode-Unterstützung benötigen.Und das ist wirklich die Spitze des Eisbergs. Das Hauptproblem ist, dass C ++ eine sehr schlechte Sprache für Anfänger ist. Es ist ziemlich inkonsistent und viele der alten wirklich, wirklich schlimmen Fallstricke wurden durch Ändern der Redewendungen behoben. Das Problem ist, dass Anfänger dann die Redewendungen von so etwas wie Effective C ++ lernen müssen. C # beseitigt viele dieser Probleme insgesamt und macht den Rest weniger zur Sorge, bis Sie weiter auf dem Lernweg sind.
Ich erwähnte das Ereignis "Speicherverlust". Dies ist kein Sprachproblem, sondern der Programmierer erwartet etwas, was die Sprache nicht kann.
Ein weiterer Grund ist, dass der Finalizer für ein C # -Objekt technisch nicht garantiert von der Laufzeit ausgeführt werden kann. Dies spielt normalerweise keine Rolle, führt jedoch dazu, dass einige Dinge anders gestaltet werden, als Sie vielleicht erwarten.
Ein weiteres Problem, auf das Programmierer gestoßen sind, ist die Erfassungssemantik anonymer Funktionen. Wenn Sie eine Variable erfassen , erfassen Sie die Variable . Beispiel:
Tut nicht was naiv gedacht wird. Dies wird
10
10-mal gedruckt .Ich bin sicher, dass es eine Reihe anderer gibt, die ich vergesse, aber das Hauptproblem ist, dass sie weniger durchdringend sind.
quelle
char*
. Ganz zu schweigen davon, dass Sie immer noch Speicher in C # verlieren können.enable_if
Meiner Meinung nach sind die Gefahren von C ++ etwas übertrieben.
Die wesentliche Gefahr ist folgende: Während Sie mit C # "unsichere" Zeigeroperationen mit dem
unsafe
Schlüsselwort ausführen können , können Sie mit C ++ (das größtenteils eine Obermenge von C darstellt) Zeiger verwenden, wann immer Sie dies wünschen. Neben den üblichen Gefahren, die mit der Verwendung von Zeigern verbunden sind (die mit C identisch sind), wie Speicherlecks, Pufferüberläufen, baumelnden Zeigern usw., bietet C ++ neue Möglichkeiten, um die Dinge ernsthaft zu vermasseln.Dieses "zusätzliche Seil", von dem Joel Spolsky sozusagen sprach, beruht im Grunde auf einer Sache: dem Schreiben von Klassen, die intern ihr eigenes Gedächtnis verwalten, auch als " Regel von 3 " bekannt (die jetzt als Regel bezeichnet werden kann) von 4 oder Regel von 5 in C ++ 11). Das heißt, wenn Sie jemals eine Klasse schreiben möchten, die ihre eigenen Speicherzuordnungen intern verwaltet, müssen Sie wissen, was Sie tun, sonst stürzt Ihr Programm wahrscheinlich ab. Sie müssen einen Konstruktor, einen Kopierkonstruktor, einen Destruktor und einen Zuweisungsoperator sorgfältig erstellen, was überraschenderweise leicht zu Fehlern führt und zur Laufzeit oft zu bizarren Abstürzen führt.
JEDOCH in der tatsächlichen täglichen C ++ Programmierung, dann ist es sehr selten in die Tat eine Klasse zu schreiben , die einen eigenen Speicher verwaltet, so dass es irreführend zu sagen , dass C ++ Programmierer immer „vorsichtig“ sein muß , diese Fallen zu vermeiden. Normalerweise machst du einfach so etwas wie:
Diese Klasse ähnelt in etwa Ihren Java- oder C # -Vorgängen. Sie erfordert keine explizite Speicherverwaltung (da die Bibliotheksklasse dies
std::string
alles automatisch erledigt), und seit der Standardeinstellung sind überhaupt keine "3er-Regeln" erforderlich Kopierkonstruktor und Zuweisungsoperator sind in Ordnung.Es ist nur, wenn Sie versuchen, etwas zu tun, wie:
In diesem Fall kann es für Anfänger schwierig sein, die Zuweisung, den Destruktor und den Kopierkonstruktor korrekt zu machen. In den meisten Fällen gibt es jedoch keinen Grund, dies jemals zu tun. Mit C ++ können Sie die manuelle Speicherverwaltung in 99% der Fälle ganz einfach vermeiden, indem Sie Bibliotheksklassen wie
std::string
und verwendenstd::vector
.Ein weiteres verwandtes Problem ist die manuelle Speicherverwaltung, bei der die Möglichkeit, dass eine Ausnahme ausgelöst wird, nicht berücksichtigt wird. Mögen:
Wenn
some_function_which_may_throw()
tatsächlich nicht eine Ausnahme auslösen, sind Sie mit einem Speicherverlust gelassen , weil der Speicher reserviert fürs
nie zurückgewonnen werden. In der Praxis ist dies jedoch kaum noch ein Problem, da die "Regel von 3" kein wirkliches Problem mehr darstellt. Es ist sehr selten (und normalerweise unnötig), sein eigenes Gedächtnis mit rohen Zeigern zu verwalten. Um das obige Problem zu vermeiden, müssen Sie lediglich einstd::string
oder verwendenstd::vector
, und der Destruktor wird beim Abwickeln des Stapels nach dem Auslösen der Ausnahme automatisch aufgerufen.Ein allgemeines Thema hier ist also, dass viele C ++ - Funktionen, die nicht von C geerbt wurden, wie automatische Initialisierung / Zerstörung, Kopierkonstruktoren und Ausnahmen, einen Programmierer dazu zwingen, bei der manuellen Speicherverwaltung in C ++ besonders vorsichtig zu sein. Dies ist jedoch wiederum nur dann ein Problem, wenn Sie zunächst die manuelle Speicherverwaltung durchführen möchten, die bei Standardcontainern und Smart Pointern kaum noch erforderlich ist.
Meiner Meinung nach ist es in C ++ zwar nicht nötig, sich daran aufzuhängen, aber in modernen C ++ sind die Fallstricke, über die Joel sprach, trivial einfach zu vermeiden.
quelle
Does C# avoid pitfalls that are avoided in C++ only by careful programming?
. Die Antwort ist "nicht wirklich, weil es trivial einfach ist, die Fallstricke zu vermeiden, über die Joel in modernem C ++ sprach"Ich würde nicht wirklich zustimmen. Vielleicht weniger Fallstricke als C ++ von 1985.
Nicht wirklich. Regeln wie die Rule of Three haben massive Bedeutung in C ++ 11 dank verloren
unique_ptr
undshared_ptr
ist standardisiert. Eine vage sinnvolle Verwendung der Standardklassen ist keine "sorgfältige Codierung", sondern eine "grundlegende Codierung". Außerdem ist der Anteil der C ++ - Benutzer, die immer noch dumm, nicht informiert oder beides sind, um Dinge wie die manuelle Speicherverwaltung auszuführen, viel geringer als zuvor. Die Realität ist, dass Dozenten, die solche Regeln demonstrieren möchten, Wochen damit verbringen müssen, Beispiele zu finden, bei denen sie noch Anwendung finden, da die Standardklassen praktisch jeden erdenklichen Anwendungsfall abdecken. Viele effektive C ++ - Techniken sind den gleichen Weg gegangen - den Weg des Dodos. Viele der anderen sind nicht wirklich so C ++ spezifisch. Lass mich sehen. Überspringen des ersten Elements, die nächsten zehn sind:final
undoverride
haben geholfen, dieses besondere Spiel zum Besseren zu verändern. Machen Sie Ihren Destruktoroverride
und Sie garantieren einen netten Compilerfehler, wenn Sie von jemandem erben, der seinen Destruktor nicht gemacht hatvirtual
. Machen Sie Ihre Klassefinal
und keine schlechte Peeling kann mitkommen und aus Versehen ohne einen virtuellen Destruktor erben.Natürlich werde ich nicht jedes einzelne Effective C ++ - Element durchgehen, aber die meisten von ihnen wenden einfach grundlegende Konzepte auf C ++ an. Dieselben Ratschläge finden Sie in jeder objektorientierten überladbaren Operator-Sprache mit Werttyp. Virtuelle Destruktoren sind die einzigen, die eine C ++ - Falle darstellen und immer noch gültig sind - obwohl sie mit der
final
Klasse von C ++ 11 wohl nicht so gültig sind wie sie waren. Denken Sie daran, dass Effective C ++ geschrieben wurde, als die Idee, OOP und die spezifischen Funktionen von C ++ anzuwenden, noch sehr neu war. In diesen Artikeln geht es kaum um die Fallstricke von C ++ und mehr darum, wie man mit dem Wechsel von C umgeht und wie man OOP richtig einsetzt.Bearbeiten: Die Fallstricke von C ++ beinhalten keine Dinge wie die Fallstricke von
malloc
. Ich meine zum einen, dass jede einzelne Falle, die Sie in C-Code finden, die Sie auch in unsicherem C # -Code finden, nicht besonders relevant ist, und zum anderen, nur weil der Standard sie für die Interoperation definiert, bedeutet dies nicht, dass die Verwendung als C ++ gilt Code. Der Standard definiertgoto
auch, aber wenn Sie einen riesigen Haufen Spaghetti-Chaos damit schreiben würden, würde ich das für Ihr Problem halten, nicht für das der Sprache. Es gibt einen großen Unterschied zwischen "sorgfältiger Codierung" und "Befolgen grundlegender Redewendungen der Sprache".using
saugt. Macht es wirklich. Und ich habe keine Ahnung, warum etwas Besseres nicht gemacht wurde. AuchBase[] = Derived[]
und so ziemlich jede Verwendung von Object, die vorhanden ist, weil die ursprünglichen Designer den massiven Erfolg der Vorlagen in C ++ nicht bemerkten, und entschieden, dass "Wir müssen einfach alles von allem erben und unsere gesamte Typensicherheit verlieren" die klügere Wahl war . Ich glaube auch, dass Sie einige böse Überraschungen in Sachen Rennbedingungen mit Delegierten und anderem solchen Spaß finden können. Dann gibt es noch andere allgemeine Dinge, wie zum Beispiel, wie schrecklich Generika im Vergleich zu Vorlagen saugen, das wirklich wirklich unnötige erzwungene Platzieren von allem in einemclass
und dergleichen.quelle
malloc
, nicht bedeutet, dass Sie es tun sollten, und nicht nur, weil Siegoto
wie eine Hündin huren können, bedeutet, dass es ein Seil ist, an dem Sie sich aufhängen können.unsafe
in C #, was genauso schlecht ist. Wenn Sie möchten, könnte ich auch jede Falle auflisten, in der C # wie C codiert wird.C # hat die Vorteile von:
char
,string
usw. ist die Implementierung definiert. Das Schisma zwischen der Windows-Herangehensweise an Unicode (wchar_t
für UTF-16,char
für veraltete "Codepages") und der * nix-Herangehensweise (UTF-8) verursacht große Schwierigkeiten bei plattformübergreifendem Code. C #, OTOH, garantiert, dass astring
UTF-16 ist.Ja:
IDisposable
Es gibt ein Buch namens Effective C #, das in seiner Struktur Effective C ++ ähnelt .
quelle
Nein, C # (und Java) sind weniger sicher als C ++
C ++ ist lokal überprüfbar . Ich kann eine einzelne Klasse in C ++ untersuchen und feststellen, dass in der Klasse weder Speicher noch andere Ressourcen verloren gehen, sofern alle Klassen, auf die verwiesen wird, korrekt sind. In Java oder C # muss jede referenzierte Klasse überprüft werden, um festzustellen, ob eine Finalisierung erforderlich ist.
C ++:
C #:
C ++:
C #:
quelle
auto_ptr
(oder einige seiner Verwandten). Das ist das sprichwörtliche Seil.auto_ptr
ist so einfach wie zu wissen, wie man es benutztIEnumerable
oder wie man es benutzt, oder keine Gleitkommazahlen für Währungen oder ähnliches zu benutzen . Es ist eine grundlegende Anwendung von DRY. Niemand, der die Grundlagen des Programmierens kennt, würde diesen Fehler machen. Im Gegensatz zuusing
. Das Problem dabeiusing
ist, dass Sie für jede Klasse wissen müssen, ob sie verfügbar ist oder nicht (und ich hoffe, dass sich dies niemals ändert). Wenn sie nicht verfügbar ist, sperren Sie automatisch alle abgeleiteten Klassen, die möglicherweise verfügbar sein müssen.Dispose
Methode haben, müssen Sie diese implementierenIDisposable
(auf die richtige Art und Weise). Wenn Ihre Klasse dies tut (was der Implementierung von RAII für Ihre Klasse in C ++ entspricht) und Sie dies verwendenusing
(wie die intelligenten Zeiger in C ++), funktioniert alles perfekt. Der Finalizer ist hauptsächlich dazu gedacht, Unfälle zu vermeiden. ErDispose
ist für die Richtigkeit verantwortlich. Wenn Sie ihn nicht verwenden, liegt das an Ihnen, nicht an C #.Ja 100% ja, da ich denke, dass es unmöglich ist, Speicher freizugeben und in C # zu verwenden (vorausgesetzt, es wird verwaltet und Sie wechseln nicht in den unsicheren Modus).
Aber wenn Sie wissen, wie man in C ++ programmiert, was eine unglaubliche Anzahl von Leuten nicht tut. Du bist ziemlich in Ordnung. Wie Charles Salvia verwalten Klassen ihre Erinnerungen nicht wirklich, da alles in bereits existierenden STL-Klassen behandelt wird. Ich benutze selten Zeiger. In der Tat ging ich Projekte ohne einen einzigen Zeiger. (C ++ 11 macht dies einfacher).
if (i=0)
Der Compiler beklagt sich über Tippfehler, dumme Fehler usw. (zum Beispiel, dass der Schlüssel hängen geblieben ist, als Sie schnell auf == geklickt haben). Andere Beispiele sind das Vergessenbreak
in switch-Anweisungen und das Nicht-Deklarieren von statischen Variablen in einer Funktion (was ich manchmal nicht mag, aber eine gute Idee imo ist).quelle
=
/==
Problem noch schlimmer durch die Verwendung==
als Referenz Gleichheit und Einführung.equals
für Wertgleichheit. Der arme Programmierer muss jetzt verfolgen, ob eine Variable 'double' oder 'Double' ist und die richtige Variante aufrufen.struct
kann man==
das unglaublich gut machen, da man die meiste Zeit nur Strings, Ints und Floats hat (dh nur Strukturelemente). In meinem eigenen Code bekomme ich dieses Problem nie, es sei denn, ich möchte Arrays vergleichen. Ich glaube nicht, dass ich jemals Listen- oder Nicht-Strukturtypen (String, Int, Float, DateTime, KeyValuePair und viele andere) vergleiche==
für die Wertgleichheit undis
für die Referenzgleichheit verwendet hat.