ความรู้เกี่ยวกับการเขียนโปรแกรมภาษา C ที่นักอิเล็กทรอนิกส์ไม่ค่อยรู้ ตอนที่ 2 การ Compile และการ Build

ความรู้เกี่ยวกับการเขียนโปรแกรมภาษา C ที่นักอิเล็กทรอนิกส์ไม่ค่อยรู้

ตอนที่ 2 การ Compile และการ Build

            สำหรับคนที่เขียนโปรแกรมมือใหม่อาจจะยังไม่เข้าใจกระบวนการ compile ของ compiler ทำให้เวลาเจอ error บางอย่างก็ไม่รู้ว่าจะแก้ไขยังไง บางทีติด error ง่ายๆเป็นชั่วโมง เราจึงจำเป็นจะต้องรู้จักกระบวนการทำงาน ของ compiler คร่าวๆ เพื่อจะได้เขียนโปรแกรมเพื่อประหยัดเวลาในการทำ project ที่มีอยู่น้อยนิดของเรา
ไฟล์ header และไฟล์ code
            โดยทั่วไปใน project ที่เราสร้างขึ้นมาจะประกอบไปด้วยไฟล์ .c (code) และ .h (header) เป็นหลัก ไฟล์ .c (เป็นที่ที่เราเขียน body จริงๆของ function) จะเป็นไฟล์ที่เราทำการ add เข้ามาใน project ส่วนไฟล์ .h (เป็นที่ที่เราประกาศ prototype ของ function) เป็นไฟล์ที่เราใช้การ include ไว้ในไฟล์อื่นๆอีกที สิ่งแรกที่ต้องเข้าใจก็คือ compiler จะทำการ compile ไฟล์ .c เป็นไฟล์ๆไป โดยมองว่าแต่ละไฟล์ไม่เกี่ยวข้องกัน โดยก่อนอื่น compiler จะแปลคำสั่ง preprocessor ก่อน (เราได้พูดถึง preprocessor ไปในตอนที่ 1) ทำให้ไฟล์ .h ที่เราใช้คำสั่ง include ไว้ เสมือนว่าถูกนำมาแปะแทรกไว้ตรงบรรทัดที่วางคำสั่ง include นั้น 
ทำไมต้องมีไฟล์ .c หลายๆ ไฟล์
            สำหรับใครยังเขียนโปรแกรมแบบมีไฟล์ main.c อยู่ไฟล์เดียวคงจะสงสัยว่าทำไมเราต้องมีไฟล์ .c หลายๆไฟล์ คำตอบก็คือเพื่อแยกการเขียนโปรแกรมให้เป็นระเบียบ จัดการง่าย ค้นหาง่าย upgrade ได้ง่าย และนำไปใช้ใหม่ได้ง่าย สมมติว่าเราใช้ serial port ของ microcontroller เราก็มักจะแยกเอาไฟล์ที่เกี่ยวข้องกับ serial port มาเขียนเป็น uart.c กับ uart.h เวลาค้นหา function ที่เกี่ยวข้องกับ serial port เราก็มาหาที่ไฟล์ uart ที่เดียว จะแก้ไขก็แก้ที่นี่ไฟล์เดียว ไม่ต้องกลัวว่าจะไปกระทบกับส่วนอื่น และสมมติว่าเราเอาไฟล์ uart ของเพื่อนที่ใช้ microcontroller เบอร์เดียวกันมาใช้ แล้วปรากฏว่าเพื่อนใจดีทิ้ง bug ไว้ให้ วันหลังเพื่อนแก้ bug ได้ก็แค่ copy เอาไฟล์ใหม่มาทับไฟล์เก่าได้เลย
วาง prototype ของ function ไว้ที่ไหน
            กลับมาที่การ compile ไฟล์ .c แต่ละไฟล์จะถูก compile จากบรรทัดบนสุดลงไปจนจบ ถ้ามีบรรทัดไหนที่มีการเรียกใช้ function แล้วเราไม่ได้เขียน prototype หรือ body ของ function นั้นไว้ก่อนหน้านั้น compiler ก็จะแจ้ง error ขึ้นมา (error อาจแตกต่างกันสำหรับ compiler แต่ละตัว) การที่ compiler ต้องรู้จัก prototype หรือ body ของ function นั้นก็เพราะว่าเวลาที่เราเรียกใช้ function ตัว compiler จะต้องรู้ว่า function นั้นมี parameter ที่เป็น input อะไรบ้าง และมีการ return ค่าเป็นอะไร เพื่อที่จะได้จองพื้นที่ใน code memory ได้ถูก ดังนั้นเราจึงมักจะเห็นการเขียน prototype ของ function ต่างๆไว้ในไฟล์ .h หรือไม่ก็ด้านบนของไฟล์ .c โดยถ้าเป็น function ที่เราจะเอาไว้เรียกใช้จากไฟล์อื่น ก็จะเขียน prototype ไว้ที่ไฟล์ .h แต่ถ้าเป็น function ที่ใช้ภายในก็จะเขียนไว้ที่ด้านบนของไฟล์ .c
การเอา #include มารวมไว้ที่ไฟล์เดียวกัน
            ปกติแล้วเราจะ include header ของไฟล์ .c แต่ละไฟล์เอาไว้ที่ไฟล์ .c ของใครของมัน แล้วที่ไฟล์ main จึงมีการ include header ไว้รวมกัน หรือถ้าไฟล์ .c อันไหนที่ต้องเรียกใช้ function จากไฟล์ .c อันอื่น ก็จะต้อง include header ของไฟล์นั้นเพิ่มเข้าไปต่างหาก แต่ถ้า project ที่เราทำมีขนาดใหญ่ มีการ include ไฟล์เป็นสิบๆไฟล์ และมีการเรียก function ข้ามกันไปมาอยู่ตลอด เราก็อาจจะเอาการ include header ทั้งหมดนั้น มาไว้รวมกันเป็นไฟล์ .h ต่างหากอีกไฟล์หนึ่ง ไฟล์ที่สร้างมาต่างหากนี้ จะถูก include ในไฟล์ .c ทุกๆไฟล์ โดยแต่ละไฟล์ก็ทำการ include ไฟล์ดังกล่าวนี้แค่ไฟล์เดียว 
            นอกเหนือจากการสร้าง prototype ของ function ไว้ในไฟล์ header เพื่อให้ compiler รู้จัก function แล้ว เรายังสามารถใช้ extern ได้อีกด้วย การประกาศให้ function ใดๆเป็น extern หมายถึงการบอกให้ compiler รู้ว่า function นั้น มี body อยู่ที่ไฟล์อื่น วิธีการใช้ก็แค่ใส่คำว่า extern ไว้ข้างหน้า prototype เช่น
extern void delay(void);
การใช้ extern เพื่อใช้ตัวแปรร่วมกันระหว่างไฟล์
            ถ้าหากว่าเรามีตัวแปรที่ต้องการใช้ร่วมกันในไฟล์ .c หลายๆไฟล์ เราก็น่าจะเอาไปประกาศใน header ไฟล์ได้เลยหรือไม่ คำตอบคือประกาศเฉยๆ ไม่ได้ แต่ต้องใส่ extern ด้วย เพราะตัวแปรไม่เหมือนกับ function การประกาศ prototype ของ function ใดๆนั้น เป็นแค่การบอกให้ compiler รู้ว่า function นั้นหน้าตาเป็นยังไง ต้องมี input เป็นอะไร และจะ return อะไรกลับมา การกระโดดเข้าไปทำงาน function นั้นแล้วกลับออกมาจะได้ทำได้ถูก แต่การประกาศตัวแปรนั้นเป็นการจองพื้นที่ เป็นการสร้าง object ของตัวแปรนั้นขึ้นมา ซึ่งถ้าเราใส่ใน header แล้วหลายๆไฟล์มีการนำไป include จะทำให้เกิดการสร้าง object ขึ้นมาหลายๆที่โดยมีชื่อที่ซ้ำกัน ทำให้เกิด error ขึ้น วิธีการที่ถูกต้องคือการใส่ extern เข้าไปด้วย compiler จะไม่สร้าง object ขึ้นมาซ้ำกัน เพราะเราได้บอกกับ compiler เอาไว้ว่าตัวแปรนั้น จริงๆแล้วถูกประกาศไว้ที่อื่น ตัวอย่างการใช้ extern กับตัวแปร เช่น
extern unsigned char gbuf[];
ข้อควรระวังในการใช้ extern
            จากตัวอย่างที่ผ่านมาจะเห็นว่าใน [ ] ไม่มีการระบุขนาดของ array เอาไว้ ซึ่งจริงๆจะใส่หรือไม่ใส่ก็ได้ แต่แนะนำว่าในการ extern array ไม่ควรจะระบุขนาดไปด้วย เพราะตอนแรกที่เราประกาศไว้ เราอาจจะใส่ขนาดไว้เท่ากันทั้งในการประกาศตัวแปรจริง กับตัวแปรที่ extern แต่ภายหลังถ้าเรามีการแก้ไขขนาดของ array ที่ประกาศไว้ แล้วลืมตามไปแก้ไขตัวแปรที่ extern ด้วย อาจทำให้เกิดความผิดพลาด เพราะ compiler จะใช้ขนาดที่เราระบุเอาไว้ตอนที่ประกาศแบบ extern (ในไฟล์ที่ประกาศเอาไว้) ในการคำนวณ โดยเฉพาะในการใช้ array 2 มิติ ซึ่งจะทำให้เกิด bug ได้
เมื่อ compile เสร็จ
            อย่างที่กล่าวไว้แต่แรกว่า compiler จะ compile ไฟล์ .c แยกกัน เมื่อcompile เสร็จก็จะได้ object ขึ้นมา โดยเกิดมาจาก function และตัวแปร ในขั้นตอนนี้ถ้ามี object ซ้ำกันก็จะเกิด error ขึ้น ถ้าไม่มี error object เหล่านี้ก็จะถูกกำหนดตำแหน่งลงบน code memory และ RAM และ compiler จะสร้างไฟล์ที่ใช้สำหรับการโหลดโปรแกรมขึ้นมา เป็นอันเสร็จขั้นตอนการ compile

<< กลับไปสู่หน้าแรกสารบัญ