เหตุใดจึงมีไฟล์ส่วนหัวและไฟล์. cpp [ปิด]


484

เหตุใด C ++ จึงมีไฟล์ส่วนหัวและไฟล์. cpp


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

มันเป็นกระบวนทัศน์ OOP ทั่วไป. h คือการประกาศคลาสและ cpp เป็นคำจำกัดความหนึ่งไม่จำเป็นต้องรู้ว่ามันถูกนำไปใช้อย่างไรเขา / เธอควรจะรู้ว่าอินเตอร์เฟซ
Manish Kakati

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

มีหลายครั้งที่ไฟล์ส่วนหัวมีความจำเป็นสำหรับการรวบรวม - ไม่ใช่เพียงแค่การตั้งค่าขององค์กรหรือวิธีการกระจายไลบรารีที่รวบรวมไว้ล่วงหน้า สมมติว่าคุณมีโครงสร้างที่ game.c ขึ้นอยู่กับ BOTH physics.c และ math.c Physics.c ยังขึ้นอยู่กับคณิตศาสตร์ หากคุณรวมไฟล์. c และลืมไฟล์. h ไปตลอดกาลคุณจะต้องมีการประกาศซ้ำกันจาก math.c และไม่ต้องการรวบรวม นี่คือสิ่งที่สมเหตุสมผลที่สุดสำหรับฉันว่าทำไมไฟล์ส่วนหัวจึงมีความสำคัญ หวังว่ามันจะช่วยคนอื่น
Samy Bencherif

ฉันคิดว่ามันเกี่ยวข้องกับข้อเท็จจริงที่ว่าอนุญาตให้ใช้ตัวอักษรและตัวเลขในส่วนขยายได้เท่านั้น ฉันไม่รู้ด้วยซ้ำว่าเป็นจริงแค่เดา
user12211554

คำตอบ:


201

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

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

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


178
ไม่จริงเลย ส่วนหัวยังคงมีส่วนสำคัญของการใช้งาน ตั้งแต่เมื่อใดที่ตัวแปรอินสแตนซ์ส่วนตัวเป็นส่วนหนึ่งของอินเทอร์เฟซของคลาส? ฟังก์ชั่นสมาชิกส่วนตัว? ถ้าอย่างนั้นพวกเขากำลังทำอะไรในหัวข้อที่เปิดเผยต่อสาธารณะ และมันก็แยกออกจากกันด้วยเทมเพลต
jalf

13
นั่นเป็นเหตุผลที่ฉันบอกว่ามันไม่สมบูรณ์แบบและสำนวน Pimpl นั้นจำเป็นสำหรับการแยกจากกันมากขึ้น เทมเพลตเป็นเวิร์มที่แตกต่างกันโดยสิ้นเชิง - แม้ว่าคำสำคัญ "ส่งออก" ได้รับการสนับสนุนอย่างเต็มที่ในคอมไพเลอร์ส่วนใหญ่ก็ยังคงฉันน้ำตาล syntactic มากกว่าการแยกจริง
Joris Timmermans

4
ภาษาอื่นจัดการเรื่องนี้อย่างไร เช่น - Java? ไม่มีแนวคิดของไฟล์ส่วนหัวใน Java
Lazer

8
@Lazer: Java ง่ายต่อการแยกวิเคราะห์ คอมไพเลอร์ Java สามารถแยกไฟล์โดยไม่ต้องรู้คลาสทั้งหมดในไฟล์อื่นและตรวจสอบประเภทภายหลัง ใน C ++ ล็อตของการสร้างนั้นคลุมเครือโดยไม่มีข้อมูลประเภทดังนั้นคอมไพเลอร์ C ++ ต้องการข้อมูลเกี่ยวกับประเภทอ้างอิงเพื่อแยกไฟล์ นั่นเป็นเหตุผลว่าทำไมจึงต้องมีส่วนหัว
Niki

15
@nikie: การแยกวิเคราะห์ "ความสะดวก" เกี่ยวข้องกับอะไร? หาก Java มีไวยากรณ์ที่ซับซ้อนอย่างน้อยเท่ากับ C ++ มันก็ยังสามารถใช้ไฟล์ java ได้ ในทั้งสองกรณีแล้ว C ล่ะ? C แยกวิเคราะห์ได้ง่าย แต่ใช้ทั้งส่วนหัวและไฟล์ c
Thomas Eding

609

การรวบรวม C ++

การคอมไพล์ใน C ++ ดำเนินการใน 2 ช่วงหลัก:

  1. ประการแรกคือการรวบรวมไฟล์ข้อความ "แหล่งที่มา" ลงในไฟล์ "วัตถุ" ไบนารี: ไฟล์ CPP เป็นไฟล์ที่รวบรวมและรวบรวมโดยไม่มีความรู้เกี่ยวกับไฟล์ CPP อื่น ๆ (หรือแม้กระทั่งห้องสมุด) เว้นแต่ได้รับอาหารผ่านการประกาศแบบดิบหรือ รวมส่วนหัว ไฟล์ CPP มักจะถูกคอมไพล์เป็นไฟล์. OBJ หรือ. O "วัตถุ"

  2. ที่สองคือการเชื่อมโยงกันของไฟล์ "วัตถุ" ทั้งหมดและทำให้การสร้างไฟล์ไบนารีสุดท้าย (ทั้งห้องสมุดหรือปฏิบัติการ)

HPP เหมาะสมกับกระบวนการนี้ทั้งหมดที่ไหน

ไฟล์ CPP ที่อ้างว้างไม่ดี ...

การรวบรวมไฟล์ CPP แต่ละไฟล์นั้นไม่ขึ้นอยู่กับไฟล์ CPP อื่น ๆ ทั้งหมดซึ่งหมายความว่าหาก A.CPP ต้องการสัญลักษณ์ที่กำหนดไว้ใน B.CPP เช่น:

// A.CPP
void doSomething()
{
   doSomethingElse(); // Defined in B.CPP
}

// B.CPP
void doSomethingElse()
{
   // Etc.
}

มันจะไม่รวบรวมเพราะ A.CPP ไม่มีทางรู้ว่า "doSomethingElse" มีอยู่ ... เว้นแต่จะมีการประกาศใน A.CPP เช่น:

// A.CPP
void doSomethingElse() ; // From B.CPP

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

จากนั้นหากคุณมี C.CPP ซึ่งใช้สัญลักษณ์เดียวกันคุณก็สามารถคัดลอก / วางการประกาศ ...

แจ้งเตือนการคัดลอก / วาง!

ใช่มีปัญหา การทำสำเนา / วางเป็นสิ่งที่อันตรายและยากต่อการบำรุงรักษา ซึ่งหมายความว่ามันจะเจ๋งถ้าเรามีวิธีที่จะไม่คัดลอก / วางและยังคงประกาศสัญลักษณ์ ... เราจะทำอย่างไร? โดยการรวมไฟล์ข้อความบางไฟล์ซึ่งโดยทั่วไปจะต่อท้ายด้วย. h, .hxx, .h ++ หรือที่ฉันต้องการสำหรับไฟล์ C ++, .hpp:

// B.HPP (here, we decided to declare every symbol defined in B.CPP)
void doSomethingElse() ;

// A.CPP
#include "B.HPP"

void doSomething()
{
   doSomethingElse() ; // Defined in B.CPP
}

// B.CPP
#include "B.HPP"

void doSomethingElse()
{
   // Etc.
}

// C.CPP
#include "B.HPP"

void doSomethingAgain()
{
   doSomethingElse() ; // Defined in B.CPP
}

วิธีการincludeทำงานหรือไม่

การรวมไฟล์จะแยกวิเคราะห์แล้วคัดลอกวางเนื้อหาในไฟล์ CPP

ตัวอย่างเช่นในรหัสต่อไปนี้โดยมีส่วนหัว A.HPP:

// A.HPP
void someFunction();
void someOtherFunction();

... แหล่งที่มา B.CPP:

// B.CPP
#include "A.HPP"

void doSomething()
{
   // Etc.
}

... จะกลายเป็นหลังจากรวม:

// B.CPP
void someFunction();
void someOtherFunction();

void doSomething()
{
   // Etc.
}

สิ่งเล็ก ๆ น้อยหนึ่ง - ทำไมต้องรวม B.HPP ใน B.CPP ด้วย

ในกรณีปัจจุบันนี้ไม่จำเป็นและ B.HPP มีการdoSomethingElseประกาศฟังก์ชันและ B.CPP มีการdoSomethingElseกำหนดฟังก์ชัน (ซึ่งก็คือการประกาศด้วยตัวเอง) แต่ในกรณีทั่วไปที่ใช้ B.HPP สำหรับการประกาศ (และรหัสแบบอินไลน์) อาจไม่มีคำจำกัดความที่สอดคล้องกัน (ตัวอย่างเช่น enums, Structs ธรรมดา ฯลฯ ) ดังนั้นจึงจำเป็นต้องมีการรวมถ้า B.CPP ใช้การประกาศเหล่านั้นจาก B.HPP โดยรวมแล้วมันเป็น "รสนิยมที่ดี" สำหรับแหล่งที่มาที่จะรวมไว้ตามค่าเริ่มต้นของส่วนหัว

ข้อสรุป

ไฟล์ส่วนหัวจึงมีความจำเป็นเนื่องจากคอมไพเลอร์ C ++ ไม่สามารถค้นหาการประกาศสัญลักษณ์เพียงอย่างเดียวได้และคุณต้องช่วยด้วยโดยรวมถึงการประกาศเหล่านั้น

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

#ifndef B_HPP_
#define B_HPP_

// The declarations in the B.hpp file

#endif // B_HPP_

หรือง่ายกว่า

#pragma once

// The declarations in the B.hpp file

2
@nimcap:: You still have to copy paste the signature from header file to cpp file, don't you?ไม่จำเป็น ตราบใดที่ CPP "รวมถึง" HPP พรีคอมไพเลอร์จะทำการคัดลอกเนื้อหาของไฟล์ HPP ลงในไฟล์ CPP โดยอัตโนมัติ ฉันปรับปรุงคำตอบเพื่อชี้แจงว่า
paercebal

7
While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself@Bob: ไม่เลย มันจะรู้เฉพาะประเภทที่ผู้ใช้ให้ซึ่งครึ่งเวลาจะไม่สนใจอ่านค่าที่ส่งคืน จากนั้นการแปลงโดยนัยเกิดขึ้น จากนั้นเมื่อคุณมีรหัส: foo(bar)คุณไม่สามารถแน่ใจได้ว่าfooเป็นฟังก์ชั่น ดังนั้นคอมไพเลอร์จะต้องมีการเข้าถึงข้อมูลในส่วนหัวเพื่อตัดสินใจว่าแหล่งรวบรวมนั้นถูกต้องหรือไม่ ... จากนั้นเมื่อโค้ดถูกคอมไพล์แล้วตัวลิงก์จะลิงก์เชื่อมโยงการทำงานของฟังก์ชันด้วยกัน
paercebal

3
@Bob: [ดำเนินการต่อ] ... ตอนนี้ตัวเชื่อมโยงสามารถทำงานได้โดยคอมไพเลอร์ฉันเดาซึ่งจะทำให้ตัวเลือกของคุณเป็นไปได้ (ฉันเดาว่านี่เป็นหัวข้อเรื่อง "โมดูล" สำหรับมาตรฐานถัดไป) Seems, they're just a pretty ugly arbitrary design.: ถ้าสร้าง C ++ ในปี 2012 แน่นอน แต่จำไว้ว่า C ++ นั้นถูกสร้างขึ้นบน C ในปี 1980 และในเวลานั้นมีข้อ จำกัด ที่แตกต่างกันมากในเวลานั้น (IIRC มันถูกตัดสินใจเพื่อวัตถุประสงค์ในการนำไปใช้เพื่อรักษา linkers เดียวกันมากกว่าของ C)
paercebal

1
@ paercebal ขอบคุณสำหรับคำอธิบายและบันทึก paercebal! ทำไมฉันถึงไม่แน่ใจนั่นfoo(bar)คือฟังก์ชั่น - ถ้ามันเป็นตัวชี้? ในความเป็นจริงการพูดถึงการออกแบบที่ไม่ดีฉันโทษ C ไม่ใช่ C ++ ฉันไม่ชอบข้อ จำกัด บางอย่างของ pure C เช่นการมีไฟล์ส่วนหัวหรือมีฟังก์ชั่นคืนค่าหนึ่งค่าเพียงค่าเดียวในขณะที่ใช้อาร์กิวเมนต์หลายตัวในอินพุต ทำไมข้อโต้แย้งหลาย แต่การส่งออกเดี่ยว) :)?
บอริส Burkov

1
@Bobo: Why can't I be sure, that foo(bar) is a functionfoo อาจเป็นประเภทดังนั้นคุณจะมีตัวสร้างคลาสที่เรียกว่า In fact, speaking of bad design, I blame C, not C++: ฉันสามารถตำหนิ C สำหรับสิ่งต่างๆมากมาย แต่การได้รับการออกแบบในยุค 70 จะไม่เป็นหนึ่งในนั้น อีกครั้งข้อ จำกัด ของเวลานั้น ... such as having header files or having functions return one and only one value: Tuples สามารถช่วยบรรเทาปัญหานั้นได้เช่นเดียวกับการส่งผ่านข้อโต้แย้งโดยการอ้างอิง ทีนี้ไวยากรณ์ที่จะเรียกคืนค่าหลายค่าและมันจะคุ้มค่าหรือไม่ที่จะเปลี่ยนภาษา
paercebal

93

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

วันนี้มันเป็นแฮ็คที่น่าสะพรึงกลัวซึ่งทำลายเวลาในการคอมไพล์ใน C ++ โดยสิ้นเชิงทำให้เกิดการพึ่งพาที่ไม่จำเป็นนับไม่ถ้วน


3
ฉันสงสัยว่าทำไมไฟล์ส่วนหัว (หรืออะไรก็ตามที่จำเป็นสำหรับการรวบรวม / เชื่อมโยง) ไม่ใช่ "สร้างโดยอัตโนมัติ"?
Mateen Ulhaq

54

เพราะใน C ++ รหัสปฏิบัติการสุดท้ายไม่ได้มีข้อมูลสัญลักษณ์ใด ๆ มันเป็นรหัสเครื่องที่บริสุทธิ์มากหรือน้อย

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


16

เพราะ C ++ สืบทอดมาจาก C. โชคไม่ดี


4
เหตุใดการสืบทอด C ++ จาก C จึงไม่เป็นที่น่าเสียดาย
Lokesh

3
@Lokesh เพราะสัมภาระ :(

1
นี่เป็นคำตอบได้อย่างไร
Shuvo Sarker

14
@ShuvoSarker เนื่องจากภาษานับพันได้แสดงให้เห็นแล้วไม่มีคำอธิบายทางเทคนิคสำหรับ C ++ ที่ทำให้โปรแกรมเมอร์เขียนลายเซ็นฟังก์ชั่นสองครั้ง คำตอบของ "ทำไม" คือ "ประวัติ"
Boris

15

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

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

ภาษาส่วนใหญ่หลังจาก C / C ++ เก็บข้อมูลนี้ในผลลัพธ์ (ตัวอย่างเช่น Java bytecode) หรือพวกเขาไม่ได้ใช้รูปแบบที่คอมไพล์แล้วให้กระจายในรูปแบบของแหล่งข้อมูลเสมอและคอมไพล์ได้ทันที (Python, Perl)


จะไม่ทำงานอ้างอิงแบบวนรอบ Ieyou ไม่สามารถสร้าง a.lib จาก a.cpp ก่อนที่จะสร้าง b.lib จาก b.cpp แต่คุณไม่สามารถสร้าง b.lib ก่อน a.lib ได้เช่นกัน
MSalters

20
ภาษาจาวาแก้ปัญหาได้แล้ว Python สามารถทำได้ภาษาใดก็ได้ที่สามารถทำได้ แต่ในเวลาที่ C ถูกคิดค้น RAM มีราคาแพงและหายากมันไม่ใช่ตัวเลือก
Aaron Digulla

6

เป็นวิธีการประมวลผลล่วงหน้าของการประกาศอินเตอร์เฟส คุณใส่อินเทอร์เฟซ (การประกาศเมธอด) ลงในไฟล์ส่วนหัวและการนำไปใช้ใน cpp แอปพลิเคชันที่ใช้ห้องสมุดของคุณจำเป็นต้องรู้อินเทอร์เฟซเท่านั้นซึ่งพวกเขาสามารถเข้าถึงผ่าน #include


4

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

ภายในโครงการเดียวมีการใช้ไฟล์ส่วนหัว IMHO เพื่อวัตถุประสงค์อย่างน้อยสองประการ:

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

3
เหตุใดผู้ขายห้องสมุดจึงไม่สามารถส่งไฟล์ "ส่วนหัว" ที่สร้างขึ้นมาได้ ไฟล์ "ส่วนหัว" ที่ไม่มีตัวประมวลผลล่วงหน้าควรให้ประสิทธิภาพที่ดีกว่ามาก
Tom Hawtin - tackline

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

-5

การตอบสนองต่อคำตอบ MadKeithV ของ ,

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

อีกเหตุผลหนึ่งคือส่วนหัวให้รหัสที่ไม่ซ้ำกับแต่ละชั้นเรียน

ดังนั้นหากเรามีสิ่งที่ชอบ

class A {..};
class B : public A {...};

class C {
    include A.cpp;
    include B.cpp;
    .....
};

เราจะมีข้อผิดพลาดเมื่อเราพยายามสร้างโครงการเนื่องจาก A เป็นส่วนหนึ่งของ B โดยมีส่วนหัวเราจะหลีกเลี่ยงอาการปวดหัวชนิดนี้ ...

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