แบบทดสอบภาษาโปรแกรม C
(stefansf.de)- กฎของภาษา C สามารถทำให้แม้แต่โค้ดที่ดูเรียบง่าย เช่น การเปรียบเทียบพอยน์เตอร์, aliasing, นัลพอยน์เตอร์, และค่าที่ยังไม่ได้กำหนดค่าเริ่มต้น กลายเป็นพฤติกรรมที่ไม่ถูกกำหนดได้
- ค่าคงที่จำนวนเต็ม,
sizeof, ค่าคงที่อักขระ, และเลขคณิตของuint8_tอาจให้ผลลัพธ์ต่างกันตามแพลตฟอร์ม, รูปแบบการเขียน, และตำแหน่งของการกำหนดค่าระหว่างทาง เนื่องจาก การเลือกชนิดข้อมูลและ integer promotion foo()กับfoo(void)ในการประกาศฟังก์ชัน, การไม่มี prototype, default argument promotions, และฟังก์ชันที่ไม่มีค่าคืน ล้วนทำให้ C และ C++ แตกต่างกันทั้งในแง่ความถูกต้องตามกฎหมายภาษาและพฤติกรรม- อาร์เรย์ไม่ใช่พอยน์เตอร์ และพารามิเตอร์แบบอาร์เรย์จะถูกปรับเป็นพอยน์เตอร์ โดย
a,&a, และ&a[0]แม้จะมีแอดเดรสเดียวกันก็ มีชนิดต่างกัน จึงใช้แทนกันไม่ได้ - ลำดับความสำคัญของโอเปอเรเตอร์กับลำดับการประเมินค่าเป็นคนละเรื่องกัน และ ถ้อยคำในมาตรฐาน เป็นตัวกำหนดผลการทำงานจริง รวมถึงโครงสร้างของเนื้อหา
switchและอายุของอ็อบเจ็กต์ชั่วคราว
พฤติกรรมที่ไม่ถูกกำหนดและกฎของพอยน์เตอร์
-
การเปรียบเทียบพอยน์เตอร์และกฎ strict aliasing
- แม้พอยน์เตอร์ชนิดเดียวกัน
pและqจะชี้ไปยังแอดเดรสเดียวกัน แต่ถ้ามาจากคนละอ็อบเจ็กต์ และไม่ได้เป็นส่วนหนึ่งของ aggregate หรือ union อ็อบเจ็กต์เดียวกัน การเปรียบเทียบp == qก็อาจเป็น พฤติกรรมที่ไม่ถูกกำหนด - ประเด็นที่ว่าพอยน์เตอร์เป็นสิ่งที่เป็นนามธรรมมากกว่าตัวเลขแอดเดรสธรรมดา มีอธิบายต่อในบทความที่เกี่ยวข้อง
- หากเข้าถึงอ็อบเจ็กต์
intผ่าน lvalue ชนิดshortจะเป็น พฤติกรรมที่ไม่ถูกกำหนด ตามกฎ strict aliasing - พอยน์เตอร์
unsigned charเป็นข้อยกเว้นที่สามารถ alias อ็อบเจ็กต์ใดก็ได้ ดังนั้นการเข้าถึงอ็อบเจ็กต์intผ่าน lvalue ชนิดunsigned charจึงถูกต้องตามกฎหมายภาษา unsigned charรับประกันว่าไม่มี padding bit และ trap representation และตั้งแต่ C11 เป็นต้นมาsigned charก็รับประกันว่าไม่มี padding bit เช่นกัน- การวิเคราะห์ aliasing ตามชนิดข้อมูลกล่าวถึงในบทความที่เกี่ยวข้อง
- แม้พอยน์เตอร์ชนิดเดียวกัน
-
นัลพอยน์เตอร์และการแทนค่าพอยน์เตอร์
- บิตแพตเทิร์นของนัลพอยน์เตอร์ไม่จำเป็นต้องเป็นศูนย์ทุกบิตเสมอไป
- มาตรฐาน C นิยาม null pointer constant แต่ไม่นิยามรูปแบบแทนค่านัลพอยน์เตอร์หรือการแทนค่าพอยน์เตอร์ทั่วไปในช่วงรันไทม์
- Symbolics Lisp Machine 3600 ใช้ทูเพิลรูปแบบ
<array-object, index>แทนพอยน์เตอร์เชิงตัวเลข และรูปแบบแทนค่านัลพอยน์เตอร์คือ<nil, 0> - ดูตัวอย่างเพิ่มเติมได้ที่ clc FAQ 5.17
- ค่าคงที่
0อาจเป็นจำนวนเต็มหรือนัลพอยน์เตอร์ขึ้นอยู่กับบริบท และ(void *)0จะถูกประเมินเป็นนัลพอยน์เตอร์ - แม้นิพจน์
eจะประเมินได้เป็น0ก็ไม่ได้รับประกันว่า(void *)eจะกลายเป็นนัลพอยน์เตอร์ - มีเพียงกรณีที่ null pointer constant ถูกแปลงเป็นชนิดพอยน์เตอร์เท่านั้นที่รับประกันว่าจะมีค่าเท่ากับนัลพอยน์เตอร์
- เลขคณิตกับนัลพอยน์เตอร์เป็นพฤติกรรมที่ไม่ถูกกำหนด ดังนั้นแม้
eจะเป็นนัลพอยน์เตอร์ ก็ไม่ได้รับประกันว่าe + 0จะยังเป็นนัลพอยน์เตอร์
-
ค่าที่ยังไม่ได้กำหนดค่าเริ่มต้น
- เมื่อมีการอ่านอ็อบเจ็กต์แบบ automatic storage duration ที่ยังไม่ได้กำหนดค่าเริ่มต้น หากอ็อบเจ็กต์นั้นอาจเป็น
registerstorage class และไม่เคยมีการนำแอดเดรสออกมาใช้เลย จะเป็น พฤติกรรมที่ไม่ถูกกำหนด ตาม C11 § 6.3.2.1 ¶ 2 - กฎนี้เชื่อมโยงกับสถาปัตยกรรม Intel Itanium ที่กล่าวถึงใน DR338
- รีจิสเตอร์จำนวนเต็มทั่วไปของ Itanium มี 64 บิตและ trap bit อีก 1 บิต โดย trap bit นี้คือ
NaT(not-a-thing) ที่ใช้บอกว่ารีจิสเตอร์นั้นถูกกำหนดค่าเริ่มต้นแล้วหรือยัง - หากมีการนำแอดเดรสของตัวแปรออกมาใช้ เงื่อนไขข้อนี้จะหายไป แต่ค่ายังคงเป็น indeterminate และอาจเป็น trap representation หรือ unspecified value
- การอ่าน trap representation จะเป็นพฤติกรรมที่ไม่ถูกกำหนดตาม C11 § 6.2.6.1 ¶ 5
- หากเป็น unspecified value ผลของ
x != xก็อาจเป็นtrueหรือfalseก็ได้ และถ้าint xเป็น unspecified แม้หลังx *= 0ก็ยังไม่รับประกันว่าxจะเป็น 0 - มีการอภิปรายเรื่อง indeterminate และ unspecified value ใน DR260, DR451, N1793, N1818, N2012, N2013, N2221
- เมื่อมีการอ่านอ็อบเจ็กต์แบบ automatic storage duration ที่ยังไม่ได้กำหนดค่าเริ่มต้น หากอ็อบเจ็กต์นั้นอาจเป็น
-
unsigned charและmemcpy- ชนิด
unsigned charไม่มี trap representation ตาม C11 § 6.2.6.1 ¶ 3 ดังนั้นค่าเริ่มต้นจึงเป็น unspecified - คำตอบ ของสมาชิกคณะกรรมการ C บน StackOverflow ระบุว่าหลังเรียกใช้ฟังก์ชันไลบรารีมาตรฐาน
memcpyแล้ว ค่าของxควรกลายเป็น specified และตามการตีความนี้x != xจะเป็นfalse - อย่างไรก็ตาม ข้อความในมาตรฐาน C ที่รองรับเรื่องนี้ยังไม่ชัดเจน และคำตอบของคณะกรรมการใน DR451 กลับระบุว่าการใช้ฟังก์ชันไลบรารีกับ indeterminate value เป็นพฤติกรรมที่ไม่ถูกกำหนด ซึ่งขัดกับการตีความนี้
- คำถามนี้ยังคงเปิดอยู่ และมีการอภิปรายเพิ่มเติมใน Uninitialized Reads
- ชนิด
ค่าคงที่จำนวนเต็ม, การเลื่อนชนิด, sizeof
-
การเขียนและชนิดของค่าคงที่จำนวนเต็ม
- ค่าคงที่จำนวนเต็มฐาน 10 ที่ไม่มี suffix จะถูกเลือกจากรายการชนิด signed เสมอ แต่ค่าคงที่ฐาน 8·16 อาจเป็นชนิด signed หรือ unsigned ก็ได้
- ตาม C17 § 6.4.4.1 ชนิดของค่าคงที่จำนวนเต็มจะถูกกำหนดเป็นชนิดแรกในรายการที่สามารถแทนค่านั้นได้
- เมื่อไม่มี suffix ค่าคงที่ฐาน 10 จะใช้ลำดับ
int,long int,long long intส่วนค่าคงที่ฐาน 8·16 จะใช้ลำดับint,unsigned int,long int,unsigned long int,long long int,unsigned long long int - ค่าคงที่ระหว่าง
INT_MAX+1ถึงUINT_MAXอาจมีชนิดต่างกันตามว่าเป็นฐาน 10 หรือฐาน 16 และอาจทำให้เกิดความต่างในโค้ดที่ไวต่อ ABI เช่นการเรียกฟังก์ชันแบบ variadic - ใน Arm 32-bit architecture ABI
intและlongมีขนาด 32 บิตและถูกส่งผ่านรีจิสเตอร์หนึ่งตัว ส่วนlong longมีขนาด 64 บิตและถูกส่งผ่านรีจิสเตอร์สองตัว - บนแพลตฟอร์มที่
intมีขนาด 32 บิต-1 < 0x8000จะเป็นtrueแต่บนแพลตฟอร์มที่intมีขนาด 16 บิตจะเป็นfalseซึ่งอาจก่อให้เกิด ปัญหาด้านการพกพา - ความต่างของชนิดค่าคงที่ยังสามารถเปลี่ยนผลลัพธ์ได้ใน generic selection, ฟังก์ชัน overload ของ C++, และนิพจน์อย่าง
sizeof(0x80000000) == sizeof(2147483648)
-
sizeof(int) > -1- ตัวดำเนินการ
sizeofคืนค่า unsigned integer ชนิดsize_t - ตาม usual arithmetic conversions ใน C11 § 6.3.1.8 ถ้าโอเปอแรนด์ signed มี rank ต่ำกว่าโอเปอแรนด์ unsigned ก็จะถูกแปลงเป็นชนิด unsigned ที่มี rank เดียวกัน
- signed integer ที่สอดคล้องกับ
-1เมื่อถูกแปลงเป็น unsigned จะกลายเป็นค่า unsigned สูงสุดของ rank นั้น - ดังนั้น
sizeof(int) > -1จะถูกประเมินเป็นfalseเสมอ
- ตัวดำเนินการ
-
ชนิดของค่าคงที่อักขระ
- ใน C ค่าคงที่อักขระมีชนิดเป็น
intตาม C11 § 6.4.4.4 ¶ 10 - ดังนั้นจึงไม่มีการรับประกันว่า
sizeof(char) == sizeof('x')จะเป็นtrueเสมอ โดยรับประกันเพียงว่าsizeof(int) == sizeof('x') - integer character constant อาจเป็นลำดับ multibyte character ตั้งแต่หนึ่งตัวขึ้นไปได้ ดังนั้น
'abc'ก็ใช้ได้ และการแทนค่านั้นขึ้นอยู่กับการกำหนดของ implementation - ค่าของ integer character constant ที่มีอักขระเดี่ยวจะเท่ากับการแทนค่าแบบจำนวนเต็มของอ็อบเจ็กต์ชนิด
charที่แทนอักขระเดี่ยวตัวเดียวกัน
- ใน C ค่าคงที่อักขระมีชนิดเป็น
-
เลขคณิตของ
uint8_tและการหาร- แม้
a,b,cจะถูกกำหนดค่าเริ่มต้นก่อนอ่าน ค่าxและzก็อาจต่างกันได้เพราะ integer promotion และตำแหน่งของการกำหนดค่าระหว่างทาง - ค่าของตัวแปรแต่ละตัวจะถูกเลื่อนเป็นขนาด
intก่อน แล้วจึงทำการบวกและหาร และผลลัพธ์ของการกำหนดค่าแต่ละครั้งจะถูก truncate เพื่อเก็บตามชนิดของตัวแปรนั้น - ตัวอย่างเช่นเมื่อ
a=255,b=1,c=2ค่าxจะเป็น((255 + 1) / 2) % 256 = 128 - ตัวแปรกลาง
yจะเป็น(255 + 1) % 256 = 0และหลังจากนั้นzจะเป็น(0 / 2) % 256 = 0ดังนั้น128 != 0 - unsigned integer overflow เป็นพฤติกรรมที่นิยามไว้แล้ว
- การคำนวณแบบ modulo สามารถแจกแจงกับการบวกได้ ดังนั้นถ้าเปลี่ยนการหารเป็นการบวก
xและzจะเท่ากันเสมอ - หากเปลี่ยนการกำหนดค่าแรกเป็น
uint8_t x = ((uint8_t)(a + b)) / c;ค่าxและzก็จะเท่ากันเสมอเช่นกัน
- แม้
-
ตัวแปร
constและ variable length array- แม้จะใช้ตัวแปร
nและmที่มีตัวกำกับconstเป็นขนาดของอาร์เรย์ ตัวแปรเหล่านี้ก็ไม่ใช่ integer constant expression ในภาษา C - ใน C11 § 6.6 ¶ 6 integer constant expression ถูกจำกัดไว้เฉพาะ integer constant, enumeration constant, character constant,
sizeof,_Alignofที่ให้ผลเป็น integer constant, และ floating constant ที่เป็นโอเปอแรนด์โดยตรงของ cast เป็นต้น - หากนิพจน์ขนาดอาร์เรย์ไม่ใช่ integer constant expression อาร์เรย์นั้นจะกลายเป็น variable length array ตาม C11 § 6.7.6.2 ¶ 4
- variable length array ไม่อนุญาตใน file scope ดังนั้น compilation unit ที่มีอาร์เรย์ส่วนกลาง
xจะคอมไพล์ไม่ผ่าน - ใน block scope อนุญาตให้ใช้ variable length array ได้ ดังนั้น compilation unit ที่มีอาร์เรย์ภายใน
yจึงอาจคอมไพล์ได้ - variable length array เป็น conditional feature ที่ implementation ไม่จำเป็นต้องรองรับ ดังนั้นในคอมไพเลอร์ที่ไม่รองรับ ตัวอย่างใน block scope ก็อาจคอมไพล์ไม่ผ่านเช่นกัน
- ใน C++ ทั้งสอง compilation unit คอมไพล์ได้ และเนื่องจาก C++ ไม่มีแนวคิด variable length array
yจึงถูกคอมไพล์เป็นอาร์เรย์ปกติที่มี 42 องค์ประกอบ
- แม้จะใช้ตัวแปร
การประกาศฟังก์ชัน, ค่าที่คืนกลับ, linkage
-
foo()และfoo(void)- การประกาศฟังก์ชันในรูปแบบ
foo()คือการประกาศฟังก์ชันที่ไม่ทราบจำนวนและชนิดของอาร์กิวเมนต์ ส่วนfoo(void)คือการประกาศ nullary function ที่ไม่มีอาร์กิวเมนต์ - ความแตกต่างนี้อธิบายไว้ในบทความเกี่ยวกับการประกาศ·นิยาม·โปรโตไทป์ของฟังก์ชัน
- การประกาศที่ไม่มีรายการอาร์กิวเมนต์จะแนะนำเพียงชื่อฟังก์ชันและไม่ได้กำหนดจำนวนกับชนิดของอาร์กิวเมนต์ จึงอาจถูกต้องตามกฎหมายเมื่อจับคู่กับนิยามฟังก์ชันในภายหลัง
- หากมีการเรียกฟังก์ชันโดยไม่มีโปรโตไทป์ จะมีการใช้ default argument promotions ทำให้
floatถูกยกเป็นdouble - หากชนิดของฟังก์ชันหลังการยกชนิดไม่เข้ากันกับชนิดในนิยามฟังก์ชันจริง ชุดการประกาศและนิยามนั้นจะใช้ไม่ได้
- การเรียกฟังก์ชันที่ไม่มีการประกาศ ในภาษา C อาจคอมไพล์ได้เพราะอนุญาต implicit function แต่ใน C++ จะเป็นข้อผิดพลาดในการคอมไพล์
- หากเรียกโดยไม่มีการประกาศ เช่น
bar(42)จะมีการใช้การยกชนิดของอาร์กิวเมนต์จำนวนเต็ม ทำให้42ถูกแทนเป็นintดังนั้นถ้าbarไม่เข้ากันกับT (*)(int)สำหรับชนิดค่าที่คืนกลับTใด ๆ ก็จะเป็นพฤติกรรมที่ไม่กำหนด
- การประกาศฟังก์ชันในรูปแบบ
-
ฟังก์ชันที่คืนค่าแต่ไม่คืนค่า
- แม้ฟังก์ชันที่มีชนิดค่าที่คืนกลับเป็น
intจะไม่คืนค่า ในภาษา C ก็อาจถือว่าถูกต้องได้ตราบใดที่ไม่มีการใช้ค่าผลลัพธ์จากการเรียก - ใน K&R C ไม่มีชนิด
voidและถ้าละชนิดไว้จะถือว่าชนิดปริยายเป็นintจึงมีความเชื่อมโยงทางประวัติศาสตร์ระหว่างฟังก์ชันที่ไม่คืนค่ากับกฎ implicitint - กฎ implicit
intถูกยกเลิกใน C99 และมีการอภิปรายที่เกี่ยวข้องใน N661 และ C99 rationale - C17 § 6.9.1 ¶ 12 ระบุว่า หากไปถึง
}ท้ายฟังก์ชันและผู้เรียกนำค่าจากการเรียกฟังก์ชันไปใช้ จะเป็น พฤติกรรมที่ไม่กำหนด - ใน C++98 § 6.6.3 ¶ 2 การไหลออกไปถึงท้ายฟังก์ชันที่คืนค่าถือเสมือน
returnที่ไม่มีค่า และในฟังก์ชันที่คืนค่านี่จะเป็นพฤติกรรมที่ไม่กำหนด - โดยทั่วไปคอมไพเลอร์ C++ ไม่สามารถพิสูจน์ได้ว่าฟังก์ชันอย่าง
abort_program()จะจบการทำงานในบางแขนงจริงหรือไม่ จึงมักให้เพียงการวินิจฉัยเตือนแทนที่จะเป็นข้อผิดพลาด
- แม้ฟังก์ชันที่มีชนิดค่าที่คืนกลับเป็น
-
linkage และ
extern- หากมีการประกาศ identifier เดิมซ้ำด้วย
externในสโคปที่มองเห็นการประกาศก่อนหน้าอยู่ linkage ของการประกาศภายหลังจะเหมือนกับ linkage ของการประกาศก่อนหน้า - C17 § 6.2.2 ¶ 4 ระบุว่า หากการประกาศก่อนหน้ากำหนด internal หรือ external linkage ไว้ การประกาศ
externภายหลังก็จะมี linkage เดียวกัน - หากมองไม่เห็นการประกาศก่อนหน้า หรือการประกาศก่อนหน้าไม่มี linkage identifier ที่ใช้
externจะมี external linkage - การผสมลำดับการประกาศในทางกลับกันอาจเป็นพฤติกรรมที่ไม่กำหนด และ GCC กับ Clang สามารถตรวจจับได้
- หากมีการประกาศ identifier เดิมซ้ำด้วย
ตัวระบุคุณสมบัติชนิดและชนิดไม่สมบูรณ์
-
constของพารามิเตอร์ฟังก์ชัน- หากพารามิเตอร์
xถูกระบุด้วยconstในการประกาศฟังก์ชัน แต่ไม่ใช่ในนิยามฟังก์ชัน และมีการเขียนค่าใส่xในตัวฟังก์ชัน ก็ยังถือว่าถูกต้องตามกฎหมาย - ตาม C11 § 6.7.6.3 ¶ 15 เมื่อตัดสินความเข้ากันได้ของชนิดพารามิเตอร์ฟังก์ชันและ composite type พารามิเตอร์แต่ละตัวที่ประกาศด้วย qualified type จะถูกมองเป็น unqualified version
- ประเด็นเดียวกันนี้ถูกกล่าวถึงใน DR040 เช่นกัน
- หากพารามิเตอร์
-
constของชนิดค่าที่คืนกลับของฟังก์ชัน- หากมีเพียงชนิดค่าที่คืนกลับในนิยามฟังก์ชันเท่านั้นที่ถูกระบุด้วย
constแต่การประกาศไม่ใช่ คำตอบก็ไม่อาจตัดสินง่าย ๆ ว่าถูกหรือผิด - ฉันทามติโดยรวมคือควรมองข้ามตัวระบุคุณสมบัติของ rvalue แต่ข้อความมาตรฐานจนถึง C11 ยังไม่ได้กล่าวเรื่องนี้ไว้อย่างชัดเจน
- ใน C17 ได้ทำให้ชัดเจนขึ้นว่าควรมองข้ามตัวระบุคุณสมบัติของ rvalue ใน cast, lvalue conversion และ function declarator
- C17 § 6.7.6.3 ¶ 5 ระบุไว้อย่างชัดเจนว่าชนิดที่ฟังก์ชันคืนกลับคือ unqualified version ของ
Tและข้อความนี้ถูกเพิ่มเข้ามาใน C17 - แม้ตัวระบุ
constของชนิดค่าที่คืนกลับจะแตกต่างกัน การกำหนดชนิดฟังก์ชันก็อาจยังถูกต้องตามกฎหมายได้ - ดูการอภิปรายเพิ่มเติมได้ที่ DR423 และ DR481
- หากมีเพียงชนิดค่าที่คืนกลับในนิยามฟังก์ชันเท่านั้นที่ถูกระบุด้วย
-
โครงสร้างไม่สมบูรณ์และตัวแปรโกลบอล
- แม้
struct fooจะเป็นชนิดไม่สมบูรณ์ในเวลาที่ประกาศตัวแปรโกลบอลจนยังไม่ทราบขนาด แต่หากชนิดนั้นสมบูรณ์ในภายหลังภายใน translation unit เดียวกัน ก็อาจอนุญาตได้ในบางสถานการณ์ - ตรรกะคล้ายกันนี้ใช้ได้กับการประกาศตัวแปรโกลบอลหรืออาร์เรย์ของชนิดไม่สมบูรณ์ด้วย
- ประเด็นนี้ถูกกล่าวถึงใน DR016 เช่นกัน
- แม้
-
external object ชนิด
void- การประกาศตัวแปรชนิด
voidที่มี internal linkage ไม่ถูกต้องตามกฎหมาย แต่การประกาศตัวแปรชนิดvoidที่มี external linkage นั้นถูกต้องตามไวยากรณ์ และไม่มีที่ใดในมาตรฐาน C11 ที่ห้ามไว้อย่างชัดเจน - ตาม C11 § 6.2.5 ¶ 19 ชนิด
voidเป็น ชนิดอ็อบเจ็กต์ไม่สมบูรณ์ที่ไม่อาจทำให้สมบูรณ์ได้ ซึ่งประกอบด้วยเซตว่างของค่า - C11 § 6.3.2.1 ¶ 1 นิยาม lvalue ว่าเป็นนิพจน์ของชนิดอ็อบเจ็กต์ที่ไม่ใช่
voidดังนั้นชื่ออ็อบเจ็กต์ชนิดvoidอย่างfooจึงไม่ใช่ lvalue ที่ใช้ได้ - เมื่อยึดตาม C11 เป็นเรื่องยากที่จะนึกถึงการดำเนินการที่มีความหมายและเป็นไปตามมาตรฐานกับ external
voidobject - DR012 กล่าวถึงว่า หากเปลี่ยนชนิดเป็น
const voidการนำที่อยู่ของอ็อบเจ็กต์fooจะถูกต้องตามกฎหมาย ซึ่งดูเหมือนเป็นช่องโหว่มากกว่าจะเป็นความสามารถที่ตั้งใจไว้
- การประกาศตัวแปรชนิด
-
การแปลง pointer-to-
const- เมื่อ
Tเป็นชนิดอ็อบเจ็กต์ที่ได้มาจากชนิดอื่น การกำหนดค่าให้cpนั้นถูกต้องตามกฎหมาย แต่สำหรับการกำหนดค่าให้cppว่าถูกต้องหรือไม่นั้นไม่มีคำตอบสั้น ๆ - หัวข้อนี้อธิบายไว้ในบทความเกี่ยวกับ implicit pointer to const conversion
- เมื่อ
อาร์เรย์, string literal, และการปรับชนิดพอยน์เตอร์
-
อาร์เรย์ไม่ใช่พอยน์เตอร์
- การกำหนดค่าเริ่มต้นให้อาร์เรย์และการกำหนดค่าเริ่มต้นให้พอยน์เตอร์ไม่ใช่สิ่งที่เทียบเท่ากัน
- รูปแบบแรกเป็นการกำหนดค่าเริ่มต้นให้ อาร์เรย์ที่แก้ไขได้ ซึ่งมีระยะเวลาจัดเก็บแบบอัตโนมัติหรือแบบสแตติก
- รูปแบบที่สองเป็นการกำหนดค่าเริ่มต้นให้พอยน์เตอร์ที่ชี้ไปยังอาร์เรย์ซึ่งมีระยะเวลาจัดเก็บแบบสแตติก และอาร์เรย์นั้นไม่จำเป็นต้องแก้ไขได้เสมอไป
- อาร์เรย์ไม่ใช่พอยน์เตอร์ และดูรายละเอียดเพิ่มเติมได้ในบทความที่เกี่ยวข้อง
-
a,&a,&a[0]- ใน
int a[42];ค่าของa,&a, และ&a[0]ล้วนประเมินเป็นที่อยู่ของสมาชิกตัวแรกของอาร์เรย์ - แต่ ชนิด ของนิพจน์ทั้งสามนั้นต่างกัน จึงใช้แทนกันไม่ได้
- ดูรายละเอียดเพิ่มเติมได้ในบทความที่เกี่ยวข้อง
- ใน
-
พารามิเตอร์อาร์เรย์และอาร์เรย์ภายในฟังก์ชัน
- หากชนิดของพารามิเตอร์ฟังก์ชันเป็น “อาร์เรย์ของ
T” จะถูกปรับเป็น “พอยน์เตอร์ไปยังT” - แม้พารามิเตอร์
xจะดูเหมือนint[42]แต่ในความเป็นจริงจะถูกปฏิบัติเป็นint * - หากตัวแปรภายในฟังก์ชัน
yเป็นint[42]ค่าsizeof(y)จะเป็น42 * sizeof(int) - โดยทั่วไปขนาดของ object pointer จะไม่เท่ากับขนาดของจำนวนเต็ม 42 ตัว ดังนั้น
sizeof(x) == sizeof(y)จึงมักเป็นfalse - ดูรายละเอียดเพิ่มเติมได้ในบทความที่เกี่ยวข้อง
- หากชนิดของพารามิเตอร์ฟังก์ชันเป็น “อาร์เรย์ของ
ตัวดำเนินการ, ลำดับการประเมินค่า, และการไหลของการควบคุม
-
x+++y- ใน C ไม่สามารถนิยามตัวดำเนินการใหม่แบบใน C++ ได้ ดังนั้นจึงไม่มีตัวดำเนินการใหม่อย่าง
+++ x+++yถูกตีความเป็นการผสมกันของตัวดำเนินการที่มีอยู่ และเทียบเท่ากับ(x++) + y--*--pก็ไม่ใช่ตัวดำเนินการใหม่เช่นกัน แต่เป็นการผสมกันของตัวดำเนินการที่มีอยู่--*--pเทียบเท่ากับ--(*(--p))และในตัวอย่างจะประเมินค่าเป็น-1พร้อมผลข้างเคียงคือกำหนดค่า-1ให้กับx[0]
- ใน C ไม่สามารถนิยามตัวดำเนินการใหม่แบบใน C++ ได้ ดังนั้นจึงไม่มีตัวดำเนินการใหม่อย่าง
-
ลำดับการประเมินค่าของโอเปอแรนด์เชิงคณิตศาสตร์
- ลำดับความสำคัญของตัวดำเนินการถูกกำหนดไว้อย่างชัดเจน แต่ ลำดับการประเมินค่า ของโอเปอแรนด์เชิงคณิตศาสตร์ไม่ได้ถูกกำหนดไว้
(x=1) + (x=2)เป็นพฤติกรรมที่ไม่ถูกกำหนด เพราะลำดับของการกำหนดค่าทั้งสองไม่ได้ถูกกำหนดไว้ จึงไม่สามารถระบุได้ว่าค่าสุดท้ายของxจะเป็น1หรือ2- เมื่อใช้ตัวเลือก
-std=c11 -O2GCC 8.2.1 ประเมินนิพจน์ตัวอย่างเป็น4ขณะที่ Clang 7.0.0 ประเมินเป็น3
-
ลำดับการประเมินค่าของตัวดำเนินการตรรกะ
- ในตัวดำเนินการตรรกะ
&&และ||นั้น ลำดับการประเมินค่าของโอเปอแรนด์ก็ถูกกำหนดไว้อย่างชัดเจนด้วย - ตามถ้อยคำในมาตรฐาน C จะมี sequence point อยู่ระหว่างการประเมินโอเปอแรนด์ตัวแรกกับการประเมินโอเปอแรนด์ตัวที่สอง
- ในตัวอย่าง จะประเมิน
x=1ก่อนและได้trueจากนั้นจึงประเมินx=2และได้trueเช่นกัน ดังนั้นนิพจน์ทั้งหมดจึงเป็นtrue
- ในตัวดำเนินการตรรกะ
-
โครงสร้างเนื้อความของ
switchที่ยืดหยุ่น- เนื้อความของคำสั่ง
switchสามารถเป็น statement ใดก็ได้ จึงอาจมีโครงสร้างที่ผสมทั้ง loop และifได้อย่างถูกต้องตามกฎหมายภาษา - แม้จะเป็น branch
trueภายในคำสั่งifที่มีนิพจน์ควบคุมเป็นfalseเสมอ หากมีcaselabel อยู่ statement นั้นก็จะกลายเป็น live ดังนั้นprintf("1");จึงไม่ใช่ dead code - หากกระโดดไปที่
case 2clause-1 และนิพจน์ควบคุมของ loop อาจไม่ถูกเรียกใช้งาน ดังนั้นตัวแปรiจึงต้องถูกกำหนดค่าเริ่มต้นไว้ล่วงหน้า - แม้
case 1จะไม่มีbreakจนเกิด fall through แต่ถ้าcase 1อยู่ใน branchtrueของifและcase 2อยู่ใน branchfalseก็อาจข้ามcase 2ไปและทำงานต่อที่case 3ได้ - หลังจากเรียกสามครั้ง
foo(0); foo(1); foo(2);เอาต์พุตบนคอนโซลจะเป็น02313223 - ตัวอย่างจริงที่มีชื่อเสียงของการผสม loop กับ switch คือ Duff's device
- เนื้อความของคำสั่ง
อายุการใช้งานของอ็อบเจ็กต์ชั่วคราวและความแตกต่างระหว่างเวอร์ชันของมาตรฐาน C
- โค้ดบางส่วนอาจเป็นพฤติกรรมที่ไม่ถูกกำหนดใน C11 แต่ไม่จำเป็นต้องเป็นเช่นนั้นใน C99
- ใน C11 อายุการใช้งานของอ็อบเจ็กต์บางชนิดสั้นลง ทำให้อ็อบเจ็กต์ที่ฟังก์ชันคอลส่งคืนมามีชีวิตอยู่เพียงจนกว่าจะประเมินโอเปอแรนด์ด้านขวาเสร็จ
- ใน C99 อ็อบเจ็กต์เดียวกันนี้จะมีชีวิตอยู่จนจบบล็อกที่ครอบอยู่
- หากอ้างอิงอ็อบเจ็กต์หลังจากอายุการใช้งานสิ้นสุดลง จะเป็น พฤติกรรมที่ไม่ถูกกำหนด ตาม C11 § 6.2.4 ¶ 2
- แม้ใน C99 อายุการใช้งานของอ็อบเจ็กต์ที่มี automatic storage duration ก็ยังผูกกับ enclosing block ที่ใกล้ที่สุด ดังนั้นหากอ้างอิงอ็อบเจ็กต์นอกบล็อกนั้นก็จะเป็นพฤติกรรมที่ไม่ถูกกำหนด
- C11 § 6.2.4 ¶ 8 กำหนดว่า non-lvalue expression ของชนิดโครงสร้างหรือ union ที่มี array member จะอ้างอิงอ็อบเจ็กต์ที่มี automatic storage duration และมี temporary lifetime
- อายุการใช้งานของอ็อบเจ็กต์ชั่วคราวนี้จะเริ่มเมื่อมีการประเมินนิพจน์ และสิ้นสุดเมื่อการประเมิน full expression หรือ full declarator ที่ครอบอยู่เสร็จสิ้น
- ความพยายามในการแก้ไขอ็อบเจ็กต์ที่มี temporary lifetime ถือเป็นพฤติกรรมที่ไม่ถูกกำหนด
- ตัวอย่างนี้นำมาจาก N1285 และมีการอภิปรายเพิ่มเติมอยู่ที่นั่น
1 ความคิดเห็น
ความเห็นจาก Lobste.rs
ข้อ 4 ใช้ไม่ได้ใน C23 แต่ก่อนหน้านั้นใช้ได้
ข้อ 10 ไม่ได้ทั้งถูกทั้งผิด เลยรู้สึกขัดใจนิดหน่อยถ้าจะเรียกว่าเป็นคำถามแบบปรนัย
ข้อ 15 ผิดในเชิงเทคนิค โดยเฉพาะเมื่อโยงกับข้อ 13 และข้อ 20 ก็เป็น “ไม่ได้ระบุไว้” เลยไม่ใช่คำตอบไหนเลยเหมือนกัน
ข้อ 30 กำกวมขึ้นอยู่กับว่าอ่านแบบไหน
ถึงอย่างนั้นก็ยังตอบถูก 27 จาก 31 ข้อ และการเป็นนักพัฒนาคอมไพเลอร์ก็คงช่วยอยู่บ้าง
พอทำไปสักสี่ข้อ ความรู้สึกที่เคยหลงเหลืออยู่ว่า C เรียบง่ายพอจะเอาไปใช้กับ side project ได้ ก็หายไปเลย
clangพร้อม-std=<language-standard>-pedantic -Wall -Wextraและแก้ทุกครั้งที่มีคำเตือนจริง ๆ พร้อมทั้งหลีกเลี่ยง pointer cast กับการเล่นกับพอยน์เตอร์ให้มากที่สุด ก็น่าจะเลี่ยงหลุมใหญ่ ๆ ได้ทุกวันนี้คำเตือนของ GCC/
clangค่อนข้างดี และ <language-standard> ใช้ได้ทั้ง c89, c99, c11, c23ถ้าใช้คอมไพเลอร์อย่าง tcc ที่ไม่ทำ optimization แปลก ๆ ก็จะเจอเรื่องชวนงงน้อยลง
ผมแค่เลือกตามเกณฑ์ว่า “ตรงนี้พฤติกรรมไหนไร้สาระที่สุด?” แล้วตอบถูก 21 จาก 32 ข้อ
ที่ผิดส่วนใหญ่ก็เพราะยังคิดถึงระดับความไร้สาระนั้นได้ไม่ลึกพอ
ผมเคยแตะ C นิดหน่อยเมื่อเกิน 15 ปีก่อน แต่พอเห็นควิซแบบนี้ก็ไม่ได้ทำให้อยากกลับไปลองใหม่เลย
ถ้ายึดตาม C23 คำตอบของ ข้อ 4 ใช้ไม่ได้
น่าสนใจดีที่ไม่ได้ใช้ C มาพักใหญ่แล้ว แต่ยังตอบถูก 27 จาก 32 ข้อ
นี่แหละเหตุผลที่ผมพึ่งพา static checker กับ linter มาตลอด
แค่ ข้อ 1 ก็เริ่มรู้สึกทะแม่งแล้ว
เขาไม่ได้พิจารณาเลยว่าพอยน์เตอร์พวกนั้นจะมาจากไหน และกรณีที่พูดถึงจะเกิดขึ้นได้ก็ต้องมีเงื่อนไขเฉพาะมาก ๆ
ในกรณีส่วนใหญ่ แค่พยายามสร้างพอยน์เตอร์แบบนั้นก็เป็น undefined behavior แล้ว แต่ถึงอย่างนั้นก็ยังพอจะมองว่าแฟร์ได้
ข้อ 3 นี่น่าตกใจจริง ๆ และเป็นอีกหนึ่งกับดักของ C
แค่การที่ integer literal ใน C มี type ที่ตายตัวตั้งแต่แรกก็น่าหงุดหงิดมากแล้ว
กฎ integer promotion ช่วยประคองไว้ได้ระดับหนึ่ง แต่ก็เป็นต้นตอของความผิดพลาดด้วย
ภาษาสมัยใหม่ส่วนใหญ่ หรืออาจจะทั้งหมด ควรห้าม implicit numeric cast ทั้งหมด, พยายามอนุมาน type ของ literal จากบริบทถ้าทำได้, และถ้าทำไม่ได้ก็ควรบังคับให้ cast แบบชัดเจน
หลังข้อ 6 ผมเลิกทำต่อเพราะไม่เชื่อใจแบบทดสอบแล้ว
ตอนแรกเป็นเพราะคำตอบของข้อ 5 เหมือนถูกออกแบบมาให้ทำข้อ 6 ผิดแทบจงใจ แต่พอกลับไปดูอีกที เหมือนตัวข้อ 6 เองจะผิดด้วย
คำอธิบายบอกว่าการเรียกฟังก์ชันเป็น undefined behavior แต่โจทย์ถามว่าคำนิยามของฟังก์ชันถูกกฎหมายไหม ซึ่งก็น่าจะถูกกฎหมายอยู่
และผมก็ไม่คิดว่านั่นจะเป็นกรณีที่หายากมากอะไร
โจทย์
switch()ดีมากจริง ๆยากแบบมีชั้นเชิง แต่กระบวนการไล่คิดในหัวสนุกมาก