portaldacalheta.pt
  • หลัก
  • การจัดการวิศวกรรม
  • Kpi และ Analytics
  • เทคโนโลยี
  • ว่องไว
เทคโนโลยี

C ++ ทำงานอย่างไร: ทำความเข้าใจกับการคอมไพล์



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 เท่านั้น



สร้างไปป์ไลน์: Preprocess, Compile และ Link

ไฟล์ซอร์ส 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

Header Guards ทำงานอย่างไร

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

เป็น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 ++: รายการที่ดีที่สุด

บทบาทของสีใน UX

การออกแบบ Ux

บทบาทของสีใน UX
คู่มือสำหรับนักพัฒนาเกี่ยวกับใบอนุญาตโอเพนซอร์ส

คู่มือสำหรับนักพัฒนาเกี่ยวกับใบอนุญาตโอเพนซอร์ส

เทคโนโลยี

โพสต์ยอดนิยม
Nvidia Shield - สิ่งที่แตกต่างบนคอนโซลเกม Android
Nvidia Shield - สิ่งที่แตกต่างบนคอนโซลเกม Android
แผ่นโกงการจัดการโครงการ
แผ่นโกงการจัดการโครงการ
เริ่มต้นใช้งาน Microservices: บทช่วยสอน Dropwizard
เริ่มต้นใช้งาน Microservices: บทช่วยสอน Dropwizard
การแยกการเรียกเก็บเงิน: เรื่องของการเพิ่มประสิทธิภาพ API ภายใน GraphQL
การแยกการเรียกเก็บเงิน: เรื่องของการเพิ่มประสิทธิภาพ API ภายใน GraphQL
กรณีศึกษา: การใช้ ApeeScape เพื่อม้วนปลาใหญ่
กรณีศึกษา: การใช้ ApeeScape เพื่อม้วนปลาใหญ่
 
การประมาณต้นทุนซอฟต์แวร์ในการจัดการโครงการแบบ Agile
การประมาณต้นทุนซอฟต์แวร์ในการจัดการโครงการแบบ Agile
แชทล่ม - เมื่อ Chatbot ล้มเหลว
แชทล่ม - เมื่อ Chatbot ล้มเหลว
ที่ปรึกษาการระดมทุนกับนายหน้า - ตัวแทนจำหน่าย
ที่ปรึกษาการระดมทุนกับนายหน้า - ตัวแทนจำหน่าย
ทำให้ Web Front-end เชื่อถือได้ด้วย Elm
ทำให้ Web Front-end เชื่อถือได้ด้วย Elm
คู่มือสำหรับนักลงทุนเกี่ยวกับน้ำมันปาล์ม
คู่มือสำหรับนักลงทุนเกี่ยวกับน้ำมันปาล์ม
โพสต์ยอดนิยม
  • ผังบัญชีบริษัทซอฟต์แวร์
  • บริษัท เอส คอร์ปอเรชั่น vs. บริษัท ซี
  • วิธีแฮกบัตรเครดิตออนไลน์
  • ผลกระทบของฟินเทคต่อธนาคาร
  • ตัวจัดการข้อผิดพลาดส่วนกลางของโหนด js
หมวดหมู่
  • การจัดการวิศวกรรม
  • Kpi และ Analytics
  • เทคโนโลยี
  • ว่องไว
  • © 2022 | สงวนลิขสิทธิ์

    portaldacalheta.pt