Mengapa kita perlu menyertakan .h sementara semuanya berfungsi saat hanya menyertakan file .cpp?

18

Mengapa kita perlu menyertakan file .hdan .cppsekaligus sementara kita dapat membuatnya bekerja hanya dengan memasukkan .cppfile?

Misalnya: membuat file.hdeklarasi yang mengandung, lalu membuat file.cppdefinisi yang mengandung dan memasukkan keduanya dalam main.cpp.

Atau: membuat file.cppdeklarasi / definisi yang mengandung (tidak ada prototipe) termasuk di dalamnya main.cpp.

Keduanya bekerja untuk saya. Saya tidak bisa melihat perbedaannya. Mungkin beberapa wawasan tentang proses kompilasi dan penautan dapat membantu.

punggawa
sumber
Berbagi penelitian Anda membantu semua orang . Beri tahu kami apa yang telah Anda coba dan mengapa itu tidak memenuhi kebutuhan Anda. Ini menunjukkan bahwa Anda telah meluangkan waktu untuk mencoba membantu diri sendiri, itu menyelamatkan kami dari mengulangi jawaban yang jelas, dan yang paling utama itu membantu Anda mendapatkan jawaban yang lebih spesifik dan relevan. Lihat juga Cara Meminta
nyamuk
Saya sangat merekomendasikan melihat pertanyaan terkait di sini di samping. programmers.stackexchange.com/questions/56215/… dan programmers.stackexchange.com/questions/115368/… dapat dibaca dengan baik
Mengapa ini pada Programmer, dan bukan pada SO?
Lightness Races dengan Monica
@LightnessRacesInOrbit karena ini adalah pertanyaan tentang gaya pengkodean dan bukan pertanyaan "bagaimana".
CashCow
@CashCow: Sepertinya Anda salah paham SO, yang bukan situs "bagaimana caranya". Sepertinya Anda salah paham tentang pertanyaan ini, yang bukan masalah gaya pengkodean.
Lightness Races dengan Monica

Jawaban:

29

Meskipun Anda dapat memasukkan .cppfile seperti yang Anda sebutkan, ini adalah ide yang buruk.

Seperti yang Anda sebutkan, deklarasi termasuk dalam file header. Ini tidak menyebabkan masalah ketika dimasukkan dalam beberapa unit kompilasi karena mereka tidak menyertakan implementasi. Menyertakan definisi fungsi atau anggota kelas beberapa kali biasanya akan menyebabkan masalah (tetapi tidak selalu) karena linker akan menjadi bingung dan melempar kesalahan.

Apa yang harus terjadi adalah setiap .cppfile menyertakan definisi untuk subset dari program, seperti kelas, kelompok fungsi yang diatur secara logis, variabel statis global (gunakan hemat jika sama sekali), dll.

Setiap unit kompilasi ( .cppfile) kemudian menyertakan deklarasi apa pun yang diperlukan untuk mengkompilasi definisi yang dikandungnya. Itu melacak fungsi dan kelas referensi tetapi tidak mengandung, sehingga linker dapat menyelesaikannya nanti ketika menggabungkan kode objek ke dalam executable atau library.

Contoh

  • Foo.h -> mengandung deklarasi (antarmuka) untuk kelas Foo.
  • Foo.cpp -> mengandung definisi (implementasi) untuk kelas Foo.
  • Main.cpp-> berisi metode utama, titik masuk program. Kode ini membuat Instan Foo dan menggunakannya.

Keduanya Foo.cppdan Main.cppperlu dimasukkan Foo.h. Foo.cppmembutuhkannya karena mendefinisikan kode yang mendukung antarmuka kelas, sehingga perlu tahu apa antarmuka itu. Main.cppmembutuhkannya karena ia menciptakan Foo dan menjalankan perilakunya, sehingga ia harus tahu apa perilakunya, ukuran Foo dalam memori dan bagaimana menemukan fungsinya, dll. tetapi ia belum membutuhkan implementasi yang sebenarnya.

Compiler akan menghasilkan Foo.odari Foo.cppyang berisi semua kode kelas Foo dalam bentuk yang dikompilasi. Ini juga menghasilkan Main.oyang mencakup metode utama dan referensi yang tidak terselesaikan ke kelas Foo.

Sekarang hadir tautan, yang menggabungkan dua file objek Foo.odan Main.omenjadi file yang dapat dieksekusi. Ia melihat referensi Foo yang belum terselesaikan di Main.otetapi melihat yang Foo.oberisi simbol yang diperlukan, sehingga "menghubungkan titik-titik" untuk berbicara. Panggilan fungsi Main.osekarang terhubung ke lokasi aktual dari kode yang dikompilasi sehingga pada saat runtime, program dapat melompat ke lokasi yang benar.

Jika Anda memasukkan Foo.cppfile ke Main.cppdalamnya, akan ada dua definisi kelas Foo. Tautan akan melihat ini dan berkata, "Saya tidak tahu yang mana yang harus dipilih, jadi ini adalah kesalahan." Langkah kompilasi akan berhasil, tetapi menghubungkan tidak akan berhasil. (Kecuali Anda tidak mengkompilasi Foo.cpptetapi mengapa ia berada di .cppfile yang terpisah ?)

Akhirnya, gagasan tentang jenis file yang berbeda tidak relevan dengan kompiler C / C ++. Ini mengkompilasi "file teks" yang diharapkan mengandung kode yang valid untuk bahasa yang diinginkan. Kadang-kadang mungkin bisa memberi tahu bahasa berdasarkan ekstensi file. Sebagai contoh, kompilasi .cfile tanpa opsi kompiler dan ia akan menganggap C, sementara ekstensi .ccatau .cppakan memberitahu itu untuk menganggap C ++. Namun, saya dapat dengan mudah memberitahu kompiler untuk mengkompilasi .hatau bahkan .docxfile sebagai C ++, dan itu akan memancarkan .ofile object ( ) jika mengandung kode C ++ yang valid dalam format teks biasa. Ekstensi ini lebih untuk kepentingan programmer. Jika saya melihat Foo.hdan Foo.cpp, saya langsung berasumsi bahwa yang pertama berisi deklarasi kelas dan yang kedua berisi definisi.


sumber
Saya tidak mengerti argumen yang Anda buat di sini. Jika Anda hanya memasukkan Foo.cppdi dalamnya Main.cppAnda tidak perlu memasukkan .hfile, Anda memiliki satu file lebih sedikit, Anda masih menang dalam memecah kode menjadi file terpisah untuk dibaca, dan perintah kompilasi Anda lebih sederhana. Sementara saya memahami pentingnya file header saya tidak berpikir ini
Benjamin Gruenbaum
2
Untuk program sepele, cukup letakkan semuanya dalam satu file. Bahkan tidak menyertakan header proyek (hanya header perpustakaan). Menyertakan file sumber adalah kebiasaan buruk yang akan menyebabkan masalah bagi program apa pun kecuali yang terkecil setelah Anda memiliki beberapa unit kompilasi dan sebenarnya mengompilasinya secara terpisah.
2
If you had included the Foo.cpp file in Main.cpp, there would be two definitions of class Foo.Kalimat paling penting berkenaan dengan pertanyaan itu.
marczellm
@Snowman Right, yang menurut saya harus Anda fokuskan - beberapa unit kompilasi. Menempel kode dengan / tanpa file header untuk memecah kode menjadi potongan-potongan kecil berguna dan ortogonal dengan tujuan file header. Jika semua yang Anda ingin lakukan adalah membaginya menjadi file-file header, belilah kami apa-apa - setelah kami membutuhkan tautan dinamis, tautan kondisional, dan pembangunan lebih lanjut, tiba-tiba mereka sangat berguna.
Benjamin Gruenbaum
Ada banyak cara untuk mengatur sumber untuk kompiler / penghubung C / C ++. Penanya tidak yakin dengan prosesnya, jadi saya menjelaskan praktik terbaik tentang bagaimana mengatur kode dan bagaimana proses itu bekerja. Anda dapat membagi rambut tentang kemungkinan untuk mengatur kode secara berbeda, tetapi kenyataannya tetap, saya menjelaskan bagaimana 99% dari proyek di luar sana melakukannya yang merupakan praktik terbaik karena suatu alasan. Ini bekerja dengan baik, dan menjaga kewarasan Anda.
9

Baca lebih lanjut tentang peran preprocessor C dan C ++ , yang secara konseptual merupakan "fase" pertama dari kompiler C atau C ++ (secara historis itu adalah program yang terpisah /lib/cpp; sekarang, untuk alasan kinerja ia terintegrasi di dalam compiler yang tepat cc1 atau cc1plus). Baca khususnya dokumentasi cpppreprosesor GNU . Jadi, dalam praktiknya, kompiler secara konseptual terlebih dahulu memproses unit kompilasi Anda (atau unit terjemahan ) dan kemudian bekerja pada formulir preproses.

Anda mungkin harus selalu menyertakan file header file.hjika mengandung (sebagaimana ditentukan oleh konvensi dan kebiasaan ):

  • definisi makro
  • ketik definisi (mis typedef. struct, classdll, ...)
  • definisi static inlinefungsi
  • deklarasi fungsi eksternal.

Perhatikan bahwa ini adalah masalah konvensi (dan kemudahan) untuk meletakkan ini dalam file header.

Tentu saja, implementasi Anda file.cppmembutuhkan semua hal di atas, jadi ingin #include "file.h" pada awalnya.

Ini adalah konvensi (tapi yang sangat umum). Anda dapat menghindari file header dan menyalin dan menempel kontennya ke file implementasi (yaitu unit terjemahan). Tetapi Anda tidak menginginkan itu (kecuali mungkin jika kode C atau C ++ Anda dibuat secara otomatis; maka Anda dapat membuat program generator melakukan copy & paste itu, meniru peran preprosesor).

Intinya adalah bahwa preprosesor melakukan operasi tekstual saja. Anda dapat (pada prinsipnya) menghindarinya sepenuhnya dengan menyalin & menempel, atau menggantinya dengan "preprosesor" lain atau pembuat kode C (seperti gpp atau m4 ).

Masalah tambahan adalah bahwa standar C (atau C ++) baru-baru ini mendefinisikan beberapa header standar. Sebagian besar implementasi benar-benar mengimplementasikan header standar ini sebagai file (khusus implementasi) , tetapi saya percaya bahwa akan mungkin untuk implementasi yang sesuai untuk mengimplementasikan standar termasuk (seperti #include <stdio.h>untuk C, atau #include <vector>untuk C ++) dengan beberapa trik sulap (misalnya menggunakan beberapa basis data atau beberapa informasi di dalam kompiler).

Jika menggunakan kompiler GCC (mis. gccAtau g++) Anda dapat menggunakan -Hflag untuk mendapatkan informasi tentang setiap inklusi, dan -C -Eflag untuk mendapatkan formulir yang telah diproses. Tentu saja ada banyak flag compiler lain yang mempengaruhi preprocessing (mis. -I /some/dir/Untuk menambahkan /some/dir/untuk mencari file yang disertakan, dan -Duntuk menentukan beberapa makro preprocessor, dll, dll ....).

NB. Versi C ++ di masa depan (mungkin C ++ 20 , bahkan mungkin lebih baru) mungkin memiliki modul C ++ .

Basile Starynkevitch
sumber
1
Jika tautannya membusuk, apakah ini masih merupakan jawaban yang bagus?
Dan Pichelman
4
Saya mengedit jawaban saya untuk memperbaikinya, dan saya dengan hati-hati memilih tautan-ke wikipedia atau ke dokumentasi GNU - yang saya percaya akan tetap relevan untuk waktu yang lama.
Basile Starynkevitch
3

Karena model multi-unit build C ++, Anda memerlukan cara untuk memiliki kode yang muncul di program Anda hanya sekali (definisi), dan Anda perlu cara untuk memiliki kode yang muncul di setiap unit terjemahan program Anda (deklarasi).

Dari sinilah lahir idiom header C ++. Itu konvensi karena suatu alasan.

Anda dapat membuang seluruh program Anda ke dalam satu unit terjemahan, tetapi ini menimbulkan masalah dengan penggunaan kembali kode, pengujian unit dan penanganan ketergantungan antar-modul. Ini juga hanya kekacauan besar.

Lightness Races dengan Monica
sumber
0

Jawaban yang dipilih dari Mengapa kita perlu menulis file header? adalah penjelasan yang masuk akal, tetapi saya ingin menambahkan detail tambahan.

Tampaknya rasional untuk file header cenderung hilang dalam mengajar dan membahas C / C ++.

File header memberikan solusi untuk dua masalah pengembangan aplikasi:

  1. Pemisahan antarmuka dan implementasi
  2. Waktu kompilasi / tautan yang ditingkatkan untuk program besar

C / C ++ dapat menskala dari program kecil ke baris file multi-juta yang sangat besar, multi-ribu file. Pengembangan aplikasi dapat berkembang dari tim yang terdiri dari satu pengembang hingga ratusan pengembang.

Anda dapat memakai beberapa topi sebagai pengembang. Secara khusus, Anda bisa menjadi pengguna antarmuka untuk fungsi dan kelas, atau Anda bisa menjadi penulis antarmuka fungsi dan kelas.

Saat Anda menggunakan fungsi, Anda harus mengetahui antarmuka fungsi, parameter apa yang digunakan, apa fungsi yang dikembalikan dan Anda perlu tahu apa fungsi itu. Ini mudah didokumentasikan dalam file header tanpa harus melihat implementasinya. Kapan Anda sudah membaca implementasi printf? Beli, kami menggunakannya setiap hari.

Saat Anda adalah pengembang antarmuka, topi mengubah arah yang lain. File header memberikan deklarasi antarmuka publik. File header menentukan apa yang dibutuhkan implementasi lain untuk menggunakan antarmuka ini. Informasi internal dan pribadi untuk antarmuka baru ini tidak (dan tidak boleh) dideklarasikan dalam file header. File header publik haruslah semua orang perlu menggunakan modul.

Untuk pengembangan skala besar, kompilasi dan penautan dapat memakan waktu lama. Dari beberapa menit hingga beberapa jam (bahkan hingga beberapa hari!). Membagi perangkat lunak menjadi antarmuka (header) dan implementasi (sumber) menyediakan metode untuk hanya mengkompilasi file yang perlu dikompilasi daripada membangun kembali semuanya.

Selain itu, file header memungkinkan pengembang untuk menyediakan perpustakaan (sudah dikompilasi) serta file header. Pengguna lain perpustakaan mungkin tidak pernah melihat implementasi yang tepat, tetapi masih bisa menggunakan perpustakaan dengan file header. Anda melakukan ini setiap hari dengan pustaka standar C / C ++.

Bahkan jika Anda mengembangkan aplikasi kecil, menggunakan teknik pengembangan perangkat lunak skala besar adalah kebiasaan yang baik. Tetapi kita juga perlu mengingat mengapa kita menggunakan kebiasaan ini.

Bill Door
sumber
0

Anda mungkin juga bertanya mengapa tidak hanya memasukkan semua kode Anda ke dalam satu file.

Jawaban paling sederhana adalah pemeliharaan kode.

Ada kalanya masuk akal untuk membuat kelas:

  • semua diuraikan dalam header
  • semua dalam unit kompilasi dan tanpa header sama sekali.

Waktu yang masuk akal untuk benar-benar sejajar dalam header adalah ketika kelas benar-benar sebuah data struct dengan beberapa pengambil dan setter dasar dan mungkin sebuah konstruktor yang mengambil nilai untuk menginisialisasi anggotanya.

(Templat, yang harus semuanya diuraikan, merupakan masalah yang sedikit berbeda).

Waktu lain untuk membuat kelas semua dalam header adalah ketika Anda mungkin menggunakan kelas dalam beberapa proyek dan secara khusus ingin menghindari tautan di perpustakaan.

Saat Anda dapat menyertakan seluruh kelas dalam unit kompilasi dan sama sekali tidak memaparkan headernya adalah:

  • Kelas "impl" yang hanya digunakan oleh kelas yang mengimplementasikannya. Ini adalah detail implementasi dari kelas itu, dan secara eksternal tidak digunakan.

  • Implementasi kelas dasar abstrak yang dibuat oleh semacam metode pabrik yang mengembalikan pointer / referensi / smart pointer) ke kelas dasar. Metode pabrik akan diekspos oleh kelas itu sendiri tidak akan. (Selain itu jika kelas memiliki instance yang mendaftar sendiri di atas meja melalui instance statis, itu bahkan tidak perlu diekspos melalui pabrik).

  • Tipe kelas "functor".

Dengan kata lain, di mana Anda tidak ingin orang lain memasukkan header.

Saya tahu apa yang mungkin Anda pikirkan ... Jika itu hanya untuk pemeliharaan, dengan memasukkan file cpp (atau header yang benar-benar inline), Anda dapat dengan mudah mengedit file untuk "menemukan" kode dan hanya membangun kembali.

Namun "pemeliharaan" tidak hanya memiliki kode yang terlihat rapi. Ini adalah masalah dampak perubahan. Secara umum diketahui bahwa jika Anda menjaga header tidak berubah dan hanya mengubah (.cpp)file implementasi , itu tidak akan diperlukan untuk membangun kembali sumber lain karena seharusnya tidak ada efek samping.

Ini membuatnya "lebih aman" untuk membuat perubahan seperti itu tanpa khawatir tentang efek knock-on dan itulah yang sebenarnya dimaksud dengan "perawatan".

Uang tunai
sumber
-1

Contoh Snowman perlu sedikit ekstensi untuk menunjukkan dengan tepat mengapa file .h diperlukan.

Tambahkan Bar kelas lain ke dalam permainan yang juga tergantung pada kelas Foo.

Foo.h -> berisi deklarasi untuk kelas Foo

Foo.cpp -> berisi definisi (implementasi) dari kelas Foo

Main.cpp -> menggunakan variabel dari tipe Foo.

Bar.cpp -> menggunakan variabel tipe Foo juga.

Sekarang semua file cpp perlu menyertakan Foo.h Akan menjadi kesalahan untuk menyertakan Foo.cpp di lebih dari satu file cpp lainnya. Linker akan gagal karena kelas Foo akan didefinisikan lebih dari sekali.

SkaarjFace
sumber
1
Bukan itu juga. Itu bisa dipecahkan dengan cara yang sama persis dengan dipecahkan dalam .hfile - dengan menyertakan penjaga dan masalah yang sama persis dengan yang Anda miliki dengan .hfile. Ini bukan untuk apa .hfile sama sekali.
Benjamin Gruenbaum
Saya tidak yakin bagaimana Anda menggunakan sertakan penjaga karena mereka hanya bekerja per file (seperti preprocessod). Dalam hal ini karena Main.cpp dan Bar.cpp adalah file yang berbeda Foo.cpp akan dimasukkan hanya sekali dalam keduanya (dijaga atau tidak tidak ada bedanya). Main.cpp dan Bar.cpp akan dikompilasi. Tetapi kesalahan tautan masih terjadi karena definisi ganda dari kelas Foo. Namun ada juga warisan yang bahkan tidak saya sebutkan. Deklarasi untuk modul eksternal (dll.) Dll.
SkaarjFace
Tidak, itu akan menjadi satu unit kompilasi, tidak ada tautan yang terjadi sama sekali sejak Anda memasukkan .cppfile.
Benjamin Gruenbaum
Saya tidak mengatakan itu tidak akan dikompilasi. Tapi itu tidak akan menghubungkan kedua main.o dan bar.o di executable yang sama. Tanpa menghubungkan apa gunanya kompilasi sama sekali.
SkaarjFace
Uhh ... menjalankan kode? Anda masih dapat menautkannya tetapi tidak menautkan file satu sama lain - melainkan mereka akan menjadi unit kompilasi yang sama.
Benjamin Gruenbaum
-2

Jika Anda menulis seluruh kode dalam file yang sama, maka itu akan membuat kode Anda jelek.
Kedua, Anda tidak akan dapat membagikan kelas tertulis Anda dengan orang lain. Menurut rekayasa perangkat lunak Anda harus menulis kode klien secara terpisah. Klien seharusnya tidak tahu bagaimana program Anda bekerja. Mereka hanya perlu keluaran Jika Anda menulis seluruh program dalam file yang sama itu akan membocorkan keamanan program Anda.

Talha Junaid
sumber