เหตุใดจึงใช้ #ifndef CLASS_H และ #define CLASS_H ในไฟล์. h แต่ไม่ใช่ใน. cpp


137

ฉันเคยเห็นคนเขียน

class.h

#ifndef CLASS_H
#define CLASS_H

//blah blah blah

#endif

คำถามคือทำไมพวกเขาไม่ทำเช่นนั้นสำหรับไฟล์. cpp ที่มีคำจำกัดความสำหรับฟังก์ชันคลาส?

สมมติว่าผมมี main.cppและรวมถึงmain.cpp ไฟล์ไม่ได้อะไรดังนั้นวิธีการที่ไม่ทราบว่าสิ่งที่อยู่ใน?class.hclass.hincludemain.cppclass.cpp


5
"นำเข้า" อาจไม่ใช่คำที่คุณต้องการใช้ที่นี่ ประกอบด้วย
Kate Gregory

5
ใน C ++ ไม่มีความสัมพันธ์แบบ 1 ต่อ 1 ระหว่างไฟล์และคลาส คุณสามารถใส่คลาสจำนวนมากเป็นไฟล์เดียวได้ตามที่คุณต้องการ (หรือแม้แต่แบ่งหนึ่งคลาสในหลาย ๆ ไฟล์แม้ว่าจะไม่ค่อยมีประโยชน์ก็ตาม) ดังนั้นแมโครที่ควรจะเป็นไม่ได้FILE_H CLASS_H
sbi

1
ดูของฉันรวมถึงคำแนะนำในยาม

คำตอบ:


303

ขั้นแรกเพื่อตอบคำถามแรกของคุณ:

เมื่อคุณเห็นสิ่งนี้ในไฟล์. h :

#ifndef FILE_H
#define FILE_H

/* ... Declarations etc here ... */

#endif

นี่เป็นเทคนิคตัวประมวลผลล่วงหน้าในการป้องกันไฟล์ส่วนหัวรวมอยู่หลายครั้งซึ่งอาจเป็นปัญหาได้จากหลายสาเหตุ ในระหว่างการรวบรวมโครงการของคุณไฟล์. cppแต่ละไฟล์ (โดยปกติ) จะถูกรวบรวม กล่าวง่ายๆว่าแปลว่าคอมไพเลอร์จะนำไฟล์. cppของคุณเปิดไฟล์ใด ๆ#includedเชื่อมต่อพวกเขาทั้งหมดเป็นไฟล์ข้อความขนาดใหญ่หนึ่งไฟล์จากนั้นทำการวิเคราะห์ไวยากรณ์และในที่สุดมันก็จะแปลงเป็นรหัสกลางบางส่วน งานและในที่สุดก็สร้างเอาท์พุทการชุมนุมสำหรับสถาปัตยกรรมเป้าหมาย ด้วยเหตุนี้หากไฟล์นั้นมี#includedหลายครั้งภายใต้. cppเดียวไฟล์คอมไพเลอร์จะต่อท้ายเนื้อหาของไฟล์สองครั้งดังนั้นหากมีคำจำกัดความในไฟล์นั้นคุณจะได้รับข้อผิดพลาดของคอมไพเลอร์ที่บอกคุณว่าคุณได้นิยามตัวแปรใหม่ เมื่อไฟล์ถูกประมวลผลโดยขั้นตอนตัวประมวลผลล่วงหน้าในกระบวนการรวบรวมครั้งแรกที่เนื้อหาของมันถึงสองบรรทัดแรกจะตรวจสอบว่าFILE_Hมีการกำหนดไว้สำหรับตัวประมวลผลล่วงหน้า ถ้าไม่มันจะกำหนดFILE_Hและดำเนินการประมวลผลรหัสระหว่างมันและ#endifคำสั่ง ครั้งต่อไปที่ตัวประมวลผลล่วงหน้าเห็นเนื้อหาของไฟล์การตรวจสอบFILE_Hจะเป็นเท็จดังนั้นมันจะสแกนลงไปที่ไฟล์#endifและดำเนินการต่อหลังจากนั้น สิ่งนี้จะป้องกันข้อผิดพลาดในการกำหนดซ้ำ

และเพื่อแก้ไขข้อกังวลที่สองของคุณ:

ในการเขียนโปรแกรม C ++ ตามหลักปฏิบัติทั่วไปเราแยกการพัฒนาออกเป็นสองประเภทไฟล์ หนึ่งอยู่กับส่วนขยายของ. hและเราเรียกสิ่งนี้ว่า "ไฟล์ส่วนหัว" พวกเขามักจะให้การประกาศของฟังก์ชั่น, ชั้นเรียน, structs, ตัวแปรทั่วโลก, พิมพ์, มาโครการประมวลผลล่วงหน้าและคำจำกัดความ ฯลฯ โดยทั่วไปพวกเขาเพียงแค่ให้ข้อมูลเกี่ยวกับรหัสของคุณ จากนั้นเรามีนามสกุล. cppซึ่งเราเรียกว่า "รหัสไฟล์" สิ่งนี้จะให้คำจำกัดความสำหรับฟังก์ชั่นเหล่านั้นสมาชิกคลาสสมาชิกโครงสร้างใด ๆ ที่ต้องการคำนิยามตัวแปรทั่วโลก ฯลฯ ดังนั้นไฟล์. hประกาศรหัสและไฟล์. cppจะดำเนินการประกาศนั้น ด้วยเหตุนี้เราโดยทั่วไปในระหว่างการคอมไพล์คอมไพล์แต่ละ. cppไฟล์ลงในวัตถุแล้วเชื่อมโยงวัตถุเหล่านั้น (เพราะคุณแทบจะไม่เคยเห็นไฟล์. cppไฟล์ใดไฟล์หนึ่งรวมไฟล์. cppอื่น)

วิธีแก้ไขปัญหา externals เหล่านี้เป็นงานสำหรับ linker เมื่อคอมไพเลอร์ของคุณประมวลผลmain.cppจะได้รับการประกาศสำหรับรหัสในclass.cppโดยรวมclass.h เพียงต้องการทราบว่าฟังก์ชั่นหรือตัวแปรเหล่านี้มีลักษณะอย่างไร (ซึ่งเป็นสิ่งที่ประกาศให้คุณ) ดังนั้นจึงรวบรวมไฟล์ main.cppของคุณเป็นไฟล์ออบเจ็กต์ (เรียกว่าmain.obj ) ในทำนองเดียวกันclass.cppจะถูกรวบรวมเป็นclass.objไฟล์. ในการสร้างไฟล์เรียกทำงานขั้นสุดท้ายตัวลิงก์จะถูกเรียกใช้เพื่อลิงก์ไฟล์อ็อบเจ็กต์ทั้งสองเข้าด้วยกัน สำหรับตัวแปรหรือฟังก์ชั่นภายนอกที่ยังไม่ได้แก้ไขคอมไพเลอร์จะวางต้นขั้วซึ่งการเข้าถึงเกิดขึ้น ตัวเชื่อมโยงจะใช้ต้นขั้วนี้และมองหารหัสหรือตัวแปรในวัตถุไฟล์อื่นที่อยู่ในรายการและหากพบมันจะรวมรหัสจากไฟล์วัตถุสองไฟล์เป็นไฟล์เอาต์พุตและแทนที่ต้นขั้วด้วยตำแหน่งสุดท้ายของฟังก์ชันหรือ ตัวแปร. วิธีนี้รหัสของคุณใน main.cpp สามารถเรียกฟังก์ชั่นและตัวแปรการใช้งานในclass.cppและถ้าหากพวกเขาจะประกาศในclass.h

ฉันหวังว่านี่จะเป็นประโยชน์


ฉันพยายามที่จะเข้าใจ. h และ. cpp ในช่วงสองสามวันที่ผ่านมา คำตอบนี้ช่วยประหยัดเวลาและความสนใจในการเรียนรู้ C ++ เขียนได้ดีมาก ขอบคุณ Justin!
Rajkumar R

คุณอธิบายได้ดีจริงๆ! บางทีคำตอบอาจจะค่อนข้างดีถ้ามันมีรูป
ลามิน

13

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

ไม่จำเป็นต้องมีเจ้าหน้าที่รักษาความปลอดภัยรวมอยู่ในไฟล์ CPP เพราะตามคำจำกัดความเนื้อหาของไฟล์ CPP จะถูกอ่านเพียงครั้งเดียว

คุณดูเหมือนจะตีความยามรวมว่ามีฟังก์ชั่นเดียวกันกับimportคำสั่งในภาษาอื่น ๆ (เช่น Java); อย่างไรก็ตามไม่ใช่อย่างนั้น #includeตัวเองเป็นประมาณเทียบเท่ากับimportในภาษาอื่น ๆ


2
"ภายในไฟล์ CPP เดียวกัน" ควรอ่าน "ภายในหน่วยการแปลเดียวกัน"
Dreamlax

@dreamlax: จุดที่ดี - นั่นคือสิ่งที่ฉันจะเขียน แต่แล้วฉันคิดว่าคนที่ไม่เข้าใจรวมถึงยามจะสับสนโดยคำว่า "หน่วยการแปล" เท่านั้น ฉันจะแก้ไขคำตอบเพื่อเพิ่ม "หน่วยการแปล" ในวงเล็บ - นั่นควรจะดีที่สุดของทั้งสองโลก
Martin B

6

ไม่ - อย่างน้อยในระหว่างขั้นตอนการรวบรวม

การแปลโปรแกรม c ++ จากซอร์สโค้ดเป็นรหัสเครื่องจะดำเนินการในสามขั้นตอน:

  1. การประมวลผลล่วงหน้า - ตัวประมวลผลล่วงหน้าแยกวิเคราะห์ซอร์สโค้ดทั้งหมดสำหรับบรรทัดที่ขึ้นต้นด้วย # และดำเนินการคำสั่ง ในกรณีของคุณเนื้อหาของไฟล์ของคุณจะแทรกอยู่ในสถานที่ของสายclass.h #include "class.hเนื่องจากคุณอาจรวมไฟล์ส่วนหัวไว้หลายแห่ง#ifndefคำสั่งจะหลีกเลี่ยงการประกาศซ้ำ - ข้อผิดพลาดเนื่องจากคำสั่ง preprocessor ไม่ได้กำหนดเพียงครั้งแรกที่มีไฟล์ส่วนหัวรวมอยู่
  2. การรวบรวมการคอมไพ - คอมไพเลอร์จะแปลไฟล์ซอร์สโค้ดที่ถูกประมวลผลล่วงหน้าทั้งหมดเป็นไฟล์อ็อบเจ็กต์ไบนารี
  3. การเชื่อมโยง - ลิงก์ลิงเกอร์ (ชื่อจึง) รวมกันกับไฟล์วัตถุ การอ้างอิงถึงคลาสหรือหนึ่งในวิธีการของคุณ (ซึ่งควรประกาศใน class.h และกำหนดไว้ใน class.cpp) ได้รับการแก้ไขให้เป็นออฟเซ็ตตามลำดับในหนึ่งในอ็อบเจ็กต์ไฟล์ ฉันเขียน 'หนึ่งในไฟล์วัตถุของคุณ' เนื่องจากคลาสของคุณไม่จำเป็นต้องกำหนดในไฟล์ชื่อ class.cpp มันอาจอยู่ในไลบรารีที่เชื่อมโยงกับโครงการของคุณ

โดยสรุปการประกาศสามารถแชร์ผ่านไฟล์ส่วนหัวในขณะที่การแมปของการประกาศไปยังคำจำกัดความทำโดย linker


4

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

เพื่อที่จะใช้สิ่งที่คุณเพียงแค่ต้องรู้ว่ามันประกาศไม่ได้เป็นคำจำกัดความ มีเพียงผู้เชื่อมโยงเท่านั้นที่ต้องรู้คำจำกัดความ

ดังนั้นนี่คือเหตุผลที่คุณจะรวมไฟล์ส่วนหัวในไฟล์ต้นฉบับหนึ่งไฟล์หรือมากกว่า แต่คุณจะไม่รวมไฟล์ต้นฉบับไว้ในไฟล์อื่น

นอกจากนี้คุณหมายถึง#includeและไม่นำเข้า


3

เสร็จสิ้นสำหรับไฟล์ส่วนหัวเพื่อให้เนื้อหาปรากฏเพียงครั้งเดียวในแต่ละไฟล์ต้นฉบับที่ประมวลผลล่วงหน้าแม้ว่าจะมีการรวมไว้มากกว่าหนึ่งครั้งก็ตาม ในครั้งแรกที่มีการรวมไว้สัญลักษณ์CLASS_H(รู้จักกันในชื่อยามรวม ) ยังไม่ได้กำหนดดังนั้นเนื้อหาทั้งหมดของไฟล์จะถูกรวมไว้ การทำเช่นนี้จะกำหนดสัญลักษณ์ดังนั้นหากรวมไว้อีกครั้งเนื้อหาของไฟล์ (ภายใน#ifndef/ #endifบล็อก) จะถูกข้ามไป

ไม่จำเป็นต้องทำสิ่งนี้กับไฟล์ต้นฉบับตั้งแต่ (ปกติ) ซึ่งไม่รวมอยู่ในไฟล์อื่น ๆ

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


2

main.cppไม่ต้องรู้ว่าสิ่งที่อยู่ในclass.cpp มันก็มีที่จะรู้ว่าการประกาศของการทำงาน / เรียนว่ามันจะไปได้ในการใช้งานและการประกาศเหล่านี้อยู่ในclass.h

ตัวเชื่อมโยงลิงก์ระหว่างสถานที่ที่ใช้ฟังก์ชั่น / คลาสที่ประกาศในclass.hและการใช้งานในclass.cpp


1

.cppไม่รวมไฟล์ (โดยใช้#include) ลงในไฟล์อื่น ดังนั้นพวกเขาไม่จำเป็นต้องมีการป้องกัน Main.cppจะทราบชื่อและลายเซ็นต์ของคลาสที่คุณได้ใช้งานclass.cppเพียงเพราะคุณได้ระบุทั้งหมดในclass.h- นี่คือจุดประสงค์ของไฟล์ส่วนหัว (ขึ้นอยู่กับคุณที่จะต้องตรวจสอบให้แน่ใจว่าclass.hได้อธิบายรหัสที่คุณใช้อย่างถูกต้องclass.cppแล้ว) รหัสที่class.cppสามารถใช้งานได้นั้นจะมีให้สำหรับรหัสที่สามารถใช้งานได้ซึ่งmain.cppต้องขอบคุณความพยายามของตัวเชื่อมโยง


1

โดยทั่วไปแล้วคาดว่าโมดูลของรหัสเช่น.cppไฟล์จะถูกรวบรวมหนึ่งครั้งและเชื่อมโยงไปยังในหลายโครงการเพื่อหลีกเลี่ยงการรวบรวมตรรกะซ้ำโดยไม่จำเป็น ยกตัวอย่างเช่นg++ -o class.cppจะผลิตซึ่งคุณสามารถเชื่อมโยงจากหลายโครงการที่จะใช้class.og++ main.cpp class.o

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

ไฟล์ส่วนหัวยังคงต้องรวมอยู่ในแต่ละโครงการหลายโครงการอย่างไรก็ตามเนื่องจากเป็นส่วนต่อประสานสำหรับแต่ละโมดูล คอมไพเลอร์จะไม่ทราบเกี่ยวกับสัญลักษณ์ใด ๆ ที่แนะนำโดย.oไฟล์หากไม่มีส่วนหัวเหล่านี้

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


0

มันเป็นเพราะ Headerfiles กำหนดสิ่งที่ชั้นมี (สมาชิกโครงสร้างข้อมูล) และไฟล์ cpp ใช้มัน

และแน่นอนเหตุผลหลักสำหรับเรื่องนี้คือคุณสามารถรวมหนึ่งไฟล์. h หลายครั้งในไฟล์. h อื่น ๆ แต่สิ่งนี้จะส่งผลให้มีหลายคำจำกัดความของคลาสซึ่งไม่ถูกต้อง

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