मुश्किल सी कोड की इन चार लाइनों के पीछे की अवधारणा


384

यह कोड आउटपुट क्यों देता है C++Sucks? इसके पीछे क्या अवधारणा है?

#include <stdio.h>

double m[] = {7709179928849219.0, 771};

int main() {
    m[1]--?m[0]*=2,main():printf((char*)m);    
}

इसका परीक्षण यहां करें


1
@BoBTFish तकनीकी रूप से, हाँ, लेकिन यह C99 में सभी समान है: ideone.com/IZOkql
nijansen

12
@nurettin मेरे भी ऐसे ही विचार थे। लेकिन यह ओपी की गलती नहीं है, यह लोग इस बेकार ज्ञान के लिए मतदान कर रहे हैं। माना जाता है कि, यह कोड ओफ़्फ़्यूसिएशन सामान दिलचस्प हो सकता है लेकिन Google में "ओफ़्फ़ुसेशन" टाइप कर सकते हैं और आपको हर उस औपचारिक भाषा में परिणाम मिलते हैं, जिसके बारे में आप सोच सकते हैं। मुझे गलत मत समझो, मुझे यहाँ ऐसा प्रश्न पूछना ठीक लगता है। यह सिर्फ एक ओवररेटेड है क्योंकि बहुत उपयोगी सवाल नहीं है।
टोबिम्कामोनबी

6
@ detonator123 "आपको यहां नया होना चाहिए" - यदि आप क्लोजर कारण को देखते हैं, तो आप पता लगा सकते हैं कि ऐसा नहीं है। आवश्यक न्यूनतम समझ आपके प्रश्न से स्पष्ट रूप से गायब है - "मुझे यह समझ में नहीं आता है, इसे समझाएं" ऐसा कुछ नहीं है जो स्टैक ओवरफ्लो पर स्वागत है। क्या आपको पहले खुद कुछ करने का प्रयास करना चाहिए था , क्या सवाल बंद नहीं हुआ होगा। यह "डबल प्रतिनिधित्व सी" या जैसे Google के लिए मामूली है।

42
मेरी बड़ी-एंडियन पॉवरपीसी मशीन प्रिंट करती है skcuS++C
एडम रोसेनफील्ड

27
मेरा शब्द, मुझे इस तरह से सवालों से नफरत है। यह मेमोरी में थोड़ा पैटर्न है जो कुछ मूर्खतापूर्ण स्ट्रिंग के समान होता है। यह किसी के लिए कोई उपयोगी उद्देश्य नहीं रखता है, और फिर भी यह प्रश्नकर्ता और उत्तरदाता दोनों के लिए सैकड़ों प्रतिनिधि अंक अर्जित करता है। इस बीच, कठिन प्रश्न जो लोगों के लिए उपयोगी हो सकते हैं, शायद कुछ मुट्ठी भर अंक अर्जित करें, यदि कोई हो। यह एसओ के साथ गलत क्या है के पोस्टर बच्चे की तरह है।
कैरी ग्रेगोरी

जवाबों:


494

संख्या 7709179928849219.0में 64-बिट के रूप में निम्नलिखित बाइनरी प्रतिनिधित्व है double:

01000011 00111011 01100011 01110101 01010011 00101011 00101011 01000011
+^^^^^^^ ^^^^---- -------- -------- -------- -------- -------- --------

+संकेत की स्थिति दिखाता है; ^प्रतिपादक की, और -मंटिसा की (यानी घातांक के बिना मूल्य)।

चूंकि प्रतिनिधित्व द्विआधारी घातांक और मंटिसा का उपयोग करता है, इसलिए संख्या को बढ़ाकर घातांक एक करके। आपका कार्यक्रम इसे 771 बार ठीक करता है, इसलिए 1075 (दशमलव प्रतिनिधित्व 10000110011) पर शुरू होने वाला घातांक 1075 + 771 = 1846 अंत में बन जाता है; 1846 का बाइनरी प्रतिनिधित्व है 11100110110। परिणामी पैटर्न इस तरह दिखता है:

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011
-------- -------- -------- -------- -------- -------- -------- --------
0x73 's' 0x6B 'k' 0x63 'c' 0x75 'u' 0x53 'S' 0x2B '+' 0x2B '+' 0x43 'C'

यह पैटर्न उस स्ट्रिंग से मेल खाता है जिसे आप मुद्रित करते हैं, केवल पीछे की ओर देखते हैं। इसी समय, सरणी का दूसरा तत्व शून्य हो जाता है, अशक्त टर्मिनेटर प्रदान करता है, जिससे स्ट्रिंग को पास होने के लिए उपयुक्त होता है printf()


22
स्ट्रिंग पीछे की ओर क्यों है?
डेरेक

95
@ डेरेक x86 थोड़ा-सा एंडियन है
Angew अब SO

16
@Derek इस प्लेटफ़ॉर्म-विशिष्ट की वजह से है endianness : सार आईईईई 754 प्रतिनिधित्व के बाइट्स तो स्ट्रिंग प्रिंट सही ढंग से, कम हो रही पते पर स्मृति में जमा हो जाती है। बड़े धीरज के साथ हार्डवेयर पर एक अलग संख्या के साथ शुरू करने की आवश्यकता होगी।
dasblinkenlight

14
@ एल्विनवोंग आप सही हैं, मानक को आईईईई 754 या किसी अन्य विशिष्ट प्रारूप की आवश्यकता नहीं है। इस कार्यक्रम के बारे में गैर-पोर्टेबल के रूप में यह हो जाता है, या इसके बहुत करीब है :-)
dasblinkenlight

10
@GrijeshChauhan मैंने एक डबल-सटीक IEEE754 कैलकुलेटर का उपयोग किया : मैंने 7709179928849219मान चिपकाया , और बाइनरी प्रतिनिधित्व वापस मिला।
dasblinkenlight

223

अधिक पठनीय संस्करण:

double m[2] = {7709179928849219.0, 771};
// m[0] = 7709179928849219.0;
// m[1] = 771;    

int main()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        main();
    }
    else
    {
        printf((char*) m);
    }
}

यह main()771 बार पुनरावर्ती कॉल करता है।

शुरुआत में m[0] = 7709179928849219.0, जिसके लिए खड़ा है C++Suc;C। हर कॉल में, m[0]अंतिम दो अक्षरों को "मरम्मत" करने के लिए दोगुना हो जाता है। अंतिम कॉल में, m[0]ASCII के चार प्रतिनिधित्व शामिल हैं C++Sucksऔर m[1]इसमें केवल शून्य हैं, इसलिए इसमें स्ट्रिंग के लिए एक शून्य टर्मिनेटर है C++Sucks। सभी अनुमानों के तहत m[0]8 बाइट्स पर संग्रहीत किया जाता है, इसलिए प्रत्येक चार 1 बाइट लेता है।

पुनरावृत्ति और अवैध main()कॉलिंग के बिना यह इस तरह दिखेगा:

double m[] = {7709179928849219.0, 0};
for (int i = 0; i < 771; i++)
{
    m[0] *= 2;
}
printf((char*) m);

8
यह पोस्टफिक्स डेक्रिमेंट है। तो इसे 771 बार कहा जाएगा।
जैक ऐडली

106

डिस्क्लेमर: यह उत्तर प्रश्न के मूल रूप में पोस्ट किया गया था, जिसमें केवल C ++ का उल्लेख था और इसमें C ++ हेडर शामिल था। शुद्ध सी से प्रश्न का रूपांतरण मूल प्रश्नकर्ता से इनपुट के बिना, समुदाय द्वारा किया गया था।


औपचारिक रूप से, इस कार्यक्रम के बारे में तर्क करना असंभव है क्योंकि यह बीमार है (यानी यह कानूनी सी ++ नहीं है)। यह C ++ 11 का उल्लंघन करता है [basic.start.main] p3:

फ़ंक्शन मेन का उपयोग किसी प्रोग्राम के भीतर नहीं किया जाएगा।

यह एक तरफ, यह इस तथ्य पर निर्भर करता है कि एक विशिष्ट उपभोक्ता कंप्यूटर पर, double8 बाइट्स लंबा है, और एक निश्चित प्रसिद्ध आंतरिक प्रतिनिधित्व का उपयोग करता है। सरणी के प्रारंभिक मानों की गणना इसलिए की जाती है कि जब "एल्गोरिथ्म" किया जाता है, तो पहले का अंतिम मान doubleऐसा होगा कि आंतरिक प्रतिनिधित्व (8 बाइट्स) 8 वर्णों का ASCII कोड होगा C++Sucks। सरणी में दूसरा तत्व तब है 0.0, जिसका पहला बाइट 0आंतरिक प्रतिनिधित्व में है, जिससे यह एक वैध सी-स्टाइल स्ट्रिंग है। यह तो उत्पादन का उपयोग करने के लिए भेजा है printf()

इसे HW पर चलाना जहाँ उपर्युक्त में से कुछ पकड़ में नहीं आता है, इसके बजाय कचरा पाठ (या शायद सीमा से बाहर पहुंच) होगा।


25
मुझे यह जोड़ना है कि यह C ++ 11 का आविष्कार नहीं है - C ++ 03 में भी basic.start.mainइसी शब्द के साथ 3.6.1 / 3 था ।
शार्प फुट

1
इस छोटे उदाहरण का मुद्दा यह बताना है कि C ++ के साथ क्या किया जा सकता है। यूबी ट्रिक्स या "क्लासिक" कोड के विशाल सॉफ़्टवेयर पैकेज का उपयोग करके जादू का नमूना।
SChepurin

1
@ अक्षरशः इस जोड़ने के लिए धन्यवाद। मेरा मतलब यह नहीं था कि अन्यथा, मैंने केवल उस मानक का हवाला दिया जिसका मैंने उपयोग किया था।
Angew को अब SO

@Angew: हाँ, मैं समझता हूँ कि, केवल यह कहना चाहता था कि शब्दांकन बहुत पुराना है।
शार्प फुट

1
@JimBalter नोटिस मैंने कहा "औपचारिक रूप से बोलना, यह तर्क के लिए असंभव है," नहीं "औपचारिक रूप से यह असंभव है।" आप सही हैं कि कार्यक्रम के बारे में तर्क करना संभव है, लेकिन आपको ऐसा करने के लिए उपयोग किए गए कंपाइलर का विवरण जानना होगा। यह पूरी तरह से एक कंपाइलर के अधिकार के भीतर होगा कि वह कॉल को समाप्त कर दे main(), या इसे हार्ड ड्राइव को प्रारूपित करने के लिए एपीआई कॉल से बदल दे, या जो भी हो।
Angew को अब SO

57

शायद कोड को समझने का सबसे आसान तरीका रिवर्स में चीजों के माध्यम से काम करना है। हम प्रिंट करने के लिए एक स्ट्रिंग के साथ शुरू करेंगे - संतुलन के लिए, हम "C ++ Rocks" का उपयोग करेंगे। महत्वपूर्ण बिंदु: मूल की तरह, यह बिल्कुल आठ वर्ण लंबा है। चूंकि हम मूल की तरह (लगभग) ऐसा करने जा रहे हैं, और इसे उल्टे क्रम में प्रिंट करते हैं, हम इसे रिवर्स ऑर्डर में डालकर शुरू करेंगे। हमारे पहले चरण के लिए, हम उस बिट पैटर्न को एक के रूप में देखेंगे double, और परिणाम का प्रिंट आउट लेंगे :

#include <stdio.h>

char string[] = "skcoR++C";

int main(){
    printf("%f\n", *(double*)string);
}

यह पैदा करता है 3823728713643449.5। इसलिए, हम किसी तरह से हेरफेर करना चाहते हैं जो स्पष्ट नहीं है, लेकिन रिवर्स करना आसान है। मैं अर्ध-मनमाने ढंग से गुणा करके 256 का चयन करूंगा, जो हमें देता है 978874550692723072। अब, हमें केवल 256 से विभाजित करने के लिए कुछ ओफ़्फ़ुसेटेड कोड लिखने की आवश्यकता है, फिर उस के अलग-अलग बाइट्स को उल्टा करके प्रिंट करें:

#include <stdio.h>

double x [] = { 978874550692723072, 8 };
char *y = (char *)x;

int main(int argc, char **argv){
    if (x[1]) {
        x[0] /= 2;  
        main(--x[1], (char **)++y);
    }
    putchar(*--y);
}

अब हमारे पास बहुत से कास्टिंग, पास करने के तर्क हैं (पुनरावर्ती) mainजिन्हें पूरी तरह से नजरअंदाज कर दिया गया है (लेकिन वेतन वृद्धि और वेतन वृद्धि प्राप्त करने के लिए मूल्यांकन पूरी तरह से महत्वपूर्ण है), और निश्चित रूप से इस तथ्य को कवर करने के लिए पूरी तरह से मनमाना लग रहा है कि हम क्या कर रहे हैं? वास्तव में बहुत सीधा है।

बेशक, चूँकि पूरा बिंदु आपत्तिजनक है, अगर हमें ऐसा लगता है कि हम और भी कदम उठा सकते हैं। उदाहरण के लिए, हम शॉर्ट-सर्किट मूल्यांकन का लाभ उठा सकते हैं, अपने ifकथन को एक एकल अभिव्यक्ति में बदल सकते हैं , इसलिए मुख्य का शरीर इस तरह दिखता है:

x[1] && (x[0] /= 2,  main(--x[1], (char **)++y));
putchar(*--y);

किसी ऐसे व्यक्ति के लिए जो आचार संहिता (और / या कोड गोल्फ) का आदी नहीं है, यह वास्तव में बहुत अजीब लग रहा है - कंप्यूटिंग और andकुछ अर्थहीन फ्लोटिंग पॉइंट नंबर की तार्किकता को त्यागना और वापसी मूल्य main, जो कि वापस नहीं आ रहा है मूल्य। इससे भी बदतर, यह एहसास किए बिना (और इस बारे में) कि शॉर्ट-सर्किट मूल्यांकन कैसे काम करता है, यह तुरंत स्पष्ट भी नहीं हो सकता है कि यह अनंत पुनरावृत्ति से कैसे बचा जाता है।

हमारा अगला कदम संभवतः प्रत्येक चरित्र को उस चरित्र को खोजने से अलग करना होगा। हम उस सही मूल्य को आसानी से वापस मूल्य के रूप में उत्पन्न करके main, और क्या mainरिटर्न प्रिंट कर सकते हैं :

x[1] && (x[0] /= 2,  putchar(main(--x[1], (char **)++y)));
return *--y;

कम से कम मेरे लिए, यह पर्याप्त रूप से बाधित लगता है, इसलिए मैं इसे उस पर छोड़ दूंगा।


1
फोरेंसिक दृष्टिकोण से प्यार करें।
रयकर

24

यह सिर्फ एक डबल ऐरे (16 बाइट्स) का निर्माण कर रहा है, जिसे - अगर एक चार सरणी के रूप में व्याख्या किया जाता है - स्ट्रिंग "सी ++ सक्स" के लिए एएससीआईआई कोड बनाएं।

हालाँकि, कोड प्रत्येक प्रणाली पर काम नहीं कर रहा है, यह कुछ अपरिभाषित तथ्यों पर निर्भर करता है:

  • डबल में ठीक 8 बाइट्स हैं
  • endianness

12

निम्नलिखित कोड प्रिंट करता है C++Suc;C, इसलिए संपूर्ण गुणा केवल अंतिम दो अक्षरों के लिए है

double m[] = {7709179928849219.0, 0};
printf("%s\n", (char *)m);

11

अन्य लोगों ने सवाल को अच्छी तरह से समझाया है, मैं एक नोट जोड़ना चाहूंगा कि यह मानक के अनुसार अपरिभाषित व्यवहार है

C ++ 11 3.6.1 / 3 मुख्य कार्य

फ़ंक्शन मेन का उपयोग किसी प्रोग्राम के भीतर नहीं किया जाएगा। मुख्य का लिंकेज (3.5) कार्यान्वयन-परिभाषित है। एक प्रोग्राम जो मुख्य को हटाए गए के रूप में परिभाषित करता है या जो मुख्य को इनलाइन, स्टेटिक, या कॉन्स्टैक्स होने की घोषणा करता है, बीमार है। मुख्य नाम अन्यथा आरक्षित नहीं है। [उदाहरण: सदस्य कार्यों, वर्गों और गणनाओं को मुख्य कहा जा सकता है, जैसा कि अन्य नामस्थानों में इकाइयाँ कर सकती हैं। उदाहरण का]


1
मैं कहता हूं कि यह और भी बीमार है (जैसा कि मैंने अपने जवाब में किया था) - यह "करेगा" का उल्लंघन करता है।
Angew अब SO

9

कोड को इस तरह दोबारा लिखा जा सकता है:

void f()
{
    if (m[1]-- != 0)
    {
        m[0] *= 2;
        f();
    } else {
          printf((char*)m);
    }
}

यह क्या कर रहा है doubleसरणी में बाइट्स का एक सेट पैदा कर रहा है mजो वर्णों के अनुरूप होने के लिए होता है 'C ++ Sucks' जिसके बाद null-termator होता है। उन्होंने दोहरे मान का चयन करके कोड को बाधित कर दिया है, जो जब 771 बार दोगुना होता है, तो मानक प्रतिनिधित्व में, सरणी के दूसरे सदस्य द्वारा प्रदान किए गए शून्य टर्मिनेटर के साथ बाइट्स का सेट।

ध्यान दें कि यह कोड एक अलग एंडियन प्रतिनिधित्व के तहत काम नहीं करेगा। इसके अलावा, कॉलिंग main()की सख्ती से अनुमति नहीं है।


3
आपकी fवापसी एक क्यों है int?
वामावर्तबाउट

1
एर, 'क्योंकि मैं intसवाल में वापसी की नकल कर रहा था । मुझे वह ठीक करने दो।
जैक आइडले

1

पहले हमें याद रखना चाहिए कि द्विआधारी प्रारूप में डबल सटीक संख्याएं मेमोरी में संग्रहीत की जाती हैं:

(i) संकेत के लिए 1 बिट

(ii) प्रतिपादक के लिए ११ बिट्स

(iii) परिमाण के लिए 52 बिट्स

बिट्स का क्रम (i) से (iii) तक घट जाता है।

पहले दशमलव भिन्नात्मक संख्या को समकक्ष भिन्नात्मक संख्या में परिवर्तित किया जाता है और फिर इसे बाइनरी में परिमाण के क्रम के रूप में व्यक्त किया जाता है।

तो नंबर 7709179928849219.0 हो जाता है

(11011011000110111010101010011001010110010101101000011)base 2


=1.1011011000110111010101010011001010110010101101000011 * 2^52

अब परिमाण बिट्स पर विचार करते समय 1. उपेक्षा की जाती है क्योंकि परिमाण विधि के सभी क्रम 1 से शुरू होंगे

तो परिमाण भाग बन जाता है:

1011011000110111010101010011001010110010101101000011 

अब की शक्ति 2 है 52 है, हम के रूप में यह करने के लिए संख्या biasing जोड़ने की जरूरत है 2 ^ (प्रतिपादक के लिए बिट्स -1) -1 यानी 2 ^ (11 -1) -1 = 1023 , इसलिए हमारे प्रतिपादक हो जाता है 52 + 1023 = 1075

अब हमारा कोड 2 , 771 बार संख्या को बदल देता है जो कि घातांक को 771 तक बढ़ा देता है

तो हमारा घातांक है (1075 + 771) = 1846 जिसका बाइनरी समतुल्य है (11100110110)

अब हमारी संख्या सकारात्मक है इसलिए हमारा साइन बिट 0 है

तो हमारी संशोधित संख्या बन जाती है:

साइन बिट + प्रतिपादक + परिमाण (बिट्स का सरल संयोजन)

0111001101101011011000110111010101010011001010110010101101000011 

चूंकि मी चार सूचक में परिवर्तित हो गया है इसलिए हम एलएसडी से 8 के भाग में बिट पैटर्न को विभाजित करेंगे

01110011 01101011 01100011 01110101 01010011 00101011 00101011 01000011 

(हेक्स समतुल्य है :)

 0x73 0x6B 0x63 0x75 0x53 0x2B 0x2B 0x43 

ASCII चार्ट जैसा कि चरित्र मानचित्र से दिखाया गया है:

s   k   c   u      S      +   +   C 

अब एक बार इसे m [1] बना दिया गया है, जिसका अर्थ है NULL वर्ण

अब यह मानते हुए कि आप इस प्रोग्राम को थोड़ा-सा एंडियन मशीन पर चलाते हैं (निचला ऑर्डर बिट कम एड्रेस में स्टोर किया जाता है) इसलिए पॉइंटर एम पॉइंटर को सबसे कम एड्रेस बिट तक ले जाते हैं और फिर 8 के चक में बिट्स उठाकर आगे बढ़ते हैं (जैसा कि चारो को टाइप किया गया * ) और प्रिंट () बंद हो जाता है जब आखिरी chunck में 00000000 का सामना किया ...

यह कोड हालांकि पोर्टेबल नहीं है।

हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.