Dan lagi ke luar angkasa: bagaimana unicorn Stellarium dikunjungi

Selama seluruh periode keberadaan mereka, orang-orang telah berupaya keras untuk mempelajari hampir seluruh area langit berbintang. Hingga saat ini, kami telah memeriksa ratusan ribu asteroid, komet, nebula, dan bintang, galaksi, dan planet. Untuk melihat semua keindahan ini sendiri, tidak perlu meninggalkan rumah dan membeli sendiri teleskop. Anda dapat menginstal Stellarium - planetarium virtual di komputer Anda, dan melihat langit malam, berbaring dengan nyaman di sofa ... Tetapi apakah itu nyaman? Untuk mengetahui jawaban atas pertanyaan ini, kami akan memeriksa Stellarium untuk kesalahan dalam kode komputer.



Sedikit tentang proyek ...


Menurut deskripsi di situs web Wikipedia, Stellarium adalah planetarium virtual open-source yang tersedia untuk Linux, Mac OS X, Microsoft Windows, Symbian, Android dan iOS, serta MeeGo. Program ini menggunakan teknologi OpenGL dan Qt untuk menciptakan langit yang realistis secara real time. Dengan Stellarium Anda dapat melihat apa yang dapat Anda lihat dengan teleskop berukuran sedang dan bahkan besar. Program ini juga menyediakan pengamatan gerhana matahari dan pergerakan komet.

Stellarium dibuat oleh programmer Prancis Fabian Chereau, yang meluncurkan proyek pada musim panas 2001. Pengembang terkemuka lainnya termasuk Robert Spearman, Johannes Gadzhozik, Matthew Gates, Timothy Reeves, Bogdan Marinov dan Johan Meeris, yang bertanggung jawab atas karya seni tersebut.

... dan tentang penganalisa


Analisis proyek dilakukan menggunakan penganalisis kode statis PVS-Studio. Ini adalah alat untuk mendeteksi kesalahan dan kerentanan potensial dalam kode sumber program yang ditulis dalam C, C ++ dan C # (segera di Jawa!). Ini berjalan pada Windows, Linux, dan macOS. Ini dirancang untuk mereka yang perlu meningkatkan kualitas kode mereka.

Analisisnya cukup sederhana. Pertama, saya mengunduh proyek Stellarium dari GitHub, dan kemudian menginstal semua paket yang diperlukan untuk perakitan. Karena proyek dibangun menggunakan Qt Creator, saya menggunakan sistem pelacakan peluncuran kompiler, yang dibangun ke dalam versi mandiri dari penganalisa. Di sana Anda dapat melihat laporan analisis yang sudah selesai.

Pembaca baru dan pengguna Stellarium mungkin bertanya-tanya: mengapa unicorn muncul dalam judul artikel dan bagaimana kaitannya dengan analisis kode? Saya menjawab: Saya adalah salah satu pengembang PVS-Studio, dan unicorn adalah maskot nakal favorit kami. Jadi naik!


Saya berharap bahwa berkat artikel ini, pembaca akan belajar sesuatu yang baru untuk diri mereka sendiri, dan pengembang Stellarium akan dapat menghilangkan beberapa kesalahan dan meningkatkan kualitas kode.

Bawalah kopi dengan croissant udara dan buat diri Anda nyaman, karena kita akan ke bagian yang paling menarik - ikhtisar hasil analisis dan analisis kesalahan!

Kondisi mencurigakan


Untuk kesenangan membaca yang lebih besar, saya sarankan untuk tidak langsung melihat peringatan penganalisa, tetapi coba di sini dan lebih jauh untuk menemukan kesalahan sendiri.

void QZipReaderPrivate::scanFiles() { .... // find EndOfDirectory header 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) break; ++i; } .... } 

Peringatan PVS-Studio: V654 Kondisi 'start_of_directory == - 1' dari loop selalu benar. qzip.cpp 617

Bisakah Anda menemukan kesalahan? Jika demikian, maka pujilah.

Kesalahannya terletak pada kondisi loop sementara . Itu selalu benar, karena variabel start_of_directory tidak berubah dalam tubuh loop. Kemungkinan besar, siklus tidak akan abadi, karena mengandung kembali dan rusak , tetapi kode tersebut terlihat aneh.

Sepertinya saya bahwa dalam kode mereka lupa membuat tugas start_of_directory = pos di tempat di mana tanda tangan sedang diverifikasi. Maka pernyataan break mungkin berlebihan. Dalam hal ini, kode dapat ditulis ulang seperti ini:

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

Namun, saya tidak yakin kode harus seperti apa. Yang terbaik adalah pengembang proyek itu sendiri menganalisis bagian dari program ini dan membuat perubahan yang diperlukan.

Kondisi aneh lainnya:

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

Peringatan PVS-Studio: V501 Ada sub-ekspresi identik 'cap.intersects (cap2)' di kiri dan di kanan operator '&&'. StelProjectorClasses.hpp 175

Seperti yang mungkin sudah Anda tebak, kesalahan terletak pada baris terakhir fungsi: programmer membuat kesalahan ketik, dan pada akhirnya ternyata fungsi mengembalikan hasil terlepas dari nilai cap3 .

Jenis kesalahan ini sangat umum: di hampir setiap proyek yang diuji, kami telah menemukan kesalahan ketik yang terkait dengan nama-nama bentuk name1 dan name2 dan sejenisnya. Biasanya, kesalahan seperti itu terkait dengan salin-tempel.

Contoh kode ini adalah contoh utama dari pola kesalahan umum lainnya, yang bahkan kami lakukan studi-mini terpisah. Rekan saya Andrei Karpov menyebutnya " efek garis terakhir ." Jika Anda tidak terbiasa dengan materi ini, saya sarankan membuka tab di browser untuk dibaca nanti, tetapi untuk sekarang, mari kita lanjutkan.

 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) { .... } } 

Peringatan PVS-Studio:

  • V560 Bagian dari ekspresi kondisional selalu benar: updatePos. StelGuiItems.cpp 732
  • Ekspresi V547 'updatePos' selalu benar. StelGuiItems.cpp 831
  • V763 Parameter 'updatePos' selalu ditulis ulang di badan fungsi sebelum digunakan. StelGuiItems.cpp 690

Nilai parameter updatePos selalu ditimpa sebelum digunakan, mis. fungsi akan bekerja sama, terlepas dari nilai yang diteruskan ke sana.

Terlihat aneh, bukan? Di semua tempat di mana parameter updatePos terlibat , itu benar . Ini berarti bahwa kondisi if (location-> text ()! = NewLocation || updatePos) dan if (updatePos) akan selalu benar.

Cuplikan lain:

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

Peringatan PVS-Studio:

  • V792 Fungsi 'toBool' yang terletak di sebelah kanan operator '&' akan dipanggil terlepas dari nilai operan kiri. Mungkin, lebih baik menggunakan '&&'. LandscapeMgr.cpp 782
  • V792 Fungsi 'toBool' yang terletak di sebelah kanan operator '&' akan dipanggil terlepas dari nilai operan kiri. Mungkin, lebih baik menggunakan '&&'. LandscapeMgr.cpp 783

Penganalisis mendeteksi ekspresi mencurigakan dalam argumen ke fungsi setFlagAtmosphere dan setFlagFog . Memang: di kedua sisi operator bit & ada nilai tipe bool . Alih-alih operator & , Anda harus menggunakan operator && , dan sekarang saya akan menjelaskan alasannya.

Ya, hasil dari ungkapan ini akan selalu benar. Sebelum menggunakan bitwise "dan", kedua operan akan dipromosikan ke int . Di C ++, konversi seperti itu tidak ambigu : false dikonversi ke 0, dan true dikonversi ke 1. Oleh karena itu, hasil dari ekspresi ini akan sama seperti jika operator && digunakan.

Namun ada nuansa. Saat menghitung hasil operasi && , apa yang disebut "perhitungan malas" digunakan. Jika nilai operan kiri salah , maka nilai kanan bahkan tidak dihitung, karena logika "dan" dalam hal apa pun akan mengembalikan false . Ini dilakukan untuk menghemat sumber daya komputasi dan memungkinkan Anda untuk menulis desain yang lebih kompleks. Misalnya, Anda dapat memverifikasi bahwa pointer bukan nol, dan jika demikian, lakukan referensi untuk melakukan pemeriksaan tambahan. Contoh: if (ptr && ptr-> foo ()) .

"Perhitungan malas" seperti itu tidak dilakukan dengan menggunakan operator bitwise & : ekspresi conf-> nilai ("...", true). ToBool () akan dievaluasi setiap waktu, terlepas dari nilai pl-> hasAtmosphere () .

Dalam kasus yang jarang terjadi, ini dilakukan dengan sengaja. Misalnya, jika menghitung operan yang tepat memiliki "efek samping," hasilnya akan digunakan kemudian. Tidak terlalu baik untuk melakukannya, karena mempersulit pemahaman kode dan mempersulit pemeliharaan. Selain itu, urutan penghitungan operan & tidak ditentukan, sehingga dalam beberapa kasus menggunakan "trik" seperti itu Anda bisa mendapatkan perilaku yang tidak terdefinisi.

Jika Anda perlu menyimpan efek samping - lakukan dalam baris terpisah dan simpan hasilnya dalam variabel terpisah. Orang-orang yang akan bekerja dengan kode ini di masa depan akan berterima kasih kepada Anda :)


Kami melanjutkan ke topik berikutnya.

Penanganan memori salah


Mari kita mulai topik memori dinamis dengan fragmen ini:

 /************ Basic Edge Operations ****************/ /* __gl_meshMakeEdge creates one edge, * two vertices, and a loop (face). * The loop consists of the two new half-edges. */ GLUEShalfEdge* __gl_meshMakeEdge(GLUESmesh* mesh) { GLUESvertex* newVertex1 = allocVertex(); GLUESvertex* newVertex2 = allocVertex(); GLUESface* newFace = allocFace(); GLUEShalfEdge* e; /* if any one is null then all get freed */ 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; } 

Peringatan PVS-Studio:

  • V773 Fungsi itu keluar tanpa melepaskan pointer 'newVertex1'. Kebocoran memori dimungkinkan. mesh.c 312
  • V773 Fungsi itu keluar tanpa melepaskan pointer 'newVertex2'. Kebocoran memori dimungkinkan. mesh.c 312
  • V773 Fungsi itu keluar tanpa melepaskan pointer 'newFace'. Kebocoran memori dimungkinkan. mesh.c 312

Fungsi ini mengalokasikan memori untuk beberapa struktur dan meneruskannya ke pointer newVertex1 , newVertex2 (nama yang menarik, bukan?) Dan newFace . Jika salah satunya berubah menjadi nol, maka semua memori yang disimpan di dalam fungsi dibebaskan, setelah itu aliran kontrol meninggalkan fungsi.

Apa yang terjadi jika memori untuk ketiga struktur dialokasikan dengan benar, dan fungsi MakeEdge (& mesh-> eHead) mengembalikan NULL ? Aliran kontrol akan mencapai pengembalian kedua.

Karena pointer newVertex1 , newVertex2 dan newFace adalah variabel lokal, mereka tidak akan ada lagi setelah keluar dari fungsi. Tetapi pelepasan memori milik mereka tidak akan terjadi. Itu akan tetap dicadangkan, tetapi kami tidak akan lagi memiliki akses ke sana.

Situasi seperti ini disebut kebocoran memori. Skenario khas dengan kesalahan seperti itu: dengan penggunaan program yang berkepanjangan, ia mulai mengkonsumsi lebih banyak dan lebih banyak RAM, hingga habisnya.

Perlu dicatat bahwa dalam contoh ini, pengembalian ketiga tidak salah. Fungsi MakeVertex dan MakeFace mentransfer alamat memori yang dialokasikan ke struktur data lain, sehingga mendelegasikan tanggung jawab untuk rilisnya.

Kelemahan berikutnya adalah dalam metode, yang membutuhkan 90 baris. Untuk kenyamanan, saya menguranginya, hanya menyisakan area masalah.

 void AstroCalcDialog::drawAngularDistanceGraph() { .... QVector<double> xs, ys; .... } 

Hanya satu baris yang tersisa. Biarkan saya memberi Anda petunjuk: ini adalah satu-satunya penyebutan objek xs dan ys .

Peringatan PVS-Studio:

  • Objek 'xs' V808 dari tipe 'QVector' telah dibuat tetapi tidak digunakan. AstroCalcDialog.cpp 5329
  • Objek 'ys' V808 dari tipe 'QVector' telah dibuat tetapi tidak digunakan. AstroCalcDialog.cpp 5329

Vektor xs dan ys dibuat, tetapi tidak digunakan di mana pun. Ternyata setiap kali Anda menggunakan metode drawAngularDistanceGraph , kreasi dan penghapusan ekstra dari wadah kosong terjadi. Saya pikir iklan ini tetap dalam kode setelah refactoring. Ini, tentu saja, bukan kesalahan, tetapi Anda harus menghapus kode tambahan.

Gips tipe aneh


Contoh lain setelah sedikit pemformatan terlihat seperti ini:

 void SatellitesDialog::updateSatelliteData() { .... // set default buttonColor = QColor(0.4, 0.4, 0.4); .... } 

Untuk memahami apa kesalahannya, Anda harus melihat prototipe konstruktor kelas Qcolor :


Peringatan PVS-Studio:

  • V674 '0,4' tipe 'ganda' literal secara implisit dilemparkan ke tipe 'int' sambil memanggil fungsi 'QColor'. Periksa argumen pertama. SatellitesDialog.cpp 413
  • V674 '0,4' tipe 'ganda' literal secara implisit dilemparkan ke tipe 'int' sambil memanggil fungsi 'QColor'. Periksa argumen kedua. SatellitesDialog.cpp 413
  • V674 '0,4' tipe 'ganda' literal secara implisit dilemparkan ke tipe 'int' sambil memanggil fungsi 'QColor'. Periksa argumen ketiga. SatellitesDialog.cpp 413

Kelas Qcolor tidak memiliki konstruktor yang menerima tipe ganda , jadi argumen dalam contoh ini akan secara implisit dikonversi ke int . Ini menyebabkan bidang r , g , b dari objek buttonColor memiliki nilai 0 .

Jika programmer berniat untuk membuat objek dari nilai tipe double , ia harus menggunakan konstruktor yang berbeda.

Misalnya, Anda bisa menggunakan konstruktor yang menerima Qrgb dengan menulis:

 buttonColor = QColor(QColor::fromRgbF(0.4, 0.4, 0.4)); 

Itu bisa dilakukan secara berbeda. Qt menggunakan nilai nyata dalam kisaran [0,0, 1.0] atau nilai integer dalam kisaran [0, 255] untuk menunjukkan warna RGB.

Oleh karena itu, programmer dapat menerjemahkan nilai dari real ke integer dengan menulis seperti ini:

 buttonColor = QColor((int)(255 * 0.4), (int)(255 * 0.4), (int)(255 * 0.4)); 

atau adil

 buttonColor = QColor(102, 102, 102); 

Apakah kamu bosan? Jangan khawatir: ada kesalahan lebih menarik di depan kita.


"Unicorn di luar angkasa." Pemandangan dari Stellarium.

Kesalahan lainnya


Pada akhirnya, saya meninggalkan Anda lebih lezat :) Mari kita turun ke salah satu dari mereka.

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

Peringatan PVS-Studio: V634 Prioritas operasi '*' lebih tinggi daripada operasi '<<'. Mungkin tanda kurung harus digunakan dalam ekspresi. StelHips.cpp 271

Nah, apakah Anda bisa mendeteksi kesalahan? Pertimbangkan ungkapan (12 * 1 << (2 * urutan)) lebih terinci. Penganalisa mengingat bahwa operasi ' * ' memiliki prioritas lebih tinggi daripada operasi pergeseran bit ' << '. Sangat mudah untuk melihat bahwa mengalikan 12 dengan 1 tidak ada gunanya, dan tanda kurung sekitar 2 * tidak diperlukan.

  ,    : int nbw = sqrt(12 * (1 << 2 * order));     <i>12 </i>     . 

Catatan Selain itu, saya ingin mencatat bahwa jika nilai operan kanan ' << ' lebih besar atau sama dengan jumlah bit dari operan kiri, maka hasilnya tidak ditentukan. Karena literal numerik adalah int secara default, yang membutuhkan 32 bit, nilai parameter urutan tidak boleh lebih dari 15 . Jika tidak, evaluasi ekspresi dapat mengakibatkan perilaku yang tidak terdefinisi.

Kami melanjutkan. Metode di bawah ini sangat membingungkan, tapi saya yakin pembaca yang canggih akan menangani deteksi kesalahan :)

 /* inherits documentation from base class */ 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(); } 

Peringatan PVS-Studio: V779 Kode yang tidak dapat dideteksi terdeteksi. Mungkin saja ada kesalahan. qcustomplot.cpp 19512.

Faktanya adalah bahwa semua jika ... cabang lain memiliki pengembalian . Oleh karena itu, aliran kontrol tidak pernah mencapai dua baris terakhir.

Pada umumnya, contoh ini akan berjalan secara normal dan bekerja dengan benar. Tetapi kehadiran kode yang tidak dapat dijangkau sendiri merupakan sinyal. Dalam hal ini, ini menunjukkan struktur metode yang salah, yang sangat menyulitkan keterbacaan dan kelengkapan kode.

Fragmen kode ini harus di refactored, mendapatkan fungsi yang lebih rapi pada output. Misalnya, seperti ini:

 /* inherits documentation from base class */ 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(); } 

Yang terakhir dalam ulasan kami adalah kesalahan yang paling saya sukai. Kode tempat masalah pendek dan sederhana:

 Plane::Plane(Vec3f &v1, Vec3f &v2, Vec3f &v3) : distance(0.0f), sDistance(0.0f) { Plane(v1, v2, v3, SPolygon::CCW); } 

Pernahkah Anda memperhatikan sesuatu yang mencurigakan? Tidak semua orang bisa :)

Peringatan PVS-Studio: V603 Objek telah dibuat tetapi tidak digunakan. Jika Anda ingin memanggil konstruktor, 'this-> Plane :: Plane (....)' harus digunakan. Plane.cpp 29

Pemrogram berharap bahwa beberapa bidang objek akan diinisialisasi di dalam konstruktor bersarang, tetapi ternyata seperti ini: ketika konstruktor Plane (Vec3f & v1, Vec3f & v2, Vec3f & v3) dipanggil, objek sementara yang tidak disebutkan namanya dibuat di dalamnya, yang segera dihapus. Akibatnya, bagian dari objek tetap tidak diinisialisasi.

Agar kode berfungsi dengan benar, Anda harus menggunakan fitur C ++ 11 yang nyaman dan aman - konstruktor yang mendelegasikan:

 Plane::Plane(Vec3f& v1, Vec3f& v2, Vec3f& v3) : Plane(v1, v2, v3, SPolygon::CCW) { distance = 0.0f; sDistance = 0.0f; } 

Tetapi jika Anda menggunakan kompiler untuk versi bahasa yang lebih lama, maka Anda dapat menulis seperti ini:

 Plane::Plane(Vec3f &v1, Vec3f &v2, Vec3f &v3) : distance(0.0f), sDistance(0.0f) { this->Plane::Plane(v1, v2, v3, SPolygon::CCW); } 

Atau lebih:

 Plane::Plane(Vec3f &v1, Vec3f &v2, Vec3f &v3) : distance(0.0f), sDistance(0.0f) { new (this) Plane(v1, v2, v3, SPolygon::CCW); } 

Saya perhatikan bahwa dua metode terakhir sangat berbahaya . Karena itu, Anda harus sangat berhati-hati dan memahami dengan baik bagaimana metode tersebut bekerja.

Kesimpulan


Kesimpulan apa yang dapat dibuat tentang kualitas kode Stellarium? Jujur saja, tidak banyak kesalahan. Juga, dalam keseluruhan proyek, saya tidak menemukan kesalahan tunggal di mana kode terkait dengan perilaku yang tidak ditentukan. Untuk proyek opensource, kualitas kode ternyata berada pada tingkat tinggi, yang saya angkat topi untuk para pengembang. Kalian hebat! Saya senang dan tertarik untuk meninjau proyek Anda.

Bagaimana dengan planetarium itu sendiri - saya sering menggunakannya. Sayangnya, tinggal di kota, saya jarang dapat menikmati langit malam yang cerah, dan Stellarium memungkinkan saya untuk berada di mana saja di dunia tanpa bangun dari sofa. Sangat nyaman!

Saya terutama menyukai mode "Constellation art". Melihat sosok-sosok besar yang menutupi seluruh langit dalam tarian aneh sungguh menakjubkan.


"Tarian yang aneh." Pemandangan dari Stellarium.

Earthlings cenderung membuat kesalahan, dan tidak ada yang memalukan dalam kenyataan bahwa kesalahan ini bocor ke dalam kode. Untuk ini, alat analisis kode, seperti PVS-Studio, dikembangkan. Jika Anda salah satu penduduk dunia - menyukainya, saya sarankan Anda unduh dan coba sendiri .

Saya harap Anda tertarik membaca artikel saya, dan Anda mempelajari sesuatu yang baru dan berguna untuk diri Anda sendiri. Dan saya berharap para pengembang koreksi awal dari kesalahan yang ditemukan.

Berlangganan saluran kami dan tetap ikuti berita dari dunia pemrograman!



Jika Anda ingin berbagi artikel ini dengan audiens yang berbahasa Inggris, silakan gunakan tautan ke terjemahan: George Gribkov. Into Space Again: bagaimana Unicorn Mengunjungi Stellarium

Source: https://habr.com/ru/post/id432954/


All Articles