เหตุใด C ++ จึงมีไฟล์ส่วนหัวและไฟล์. cpp
เหตุใด C ++ จึงมีไฟล์ส่วนหัวและไฟล์. cpp
คำตอบ:
เหตุผลหลักก็คือเพื่อแยกอินเทอร์เฟซจากการใช้งาน ส่วนหัวจะประกาศว่า "อะไร" คลาส (หรือสิ่งที่กำลังถูกนำไปใช้) จะทำในขณะที่ไฟล์ cpp กำหนด "วิธี" มันจะทำหน้าที่คุณสมบัติเหล่านั้น
สิ่งนี้จะลดการพึ่งพาเพื่อให้โค้ดที่ใช้ส่วนหัวไม่จำเป็นต้องทราบรายละเอียดทั้งหมดของการใช้งานและคลาส / ส่วนหัวอื่น ๆ ที่จำเป็นสำหรับสิ่งนั้นเท่านั้น สิ่งนี้จะลดเวลาการคอมไพล์และปริมาณการคอมไพล์ซ้ำที่จำเป็นเมื่อมีบางสิ่งในการใช้งานเปลี่ยนแปลงไป
มันไม่สมบูรณ์แบบและคุณมักจะหันไปใช้เทคนิคต่าง ๆ เช่นPimpl Idiomเพื่อแยกอินเทอร์เฟซและการนำไปใช้อย่างเหมาะสม แต่เป็นการเริ่มต้นที่ดี
การคอมไพล์ใน C ++ ดำเนินการใน 2 ช่วงหลัก:
ประการแรกคือการรวบรวมไฟล์ข้อความ "แหล่งที่มา" ลงในไฟล์ "วัตถุ" ไบนารี: ไฟล์ CPP เป็นไฟล์ที่รวบรวมและรวบรวมโดยไม่มีความรู้เกี่ยวกับไฟล์ CPP อื่น ๆ (หรือแม้กระทั่งห้องสมุด) เว้นแต่ได้รับอาหารผ่านการประกาศแบบดิบหรือ รวมส่วนหัว ไฟล์ CPP มักจะถูกคอมไพล์เป็นไฟล์. OBJ หรือ. O "วัตถุ"
ที่สองคือการเชื่อมโยงกันของไฟล์ "วัตถุ" ทั้งหมดและทำให้การสร้างไฟล์ไบนารีสุดท้าย (ทั้งห้องสมุดหรือปฏิบัติการ)
HPP เหมาะสมกับกระบวนการนี้ทั้งหมดที่ไหน
การรวบรวมไฟล์ 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 มีการ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
You still have to copy paste the signature from header file to cpp file, don't you?
ไม่จำเป็น ตราบใดที่ CPP "รวมถึง" HPP พรีคอมไพเลอร์จะทำการคัดลอกเนื้อหาของไฟล์ HPP ลงในไฟล์ CPP โดยอัตโนมัติ ฉันปรับปรุงคำตอบเพื่อชี้แจงว่า
While compiling A.cpp, compiler knows the types of arguments and return value of doSomethingElse from the call itself
@Bob: ไม่เลย มันจะรู้เฉพาะประเภทที่ผู้ใช้ให้ซึ่งครึ่งเวลาจะไม่สนใจอ่านค่าที่ส่งคืน จากนั้นการแปลงโดยนัยเกิดขึ้น จากนั้นเมื่อคุณมีรหัส: foo(bar)
คุณไม่สามารถแน่ใจได้ว่าfoo
เป็นฟังก์ชั่น ดังนั้นคอมไพเลอร์จะต้องมีการเข้าถึงข้อมูลในส่วนหัวเพื่อตัดสินใจว่าแหล่งรวบรวมนั้นถูกต้องหรือไม่ ... จากนั้นเมื่อโค้ดถูกคอมไพล์แล้วตัวลิงก์จะลิงก์เชื่อมโยงการทำงานของฟังก์ชันด้วยกัน
Seems, they're just a pretty ugly arbitrary design.
: ถ้าสร้าง C ++ ในปี 2012 แน่นอน แต่จำไว้ว่า C ++ นั้นถูกสร้างขึ้นบน C ในปี 1980 และในเวลานั้นมีข้อ จำกัด ที่แตกต่างกันมากในเวลานั้น (IIRC มันถูกตัดสินใจเพื่อวัตถุประสงค์ในการนำไปใช้เพื่อรักษา linkers เดียวกันมากกว่าของ C)
foo(bar)
คือฟังก์ชั่น - ถ้ามันเป็นตัวชี้? ในความเป็นจริงการพูดถึงการออกแบบที่ไม่ดีฉันโทษ C ไม่ใช่ C ++ ฉันไม่ชอบข้อ จำกัด บางอย่างของ pure C เช่นการมีไฟล์ส่วนหัวหรือมีฟังก์ชั่นคืนค่าหนึ่งค่าเพียงค่าเดียวในขณะที่ใช้อาร์กิวเมนต์หลายตัวในอินพุต ทำไมข้อโต้แย้งหลาย แต่การส่งออกเดี่ยว) :)?
Why can't I be sure, that foo(bar) is a function
foo อาจเป็นประเภทดังนั้นคุณจะมีตัวสร้างคลาสที่เรียกว่า 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 สามารถช่วยบรรเทาปัญหานั้นได้เช่นเดียวกับการส่งผ่านข้อโต้แย้งโดยการอ้างอิง ทีนี้ไวยากรณ์ที่จะเรียกคืนค่าหลายค่าและมันจะคุ้มค่าหรือไม่ที่จะเปลี่ยนภาษา
เนื่องจาก C ซึ่งแนวคิดนี้เกิดขึ้นมีอายุ 30 ปีและย้อนกลับไปจึงเป็นวิธีเดียวที่สามารถทำงานร่วมกันในการเชื่อมโยงโค้ดจากหลาย ๆ ไฟล์เข้าด้วยกัน
วันนี้มันเป็นแฮ็คที่น่าสะพรึงกลัวซึ่งทำลายเวลาในการคอมไพล์ใน C ++ โดยสิ้นเชิงทำให้เกิดการพึ่งพาที่ไม่จำเป็นนับไม่ถ้วน
เพราะใน C ++ รหัสปฏิบัติการสุดท้ายไม่ได้มีข้อมูลสัญลักษณ์ใด ๆ มันเป็นรหัสเครื่องที่บริสุทธิ์มากหรือน้อย
ดังนั้นคุณต้องมีวิธีการอธิบายส่วนต่อประสานของชิ้นส่วนของรหัสที่แยกออกจากตัวรหัส คำอธิบายนี้อยู่ในไฟล์ส่วนหัว
เพราะ C ++ สืบทอดมาจาก C. โชคไม่ดี
เพราะคนที่ออกแบบรูปแบบห้องสมุดไม่ต้องการที่จะ "เสีย" พื้นที่สำหรับข้อมูลที่ไม่ค่อยได้ใช้เช่นมาโครตัวประมวลผลล่วงหน้า C และการประกาศฟังก์ชั่น
เนื่องจากคุณต้องการข้อมูลนั้นเพื่อบอกคอมไพเลอร์ของคุณ "ฟังก์ชั่นนี้สามารถใช้งานได้ในภายหลังเมื่อตัวเชื่อมโยงทำงาน" พวกเขาจึงต้องมีไฟล์ที่สองที่สามารถเก็บข้อมูลที่ใช้ร่วมกันนี้ได้
ภาษาส่วนใหญ่หลังจาก C / C ++ เก็บข้อมูลนี้ในผลลัพธ์ (ตัวอย่างเช่น Java bytecode) หรือพวกเขาไม่ได้ใช้รูปแบบที่คอมไพล์แล้วให้กระจายในรูปแบบของแหล่งข้อมูลเสมอและคอมไพล์ได้ทันที (Python, Perl)
เป็นวิธีการประมวลผลล่วงหน้าของการประกาศอินเตอร์เฟส คุณใส่อินเทอร์เฟซ (การประกาศเมธอด) ลงในไฟล์ส่วนหัวและการนำไปใช้ใน cpp แอปพลิเคชันที่ใช้ห้องสมุดของคุณจำเป็นต้องรู้อินเทอร์เฟซเท่านั้นซึ่งพวกเขาสามารถเข้าถึงผ่าน #include
บ่อยครั้งที่คุณต้องการให้คำจำกัดความของอินเทอร์เฟซโดยไม่ต้องจัดส่งรหัสทั้งหมด ตัวอย่างเช่นหากคุณมีไลบรารีที่ใช้ร่วมกันคุณจะต้องจัดส่งไฟล์ส่วนหัวด้วยซึ่งจะกำหนดฟังก์ชั่นและสัญลักษณ์ทั้งหมดที่ใช้ในไลบรารีที่แชร์ หากไม่มีไฟล์ส่วนหัวคุณจะต้องจัดส่งแหล่งที่มา
ภายในโครงการเดียวมีการใช้ไฟล์ส่วนหัว IMHO เพื่อวัตถุประสงค์อย่างน้อยสองประการ:
การตอบสนองต่อคำตอบ MadKeithV ของ ,
สิ่งนี้จะลดการพึ่งพาเพื่อให้โค้ดที่ใช้ส่วนหัวไม่จำเป็นต้องทราบรายละเอียดทั้งหมดของการใช้งานและคลาส / ส่วนหัวอื่น ๆ ที่จำเป็นสำหรับสิ่งนั้นเท่านั้น สิ่งนี้จะลดเวลาการคอมไพล์และจำนวนการคอมไพล์ซ้ำที่จำเป็นเมื่อมีบางสิ่งในการใช้งานเปลี่ยนแปลงไป
อีกเหตุผลหนึ่งคือส่วนหัวให้รหัสที่ไม่ซ้ำกับแต่ละชั้นเรียน
ดังนั้นหากเรามีสิ่งที่ชอบ
class A {..};
class B : public A {...};
class C {
include A.cpp;
include B.cpp;
.....
};
เราจะมีข้อผิดพลาดเมื่อเราพยายามสร้างโครงการเนื่องจาก A เป็นส่วนหนึ่งของ B โดยมีส่วนหัวเราจะหลีกเลี่ยงอาการปวดหัวชนิดนี้ ...