พื้นฐานการเขียนโปรแกรม:การเขียนโปรแกรมแบบปลอดบัก
การทำอะไรสักอย่าง ถ้าไม่รอบคอบแน่นอนว่าจะมีข้อผิดพลาดเกิดขึ้นได้ง่าย ต่อให้รอบคอบแค่ไหน บางครั้งข้อผิดพลาดก็ยังเกิดขึ้นได้ เช่นเดียวกับการเขียนโปรแกรม ต่อให้โปรแกรมง่ายแค่ไหน สั้นแค่ไหน ก็มีโอกาสมีข้อผิดพลาดในโปรแกรมนั้นได้เสมอ จนมีคำกล่าวที่ว่า "มีข้อผิดพลาดอย่างน้อยหนึ่งที่ที่ยังหาไม่พบในโปรแกรมเสมอ"
เนื้อหา
- 1 อะไรคือบัก
- 2 บักเกิดขึ้นได้อย่างไรบ้าง
- 2.1 Divide by zero:หารด้วยศูนย์
- 2.2 Infinite loops:ลูปอนันต์
- 2.3 Arithmetic overflow or underflow:ค่าเกินขอบเขตตัวแปร
- 2.4 Exceeding array bounds:ค่าตัวชี้เกินขนาดของตัวแปรArray
- 2.5 Using an uninitialized variable:ใช้ตัวแปรที่ไม่ได้มีการกำหนดค่า
- 2.6 Accessing memory not owned (Access violation):การอ้างค่าจากหน่วยความจำที่ไม่มีสิทธิ์เข้าถึง
- 2.7 Memory leak or Handle leak:หน่วยความจำไม่พอ
- 2.8 Stack overflow or underflow:ปัญหาสแตกล้น และปัญหาข้อมูลในสแตกไม่พอ
- 2.9 Buffer overflow:
- 2.10 Deadlock:การแย่งทรัพยากร
- 2.11 Off by one error:
- 2.12 Race hazard:
- 2.13 Loss of precision in type conversion:
- 3 หลักการลดจำนวนบัก
อะไรคือบัก
เกริ่นเรื่องข้อผิดพลาดในโปรแกรมมาตั้งนาน ท่านผู้อ่านคงเริ่มสงสัยแล้วว่า ชื่อหัวข้อที่ว่า "การเขียนโปรแกรมแบบปลอดบัก" เกี่ยวข้องยังไงกับข้อผิดพลาด หลายๆท่านคงเริ่มเดาออกแล้วว่า เจ้า บัก นี่แหละ หมายถึงข้อผิดพลาดในโปรแกรม แต่บางท่านคงยังไม่ทราบว่าเหตุใดจึงเรียก ข้อผิดพลาดในโปรแกรมว่า บัก
บัก ในภาษาอังกฤษ เขียนว่า bug แปลว่า แมลง โดยในสมัยก่อน ช่วงที่คอมพิวเตอร์ยังเป็นเครื่องใหญ่ขนาดเท่าห้องอยู่นั้นอุปกรณ์แต่ละส่วนของคอมพิวเตอร์ก็ใหญ่กว่าในปัจจุบันมาก ใหญ่พอที่จะให้แมลงลงไปอยู่ระหว่างอุปกรณ์ต่างๆภายในเครื่องได้ โดยแมลงตัวแรกที่ถูกพบว่าไปติดอยู่ในเครื่องคอมพิวเตอร์ซึ่งเป็นเหตุให้เกิดข้อผิดพลาดขึ้นนั้น เป็นผีเสื้อกลางคืน ซึ่งถูกพบโดย Grace Murray Hopper ในเครื่อง Mark II ณ มหาวิทยาลัยฮาร์วาร์ด Grace Murray Hopperได้นำเอาซากของผีเสื้อกลางคืนออกจากเครื่อง Mark II และนำไปแปะไว้ในสมุดบันทึก(log book) ซึ่งการกระทำนี้เอง เป็นต้นกำเนิดของคำว่า ดีบัก(debug)
อ่านเพิ่มเติมได้ที่ประวัติของ"บัก"
บักเกิดขึ้นได้อย่างไรบ้าง
สาเหตุหลักๆที่ทำให้เกินบักได้แก่
Divide by zero:หารด้วยศูนย์
ก่อนอื่นลองดูตัวอย่างโปรแกรมข้างล่างกันก่อน
... double a,b,c; ... a=b/c; ...
จะเห็นได้ว่ามีการนำค่าในตัวแปร b หารด้วยค่าในตัวแปร c แล้วนำค่าที่ได้มาใส่ในตัวแปรชื่อ a ซึ่งการกระทำนี้จะไม่มีผลอะไรถ้าตัวแปร c ไม่เป็น 0 เนื่องจากเรารู้กันอยู่แล้วว่านั้นไม่มีความหมายทางคณิตศาสตร์ แต่ค่าอื่นที่ไม่ใช่ 0 ก็ไม่สามารถนำ 0 ไปหารได้้้ ดังนั้นการหารด้วย0จึงเป็นการกระทำที่ไม่สามารถหาคำตอบที่ถูกต้องได้ ซึ่งถ้าสั่งให้คอมพิวเตอร์คำนวน ก็จะได้ค่าที่ผิดพลาดออกมา ซึ่งไม่สามารถนำไปใช้ได้ ดังนั้น การเขียนโปรแกรมที่มีการคำนวนด้วยการหารทุกครั้ง จำเป็นต้องตรวจสอบว่าตัวหารไม่เป็น 0 ก่อนจึงจะทำการหารได้
Infinite loops:ลูปอนันต์
ข้อกำหนดหนึ่งของการตัดสินว่าวิธีการทำงานใดๆเป็น Algorithm หรือเปล่านั้นคือ การทำงานจะต้องยืนยันได้ว่ามีจุดสิ้นสุด
... i=10; do printf("%d\n",i--); until (i=0); ...
ถ้าโดยปกติแล้ว โปรแกรมข้างต้นนี้ก็จะทำงาน 10 รอบ โดยแต่ละรอบจะพิมพ์ตัวเลขตั้งแต่ 10 จนถึง 1 อย่างละ 1 ตัว แต่ในความจริงแล้ว โปรแกรมจะพิมพ์ 10 หนึ่งครั้งแล้ว ตามด้วย 0 ไปเรื่อยๆ ไม่รู้จบ (ดูที่เงื่อนไขในวงเล็บ) ซึ่งอันนี้เป็นข้อผิดพลาดอีกอันนึงที่เกิดขึ้นได้บ่อยๆคือ การที่โปรแกรมทำงานแบบไม่รู้จบ หรือที่บางคนเรียกกันว่า "ติดลูป" ซึ่งทำให้โปรแกรมพบกับข้อผิดพลาดได้
Arithmetic overflow or underflow:ค่าเกินขอบเขตตัวแปร
int a,b,c; ... a=b+c;
โปรแกรมง่ายๆข้างต้นนี้ จะมีปัญหาใหญ่ ถ้า b+c นั้นมีค่าเกินขอบเขตของตัวแปร int ซึ่งจะทำให้การคำนวนได้ค่าที่ผิดไปจากค่าที่ต้องการ
Exceeding array bounds:ค่าตัวชี้เกินขนาดของตัวแปรArray
int a[SIZE],index,data; ... a[index]=data;
นี่ก็เป็นอีกตัวอย่างนึงที่แสดงให้เห็นได้ว่าการเขียนโปรแกรมสามารถมีบักได้ง่ายมาก โปรแกรมข้างต้นจะทำงานผิดพลาดถ้าindexมีค่ามากกว่าหรือเท่ากับSIZE หรือน้อยกว่า 0 ซึ่งถ้าไม่มีการตรวจสอบที่ดี ก็จะทำให้มี บัก ในโปรแกรมได้
Using an uninitialized variable:ใช้ตัวแปรที่ไม่ได้มีการกำหนดค่า
int a,b,c; a=b+c;
ถ้าไม่ได้กำหนดค่าเริ่มต้นให้ แน่นอนว่าไม่มีใครบอกได้ว่าค่าใน a, b และ c จะเป็นค่าอะไร ซึ่งทำให้มีปัญหาได้ค่าที่ไม่ถูกต้องเวลานำไปใช้ หลายๆท่านอาจจะเถียงว่า compiler เป็นผู้ initial ค่าให้ตัวแปรอยู่แล้ว แต่ก็ไม่ใช่ทุก compiler ดังนั้น programmer จึงควรที่จะ initial ค่าของตัวแปรทุกตัวดัวยตัวเอง
Accessing memory not owned (Access violation):การอ้างค่าจากหน่วยความจำที่ไม่มีสิทธิ์เข้าถึง
.สำหรับคอมพิวเตอร์ที่เราใช้อยู่นั้น มีการแบ่งหน่วยความจำออกเป็นส่วนต่างๆ มีทั้งส่วนที่โปรแกรมสามารถใช้ได้ และส่วนที่Operating Systemเป็นผู้ใช้งาน ซึ่งบางส่วนนี้ถ้าโปรแกรมไปอ้างค่าจากหน่วยความจำเหล่านี้ ก็อาจจะมีปัญหากับระบบได้ ส่วนใหญ่แล้ว โปรแกรมต่างๆจะสามารถอ้างหน่วยความจำได้เฉพาะส่วนที่เป็นเจ้าของเองเท่านั้น ดังนั้นถ้าหากมีการอ้างหน่วยความจำที่ไม่ได้เป็นเจ้าของ หรือไม่มีสิทธิ์เข้าถึงก็อาจจะทำให้ได้ค่าที่ไม่ถูกต้อง หรือไปทำให้ค่าที่เก็บอยู่ในหน่วยความจำส่วนนั่นผิดพลาดได้
Memory leak or Handle leak:หน่วยความจำไม่พอ
Stack overflow or underflow:ปัญหาสแตกล้น และปัญหาข้อมูลในสแตกไม่พอ
int recursive(){ return recursive(); }
ถ้าเอาฟังก์ชันข้างบนไปรัน ฟังก์ชันดังกล่าวจะเรียกตัวเองไปเรื่อยๆ จนสแตกที่ใช้เก็บตำแหน่งเวลาคืนค่าเมื่อจบฟังก์ชันไม่พอ จนเกิดStack overflowหรือปัญหาสแตกล้นขึ้น ส่วนในทางตรงกันข้าม ถ้าtop of stackเกิดข้อผิดพลาด ก็จะทำให้เกิด Stack underflow หรือปัญหาข้อมูลในสแตกไม่เพียงพอขึ้นได้
Buffer overflow:
int strcpy(char *source,char *target){ int count = 0; while (*source) do{ *(target++)=*(source++); count++; } *target=0; return count; }
โปรแกรมนี้จะมีปัญหาคือ ถ้าจำนวนตัวอักษร ของ source มีมากกว่าจำนวนตัวอักษรที่ target เก็บได้ก็จะทำให้เกิดการ overflow ได้
Deadlock:การแย่งทรัพยากร
Off by one error:
สำหรับข้อมูลแบบArray ปกติแล้วจะมีการอ้างถึงโดยใช้ index โดยปกติแล้ว index จะมีค่าตั้งแต่ 0 ถึง จำนวนข้อมูล-1 ซึ่ง-1นี้เองเป็นตัวทำให้เกิดข้อผิดพลาดในโปรแกรมบ่อยมาก
Race hazard:
ปัญหาเรื่องเวลา ส่วนใหญ่ปัญหานี้ จะเกิดเมื่อมีการทำงานพร้อมๆกันหลายๆprocessหรือหลายๆthread ตัวอย่างง่ายๆเช่น
for (int i = 0;i<10;i++){ j=i; printf("Copy from i = %d\n",j); }
for (int k = 0;k<10;k++){ j=k; printf("Copy from k = %d\n",j); }
Loss of precision in type conversion:
หลักการลดจำนวนบัก
ถึงแม้ว่าการเีขียนโปรแกรมที่ไม่ให้มีข้อผิดพลาดเลยนั้น เป็นไปได้ยาก แต่การที่จะเขียนโปรแกรมที่สามารถจะหาข้อผิดพลาดที่มีในโปรแกรมได้ง่ายนั้นไม่เป็นสิ่งที่เกินความสามารถ
Layout
สิ่งแรกที่จะทำให้หาข้อผิดพลาดในโปรแกรมได้ง่าย ก็คือต้องเขียนโปรแกรมโปรแกรมให้อ่านง่าย เพื่อที่จะได้สามารถทำความเข้าใจกับโปรแกรมได้ง่าย อย่างเช่น
long power(int x,int y){ long z=1; for (;y--;)z*=x; return z; }
อ่านยากกว่า
long power(int x,int y){ long z=1; for (null;y--;null) z*=x; return z; }
โดยหลักการง่ายๆก็คือ ใส่ช่องว่างเข้้าไปข้างหน้าแต่ละบรรทัด ให้สามาีรถแยกได้ว่า คำสั่งนี้ อยู่ในชุดคำสั่งไหน และพยายามใส่ null เข้าไปแทนคำสั่งที่ละไว้ เพื่อให้เข้าใจได้ง่ายว่าเป็นส่วนไหนของคำสั่ง
Name
ในการเขียนโปรแกรมนั้นมีการใช้ทั้งค่าคงที่ และตัวแปรต่างๆมากมาย เพื่อให้ง่ายต่อการทำความเข้าใจ และหาข้อผิดพลาดได้ง่ายนั้น โปรแกรมเมอร์ควรจะเลือกใช้ชื่อที่เข้าใจง่าย และสื่อถึงความหมายของตัวแปรหรือค่าคงที่นั้นๆ ตัวอย่างเช่น
long power(int x,int y){ long z=1; for (null;y--;null) z*=x; return z; }
จะทำความเข้าใจได้ยากกว่า
long power(int base,int index){ long result=1; for (null;index--;null) result*=base; return result; }
Refactoring
อีกสิ่งหนึ่งที่ทำให้เกิด bug ได้ง่าย และเป็นสิ่งที่โปรแกรมเมอร์ทำกันบ่อยคือการ Copy&Paste โดยส่วนใหญ่แล้ว จะทำการCopy&Pasteเพื่อสร้างCodeที่มีการทำงานเหมือนหรือคล้ายกับCodeที่มีอยู่เดิม แต่การกระทำดังกล่าว มักทำให้เกิด bug เพราะอาจจะทำให้ลืมแก้ไขส่วนที่จำเป็นต้องแก้ไข หลังทำการ Copy&Paste แล้ว หรือว่าถ้ามีการแก้ไขCodeส่วนหนึ่ง แล้วลืมแก้ไขCodeอื่นที่เหมือนกันก็จะทำให้เกิด bug ขึ้นมาได้ bug ที่เกิดจากการCopy&Pasteนั้น สามารถแก้ไขได้ด้วยการทำลRefactoring
Preprocesser
- #define
เป็นการกำหนดค่าบางค่าให้กับชื่อที่ต้องการ โดยมีรูปแบบการเขียนดังนี้
#define ชื่อ ค่า
หรือสามารถเขียนหลายๆบรรทัดได้โดย
#define ชื่อ ค่าบรรทัดที่1 \ ค่าบรรทัดที่2 \ ค่าบรรทัดที่3 \ ... ค่าบรรทัดที่n
โดยcompilerจะแทนที่ "ชื่อ" ทุกตัวในโปรแกรมด้วย "ค่า" ที่กำหนดไว้ เช่น
#define BorderWidth 5
หมายความว่า ให้ compiler แปลงชื่อ BorderWidth ทุกตัวให้เป็น 5
ซึ่งการใช้ Preprocesser #define ทำให้สามารถใช้ชื่อแทนค่าคงที่ได้ ซึ่งทำให้การเปลี่ยนค่าคงที่ตัวเดียวกันสามารถทำได้ง่าย เช่น
#define BorderWidth 5 int BoxWidth(int InnerWidth){ return InnerWidth+(BorderWidth<<1); // BoxWidth=InnerWidth+(BorderWidth*2) } int BoxHeight(int InnerHeight){ return InnerHeight+(BorderWidth<<1); // BoxHeight=InnerHeight+(BorderWidth*2) }
สามารถแก้ไขค่าความกว้่างของขอบและทำความเข้าใจได้ง่ายกว่า
int BoxWidth(int InnerWidth){ return InnerWidth+(5<<1); // BoxWidth=InnerWidth+(5*2) } int BoxHeight(int InnerHeight){ return InnerHeight+(5<<1); // BoxHeight=InnerHeight+(5*2) }
- #if
- #endif
- #else
- #ifdef
- #ifndef
Trap
- Precondition เงื่อนไขก่อนเริ่มการทำงาน
- invariant เงื่อนไขระหว่างทำงาน
- Post-condition เงื่อนไขเมื่อทำงานเสร็จ
กล่าวโดยสรุปแล้ว บัก เกิดเพราะความไม่รอบคอบและความไม่รู้ของโปรแกรมเมอร์ ดังนั้นการลดจำนวนบักที่จำเป็นก็คือ โปรแกรมเมอร์ต้องรอบคอบ และหาความรู้ใหม่ๆเพิ่มเติมอยู่อย่างสม่ำเสมอ