สิ่งที่ควรไปเป็นไฟล์. h


97

เมื่อแบ่งรหัสของคุณออกเป็นหลาย ๆ ไฟล์สิ่งที่ควรจะเป็นไฟล์. h และสิ่งที่ควรจะเป็นไฟล์. cpp?


1
คำถามที่เกี่ยวข้อง: stackoverflow.com/questions/333889/…
Spoike

7
นี่เป็นปัญหารูปแบบที่แท้จริง แต่ฉันเชื่อว่าการประกาศ C ++ ไปใน.hppไฟล์ในขณะที่การประกาศ C เข้าสู่.hไฟล์ สิ่งนี้มีประโยชน์มากเมื่อผสมโค้ด C และ C ++ (เช่นโมดูลเดิมใน C)
Thomas Matthews

@ThomasMatthews เข้าท่า การปฏิบัตินั้นใช้บ่อยหรือไม่?
TY

@lightningleaf: ใช่การฝึกฝนมักใช้โดยเฉพาะอย่างยิ่งเมื่อผสมภาษา C ++ และ C
Thomas Matthews

คำตอบ:


119

ไฟล์ส่วนหัว ( .h) ได้รับการออกแบบมาเพื่อให้ข้อมูลที่จำเป็นในหลายไฟล์ สิ่งต่างๆเช่นการประกาศคลาสต้นแบบฟังก์ชันและการแจงนับมักจะอยู่ในไฟล์ส่วนหัว ในคำว่า "คำจำกัดความ"

ไฟล์โค้ด ( .cpp) ได้รับการออกแบบมาเพื่อให้ข้อมูลการใช้งานที่จำเป็นต้องทราบในไฟล์เดียวเท่านั้น โดยทั่วไปแล้วหน่วยงานของฟังก์ชันและตัวแปรภายในที่โมดูลอื่นควร / จะไม่เข้าถึงเป็นสิ่งที่อยู่ใน.cppไฟล์ ในคำว่า "การใช้งาน"

คำถามที่ง่ายที่สุดที่จะถามตัวเองว่าอะไรอยู่ที่ไหน "ถ้าฉันเปลี่ยนสิ่งนี้ฉันจะต้องเปลี่ยนรหัสในไฟล์อื่นเพื่อให้รวบรวมสิ่งต่างๆอีกครั้งหรือไม่" ถ้าคำตอบคือ "ใช่" อาจอยู่ในไฟล์ส่วนหัว หากคำตอบคือ "ไม่" อาจเป็นเพราะอยู่ในไฟล์โค้ด


4
ยกเว้นข้อมูลคลาสส่วนตัวจะต้องเข้าไปในส่วนหัว เทมเพลตต้องกำหนดส่วนหัวอย่างสมบูรณ์ (เว้นแต่คุณจะใช้หนึ่งในคอมไพเลอร์เพียงไม่กี่ตัวที่รองรับexport) วิธีเดียวที่ # 1 คือ PIMPL # 2 จะเป็นไปได้หากexportได้รับการสนับสนุนและอาจเป็นไปได้โดยใช้ c ++ 0x และexternเทมเพลต IMO ไฟล์ส่วนหัวใน c ++ สูญเสียประโยชน์ไปมาก
KitsuneYMG

26
ทุกอย่างดี แต่มีคำศัพท์ที่ไม่ถูกต้อง ในคำหนึ่งคำว่า "declarations" - คำว่า "definition" มีความหมายเหมือนกันกับ "การใช้งาน" เฉพาะโค้ดประกาศโค้ดอินไลน์ข้อกำหนดมาโครและโค้ดเทมเพลตเท่านั้นที่ควรอยู่ในส่วนหัว กล่าวคือไม่มีอะไรที่สร้างอินสแตนซ์โค้ดหรือข้อมูล
Clifford

8
ฉันต้องเห็นด้วยกับ Clifford คุณใช้การประกาศข้อกำหนดและคำจำกัดความค่อนข้างหลวมและสลับกันได้บ้าง แต่มีความหมายที่แม่นยำใน C ++ ตัวอย่าง: การประกาศคลาสแนะนำชื่อคลาส แต่ไม่ได้บอกว่ามีอะไรอยู่ในนั้น คำจำกัดความของคลาสจะแสดงรายการสมาชิกและฟังก์ชันเพื่อนทั้งหมด ทั้งสองอย่างสามารถใส่ลงในไฟล์ส่วนหัวได้โดยไม่มีปัญหา สิ่งที่คุณเรียกว่า "ฟังก์ชั่นต้นแบบ" เป็นฟังก์ชั่นการประกาศ แต่นิยามฟังก์ชันคือสิ่งที่มีโค้ดของฟังก์ชันและควรวางไว้ในไฟล์ cpp - เว้นแต่จะเป็นแบบอินไลน์หรือ (บางส่วน) เทมเพลต
sellibitze

5
มีความหมายที่แม่นยำในภาษา C ++ ไม่มีความหมายที่แน่นอนในภาษาอังกฤษ คำตอบของฉันถูกเขียนขึ้นในช่วงหลัง
Amber

57

ความจริงก็คือใน C ++ สิ่งนี้ค่อนข้างซับซ้อนกว่าที่องค์กรส่วนหัว / แหล่งที่มาของ C

คอมไพเลอร์มองเห็นอะไร?

คอมไพเลอร์มองเห็นไฟล์ซอร์สขนาดใหญ่ (.cpp) หนึ่งไฟล์ที่มีส่วนหัวรวมอยู่อย่างถูกต้อง ซอร์สไฟล์คือหน่วยคอมไพล์ที่จะถูกคอมไพล์เป็นอ็อบเจ็กต์ไฟล์

เหตุใดส่วนหัวจึงจำเป็น?

เนื่องจากหน่วยคอมไพล์หนึ่งหน่วยอาจต้องการข้อมูลเกี่ยวกับการนำไปใช้งานในหน่วยคอมไพล์อื่น ดังนั้นเราสามารถเขียนตัวอย่างการนำฟังก์ชันไปใช้ในแหล่งเดียวและเขียนการประกาศฟังก์ชันนี้ในแหล่งอื่นที่จำเป็นต้องใช้

ในกรณีนี้มีสำเนาข้อมูลเดียวกันสองชุด ซึ่งชั่วร้าย ...

วิธีแก้ปัญหาคือการแบ่งปันรายละเอียดบางอย่าง ในขณะที่การนำไปใช้งานควรจะยังคงอยู่ใน Source การประกาศสัญลักษณ์ที่ใช้ร่วมกันเช่นฟังก์ชันหรือคำจำกัดความของโครงสร้างคลาส enums ฯลฯ อาจจำเป็นต้องแชร์

ส่วนหัวใช้เพื่อใส่รายละเอียดที่แชร์เหล่านั้น

ย้ายไปที่ส่วนหัวของการประกาศสิ่งที่ต้องใช้ร่วมกันระหว่างแหล่งที่มาหลายแหล่ง

ไม่มีอะไรมาก?

ใน C ++ มีสิ่งอื่น ๆ ที่สามารถใส่ไว้ในส่วนหัวได้เนื่องจากต้องการแชร์ด้วยเช่นกัน:

  • รหัสอินไลน์
  • เทมเพลต
  • ค่าคงที่ (โดยปกติจะเป็นค่าที่คุณต้องการใช้ภายในสวิตช์ ... )

ย้ายไปที่ส่วนหัวทุกสิ่งที่ต้องแชร์รวมถึงการใช้งานร่วมกัน

หมายความว่าอาจมีแหล่งที่มาภายในส่วนหัวหรือไม่?

ใช่. อันที่จริงมีหลายสิ่งหลายอย่างที่อาจอยู่ใน "ส่วนหัว" (เช่นใช้ร่วมกันระหว่างแหล่งที่มา)

  • ประกาศไปข้างหน้า
  • การประกาศ / คำจำกัดความของฟังก์ชัน / โครงสร้าง / คลาส / เทมเพลต
  • การใช้โค้ดแบบอินไลน์และเทมเพลต

กลายเป็นเรื่องซับซ้อนและในบางกรณี (การอ้างอิงแบบวงกลมระหว่างสัญลักษณ์) ไม่สามารถเก็บไว้ในส่วนหัวเดียวได้

ส่วนหัวสามารถแบ่งออกเป็นสามส่วน

ซึ่งหมายความว่าในกรณีที่รุนแรงคุณสามารถมี:

  • ส่วนหัวของการประกาศไปข้างหน้า
  • ส่วนหัวคำประกาศ / คำจำกัดความ
  • ส่วนหัวการใช้งาน
  • แหล่งที่มาของการนำไปใช้งาน

ลองจินตนาการว่าเรามี MyObject ที่เป็นเทมเพลต เราสามารถมี:

// - - - - MyObject_forward.hpp - - - - 
// This header is included by the code which need to know MyObject
// does exist, but nothing more.
template<typename T>
class MyObject ;

.

// - - - - MyObject_declaration.hpp - - - - 
// This header is included by the code which need to know how
// MyObject is defined, but nothing more.
#include <MyObject_forward.hpp>

template<typename T>
class MyObject
{
   public :
      MyObject() ;
   // Etc.
} ;

void doSomething() ;

.

// - - - - MyObject_implementation.hpp - - - - 
// This header is included by the code which need to see
// the implementation of the methods/functions of MyObject,
// but nothing more.
#include <MyObject_declaration.hpp>

template<typename T>
MyObject<T>::MyObject()
{
   doSomething() ;
}

// etc.

.

// - - - - MyObject_source.cpp - - - - 
// This source will have implementation that does not need to
// be shared, which, for templated code, usually means nothing...
#include <MyObject_implementation.hpp>

void doSomething()
{
   // etc.
} ;

// etc.

ว้าว!

ใน "ชีวิตจริง" มักจะซับซ้อนน้อยกว่า โค้ดส่วนใหญ่จะมีเฉพาะส่วนหัว / แหล่งที่มาที่เรียบง่ายโดยมีโค้ดอินไลน์บางส่วนในซอร์ส

แต่ในกรณีอื่น ๆ (อ็อบเจ็กต์ที่เทมเพลตซึ่งรู้จักกัน) ฉันต้องมีสำหรับแต่ละอ็อบเจ็กต์การประกาศและส่วนหัวการนำไปใช้งานแยกกันโดยมีซอร์สที่ว่างเปล่ารวมถึงส่วนหัวเหล่านั้นเพื่อช่วยให้ฉันเห็นข้อผิดพลาดในการคอมไพล์

อีกเหตุผลหนึ่งในการแบ่งส่วนหัวออกเป็นส่วนหัวที่แยกจากกันอาจเป็นการเร่งความเร็วในการรวบรวม จำกัด ปริมาณของสัญลักษณ์ที่แยกวิเคราะห์ตามความจำเป็นที่เข้มงวดและหลีกเลี่ยงการคอมไพล์ซ้ำโดยไม่จำเป็นของแหล่งที่มาซึ่งใส่ใจเฉพาะการประกาศไปข้างหน้าเมื่อการปรับใช้วิธีการแบบอินไลน์เปลี่ยนไป

สรุป

คุณควรทำให้การจัดระเบียบรหัสของคุณง่ายที่สุดเท่าที่จะเป็นไปได้และแบบแยกส่วนมากที่สุด ใส่ไฟล์ต้นฉบับให้มากที่สุด แสดงเฉพาะในส่วนหัวเท่านั้นสิ่งที่ต้องแชร์

แต่วันที่คุณมีการพึ่งพาแบบวงกลมระหว่างอ็อบเจ็กต์เทมเพลตอย่าแปลกใจถ้าองค์กรโค้ดของคุณค่อนข้าง "น่าสนใจ" มากกว่าที่องค์กรส่วนหัว / แหล่งที่มาธรรมดา

^ _ ^


19

นอกเหนือจากคำตอบอื่น ๆ ทั้งหมดฉันจะบอกคุณว่าสิ่งที่คุณไม่ได้วางไว้ในไฟล์ส่วนหัว:
usingการประกาศ (สิ่งที่พบบ่อยที่สุดusing namespace std;) ไม่ควรปรากฏในไฟล์ส่วนหัวเนื่องจากจะทำให้เนมสเปซของไฟล์ต้นทางที่รวมอยู่ .


+1 โดยมีข้อแม้ที่คุณสามารถทำได้โดยใช้ตราบเท่าที่มันอยู่ในเนมสเปซรายละเอียดบางอย่าง (หรือเนมสเปซที่ไม่ระบุตัวตน) แต่ใช่อย่าใช้usingเพื่อนำสิ่งต่างๆเข้าสู่เนมสเปซส่วนกลางในส่วนหัว
KitsuneYMG

+1 อันนี้ตอบง่ายกว่าเยอะ :) นอกจากนี้ไฟล์ส่วนหัวไม่ควรมีเนมสเปซที่ไม่ระบุชื่อ
sellibitze

เป็นเรื่องปกติที่ไฟล์ส่วนหัวจะมีเนมสเปซที่ไม่ระบุตัวตนตราบใดที่คุณเข้าใจความหมายนั่นคือหน่วยการแปลแต่ละหน่วยจะมีสำเนาของสิ่งที่คุณกำหนดเนมสเปซต่างกัน แนะนำให้ใช้ฟังก์ชันอินไลน์ในเนมสเปซที่ไม่ระบุชื่อใน C ++ สำหรับกรณีที่คุณใช้static inlineใน C99 เนื่องจากมีบางอย่างเกี่ยวข้องกับสิ่งที่เกิดขึ้นเมื่อคุณรวมการเชื่อมโยงภายในกับเทมเพลต เนมสเปซ Anon ให้คุณ "ซ่อน" ฟังก์ชันในขณะที่รักษาการเชื่อมโยงภายนอก
Steve Jessop

สตีฟสิ่งที่คุณเขียนไม่ได้ทำให้ฉันมั่นใจ โปรดเลือกตัวอย่างที่เป็นรูปธรรมที่คุณคิดว่าเนมสเปซ anon มีความหมายโดยรวมในไฟล์ส่วนหัว
sellibitze

7

สิ่งที่รวบรวมเป็นศูนย์ (zero binary footprint) จะไปอยู่ในไฟล์ส่วนหัว

ตัวแปรไม่ได้รวมเข้ากับอะไรเลย แต่การประกาศประเภททำ (เพราะพวกเขาอธิบายว่าตัวแปรทำงานอย่างไร)

ฟังก์ชันทำไม่ได้ แต่ฟังก์ชันแบบอินไลน์ทำ (หรือมาโคร) เนื่องจากสร้างโค้ดเฉพาะที่เรียกเท่านั้น

เทมเพลตไม่ใช่โค้ดเป็นเพียงสูตรสำหรับสร้างโค้ดเท่านั้น ดังนั้นพวกเขาจึงไปในไฟล์ h ด้วย


1
"ฟังก์ชันอินไลน์ ... สร้างโค้ดเฉพาะที่เรียก" ที่ไม่เป็นความจริง. ฟังก์ชันอินไลน์อาจอินไลน์หรือไม่ก็ได้ในไซต์การโทร แต่แม้ว่าจะมีการอินไลน์ แต่เนื้อหาของฟังก์ชันจริงก็ยังคงมีอยู่เช่นเดียวกับฟังก์ชันที่ไม่ใช่อินไลน์ เหตุผลที่เป็นไปได้ที่จะมีฟังก์ชั่นอินไลน์ในส่วนหัวนั้นไม่เกี่ยวข้องกับว่าพวกเขาสร้างโค้ดหรือไม่เนื่องจากฟังก์ชันแบบอินไลน์ไม่ได้ทริกเกอร์กฎคำจำกัดความเดียวดังนั้นจึงไม่เหมือนกับฟังก์ชันที่ไม่ใช่อินไลน์จึงไม่มีปัญหาในการเชื่อมโยงหน่วยการแปลสองหน่วยเข้าด้วยกัน ซึ่งมีทั้งส่วนหัว
Steve Jessop

4

โดยทั่วไปคุณใส่การประกาศในไฟล์ส่วนหัวและข้อกำหนดในไฟล์การนำไปใช้งาน (.cpp) ข้อยกเว้นคือเทมเพลตซึ่งคำจำกัดความจะต้องอยู่ในส่วนหัวด้วย

คำถามนี้และคำถามที่คล้ายกับคำถามนี้ถูกถามบ่อยใน SO - ดูทำไมต้องมีไฟล์ส่วนหัวและไฟล์. cpp ใน C ++ และไฟล์ส่วนหัว C ++ การแยกโค้ดเช่น


แน่นอนคุณสามารถใส่นิยามคลาสลงในไฟล์ส่วนหัวได้ด้วย พวกเขาไม่จำเป็นต้องเป็นแม่แบบด้วยซ้ำ
sellibitze

2

การประกาศคลาสและฟังก์ชันของคุณรวมถึงเอกสารประกอบและคำจำกัดความสำหรับฟังก์ชัน / วิธีการแบบอินไลน์ (แม้ว่าบางคนชอบที่จะใส่ไว้ในไฟล์. inl แยกต่างหาก)


2

ไฟล์ส่วนหัวส่วนใหญ่มีโครงกระดูกคลาสหรือการประกาศ (ไม่เปลี่ยนแปลงบ่อย)

และไฟล์ cpp มีการใช้งานคลาส (เปลี่ยนแปลงบ่อย)


5
โปรดอย่าใช้คำศัพท์ที่ไม่ได้มาตรฐาน "คลาสโครงกระดูก" คืออะไร "การใช้คลาส" คืออะไร นอกจากนี้สิ่งที่คุณเรียกว่าการประกาศในบริบทของคลาสอาจรวมถึงคำจำกัดความของคลาสด้วย
sellibitze

1

ไฟล์ส่วนหัว (.h) ควรใช้สำหรับการประกาศคลาสโครงสร้างและวิธีการต้นแบบ ฯลฯ การนำอ็อบเจ็กต์เหล่านั้นไปใช้ใน cpp

ใน. h

    class Foo {
    int j;

    Foo();
    Foo(int)
    void DoSomething();
}

1

ฉันคาดหวังว่าจะได้เห็น:

  • ประกาศ
  • ความคิดเห็น
  • คำจำกัดความที่ทำเครื่องหมายในบรรทัด
  • เทมเพลต

คำตอบจริงๆคือสิ่งที่ไม่ควรใส่:

  • คำจำกัดความ (สามารถนำไปสู่สิ่งที่ถูกกำหนดแบบทวีคูณ)
  • การใช้คำประกาศ / คำสั่ง (บังคับให้ทุกคนรวมทั้งส่วนหัวของคุณอาจทำให้เกิดเครื่องหมายชื่อ)

1
คุณสามารถใส่คำจำกัดความของคลาสไว้ในไฟล์ส่วนหัวได้เช่นกัน การประกาศชั้นเรียนไม่ได้พูดอะไรเกี่ยวกับสมาชิก
sellibitze

1

ส่วนหัวกำหนดบางสิ่ง แต่ไม่ได้บอกอะไรเกี่ยวกับการนำไปใช้งาน (ไม่รวมเทมเพลตใน "metafore" นี้

จากที่กล่าวไปคุณต้องแบ่ง "คำจำกัดความ" ออกเป็นกลุ่มย่อยในกรณีนี้คำจำกัดความสองประเภท

  • คุณกำหนด "เค้าโครง" ของโครงสร้างของคุณโดยบอกเฉพาะเท่าที่จำเป็นโดยกลุ่มการใช้งานโดยรอบ
  • คำจำกัดความของตัวแปรฟังก์ชันและคลาส

ตอนนี้ฉันกำลังพูดถึงกลุ่มย่อยแรกแน่นอน

ส่วนหัวมีไว้เพื่อกำหนดเค้าโครงของโครงสร้างของคุณเพื่อช่วยให้ซอฟต์แวร์ที่เหลือใช้การนำไปใช้งานได้ คุณอาจต้องการเห็นว่ามันเป็น "นามธรรม" ของการนำไปใช้งานซึ่งพูดได้อย่างไร้เสียง แต่ฉันคิดว่ามันเหมาะกับกรณีนี้มากทีเดียว

ตามที่ผู้โพสต์ก่อนหน้านี้ได้กล่าวและแสดงให้คุณประกาศพื้นที่การใช้งานส่วนตัวและสาธารณะรวมถึงตัวแปรส่วนตัวและสาธารณะด้วย ตอนนี้ฉันไม่ต้องการออกแบบโค้ดที่นี่ แต่คุณอาจต้องการพิจารณาสิ่งที่คุณใส่ในส่วนหัวของคุณเนื่องจากนั่นคือ Layer ระหว่างผู้ใช้ปลายทางและการนำไปใช้งาน


1
  • ไฟล์ส่วนหัว - ไม่ควรเปลี่ยนระหว่างการพัฒนาบ่อยเกินไป -> คุณควรคิดและเขียนพร้อมกัน (ในกรณีที่ดีที่สุด)
  • ไฟล์ต้นฉบับ - การเปลี่ยนแปลงระหว่างการใช้งาน

นี่คือการปฏิบัติอย่างหนึ่ง สำหรับโครงการขนาดเล็กบางโครงการอาจเป็นหนทางไป แต่คุณอาจลองเลิกใช้ฟังก์ชันและต้นแบบ (ในไฟล์ส่วนหัว) แทนที่จะเปลี่ยนลายเซ็นหรือลบออก อย่างน้อยก็จนกว่าจะเปลี่ยนเบอร์หลัก. เช่นเดียวกับเมื่อ 1.9.2 ถูกกระแทกเป็น 2.0.0 เบต้า
TamusJRoyce

1

ส่วนหัว (.h)

  • มาโครและรวมที่จำเป็นสำหรับอินเทอร์เฟซ (ให้น้อยที่สุด)
  • การประกาศฟังก์ชันและคลาส
  • เอกสารของอินเทอร์เฟซ
  • การประกาศฟังก์ชัน / วิธีการแบบอินไลน์ถ้ามี
  • extern เป็นตัวแปรส่วนกลาง (ถ้ามี)

ร่างกาย (.cpp)

  • มาโครที่เหลือและรวมถึง
  • รวมส่วนหัวของโมดูล
  • ความหมายของฟังก์ชันและวิธีการ
  • ตัวแปรส่วนกลาง (ถ้ามี)

ตามหลักทั่วไปคุณวางส่วนที่ "แชร์" ของโมดูลไว้ที่. h (ส่วนที่โมดูลอื่น ๆ ต้องสามารถมองเห็นได้) และส่วน "ไม่แชร์" บน. cpp

PD: ใช่ฉันได้รวมตัวแปรส่วนกลางแล้ว ฉันเคยใช้มันมาบ้างแล้วและสิ่งสำคัญคืออย่ากำหนดไว้ในส่วนหัวไม่เช่นนั้นคุณจะได้รับโมดูลจำนวนมากโดยแต่ละโมดูลจะกำหนดตัวแปรของตัวเอง

แก้ไข: แก้ไขหลังจากความคิดเห็นของเดวิด


ตามหลักการทั่วไปแล้วมีเพียงไม่กี่รายการที่ควรรวมอยู่ในไฟล์. h และไฟล์. cpp ควรมีส่วนหัวที่ต้องการ ทำให้เวลาในการรวบรวมสั้นลงและไม่ก่อให้เกิดเนมสเปซ
David Thornley
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.