การโปรแกรมภาษาซี สำหรับโปรแกรมเมอร์จาวาและซีชาร์ป

จาก Theory Wiki
ข้ามไปที่: นำทาง, สืบค้น

เอกสารนี้เกี่ยวข้องกับการโปรแกรมภาษาซี โดยออกแบบสำหรับผู้มีความรู้พื้นฐานการโปรแกรมในภาษาตระกูล java และ c# มาแล้ว

UNIQ3ed280863d651b8c-geshi-00000000-QINU

เนื้อหา

พอยน์เตอร์ (Pointers)

โปรแกรมภาษาซีมองหน่วยความจำเป็นตาราง แต่ละหน่วยย่อยของหน่วยความจำจะมีตำแหน่งระบุอยู่ ไล่เรียงกันไป หน่วยย่อยสุดของการอ้างถึงหน่วยความจำคือไบต์

พอยน์เตอร์เป็นตัวแปรที่ใช้เก็บตำแหน่งในหน่วยความจำ หรือเรียกว่าตัวแปรพอยน์เตอร์ ชี้ ไปยังตำแหน่งที่มันเก็บอยู่

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

การประกาศตัวแปรแบบพอยน์เตอร์ทำได้โดยการใส่ * หน้าชื่อตัวแปร เช่นการเขียน int *a; คือการประกาศให้ตัวแปร a เป็นตัวแปรพอยน์เตอร์ไปยังตำแหน่งข้อมูลที่เก็บข้อมูลประเภท int

เมื่อเรามีตัวแปรพอยน์เตอร์แล้ว การอ้างถึง ข้อมูล ที่ตัวแปรนั้นชี้อยู่ ทำได้โดยใช้ตัวดำเนินการ * ใส่ด้านหน้า ในทางกลับกัน การหาตำแหน่งในหน่วยความจำจากตัวแปร (หรือข้อมูล) ทำได้โดยใช้ตัวดำเนินการ & พิจารณาโปรแกรมด้านล่าง

UNIQ3ed280863d651b8c-geshi-00000001-QINU

ให้ผลลัพธ์เป็น

10, 20
30, 20
30, 40

พอยน์เตอร์มีประโยชน์มากในการเขียนฟังก์ชันให้มีผลข้างเคียง (side effect) ตัวอย่างเช่นฟังก์ชัน swap ด้านล่าง

UNIQ3ed280863d651b8c-geshi-00000002-QINU

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

UNIQ3ed280863d651b8c-geshi-00000003-QINU

การอ่านและเขียนผลลัพธ์

สำหรับการอ่านสตริง ดูในส่วนของการประมวลผลสตริง (ด้านล่าง)

คำสั่งพื้นฐานของภาษา C ในการอ่านและเขียนผลลัพธ์คือ scanf และ printf ซึ่งประกาศอยู่ในไฟล์หัว stdio.h

ในการอ่านและแสดงผลลัพธ์นั้น ฟังก์ชันทั้งสองจะพิจารณาข้อมูลและตัวแปรที่รับมาตามประเภทข้อมูลที่ระบุใน format string (ซึ่งระบุเป็นอาร์กิวเมนต์แรก)

พิจารณาส่วนของโปรแกรมต่อไปนี้

UNIQ3ed280863d651b8c-geshi-00000004-QINU

สังเกตว่าในตัวอย่างข้างต้นฟังก์ชัน scanf นั้นรับอาร์กิวเมนต์เป็นตำแหน่งของตัวแปร a และ b อย่างที่เกริ่นไว้ ถ้าเราไม่ได้ระบุชนิดข้อมูล (%d สำหรับ int หรือ %d สำหรับ float) ฟังก์ชัน scanf ก็จะไม่ทราบว่าจะจัดการกับข้อมูลที่อ่านเข้ามาอย่างไร

ในการอ่านข้อมูลด้วยฟังก์ชัน scanf นั้น ฟังก์ชันดังกล่าวจะอ่านข้ามช่องว่างและบรรทัดใหม่ให้เสมอทำให้สะดวกเวลาอ่านข้อมูลหลาย ๆ ตัว (แต่อาจมีปัญหาบ้างถ้าต้องการอ่าน string ที่มีช่องว่างอยู่ด้วย)

ความผิดพลาดที่เกิดขึ้นบ่อยเวลาใช้ฟังก์ชัน scanf ก็คือการลืมใส่ & เพื่อระบุตำแหน่ง เช่นดังตัวอย่างด้านล่าง

UNIQ3ed280863d651b8c-geshi-00000005-QINU

อย่างไรก็ตาม ไม่ใช่ว่าเราจะใส่ & เสมอไป ตัวอย่างเช่น ส่วนของโปรแกรมด้านล่างอ่านข้อมูลเข้าไปที่ตัวแปร a ได้อย่างถูกต้อง

UNIQ3ed280863d651b8c-geshi-00000006-QINU

ด้านล่างเป็นตารางของ format ที่ใช้บ่อย ๆ (รวมทั้งตัวอย่างการระบุการเว้นและทศนิยมสำหรับใช้ในคำสั่ง printf)

formatประเภทข้อมูล
%dint
%5dแสดง int แบบ 5 หลัก ชิดขวา
%ffloat
%5.2fแสดง float แบบ 5 หลักมีทศนิยม 2 ตำแหน่ง
%lfdouble
%sstring, เมื่อใช้กับฟังก์ชัน scanf จะถือว่า string ดังกล่าวมีขอบเขตอยู่ที่ช่องว่างหรือบรรทัดใหม่

อาร์เรย์และพอยน์เตอร์

UNIQ3ed280863d651b8c-geshi-00000007-QINU

โครงสร้าง (struct)

อาร์กิวเมนต์จาก command line

การประมวลผลสตริง

การคอมไพล์และลิงก์โปรแกรมที่อยู่ในหลายแฟ้ม

วิธีการคลาสสิกในการจัดการงานที่ใหญ่ ก็คือการแบ่งงานดังกล่าวเป็นส่วนย่อย ๆ การพัฒนาโปรแกรมก็เช่นเดียวกัน ถ้าโปรแกรมที่เราต้องการพัฒนามีขนาดใหญ่ การแบ่งโปรแกรมดังกล่าวออกเป็นส่วนย่อย ๆ ที่มีขนาดเล็กลงมักช่วยให้พัฒนาและทดสอบความถูกต้องได้ง่ายขึ้น นอกจากนี้ยังมีประโยชน์ที่ช่วยลดเวลาในการคอมไพล์อีกด้วย (โปรแกรมใหญ่ ๆ ถ้าคอมไพล์ทั้งหมดอาจใช้เวลานานมาก)

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

แฟ้มเป้าหมายและการลิงก์

พิจารณาโปรแกรมตัวอย่างด้านล่าง สมมติว่าชื่อ square.c

UNIQ3ed280863d651b8c-geshi-00000008-QINU

เมื่อเราสั่ง

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 UNIQ3ed280863d651b8c-geshi-00000009-QINU

แฟ้ม b.c UNIQ3ed280863d651b8c-geshi-0000000A-QINU

เมื่อสั่งคอมไฟล์แฟ้มทั้งสองด้วยคำสั่ง

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 UNIQ3ed280863d651b8c-geshi-0000000B-QINU

เราจะพบว่า เรายังสามารถรวมแฟ้มโปรแกรม a.c และ b.c เข้าด้วยกันได้เช่นเดิม อย่างไรก็ตาม เราพบว่าการเรียกใช้ฟังก์ชัน b ใน a.c นั้นผิดพลาด เพราะว่ามีการเรียกใช้ฟังก์ชันโดยส่งค่าผิด

สังเกตว่าระหว่างที่คอมไพเลอร์แปลโปรแกรม a.c นั้น คอมไพเลอร์ไม่มีข้อมูลของฟังก์ชัน b อยู่เลย ทำให้ไม่ทราบว่าการเรียกใช้นั้นผิดพลาด

ความผิดพลาดเช่นนี้สามารถป้องกันได้โดยใส่การประกาศฟังก์ชัน b ไว้ก่อนที่ตอนต้นของโปรแกรม a.c ดังนี้

UNIQ3ed280863d651b8c-geshi-0000000C-QINU

เมื่อสั่งคอมไพล์เราจะพบว่าคอมไพเลอร์แสดงข้อผิดพลาดดังนี้

$ 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 UNIQ3ed280863d651b8c-geshi-0000000D-QINU แล้ว include ในแฟ้ม a.c ดังนี้ แฟ้ม a.c UNIQ3ed280863d651b8c-geshi-0000000E-QINU

การใช้ตัวแปรร่วมกันระหว่างสองแฟ้ม

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

สมมติว่าเรามีแฟ้มโปรแกรม a.c, b.c, และ c.c หากเราต้องการใช้ตัวแปร myglobal ร่วมกัน เราสามารถประกาศให้ตัวแปรมีตัวตนอยู่ในแฟ้มวัตถุ b.o ได้ดังนี้

ในแฟ้ม a.c UNIQ3ed280863d651b8c-geshi-0000000F-QINU

ในแฟ้ม b.c UNIQ3ed280863d651b8c-geshi-00000010-QINU

ในแฟ้ม c.c UNIQ3ed280863d651b8c-geshi-00000011-QINU

การประกาศไม่ให้ส่งออก: static

ฟังก์ชันและตัวแปร global ทั้งหมดที่เราประกาศจะถูกส่งออกเสมอ ดังนั้นอีกปัญหาที่พบก็คือ มีการใช้ชื่อฟังก์ชันที่ต้องการใช้ภายในแฟ้มโปรแกรมซ้ำกันโดยไม่ได้ตั้งใจ ซึ่งจะพบเมื่อนำแฟ้มวัตถุมาลิงก์กัน เราสามารถหลีกเลี่ยงปัญหาดังกล่าวได้โดยประกาศให้ฟังก์ชันหรือตัวแปรเหล่านั้นเป็นฟังก์ชันหรือตัวแปรภายใน โดยระบุว่าเป็น static

ตัวอย่างเช่นด้านล่าง มีแฟ้มโปรแกรม a.c, b.c, และ c.c

แฟ้ม a.c UNIQ3ed280863d651b8c-geshi-00000012-QINU

แฟ้ม b.c UNIQ3ed280863d651b8c-geshi-00000013-QINU

แฟ้ม c.c UNIQ3ed280863d651b8c-geshi-00000014-QINU

ผลจากการสร้างแฟ้มวัตถุจะได้ว่าแต่ละแฟ้มมีข้อมูลดังนี้

      a.o                 b.o                c.o
---------------      --------------     --------------
has: cutstr          has: acounter      has: -
need: acounter       need: -            need: cutstr

ซึ่งสามารถลิงก์รวมกันได้โดยไม่มีปัญหา

การป้องกันการประกาศซ้ำ

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

ดังนั้นในการเขียนแฟ้มหัว เรามักใช้ preprocessor ในการช่วยป้องกันการประกาศซ้ำ ดังตัวอย่างด้านล่าง

แฟ้ม b.h UNIQ3ed280863d651b8c-geshi-00000015-QINU

คู่ ifndef-endif จะป้องกันไม่ให้ส่วนด้านในโดนเรียกมากกว่าหนึ่งครั้ง (ทำไม??)

หลักการทั่วไปโดยสรุป

ดังนั้นวิธีการเขียนโดยทั่วไปเราจะทำดังนี้

พิจารณาแฟ้มโปรแกรม sub1.c

  1. เราจะเขียนโปรแกรมและนิยามฟังก์ชันและตัวแปรต่าง ๆ ในแฟ้มนามสกุล .c (เช่น sub1.c)
  2. สำหรับฟังก์ชันที่สามารถเรียกใช้จากแฟ้มอื่น ๆ ได้ เราจะประกาศไว้ในแฟ้มนามสกุล .h (เช่น sub1.h)
  3. สำหรับตัวแปรที่สามารถเรียกใช้จากแฟ้มอื่น ๆ ได้ เราจะประกาศแบบ extern ไว้ในแฟ้มนามสกุล .h
  4. สำหรับตัวแปรและฟังก์ชันที่ไม่ต้องการส่งออก ให้ประกาศด้วย static
  5. โปรแกรมอื่น ๆ ที่มีการเรียกใช้ฟังก์ชันที่เขียนนี้ ให้ include แฟ้มนามสกุล .h ดังกล่าว
  6. เพื่อป้องกันการประกาศผิดพลาด ในแฟ้ม .c เอง ก็ควร include แฟ้มนามสกุล .h ของตัวเองด้วย (นั่นคือ sub1.c ควร include sub1.h)
  7. เพื่อป้องกันการประกาศซ้ำเนื่องจากมีการ include หลายครั้ง ควรครอบการประกาศด้วย ifndef-endif ดังตัวอย่างด้านบน

ข้อผิดพลาดที่พบบ่อย

ข้อผิดพลาดที่พบบ่อยคือในการนิยามฟังก์ชันที่มีการเรียกใช้จากหลาย ๆ แฟ้ม ลงในแฟ้ม .h เพื่อ include โดยตรงเลย ยกตัวอย่างเช่น

แฟ้ม c.h UNIQ3ed280863d651b8c-geshi-00000016-QINU

แล้วก็ 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" {...} รายละเอียดของการเขียนจะเพิ่มในโอกาสต่อไป

เครื่องมือส่วนตัว
สิ่งที่แตกต่าง
การกระทำ
ป้ายบอกทาง
เครื่องมือเพิ่ม