ผลต่างระหว่างรุ่นของ "การโปรแกรมภาษาซี สำหรับโปรแกรมเมอร์จาวาและซีชาร์ป"
Jittat (คุย | มีส่วนร่วม) |
Jittat (คุย | มีส่วนร่วม) |
||
(ไม่แสดง 25 รุ่นระหว่างกลางโดยผู้ใช้ 5 คน) | |||
แถว 1: | แถว 1: | ||
− | เอกสารนี้เกี่ยวข้องกับการโปรแกรมภาษาซี โดยออกแบบสำหรับผู้มีความรู้พื้นฐานการโปรแกรมในภาษาตระกูล java และ c# มาแล้ว | + | เอกสารนี้เกี่ยวข้องกับการโปรแกรมภาษาซี โดยออกแบบสำหรับผู้มีความรู้พื้นฐานการโปรแกรมในภาษาตระกูล java และ c# มาแล้ว |
<geshi lang="c"> | <geshi lang="c"> | ||
แถว 116: | แถว 116: | ||
== อาร์เรย์และพอยน์เตอร์ == | == อาร์เรย์และพอยน์เตอร์ == | ||
+ | |||
+ | <geshi lang="c"> | ||
+ | #include <stdio.h> | ||
+ | #include <assert.h> | ||
+ | |||
+ | #define MAX_N 100 | ||
+ | |||
+ | int a[MAX_N]; | ||
+ | int n; | ||
+ | |||
+ | void read_array(int *np, int a[]) | ||
+ | { | ||
+ | int i; | ||
+ | |||
+ | scanf("%d", np); | ||
+ | for(i=0; i < *np; i++) | ||
+ | scanf("%d", &a[i]); | ||
+ | } | ||
+ | |||
+ | int max_array(int n, int a[]) | ||
+ | { | ||
+ | return 0; // edit this | ||
+ | } | ||
+ | |||
+ | void test_max_array() | ||
+ | { | ||
+ | int x[] = {1,10,20,3,5}; | ||
+ | assert(max_array(5,x)==20); | ||
+ | int y[] = {}; | ||
+ | assert(max_array(0,x)==0); | ||
+ | int z[] = {-10000,-100,-5000}; | ||
+ | assert(max_array(3,z)==-100); | ||
+ | } | ||
+ | |||
+ | int sum_array(int n, int a[]) | ||
+ | { | ||
+ | return 0; // edit this | ||
+ | } | ||
+ | |||
+ | void test_sum_array() | ||
+ | { | ||
+ | int x[] = {}; | ||
+ | assert(sum_array(0,x)==0); | ||
+ | |||
+ | int y[] = {1,2,3,4}; | ||
+ | assert(sum_array(4,y)==10); | ||
+ | |||
+ | int z[] = {-100}; | ||
+ | assert(sum_array(1,z)==-100); | ||
+ | } | ||
+ | |||
+ | main() | ||
+ | { | ||
+ | test_max_array(); | ||
+ | } | ||
+ | </geshi> | ||
== โครงสร้าง (struct) == | == โครงสร้าง (struct) == | ||
แถว 128: | แถว 184: | ||
ในการนำแฟ้มโปรแกรมต้นฉบับหลาย ๆ แฟ้มมารวมกัน เราจำเป็นจะต้องเข้าใจการทำงานพื้นฐานของการคอมไพล์และลิงก์เสียก่อน | ในการนำแฟ้มโปรแกรมต้นฉบับหลาย ๆ แฟ้มมารวมกัน เราจำเป็นจะต้องเข้าใจการทำงานพื้นฐานของการคอมไพล์และลิงก์เสียก่อน | ||
+ | === แฟ้มเป้าหมายและการลิงก์ === | ||
พิจารณาโปรแกรมตัวอย่างด้านล่าง สมมติว่าชื่อ <tt>square.c</tt> | พิจารณาโปรแกรมตัวอย่างด้านล่าง สมมติว่าชื่อ <tt>square.c</tt> | ||
แถว 145: | แถว 202: | ||
gcc square.c -o square | gcc square.c -o square | ||
− | สิ่งที่เกิดขึ้นระหว่างการคอมไพล์เพื่อสร้างแฟ้ม <tt>square</tt> ที่ทำงานได้ คือคอมไพเลอร์จะแปลแฟ้ม <tt>square.c</tt> | + | สิ่งที่เกิดขึ้นระหว่างการคอมไพล์เพื่อสร้างแฟ้ม <tt>square</tt> ที่ทำงานได้ คือคอมไพเลอร์จะแปลแฟ้ม <tt>square.c</tt> ให้เป็นแฟ้มวัตถุ (object file) ก่อน ในแฟ้มดังกล่าวจะประกอบไปด้วยรหัสภาษาเครื่องของคำสั่งที่เขียนใน <tt>square.c</tt> ทั้งหมด อย่างไรก็ตาม สังเกตว่า โปรแกรมดังกล่าวมีการใช้งานฟังก์ชัน <tt>scanf</tt> และ <tt>printf</tt> ซึ่งไม่ได้ถูกนิยามไว้ในแฟ้มดังกล่าว (แม้มีการประกาศฟังก์ชันในแฟ้ม <tt>stdio.h</tt> แต่ไม่มีการระบุนิยามของฟังก์ชันดังกล่าว) |
− | + | ในขั้นตอนของการสร้างแฟ้มวัตถุ ถ้าโปรแกรมมีการอ้างอิงถึงฟังก์ชันที่อยู่นอกขอบเขตแฟ้ม การอ้างอิงดังกล่าวจะถูก "จด" เอาไว้ เพื่อนำไปค้นหาและเชื่อมโยงต่อในขั้นตอนการลิงก์ | |
เราสามารถสั่งให้ <tt>gcc</tt> ทำงานเฉพาะขั้นตอนแรกได้โดยสั่ง | เราสามารถสั่งให้ <tt>gcc</tt> ทำงานเฉพาะขั้นตอนแรกได้โดยสั่ง | ||
แถว 159: | แถว 216: | ||
มี: main | มี: main | ||
ต้องการ: scanf, printf | ต้องการ: scanf, printf | ||
+ | |||
+ | เมื่อนำแฟ้มวัตถุดังกล่าวที่มีการประกาศ <tt>main</tt> ไปลิงก์เข้ากับไลบรารีภาษาซี ซึ่งมีการประกาศ <tt>scanf</tt> และ <tt>printf</tt> ไว้ ก็จะได้ผลลัพธ์ที่ต้องการ | ||
+ | |||
+ | '''หมายเหตุ''': เราสามารถทดลองดูผลลัพธ์ที่เกิดจากการ "หากันไม่เจอได้" โดยลองเปลี่ยนชื่อฟังก์ชัน <tt>main</tt> หรือแก้ฟังก์ชัน <tt>scanf</tt> หรือ <tt>printf</tt> เป็นชื่ออื่นที่ไม่มีการนิยามในระบบ | ||
+ | |||
+ | === การลิงก์เมื่อมีแฟ้มโปรแกรมต้นฉบับหลายแฟ้ม === | ||
+ | |||
+ | จากตัวอย่างที่แล้ว ทุก ๆ แฟ้มโปรแกรมต้นฉบับเมื่อแปลงเป็นแฟ้มวัตถุจะมีการประกาศนิยามวัตถุต่าง ๆ เอาไว้ สมมติเรามีแฟ้มโปรแกรมสองแฟ้มดังนี้ | ||
+ | |||
+ | '''แฟ้ม <tt>a.c</tt>''' | ||
+ | <geshi lang="c"> | ||
+ | include <stdio.h> | ||
+ | |||
+ | void a() | ||
+ | { | ||
+ | printf("this is a\n"); | ||
+ | } | ||
+ | |||
+ | void c() | ||
+ | { | ||
+ | b(); | ||
+ | printf("this is c\n"); | ||
+ | } | ||
+ | |||
+ | main() | ||
+ | { | ||
+ | c(); | ||
+ | } | ||
+ | </geshi> | ||
+ | |||
+ | '''แฟ้ม <tt>b.c</tt>''' | ||
+ | <geshi lang="c"> | ||
+ | #include <stdio.h> | ||
+ | |||
+ | void b() | ||
+ | { | ||
+ | a(); | ||
+ | printf("this is b\n"); | ||
+ | } | ||
+ | </geshi> | ||
+ | |||
+ | เมื่อสั่งคอมไฟล์แฟ้มทั้งสองด้วยคำสั่ง | ||
+ | |||
+ | gcc -c a.c | ||
+ | gcc -c b.c | ||
+ | |||
+ | ได้แฟ้มวัตถุ <tt>a.o</tt> และ <tt>b.o</tt> มา แต่ละแฟ้มจะมีข้อมูลสำหรับการเชื่อมโยงและการอ้างอิงดังนี้ | ||
+ | |||
+ | a.o b.o | ||
+ | --------------- ---------------- | ||
+ | has: main, a, c has: b | ||
+ | want: b want: a | ||
+ | |||
+ | ดังนั้น เมื่อเรานำทั้งสองแฟ้มมาลิงก์รวมกัน การอ้างอิงดังกล่าวก็จะถูกเชื่อมโยงได้เรียบร้อย การลิงก์สามารถทำได้โดยสั่ง | ||
+ | |||
+ | gcc a.o b.o | ||
+ | |||
+ | ผลลัพธ์ที่ได้จะอยู่ในแฟ้ม <tt>a.out</tt> ตามปกติ ถ้าหากต้องการได้แฟ้มที่ทำงานได้ชื่อ abc เราสามารถใช้ option '''-o''' ในการระบุชื่อแฟ้มได้ โดยสั่ง | ||
+ | |||
+ | gcc a.o b.o -o abc | ||
+ | |||
+ | === การประกาศฟังก์ชัน === | ||
+ | |||
+ | อย่างไรก็ตาม ถ้าเราแก้ไขแฟ้ม <tt>b.c</tt> ให้เป็นดังนี้ | ||
+ | |||
+ | '''แฟ้ม <tt>b.c</tt>''' | ||
+ | <geshi lang="c"> | ||
+ | #include <stdio.h> | ||
+ | |||
+ | void b(int x) /* เปลี่ยนการประกาศ */ | ||
+ | { | ||
+ | int i; | ||
+ | |||
+ | for(i=0; i<x; i++) | ||
+ | a(); | ||
+ | printf("this is b\n"); | ||
+ | } | ||
+ | </geshi> | ||
+ | |||
+ | เราจะพบว่า เรายังสามารถรวมแฟ้มโปรแกรม <tt>a.c</tt> และ <tt>b.c</tt> เข้าด้วยกันได้เช่นเดิม อย่างไรก็ตาม เราพบว่าการเรียกใช้ฟังก์ชัน <tt>b</tt> ใน <tt>a.c</tt> นั้น'''ผิดพลาด''' เพราะว่ามีการเรียกใช้ฟังก์ชันโดยส่งค่าผิด | ||
+ | |||
+ | สังเกตว่าระหว่างที่คอมไพเลอร์แปลโปรแกรม <tt>a.c</tt> นั้น คอมไพเลอร์ไม่มีข้อมูลของฟังก์ชัน <tt>b</tt> อยู่เลย ทำให้ไม่ทราบว่าการเรียกใช้นั้นผิดพลาด | ||
+ | |||
+ | ความผิดพลาดเช่นนี้สามารถป้องกันได้โดยใส่การประกาศฟังก์ชัน <tt>b</tt> ไว้ก่อนที่ตอนต้นของโปรแกรม <tt>a.c</tt> ดังนี้ | ||
+ | |||
+ | <geshi lang="c"> | ||
+ | #include <stdio.h> | ||
+ | |||
+ | void b(int x); | ||
+ | |||
+ | void a() | ||
+ | { | ||
+ | // . . . ละไว้ | ||
+ | } | ||
+ | // . . . ละไว้ | ||
+ | </geshi> | ||
+ | |||
+ | เมื่อสั่งคอมไพล์เราจะพบว่าคอมไพเลอร์แสดงข้อผิดพลาดดังนี้ | ||
+ | |||
+ | $ gcc a.c b.c | ||
+ | a.c: In function ‘c’: | ||
+ | a.c:12: error: too few arguments to function ‘b’ | ||
+ | |||
+ | ดังนั้นเพื่อรับประกันว่าการเรียกใช้ฟังก์ชันนั้นถูกต้อง เราจึงต้องมีการประกาศฟังก์ชันไว้ก่อนที่จะใช้ นี่เป็นอีกสาเหตุหนึ่งที่เราต้อง include แฟ้ม <tt>stdio.h</tt> | ||
+ | |||
+ | อย่างไรก็ตาม คนที่จะประกาศฟังก์ชันได้ถูกต้องที่สุด คือคนที่เขียนฟังก์ชันนั้นเอง ดังนั้นโดยทั่วไป เราจะพบว่าในการเขียนโปรแกรมภาษาซีที่แยกเป็นส่วน ๆ จะมีการเขียนแฟ้มหัว (header) ประกอบไปกับแฟ้มโปรแกรม (นามสกุล .c) ที่แฟ้มหัวดังกล่าว ก็จะมีแค่การประกาศฟังก์ชันเท่านั้น (ไม่ต้องมีส่วนนิยามฟังก์ชัน) จากตัวอย่างข้างต้น เราจะเขียนแฟ้ม <tt>b.h</tt> ดังนี้ | ||
+ | |||
+ | '''แฟ้ม b.h''' | ||
+ | <geshi lang="c"> | ||
+ | void b(int x); // สังเกตว่าประกาศว่ามีฟังก์ชันนี้เท่านั้น ไม่ได้นิยาม | ||
+ | </geshi> | ||
+ | แล้ว include ในแฟ้ม a.c ดังนี้ | ||
+ | '''แฟ้ม a.c''' | ||
+ | <geshi lang="c"> | ||
+ | #include "b.h" // ใส่ในเครื่องหมายคำพูดเพื่อบอกว่าแฟ้มนี้อยู่ในตำแหน่งเดียวกัน ไม่ใช่อยู่ในไดเร็กทรอรีมาตรฐาน | ||
+ | #include <stdio.h> | ||
+ | </geshi> | ||
+ | |||
+ | === การใช้ตัวแปรร่วมกันระหว่างสองแฟ้ม === | ||
+ | |||
+ | การใช้ตัวแปรร่วมกันระหว่างหลายแฟ้มโปรแกรมตัวแปรก็สามารถทำได้ในลักษณะเดียวกับฟังก์ชัน นั่นคือ ตัวแปรจะต้องถูกประกาศในทุก ๆ แฟ้ม แต่มีตัวตน (มีการจองเนื้อที่) อยู่แค่ในแฟ้มวัตถุเดียว อย่างไรก็ตาม ปกติตัวแปรจะมีตัวตนเมื่อประกาศ ดังนั้นเราจะระบุคำขยายกับตัวแปรว่า <tt>extern</tt> เพื่อบอกว่า ตัวแปรนี้มีตัวตนอยู่ด้านนอก | ||
+ | |||
+ | สมมติว่าเรามีแฟ้มโปรแกรม <tt>a.c</tt>, <tt>b.c</tt>, และ <tt>c.c</tt> หากเราต้องการใช้ตัวแปร <tt>myglobal</tt> ร่วมกัน เราสามารถประกาศให้ตัวแปรมีตัวตนอยู่ในแฟ้มวัตถุ <tt>b.o</tt> ได้ดังนี้ | ||
+ | |||
+ | '''ในแฟ้ม a.c''' | ||
+ | <geshi lang="c"> | ||
+ | extern int myglobal; | ||
+ | </geshi> | ||
+ | |||
+ | '''ในแฟ้ม b.c''' | ||
+ | <geshi lang="c"> | ||
+ | int myglobal; | ||
+ | </geshi> | ||
+ | |||
+ | '''ในแฟ้ม c.c''' | ||
+ | <geshi lang="c"> | ||
+ | extern int myglobal; | ||
+ | </geshi> | ||
+ | |||
+ | === การประกาศไม่ให้ส่งออก: static === | ||
+ | |||
+ | ฟังก์ชันและตัวแปร global ทั้งหมดที่เราประกาศจะถูกส่งออกเสมอ ดังนั้นอีกปัญหาที่พบก็คือ มีการใช้ชื่อฟังก์ชันที่ต้องการใช้ภายในแฟ้มโปรแกรมซ้ำกันโดยไม่ได้ตั้งใจ ซึ่งจะพบเมื่อนำแฟ้มวัตถุมาลิงก์กัน เราสามารถหลีกเลี่ยงปัญหาดังกล่าวได้โดยประกาศให้ฟังก์ชันหรือตัวแปรเหล่านั้นเป็นฟังก์ชันหรือตัวแปรภายใน โดยระบุว่าเป็น <tt>static</tt> | ||
+ | |||
+ | ตัวอย่างเช่นด้านล่าง มีแฟ้มโปรแกรม <tt>a.c</tt>, <tt>b.c</tt>, และ <tt>c.c</tt> | ||
+ | |||
+ | '''แฟ้ม a.c''' | ||
+ | <geshi lang="c"> | ||
+ | #include "b.h" // มีการประกาศ extern int acounter ในนี้ | ||
+ | void cutstr(char *st) { /* . . . */ } | ||
+ | </geshi> | ||
+ | |||
+ | '''แฟ้ม b.c''' | ||
+ | <geshi lang="c"> | ||
+ | int acounter; | ||
+ | static void cutstr() { /* . . . */ } | ||
+ | </geshi> | ||
+ | |||
+ | '''แฟ้ม c.c''' | ||
+ | <geshi lang="c"> | ||
+ | #include "a.h" // มีการประกาศ cutstr ไว้ในนี้ | ||
+ | static int acounter; | ||
+ | </geshi> | ||
+ | |||
+ | ผลจากการสร้างแฟ้มวัตถุจะได้ว่าแต่ละแฟ้มมีข้อมูลดังนี้ | ||
+ | |||
+ | a.o b.o c.o | ||
+ | --------------- -------------- -------------- | ||
+ | has: cutstr has: acounter has: - | ||
+ | need: acounter need: - need: cutstr | ||
+ | |||
+ | ซึ่งสามารถลิงก์รวมกันได้โดยไม่มีปัญหา | ||
+ | |||
+ | === การป้องกันการประกาศซ้ำ === | ||
+ | |||
+ | แฟ้มหัวของเราสามารถถูก include ได้จากโปรแกรมและแฟ้มหัวอื่น ๆ และบางทีอาจถูก include ซ้ำได้หลายครั้งในแฟ้มโปรแกรมเดียวกัน ซึ่งจะก่อให้เกิดข้อผิดพลาดเนื่องจากฟังก์ชันอาจถูกประกาศซ้ำหลายครั้ง | ||
+ | |||
+ | ดังนั้นในการเขียนแฟ้มหัว เรามักใช้ preprocessor ในการช่วยป้องกันการประกาศซ้ำ ดังตัวอย่างด้านล่าง | ||
+ | |||
+ | '''แฟ้ม b.h''' | ||
+ | <geshi lang="c"> | ||
+ | #ifndef B_H_INCLUDED | ||
+ | #define B_H_INCLUDED | ||
+ | |||
+ | // declare your functions here | ||
+ | |||
+ | #endif | ||
+ | </geshi> | ||
+ | |||
+ | คู่ <tt>ifndef-endif</tt> จะป้องกันไม่ให้ส่วนด้านในโดนเรียกมากกว่าหนึ่งครั้ง (ทำไม??) | ||
+ | |||
+ | === หลักการทั่วไปโดยสรุป === | ||
+ | ดังนั้นวิธีการเขียนโดยทั่วไปเราจะทำดังนี้ | ||
+ | |||
+ | พิจารณาแฟ้มโปรแกรม <tt>sub1.c</tt> | ||
+ | |||
+ | # เราจะเขียนโปรแกรมและนิยามฟังก์ชันและตัวแปรต่าง ๆ ในแฟ้มนามสกุล .c (เช่น <tt>sub1.c</tt>) | ||
+ | # สำหรับฟังก์ชันที่สามารถเรียกใช้จากแฟ้มอื่น ๆ ได้ เราจะประกาศไว้ในแฟ้มนามสกุล .h (เช่น <tt>sub1.h</tt>) | ||
+ | # สำหรับตัวแปรที่สามารถเรียกใช้จากแฟ้มอื่น ๆ ได้ เราจะประกาศแบบ extern ไว้ในแฟ้มนามสกุล .h | ||
+ | # สำหรับตัวแปรและฟังก์ชันที่ไม่ต้องการส่งออก ให้ประกาศด้วย <tt>static</tt> | ||
+ | # โปรแกรมอื่น ๆ ที่มีการเรียกใช้ฟังก์ชันที่เขียนนี้ ให้ include แฟ้มนามสกุล .h ดังกล่าว | ||
+ | # เพื่อป้องกันการประกาศผิดพลาด ในแฟ้ม .c เอง ก็ควร include แฟ้มนามสกุล .h ของตัวเองด้วย (นั่นคือ <tt>sub1.c</tt> ควร include <tt>sub1.h</tt>) | ||
+ | # เพื่อป้องกันการประกาศซ้ำเนื่องจากมีการ include หลายครั้ง ควรครอบการประกาศด้วย <tt>ifndef-endif</tt> ดังตัวอย่างด้านบน | ||
+ | |||
+ | === ข้อผิดพลาดที่พบบ่อย === | ||
+ | |||
+ | ข้อผิดพลาดที่พบบ่อยคือในการนิยามฟังก์ชันที่มีการเรียกใช้จากหลาย ๆ แฟ้ม ลงในแฟ้ม .h เพื่อ include โดยตรงเลย ยกตัวอย่างเช่น | ||
+ | |||
+ | '''แฟ้ม c.h''' | ||
+ | <geshi lang="c"> | ||
+ | #include <stdio.h> | ||
+ | void common() | ||
+ | { | ||
+ | printf("This is common function\n"); | ||
+ | } | ||
+ | </geshi> | ||
+ | |||
+ | แล้วก็ include ลงในทุกแฟ้มที่เรียกใช้ฟังก์ชัน <tt>common</tt> เช่นในแฟ้ม <tt>a.c</tt> และ <tt>b.c</tt> จะพบปัญหาว่าเมื่อสั่งคอมไพล์ | ||
+ | |||
+ | gcc a.c b.c | ||
+ | |||
+ | จะได้ข้อผิดพลาดดังนี้ | ||
+ | |||
+ | /tmp/ccfIypDf.o: In function `common': | ||
+ | b.c:(.text+0x0): multiple definition of `common' | ||
+ | /tmp/ccQGRY8R.o:a.c:(.text+0x0): first defined here | ||
+ | collect2: ld returned 1 exit status | ||
+ | |||
+ | เพราะว่าแฟ้มวัตถุทั้งสองมีข้อมูลการเชื่อมโยงดังนี้ | ||
+ | |||
+ | a.o b.o | ||
+ | ------------------ -------------------- | ||
+ | has: common, etc. has: common, etc | ||
+ | |||
+ | และทั้งสองนิยามชนกันเมื่อถูกลิงก์ | ||
+ | |||
+ | ดังนั้นข้อควรสังเกตคือ | ||
+ | |||
+ | วัตถุสามารถถูกประกาศได้หลายครั้ง (ไม่เกินหนึ่งครั้งในแต่ละแฟ้ม) แต่ทั้งโปรแกรม (รวมทุกแฟ้มแล้ว) สามารถมีได้นิยามเดียว | ||
+ | |||
+ | === การลิงก์ระหว่างโปรแกรมภาษา C กับโปรแกรมภาษา C++ === | ||
+ | |||
+ | โปรแกรมภาษา C++ มีรูปแบบการสร้างชื่อฟังก์ชันเพื่อส่งออกและแสดงการอ้างอิงในแฟ้มวัตถุแตกต่างจากในภาษา C เนื่องจากใน C++ ฟังก์ชันหนึ่ง ๆ สามารถมีได้หลายการนิยาม (overloading) ทำให้ต้องมีการระบุประเภทของข้อมูลด้วย เพื่อให้โปรแกรมภาษา C++ สามารถใช้ฟังก์ชันที่ประกาศในแฟ้มวัตถุภาษา C ได้ เราจำเป็นต้อง ครอบการประกาศฟังก์ชันดังกล่าวด้วย <tt>extern "C" {...}</tt> รายละเอียดของการเขียนจะเพิ่มในโอกาสต่อไป |
รุ่นแก้ไขปัจจุบันเมื่อ 12:47, 10 พฤศจิกายน 2556
เอกสารนี้เกี่ยวข้องกับการโปรแกรมภาษาซี โดยออกแบบสำหรับผู้มีความรู้พื้นฐานการโปรแกรมในภาษาตระกูล java และ c# มาแล้ว
<geshi lang="c">
- include <stdio.h>
main() {
printf("Hello, world.\n");
} </geshi>
เนื้อหา
พอยน์เตอร์ (Pointers)
โปรแกรมภาษาซีมองหน่วยความจำเป็นตาราง แต่ละหน่วยย่อยของหน่วยความจำจะมีตำแหน่งระบุอยู่ ไล่เรียงกันไป หน่วยย่อยสุดของการอ้างถึงหน่วยความจำคือไบต์
พอยน์เตอร์เป็นตัวแปรที่ใช้เก็บตำแหน่งในหน่วยความจำ หรือเรียกว่าตัวแปรพอยน์เตอร์ ชี้ ไปยังตำแหน่งที่มันเก็บอยู่
อย่างไรก็ตามเนื่องจากการชี้ไปยังหน่วยความจำตำแหน่งใด ๆ โดยไม่ระบุประเภทข้อมูลที่เก็บอยู่ที่จุดนั้นไม่เพียงพอในการประมวลผล โดยทั่วไปแล้วการประกาศพอยน์เตอร์จำเป็นจะต้องระบุประเภทข้อมูลที่ตัวแปรนั้นชี้ไปด้วย
การประกาศตัวแปรแบบพอยน์เตอร์ทำได้โดยการใส่ * หน้าชื่อตัวแปร เช่นการเขียน int *a; คือการประกาศให้ตัวแปร a เป็นตัวแปรพอยน์เตอร์ไปยังตำแหน่งข้อมูลที่เก็บข้อมูลประเภท int
เมื่อเรามีตัวแปรพอยน์เตอร์แล้ว การอ้างถึง ข้อมูล ที่ตัวแปรนั้นชี้อยู่ ทำได้โดยใช้ตัวดำเนินการ * ใส่ด้านหน้า ในทางกลับกัน การหาตำแหน่งในหน่วยความจำจากตัวแปร (หรือข้อมูล) ทำได้โดยใช้ตัวดำเนินการ & พิจารณาโปรแกรมด้านล่าง
<geshi lang="c">
- include <stdio.h>
main() {
int a = 10; int b = 20; int *c; printf("%d, %d\n",a,b); c = &a; *c = 30; printf("%d, %d\n",a,b); c = &b; *c = 40; printf("%d, %d\n",a,b);
} </geshi>
ให้ผลลัพธ์เป็น
10, 20 30, 20 30, 40
พอยน์เตอร์มีประโยชน์มากในการเขียนฟังก์ชันให้มีผลข้างเคียง (side effect) ตัวอย่างเช่นฟังก์ชัน swap ด้านล่าง
<geshi lang="c"> void swap(int *a, int *b) {
int tmp = *a; *a = *b; *b = tmp;
} </geshi>
เนื่องจากตัวแปรแบบพอยน์เตอร์เป็นตัวแปรที่อยู่ในหน่วยความจำ ตัวแปรเราจึงมีตัวแปรพอยน์เตอร์ที่ชี้ไปยังข้อมูลแบบพอยน์เตอร์ได้ พิจารณาส่วนของโปรแกรมต่อไปนี้
<geshi lang="c">
int a = 10, b = 20; int *c = &a; int **d = &c; *c = 30; // ตัวแปร a เปลี่ยนค่าเป็น 30 *d = &b; // ตอนนี้ c ชี้ไปที่ b *c = 100; // ตัวแปร b เปลี่ยนค่าเป็น 100 **d = 200; // **d = *(*d) = *c นั่นคือ หลังคำสั่งนี้ ตัวแปร b เปลี่ยนค่าเป็น 200
</geshi>
การอ่านและเขียนผลลัพธ์
- สำหรับการอ่านสตริง ดูในส่วนของการประมวลผลสตริง (ด้านล่าง)
คำสั่งพื้นฐานของภาษา C ในการอ่านและเขียนผลลัพธ์คือ scanf และ printf ซึ่งประกาศอยู่ในไฟล์หัว stdio.h
ในการอ่านและแสดงผลลัพธ์นั้น ฟังก์ชันทั้งสองจะพิจารณาข้อมูลและตัวแปรที่รับมาตามประเภทข้อมูลที่ระบุใน format string (ซึ่งระบุเป็นอาร์กิวเมนต์แรก)
พิจารณาส่วนของโปรแกรมต่อไปนี้
<geshi lang="c">
int a; float b; scanf("%d %f", &a, &b); printf("a plus 100 is %d. b divided by 2 is %f.\n", a+100, b/2);
</geshi>
สังเกตว่าในตัวอย่างข้างต้นฟังก์ชัน scanf นั้นรับอาร์กิวเมนต์เป็นตำแหน่งของตัวแปร a และ b อย่างที่เกริ่นไว้ ถ้าเราไม่ได้ระบุชนิดข้อมูล (%d สำหรับ int หรือ %d สำหรับ float) ฟังก์ชัน scanf ก็จะไม่ทราบว่าจะจัดการกับข้อมูลที่อ่านเข้ามาอย่างไร
ในการอ่านข้อมูลด้วยฟังก์ชัน scanf นั้น ฟังก์ชันดังกล่าวจะอ่านข้ามช่องว่างและบรรทัดใหม่ให้เสมอทำให้สะดวกเวลาอ่านข้อมูลหลาย ๆ ตัว (แต่อาจมีปัญหาบ้างถ้าต้องการอ่าน string ที่มีช่องว่างอยู่ด้วย)
ความผิดพลาดที่เกิดขึ้นบ่อยเวลาใช้ฟังก์ชัน scanf ก็คือการลืมใส่ & เพื่อระบุตำแหน่ง เช่นดังตัวอย่างด้านล่าง
<geshi lang="c">
int a; scanf("%d", a); // ถ้า a มีค่า 0, scanf จะพยายามเขียนข้อมูลลงในตำแหน่ง 0
</geshi>
อย่างไรก็ตาม ไม่ใช่ว่าเราจะใส่ & เสมอไป ตัวอย่างเช่น ส่วนของโปรแกรมด้านล่างอ่านข้อมูลเข้าไปที่ตัวแปร a ได้อย่างถูกต้อง
<geshi lang="c">
int a; int *b = &a; scanf("%d", b); // b ชี้ไปที่ a
</geshi>
ด้านล่างเป็นตารางของ format ที่ใช้บ่อย ๆ (รวมทั้งตัวอย่างการระบุการเว้นและทศนิยมสำหรับใช้ในคำสั่ง printf)
format | ประเภทข้อมูล |
%d | int |
%5d | แสดง int แบบ 5 หลัก ชิดขวา |
%f | float |
%5.2f | แสดง float แบบ 5 หลักมีทศนิยม 2 ตำแหน่ง |
%lf | double |
%s | string, เมื่อใช้กับฟังก์ชัน scanf จะถือว่า string ดังกล่าวมีขอบเขตอยู่ที่ช่องว่างหรือบรรทัดใหม่ |
อาร์เรย์และพอยน์เตอร์
<geshi lang="c">
- include <stdio.h>
- include <assert.h>
- define MAX_N 100
int a[MAX_N]; int n;
void read_array(int *np, int a[]) {
int i;
scanf("%d", np); for(i=0; i < *np; i++) scanf("%d", &a[i]);
}
int max_array(int n, int a[]) {
return 0; // edit this
}
void test_max_array() {
int x[] = {1,10,20,3,5}; assert(max_array(5,x)==20); int y[] = {}; assert(max_array(0,x)==0); int z[] = {-10000,-100,-5000}; assert(max_array(3,z)==-100);
}
int sum_array(int n, int a[]) {
return 0; // edit this
}
void test_sum_array() {
int x[] = {}; assert(sum_array(0,x)==0);
int y[] = {1,2,3,4}; assert(sum_array(4,y)==10);
int z[] = {-100}; assert(sum_array(1,z)==-100);
}
main() {
test_max_array();
} </geshi>
โครงสร้าง (struct)
อาร์กิวเมนต์จาก command line
การประมวลผลสตริง
การคอมไพล์และลิงก์โปรแกรมที่อยู่ในหลายแฟ้ม
วิธีการคลาสสิกในการจัดการงานที่ใหญ่ ก็คือการแบ่งงานดังกล่าวเป็นส่วนย่อย ๆ การพัฒนาโปรแกรมก็เช่นเดียวกัน ถ้าโปรแกรมที่เราต้องการพัฒนามีขนาดใหญ่ การแบ่งโปรแกรมดังกล่าวออกเป็นส่วนย่อย ๆ ที่มีขนาดเล็กลงมักช่วยให้พัฒนาและทดสอบความถูกต้องได้ง่ายขึ้น นอกจากนี้ยังมีประโยชน์ที่ช่วยลดเวลาในการคอมไพล์อีกด้วย (โปรแกรมใหญ่ ๆ ถ้าคอมไพล์ทั้งหมดอาจใช้เวลานานมาก)
ในการนำแฟ้มโปรแกรมต้นฉบับหลาย ๆ แฟ้มมารวมกัน เราจำเป็นจะต้องเข้าใจการทำงานพื้นฐานของการคอมไพล์และลิงก์เสียก่อน
แฟ้มเป้าหมายและการลิงก์
พิจารณาโปรแกรมตัวอย่างด้านล่าง สมมติว่าชื่อ square.c
<geshi lang="c">
- include <stdio.h>
main() {
int a; scanf("%d",&a); printf("The square of %d is %d\n", a, a*a);
} </geshi>
เมื่อเราสั่ง
gcc square.c -o square
สิ่งที่เกิดขึ้นระหว่างการคอมไพล์เพื่อสร้างแฟ้ม square ที่ทำงานได้ คือคอมไพเลอร์จะแปลแฟ้ม square.c ให้เป็นแฟ้มวัตถุ (object file) ก่อน ในแฟ้มดังกล่าวจะประกอบไปด้วยรหัสภาษาเครื่องของคำสั่งที่เขียนใน square.c ทั้งหมด อย่างไรก็ตาม สังเกตว่า โปรแกรมดังกล่าวมีการใช้งานฟังก์ชัน scanf และ printf ซึ่งไม่ได้ถูกนิยามไว้ในแฟ้มดังกล่าว (แม้มีการประกาศฟังก์ชันในแฟ้ม stdio.h แต่ไม่มีการระบุนิยามของฟังก์ชันดังกล่าว)
ในขั้นตอนของการสร้างแฟ้มวัตถุ ถ้าโปรแกรมมีการอ้างอิงถึงฟังก์ชันที่อยู่นอกขอบเขตแฟ้ม การอ้างอิงดังกล่าวจะถูก "จด" เอาไว้ เพื่อนำไปค้นหาและเชื่อมโยงต่อในขั้นตอนการลิงก์
เราสามารถสั่งให้ gcc ทำงานเฉพาะขั้นตอนแรกได้โดยสั่ง
gcc -c square.c
เราจะได้แฟ้มผลลัพธ์ชื่อ square.o แฟ้มดังกล่าวจะระบุข้อมูลดังนี้
square.o -------- มี: main ต้องการ: scanf, printf
เมื่อนำแฟ้มวัตถุดังกล่าวที่มีการประกาศ main ไปลิงก์เข้ากับไลบรารีภาษาซี ซึ่งมีการประกาศ scanf และ printf ไว้ ก็จะได้ผลลัพธ์ที่ต้องการ
หมายเหตุ: เราสามารถทดลองดูผลลัพธ์ที่เกิดจากการ "หากันไม่เจอได้" โดยลองเปลี่ยนชื่อฟังก์ชัน main หรือแก้ฟังก์ชัน scanf หรือ printf เป็นชื่ออื่นที่ไม่มีการนิยามในระบบ
การลิงก์เมื่อมีแฟ้มโปรแกรมต้นฉบับหลายแฟ้ม
จากตัวอย่างที่แล้ว ทุก ๆ แฟ้มโปรแกรมต้นฉบับเมื่อแปลงเป็นแฟ้มวัตถุจะมีการประกาศนิยามวัตถุต่าง ๆ เอาไว้ สมมติเรามีแฟ้มโปรแกรมสองแฟ้มดังนี้
แฟ้ม a.c <geshi lang="c"> include <stdio.h>
void a() {
printf("this is a\n");
}
void c() {
b(); printf("this is c\n");
}
main() {
c();
} </geshi>
แฟ้ม b.c <geshi lang="c">
- include <stdio.h>
void b() {
a(); printf("this is b\n");
} </geshi>
เมื่อสั่งคอมไฟล์แฟ้มทั้งสองด้วยคำสั่ง
gcc -c a.c gcc -c b.c
ได้แฟ้มวัตถุ a.o และ b.o มา แต่ละแฟ้มจะมีข้อมูลสำหรับการเชื่อมโยงและการอ้างอิงดังนี้
a.o b.o --------------- ---------------- has: main, a, c has: b want: b want: a
ดังนั้น เมื่อเรานำทั้งสองแฟ้มมาลิงก์รวมกัน การอ้างอิงดังกล่าวก็จะถูกเชื่อมโยงได้เรียบร้อย การลิงก์สามารถทำได้โดยสั่ง
gcc a.o b.o
ผลลัพธ์ที่ได้จะอยู่ในแฟ้ม a.out ตามปกติ ถ้าหากต้องการได้แฟ้มที่ทำงานได้ชื่อ abc เราสามารถใช้ option -o ในการระบุชื่อแฟ้มได้ โดยสั่ง
gcc a.o b.o -o abc
การประกาศฟังก์ชัน
อย่างไรก็ตาม ถ้าเราแก้ไขแฟ้ม b.c ให้เป็นดังนี้
แฟ้ม b.c <geshi lang="c">
- include <stdio.h>
void b(int x) /* เปลี่ยนการประกาศ */ {
int i;
for(i=0; i<x; i++) a(); printf("this is b\n");
} </geshi>
เราจะพบว่า เรายังสามารถรวมแฟ้มโปรแกรม a.c และ b.c เข้าด้วยกันได้เช่นเดิม อย่างไรก็ตาม เราพบว่าการเรียกใช้ฟังก์ชัน b ใน a.c นั้นผิดพลาด เพราะว่ามีการเรียกใช้ฟังก์ชันโดยส่งค่าผิด
สังเกตว่าระหว่างที่คอมไพเลอร์แปลโปรแกรม a.c นั้น คอมไพเลอร์ไม่มีข้อมูลของฟังก์ชัน b อยู่เลย ทำให้ไม่ทราบว่าการเรียกใช้นั้นผิดพลาด
ความผิดพลาดเช่นนี้สามารถป้องกันได้โดยใส่การประกาศฟังก์ชัน b ไว้ก่อนที่ตอนต้นของโปรแกรม a.c ดังนี้
<geshi lang="c">
- include <stdio.h>
void b(int x);
void a() {
// . . . ละไว้
} // . . . ละไว้ </geshi>
เมื่อสั่งคอมไพล์เราจะพบว่าคอมไพเลอร์แสดงข้อผิดพลาดดังนี้
$ gcc a.c b.c a.c: In function ‘c’: a.c:12: error: too few arguments to function ‘b’
ดังนั้นเพื่อรับประกันว่าการเรียกใช้ฟังก์ชันนั้นถูกต้อง เราจึงต้องมีการประกาศฟังก์ชันไว้ก่อนที่จะใช้ นี่เป็นอีกสาเหตุหนึ่งที่เราต้อง include แฟ้ม stdio.h
อย่างไรก็ตาม คนที่จะประกาศฟังก์ชันได้ถูกต้องที่สุด คือคนที่เขียนฟังก์ชันนั้นเอง ดังนั้นโดยทั่วไป เราจะพบว่าในการเขียนโปรแกรมภาษาซีที่แยกเป็นส่วน ๆ จะมีการเขียนแฟ้มหัว (header) ประกอบไปกับแฟ้มโปรแกรม (นามสกุล .c) ที่แฟ้มหัวดังกล่าว ก็จะมีแค่การประกาศฟังก์ชันเท่านั้น (ไม่ต้องมีส่วนนิยามฟังก์ชัน) จากตัวอย่างข้างต้น เราจะเขียนแฟ้ม b.h ดังนี้
แฟ้ม b.h <geshi lang="c"> void b(int x); // สังเกตว่าประกาศว่ามีฟังก์ชันนี้เท่านั้น ไม่ได้นิยาม </geshi> แล้ว include ในแฟ้ม a.c ดังนี้ แฟ้ม a.c <geshi lang="c">
- include "b.h" // ใส่ในเครื่องหมายคำพูดเพื่อบอกว่าแฟ้มนี้อยู่ในตำแหน่งเดียวกัน ไม่ใช่อยู่ในไดเร็กทรอรีมาตรฐาน
- include <stdio.h>
</geshi>
การใช้ตัวแปรร่วมกันระหว่างสองแฟ้ม
การใช้ตัวแปรร่วมกันระหว่างหลายแฟ้มโปรแกรมตัวแปรก็สามารถทำได้ในลักษณะเดียวกับฟังก์ชัน นั่นคือ ตัวแปรจะต้องถูกประกาศในทุก ๆ แฟ้ม แต่มีตัวตน (มีการจองเนื้อที่) อยู่แค่ในแฟ้มวัตถุเดียว อย่างไรก็ตาม ปกติตัวแปรจะมีตัวตนเมื่อประกาศ ดังนั้นเราจะระบุคำขยายกับตัวแปรว่า extern เพื่อบอกว่า ตัวแปรนี้มีตัวตนอยู่ด้านนอก
สมมติว่าเรามีแฟ้มโปรแกรม a.c, b.c, และ c.c หากเราต้องการใช้ตัวแปร myglobal ร่วมกัน เราสามารถประกาศให้ตัวแปรมีตัวตนอยู่ในแฟ้มวัตถุ b.o ได้ดังนี้
ในแฟ้ม a.c <geshi lang="c"> extern int myglobal; </geshi>
ในแฟ้ม b.c <geshi lang="c"> int myglobal; </geshi>
ในแฟ้ม c.c <geshi lang="c"> extern int myglobal; </geshi>
การประกาศไม่ให้ส่งออก: static
ฟังก์ชันและตัวแปร global ทั้งหมดที่เราประกาศจะถูกส่งออกเสมอ ดังนั้นอีกปัญหาที่พบก็คือ มีการใช้ชื่อฟังก์ชันที่ต้องการใช้ภายในแฟ้มโปรแกรมซ้ำกันโดยไม่ได้ตั้งใจ ซึ่งจะพบเมื่อนำแฟ้มวัตถุมาลิงก์กัน เราสามารถหลีกเลี่ยงปัญหาดังกล่าวได้โดยประกาศให้ฟังก์ชันหรือตัวแปรเหล่านั้นเป็นฟังก์ชันหรือตัวแปรภายใน โดยระบุว่าเป็น static
ตัวอย่างเช่นด้านล่าง มีแฟ้มโปรแกรม a.c, b.c, และ c.c
แฟ้ม a.c <geshi lang="c">
- include "b.h" // มีการประกาศ extern int acounter ในนี้
void cutstr(char *st) { /* . . . */ } </geshi>
แฟ้ม b.c <geshi lang="c"> int acounter; static void cutstr() { /* . . . */ } </geshi>
แฟ้ม c.c <geshi lang="c">
- include "a.h" // มีการประกาศ cutstr ไว้ในนี้
static int acounter; </geshi>
ผลจากการสร้างแฟ้มวัตถุจะได้ว่าแต่ละแฟ้มมีข้อมูลดังนี้
a.o b.o c.o --------------- -------------- -------------- has: cutstr has: acounter has: - need: acounter need: - need: cutstr
ซึ่งสามารถลิงก์รวมกันได้โดยไม่มีปัญหา
การป้องกันการประกาศซ้ำ
แฟ้มหัวของเราสามารถถูก include ได้จากโปรแกรมและแฟ้มหัวอื่น ๆ และบางทีอาจถูก include ซ้ำได้หลายครั้งในแฟ้มโปรแกรมเดียวกัน ซึ่งจะก่อให้เกิดข้อผิดพลาดเนื่องจากฟังก์ชันอาจถูกประกาศซ้ำหลายครั้ง
ดังนั้นในการเขียนแฟ้มหัว เรามักใช้ preprocessor ในการช่วยป้องกันการประกาศซ้ำ ดังตัวอย่างด้านล่าง
แฟ้ม b.h <geshi lang="c">
- ifndef B_H_INCLUDED
- define B_H_INCLUDED
// declare your functions here
- endif
</geshi>
คู่ ifndef-endif จะป้องกันไม่ให้ส่วนด้านในโดนเรียกมากกว่าหนึ่งครั้ง (ทำไม??)
หลักการทั่วไปโดยสรุป
ดังนั้นวิธีการเขียนโดยทั่วไปเราจะทำดังนี้
พิจารณาแฟ้มโปรแกรม sub1.c
- เราจะเขียนโปรแกรมและนิยามฟังก์ชันและตัวแปรต่าง ๆ ในแฟ้มนามสกุล .c (เช่น sub1.c)
- สำหรับฟังก์ชันที่สามารถเรียกใช้จากแฟ้มอื่น ๆ ได้ เราจะประกาศไว้ในแฟ้มนามสกุล .h (เช่น sub1.h)
- สำหรับตัวแปรที่สามารถเรียกใช้จากแฟ้มอื่น ๆ ได้ เราจะประกาศแบบ extern ไว้ในแฟ้มนามสกุล .h
- สำหรับตัวแปรและฟังก์ชันที่ไม่ต้องการส่งออก ให้ประกาศด้วย static
- โปรแกรมอื่น ๆ ที่มีการเรียกใช้ฟังก์ชันที่เขียนนี้ ให้ include แฟ้มนามสกุล .h ดังกล่าว
- เพื่อป้องกันการประกาศผิดพลาด ในแฟ้ม .c เอง ก็ควร include แฟ้มนามสกุล .h ของตัวเองด้วย (นั่นคือ sub1.c ควร include sub1.h)
- เพื่อป้องกันการประกาศซ้ำเนื่องจากมีการ include หลายครั้ง ควรครอบการประกาศด้วย ifndef-endif ดังตัวอย่างด้านบน
ข้อผิดพลาดที่พบบ่อย
ข้อผิดพลาดที่พบบ่อยคือในการนิยามฟังก์ชันที่มีการเรียกใช้จากหลาย ๆ แฟ้ม ลงในแฟ้ม .h เพื่อ include โดยตรงเลย ยกตัวอย่างเช่น
แฟ้ม c.h <geshi lang="c">
- include <stdio.h>
void common() {
printf("This is common function\n");
} </geshi>
แล้วก็ include ลงในทุกแฟ้มที่เรียกใช้ฟังก์ชัน common เช่นในแฟ้ม a.c และ b.c จะพบปัญหาว่าเมื่อสั่งคอมไพล์
gcc a.c b.c
จะได้ข้อผิดพลาดดังนี้
/tmp/ccfIypDf.o: In function `common': b.c:(.text+0x0): multiple definition of `common' /tmp/ccQGRY8R.o:a.c:(.text+0x0): first defined here collect2: ld returned 1 exit status
เพราะว่าแฟ้มวัตถุทั้งสองมีข้อมูลการเชื่อมโยงดังนี้
a.o b.o ------------------ -------------------- has: common, etc. has: common, etc
และทั้งสองนิยามชนกันเมื่อถูกลิงก์
ดังนั้นข้อควรสังเกตคือ
วัตถุสามารถถูกประกาศได้หลายครั้ง (ไม่เกินหนึ่งครั้งในแต่ละแฟ้ม) แต่ทั้งโปรแกรม (รวมทุกแฟ้มแล้ว) สามารถมีได้นิยามเดียว
การลิงก์ระหว่างโปรแกรมภาษา C กับโปรแกรมภาษา C++
โปรแกรมภาษา C++ มีรูปแบบการสร้างชื่อฟังก์ชันเพื่อส่งออกและแสดงการอ้างอิงในแฟ้มวัตถุแตกต่างจากในภาษา C เนื่องจากใน C++ ฟังก์ชันหนึ่ง ๆ สามารถมีได้หลายการนิยาม (overloading) ทำให้ต้องมีการระบุประเภทของข้อมูลด้วย เพื่อให้โปรแกรมภาษา C++ สามารถใช้ฟังก์ชันที่ประกาศในแฟ้มวัตถุภาษา C ได้ เราจำเป็นต้อง ครอบการประกาศฟังก์ชันดังกล่าวด้วย extern "C" {...} รายละเอียดของการเขียนจะเพิ่มในโอกาสต่อไป