รหัส C ที่พิมพ์จาก 1 ถึง 1,000 โดยไม่มีลูปหรือคำสั่งแบบมีเงื่อนไขทำงานอย่างไร


148

ฉันพบCรหัสที่พิมพ์จาก 1 ถึง 1,000 โดยไม่มีลูปหรือเงื่อนไข : แต่ฉันไม่เข้าใจวิธีการทำงาน ทุกคนสามารถอ่านรหัสและอธิบายแต่ละบรรทัดได้หรือไม่

#include <stdio.h>
#include <stdlib.h>

void main(int j) {
  printf("%d\n", j);
  (&main + (&exit - &main)*(j/1000))(j+1);
}

1
คุณรวบรวมเป็น C หรือเป็น C ++? คุณเห็นข้อผิดพลาดอะไร คุณไม่สามารถโทรmainใน C ++
ninjalj

@ninjalj ฉันได้สร้างโครงการ C ++ และคัดลอก / ผ่านรหัสข้อผิดพลาดคือ: ผิดกฎหมายตัวถูกดำเนินการทางซ้ายมีประเภท 'void (__cdecl *) (int)' และนิพจน์ต้องเป็นตัวชี้ไปยังชนิดของวัตถุที่สมบูรณ์
ob_dev

1
@ninjalj รหัสเหล่านี้ทำงานบน ideone.org แต่ไม่ได้อยู่ใน visual studio ideone.com/MtJ1M
ob_dev

@oussama ที่คล้ายกัน แต่ยากกว่าที่จะเข้าใจเล็กน้อย: ideone.com/2ItXm ยินดีต้อนรับ :)
ทำเครื่องหมาย

2
ฉันได้ลบอักขระ '&' ทั้งหมดออกจากบรรทัดเหล่านี้ (& main + (& exit - & main) * (j / 1000)) (j + 1); และรหัสนี้ยังใช้งานได้
ob_dev

คำตอบ:


264

อย่าเขียนโค้ดแบบนั้น


สำหรับj<1000, j/1000คือศูนย์ (การหารจำนวนเต็ม) ดังนั้น:

(&main + (&exit - &main)*(j/1000))(j+1);

เทียบเท่ากับ:

(&main + (&exit - &main)*0)(j+1);

ซึ่งเป็น:

(&main)(j+1);

ซึ่งเรียกร้องกับmainj+1

ถ้าj == 1000เช่นนั้นบรรทัดเดียวกันจะออกมาเป็น:

(&main + (&exit - &main)*1)(j+1);

ซึ่งเดือดลงไป

(&exit)(j+1);

ซึ่งเป็นexit(j+1)และออกจากโปรแกรม


(&exit)(j+1)และexit(j+1)มันก็เป็นสิ่งเดียวกัน - การอ้างอิง C99 §6.3.2.1 / 4:

ตัวออกแบบฟังก์ชั่นคือการแสดงออกที่มีประเภทฟังก์ชั่น ยกเว้นเมื่อเป็นตัวถูกดำเนินการของตัวดำเนินการ sizeof หรือ unary & operator ตัวออกแบบฟังก์ชั่นที่มีประเภท " function return type " จะถูกแปลงเป็นนิพจน์ที่มีประเภท " pointer to function return type "

exitเป็นตัวกำหนดฟังก์ชั่น แม้ว่าจะไม่มี&โอเปอเรเตอร์แอดเดรสของผู้ใช้ แต่ก็ถือว่าเป็นตัวชี้ไปยังฟังก์ชัน ( &เพียงแค่ทำให้ชัดเจน)

และฟังก์ชั่นการโทรอธิบายไว้ใน§6.5.2.2 / 1 และต่อไปนี้:

การแสดงออกที่หมายถึงฟังก์ชั่นที่เรียกว่าจะต้องมีตัวชี้ประเภทเพื่อฟังก์ชั่นการส่งกลับเป็นโมฆะหรือกลับประเภทวัตถุอื่นที่ไม่ใช่ประเภทอาร์เรย์

ดังนั้นexit(j+1)ทำงานเนื่องจากการแปลงโดยอัตโนมัติของประเภทฟังก์ชั่นเป็นประเภทตัวชี้ไปยังฟังก์ชั่นและ(&exit)(j+1)ทำงานได้ดีกับการแปลงที่ชัดเจนเป็นประเภทตัวชี้ไปยังฟังก์ชั่น

ที่ถูกกล่าวว่ารหัสข้างต้นไม่สอดคล้อง ( mainใช้เวลาสองข้อโต้แย้งหรือไม่มีเลย) และ&exit - &mainฉันเชื่อว่าไม่ได้กำหนดตาม§6.5.6 / 9:

เมื่อพอยน์เตอร์สองตัวถูกลบทั้งคู่จะชี้ไปที่องค์ประกอบของวัตถุอาร์เรย์เดียวกันหรือผ่านองค์ประกอบสุดท้ายของวัตถุอาร์เรย์ ...

การเพิ่ม(&main + ...)จะถูกต้องในตัวเองและสามารถใช้ถ้าปริมาณเพิ่มเป็นศูนย์เนื่องจาก§6.5.6 / 7 พูดว่า:

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

ดังนั้นการเพิ่มศูนย์ให้&mainจะโอเค (แต่ไม่ค่อยมีประโยชน์)


4
foo(arg)และ(&foo)(arg)เทียบเท่าพวกเขาเรียกฟูด้วยอาร์กิวเมนต์หาเรื่อง newty.de/fpt/fpt.htmlเป็นหน้าที่น่าสนใจเกี่ยวกับพอยน์เตอร์ของฟังก์ชัน
Mat

1
@ Krishnabhadra: ในกรณีแรกfooคือตัวชี้&fooเป็นที่อยู่ของตัวชี้นั้น ในกรณีที่สองfooคืออาร์เรย์และ&fooเทียบเท่ากับ foo
Mat

8
ซับซ้อนโดยไม่จำเป็นอย่างน้อยสำหรับ C99:((void(*[])()){main, exit})[j / 1000](j + 1);
ต่อ Johansson

1
&fooไม่เหมือนกับfooเมื่อมาถึงอาร์เรย์ &fooเป็นตัวชี้ไปยังอาร์เรย์fooเป็นตัวชี้ไปยังองค์ประกอบแรก พวกเขามีค่าเดียวกันแม้ว่า สำหรับฟังก์ชั่นfunและ&funเป็นทั้งตัวชี้ไปยังฟังก์ชั่น
ต่อ Johansson

1
หากคุณดูคำตอบที่เกี่ยวข้องกับคำถามอื่น ๆ ที่อ้างถึงข้างต้นคุณจะเห็นว่ามีรูปแบบที่แตกต่างกันในความเป็นจริงแล้ว C99 น่ากลัว แต่จริง
Daniel Pryden

41

จะใช้การเรียกซ้ำการคำนวณตัวชี้และใช้ประโยชน์จากพฤติกรรมการปัดเศษของการแบ่งจำนวนเต็ม

j/1000รอบระยะลงไปที่ 0 สำหรับทุกj < 1000; เมื่อครบj1,000 ครั้งมันจะประเมินเป็น 1

ตอนนี้ถ้าคุณมีa + (b - a) * nที่nเป็น 0 หรือ 1 คุณจะจบลงด้วยaถ้าn == 0และถ้าb n == 1การใช้&main(ที่อยู่ของmain()) และ&exitสำหรับaและbคำ(&main + (&exit - &main) * (j/1000))จะคืนค่า&mainเมื่อjต่ำกว่า 1,000 &exitมิฉะนั้น j+1ตัวชี้ฟังก์ชั่นส่งผลให้เป็นอาหารแล้วการโต้แย้ง

ผลลัพธ์การสร้างทั้งหมดนี้มีพฤติกรรมแบบเรียกซ้ำ: ในขณะที่jอยู่ต่ำกว่า 1,000 mainเรียกตัวเองซ้ำ ๆ เมื่อjถึง 1,000 จะเรียกexitใช้แทนทำให้โปรแกรมออกโดยใช้รหัสออก 1001 (ซึ่งค่อนข้างสกปรก แต่ใช้งานได้)


1
คำตอบที่ดี แต่มีข้อสงสัยอย่างหนึ่ง .. ทางออกหลักที่มีรหัสทางออก 1001 เป็นอย่างไร หลักไม่ได้ส่งคืนอะไร .. มีค่าส่งคืนเริ่มต้นหรือไม่
Krishnabhadra

2
เมื่อ j ถึง 1,000 หลักจะไม่ถูกเรียกคืนเข้าไปอีก แต่จะเรียกใช้ฟังก์ชัน libc exitซึ่งรับรหัสทางออกเป็นอาร์กิวเมนต์และออกจากกระบวนการปัจจุบันแทน ณ จุดนั้น j คือ 1,000 ดังนั้น j + 1 เท่ากับ 1001 ซึ่งกลายเป็นรหัสออก
tdammers
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.