Bjarne Stroustrup’s ภาษาโปรแกรม C ++ มีบทที่ชื่อว่า“ A Tour of C ++: The Basics” - Standard C ++ ในบทที่ 2.2 กล่าวถึงกระบวนการคอมไพล์และการเชื่อมโยงใน C ++ ในครึ่งหน้า การรวบรวมและการเชื่อมโยงเป็นกระบวนการพื้นฐานสองอย่างที่เกิดขึ้นตลอดเวลาในระหว่างการพัฒนาซอฟต์แวร์ C ++ แต่ที่น่าแปลกก็คือพวกเขาไม่เข้าใจนักพัฒนา C ++ จำนวนมาก
เหตุใดซอร์สโค้ด C ++ จึงแบ่งออกเป็นไฟล์ส่วนหัวและไฟล์ต้นฉบับ คอมไพเลอร์มองเห็นแต่ละส่วนเป็นอย่างไร สิ่งนั้นมีผลต่อการคอมไพล์และการเชื่อมโยงอย่างไร? มีคำถามอื่น ๆ อีกมากมายเช่นนี้ที่คุณอาจเคยคิด แต่ยอมรับว่าเป็นอนุสัญญา
ไม่ว่าคุณจะออกแบบแอปพลิเคชัน C ++ ใช้คุณสมบัติใหม่ ๆ พยายามแก้ไขข้อบกพร่อง (โดยเฉพาะข้อบกพร่องแปลก ๆ บางอย่าง) หรือพยายามทำให้โค้ด C และ C ++ ทำงานร่วมกันการรู้ว่าการคอมไพล์และการเชื่อมโยงทำงานอย่างไรจะช่วยให้คุณประหยัดเวลาได้มากและ ทำให้งานเหล่านั้นน่าพอใจยิ่งขึ้น ในบทความนี้คุณจะได้เรียนรู้ว่า
บทความนี้จะอธิบายว่าคอมไพเลอร์ C ++ ทำงานอย่างไรกับโครงสร้างภาษาพื้นฐานบางอย่างตอบคำถามทั่วไปที่เกี่ยวข้องกับกระบวนการและช่วยให้คุณแก้ไขข้อผิดพลาดที่เกี่ยวข้องบางประการที่นักพัฒนามักทำในการพัฒนา C ++
หมายเหตุ: บทความนี้มีตัวอย่างซอร์สโค้ดที่สามารถดาวน์โหลดได้จาก https://bitbucket.org/danielmunoz/cpp-article
ตัวอย่างถูกรวบรวมในเครื่อง CentOS Linux:
$ uname -sr Linux 3.10.0-327.36.3.el7.x86_64
ใช้รุ่น g ++:
$ g++ --version g++ (GCC) 4.8.5 20150623 (Red Hat 4.8.5-11)
ไฟล์ต้นฉบับที่ให้มาควรเป็นแบบพกพาไปยังระบบปฏิบัติการอื่น ๆ แม้ว่า Makefiles ที่มาพร้อมกับพวกเขาสำหรับกระบวนการสร้างอัตโนมัติควรพกพาได้เฉพาะกับระบบที่คล้าย Unix เท่านั้น
ไฟล์ซอร์ส C ++ แต่ละไฟล์ต้องถูกคอมไพล์ลงในอ็อบเจ็กต์ไฟล์ ไฟล์อ็อบเจ็กต์ที่เป็นผลมาจากการคอมไพล์ของไฟล์ต้นฉบับหลายไฟล์จะถูกเชื่อมโยงเข้ากับไฟล์ปฏิบัติการไลบรารีแบบแบ่งใช้หรือไลบรารีแบบคงที่ ไฟล์ต้นฉบับ C ++ โดยทั่วไปมีส่วนต่อท้ายนามสกุล. cpp, .cxx หรือ. cc
ไฟล์ต้นฉบับ C ++ สามารถรวมไฟล์อื่น ๆ ที่เรียกว่าไฟล์ส่วนหัวด้วย #include
คำสั่ง ไฟล์ส่วนหัวมีนามสกุลเช่น. h, .hpp หรือ. hxx หรือไม่มีนามสกุลเลยเหมือนในไลบรารีมาตรฐาน C ++ และไฟล์ส่วนหัวของไลบรารีอื่น ๆ (เช่น Qt) ส่วนขยายไม่สำคัญสำหรับตัวประมวลผลล่วงหน้า C ++ ซึ่งจะแทนที่บรรทัดที่มี #include
คำสั่งที่มีเนื้อหาทั้งหมดของไฟล์ที่รวมอยู่
ขั้นตอนแรกที่คอมไพลเลอร์จะทำกับซอร์สไฟล์คือรันตัวประมวลผลล่วงหน้าบนไฟล์ ไฟล์ต้นทางเท่านั้นที่จะถูกส่งไปยังคอมไพเลอร์ (เพื่อประมวลผลล่วงหน้าและคอมไพล์) ไฟล์ส่วนหัวจะไม่ถูกส่งต่อไปยังคอมไพเลอร์ แต่จะรวมจากไฟล์ต้นฉบับ
ไฟล์ส่วนหัวแต่ละไฟล์สามารถเปิดได้หลายครั้งในระหว่างขั้นตอนก่อนการประมวลผลของไฟล์ต้นฉบับทั้งหมดขึ้นอยู่กับจำนวนไฟล์ต้นฉบับที่รวมไว้หรือจำนวนไฟล์ส่วนหัวอื่น ๆ ที่รวมอยู่ในไฟล์ต้นฉบับด้วย (อาจมีได้หลายระดับ) . ในทางกลับกันไฟล์ซอร์สจะถูกเปิดเพียงครั้งเดียวโดยคอมไพเลอร์ (และตัวประมวลผลล่วงหน้า) เมื่อไฟล์เหล่านั้นถูกส่งไปยังไฟล์นั้น
สำหรับไฟล์ซอร์ส C ++ แต่ละไฟล์ตัวประมวลผลก่อนจะสร้างหน่วยการแปลโดยการแทรกเนื้อหาลงในไฟล์เมื่อพบคำสั่ง #include ในเวลาเดียวกันกับที่ไฟล์นั้นจะลอกโค้ดออกจากไฟล์ต้นฉบับและส่วนหัวเมื่อพบ การรวบรวมตามเงื่อนไข บล็อกที่มีคำสั่งประเมินเป็น false
มันจะทำบางอย่าง งานอื่น ๆ เช่นการแทนที่มาโคร
เมื่อพรีโปรเซสเซอร์เสร็จสิ้นการสร้างหน่วยการแปล (บางครั้งมีขนาดใหญ่) คอมไพเลอร์จะเริ่มเฟสการคอมไพล์และสร้างอ็อบเจ็กต์ไฟล์
webpack รวม node_modules
ในการรับหน่วยการแปลนั้น (ซอร์สโค้ดที่ประมวลผลล่วงหน้า) ให้ใช้ -E
สามารถส่งผ่านไปยังคอมไพเลอร์ g ++ พร้อมกับ -o
ตัวเลือกเพื่อระบุชื่อที่ต้องการของไฟล์ต้นฉบับที่ประมวลผลล่วงหน้า
ใน cpp-article/hello-world
ไดเรกทอรีมีไฟล์ตัวอย่าง“ hello-world.cpp”:
#include int main(int argc, char* argv[]) { std::cout << 'Hello world' << std::endl; return 0; }
สร้างไฟล์ที่ประมวลผลล่วงหน้าโดย:
$ g++ -E hello-world.cpp -o hello-world.ii
และดูจำนวนบรรทัด:
$ wc -l hello-world.ii 17558 hello-world.ii
มี 17,588 เส้นในเครื่องของฉัน คุณยังสามารถเรียกใช้ make
ในไดเรกทอรีนั้นและจะทำตามขั้นตอนเหล่านั้นให้คุณ
เราจะเห็นว่าคอมไพเลอร์ต้องรวบรวมไฟล์ขนาดใหญ่กว่าซอร์สไฟล์ธรรมดาที่เราเห็น นี่เป็นเพราะส่วนหัวที่รวมไว้ และในตัวอย่างของเราเราได้รวมส่วนหัวไว้เพียงส่วนเดียว หน่วยการแปลจะใหญ่ขึ้นเรื่อย ๆ เมื่อเรารวมส่วนหัวไว้ด้วย
กระบวนการก่อนประมวลผลและคอมไพล์นี้คล้ายกับภาษา C เป็นไปตามกฎ C สำหรับการคอมไพล์และวิธีการรวมไฟล์ส่วนหัวและสร้างอ็อบเจ็กต์โค้ดนั้นเกือบจะเหมือนกัน
มาดูไฟล์ใน cpp-article/symbols/c-vs-cpp-names
ไดเรกทอรี
มีซอร์สไฟล์ C (ไม่ใช่ C ++) อย่างง่ายที่ชื่อ sum.c ซึ่งส่งออกสองฟังก์ชันหนึ่งสำหรับการเพิ่มจำนวนเต็มสองตัวและหนึ่งสำหรับการเพิ่มสองโฟลต:
int sumI(int a, int b) { return a + b; } float sumF(float a, float b) { return a + b; }
คอมไพล์ (หรือรัน make
และขั้นตอนทั้งหมดในการสร้างแอพตัวอย่างทั้งสองที่จะเรียกใช้งาน) เพื่อสร้างไฟล์อ็อบเจ็กต์ sum.o:
$ gcc -c sum.c
ตอนนี้ดูสัญลักษณ์ที่ส่งออกและนำเข้าโดยไฟล์วัตถุนี้:
$ nm sum.o 0000000000000014 T sumF 0000000000000000 T sumI
ไม่มีการนำเข้าสัญลักษณ์และสองสัญลักษณ์จะถูกส่งออก: sumF
และ sumI
. สัญลักษณ์เหล่านั้นจะถูกส่งออกเป็นส่วนหนึ่งของเซ็กเมนต์. text (T) ดังนั้นจึงเป็นชื่อฟังก์ชันโค้ดที่เรียกใช้งานได้
หากซอร์สไฟล์อื่น (ทั้ง C หรือ C ++) ต้องการเรียกใช้ฟังก์ชันเหล่านั้นจำเป็นต้องประกาศก่อนเรียกใช้
วิธีมาตรฐานในการทำคือสร้างไฟล์ส่วนหัวที่ประกาศและรวมไว้ในไฟล์ต้นฉบับที่เราต้องการเรียกใช้ ส่วนหัวสามารถมีชื่อและนามสกุลใดก็ได้ ฉันเลือก sum.h
:
#ifdef __cplusplus extern 'C' { #endif int sumI(int a, int b); float sumF(float a, float b); #ifdef __cplusplus } // end extern 'C' #endif
คืออะไร ifdef
/ endif
บล็อกการรวบรวมตามเงื่อนไข? หากฉันรวมส่วนหัวนี้จากซอร์สไฟล์ C ฉันต้องการให้มันกลายเป็น:
int sumI(int a, int b); float sumF(float a, float b);
แต่ถ้าฉันรวมไฟล์เหล่านี้จากซอร์สไฟล์ C ++ ฉันต้องการให้มันกลายเป็น:
extern 'C' { int sumI(int a, int b); float sumF(float a, float b); } // end extern 'C'
ภาษาซีไม่รู้อะไรเกี่ยวกับ extern 'C'
คำสั่ง แต่ C ++ ทำและต้องการคำสั่งนี้ที่ใช้กับการประกาศฟังก์ชัน C นี้เป็นเพราะ C ++ เปลี่ยนชื่อฟังก์ชัน (และวิธีการ) เนื่องจากรองรับฟังก์ชัน / วิธีการมากเกินไปในขณะที่ C ไม่ทำ
สิ่งนี้สามารถเห็นได้ในซอร์สไฟล์ C ++ ชื่อ print.cpp:
#include // std::cout, std::endl #include 'sum.h' // sumI, sumF void printSum(int a, int b) { std::cout << a << ' + ' << b << ' = ' << sumI(a, b) << std::endl; } void printSum(float a, float b) { std::cout << a << ' + ' << b << ' = ' << sumF(a, b) << std::endl; } extern 'C' void printSumInt(int a, int b) { printSum(a, b); } extern 'C' void printSumFloat(float a, float b) { printSum(a, b); }
มีสองฟังก์ชันที่มีชื่อเดียวกัน (printSum
) ซึ่งแตกต่างกันในประเภทของพารามิเตอร์เท่านั้น: int
หรือ float
. การโอเวอร์โหลดฟังก์ชันเป็นคุณสมบัติ C ++ ซึ่งไม่มีอยู่ใน C ในการใช้คุณลักษณะนี้และแยกความแตกต่างของฟังก์ชันเหล่านั้น C ++ จะเปลี่ยนชื่อฟังก์ชันดังที่เราเห็นในชื่อสัญลักษณ์ที่ส่งออก (ฉันจะเลือกเฉพาะสิ่งที่เกี่ยวข้องจากเอาต์พุตของ nm):
$ g++ -c print.cpp $ nm print.o 0000000000000132 T printSumFloat 0000000000000113 T printSumInt U sumF U sumI 0000000000000074 T _Z8printSumff 0000000000000000 T _Z8printSumii U _ZSt4cout
ฟังก์ชันเหล่านั้นจะถูกส่งออก (ในระบบของฉัน) เป็น _Z8printSumff
สำหรับรุ่นลอยและ _Z8printSumii
สำหรับเวอร์ชัน int ชื่อฟังก์ชั่นทุกชื่อใน C ++ ถูกแยกออกเว้นแต่จะประกาศเป็น extern 'C'
มีสองฟังก์ชันที่ประกาศด้วยการเชื่อมโยง C ใน print.cpp
: printSumInt
และ printSumFloat
.
ดังนั้นจึงไม่สามารถใส่ได้มากเกินไปมิฉะนั้นชื่อที่ส่งออกจะเหมือนกันเนื่องจากไม่ได้ถูกทำให้ยุ่งเหยิง ฉันต้องแยกพวกเขาออกจากกันโดยการแก้ไขค่า Int หรือ Float ต่อท้ายชื่อ
เนื่องจากพวกมันไม่ได้ถูกหักงอจึงสามารถเรียกได้จากรหัส C ดังที่เราจะได้เห็นในไม่ช้า
หากต้องการดูชื่อที่สับสนเหมือนที่เราเห็นในซอร์สโค้ด C ++ เราสามารถใช้ -C
(demangle) ในตัวเลือก nm
คำสั่ง อีกครั้งฉันจะคัดลอกเฉพาะส่วนที่เกี่ยวข้องเดียวกันของผลลัพธ์:
$ nm -C print.o 0000000000000132 T printSumFloat 0000000000000113 T printSumInt U sumF U sumI 0000000000000074 T printSum(float, float) 0000000000000000 T printSum(int, int) U std::cout
ด้วยตัวเลือกนี้แทน _Z8printSumff
เราเห็น printSum(float, float)
และแทนที่จะเป็น _ZSt4cout
เราเห็น std :: cout ซึ่งเป็นชื่อที่เป็นมิตรกับมนุษย์มากกว่า
นอกจากนี้เรายังเห็นว่ารหัส C ++ ของเราเรียกรหัส C: print.cpp
กำลังโทรมา sumI
และ sumF
ซึ่งเป็นฟังก์ชัน C ที่ประกาศว่ามีการเชื่อมโยง C ใน sum.h
สิ่งนี้สามารถเห็นได้ในเอาต์พุต nm ของการพิมพ์ด้านบนซึ่งแจ้งให้ทราบถึงสัญลักษณ์ (U) ที่ไม่ได้กำหนดไว้: sumF
, sumI
และ std::cout
. สัญลักษณ์ที่ไม่ได้กำหนดเหล่านั้นควรถูกจัดเตรียมไว้ในอ็อบเจ็กต์ไฟล์ (หรือไลบรารี) อย่างใดอย่างหนึ่งซึ่งจะถูกเชื่อมโยงพร้อมกับเอาต์พุตไฟล์อ็อบเจ็กต์นี้ในเฟสลิงก์
จนถึงตอนนี้เราเพิ่งรวบรวมซอร์สโค้ดเป็นออบเจ็กต์โค้ดเรายังไม่ได้เชื่อมโยง หากเราไม่เชื่อมโยงไฟล์ออบเจ็กต์ที่มีคำจำกัดความสำหรับสัญลักษณ์ที่นำเข้าเหล่านั้นเข้ากับไฟล์ออบเจ็กต์นี้ตัวเชื่อมโยงจะหยุดด้วยข้อผิดพลาด 'สัญลักษณ์ที่ขาดหายไป'
วิธีการคำนวณที่นั่นฉัน
โปรดทราบว่าตั้งแต่ print.cpp
เป็นซอร์สไฟล์ C ++ ที่รวบรวมด้วยคอมไพเลอร์ C ++ (g ++) โค้ดทั้งหมดในนั้นถูกคอมไพล์เป็นโค้ด C ++ ฟังก์ชั่นที่มีการเชื่อมโยง C เช่น printSumInt
และ printSumFloat
ยังเป็นฟังก์ชัน C ++ ที่สามารถใช้คุณสมบัติ C ++ เฉพาะชื่อของสัญลักษณ์เท่านั้นที่เข้ากันได้กับ C แต่รหัสคือ C ++ ซึ่งจะเห็นได้จากการที่ฟังก์ชันทั้งสองเรียกใช้ฟังก์ชันที่โอเวอร์โหลด (printSum
) ซึ่งจะไม่สามารถเกิดขึ้นได้หาก printSumInt
หรือ printSumFloat
ถูกรวบรวมใน C.
มาดูกันตอนนี้ print.hpp
ไฟล์ส่วนหัวที่สามารถรวมได้ทั้งจากไฟล์ต้นฉบับ C หรือ C ++ ซึ่งจะอนุญาตให้ printSumInt
และ printSumFloat
ที่จะเรียกทั้งจาก C และจาก C ++ และ printSum
ที่จะเรียกจาก C ++:
#ifdef __cplusplus void printSum(int a, int b); void printSum(float a, float b); extern 'C' { #endif void printSumInt(int a, int b); void printSumFloat(float a, float b); #ifdef __cplusplus } // end extern 'C' #endif
หากเรารวมมันจากไฟล์ซอร์ส C เราแค่ต้องการดู:
void printSumInt(int a, int b); void printSumFloat(float a, float b);
printSum
ไม่สามารถมองเห็นได้จากรหัส C เนื่องจากชื่อของมันถูกทำให้ยุ่งเหยิงดังนั้นเราจึงไม่มีวิธี (มาตรฐานและแบบพกพา) ในการประกาศสำหรับรหัส C ใช่ฉันสามารถประกาศเป็น:
void _Z8printSumii(int a, int b); void _Z8printSumff(float a, float b);
และตัวเชื่อมโยงจะไม่บ่นเพราะนั่นเป็นชื่อที่แน่นอนที่คอมไพเลอร์ที่ติดตั้งไว้ในปัจจุบันของฉันคิดค้นขึ้นมา แต่ฉันไม่รู้ว่ามันจะใช้ได้กับตัวเชื่อมโยงของคุณหรือไม่ (ถ้าคอมไพเลอร์ของคุณสร้างชื่อที่แตกต่างกัน) หรือแม้แต่สำหรับ เวอร์ชันถัดไปของลิงก์เกอร์ของฉัน ฉันไม่รู้ด้วยซ้ำว่าการโทรจะเป็นไปตามที่คาดไว้หรือไม่เนื่องจากการใช้งานที่แตกต่างกัน เรียกประชุม (วิธีการส่งผ่านพารามิเตอร์และค่าส่งคืน) ที่เฉพาะของคอมไพเลอร์และอาจแตกต่างกันสำหรับการเรียก C และ C ++ (โดยเฉพาะสำหรับฟังก์ชัน C ++ ที่เป็นฟังก์ชันสมาชิกและรับตัวชี้นี้เป็นพารามิเตอร์)
คอมไพเลอร์ของคุณสามารถใช้รูปแบบการเรียกหนึ่งสำหรับฟังก์ชัน C ++ ปกติและอีกแบบหนึ่งหากมีการประกาศว่ามีการเชื่อมโยง 'C' ภายนอก ดังนั้นการโกงคอมไพเลอร์โดยบอกว่าฟังก์ชันหนึ่งใช้รูปแบบการเรียก C ในขณะที่ใช้ C ++ จริง ๆ เพื่อให้ได้ผลลัพธ์ที่ไม่คาดคิดหากอนุสัญญาที่ใช้สำหรับแต่ละข้อแตกต่างกันในการคอมไพล์ toolchain ของคุณ
มี วิธีมาตรฐานในการผสม C และ C ++ รหัสและวิธีมาตรฐานในการเรียกใช้ฟังก์ชันที่โอเวอร์โหลด C ++ จาก C คือถึง ห่อไว้ในฟังก์ชันด้วยการเชื่อมโยง C อย่างที่เราทำโดยการห่อ printSum
ด้วย printSumInt
และ printSumFloat
.
ถ้าเรารวม print.hpp
จากไฟล์ต้นฉบับ C ++, __cplusplus
มาโครตัวประมวลผลล่วงหน้าจะถูกกำหนดและไฟล์จะถูกมองว่า:
void printSum(int a, int b); void printSum(float a, float b); extern 'C' { void printSumInt(int a, int b); void printSumFloat(float a, float b); } // end extern 'C'
สิ่งนี้จะช่วยให้รหัส C ++ เรียกใช้ฟังก์ชันที่โอเวอร์โหลด printSum หรือ wrappers printSumInt
และ printSumFloat
.
ตอนนี้เรามาสร้างซอร์สไฟล์ C ที่มีฟังก์ชันหลักซึ่งเป็นจุดเริ่มต้นสำหรับโปรแกรม ฟังก์ชันหลัก C นี้จะเรียก printSumInt
และ printSumFloat
นั่นคือจะเรียกใช้ฟังก์ชัน C ++ ทั้งสองด้วยการเชื่อมโยง C จำไว้ว่าฟังก์ชันเหล่านี้คือฟังก์ชัน C ++ (ส่วนของฟังก์ชันเรียกใช้โค้ด C ++) ที่ไม่มีชื่อที่ยุ่งเหยิงของ C ++ เท่านั้น ไฟล์ชื่อ c-main.c
:
#include 'print.hpp' int main(int argc, char* argv[]) { printSumInt(1, 2); printSumFloat(1.5f, 2.5f); return 0; }
คอมไพล์เพื่อสร้างไฟล์อ็อบเจ็กต์:
$ gcc -c c-main.c
และดูสัญลักษณ์ที่นำเข้า / ส่งออก:
$ nm c-main.o 0000000000000000 T main U printSumFloat U printSumInt
ส่งออกหลักและนำเข้า printSumFloat
และ printSumInt
ตามที่คาดไว้
ในการเชื่อมโยงทั้งหมดเข้าด้วยกันเป็นไฟล์ปฏิบัติการเราจำเป็นต้องใช้ C ++ linker (g ++) เนื่องจากอย่างน้อยหนึ่งไฟล์ที่เราจะเชื่อมโยง print.o
ถูกคอมไพล์ใน C ++:
$ g++ -o c-app sum.o print.o c-main.o
การดำเนินการสร้างผลลัพธ์ที่คาดหวัง:
$ ./c-app 1 + 2 = 3 1.5 + 2.5 = 4
ตอนนี้เรามาลองใช้ไฟล์หลัก C ++ ชื่อ cpp-main.cpp
:
#include 'print.hpp' int main(int argc, char* argv[]) { printSum(1, 2); printSum(1.5f, 2.5f); printSumInt(3, 4); printSumFloat(3.5f, 4.5f); return 0; }
รวบรวมและดูสัญลักษณ์ที่นำเข้า / ส่งออกของ cpp-main.o
ไฟล์ออบเจ็กต์:
$ g++ -c cpp-main.cpp $ nm -C cpp-main.o 0000000000000000 T main U printSumFloat U printSumInt U printSum(float, float) U printSum(int, int)
ส่งออกหลักและนำเข้าการเชื่อมโยง C printSumFloat
และ printSumInt
และ printSum
.
คุณอาจสงสัยว่าเหตุใดสัญลักษณ์หลักจึงไม่ส่งออกเป็นสัญลักษณ์ที่แหลกเหลวเช่น main(int, char**)
จากซอร์ส C ++ นี้เนื่องจากเป็นซอร์สไฟล์ C ++ และไม่ได้กำหนดเป็น extern 'C'
อืม main
คือ ฟังก์ชันที่กำหนดการใช้งานพิเศษ และดูเหมือนว่าการใช้งานของฉันจะเลือกใช้การเชื่อมโยง C ไม่ว่าจะกำหนดไว้ในไฟล์ต้นฉบับ C หรือ C ++ ก็ตาม
การเชื่อมโยงและเรียกใช้โปรแกรมให้ผลลัพธ์ที่คาดหวัง:
$ g++ -o cpp-app sum.o print.o cpp-main.o $ ./cpp-app 1 + 2 = 3 1.5 + 2.5 = 4 3 + 4 = 7 3.5 + 4.5 = 8
จนถึงตอนนี้ฉันระมัดระวังที่จะไม่รวมส่วนหัวของฉันสองครั้งไม่ว่าจะโดยตรงหรือโดยอ้อมจากไฟล์ต้นฉบับเดียวกัน แต่เนื่องจากส่วนหัวหนึ่งสามารถรวมส่วนหัวอื่นได้ส่วนหัวเดียวกันจึงสามารถรวมโดยอ้อมได้หลายครั้ง และเนื่องจากเนื้อหาส่วนหัวถูกแทรกในตำแหน่งที่รวมไว้เท่านั้นจึงง่ายต่อการลงท้ายด้วยการประกาศซ้ำ
เป็นcfo เป็นเจ้าหน้าที่ของบริษัท
ดูไฟล์ตัวอย่างใน cpp-article/header-guards
// unguarded.hpp class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; // guarded.hpp: #ifndef __GUARDED_HPP #define __GUARDED_HPP class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; #endif // __GUARDED_HPP
ความแตกต่างคือใน guarded.hpp เราล้อมรอบส่วนหัวทั้งหมดในเงื่อนไขที่จะรวมไว้ก็ต่อเมื่อ __GUARDED_HPP
ไม่ได้กำหนดมาโครตัวประมวลผลล่วงหน้า ในครั้งแรกที่ตัวประมวลผลก่อนรวมไฟล์นี้จะไม่มีการกำหนด แต่เนื่องจากมาโครถูกกำหนดไว้ภายในรหัสที่มีการป้องกันนั้นในครั้งต่อไปที่รวมไว้ (จากไฟล์ต้นฉบับเดียวกันไม่ว่าโดยตรงหรือโดยอ้อม) ตัวประมวลผลก่อนจะเห็นเส้นระหว่าง #ifndef และ #endif และจะละทิ้งโค้ดทั้งหมดระหว่าง พวกเขา
โปรดทราบว่ากระบวนการนี้เกิดขึ้นกับไฟล์ต้นฉบับทุกไฟล์ที่เรารวบรวม หมายความว่าไฟล์ส่วนหัวนี้สามารถรวมได้ครั้งเดียวและครั้งเดียวสำหรับไฟล์ต้นฉบับแต่ละไฟล์ ความจริงที่ว่าไฟล์นั้นรวมอยู่ในไฟล์ต้นฉบับเดียวจะไม่ป้องกันไม่ให้รวมไฟล์นั้นจากไฟล์ต้นฉบับอื่นเมื่อมีการรวบรวมไฟล์ต้นฉบับนั้น เพียงแค่ป้องกันไม่ให้รวมมากกว่าหนึ่งครั้งจากไฟล์ต้นฉบับเดียวกัน
ไฟล์ตัวอย่าง main-guarded.cpp
รวม guarded.hpp
สองครั้ง:
#include 'guarded.hpp' #include 'guarded.hpp' int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }
แต่เอาต์พุตที่ประมวลผลล่วงหน้าจะแสดงคำจำกัดความของคลาสเท่านั้น A
:
$ g++ -E main-guarded.cpp # 1 'main-guarded.cpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'main-guarded.cpp' # 1 'guarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 2 'main-guarded.cpp' 2 int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }
ดังนั้นจึงสามารถรวบรวมได้โดยไม่มีปัญหา:
$ g++ -o guarded main-guarded.cpp
แต่ main-unguarded.cpp
ไฟล์รวม unguarded.hpp
สองครั้ง:
#include 'unguarded.hpp' #include 'unguarded.hpp' int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }
และเอาต์พุตที่ประมวลผลล่วงหน้าแสดงคำจำกัดความของคลาส A สองคำ:
$ g++ -E main-unguarded.cpp # 1 'main-unguarded.cpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'main-unguarded.cpp' # 1 'unguarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 2 'main-unguarded.cpp' 2 # 1 'unguarded.hpp' 1 class A { public: A(int a) : m_a(a) {} void setA(int a) { m_a = a; } int getA() const { return m_a; } private: int m_a; }; # 3 'main-unguarded.cpp' 2 int main(int argc, char* argv[]) { A a(5); a.setA(0); return a.getA(); }
สิ่งนี้จะทำให้เกิดปัญหาเมื่อคอมไพล์:
$ g++ -o unguarded main-unguarded.cpp
รวมไฟล์จาก main-unguarded.cpp:2:0
:
unguarded.hpp:1:7: error: redefinition of 'class A' class A { ^ In file included from main-unguarded.cpp:1:0: unguarded.hpp:1:7: error: previous definition of 'class A' class A { ^
เพื่อความสั้นฉันจะไม่ใช้ส่วนหัวที่มีการป้องกันในบทความนี้หากไม่จำเป็นเนื่องจากส่วนใหญ่เป็นตัวอย่างสั้น ๆ แต่ปกป้องไฟล์ส่วนหัวของคุณเสมอ ไม่ใช่ไฟล์ต้นฉบับของคุณซึ่งจะไม่รวมจากที่ใดก็ได้ แค่ไฟล์ส่วนหัว
ดูที่ by-value.cpp
ไฟล์ใน cpp-article/symbols/pass-by
:
#include #include #include // std::vector, std::accumulate, std::cout, std::endl using namespace std; int sum(int a, const int b) { cout << 'sum(int, const int)' << endl; const int c = a + b; ++a; // Possible, not const // ++b; // Not possible, this would result in a compilation error return c; } float sum(const float a, float b) { cout << 'sum(const float, float)' << endl; return a + b; } int sum(vector v) { cout << 'sum(vector)' << endl; return accumulate(v.begin(), v.end(), 0); } float sum(const vector v) { cout << 'sum(const vector)' << endl; return accumulate(v.begin(), v.end(), 0.0f); }
ตั้งแต่ฉันใช้ using namespace std
คำสั่งฉันไม่จำเป็นต้องตรวจสอบชื่อของสัญลักษณ์ (ฟังก์ชันหรือคลาส) ภายในเนมสเปซมาตรฐานในหน่วยการแปลที่เหลือซึ่งในกรณีของฉันคือส่วนที่เหลือของไฟล์ต้นฉบับ หากนี่เป็นไฟล์ส่วนหัวฉันไม่ควรแทรกคำสั่งนี้เพราะควรรวมไฟล์ส่วนหัวจากไฟล์ต้นฉบับหลายไฟล์ คำสั่งนี้จะนำไปสู่ขอบเขตส่วนกลางของไฟล์ซอร์สแต่ละไฟล์ทั้งเนมสเปซมาตรฐานจากจุดที่รวมส่วนหัวของฉัน
แม้แต่ส่วนหัวที่รวมอยู่หลังการขุดในไฟล์เหล่านั้นก็จะมีสัญลักษณ์เหล่านั้นอยู่ในขอบเขต สิ่งนี้สามารถทำให้เกิดการปะทะกันของชื่อเนื่องจากพวกเขาไม่คาดคิดว่าจะเกิดขึ้น ดังนั้นอย่าใช้คำสั่งนี้ในส่วนหัว ใช้เฉพาะในไฟล์ต้นฉบับหากคุณต้องการและหลังจากที่คุณรวมส่วนหัวทั้งหมดแล้วเท่านั้น
สังเกตว่าพารามิเตอร์บางตัวเป็นค่าคงที่อย่างไร ซึ่งหมายความว่าไม่สามารถเปลี่ยนแปลงได้ในเนื้อหาของฟังก์ชันหากเราพยายาม มีข้อผิดพลาดในการคอมไพล์ นอกจากนี้โปรดทราบว่าพารามิเตอร์ทั้งหมดในไฟล์ต้นฉบับนี้ถูกส่งผ่านด้วยค่าไม่ใช่โดยการอ้างอิง (&) หรือโดยตัวชี้ (*) ซึ่งหมายความว่าผู้โทรจะทำสำเนาและส่งต่อไปยังฟังก์ชัน ดังนั้นจึงไม่สำคัญสำหรับผู้โทรว่าพวกเขาเป็น const หรือไม่เพราะถ้าเราแก้ไขในส่วนของฟังก์ชันเราจะแก้ไขเฉพาะสำเนาเท่านั้นไม่ใช่ค่าดั้งเดิมที่ผู้โทรส่งไปยังฟังก์ชัน
เนื่องจากความเสถียรของพารามิเตอร์ที่ส่งผ่านโดยค่า (สำเนา) ไม่สำคัญสำหรับผู้เรียกจึงไม่ยุ่งเหยิงในลายเซ็นฟังก์ชันดังที่เห็นได้หลังจากรวบรวมและตรวจสอบรหัสวัตถุ (เฉพาะเอาต์พุตที่เกี่ยวข้อง):
$ g++ -c by-value.cpp $ nm -C by-value.o 000000000000001e T sum(float, float) 0000000000000000 T sum(int, int) 0000000000000087 T sum(std::vector) 0000000000000048 T sum(std::vector )
ลายเซ็นไม่ได้ระบุว่าพารามิเตอร์ที่คัดลอกเป็น const หรือไม่ในเนื้อความของฟังก์ชัน มันไม่สำคัญ สิ่งนี้มีความสำคัญสำหรับนิยามฟังก์ชันเท่านั้นเพื่อแสดงให้ผู้อ่านเข้าใจเนื้อหาของฟังก์ชันได้อย่างรวดเร็วว่าค่าเหล่านั้นจะเปลี่ยนแปลงไปหรือไม่ ในตัวอย่างมีเพียงครึ่งหนึ่งของพารามิเตอร์เท่านั้นที่ประกาศเป็น const ดังนั้นเราจึงสามารถเห็นความเปรียบต่างได้ แต่ถ้าเราต้องการเป็น const ถูกต้อง ทุกคนควรได้รับการประกาศเนื่องจากไม่มีการแก้ไขใด ๆ ในเนื้อความของฟังก์ชัน (และไม่ควร)
เนื่องจากไม่สำคัญสำหรับการประกาศฟังก์ชันซึ่งเป็นสิ่งที่ผู้โทรเห็นเราจึงสามารถสร้าง by-value.hpp
ส่วนหัวดังนี้:
#include int sum(int a, int b); float sum(float a, float b); int sum(std::vector v); int sum(std::vector v);
อนุญาตให้เพิ่มคุณสมบัติ const ที่นี่ (คุณสามารถกำหนดเป็นตัวแปร const ที่ไม่ได้ const ในคำจำกัดความได้ด้วยซ้ำและจะใช้งานได้) แต่ไม่จำเป็นและจะทำให้การประกาศเป็นคำอธิบายโดยไม่จำเป็นเท่านั้น
มาดูกัน by-reference.cpp
:
#include #include #include using namespace std; int sum(const int& a, int& b) { cout << 'sum(const int&, int&)' << endl; const int c = a + b; ++b; // Will modify caller variable // ++a; // Not allowed, but would also modify caller variable return c; } float sum(float& a, const float& b) { cout << 'sum(float&, const float&)' << endl; return a + b; } int sum(const std::vector& v) { cout << 'sum(const std::vector&)' << endl; return accumulate(v.begin(), v.end(), 0); } float sum(const std::vector& v) { cout << 'sum(const std::vector&)' << endl; return accumulate(v.begin(), v.end(), 0.0f); }
ความมั่นคงเมื่อผ่านการอ้างอิงมีความสำคัญสำหรับผู้โทรเพราะจะบอกผู้โทรว่าจะแก้ไขข้อโต้แย้งหรือไม่โดยผู้เรียก ดังนั้นสัญลักษณ์จะถูกส่งออกพร้อมกับความมั่นคง:
$ g++ -c by-reference.cpp $ nm -C by-reference.o 0000000000000051 T sum(float&, float const&) 0000000000000000 T sum(int const&, int&) 00000000000000fe T sum(std::vector const&) 00000000000000a3 T sum(std::vector const&)
สิ่งนี้ควรแสดงในส่วนหัวที่ผู้โทรจะใช้:
#include int sum(const int&, int&); float sum(float&, const float&); int sum(const std::vector&); float sum(const std::vector&);
โปรดทราบว่าฉันไม่ได้เขียนชื่อของตัวแปรในการประกาศ (ในส่วนหัว) อย่างที่ทำอยู่ นอกจากนี้ยังถูกกฎหมายสำหรับตัวอย่างนี้และสำหรับตัวอย่างก่อนหน้านี้ ไม่จำเป็นต้องใช้ชื่อตัวแปรในการประกาศเนื่องจากผู้เรียกไม่จำเป็นต้องรู้ว่าคุณต้องการตั้งชื่อตัวแปรของคุณอย่างไร แต่โดยทั่วไปชื่อพารามิเตอร์เป็นที่ต้องการในการประกาศเพื่อให้ผู้ใช้สามารถทราบได้อย่างรวดเร็วว่าพารามิเตอร์แต่ละตัวหมายถึงอะไรและจะส่งอะไรในการโทร
ข้อใดต่อไปนี้ไม่ใช่ 1 ใน 5 ปัจจัยการแข่งขันที่มีผลกระทบต่อขายส่งเท่านั้น
น่าแปลกที่ชื่อตัวแปรไม่จำเป็นในนิยามของฟังก์ชัน จำเป็นต่อเมื่อคุณใช้พารามิเตอร์ในฟังก์ชันจริงๆ แต่ถ้าคุณไม่เคยใช้คุณสามารถปล่อยพารามิเตอร์เป็นประเภท แต่ไม่มีชื่อ เหตุใดฟังก์ชันจึงประกาศพารามิเตอร์ที่ไม่เคยใช้ บางครั้งฟังก์ชัน (หรือวิธีการ) เป็นเพียงส่วนหนึ่งของอินเทอร์เฟซเช่นอินเทอร์เฟซการเรียกกลับซึ่งกำหนดพารามิเตอร์บางอย่างที่ส่งผ่านไปยังผู้สังเกตการณ์ ผู้สังเกตการณ์ต้องสร้างการเรียกกลับด้วยพารามิเตอร์ทั้งหมดที่อินเทอร์เฟซระบุเนื่องจากผู้เรียกจะส่งพารามิเตอร์ทั้งหมด แต่ผู้สังเกตการณ์อาจไม่สนใจสิ่งเหล่านี้ทั้งหมดดังนั้นแทนที่จะได้รับคำเตือนจากคอมไพเลอร์เกี่ยวกับ 'พารามิเตอร์ที่ไม่ได้ใช้' คำจำกัดความของฟังก์ชันสามารถปล่อยไว้โดยไม่มีชื่อ
// by-pointer.cpp: #include #include #include using namespace std; int sum(int const * a, int const * const b) { cout << 'sum(int const *, int const * const)' << endl; const int c = *a+ *b; // *a = 4; // Can't change. The value pointed to is const. // *b = 4; // Can't change. The value pointed to is const. a = b; // I can make a point to another const int // b = a; // Can't change where b points because the pointer itself is const. return c; } float sum(float * const a, float * b) { cout << 'sum(int const * const, float const *)' << endl; return *a + *b; } int sum(const std::vector* v) { cout << 'sum(std::vector const *)' begin(), v->end(), 0); v = NULL; // I can make v point to somewhere else return c; } float sum(const std::vector * const v) { cout << 'sum(std::vector const * const)' begin(), v->end(), 0.0f); }
ในการประกาศตัวชี้ไปยังองค์ประกอบ const (int ในตัวอย่าง) คุณสามารถประกาศประเภทเป็นอย่างใดอย่างหนึ่ง:
int const * const int *
หากคุณต้องการให้ตัวชี้เป็น const ด้วยนั่นคือไม่สามารถเปลี่ยนพอยน์เตอร์ให้ชี้ไปที่อย่างอื่นได้คุณต้องเพิ่ม const หลังดาว:
int const * const const int * const
หากคุณต้องการให้ตัวชี้เป็น const แต่ไม่ใช่องค์ประกอบที่ชี้ด้วย:
int * const
เปรียบเทียบลายเซ็นของฟังก์ชันกับการตรวจสอบแบบแยกส่วนของไฟล์อ็อบเจ็กต์:
$ g++ -c by-pointer.cpp $ nm -C by-pointer.o 000000000000004a T sum(float*, float*) 0000000000000000 T sum(int const*, int const*) 0000000000000105 T sum(std::vector const*) 000000000000009c T sum(std::vector const*)
ดังที่คุณเห็น nm
เครื่องมือใช้สัญกรณ์แรก (const หลังประเภท) นอกจากนี้โปรดทราบว่าสิ่งเดียวที่ส่งออกและสำคัญสำหรับผู้โทรคือฟังก์ชันจะแก้ไของค์ประกอบที่ชี้โดยตัวชี้หรือไม่ ความมั่นคงของตัวชี้นั้นไม่เกี่ยวข้องกับผู้โทรเนื่องจากตัวชี้จะถูกส่งเป็นสำเนาเสมอ ฟังก์ชันสามารถสร้างสำเนาของตัวชี้เพื่อชี้ไปที่อื่นได้เท่านั้นซึ่งไม่เกี่ยวข้องกับผู้โทร
ดังนั้นไฟล์ส่วนหัวสามารถสร้างเป็น:
#include int sum(int const* a, int const* b); float sum(float* a, float* b); int sum(std::vector* const); float sum(std::vector* const);
การส่งผ่านตัวชี้ก็เหมือนกับการส่งผ่านโดยการอ้างอิง ความแตกต่างอย่างหนึ่งคือเมื่อคุณผ่านการอ้างอิงผู้โทรจะถูกคาดหวังและถือว่าผ่านการอ้างอิงขององค์ประกอบที่ถูกต้องไม่ชี้ไปที่ NULL หรือที่อยู่อื่นที่ไม่ถูกต้องในขณะที่ตัวชี้สามารถชี้ไปที่ NULL ได้เช่นกัน สามารถใช้พอยน์เตอร์แทนการอ้างอิงได้เมื่อผ่าน NULL มีความหมายพิเศษ
เนื่องจาก C ++ 11 สามารถส่งผ่านค่า ย้ายความหมาย . หัวข้อนี้จะไม่ได้รับการปฏิบัติในบทความนี้ แต่สามารถศึกษาได้ในบทความอื่น ๆ เช่น การส่งผ่านอาร์กิวเมนต์ใน C ++ .
อีกหัวข้อที่เกี่ยวข้องซึ่งจะไม่กล่าวถึงในที่นี้คือวิธีเรียกใช้ฟังก์ชันเหล่านั้นทั้งหมด หากส่วนหัวทั้งหมดเหล่านั้นรวมอยู่ในไฟล์ต้นฉบับ แต่ไม่ได้ถูกเรียกการคอมไพล์และการเชื่อมโยงจะสำเร็จ แต่ถ้าคุณต้องการเรียกใช้ฟังก์ชันทั้งหมดจะมีข้อผิดพลาดบางประการเนื่องจากการเรียกบางอย่างจะไม่ชัดเจน คอมไพลเลอร์จะสามารถเลือกผลรวมได้มากกว่าหนึ่งเวอร์ชันสำหรับอาร์กิวเมนต์บางอย่างโดยเฉพาะอย่างยิ่งเมื่อเลือกว่าจะส่งผ่านสำเนาหรือโดยการอ้างอิง (หรือการอ้างอิง const) การวิเคราะห์นั้นอยู่นอกขอบเขตของบทความนี้
มาดูสถานการณ์ในชีวิตจริงที่เกี่ยวข้องกับเรื่องนี้ซึ่งจุดบกพร่องที่หายากอาจปรากฏขึ้น
ไปที่ไดเรกทอรี cpp-article/diff-flags
และดูที่ Counters.hpp
:
class Counters { public: Counters() : #ifndef NDEBUG // Enabled in debug builds m_debugAllCounters(0), #endif m_counter1(0), m_counter2(0) { } #ifndef NDEBUG // Enabled in debug build #endif void inc1() { #ifndef NDEBUG // Enabled in debug build ++m_debugAllCounters; #endif ++m_counter1; } void inc2() { #ifndef NDEBUG // Enabled in debug build ++m_debugAllCounters; #endif ++m_counter2; } #ifndef NDEBUG // Enabled in debug build int getDebugAllCounters() { return m_debugAllCounters; } #endif int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: #ifndef NDEBUG // Enabled in debug builds int m_debugAllCounters; #endif int m_counter1; int m_counter2; };
คลาสนี้มีตัวนับสองตัวซึ่งเริ่มต้นด้วยศูนย์และสามารถเพิ่มหรืออ่านได้ สำหรับการแก้ไขข้อบกพร่องซึ่งเป็นวิธีที่ฉันจะเรียกบิวด์โดยที่ NDEBUG
ไม่ได้กำหนดมาโครฉันยังเพิ่มตัวนับที่สามซึ่งจะเพิ่มขึ้นทุกครั้งที่มีการเพิ่มตัวนับอีกสองตัว นั่นจะเป็นตัวช่วยดีบักประเภทหนึ่งสำหรับคลาสนี้ คลาสไลบรารีของบุคคลที่สามจำนวนมากหรือแม้แต่ส่วนหัว C ++ ในตัว (ขึ้นอยู่กับคอมไพเลอร์) ใช้เทคนิคเช่นนี้เพื่ออนุญาตให้มีการดีบักในระดับต่างๆ สิ่งนี้ช่วยให้การแก้ไขจุดบกพร่องสามารถตรวจจับตัวทำซ้ำที่อยู่นอกช่วงและสิ่งที่น่าสนใจอื่น ๆ ที่ผู้สร้างไลบรารีสามารถคิดได้ ฉันจะเรียกการสร้างรุ่น 'สร้างโดยที่ NDEBUG
กำหนดมาโครแล้ว”
สำหรับรุ่นที่วางจำหน่ายส่วนหัวที่คอมไพล์ไว้ล่วงหน้าจะมีลักษณะดังนี้ (ฉันใช้ grep
เพื่อลบบรรทัดว่าง):
$ g++ -E -DNDEBUG Counters.hpp | grep -v -e '^$' # 1 'Counters.hpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'Counters.hpp' class Counters { public: Counters() : m_counter1(0), m_counter2(0) { } void inc1() { ++m_counter1; } void inc2() { ++m_counter2; } int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: int m_counter1; int m_counter2; };
ในขณะที่สร้างการแก้ไขข้อบกพร่องจะมีลักษณะดังนี้:
$ g++ -E Counters.hpp | grep -v -e '^$' # 1 'Counters.hpp' # 1 '' # 1 '' # 1 '/usr/include/stdc-predef.h' 1 3 4 # 1 '' 2 # 1 'Counters.hpp' class Counters { public: Counters() : m_debugAllCounters(0), m_counter1(0), m_counter2(0) { } void inc1() { ++m_debugAllCounters; ++m_counter1; } void inc2() { ++m_debugAllCounters; ++m_counter2; } int getDebugAllCounters() { return m_debugAllCounters; } int get1() const { return m_counter1; } int get2() const { return m_counter2; } private: int m_debugAllCounters; int m_counter1; int m_counter2; };
มีตัวนับอีกหนึ่งตัวในการสร้างดีบักดังที่ฉันได้อธิบายไว้ก่อนหน้านี้
ฉันยังสร้างไฟล์ตัวช่วยบางไฟล์
// increment1.hpp: // Forward declaration so I don't have to include the entire header here class Counters; int increment1(Counters&); // increment1.cpp: #include 'Counters.hpp' void increment1(Counters& c) { c.inc1(); }
// increment2.hpp: // Forward declaration so I don't have to include the entire header here class Counters; int increment2(Counters&); // increment2.cpp: #include 'Counters.hpp' void increment2(Counters& c) { c.inc2(); }
// main.cpp: #include #include 'Counters.hpp' #include 'increment1.hpp' #include 'increment2.hpp' using namespace std; int main(int argc, char* argv[]) { Counters c; increment1(c); // 3 times increment1(c); increment1(c); increment2(c); // 4 times increment2(c); increment2(c); increment2(c); cout << 'c.get1(): ' << c.get1() << endl; // Should be 3 cout << 'c.get2(): ' << c.get2() << endl; // Should be 4 #ifndef NDEBUG // For debug builds cout << 'c.getDebugAllCounters(): ' << c.getDebugAllCounters() << endl; // Should be 3 + 4 = 7 #endif return 0; }
และ Makefile
ที่สามารถปรับแต่งแฟล็กคอมไพเลอร์สำหรับ increment2.cpp
เท่านั้น:
all: main.o increment1.o increment2.o g++ -o diff-flags main.o increment1.o increment2.o main.o: main.cpp increment1.hpp increment2.hpp Counters.hpp g++ -c -O2 main.cpp increment1.o: increment1.cpp Counters.hpp g++ -c $(CFLAGS) -O2 increment1.cpp increment2.o: increment2.cpp Counters.hpp g++ -c -O2 increment2.cpp clean: rm -f *.o diff-flags
มารวบรวมทั้งหมดในโหมดดีบักโดยไม่กำหนด NDEBUG
:
$ CFLAGS='' make g++ -c -O2 main.cpp g++ -c -O2 increment1.cpp g++ -c -O2 increment2.cpp g++ -o diff-flags main.o increment1.o increment2.o
ตอนนี้เรียกใช้:
$ ./diff-flags c.get1(): 3 c.get2(): 4 c.getDebugAllCounters(): 7
ผลลัพธ์เป็นไปตามที่คาดไว้ ตอนนี้เรามารวบรวมไฟล์เพียงหนึ่งไฟล์ด้วย NDEBUG
กำหนดไว้ซึ่งจะเป็นโหมดเผยแพร่และดูว่าเกิดอะไรขึ้น:
โหนด js เกิดข้อผิดพลาดใหม่
$ make clean rm -f *.o diff-flags $ CFLAGS='-DNDEBUG' make g++ -c -O2 main.cpp g++ -c -DNDEBUG -O2 increment1.cpp g++ -c -O2 increment2.cpp g++ -o diff-flags main.o increment1.o increment2.o $ ./diff-flags c.get1(): 0 c.get2(): 4 c.getDebugAllCounters(): 7
ผลลัพธ์ไม่เป็นไปตามที่คาดไว้ increment1
ฟังก์ชันจะเห็นคลาส Counters เวอร์ชันที่วางจำหน่ายซึ่งมีฟิลด์สมาชิก int เพียงสองฟิลด์ ดังนั้นมันจึงเพิ่มฟิลด์แรกโดยคิดว่ามันเป็น m_counter1
และไม่ได้เพิ่มอะไรอีกเลยเพราะมันไม่รู้อะไรเกี่ยวกับ m_debugAllCounters
ฟิลด์ ฉันพูดแบบนั้น increment1
เพิ่มตัวนับเนื่องจากเมธอด inc1 ใน Counter
อยู่ในบรรทัดดังนั้นจึงถูกแทรกใน increment1
ฟังก์ชั่นร่างกายไม่ถูกเรียกจากมัน คอมไพเลอร์อาจตัดสินใจที่จะอินไลน์เนื่องจาก -O2
ใช้แฟล็กระดับการเพิ่มประสิทธิภาพ
ดังนั้น m_counter1
ไม่เคยเพิ่มขึ้นและ m_debugAllCounters
ถูกเพิ่มขึ้นแทนโดยไม่ได้ตั้งใจใน increment1
นั่นคือเหตุผลที่เราเห็น 0 สำหรับ m_counter1
แต่เรายังคงเห็น 7 สำหรับ m_debugAllCounters
.
การทำงานในโปรเจ็กต์ที่เรามีไฟล์ต้นฉบับจำนวนมากซึ่งจัดกลุ่มในไลบรารีแบบคงที่จำนวนมากมันเกิดขึ้นที่ไลบรารีเหล่านั้นบางส่วนถูกคอมไพล์โดยไม่มีตัวเลือกการดีบักสำหรับ std::vector
และอื่น ๆ ถูกรวบรวมด้วยตัวเลือกเหล่านั้น
อาจเป็นไปได้ว่าในบางครั้งไลบรารีทั้งหมดใช้แฟล็กเดียวกัน แต่เมื่อเวลาผ่านไปไลบรารีใหม่จะถูกเพิ่มโดยไม่คำนึงถึงแฟล็กเหล่านั้น (ไม่ใช่แฟล็กเริ่มต้น แต่ถูกเพิ่มด้วยมือ) เราใช้ IDE ในการคอมไพล์ดังนั้นเพื่อดูแฟล็กสำหรับแต่ละไลบรารีคุณต้องเจาะลึกลงในแท็บและหน้าต่างโดยมีแฟล็ก (และหลายอัน) ที่แตกต่างกันสำหรับโหมดคอมไพล์ที่แตกต่างกัน (รีลีส, ดีบัก, โปรไฟล์ ... ) ดังนั้นมันจึงยากกว่า โปรดทราบว่าการตั้งค่าสถานะไม่สอดคล้องกัน
สิ่งนี้ทำให้เกิดขึ้นได้ยากเมื่ออ็อบเจ็กต์ไฟล์คอมไพล์ด้วยแฟล็กชุดเดียวส่งผ่าน std::vector
ไปยังไฟล์ออบเจ็กต์ที่คอมไพล์ด้วยชุดแฟล็กที่แตกต่างกันซึ่งดำเนินการบางอย่างกับเวกเตอร์นั้นแอปพลิเคชันจะล้ม ลองนึกภาพว่าการดีบักนั้นไม่ใช่เรื่องง่ายเนื่องจากมีการรายงานข้อขัดข้องว่าเกิดขึ้นในเวอร์ชันที่วางจำหน่ายและจะไม่เกิดขึ้นในเวอร์ชันดีบัก (อย่างน้อยก็ไม่ใช่ในสถานการณ์เดียวกันกับที่มีการรายงาน)
ตัวดีบักเกอร์ยังทำสิ่งที่บ้าคลั่งเพราะมันกำลังดีบักโค้ดที่ปรับให้เหมาะสมมาก ข้อขัดข้องเกิดขึ้นในรหัสที่ถูกต้องและไม่สำคัญ
ในบทความนี้คุณได้เรียนรู้เกี่ยวกับโครงสร้างภาษาพื้นฐานของ C ++ และวิธีการทำงานของคอมไพเลอร์เริ่มตั้งแต่ขั้นตอนการประมวลผลไปจนถึงขั้นตอนการเชื่อมโยง การรู้วิธีการทำงานสามารถช่วยให้คุณมองกระบวนการทั้งหมดแตกต่างกันและให้ข้อมูลเชิงลึกมากขึ้นเกี่ยวกับกระบวนการเหล่านี้ที่เราใช้ในการพัฒนา C ++
ตั้งแต่ขั้นตอนการคอมไพล์สามขั้นตอนไปจนถึงการรวบรวมชื่อฟังก์ชันและการสร้างลายเซ็นฟังก์ชันที่แตกต่างกันในสถานการณ์ต่างๆคอมไพเลอร์จะทำงานมากมายเพื่อเสนอพลังของ C ++ เป็นภาษาโปรแกรมที่คอมไพล์
ฉันหวังว่าคุณจะพบความรู้จากบทความนี้ที่เป็นประโยชน์ในโครงการ C ++ ของคุณ
ที่เกี่ยวข้อง: วิธีการเรียนรู้ภาษา C และ C ++: รายการที่ดีที่สุด