พื้นฐานการเขียนโปรแกรม:การเขียนโปรแกรมแบบปลอดบัก

จาก Theory Wiki
ไปยังการนำทาง ไปยังการค้นหา

การทำอะไรสักอย่าง ถ้าไม่รอบคอบแน่นอนว่าจะมีข้อผิดพลาดเกิดขึ้นได้ง่าย ต่อให้รอบคอบแค่ไหน บางครั้งข้อผิดพลาดก็ยังเกิดขึ้นได้ เช่นเดียวกับการเขียนโปรแกรม ต่อให้โปรแกรมง่ายแค่ไหน สั้นแค่ไหน ก็มีโอกาสมีข้อผิดพลาดในโปรแกรมนั้นได้เสมอ จนมีคำกล่าวที่ว่า "มีข้อผิดพลาดอย่างน้อยหนึ่งที่ที่ยังหาไม่พบในโปรแกรมเสมอ"

อะไรคือบัก

เกริ่นเรื่องข้อผิดพลาดในโปรแกรมมาตั้งนาน ท่านผู้อ่านคงเริ่มสงสัยแล้วว่า ชื่อหัวข้อที่ว่า "การเขียนโปรแกรมแบบปลอดบัก" เกี่ยวข้องยังไงกับข้อผิดพลาด หลายๆท่านคงเริ่มเดาออกแล้วว่า เจ้า บัก นี่แหละ หมายถึงข้อผิดพลาดในโปรแกรม แต่บางท่านคงยังไม่ทราบว่าเหตุใดจึงเรียก ข้อผิดพลาดในโปรแกรมว่า บัก

บัก ในภาษาอังกฤษ เขียนว่า 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 เงื่อนไขเมื่อทำงานเสร็จ

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