मुख्य सामग्री पर जाएँ

व्यंजक का मान और सुरक्षित अपडेट

पाठ 2 / 1315 मिनटPDF नोट्समुफ़्त

पूर्णांक ऋणात्मक होने पर क्या बदलता है?

C
#include <stdio.h>

int main(void)
{
    int a = -19;
    int b = 4;
    int q = a / b;
    int r = a % b;
    int z = 2 + q * 3;
    printf("%d %d %d\n", q, r, z);
    return 0;
}

भागफल हमेशा नीचे की ओर राउंड करके नहीं मिलता और शेषफल हमेशा धनात्मक नहीं होता। इस प्रोग्राम का ट्रेस करने के लिए पूर्णांक भाग का सही नियम चाहिए। इसी पाठ में आगे हम व्यंजक से मिलने वाले मान को उससे चर में होने वाले बदलाव से अलग रखेंगे।

पिछले ट्रेस के नियम साथ रखें

पिछले पाठ की वर्तमान अवस्था वाली विधि, इनिशियलाइज़ेशन, सरल असाइनमेंट, अंकगणितीय समूह और %d वाला आउटपुट आना चाहिए। नीचे के छोटे कोड ब्लॉक ऊपर दिखाए गए पूरे ढाँचे के अलग-अलग मुख्य भाग हैं: #include <stdio.h>, int main(void), ब्रेसिज़ और अंत में return 0;। हर ब्लॉक अपनी शुरुआती अवस्था से शुरू होता है।

हम ISO C11, ज्ञात शुरुआती signed int मान और -32767 से 32767 के भीतर के बीच के परिणाम लेते रहेंगे। वास्तव में होने वाली हर भाग-गणना का भाजक शून्य नहीं है और भागफल int में आ सकता है। एक असुरक्षित व्यंजक को केवल पहचानने के लिए अलग चिह्नित किया जाएगा; उसे चलाकर आउटपुट का अनुमान नहीं लगाना है।

पूर्णांक भाग: शून्य की ओर जाएँ

पूर्णांक ऑपरेंड के लिए / गणितीय भागफल का भिन्न वाला हिस्सा हटा देता है। ऋणात्मक तरफ कौन-सा पूर्णांक बचेगा, यह “शून्य की ओर” से समझ आता है। -19 को 4 से भाग देने पर गणितीय भागफल -4.75 है; भिन्न वाला हिस्सा हटाने पर -4 मिलता है, जो शून्य के अधिक पास है। -5 नहीं मिलता।

शेषफल का चिह्न अंदाज़ से चुनने के बजाय तीन कदम लें:

  1. शून्य की ओर काटकर पूर्णांक भागफल q निकालें।
  2. r = a - q * b से शेषफल निकालें।
  3. जाँचें कि q * b + r से फिर a मिलता है।

ये गणितीय बराबरियाँ मानों के संबंध बताती हैं; यहाँ लिखी हर बराबरी प्रोग्राम में चिपकाने योग्य C असाइनमेंट नहीं है। इस पाठ की सीमित संख्याओं के लिए जाँच में होने वाला गुणा और घटाव भी int के भीतर सुरक्षित हैं।

शेषफल या तो शून्य है या उसका चिह्न भाज्य a के चिह्न जैसा है। उसका परिमाण भाजक b के परिमाण से छोटा है। भाजक का चिह्न भागफल को प्रभावित करता है, लेकिन गैर-शून्य शेषफल का चिह्न उससे तय नहीं होता। यह C का शेषफल है; इसे किसी दूसरी भाषा के ऐसे नियम से न बदलें जिसमें धनात्मक भाजक के साथ परिणाम हमेशा गैर-ऋणात्मक होता हो।

हल किया हुआ उदाहरण 1: भागफल, शेषफल, फिर व्यंजक

शुरुआती प्रोग्राम को घोषणाओं से ट्रेस करें।

  1. पहली दो घोषणाओं के बाद: a=-19, b=4।
  2. q = a / b से -4 मिलता है, क्योंकि -4.75 का भिन्न वाला हिस्सा शून्य की ओर हटाया जाता है। अब q=-4।
  3. r = a % b से -3 मिलता है। जाँचें: (-4) * 4 = -16 और -19 - (-16) = -3। अब r=-3।
  4. z = 2 + q * 3 में गुणा की प्राथमिकता अधिक है: q * 3 = -12, फिर 2 + (-12) = -10। अब z=-10।
  5. q, r, z के क्रम में आउटपुट -4 -3 -10 है, फिर नई पंक्ति आती है। a और b में से किसी का मान नहीं बदला।

जोड़ी -5, 1 से फिर -19 बनता है, लेकिन उसमें नीचे की ओर राउंड किया गया है, इसलिए वह C11 की भागफल-शेषफल जोड़ी नहीं है। केवल कुल संख्या फिर बना लेना पर्याप्त नहीं है: भागफल शून्य की ओर काटने वाले नियम से भी मेल खाना चाहिए। जोड़ी -4, 3 का भागफल सही है, लेकिन उससे मूल संख्या नहीं बनती: -16 + 3 का मान -13 है।

पास के तीन मामलों में चिह्न का नियम जाँचें

C
printf("%d %d\n", 19 / -4, 19 % -4);
printf("%d %d\n", -19 / -4, -19 % -4);
printf("%d %d\n", -20 / 4, -20 % 4);
  1. 19 / -4 से -4 मिलता है; 19 - (-4)*(-4) = 3। पहली पंक्ति -4 3 है।
  2. -19 / -4 से 4 मिलता है; -19 - 4*(-4) = -3। दूसरी पंक्ति 4 -3 है।
  3. -20 / 4 से -5 मिलता है; -20 - (-5)*4 = 0। तीसरी पंक्ति -5 0 है।

ऋणात्मक भाज्य के साथ भी शेषफल शून्य हो सकता है। “शेषफल का चिह्न भाज्य जैसा है” कहते समय शून्य वाला अपवाद ज़रूरी है। ये कॉल केवल स्थिर संख्याएँ पढ़ते हैं, इसलिए आर्ग्युमेंट के मान निकालने का क्रम परिणाम नहीं बदल सकता।

समान प्राथमिकता वाले ऑपरेटरों का समूह

*, / और % की प्राथमिकता समान है और उनका समूह बाईं ओर से बनता है। जैसे 18 / 4 * 3 का समूह (18 / 4) * 3 है। पहले भागफल 4 है, फिर गुणनफल 12 है। समूह 18 / (4 * 3) नहीं है, जिसका परिणाम 1 होगा।

भिन्न वाला हिस्सा बीच की किसी भाग-गणना में ही हट सकता है। सामान्यतः सभी गणनाएँ भिन्नों के साथ करके केवल अंत में हिस्सा हटाना सही नहीं है: C में (18 / 4) * 3 का भागफल गुणा में इस्तेमाल होने से पहले ही अपना भिन्न वाला हिस्सा खो चुका है। कोष्ठक यह बदल सकते हैं कि कौन-सा भाग माँगा गया है। वे सामान्यतः चर बदलने वाले प्रभावों का बाएँ-से-दाएँ समय-क्रम तय नहीं करते। इस हिस्से के उदाहरणों में अंकगणितीय व्यंजक के भीतर कोई चर नहीं बदलता।

मान निकालना और चर बदलना अलग हैं

C
int x = 4;
int next = x + 1;
printf("%d %d\n", x, next);

x + 1 से 5 मिलता है, जिससे next का शुरुआती मान रखा जाता है। इससे x में कुछ नहीं लिखा जाता। आउटपुट 4 5 है। इसके विपरीत, x = x + 1; परिणाम को वापस x में रखेगा। जब भी व्यंजक से चर बदल सकता हो, दो प्रश्न पूछें: “यह कौन-सा मान देता है?” और “यह स्टेटमेंट पूरा होने पर अवस्था क्या है?”

एक चर पर संयुक्त असाइनमेंट

यहाँ लिए गए साधारण int चर के लिए score += 5; का संख्यात्मक प्रभाव score = score + 5; जैसा है। इसी तरह score -= 4; में 4 घटाकर परिणाम रखा जाता है और score /= 5; में पूर्णांक भागफल रखा जाता है। इन ऑपरेटरों में गणना और असाइनमेंट दोनों हैं; += केवल + लिखने का दूसरा तरीका नहीं है।

C
int score = 17;
score += 5;
score -= 4;
score /= 5;
printf("%d\n", score);
  1. इनिशियलाइज़ेशन से score=17।
  2. score += 5; के बाद: score=22।
  3. score -= 4; के बाद: score=18।
  4. score /= 5; के बाद: score=3, क्योंकि पूर्णांक 18 / 5 का मान 3 है।
  5. आउटपुट 3 है, फिर नई पंक्ति आती है।

संयुक्त रूप में बाईं ओर की परिणाम रखने वाली जगह का मूल्यांकन केवल एक बार होता है। इस पाठ में वह एक साधारण चर है, इसलिए जगह ढूँढ़ने की कोई अतिरिक्त गणना ट्रेस नहीं करनी पड़ती। विस्तार वाले रूप को अधिक जटिल व्यंजकों की कॉपी करने की अनुमति न समझें। संयुक्त असाइनमेंट पर भी वही गणितीय जाँचें लागू हैं: वास्तव में शून्य से भाग नहीं होना चाहिए और signed परिणाम सीमा से बाहर नहीं जाना चाहिए।

एक बढ़ाना: केवल नारा नहीं, दो अलग प्रश्न

इन्क्रिमेंट ऑपरेटर ++ किसी चर में एक जोड़ता है। उसकी जगह तय करती है कि व्यंजक कौन-सा मान देगा:

  • x++ को postfix increment कहते हैं। व्यंजक x का पुराना मान देता है। साथ ही चर का मान एक बढ़ता है
  • ++x को prefix increment कहते हैं। व्यंजक x का बढ़ा हुआ मान देता है

बढ़ने की मात्रा दोनों में अलग नहीं है: दोनों x को एक बढ़ाते हैं। पुराना मान 3 हो तो अलग x++; या ++x; स्टेटमेंट पूरा होने के बाद x=4 होगा। फर्क तब महत्वपूर्ण है जब व्यंजक का मान किसी दूसरे चर में रखा जाए।

हर अपडेट को हम अपने अलग पूर्ण व्यंजक (full expression) में रखते हैं। int before = x++; का इनिशियलाइज़र एक पूर्ण व्यंजक है; अलग अपडेट स्टेटमेंट का व्यंजक भी ऐसा ही है। अगले पूर्ण व्यंजक के शुरू होने से पहले इसके आवश्यक बदलाव पूरे हो जाते हैं। Postfix का अर्थ “किसी बाद के स्टेटमेंट तक अपडेट टाल दो” नहीं है। कई अलग स्टेटमेंट को एक संयुक्त व्यंजक में बदलकर यह न मानें कि अर्थ वही रहेगा।

हल किया हुआ उदाहरण 2: मिलने वाला मान और अंतिम अवस्था

C
int x = 3;
int before = x++;
int after = ++x;
printf("%d %d %d\n", before, after, x);
  1. int x = 3; के बाद: x=3।
  2. int before = x++; में postfix व्यंजक before को शुरू करने के लिए 3 देता है। इस घोषणा के अंत तक x बढ़कर 4 हो गया है। अवस्था: x=4, before=3।
  3. int after = ++x; में x, 4 से बढ़कर 5 होता है और prefix व्यंजक after को शुरू करने के लिए 5 देता है। अवस्था: x=5, before=3, after=5।
  4. आउटपुट 3 5 5 है, फिर नई पंक्ति आती है, क्योंकि आर्ग्युमेंट का क्रम before, after, फिर x है।

before में 3 ही रहता है; वह रखे गए मान की कॉपी है, x का दूसरा नाम नहीं। यदि 3 4 5 मिला, तो prefix व्यंजक को पुराना मान दे दिया गया। यदि 4 5 5 मिला, तो postfix व्यंजक को नया मान दे दिया गया। दोनों गलतियों से बचने के लिए व्यंजक से मिला मान और उसके बाद की अवस्था अलग-अलग लिखें।

सुरक्षा की सही सीमा

C साधारण + के ऑपरेंड को बाएँ से दाएँ मूल्यांकित करने का वादा नहीं करता। यदि कोई व्यंजक एक चर को बदलता भी है और उसी चर को दूसरी जगह आवश्यक क्रम के बिना पढ़ता भी है, तो उसमें अपरिभाषित व्यवहार (undefined behavior) हो सकता है। इसका अर्थ है कि C11 प्रोग्राम का कोई निर्धारित परिणाम नहीं देता। यह केवल दो सामान्य उत्तरों में से एक चुनने की बात नहीं है और क्रैश होना भी आवश्यक नहीं है।

हल किए हुए उदाहरण 2 की तरह अपडेट अलग-अलग पूर्ण व्यंजकों में रखें। अंकगणितीय समूह बदलने वाले कोष्ठक अपने-आप ऑपरेंड के बीच आवश्यक क्रम नहीं बनाते। असुरक्षित व्यंजक के लिए किसी कंपाइलर पर एक बार मिला परिणाम नियम नहीं बन जाता। अभ्यास प्रश्न 4 में संख्या का अनुमान लगाने के बजाय यही सीमा पहचाननी है।

अभ्यास: हल पढ़ने से पहले प्रयास करें

इन चार मौलिक प्रश्नों के अंक नहीं हैं। हर निश्चित उदाहरण में व्यंजकों के मान और अंतिम अवस्था लिखें। असुरक्षित उदाहरण के लिए संख्या की जगह उसके व्यवहार का वर्गीकरण करें।

अभ्यास प्रश्न 1: ऋणात्मक भाज्य

C11 के अनुसार भागफल और शेषफल निकालिए। आउटपुट लिखिए और मूल संख्या फिर बनाकर जाँच दिखाइए।

C
int a=-23,b=6;
printf("%d %d\n",a/b,a%b);

अभ्यास प्रश्न 2: इन्क्रिमेंट के दो रूप

अंतिम n और p, q में रखे मान निकालिए। प्रिंट होने वाले आर्ग्युमेंट के क्रम का ध्यान रखें।

C
int n=4;
int p=++n;
int q=n++;
printf("%d %d %d\n",n,p,q);

अभ्यास प्रश्न 3: कोष्ठक का प्रभाव

दोनों परिणाम निकालिए और समझाइए कि वे अलग क्यों हैं।

C
int a=24/5*2;
int b=24/(5*2);
printf("%d %d\n",a,b);

अभ्यास प्रश्न 4: वर्गीकरण करें, चलाएँ नहीं

क्या इस अंश में C11, r का परिभाषित मान देता है? समझाइए कि यदि बाद में r प्रिंट किया जाए, तो क्या एक निश्चित संख्यात्मक आउटपुट माँगना सही होगा। संख्या चुनने के लिए इस अंश को न चलाएँ।

C
int i=4;
int r=i++ + i;

अभ्यास के हल और गलत उत्तरों का कारण

अभ्यास प्रश्न 1 का हल

  1. -23 और 6 का गणितीय भागफल -4 और -3 के बीच है; शून्य की ओर काटने पर q=-3 मिलता है।
  2. q*b = (-3)*6 = -18।
  3. शेषफल a-q*b = -23-(-18) = -5 है।
  4. जाँच: (-3)*6+(-5)=-23; शेषफल का परिमाण 5, 6 से छोटा है और उसका चिह्न ऋणात्मक भाज्य जैसा है।
  5. आउटपुट: -3 -5, फिर नई पंक्ति।

-4 1 में C11 के शून्य की ओर काटने के बजाय floor division लिया गया है। -3 5 गलत तरीके से शेषफल को धनात्मक बनाता है और उससे -23 के बजाय -13 बनता है। -3 -1 का चिह्न संभव लगता है, लेकिन गणना की जाँच विफल होती है: -18-1=-19।

अभ्यास प्रश्न 2 का हल

  1. शुरुआत में n=4।
  2. int p=++n; पहले n को 5 करता है और p को 5 देता है। अवस्था: n=5, p=5।
  3. int q=n++; वर्तमान 5 को q में देता है और घोषणा के अंत तक n को 6 करता है। अवस्था: n=6, p=5, q=5।
  4. प्रिंट का क्रम n, p, q है, इसलिए आउटपुट 6 5 5 है, फिर नई पंक्ति।

6 5 6 postfix को नया मान देता है। 5 5 5 भूल जाता है कि postfix फिर भी n बदलता है। 6 4 5 prefix को पुराना मान देता है। printf चलने से पहले दोनों अपडेट हो चुके हैं।

अभ्यास प्रश्न 3 का हल

  1. पहली घोषणा में समान प्राथमिकता वाले / और * का समूह बाईं ओर से है: a=(24/5)*2।
  2. पूर्णांक 24/5 से 4 मिलता है; फिर 4*2=8। इसलिए a=8।
  3. दूसरी घोषणा में कोष्ठक भाजक 5*2=10 बनाते हैं। पूर्णांक 24/10 से 2 मिलता है। इसलिए b=2।
  4. आउटपुट: 8 2, फिर नई पंक्ति।

2 2 पहले व्यंजक में बिना कारण नए कोष्ठक जोड़ देता है। 9 2 बीच में भिन्न वाला 4.8 रखता है और गुणा के बाद ही हिस्सा हटाता है; C व्यंजक में पहले ही पूर्णांक भागफल 4 आ चुका था। 8 8 दूसरी घोषणा के कोष्ठक अनदेखे करता है। समझाने के लिए समूह और पूर्णांक भाग, दोनों ज़रूरी हैं।

अभ्यास प्रश्न 4 का हल

इस अंश में C11 के अनुसार अपरिभाषित व्यवहार है। i++ + i में इन्क्रिमेंट द्वारा i को बदलना और दूसरे ऑपरेंड द्वारा i को पढ़ना एक-दूसरे के सापेक्ष unsequenced हैं, यानी इनके बीच आवश्यक क्रम निर्धारित नहीं है। साधारण + इसे सुरक्षित बनाने वाला क्रम नहीं देता। नियम N1570 §6.5p2 में है।

r के लिए C11 द्वारा निर्धारित कोई संख्यात्मक मान नहीं है और सही ट्रेस-प्रश्न एक निश्चित आउटपुट नहीं माँग सकता। 8 और 9 कल्पना किए गए मूल्यांकन-समय पर आधारित लुभावने उत्तर हैं, मानक से परिभाषित उत्तर नहीं। “8 या 9 में से कोई एक” भी गलत है: अपरिभाषित व्यवहार इम्प्लीमेंटेशन को केवल इन दो परिणामों तक सीमित नहीं करता। “कंपाइल होना ज़रूर विफल होगा” और “क्रैश होना ही चाहिए” की भी गारंटी नहीं है। i++ के चारों ओर या पूरे जोड़ के चारों ओर कोष्ठक लगाने से क्रम की समस्या ठीक नहीं होती।

यदि इरादा “पुराना मान रखें, एक बढ़ाएँ, फिर पुराने और नए मान को जोड़ें” है, तो ये चरण अलग-अलग लिखें:

C
int i = 4;
int old = i;
i = i + 1;
int r = old + i;
printf("%d %d\n", r, i);
  1. शुरुआत में i=4।
  2. old=i से old में 4 रखा जाता है।
  3. i=i+1 से i=5 होता है।
  4. r=old+i में 4+5=9 लिया जाता है।
  5. r और i के लिए आउटपुट 9 5 है, फिर नई पंक्ति।

यह स्पष्ट रूप से चुने गए इरादे वाला नया, परिभाषित प्रोग्राम है। इससे यह सिद्ध नहीं होता कि असुरक्षित अंश का “वास्तविक अर्थ 9 था”। उस असुरक्षित अंश का कोई परिभाषित अर्थ था ही नहीं जिसे बचाया जा सके।

आगे बढ़ने से पहले

अब आप गणना और चर में रखे जाने वाले अपडेट का अंतर समझते हैं, अलग पूर्ण व्यंजकों में prefix और postfix का ट्रेस कर सकते हैं तथा C11 का भागफल-शेषफल नियम लगा सकते हैं। अगले पाठ में शर्तें व्यंजकों के मान का उपयोग करके तय करेंगी कि कौन-से स्टेटमेंट चलेंगे।

स्रोत संबंधी नोट

कोड, गणनाएँ और गलतियों की व्याख्याएँ मौलिक हैं। WG14 N1570 C11 समिति-ड्राफ्ट में सटीक संदर्भ: पूर्णांक भाग/शेषफल §6.5.5p5–6; बाईं ओर से समूह §6.5.5p1; संयुक्त असाइनमेंट §6.5.16.2p3; postfix §6.5.2.4p2; prefix §6.5.3.1p2; पूर्ण व्यंजक §6.8p4 और उनका क्रम §5.1.2.3p3; बिना आवश्यक क्रम के बदलाव/पढ़ना §6.5p2; सीमा से बाहर के परिणाम §6.5p5। कंपाइलर का आउटपुट परिभाषित उदाहरणों की जाँच में सहायक है, लेकिन वही भाषा की परिभाषा नहीं है।

उदाहरण से समझें

एक काउंटर और छोटी रसीद सोचें। Postfix में रसीद पर काउंटर का पुराना मान आता है और काउंटर एक बढ़ता है; prefix में काउंटर बढ़ता है और रसीद पर नया मान आता है। पूरा काम होने पर दोनों रूपों में काउंटर एक बढ़ चुका है, लेकिन रसीदों के मान अलग हो सकते हैं। अगला काम शुरू करने से पहले एक काम पूरा करें। यह उदाहरण अलग-अलग पूर्ण व्यंजक समझने में मदद करता है; एक ही C व्यंजक में कई अपडेट भर देने पर उनका कोई सुरक्षित क्रम तय नहीं करता।

सूत्र और नियम

  • C11 में पूर्णांक / शून्य की ओर काटता है: -19 / 4 का मान -4 है
  • गैर-शून्य b और सीमा में आने वाले भागफल के लिए: (a / b) * b + a % b का मान a है
  • गैर-शून्य शेषफल का चिह्न भाज्य जैसा है; उसका परिमाण भाजक के परिमाण से छोटा है
  • * / % का समूह बाईं ओर से है: 24 / 5 * 2 का अर्थ (24 / 5) * 2 है
  • साधारण int चर पर x += k, x -= k और x /= k संबंधित गणना का परिणाम रखते हैं
  • x + 1 मान निकालता है, x नहीं बदलता; x = x + 1 उसे रखता है
  • x++ पुराना मान देता है; ++x नया मान देता है; दोनों x को एक बढ़ाते हैं
  • अलग पूर्ण व्यंजकों में दिखाए गए अपडेट अगले व्यंजक से पहले पूरे होते हैं
  • i++ + i में अपरिभाषित व्यवहार है, कोई निर्धारित संख्यात्मक उत्तर नहीं
  • कोष्ठक समूह तय करते हैं; वे सामान्यतः बदलावों का समय-क्रम तय नहीं करते
  • हर बीच का परिणाम सीमा में रखें और वास्तव में उपयोग होने वाला हर भाजक गैर-शून्य रखें

इस पाठ के नोट्स

प्रगति सहेजने के लिए साइन इन करें। साइन इन