Jadi saya menyelesaikan tugas pemrograman C ++ pertama saya dan menerima nilai saya. Tetapi menurut penilaian, saya kehilangan nilai untuk including cpp files instead of compiling and linking them
. Saya tidak terlalu jelas tentang apa artinya itu.
Melihat kembali kode saya, saya memilih untuk tidak membuat file header untuk kelas saya, tetapi melakukan segalanya dalam file cpp (sepertinya berfungsi dengan baik tanpa file header ...). Saya menduga bahwa grader berarti saya menulis '#include "mycppfile.cpp";' di beberapa file saya.
Alasan saya untuk #include
membuat file cpp adalah: - Segala sesuatu yang seharusnya masuk ke file header ada di file cpp saya, jadi saya berpura-pura itu seperti file header - Di monyet-lihat-monyet lakukan mode, saya melihat yang lain file header ada #include
di file, jadi saya melakukan hal yang sama untuk file cpp saya.
Jadi apa yang sebenarnya saya lakukan salah, dan mengapa itu buruk?
sumber
Jawaban:
Sepengetahuan saya, standar C ++ tidak mengenal perbedaan antara file header dan file sumber. Sejauh menyangkut bahasa, file teks apa pun dengan kode hukum sama dengan yang lain. Namun, meskipun tidak ilegal, termasuk file sumber ke dalam program Anda akan menghilangkan banyak keuntungan yang Anda dapatkan dari memisahkan file sumber Anda.
Intinya, apa yang
#include
dilakukan adalah memerintahkan preprosesor untuk mengambil seluruh file yang Anda tentukan, dan menyalinnya ke file aktif Anda sebelum kompiler mengambilnya. Jadi ketika Anda memasukkan semua file sumber dalam proyek Anda bersama-sama, pada dasarnya tidak ada perbedaan antara apa yang telah Anda lakukan, dan hanya membuat satu file sumber besar tanpa pemisahan sama sekali."Oh, itu bukan masalah besar. Jika berjalan, tidak apa-apa," aku mendengar kamu menangis. Dan dalam arti tertentu, Anda benar. Tetapi saat ini Anda sedang berhadapan dengan program kecil mungil, dan CPU yang bagus dan relatif tidak terbebani untuk mengkompilasi untuk Anda. Anda tidak akan selalu seberuntung itu.
Jika Anda mempelajari bidang pemrograman komputer yang serius, Anda akan melihat proyek dengan jumlah garis yang dapat mencapai jutaan, bukan puluhan. Itu banyak baris. Dan jika Anda mencoba untuk mengkompilasi salah satu dari ini di komputer desktop modern, ini bisa memakan waktu beberapa jam, bukan detik.
"Oh, tidak! Kedengarannya mengerikan! Namun bisakah aku mencegah nasib buruk ini ?!" Sayangnya, tidak banyak yang dapat Anda lakukan tentang itu. Jika dibutuhkan waktu berjam-jam untuk dikompilasi, dibutuhkan waktu berjam-jam untuk dikompilasi. Tapi itu hanya benar-benar penting pertama kali - setelah Anda mengkompilasinya sekali, tidak ada alasan untuk mengkompilasinya lagi.
Kecuali Anda mengubah sesuatu.
Sekarang, jika Anda memiliki dua juta baris kode digabungkan menjadi satu raksasa raksasa, dan perlu melakukan perbaikan bug sederhana seperti, katakanlah
x = y + 1
, itu berarti Anda harus mengkompilasi semua dua juta baris lagi untuk menguji ini. Dan jika Anda mengetahui bahwa Anda bermaksud melakukanx = y - 1
sebaliknya, sekali lagi, dua juta baris kompilasi sedang menunggu Anda. Itu menghabiskan berjam-jam waktu yang bisa lebih baik dihabiskan untuk melakukan hal lain."Tapi aku benci menjadi tidak produktif! Kalau saja ada cara untuk menyusun bagian-bagian berbeda dari basis kode saya secara individual, dan entah bagaimana menghubungkannya bersama setelah itu!" Ide bagus, secara teori. Tetapi bagaimana jika program Anda perlu tahu apa yang terjadi dalam file yang berbeda? Tidak mungkin untuk sepenuhnya memisahkan basis kode Anda kecuali jika Anda ingin menjalankan banyak file .exe kecil.
"Tapi pastinya itu mungkin! Pemrograman kedengarannya seperti siksaan murni jika tidak! Bagaimana jika saya menemukan cara untuk memisahkan antarmuka dari implementasi ? Katakan dengan mengambil informasi yang cukup dari segmen kode yang berbeda ini untuk mengidentifikasi mereka ke seluruh program, dan menempatkan mereka dalam semacam file header ? Dan dengan begitu, saya dapat menggunakan
#include
arahan preprosesor untuk hanya membawa informasi yang diperlukan untuk dikompilasi! "Hmm. Anda mungkin ke sesuatu di sana. Biarkan saya tahu cara kerjanya bagi Anda.
sumber
Ini mungkin jawaban yang lebih rinci daripada yang Anda inginkan, tetapi saya pikir penjelasan yang layak dibenarkan.
Dalam C dan C ++, satu file sumber didefinisikan sebagai satu unit terjemahan . Secara konvensi, file header memiliki deklarasi fungsi, ketik definisi, dan definisi kelas. Implementasi fungsi aktual berada di unit terjemahan, yaitu file .cpp.
Gagasan di balik ini adalah bahwa fungsi dan fungsi anggota kelas / struct dikompilasi dan dirakit sekali, maka fungsi lain dapat memanggil kode itu dari satu tempat tanpa membuat duplikat. Fungsi Anda dinyatakan sebagai "eksternal" secara implisit.
Jika Anda ingin fungsi menjadi lokal untuk unit terjemahan, Anda mendefinisikannya sebagai 'statis'. Apa artinya ini? Ini berarti bahwa jika Anda memasukkan file sumber dengan fungsi eksternal, Anda akan mendapatkan kesalahan redefinisi, karena kompiler menemukan implementasi yang sama lebih dari sekali. Jadi, Anda ingin semua unit terjemahan Anda melihat deklarasi fungsi tetapi bukan badan fungsi .
Jadi bagaimana semua itu dihaluskan bersama pada akhirnya? Itu adalah tugas penghubung. Linker membaca semua file objek yang dihasilkan oleh tahap assembler dan menyelesaikan simbol. Seperti yang saya katakan sebelumnya, simbol hanyalah sebuah nama. Misalnya, nama variabel atau fungsi. Ketika unit terjemahan yang memanggil fungsi atau menyatakan tipe tidak mengetahui implementasi untuk fungsi atau tipe tersebut, simbol tersebut dikatakan tidak terselesaikan. Linker menyelesaikan simbol yang tidak terselesaikan dengan menghubungkan unit terjemahan yang menyimpan simbol yang tidak ditentukan bersama dengan yang berisi implementasi. Fiuh. Ini berlaku untuk semua simbol yang terlihat secara eksternal, apakah simbol tersebut diimplementasikan dalam kode Anda, atau disediakan oleh perpustakaan tambahan. Perpustakaan sebenarnya hanya arsip dengan kode yang dapat digunakan kembali.
Ada dua pengecualian penting. Pertama, jika Anda memiliki fungsi kecil, Anda bisa membuatnya sejajar. Ini berarti bahwa kode mesin yang dihasilkan tidak menghasilkan panggilan fungsi eksternal, tetapi secara harfiah digabungkan di tempat. Karena mereka biasanya kecil, ukuran overhead tidak masalah. Anda dapat membayangkan mereka statis dalam cara mereka bekerja. Jadi aman untuk mengimplementasikan fungsi inline di header. Implementasi fungsi di dalam kelas atau definisi struct juga sering diuraikan secara otomatis oleh kompiler.
Pengecualian lainnya adalah templat. Karena kompiler perlu melihat definisi tipe templat keseluruhan saat membuat instance, tidak mungkin memisahkan implementasi dari definisi tersebut dengan fungsi mandiri atau kelas normal. Yah, mungkin ini mungkin sekarang, tetapi mendapatkan dukungan kompiler yang tersebar luas untuk kata kunci "ekspor" membutuhkan waktu yang sangat lama. Jadi tanpa dukungan untuk 'ekspor', unit terjemahan mendapatkan salinan lokal mereka sendiri dari jenis dan fungsi templated instantiated, mirip dengan bagaimana fungsi inline bekerja. Dengan dukungan untuk 'ekspor', ini tidak terjadi.
Untuk dua pengecualian, beberapa orang merasa "lebih baik" untuk meletakkan implementasi fungsi inline, fungsi templated, dan tipe templated dalam file .cpp, dan kemudian # sertakan file .cpp. Apakah ini header atau file sumber tidak terlalu penting; preprocessor tidak peduli dan hanya sebuah konvensi.
Ringkasan cepat seluruh proses mulai dari kode C ++ (beberapa file) dan hingga yang dapat dieksekusi akhir:
Sekali lagi, ini jelas lebih dari yang Anda minta, tapi saya harap detail seluk beluk membantu Anda melihat gambaran yang lebih besar.
sumber
int add(int, int);
adalah deklarasi fungsi . Bagian prototipe itu adilint, int
. Namun, semua fungsi dalam C ++ memiliki prototipe, jadi istilah ini benar-benar masuk akal dalam C. Saya telah mengedit jawaban Anda untuk efek ini.export
untuk templat telah dihapus dari bahasa pada tahun 2011. Itu tidak pernah benar-benar didukung oleh kompiler.Solusi khasnya adalah menggunakan
.h
file hanya untuk deklarasi dan.cpp
file untuk implementasi. Jika Anda perlu menggunakan kembali implementasi Anda memasukkan.h
file yang sesuai ke dalam.cpp
file di mana kelas / fungsi / apa pun yang diperlukan digunakan dan tautan ke.cpp
file yang sudah dikompilasi (baik.obj
file - biasanya digunakan dalam satu proyek - atau file .lib - biasanya digunakan untuk digunakan kembali dari berbagai proyek). Dengan cara ini Anda tidak perlu mengkompilasi ulang semuanya jika hanya implementasi yang berubah.sumber
Pikirkan file cpp sebagai kotak hitam dan file .h sebagai panduan tentang cara menggunakan kotak hitam itu.
File cpp dapat dikompilasi sebelumnya. Ini tidak berfungsi di dalam Anda # sertakan mereka, karena perlu "sertakan" kode yang sebenarnya ke dalam program Anda setiap kali dikompilasi. Jika Anda hanya menyertakan header, itu bisa menggunakan file header untuk menentukan bagaimana menggunakan file cpp yang telah dikompilasi.
Meskipun ini tidak akan membuat banyak perbedaan untuk proyek pertama Anda, jika Anda mulai menulis program cpp besar, orang-orang akan membenci Anda karena waktu kompilasi akan meledak.
Juga baca ini: File Header Sertakan Pola
sumber
File header biasanya berisi deklarasi fungsi / kelas, sedangkan file .cpp berisi implementasi yang sebenarnya. Pada waktu kompilasi, setiap file .cpp akan dikompilasi menjadi file objek (biasanya ekstensi .o), dan linker menggabungkan berbagai file objek ke dalam executable akhir. Proses penautan umumnya jauh lebih cepat daripada kompilasi.
Manfaat dari pemisahan ini: Jika Anda mengkompilasi ulang salah satu file .cpp di proyek Anda, Anda tidak perlu mengkompilasi ulang semua yang lain. Anda cukup membuat file objek baru untuk file .cpp tertentu. Kompiler tidak harus melihat file .cpp lainnya. Namun, jika Anda ingin memanggil fungsi dalam file .cpp Anda saat ini yang diimplementasikan dalam file .cpp lainnya, Anda harus memberi tahu kompilator argumen apa yang mereka ambil; itulah tujuan memasukkan file header.
Kekurangan: Ketika mengkompilasi file .cpp yang diberikan, kompiler tidak dapat 'melihat' apa yang ada di dalam file .cpp lainnya. Jadi tidak tahu bagaimana fungsi-fungsi yang ada diimplementasikan, dan sebagai hasilnya tidak dapat mengoptimalkan secara agresif. Tapi saya pikir Anda belum perlu memikirkannya sendiri (:
sumber
Gagasan dasar bahwa header hanya disertakan dan file cpp hanya dikompilasi. Ini akan menjadi lebih bermanfaat setelah Anda memiliki banyak file cpp, dan mengkompilasi ulang seluruh aplikasi saat Anda memodifikasi hanya satu di antaranya yang akan terlalu lambat. Atau kapan fungsi dalam file akan mulai tergantung satu sama lain. Jadi, Anda harus memisahkan deklarasi kelas ke dalam file header Anda, meninggalkan implementasi dalam file cpp dan menulis Makefile (atau sesuatu yang lain, tergantung pada alat apa yang Anda gunakan) untuk mengkompilasi file cpp dan menautkan file objek yang dihasilkan ke dalam suatu program.
sumber
Jika Anda #memasukkan file cpp ke beberapa file lain di program Anda, kompiler akan mencoba mengkompilasi file cpp beberapa kali, dan akan menghasilkan kesalahan karena akan ada beberapa implementasi dari metode yang sama.
Kompilasi akan memakan waktu lebih lama (yang menjadi masalah pada proyek-proyek besar), jika Anda mengedit dalam file cpp #included, yang kemudian memaksa kompilasi ulang file apa pun #termasuk mereka.
Cukup masukkan deklarasi Anda ke file header dan sertakan (karena mereka tidak benar-benar menghasilkan kode per se), dan linker akan menghubungkan deklarasi dengan kode cpp yang sesuai (yang kemudian hanya dikompilasi sekali).
sumber
Meskipun tentu saja mungkin untuk melakukan seperti yang Anda lakukan, praktik standarnya adalah menempatkan deklarasi bersama ke dalam file header (.h), dan definisi fungsi dan variabel - implementasi - ke dalam file sumber (.cpp).
Sebagai konvensi, ini membantu memperjelas di mana semuanya berada, dan membuat perbedaan yang jelas antara antarmuka dan implementasi modul Anda. Ini juga berarti bahwa Anda tidak perlu memeriksa untuk melihat apakah file .cpp termasuk dalam yang lain, sebelum menambahkan sesuatu ke dalamnya yang dapat pecah jika didefinisikan dalam beberapa unit yang berbeda.
sumber
re-usability, arsitektur dan enkapsulasi data
ini sebuah contoh:
katakanlah Anda membuat file cpp yang berisi bentuk sederhana dari rutinitas string semua dalam kelas mystring, Anda menempatkan kelas mendeklarasikan ini dalam mystring.h kompilasi mystring.cpp ke file .obj
sekarang di program utama Anda (mis. main.cpp) Anda memasukkan header dan tautan dengan mystring.obj. untuk menggunakan mystring di program Anda, Anda tidak peduli tentang detail bagaimana mystring diimplementasikan karena header mengatakan apa yang bisa dilakukan
sekarang jika seorang teman ingin menggunakan kelas mistis Anda, Anda memberinya mystring.h dan mystring.obj, dia juga tidak perlu tahu cara kerjanya selama itu bekerja.
nanti jika Anda memiliki lebih banyak file .obj tersebut, Anda dapat menggabungkannya menjadi file .lib dan menautkannya.
Anda juga dapat memutuskan untuk mengubah file mystring.cpp dan mengimplementasikannya secara lebih efektif, ini tidak akan memengaruhi main.cpp Anda atau program teman Anda.
sumber
Jika itu bekerja untuk Anda, maka tidak ada yang salah dengan itu - kecuali bahwa itu akan mengacak-acak bulu orang yang berpikir bahwa hanya ada satu cara untuk melakukan sesuatu.
Banyak jawaban yang diberikan di sini membahas optimasi untuk proyek perangkat lunak skala besar. Ini adalah hal-hal yang baik untuk diketahui, tetapi tidak ada gunanya mengoptimalkan proyek kecil seolah-olah itu adalah proyek besar - itulah yang dikenal sebagai "optimasi prematur". Tergantung pada lingkungan pengembangan Anda, mungkin ada kompleksitas tambahan yang signifikan yang terlibat dalam pengaturan konfigurasi pembangunan untuk mendukung beberapa file sumber per program.
Jika, seiring berjalannya waktu, proyek Anda berkembang dan Anda merasa bahwa proses pembuatannya terlalu lama, maka Anda dapat memperbaiki kode Anda untuk menggunakan banyak file sumber untuk pembuatan bertahap yang lebih cepat.
Beberapa jawaban membahas pemisahan antarmuka dari implementasi. Namun, ini bukan fitur bawaan dari file sertakan, dan sangat umum untuk memasukkan # "header" file yang secara langsung memasukkan implementasinya (bahkan C ++ Standard Library melakukan ini pada tingkat yang signifikan).
Satu-satunya hal yang benar-benar "tidak konvensional" tentang apa yang telah Anda lakukan adalah memberi nama file yang disertakan ".cpp" alih-alih ".h" atau ".hpp".
sumber
Ketika Anda mengkompilasi dan menautkan suatu program, kompiler pertama-tama mengkompilasi masing-masing file cpp dan kemudian mereka menautkan (menghubungkan) mereka. Header tidak akan pernah dikompilasi, kecuali dimasukkan dalam file cpp terlebih dahulu.
Biasanya header adalah deklarasi dan cpp adalah file implementasi. Di header Anda mendefinisikan antarmuka untuk kelas atau fungsi tetapi Anda mengabaikan bagaimana Anda benar-benar mengimplementasikan detail. Dengan cara ini Anda tidak perlu mengkompilasi ulang setiap file cpp jika Anda mengubahnya.
sumber
Saya akan menyarankan Anda untuk pergi melalui Desain Software C ++ Skala Besar oleh John Lakos . Di kampus, kami biasanya menulis proyek kecil di mana kami tidak menemukan masalah seperti itu. Buku ini menyoroti pentingnya memisahkan antarmuka dan implementasinya.
File header biasanya memiliki antarmuka yang seharusnya tidak terlalu sering diubah. Demikian pula dengan melihat pola seperti idiom Virtual Constructor akan membantu Anda memahami konsep lebih lanjut.
Saya masih belajar seperti Anda :)
sumber
Ini seperti menulis buku, Anda hanya ingin mencetak bab yang sudah selesai
Katakanlah Anda sedang menulis buku. Jika Anda meletakkan bab-bab dalam file yang terpisah maka Anda hanya perlu mencetak bab jika Anda telah mengubahnya. Bekerja pada satu bab tidak mengubah yang lain.
Tetapi termasuk file cpp, dari sudut pandang kompiler, seperti mengedit semua bab buku dalam satu file. Kemudian jika Anda mengubahnya, Anda harus mencetak semua halaman dari seluruh buku untuk mendapatkan bab revisi Anda dicetak. Tidak ada opsi "cetak halaman yang dipilih" dalam pembuatan kode objek.
Kembali ke perangkat lunak: Saya memiliki Linux dan Ruby src tergeletak di sana. Ukuran garis kode yang kasar ...
Salah satu dari empat kategori tersebut memiliki banyak kode, karenanya diperlukan modularitas. Basis kode semacam ini sangat tipikal untuk sistem dunia nyata.
sumber