1 คะแนน โดย GN⁺ 19 시간 전 | 1 ความคิดเห็น | แชร์ทาง WhatsApp
  • กฎของภาษา 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 ที่ยังไม่ได้กำหนดค่าเริ่มต้น หากอ็อบเจ็กต์นั้นอาจเป็น register storage 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
  • 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 ที่แทนอักขระเดี่ยวตัวเดียวกัน
  • เลขคณิตของ 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 จึงมีความเชื่อมโยงทางประวัติศาสตร์ระหว่างฟังก์ชันที่ไม่คืนค่ากับกฎ implicit int
    • กฎ 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 สามารถตรวจจับได้

ตัวระบุคุณสมบัติชนิดและชนิดไม่สมบูรณ์

  • 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 void object
    • 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]
  • ลำดับการประเมินค่าของโอเปอแรนด์เชิงคณิตศาสตร์

    • ลำดับความสำคัญของตัวดำเนินการถูกกำหนดไว้อย่างชัดเจน แต่ ลำดับการประเมินค่า ของโอเปอแรนด์เชิงคณิตศาสตร์ไม่ได้ถูกกำหนดไว้
    • (x=1) + (x=2) เป็นพฤติกรรมที่ไม่ถูกกำหนด เพราะลำดับของการกำหนดค่าทั้งสองไม่ได้ถูกกำหนดไว้ จึงไม่สามารถระบุได้ว่าค่าสุดท้ายของ x จะเป็น 1 หรือ 2
    • เมื่อใช้ตัวเลือก -std=c11 -O2 GCC 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 เสมอ หากมี case label อยู่ statement นั้นก็จะกลายเป็น live ดังนั้น printf("1"); จึงไม่ใช่ dead code
    • หากกระโดดไปที่ case 2 clause-1 และนิพจน์ควบคุมของ loop อาจไม่ถูกเรียกใช้งาน ดังนั้นตัวแปร i จึงต้องถูกกำหนดค่าเริ่มต้นไว้ล่วงหน้า
    • แม้ case 1 จะไม่มี break จนเกิด fall through แต่ถ้า case 1 อยู่ใน branch true ของ if และ case 2 อยู่ใน branch false ก็อาจข้าม 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 ได้ ก็หายไปเลย

    • ถ้าใช้ GCC หรือ clang พร้อม -std=<language-standard> -pedantic -Wall -Wextra และแก้ทุกครั้งที่มีคำเตือนจริง ๆ พร้อมทั้งหลีกเลี่ยง pointer cast กับการเล่นกับพอยน์เตอร์ให้มากที่สุด ก็น่าจะเลี่ยงหลุมใหญ่ ๆ ได้
      ทุกวันนี้คำเตือนของ GCC/clang ค่อนข้างดี และ <language-standard> ใช้ได้ทั้ง c89, c99, c11, c23
    • C นั้นเรียบง่าย แต่การเล่นกายกรรมรอบ ๆ undefined behavior ไม่ได้เรียบง่ายเลย
      ถ้าใช้คอมไพเลอร์อย่าง tcc ที่ไม่ทำ optimization แปลก ๆ ก็จะเจอเรื่องชวนงงน้อยลง
  • ผมแค่เลือกตามเกณฑ์ว่า “ตรงนี้พฤติกรรมไหนไร้สาระที่สุด?” แล้วตอบถูก 21 จาก 32 ข้อ
    ที่ผิดส่วนใหญ่ก็เพราะยังคิดถึงระดับความไร้สาระนั้นได้ไม่ลึกพอ
    ผมเคยแตะ C นิดหน่อยเมื่อเกิน 15 ปีก่อน แต่พอเห็นควิซแบบนี้ก็ไม่ได้ทำให้อยากกลับไปลองใหม่เลย

    • อ้างอิงไว้ด้วยว่า ChatGPT ตอบถูก 22 จาก 32 ข้อ โดยยังไม่ได้ดูคำอธิบายเพิ่มเติมหลังแต่ละคำตอบ
  • ถ้ายึดตาม 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() ดีมากจริง ๆ
    ยากแบบมีชั้นเชิง แต่กระบวนการไล่คิดในหัวสนุกมาก