อะไรทำให้การใช้พอยน์เตอร์นี้ไม่สามารถคาดเดาได้?


108

ขณะนี้ฉันกำลังเรียนรู้คำแนะนำและอาจารย์ของฉันให้ตัวอย่างโค้ดชิ้นนี้:

//We cannot predict the behavior of this program!

#include <iostream>
using namespace std;

int main()
{
    char * s = "My String";
    char s2[] = {'a', 'b', 'c', '\0'};

    cout << s2 << endl;

    return 0;
}

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


2
คุณแน่ใจหรือไม่ว่าคุณทำซ้ำรหัสของศาสตราจารย์อย่างถูกต้อง? แม้ว่าจะเป็นไปได้อย่างเป็นทางการที่จะโต้แย้งว่าโปรแกรมนี้อาจก่อให้เกิดพฤติกรรมที่ "คาดเดาไม่ได้" แต่ก็ไม่มีเหตุผลที่จะทำเช่นนั้น และฉันสงสัยว่าศาสตราจารย์คนใดจะใช้สิ่งที่ลึกลับเพื่อแสดงให้นักเรียนเห็นว่า "คาดเดาไม่ได้"
AnT

1
@Lightness Races in Orbit: คอมไพเลอร์ได้รับอนุญาตให้ "ยอมรับ" โค้ดที่มีรูปแบบไม่ถูกต้องหลังจากที่ออกข้อความวินิจฉัยที่จำเป็น แต่ข้อกำหนดภาษาไม่ได้กำหนดลักษณะการทำงานของรหัส กล่าวคือเนื่องจากข้อผิดพลาดในการเริ่มต้นsโปรแกรมหากได้รับการยอมรับจากคอมไพเลอร์บางตัวมีพฤติกรรมที่ไม่สามารถคาดเดาได้อย่างเป็นทางการ
AnT

2
@TheParamagneticCroissant: ไม่การเริ่มต้นเป็นรูปแบบที่ไม่ดีในยุคปัจจุบัน
Lightness Races ใน Orbit

2
@ ครัวซองต์พาราแมกเนติก: ดังที่ฉันได้กล่าวไว้ข้างต้นภาษาไม่จำเป็นต้องใช้รหัสที่ไม่ถูกต้องเพื่อ "ไม่สามารถรวบรวม" ได้ คอมไพเลอร์จำเป็นเพียงเพื่อทำการวินิจฉัย หลังจากนั้นพวกเขาจะได้รับอนุญาตให้ดำเนินการต่อและคอมไพล์โค้ด "สำเร็จ" อย่างไรก็ตามลักษณะการทำงานของรหัสดังกล่าวไม่ได้กำหนดโดยข้อกำหนดภาษา
AnT

2
ฉันอยากรู้ว่าศาสตราจารย์ของคุณให้คำตอบอะไรแก่คุณ
Daniël W. Crompton

คำตอบ:


125

พฤติกรรมของโปรแกรมไม่มีอยู่จริงเนื่องจากมีรูปแบบไม่ถูกต้อง

char* s = "My String";

สิ่งนี้ผิดกฎหมาย ก่อนปี 2554 ได้เลิกใช้งานเป็นเวลา 12 ปี

บรรทัดที่ถูกต้องคือ:

const char* s = "My String";

นอกเหนือจากนั้นโปรแกรมก็ใช้ได้ ศาสตราจารย์ของคุณควรดื่มวิสกี้ให้น้อยลง!


10
ด้วย -pedantic มันทำ: main.cpp: 6: 16: คำเตือน: ISO C ++ ห้ามการแปลงค่าคงที่ของสตริงเป็น 'char *' [-Wpedantic]
marcinj

17
@black: ไม่ความจริงที่ว่าการแปลงนั้นผิดกฎหมายทำให้โปรแกรมมีรูปแบบไม่ถูกต้อง มันถูกเลิกใช้ในอดีต เราไม่ได้อยู่ในอดีตอีกต่อไป
Lightness Races ใน Orbit

17
(ซึ่งเป็นเรื่องโง่เพราะนั่นคือจุดประสงค์ของการเลิกใช้งาน 12 ปี)
Lightness Races in Orbit

17
@black: พฤติกรรมของโปรแกรมที่มีรูปแบบไม่ถูกต้องไม่ได้ "กำหนดไว้อย่างสมบูรณ์"
Lightness Races ใน Orbit

11
ไม่ว่าคำถามจะเกี่ยวกับ C ++ ไม่ใช่เกี่ยวกับ GCC บางเวอร์ชัน
Lightness Races ใน Orbit

81

คำตอบคือขึ้นอยู่กับมาตรฐาน C ++ ที่คุณกำลังรวบรวม รหัสทั้งหมดถูกสร้างขึ้นอย่างสมบูรณ์แบบในทุกมาตรฐาน‡ยกเว้นบรรทัดนี้:

char * s = "My String";

ตอนนี้สตริงลิเทอรัลมีประเภทconst char[10]และเรากำลังพยายามเตรียมใช้งานตัวชี้ที่ไม่ใช่คอนสแตนซ์ สำหรับประเภทอื่น ๆ ทั้งหมดนอกเหนือจากcharตระกูลของสตริงลิเทอรัลการเริ่มต้นดังกล่าวผิดกฎหมายเสมอ ตัวอย่างเช่น:

const int arr[] = {1};
int *p = arr; // nope!

อย่างไรก็ตามในก่อน C ++ 11 สำหรับตัวอักษรสตริงมีข้อยกเว้นใน§4.2 / 2:

สตริงลิเทอรัล (2.13.4) ที่ไม่ใช่ลิเทอรัลสตริงแบบกว้างสามารถแปลงเป็นค่า r ประเภท " pointer to char "; [... ]. ไม่ว่าในกรณีใดผลลัพธ์คือตัวชี้ไปยังองค์ประกอบแรกของอาร์เรย์ การแปลงนี้จะได้รับการพิจารณาก็ต่อเมื่อมีประเภทเป้าหมายของตัวชี้ที่เหมาะสมอย่างชัดเจนและไม่ใช่เมื่อมีความจำเป็นทั่วไปในการแปลงจาก lvalue เป็น rvalue [หมายเหตุ:การแปลงนี้จะเลิกใช้ ดูภาคผนวก D. ]

ดังนั้นใน C ++ 03 โค้ดจึงใช้ได้ดี (แม้ว่าจะเลิกใช้งานแล้ว) และมีพฤติกรรมที่ชัดเจนและคาดเดาได้

ใน C ++ 11 บล็อกนั้นไม่มีอยู่ - ไม่มีข้อยกเว้นสำหรับตัวอักษรสตริงที่แปลงเป็นchar*ดังนั้นรหัสจึงมีรูปแบบที่ไม่ถูกต้องตามint*ตัวอย่างที่ฉันให้ไว้ คอมไพเลอร์มีหน้าที่ต้องออกการวินิจฉัยและในกรณีเช่นนี้เป็นการละเมิดที่ชัดเจนของระบบประเภท C ++ เราคาดหวังว่าคอมไพเลอร์ที่ดีจะไม่เพียง แต่สอดคล้องในเรื่องนี้ (เช่นโดยการออกคำเตือน) แต่จะล้มเหลว ทันที.

โค้ดไม่ควรคอมไพล์ - แต่ทำทั้งบน gcc และ clang (ฉันคิดว่าเพราะอาจมีโค้ดจำนวนมากที่จะเสียโดยมีกำไรเพียงเล็กน้อยแม้ว่ารูระบบประเภทนี้จะเลิกใช้งานมานานกว่าทศวรรษแล้วก็ตาม) รหัสมีรูปแบบที่ไม่ถูกต้องดังนั้นจึงไม่สมเหตุสมผลที่จะให้เหตุผลเกี่ยวกับลักษณะการทำงานของโค้ด แต่เมื่อพิจารณาถึงกรณีเฉพาะนี้และประวัติที่ได้รับอนุญาตก่อนหน้านี้ฉันไม่เชื่อว่าจะเป็นการยืดที่ไม่มีเหตุผลในการตีความรหัสผลลัพธ์ราวกับว่าเป็นโดยนัยconst_castบางอย่างเช่น:

const int arr[] = {1};
int *p = const_cast<int*>(arr); // OK, technically

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

std::cout << *p; // fine, prints 1
*p = 5;          // will compile, but undefined behavior, which
                 // certainly qualifies as "unpredictable"

เนื่องจากไม่มีการแก้ไขผ่านsที่ใดก็ได้ในโค้ดของคุณโปรแกรมจึงใช้ได้ใน C ++ 03 ควรล้มเหลวในการคอมไพล์ใน C ++ 11 แต่จะดำเนินการต่อ - และเนื่องจากคอมไพเลอร์อนุญาตจึงยังไม่มีพฤติกรรมที่ไม่ได้กำหนดไว้ในนั้น† . ด้วยค่าเผื่อที่คอมไพลเลอร์ยังคง [ไม่ถูกต้อง] ตีความกฎ C ++ 03 ฉันไม่เห็นสิ่งใดที่จะนำไปสู่พฤติกรรมที่ "คาดเดาไม่ได้" เขียนถึงsแม้ว่าและการเดิมพันทั้งหมดจะปิดลง ทั้งใน C ++ 03 และ C ++ 11


†แม้ว่าอีกครั้งตามคำนิยามรหัสที่มีรูปแบบไม่ถูกต้องไม่ให้ความคาดหวังถึงพฤติกรรมที่สมเหตุสมผล
‡ยกเว้นไม่ดูคำตอบของ Matt McNabb


ฉันคิดว่าที่นี่ "คาดเดาไม่ได้" ตั้งใจโดยศาสตราจารย์เพื่อหมายความว่าไม่มีใครสามารถใช้มาตรฐานในการทำนายว่าคอมไพเลอร์จะทำอะไรกับรหัสที่ไม่เหมาะสม (นอกเหนือจากการออกการวินิจฉัย) ใช่มันสามารถปฏิบัติต่อมันได้ตามที่ C ++ 03 บอกว่าควรได้รับการปฏิบัติและ (ที่เสี่ยงต่อการเข้าใจผิด "No True Scotsman") ทำให้เราสามารถคาดเดาได้ด้วยความมั่นใจว่านี่เป็นเพียงสิ่งเดียวที่ผู้รวบรวม - เขียนที่สมเหตุสมผล จะเลือกว่าโค้ดคอมไพล์เลยหรือไม่ จากนั้นอีกครั้งมันสามารถถือว่าเป็นความหมายในการย้อนกลับสตริงลิเทอรัลก่อนที่จะส่งเป็น non-const C ++ มาตรฐานไม่สนใจ
Steve Jessop

2
@SteveJessop ฉันไม่ซื้อการตีความนั้น นี่ไม่ใช่พฤติกรรมที่ไม่ได้กำหนดหรือหมวดหมู่ของรหัสที่มีรูปแบบไม่ถูกต้องซึ่งป้ายกำกับมาตรฐานไม่จำเป็นต้องมีการวินิจฉัย เป็นการละเมิดระบบประเภทธรรมดาที่ควรคาดเดาได้ง่ายมาก (คอมไพล์และทำสิ่งปกติบน C ++ 03 ไม่สามารถคอมไพล์บน C ++ 11) คุณไม่สามารถใช้ข้อบกพร่องของคอมไพเลอร์ (หรือใบอนุญาตทางศิลปะ) เพื่อแนะนำว่าโค้ดนั้นไม่สามารถคาดเดาได้มิฉะนั้นโค้ดทั้งหมดจะไม่สามารถคาดเดาได้โดยอัตโนมัติ
Barry

ฉันไม่ได้พูดถึงบั๊กของคอมไพเลอร์ฉันกำลังพูดถึงว่ามาตรฐานกำหนดพฤติกรรม (ถ้ามี) ของโค้ดหรือไม่ ฉันสงสัยว่าศาสตราจารย์กำลังทำเช่นเดียวกันและ "คาดเดาไม่ได้" เป็นเพียงวิธีการพูดที่ไม่ชัดเจนว่ามาตรฐานปัจจุบันไม่ได้กำหนดพฤติกรรม อย่างไรก็ตามสิ่งที่ดูเหมือนจะเป็นไปได้สำหรับฉันมากกว่าที่ศาสตราจารย์เชื่ออย่างไม่ถูกต้องว่านี่เป็นโปรแกรมที่มีรูปแบบที่ดีและมีพฤติกรรมที่ไม่ได้กำหนด
Steve Jessop

1
ไม่มันไม่ได้ มาตรฐานไม่ได้กำหนดลักษณะการทำงานของโปรแกรมที่ไม่เหมาะสม
Steve Jessop

1
@supercat: มันเป็นจุดที่ยุติธรรม แต่ฉันไม่เชื่อว่ามันเป็นเหตุผลหลัก ฉันคิดว่าเหตุผลหลักที่มาตรฐานไม่ได้ระบุลักษณะการทำงานของโปรแกรมที่มีรูปแบบไม่ถูกต้องนั่นคือเพื่อให้คอมไพเลอร์สามารถรองรับส่วนขยายของภาษาได้โดยการเพิ่มไวยากรณ์ที่มีรูปแบบไม่ถูกต้อง (เช่น Objective C) การอนุญาตให้ใช้งานเพื่อสร้างความสยองขวัญทั้งหมดจากการทำความสะอาดหลังจากการรวบรวมล้มเหลวเป็นเพียงโบนัส :-)
Steve Jessop

20

คำตอบอื่น ๆ ครอบคลุมว่าโปรแกรมนี้มีรูปแบบที่ไม่ถูกต้องใน C ++ 11 เนื่องจากการกำหนดconst charอาร์เรย์ให้กับไฟล์char *.

อย่างไรก็ตามโปรแกรมนั้นมีรูปแบบที่ไม่ถูกต้องก่อน C ++ 11 ด้วย

ทับถมอยู่ในoperator<< <ostream>ข้อกำหนดสำหรับiostreamการรวมostreamถูกเพิ่มใน C ++ 11

ในอดีตการใช้งานส่วนใหญ่จะiostreamรวมถึงostreamอย่างไรก็ตามอาจเพื่อความสะดวกในการนำไปใช้งานหรืออาจเพื่อให้ QoI ดีขึ้น

แต่จะเป็นไปตามiostreamที่กำหนดเฉพาะostreamคลาสโดยไม่กำหนดoperator<<โอเวอร์โหลด


13

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

มิฉะนั้นโปรแกรมนี้จะถูกกำหนดไว้อย่างชัดเจนสำหรับฉัน:

  • กฎที่กำหนดว่าอาร์เรย์อักขระกลายเป็นตัวชี้อักขระอย่างไรเมื่อส่งผ่านเป็นพารามิเตอร์ (เช่นด้วยcout << s2) ได้รับการกำหนดไว้อย่างดี
  • อาร์เรย์ถูกยกเลิกด้วย null ซึ่งเป็นเงื่อนไขสำหรับoperator<<a char*(หรือ a const char*)
  • #include <iostream>รวมถึง<ostream>ซึ่งในทางกลับกันกำหนดoperator<<(ostream&, const char*)ทุกอย่างจึงดูเหมือนจะเข้าที่

12

คุณไม่สามารถคาดเดาพฤติกรรมของคอมไพเลอร์ได้ด้วยเหตุผลที่ระบุไว้ข้างต้น ( ไม่ควรรวบรวม แต่อาจไม่ได้)

หากการคอมไพล์สำเร็จแสดงว่าพฤติกรรมนั้นถูกกำหนดไว้อย่างดี คุณสามารถคาดเดาพฤติกรรมของโปรแกรมได้อย่างแน่นอน

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

ดังนั้นฉันว่าคำพูดของศาสตราจารย์ของคุณไม่ถูกต้อง คุณไม่สามารถทำนายพฤติกรรมของคอมไพเลอร์เมื่อต้องเผชิญกับรหัสนี้ แต่ที่แตกต่างไปจากพฤติกรรมของโปรแกรม ดังนั้นถ้าเขาจะเลือกไข่เหาเขาควรตรวจสอบให้แน่ใจว่าถูกต้อง หรือแน่นอนคุณอาจอ้างเขาผิดและความผิดพลาดอยู่ในการแปลสิ่งที่เขาพูด


10

ตามที่คนอื่นสังเกตเห็นว่ารหัสนี้ผิดกฎหมายภายใต้ C ++ 11 แม้ว่าจะถูกต้องภายใต้เวอร์ชันก่อนหน้าก็ตาม ดังนั้นคอมไพเลอร์สำหรับ C ++ 11 จึงจำเป็นต้องออกการวินิจฉัยอย่างน้อยหนึ่งครั้ง แต่พฤติกรรมของคอมไพลเลอร์หรือส่วนที่เหลือของระบบบิลด์ไม่ได้ระบุไว้นอกเหนือจากนั้น ไม่มีสิ่งใดในมาตรฐานที่จะห้ามไม่ให้คอมไพเลอร์ออกจากการตอบสนองต่อข้อผิดพลาดอย่างกะทันหันโดยปล่อยให้ไฟล์อ็อบเจ็กต์ที่เขียนขึ้นบางส่วนซึ่งผู้เชื่อมโยงอาจคิดว่าถูกต้องทำให้ไฟล์ปฏิบัติการเสียหาย

แม้ว่าคอมไพลเลอร์ที่ดีควรตรวจสอบให้แน่ใจก่อนที่จะออกจากไฟล์อ็อบเจ็กต์ใด ๆ ที่คาดว่าจะสร้างขึ้นนั้นจะถูกต้องไม่มีอยู่จริงหรือเป็นที่จดจำได้ว่าไม่ถูกต้อง แต่ปัญหาดังกล่าวก็อยู่นอกเขตอำนาจศาลของมาตรฐาน ในขณะที่มีในอดีต (และอาจยังคงเป็น) บางแพลตฟอร์มที่การคอมไพล์ล้มเหลวอาจส่งผลให้ไฟล์ปฏิบัติการที่ปรากฏถูกต้องตามกฎหมายซึ่งขัดข้องโดยพลการเมื่อโหลด (และฉันต้องทำงานกับระบบที่ข้อผิดพลาดในการเชื่อมโยงมักมีพฤติกรรมดังกล่าว) ฉันจะไม่บอกว่าผลที่ตามมาของข้อผิดพลาดทางไวยากรณ์มักไม่สามารถคาดเดาได้ ในระบบที่ดีการสร้างที่พยายามจะสร้างไฟล์ปฏิบัติการด้วยความพยายามอย่างเต็มที่ของคอมไพเลอร์ในการสร้างโค้ดหรือจะไม่สร้างไฟล์ปฏิบัติการเลย ระบบบางระบบจะทิ้งไฟล์ปฏิบัติการเก่าไว้หลังจากสร้างล้มเหลว

ความชอบส่วนตัวของฉันคือระบบที่ใช้ดิสก์ในการเปลี่ยนชื่อไฟล์เอาต์พุตเพื่อให้มีโอกาสเกิดขึ้นได้ยากเมื่อปฏิบัติการนั้นจะมีประโยชน์ในขณะที่หลีกเลี่ยงความสับสนที่อาจเกิดจากการเชื่อผิด ๆ ว่ากำลังรันโค้ดใหม่และสำหรับโปรแกรมฝังตัว ระบบเพื่ออนุญาตให้โปรแกรมเมอร์ระบุโปรแกรมที่ควรโหลดสำหรับแต่ละโปรเจ็กต์หากไฟล์ปฏิบัติการที่ถูกต้องไม่พร้อมใช้งานภายใต้ชื่อปกติ [ควรเป็นสิ่งที่บ่งชี้อย่างปลอดภัยว่าไม่มีโปรแกรมที่ใช้งานได้] โดยทั่วไปชุดเครื่องมือระบบฝังตัวจะไม่มีทางรู้ว่าโปรแกรมดังกล่าวควรทำอย่างไร แต่ในหลาย ๆ กรณีคนที่เขียนโค้ด "จริง" สำหรับระบบจะสามารถเข้าถึงรหัสทดสอบฮาร์ดแวร์บางอย่างที่สามารถปรับให้เข้ากับ วัตถุประสงค์. ฉันไม่รู้ว่าฉันเคยเห็นพฤติกรรมการเปลี่ยนชื่อแล้ว

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.