Während des gesamten Zeitraums ihres Bestehens haben die Menschen enorme Anstrengungen unternommen, um fast den gesamten Bereich des Sternenhimmels zu untersuchen. Bisher haben wir Hunderttausende von Asteroiden, Kometen, Nebeln und Sternen, Galaxien und Planeten untersucht. Um all diese Schönheit selbst zu sehen, ist es nicht notwendig, das Haus zu verlassen und sich ein Teleskop zu kaufen. Sie können Stellarium - ein virtuelles Planetarium - auf Ihrem Computer installieren und den Nachthimmel betrachten, der bequem auf der Couch liegt ... Aber ist es bequem? Um die Antwort auf diese Frage herauszufinden, überprüfen wir Stellarium auf Fehler im Computercode.
Ein bisschen über das Projekt ...
Wie auf der Wikipedia-Website beschrieben, ist
Stellarium ein virtuelles Open-Source-Planetarium, das für Linux, Mac OS X, Microsoft Windows, Symbian, Android und iOS sowie MeeGo verfügbar ist. Das Programm verwendet OpenGL- und Qt-Technologien, um in Echtzeit einen realistischen Himmel zu schaffen. Mit Stellarium können Sie sehen, was Sie mit einem mittleren und sogar großen Teleskop sehen können. Das Programm bietet auch Beobachtungen von Sonnenfinsternissen und der Bewegung von Kometen.
Stellarium wurde vom französischen Programmierer Fabian Chereau erstellt, der das Projekt im Sommer 2001 startete. Weitere bekannte Entwickler sind Robert Spearman, Johannes Gadzhozik, Matthew Gates, Timothy Reeves, Bogdan Marinov und Johan Meeris, der für das Kunstwerk verantwortlich ist.
... und über den Analysator
Die Analyse des Projekts wurde mit dem statischen Code-Analysator PVS-Studio durchgeführt. Dies ist ein Tool zum Erkennen von Fehlern und potenziellen Schwachstellen im Quellcode von Programmen, die in C, C ++ und C # geschrieben wurden (bald in Java!). Es läuft unter Windows, Linux und MacOS. Es ist für diejenigen gedacht, die die Qualität ihres Codes verbessern müssen.
Die Analyse war recht einfach. Zuerst habe ich das Stellarium-Projekt von GitHub
heruntergeladen und dann alle für die Assembly erforderlichen Pakete installiert. Da das Projekt mit Qt Creator erstellt wurde, habe ich das Compiler-Startverfolgungssystem verwendet, das in die
Standalone- Version des Analysators integriert ist. Dort können Sie den fertigen Analysebericht anzeigen.
Neue Leser und Benutzer von
Stellarium haben sich vielleicht gefragt: Warum erscheint das Einhorn im Titel des Artikels und in welcher Beziehung steht es zur Codeanalyse? Ich antworte: Ich bin einer der Entwickler von PVS-Studio und das Einhorn ist unser Lieblingsmaskottchen. Also auf!
Ich hoffe, dass die Leser dank dieses Artikels etwas Neues für sich selbst lernen und Stellarium-Entwickler in der Lage sein werden, einige Fehler zu beseitigen und die Qualität des Codes zu verbessern.
Bringen Sie sich Kaffee mit einem Luftcroissant und machen Sie es sich bequem, denn wir gehen zum interessantesten Teil - einem Überblick über die Ergebnisse der Analyse und Analyse von Fehlern!
Verdächtige Bedingungen
Für mehr Lesevergnügen empfehle ich, nicht direkt auf die Warnung des Analysators zu schauen, sondern hier und weiter zu versuchen, selbst Fehler zu finden.
void QZipReaderPrivate::scanFiles() { ....
PVS-Studio- Warnung : V654 Die Bedingung 'start_of_directory == - 1' der Schleife ist immer wahr. qzip.cpp 617
Könnten Sie einen Fehler finden? Wenn ja, dann loben.
Der Fehler liegt im Zustand der
while-Schleife .
Dies ist immer der Fall, da sich die Variable
start_of_directory im Hauptteil der Schleife nicht ändert. Höchstwahrscheinlich wird der Zyklus nicht ewig sein, da er
Return und
Break enthält, aber ein solcher Code sieht seltsam aus.
Es scheint mir, dass sie im Code vergessen haben, die Zuweisung
start_of_directory = pos an der Stelle
vorzunehmen, an der die Signatur überprüft wird. Dann ist die
break- Anweisung vielleicht überflüssig. In diesem Fall kann der Code folgendermaßen umgeschrieben werden:
int i = 0; int start_of_directory = -1; EndOfDirectory eod; while (start_of_directory == -1) { const int pos = device->size() - int(sizeof(EndOfDirectory)) - i; if (pos < 0 || i > 65535) { qWarning() << "QZip: EndOfDirectory not found"; return; } device->seek(pos); device->read((char *)&eod, sizeof(EndOfDirectory)); if (readUInt(eod.signature) == 0x06054b50) start_of_directory = pos; ++i; }
Ich bin mir jedoch nicht sicher, wie der Code so aussehen soll. Es ist am besten, wenn die Projektentwickler diesen Teil des Programms selbst analysieren und die erforderlichen Änderungen vornehmen.
Ein weiterer seltsamer Zustand:
class StelProjectorCylinder : public StelProjector { public: .... protected: .... virtual bool intersectViewportDiscontinuityInternal(const Vec3d& capN, double capD) const { static const SphericalCap cap1(1,0,0); static const SphericalCap cap2(-1,0,0); static const SphericalCap cap3(0,0,-1); SphericalCap cap(capN, capD); return cap.intersects(cap1) && cap.intersects(cap2) && cap.intersects(cap2); } };
PVS-Studio Warnung: V501 Links und rechts vom Operator '&&' befinden sich identische Unterausdrücke 'cap.intersects (cap2)'. StelProjectorClasses.hpp 175
Wie Sie wahrscheinlich bereits vermutet haben, liegt der Fehler in der letzten Zeile der Funktion: Der Programmierer hat einen Tippfehler gemacht, und am Ende stellte sich heraus, dass die Funktion das Ergebnis unabhängig vom Wert von
cap3 zurückgibt .
Diese Art von Fehler ist äußerst häufig: In fast jedem verifizierten Projekt sind Tippfehler aufgetreten, die mit Namen der
Formnamen1 und
name2 und dergleichen
verknüpft sind . In der Regel hängen solche Fehler mit dem Kopieren und Einfügen zusammen.
Diese Code-Instanz ist ein Paradebeispiel für ein weiteres häufiges Fehlermuster, für das wir sogar eine separate Ministudie durchgeführt haben. Mein Kollege Andrei Karpov nannte es den "
Last-Line-Effekt ". Wenn Sie mit diesem Material nicht vertraut sind, empfehle ich, einen Tab im Browser zu öffnen, um ihn später zu lesen. Lassen Sie uns jedoch vorerst fortfahren.
void BottomStelBar::updateText(bool updatePos) { .... updatePos = true; .... if (location->text() != newLocation || updatePos) { updatePos = true; .... } .... if (fov->text() != str) { updatePos = true; .... } .... if (fps->text() != str) { updatePos = true; .... } if (updatePos) { .... } }
PVS-Studio-Warnungen:- V560 Ein Teil des bedingten Ausdrucks ist immer wahr: updatePos. StelGuiItems.cpp 732
- V547 Der Ausdruck 'updatePos' ist immer wahr. StelGuiItems.cpp 831
- V763 Der Parameter 'updatePos' wird vor seiner Verwendung immer im Funktionskörper neu geschrieben. StelGuiItems.cpp 690
Der Wert des
updatePos- Parameters
wird immer überschrieben, bevor er verwendet wird, d. H. Die Funktion funktioniert unabhängig vom übergebenen Wert gleich.
Sieht komisch aus, nicht wahr? An allen Stellen, an denen der Parameter
updatePos beteiligt ist , ist dies der
Fall . Dies bedeutet, dass die Bedingungen
if (location-> text ()! = NewLocation || updatePos) und
if (updatePos) immer wahr sind.
Ein weiterer Ausschnitt:
void LandscapeMgr::onTargetLocationChanged(StelLocation loc) { .... if (pl && flagEnvironmentAutoEnabling) { QSettings* conf = StelApp::getInstance().getSettings(); setFlagAtmosphere(pl->hasAtmosphere() & conf->value("landscape/flag_atmosphere", true).toBool()); setFlagFog(pl->hasAtmosphere() & conf->value("landscape/flag_fog", true).toBool()); setFlagLandscape(true); } .... }
PVS-Studio-Warnungen:- V792 Die Funktion 'toBool' rechts neben dem Operator '&' wird unabhängig vom Wert des linken Operanden aufgerufen. Vielleicht ist es besser, '&&' zu verwenden. LandscapeMgr.cpp 782
- V792 Die Funktion 'toBool' rechts neben dem Operator '&' wird unabhängig vom Wert des linken Operanden aufgerufen. Vielleicht ist es besser, '&&' zu verwenden. LandscapeMgr.cpp 783
Der Analysator hat in den Argumenten für die
Funktionen setFlagAtmosphere und
setFlagFog einen verdächtigen Ausdruck
festgestellt . In der Tat: Auf beiden Seiten des Bitoperators
& gibt es Werte vom Typ
bool . Anstelle des Operators
& sollten Sie den Operator
&& verwenden , und jetzt werde ich erklären, warum.
Ja, das Ergebnis dieses Ausdrucks ist immer korrekt. Bevor Sie das bitweise "und" verwenden, werden beide Operanden auf
int heraufgestuft . In C ++ ist eine solche Konvertierung
eindeutig : false wird in 0 und true in 1 konvertiert. Daher ist das Ergebnis dieses Ausdrucks dasselbe, als ob der Operator
&& verwendet würde.
Aber es gibt eine Nuance. Bei der Berechnung des Ergebnisses der
&& -Operation wird die sogenannte "Lazy-Berechnung" verwendet. Wenn der Wert des linken Operanden
falsch ist , wird der rechte Wert nicht einmal berechnet, da das logische "und" in jedem Fall
false zurückgibt . Dies spart Rechenressourcen und ermöglicht das Schreiben komplexerer Designs. Sie können beispielsweise überprüfen, ob der Zeiger nicht null ist, und ihn in diesem Fall dereferenzieren, um eine zusätzliche Überprüfung durchzuführen. Beispiel:
if (ptr && ptr-> foo ()) .
Eine solche "verzögerte Berechnung" wird nicht durchgeführt, wenn der bitweise Operator
& : Ausdrücke
conf-> value ("...", true) verwendet wird. ToBool () wird jedes Mal ausgewertet, unabhängig vom Wert
pl-> hasAtmosphere () .
In seltenen Fällen erfolgt dies absichtlich. Wenn beispielsweise die Berechnung des richtigen Operanden „Nebenwirkungen“ hat, deren Ergebnis später verwendet wird. Es ist auch nicht sehr gut, dies zu tun, da dies das Verständnis des Codes und die Wartung des Codes erschwert. Außerdem ist die Reihenfolge der Berechnung der Operanden
& nicht definiert, sodass in einigen Fällen bei Verwendung solcher "Tricks" undefiniertes Verhalten auftreten kann.
Wenn Sie Nebenwirkungen speichern müssen, tun Sie dies in einer separaten Zeile und speichern Sie das Ergebnis in einer separaten Variablen. Leute, die in Zukunft mit diesem Code arbeiten werden, werden Ihnen dankbar sein :)
Wir fahren mit dem nächsten Thema fort.
Falsche Speicherbehandlung
Beginnen wir das Thema dynamisches Gedächtnis mit diesem Fragment:
GLUEShalfEdge* __gl_meshMakeEdge(GLUESmesh* mesh) { GLUESvertex* newVertex1 = allocVertex(); GLUESvertex* newVertex2 = allocVertex(); GLUESface* newFace = allocFace(); GLUEShalfEdge* e; if ( newVertex1 == NULL || newVertex2 == NULL || newFace == NULL) { if (newVertex1 != NULL) { memFree(newVertex1); } if (newVertex2 != NULL) { memFree(newVertex2); } if (newFace != NULL) { memFree(newFace); } return NULL; } e = MakeEdge(&mesh->eHead); if (e == NULL) { return NULL; } MakeVertex(newVertex1, e, &mesh->vHead); MakeVertex(newVertex2, e->Sym, &mesh->vHead); MakeFace(newFace, e, &mesh->fHead); return e; }
PVS-Studio-Warnungen:- V773 Die Funktion wurde beendet, ohne den Zeiger 'newVertex1' freizugeben. Ein Speicherverlust ist möglich. mesh.c 312
- V773 Die Funktion wurde beendet, ohne den Zeiger 'newVertex2' freizugeben. Ein Speicherverlust ist möglich. mesh.c 312
- V773 Die Funktion wurde beendet, ohne den Zeiger 'newFace' loszulassen. Ein Speicherverlust ist möglich. mesh.c 312
Die Funktion reserviert Speicher für mehrere Strukturen und übergibt ihn an die Zeiger
newVertex1 ,
newVertex2 (interessante Namen, richtig?) Und
newFace . Wenn sich herausstellt, dass einer von ihnen Null ist, wird der gesamte in der Funktion reservierte Speicher freigegeben, wonach der Steuerungsfluss die Funktion verlässt.
Was passiert, wenn der Speicher für alle drei Strukturen korrekt zugewiesen ist und die Funktion
MakeEdge (& mesh-> eHead) NULL zurückgibt? Der Kontrollfluss erreicht die zweite
Rückkehr .
Da die Zeiger
newVertex1 ,
newVertex2 und
newFace lokale Variablen sind, existieren sie nach dem Beenden der Funktion nicht mehr. Aber die Freigabe der Erinnerung, die ihnen gehörte, wird nicht stattfinden. Es bleibt reserviert, aber wir haben keinen Zugriff mehr darauf.
Solche Situationen werden als Speicherlecks bezeichnet. Ein typisches Szenario mit einem solchen Fehler: Bei längerer Nutzung des Programms wird bis zu seiner Erschöpfung immer mehr RAM verbraucht.
Es ist zu beachten, dass in diesem Beispiel die dritte
Rückgabe nicht fehlerhaft ist. Die
Funktionen MakeVertex und
MakeFace übertragen die Adressen des zugewiesenen Speichers auf andere Datenstrukturen und delegieren so die Verantwortung für dessen Freigabe.
Der nächste Fehler liegt in der Methode, die 90 Zeilen dauert. Der Einfachheit halber habe ich es reduziert und nur Problembereiche belassen.
void AstroCalcDialog::drawAngularDistanceGraph() { .... QVector<double> xs, ys; .... }
Nur noch eine Zeile übrig. Lassen Sie mich einen Hinweis geben: Dies ist die einzige Erwähnung von
xs und
ys Objekten.
PVS-Studio-Warnungen:- Das V808 -Objekt 'xs' vom Typ 'QVector' wurde erstellt, aber nicht verwendet. AstroCalcDialog.cpp 5329
- Das V808 -Objekt 'ys' vom Typ 'QVector' wurde erstellt, aber nicht verwendet. AstroCalcDialog.cpp 5329
Die Vektoren
xs und
ys werden erstellt, aber nirgendwo verwendet. Es stellt sich heraus, dass jedes Mal, wenn Sie die
drawAngularDistanceGraph- Methode verwenden, ein leerer Container zusätzlich erstellt und gelöscht wird. Ich denke, diese Anzeige blieb nach dem Refactoring im Code. Dies ist natürlich kein Fehler, aber Sie sollten den zusätzlichen Code entfernen.
Seltsame Abgüsse
Ein weiteres Beispiel nach einigem Formatieren sieht folgendermaßen aus:
void SatellitesDialog::updateSatelliteData() { ....
Um zu verstehen, was der Fehler ist, müssen Sie sich die Prototypen
der Konstruktoren der Qcolor-Klasse ansehen :
PVS-Studio-Warnungen:- V674 Das Literal '0.4' vom Typ 'double' wird beim Aufrufen der Funktion 'QColor' implizit in den Typ 'int' umgewandelt. Überprüfen Sie das erste Argument. SatellitesDialog.cpp 413
- V674 Das Literal '0.4' vom Typ 'double' wird beim Aufrufen der Funktion 'QColor' implizit in den Typ 'int' umgewandelt. Untersuchen Sie das zweite Argument. SatellitesDialog.cpp 413
- V674 Das Literal '0.4' vom Typ 'double' wird beim Aufrufen der Funktion 'QColor' implizit in den Typ 'int' umgewandelt. Untersuchen Sie das dritte Argument. SatellitesDialog.cpp 413
Die
Qcolor- Klasse
verfügt nicht über Konstruktoren, die den
Doppeltyp akzeptieren.
Daher werden die Argumente im Beispiel implizit in
int konvertiert. Dadurch haben die Felder
r ,
g ,
b des
buttonColor- Objekts den Wert
0 .
Wenn der Programmierer beabsichtigte, ein Objekt aus Werten vom Typ
double zu erstellen, sollte er einen anderen Konstruktor verwenden.
Sie können beispielsweise einen Konstruktor verwenden, der
Qrgb akzeptiert, indem Sie
Folgendes schreiben:
buttonColor = QColor(QColor::fromRgbF(0.4, 0.4, 0.4));
Es hätte anders gemacht werden können. Qt verwendet reelle Werte im Bereich [0.0, 1.0] oder ganzzahlige Werte im Bereich [0, 255], um RGB-Farben anzuzeigen.
Daher könnte der Programmierer die Werte von real in integer übersetzen, indem er wie folgt schreibt:
buttonColor = QColor((int)(255 * 0.4), (int)(255 * 0.4), (int)(255 * 0.4));
oder einfach
buttonColor = QColor(102, 102, 102);
Bist du gelangweilt? Keine Sorge, es liegen weitere interessante Fehler vor uns.
"Einhorn im Weltraum." Blick aus dem Stellarium.Andere Fehler
Am Ende habe ich dir noch etwas Leckeres hinterlassen :) Lass uns zu einem von ihnen kommen.
HipsTile* HipsSurvey::getTile(int order, int pix) { .... if (order == orderMin && !allsky.isNull()) { int nbw = sqrt(12 * 1 << (2 * order)); int x = (pix % nbw) * allsky.width() / nbw; int y = (pix / nbw) * allsky.width() / nbw; int s = allsky.width() / nbw; QImage image = allsky.copy(x, y, s, s); .... } .... }
PVS-Studio Warnung: V634 Die Priorität der Operation '*' ist höher als die der Operation '<<'. Es ist möglich, dass im Ausdruck Klammern verwendet werden. StelHips.cpp 271
Konnten Sie den Fehler erkennen? Betrachten Sie den Ausdruck
(12 * 1 << (2 * Reihenfolge)) genauer. Der Analysator erinnert daran, dass die Operation '
* ' eine höhere Priorität hat als die Bitverschiebungsoperation '
<< '. Es ist leicht zu erkennen, dass das Multiplizieren von
12 mit
1 sinnlos ist und die Klammern um die
2 * -Ordnung nicht benötigt werden.
, : int nbw = sqrt(12 * (1 << 2 * order)); <i>12 </i> .
Hinweis Außerdem möchte ich darauf hinweisen, dass das Ergebnis nicht definiert ist, wenn der Wert des rechten Operanden '
<< ' größer oder gleich der Anzahl der Bits des linken Operanden ist. Da numerische Literale standardmäßig
int sind und
32 Bit benötigen, sollte der Wert des
Ordnungsparameters 15 nicht überschreiten. Andernfalls kann die Auswertung des Ausdrucks zu undefiniertem Verhalten führen.
Wir fahren fort. Die folgende Methode ist sehr verwirrend, aber ich bin sicher, dass ein erfahrener Leser die Fehlererkennung übernehmen wird :)
QCPRange QCPStatisticalBox:: getKeyRange(bool& foundRange, SignDomain inSignDomain) const { foundRange = true; if (inSignDomain == sdBoth) { return QCPRange(mKey - mWidth * 0.5, mKey + mWidth * 0.5); } else if (inSignDomain == sdNegative) { if (mKey + mWidth * 0.5 < 0) return QCPRange(mKey - mWidth * 0.5, mKey + mWidth * 0.5); else if (mKey < 0) return QCPRange(mKey - mWidth * 0.5, mKey); else { foundRange = false; return QCPRange(); } } else if (inSignDomain == sdPositive) { if (mKey - mWidth * 0.5 > 0) return QCPRange(mKey - mWidth * 0.5, mKey + mWidth * 0.5); else if (mKey > 0) return QCPRange(mKey, mKey + mWidth * 0.5); else { foundRange = false; return QCPRange(); } } foundRange = false; return QCPRange(); }
PVS-Studio Warnung: V779 Nicht erreichbarer Code erkannt. Möglicherweise liegt ein Fehler vor. qcustomplot.cpp 19512.
Tatsache ist, dass alle,
wenn ... sonst Filialen eine
Rendite haben . Daher erreicht der Kontrollfluss niemals die letzten beiden Zeilen.
Im Großen und Ganzen wird dieses Beispiel normal ausgeführt und funktioniert ordnungsgemäß. Das Vorhandensein von nicht erreichbarem Code allein ist jedoch ein Signal. In diesem Fall weist dies auf die falsche Struktur der Methode hin, was die Lesbarkeit und Verständlichkeit des Codes erheblich erschwert.
Dieses Codefragment sollte überarbeitet werden, um eine sauberere Funktion am Ausgang zu erhalten. Zum Beispiel so:
QCPRange QCPStatisticalBox:: getKeyRange(bool& foundRange, SignDomain inSignDomain) const { foundRange = true; switch (inSignDomain) { case sdBoth: { return QCPRange(mKey - mWidth * 0.5, mKey + mWidth * 0.5); break; } case sdNegative: { if (mKey + mWidth * 0.5 < 0) return QCPRange(mKey - mWidth * 0.5, mKey + mWidth * 0.5); else if (mKey < 0) return QCPRange(mKey - mWidth * 0.5, mKey); break; } case sdPositive: { if (mKey - mWidth * 0.5 > 0) return QCPRange(mKey - mWidth * 0.5, mKey + mWidth * 0.5); else if (mKey > 0) return QCPRange(mKey, mKey + mWidth * 0.5); break; } } foundRange = false; return QCPRange(); }
Der letzte in unserer Bewertung wird der Fehler sein, der mir am besten gefallen hat. Der Fehlercode ist kurz und einfach:
Plane::Plane(Vec3f &v1, Vec3f &v2, Vec3f &v3) : distance(0.0f), sDistance(0.0f) { Plane(v1, v2, v3, SPolygon::CCW); }
Haben Sie etwas Verdächtiges bemerkt? Nicht jeder kann :)
PVS-Studio Warnung: V603 Das Objekt wurde erstellt, wird jedoch nicht verwendet. Wenn Sie den Konstruktor aufrufen möchten, sollte 'this-> Plane :: Plane (....)' verwendet werden. Plane.cpp 29
Der Programmierer hat erwartet, dass einige der Felder des Objekts im verschachtelten Konstruktor initialisiert werden, aber es stellte sich folgendermaßen heraus: Wenn der
Ebenenkonstruktor (Vec3f & v1, Vec3f & v2, Vec3f & v3) aufgerufen wird, wird ein unbenanntes temporäres Objekt darin erstellt, das sofort gelöscht wird. Infolgedessen bleibt ein Teil des Objekts nicht initialisiert.
Damit der Code ordnungsgemäß funktioniert, sollten Sie die praktische und sichere Funktion von C ++ 11 verwenden - den delegierenden Konstruktor:
Plane::Plane(Vec3f& v1, Vec3f& v2, Vec3f& v3) : Plane(v1, v2, v3, SPolygon::CCW) { distance = 0.0f; sDistance = 0.0f; }
Wenn Sie den Compiler jedoch für ältere Versionen der Sprache verwenden, können Sie folgendermaßen schreiben:
Plane::Plane(Vec3f &v1, Vec3f &v2, Vec3f &v3) : distance(0.0f), sDistance(0.0f) { this->Plane::Plane(v1, v2, v3, SPolygon::CCW); }
Oder so:
Plane::Plane(Vec3f &v1, Vec3f &v2, Vec3f &v3) : distance(0.0f), sDistance(0.0f) { new (this) Plane(v1, v2, v3, SPolygon::CCW); }
Ich stelle fest, dass die letzten beiden Methoden
sehr gefährlich sind . Daher sollten Sie sehr vorsichtig sein und gut verstehen, wie solche Methoden funktionieren.
Fazit
Welche Schlussfolgerungen können über die Qualität des Stellarium-Codes gezogen werden? Ehrlich gesagt gab es nicht sehr viele Fehler. Außerdem habe ich im gesamten Projekt keinen einzigen Fehler gefunden, bei dem der Code an undefiniertes Verhalten gebunden ist. Für das OpenSource-Projekt stellte sich heraus, dass die Qualität des Codes auf einem hohen Niveau war, wofür ich den Entwicklern den Hut abnehme. Ihr seid großartig! Ich war erfreut und interessiert, Ihr Projekt zu überprüfen.
Was ist mit dem Planetarium selbst - ich benutze es ziemlich oft. Leider kann ich in einer Stadt selten einen klaren Nachthimmel genießen, und Stellarium ermöglicht es mir, überall auf der Welt zu sein, ohne von der Couch aufzustehen. Es ist wirklich bequem!
Besonders gut gefällt mir der Modus „Constellation Art“. Der Anblick der riesigen Figuren, die den ganzen Himmel in einem seltsamen Tanz bedecken, ist atemberaubend.
"Ein seltsamer Tanz." Blick aus dem Stellarium.Erdlinge neigen dazu, Fehler zu machen, und es ist nicht beschämend, dass diese Fehler in den Code gelangen. Hierzu werden Code-Analyse-Tools wie PVS-Studio entwickelt. Wenn Sie einer der Erdlinge sind -
wie es, empfehle
ich Ihnen, es
herunterzuladen und selbst auszuprobieren .
Ich hoffe, Sie waren daran interessiert, meinen Artikel zu lesen, und Sie haben etwas Neues und Nützliches für sich selbst gelernt. Und ich wünsche den Entwicklern eine frühzeitige Korrektur der gefundenen Fehler.
Abonnieren Sie unsere Kanäle und bleiben Sie auf dem Laufenden, um Neuigkeiten aus der Programmwelt zu erhalten!

Wenn Sie diesen Artikel einem englischsprachigen Publikum zugänglich machen möchten, verwenden Sie bitte den Link zur Übersetzung: George Gribkov.
Wieder ins All: Wie das Einhorn das Stellarium besuchte