الغوص في الأنظمة › 4. التمثيل الثنائي والبيانات 5 / 18 السابق التالي

4. الثنائي وتمثيل البيانات

من الألواح الحجرية البسيطة ورسومات الكهوف إلى الكلمات المكتوبة وأخاديد الفونوغراف، سعى البشر دائماً إلى تسجيل المعلومات وتخزينها. وفي هذا الفصل، سنوصّف كيف تمثّل الحوسبة الرقمية، أحدث اختراقات البشرية الكبرى في التخزين، المعلومات. كما نوضح كيفية استخلاص المعنى من البيانات الرقمية.

تستخدم الحواسيب الحديثة وسائط متنوعة لتخزين المعلومات (مثل الأقراص المغناطيسية والأقراص الضوئية وذاكرة الفلاش والأشرطة والدوائر الكهربائية البسيطة). ونوصّف أجهزة التخزين لاحقاً في الفصل 11؛ غير أنه في هذه المناقشة لا يهم الوسط إلى حد كبير — فسواء أكان هناك ليزر يمسح سطح قرص DVD أم رأس قرص ينزلق فوق طبق مغناطيسي، فإن ناتج جهاز التخزين في النهاية تسلسل من الإشارات الكهربائية. ولتبسيط الدوائر، تكون كل إشارة ثنائية (binary)، أي أنها لا تأخذ إلا إحدى حالتين: غياب الجهد الكهربائي (ويُفسَّر بصفر) ووجود الجهد الكهربائي (واحد). يستكشف هذا الفصل كيفية ترميز الأنظمة للمعلومات إلى ثنائي، بصرف النظر عن وسط التخزين الأصلي.

في النظام الثنائي، تقابل كل إشارة بتّة (bit) واحدة (رقم ثنائي) من المعلومات: صفر أو واحد. وقد يثير الدهشة أن جميع البيانات يمكن تمثيلها باستخدام الأصفار والآحاد فقط. وبالطبع، كلما زاد تعقيد المعلومات، زاد عدد البتّات اللازمة لتمثيلها. ولحسن الحظ، يتضاعف عدد القيم الفريدة مع كل بتّة إضافية في تسلسل البتّات، لذا يستطيع تسلسل من N بتّة تمثيل 2N قيمة فريدة.

يوضح الشكل 1 نمو عدد القيم القابلة للتمثيل مع زيادة طول تسلسل البتّات. يمكن لبتّة واحدة تمثيل قيمتين: 0 و1. ويمكن لبتّتين تمثيل أربع قيم: قيمتا البتّة الواحدة مع صفر بادئ (00 و01)، وقيمتا البتّة الواحدة مع واحد بادئ (10 و11). وينطبق النمط نفسه على أي بتّة إضافية توسّع تسلسل بتّات موجوداً: فقد تكون البتّة الجديدة 0 أو 1، وفي كلتا الحالتين تمثّل البتّات المتبقية نطاق القيم نفسه الذي كانت تمثله قبل إضافة البتّة الجديدة. وبذلك، تؤدي إضافة بتّات إضافية إلى زيادة عدد القيم التي يستطيع التسلسل الجديد تمثيلها زيادة أُسّية.

ببتّة واحدة يمكننا تمثيل قيمتين، وببتّتين نحصل على أربع قيم، وبثلاث بتّات يمكننا تمثيل ثماني قيم، وأربع بتّات تنتج 16 قيمة متميّزة. وبشكل عام يمكننا تمثيل 2^N^ قيمة بـN بتّة. الشكل 1. القيم التي يمكن تمثيلها ببتّة واحدة إلى أربع بتّات. والبتّات المسطّر تحتها تقابل البادئة الآتية من الصف أعلاه.

ولأن البتّة الواحدة لا تمثّل معلومات كثيرة، تجمع أنظمة التخزين البتّات عادةً في تسلسلات أطول لتخزين قيم أكثر أهمية. والتجميع الأكثر انتشاراً هو البايت (byte)، وهو مجموعة من ثماني بتّات. ويمثّل البايت الواحد 28 = 256 قيمة فريدة (0-255) — وهو عدد كافٍ لعدّ حروف اللغة الإنجليزية ورموز الترقيم الشائعة. والبايتات أصغر وحدة ذاكرة قابلة للعنونة في نظام الحاسوب، أي أن البرنامج لا يستطيع طلب أقل من ثماني بتّات لتخزين متغيّر.

وتعرّف المعالجات الحديثة أيضاً الكلمة (word) عادةً بأنها 32 بتّة أو 64 بتّة، بحسب تصميم العتاد. ويحدد حجم الكلمة الحجم «الافتراضي» الذي يستخدمه عتاد النظام لنقل البيانات من مكوّن إلى آخر (مثلاً بين الذاكرة والسجلات). وهذه التسلسلات الأكبر ضرورية لتخزين الأعداد، لأن البرامج كثيراً ما تحتاج إلى العدّ بما يتجاوز 256!

إذا كنت قد برمجت بلغة C، فأنت تعرف أنه يجب الإعلان عن متغيّر قبل استخدامه. وتُخبر هذه الإعلانات مصرّف C بخاصيتين مهمتين تتعلقان بالتمثيل الثنائي للمتغيّر: عدد البتّات المطلوب تخصيصها له، والطريقة التي ينوي البرنامج تفسير تلك البتّات بها. ومن الناحية المفاهيمية، عدد البتّات أمر مباشر، إذ يبحث المصرّف ببساطة عن عدد البتّات المرتبط بالنوع المعلَن (مثلاً، char بايت واحد) ويربط ذلك المقدار من الذاكرة بالمتغيّر. أما تفسير تسلسل البتّات فهو أكثر إثارة مفاهيمياً. فكل البيانات في ذاكرة الحاسوب تُخزَّن كبتّات، لكن البتّات لا معنى جوهري لها. فمثلاً، حتى ببتّة واحدة فقط، يمكنك تفسير قيمتي البتّة بطرق مختلفة كثيرة: أعلى وأسفل، أسود وأبيض، نعم ولا، مفتوح ومغلق، إلخ.

وتوسيع طول تسلسل البتّات يوسّع نطاق تفسيراته. فمثلاً، يستخدم متغيّر char معيار الترميز الأمريكي لتبادل المعلومات (ASCII) الذي يحدد كيفية مقابلة قيمة ثنائية من ثماني بتّات بحروف اللغة الإنجليزية ورموز الترقيم. ويعرض الجدول 1 مجموعة فرعية صغيرة من معيار ASCII (وللمرجع الكامل، شغّل man ascii في سطر الأوامر). ولا يوجد سبب خاص لمقابلة المحرف 'X' للقيمة 01011000، فلا تتكلف عناء حفظ الجدول. والمهم أن كل برنامج يخزّن حروفاً يتفق على تفسير تسلسلها الثنائي، ولهذا يُعرَّف ASCII من لجنة معايير.

القيمة الثنائية تفسير المحرف القيمة الثنائية تفسير المحرف
01010111 W 00100000 مسافة
01011000 X 00100001 !
01011001 Y 00100010 "
01011010 Z 00100011 #

يمكن ترميز أي معلومات إلى ثنائي، بما في ذلك بيانات غنية مثل الرسومات والصوت. فمثلاً، لنفترض أن مخطط ترميز صورة ما يحدد أن 00 و01 و10 و11 تقابل الألوان الأبيض والبرتقالي والأزرق والأسود. ويوضح الشكل 2 كيف قد نستخدم استراتيجية الترميز البسيطة هذه ذات البتّتين لرسم صورة بدائية لسمكة باستخدام 12 بايتاً فقط. وفي الجزء (a)، تقابل كل خلية من الصورة تسلسلاً من بتّتين. ويعرض الجزءان (b) و(c) الترميز الثنائي المقابل كتسلسلات من بتّتين وبايتات على الترتيب. ورغم أن مخطط الترميز في هذا المثال مبسّط لأغراض التعلّم، فإن الفكرة العامة شبيهة بما تستخدمه أنظمة الرسومات الحقيقية، وإن كانت تستخدم بتّات أكثر بكثير لمجموعة أوسع من الألوان.

صورة سمكة بخلفية زرقاء (10)، وعين بيضاء (00)، وبؤبؤ أسود (11)، وجسم برتقالي (01). الشكل 2. (a) تمثيل الصورة، و(b) تمثيل الخلايا ببتّتين، و(c) تمثيل البايتات لصورة سمكة بسيطة.

وبعد أن قدّمنا مخططي ترميز للتو، قد يعني التسلسل الثنائي نفسه، 01011010، المحرف 'Z' لمحرّر نصوص، بينما قد يفسّره برنامج رسومات كجزء من زعنفة ذيل سمكة. وأي التفسيرين صحيح يعتمد على السياق. ورغم أن البتّات الأساسية هي نفسها، كثيراً ما يجد البشر بعض التفسيرات أسهل بكثير في الفهم من غيرها (مثلاً إدراك السمكة كخلايا ملوّنة لا كجدول بايتات).

يتناول ما تبقى من هذا الفصل أساساً تمثيل الأعداد الثنائية والتلاعب بها، لكن النقطة العامة جديرة بالتكرار: كل المعلومات تُخزَّن في ذاكرة الحاسوب كأصفار وآحاد، ويعود تفسير معنى تلك البتّات إلى البرامج أو الأشخاص الذين يشغّلونها.

بعد أن رأينا أن التسلسلات الثنائية يمكن تفسيرها بجميع أنواع الطرق غير العددية، لنحوّل اهتمامنا إلى الأعداد. وتحديداً، سنبدأ بالأعداد غير المؤشّرة (unsigned) التي يمكن تفسيرها كصفر أو موجبة، لكنها لا تكون سالبة أبداً (فليس لها إشارة).

4.1.1. الأعداد العشرية

بدلاً من البدء بالثنائي، لنفحص أولاً نظام أعداد مرتاحين لاستخدامه بالفعل، وهو نظام الأعداد العشرية (decimal number system) الذي يستخدم أساساً هو 10. والأساس 10 يعني خاصيتين مهمتين لتفسير القيم العشرية وتمثيلها.

  1. يخزّن أي رقم فردي في عدد بالأساس 10 إحدى 10 قيم فريدة (0-9). ولتخزين قيمة أكبر من 9، يجب أن يُحمَل (carry) الرقم إلى رقم إضافي إلى اليسار. فمثلاً، إذا بدأ رقم واحد عند قيمته العظمى (9) وأضفنا إليه 1، فتحتاج النتيجة إلى رقمين (9 + 1 = 10). وينطبق النمط نفسه على أي رقم، بصرف النظر عن موضعه في العدد (مثلاً، 5080 + 20 = 5100).
  2. يحدد موضع كل رقم في العدد مدى أهمية ذلك الرقم للقيمة الكلية للعدد. فبترقيم الأرقام من اليمين إلى اليسار d0 وd1 وd2 إلخ، يسهم كل رقم متتالٍ بعامل عشرة أكثر من الذي يليه. فمثلاً، خذ القيمة 8425 (الشكل 1).

في العدد 8425 الرقم 0 هو 5 الذي في "موضع الآحاد"، والرقم 1 هو 2 الذي في "موضع العشرات"، والرقم 2 هو 4 في "موضع المئات"، وأخيراً الرقم 3 هو 8 في "موضع الآلاف". الشكل 1. أهمية كل رقم في عدد بالأساس 10، بالأسماء التي قد تكون أعطيتها لكل رقم في المرحلة الابتدائية.

في مثال القيمة 8425، يسهم الرقم 5 في خانة «الآحاد» بـ 5 (5 × 100). ويسهم الرقم 2 في خانة «العشرات» بـ 20 (2 × 101). ويسهم الرقم 4 في خانة «المئات» بـ 400 (4 × 102)، وأخيراً يسهم الرقم 8 في خانة «الآلاف» بـ 8000 (8 × 103). وبصيغة أكثر رسمية، يمكن التعبير عن 8425 كما يلي:

(8 × 103) + (4 × 102) + (2 × 101) + (5 × 100)

ونمط زيادة الأسس المطبقة على أساس 10 هذا هو سبب تسميته نظام أعداد بالأساس 10. وإسناد أرقام المواضع للأرقام من اليمين إلى اليسار بدءاً من d0 يعني أن كل رقم di يسهم بـ 10i في القيمة الكلية. وبذلك يمكن التعبير عن القيمة الكلية لأي عدد عشري من N رقماً كما يلي:

(dN-1 × 10N-1) + (dN-2 × 10N-2) + … + (d2 × 102) + (d1 × 101) + (d0 × 100)

ولحسن الحظ، كما سنرى قريباً، ينطبق نمط شبيه جداً على أنظمة أعداد أخرى.

ملاحظة — تمييز أسس الأعداد

بعد أن صرنا على وشك تقديم نظام أعداد ثانٍ، تتمثل مشكلة محتملة في عدم وضوح كيفية تفسير عدد ما. فمثلاً، تأمّل القيمة 1000. فليس واضحاً فوراً هل ينبغي تفسير ذلك العدد كقيمة عشرية (أي ألف) أم كقيمة ثنائية (أي ثمانية، لأسباب سنشرحها قريباً). وللمساعدة في التوضيح، سيبقي ما تبقى من هذا الفصل بادئة صريحة على جميع الأعداد غير العشرية. وسنقدّم قريباً النظام الثنائي، وبادئته 0b، والنظام الست عشري، الذي يستخدم البادئة 0x.

وبالتالي، إذا رأيت 1000، فينبغي أن تفترض أنها «ألف» عشرية، وإذا رأيت 0b1000، فينبغي أن تفسّرها كعدد ثنائي، وهو في هذه الحالة القيمة «ثمانية».

4.1.2. الأعداد الثنائية غير المؤشّرة

رغم أنك ربما لم تفكر يوماً في الصيغة المحددة التي تصف الأعداد العشرية كقوى للعدد 10، فمفهوم المواضع { الآحاد والعشرات والمئات وغيرها } ينبغي أن يكون مألوفاً. ولحسن الحظ، تنطبق مصطلحات مشابهة على أنظمة أعداد أخرى، مثل الثنائي. وبالطبع يختلف الأساس في أنظمة الأعداد الأخرى، لذا يسهم كل موضع رقم بمقدار مختلف في قيمته العددية.

يستخدم نظام الأعداد الثنائية أساساً هو 2 بدلاً من 10 في النظام العشري. وتحليله بالطريقة نفسها التي حللنا بها العشري للتو يكشف عدة أوجه تشابه (مع استبدال 2 بـ 10):

  1. يخزّن أي بتّة فردية في عدد بالأساس 2 إحدى قيمتين فريدتين (0 أو 1). ولتخزين قيمة أكبر من 1، يجب أن يحمل الترميز الثنائي إلى بتّة إضافية إلى اليسار. فمثلاً، إذا بدأت بتّة واحدة عند قيمتها العظمى (1) وأضفنا إليها 1، فتحتاج النتيجة إلى بتّتين (1 + 1 = 0b10). وينطبق النمط نفسه على أي بتّة، بصرف النظر عن موضعها في العدد (مثلاً، 0b100100 + 0b100 = 0b101000).

  2. يحدد موضع كل بتّة في العدد مدى أهمية تلك البتّة للقيمة العددية للعدد. وبترقيم الأرقام من اليمين إلى اليسار d0 وd1 وd2 إلخ، تسهم كل بتّة متتالية بعامل اثنين أكثر من التي تليها.

تشير النقطة الأولى إلى أن العدّ في النظام الثنائي يتبع النمط نفسه المتبع في العشري: بمجرد تعداد القيم وإضافة الأرقام (البتّات). ولأن هذا القسم يركّز على الأعداد غير المؤشّرة (الصفر والموجبة فقط)، فمن الطبيعي البدء بالعدّ من الصفر. ويعرض الجدول 1 كيفية عدّ الأعداد الطبيعية القليلة الأولى في النظام الثنائي. وكما ترى من الجدول، يزيد العدّ الثنائي عدد الأرقام سريعاً. ومن البديهي أن هذا النمو منطقي، لأن كل رقم ثنائي (قيمتان محتملتان) يمثّل معلومات أقل من رقم عشري (10 قيم محتملة).

القيمة الثنائية القيمة العشرية
0 0
1 1
10 2
11 3
100 4
101 5
… …

النقطة الثانية المتعلقة بترقيم الأرقام تبدو مألوفة جداً! بل إنها شديدة الشبه بالعشري حتى إنها تؤدي إلى صيغة شبه مطابقة لتفسير عدد ثنائي. ما عليك إلا استبدال 10 في أساس كل أس بـ 2:

(dN-1 × 2N-1) + (dN-2 × 2N-2) + … + (d2 × 22) + (d1 × 21) + (d0 × 20)

ويؤدي تطبيق هذه الصيغة إلى التفسير غير المؤشّر لأي عدد ثنائي. فمثلاً، خذ 0b1000:

(1 × 23) + (0 × 22) + (0 × 21) + (0 × 20)

= 8 + 0 + 0 + 0 = 8

وفيما يلي مثال أطول على بايت واحد، 0b10110100:

(1 × 27) + (0 × 26) + (1 × 25) + (1 × 24) + (0 × 23) + (1 × 22) + (0 × 21) + (0 × 20)

= 128 + 0 + 32 + 16 + 0 + 4 + 0 + 0 = 180

4.1.3. النظام الست عشري

حتى الآن، فحصنا نظامي أعداد، العشري والثنائي. ويتميز العشري بارتياح البشر له، بينما يطابق الثنائي طريقة تخزين البيانات في العتاد. ومن المهم ملاحظة أنهما متكافئان في قوتهما التعبيرية. أي أنه لا يوجد عدد يمكن تمثيله في أحدهما ولا يمكن تمثيله في الآخر. وبالنظر إلى تكافؤهما، قد يدهشك أننا سنناقش نظام أعداد آخر: النظام الست عشري (hexadecimal) ذو الأساس 16.

وبوجود نظامي أعداد جيدين تماماً، قد تتساءل لماذا نحتاج إلى آخر. والجواب هو الملاءمة أساساً. وكما يظهر في الجدول 1، تنمو تسلسلات البتّات الثنائية سريعاً إلى عدد كبير من الأرقام. ويميل البشر إلى صعوبة استيعاب تسلسلات طويلة تحتوي أصفاراً وآحاداً فقط. وفي حين أن العشري أكثر إيجازاً، فإن أساسه 10 غير متوافق مع أساس الثنائي 2.

لا يلتقط العشري بسهولة النطاق الذي يمكن التعبير عنه بعدد ثابت من البتّات. فمثلاً، لنفترض أن حاسوباً قديماً يستخدم عناوين ذاكرة من 16 بتّة. ويتراوح نطاق عناوينه الصالحة من 0b0000000000000000 إلى 0b1111111111111111. وبالتمثيل العشري، تتراوح العناوين من 0 إلى 65535. من الواضح أن التمثيلات العشرية أكثر إيجازاً من التسلسلات الثنائية الطويلة، لكن ما لم تحفظ تحويلاتها، يصعب التفكير في الأعداد العشرية. وتزداد المشكلتان سوءاً على الأجهزة الحديثة التي تستخدم عناوين 32 أو 64 بتّة!

وهنا يتألق أساس النظام الست عشري 16 مع هذه التسلسلات الطويلة من البتّات. فالأساس الكبير يتيح لكل رقم تمثيل معلومات كافية لتكون الأعداد الست عشرية موجزة. وزيادةً على ذلك، لأن الأساس نفسه قوة للعدد اثنين (24 = 16)، فمن السهل ربط الست عشري بالثنائي والعكس. ومن باب الاكتمال، لنحلل النظام الست عشري بالطريقة نفسها التي حللنا بها العشري والثنائي:

  1. يخزّن أي رقم فردي في عدد بالأساس 16 إحدى 16 قيمة فريدة. ووجود أكثر من 10 قيم يمثّل تحدياً جديداً للنظام الست عشري — فأرقام الأساس 10 التقليدية تتوقف عند قيمة عظمى هي 9. وبالاصطلاح، يستخدم النظام الست عشري الحروف لتمثيل القيم الأكبر من 9، حيث A تعني 10 وB تعني 11 وصولاً إلى F التي تعني 15. وكالأنظمة الأخرى، لتخزين قيمة أكبر من 15، يجب أن يحمل العدد إلى رقم إضافي إلى اليسار. فمثلاً، إذا بدأ رقم واحد عند قيمته العظمى (F) وأضفنا إليه 1، فتحتاج النتيجة إلى رقمين (0xF + 0x1 = 0x10؛ لاحظ أننا نستخدم 0x للإشارة إلى الأعداد الست عشرية).
  2. يحدد موضع كل رقم في العدد مدى أهمية ذلك الرقم للقيمة العددية للعدد. وبترقيم الأرقام من اليمين إلى اليسار d0 وd1 وd2 إلخ، يسهم كل رقم متتالٍ بعامل 16 أكثر من الذي يليه.

ولا عجب أن الصيغة الموثوقة نفسها لتفسير عدد تنطبق على النظام الست عشري، بأساس 16:

(dN-1 × 16N-1) + (dN-2 × 16N-2) + … + (d2 × 162) + (d1 × 161) + (d0 × 160)

فمثلاً، لتحديد القيمة العشرية لـ 0x23C8:

(2 × 163) + (3 × 162) + (C × 161) + (8 × 160)

= (2 × 163) + (3 × 162) + (12 × 161) + (8 × 160)

= (2 × 4096) + (3 × 256) + (12 × 16) + (8 × 1)

= 8192 + 768 + 192 + 8 = 9160

تحذير — مفهوم خاطئ عن النظام الست عشري

قد لا تصادف الأعداد الست عشرية كثيراً في بداية تعلّمك برمجة الأنظمة. بل إن السياق الوحيد الذي يُرجَّح أن تجدها فيه هو تمثيل عناوين الذاكرة. فمثلاً، إذا طبعت عنوان متغيّر باستخدام رمز التنسيق %p (المؤشّر) لـ printf فستحصل على ناتج ست عشري.

كثيراً ما يبدأ الطلاب بمساواة عناوين الذاكرة (مثل متغيّرات المؤشّرات في C) بالنظام الست عشري. ورغم أنك قد تعتاد رؤية العناوين ممثَّلة بهذه الطريقة، فتذكّر أنها لا تزال تُخزَّن ثنائياً في العتاد، شأنها شأن كل البيانات الأخرى!

4.1.4. حدود التخزين

من الناحية المفاهيمية، توجد أعداد صحيحة غير مؤشّرة لا نهائية. لكن عملياً، يجب على المبرمج اختيار عدد البتّات المخصصة لمتغيّر قبل تخزينه، لأسباب متنوعة:

  • قبل تخزين قيمة، يجب على البرنامج تخصيص مساحة تخزين لها. وفي C، يخبر الإعلان عن متغيّر المصرّف بمقدار الذاكرة التي يحتاجها بناءً على نوعه.
  • لأجهزة التخزين العتادية سعة محدودة. وفي حين أن الذاكرة الرئيسية للنظام كبيرة عادةً ويُستبعد أن تكون عاملاً مقيّداً، فإن مواقع التخزين داخل المعالج المستخدمة كـ«مساحة عمل» مؤقتة (أي السجلات) أكثر تقييداً. ويستخدم المعالج سجلات محدودة بحجم كلمته (عادةً 32 أو 64 بتّة، بحسب معمارية المعالج).
  • كثيراً ما تنقل البرامج البيانات من جهاز تخزين إلى آخر (مثلاً بين سجلات المعالج والذاكرة الرئيسية). وكلما كبرت القيم، احتاجت أجهزة التخزين إلى أسلاك أكثر لتوصيل الإشارات بينها. ومن ثم، فإن توسيع التخزين يزيد تعقيد العتاد ويترك مساحة مادية أقل لمكوّنات أخرى.

يحدد عدد البتّات المستخدمة لتخزين عدد صحيح نطاق قيمه القابلة للتمثيل. ويصوّر الشكل 2 كيف قد نتصور فضاءي تخزين الأعداد الصحيحة غير المؤشّرة اللانهائي والمحدود.

يبدأ خط الأعداد غير المؤشّر اللانهائي من الصفر ويزداد بلا نهاية. أمّا خط الأعداد غير المؤشّر المحدود فيبدأ من 0 وينتهي عند قيمة قصوى، ومحاولة الانتقال خارج أحد الطرفين تُلتفّ إلى الطرف الآخر. الشكل 2. توضيح (a) خط أعداد غير مؤشّر لانهائي و(b) خط أعداد غير مؤشّر محدود. و«يلتف» الأخير عند كلا الطرفين (تجاوز).

تُعرف محاولة تخزين قيمة أكبر في متغيّر مما تسمح به مساحته باسم تجاوز العدد الصحيح (integer overflow). ويؤجّل هذا الفصل تفاصيل التجاوز إلى قسم لاحق. والآن، فكّر فيه كمعدّاد مسافات سيارة «يلتف» عائداً إلى الصفر إذا حاول الزيادة بما يتجاوز قيمته العظمى. وبالمثل، طرح واحد من صفر يعطي القيمة العظمى.

عند هذه النقطة، يطرح السؤال الطبيعي عن النظام الثنائي غير المؤشّر نفسه: «ما أكبر قيمة موجبة يمكن لـ N بتّة تخزينها؟» وبعبارة أخرى، بالنظر إلى تسلسل من N بتّة جميعها 1، ما القيمة التي يمثلها التسلسل؟ وبالتفكير غير الرسمي في هذا السؤال، يُظهر تحليل القسم السابق أن N بتّة تعطي 2N تسلسل بتّات فريداً. ولأن أحد هذه التسلسلات يجب أن يمثّل العدد 0، فإن ذلك يترك 2N - 1 قيمة موجبة تتراوح من 1 إلى 2N - 1. وبذلك يجب أن تكون القيمة العظمى لعدد ثنائي غير مؤشّر من N بتّة هي 2N - 1.

فمثلاً، توفّر 8 بتّات 28 = 256 تسلسلاً فريداً. وأحد هذه التسلسلات، 0b00000000، محجوز للعدد 0، ما يترك 255 تسلسلاً لتخزين قيم موجبة. لذا يمثّل متغيّر من 8 بتّات القيم الموجبة من 1 إلى 255، وأكبرها 255.

يُرجَّح أن تصادف كل من أسس الأعداد الثلاثة التي قدّمناها في هذا الفصل في سياقات مختلفة. وفي بعض الحالات، قد تحتاج إلى التحويل من أساس إلى آخر. يبدأ هذا القسم بعرض كيفية التحويل بين الثنائي والست عشري، لأن هذين يترابطان بسهولة. وبعد ذلك، سنستكشف التحويلات من العشري وإليه.

4.2.1. التحويل بين الثنائي والست عشري

ولأن أساسي الثنائي والست عشري كلاهما قوة للعدد 2، فإن التحويل بينهما مباشر نسبياً. وتحديداً، يحمل كل رقم ست عشري إحدى 16 قيمة فريدة، وتمثّل أربع بتّات أيضاً 24 = 16 قيمة فريدة، ما يجعل قوتهما التعبيرية متكافئة. يعدّد الجدول 1 المقابلة واحداً لواحد بين أي تسلسل من أربع بتّات وأي رقم ست عشري واحد.

ثنائي ست عشري ثنائي ست عشري
0000 0 1000 8
0001 1 1001 9
0010 2 1010 A
0011 3 1011 B
0100 4 1100 C
0101 5 1101 D
0110 6 1110 E
0111 7 1111 F

لاحظ أن محتوى الجدول 1 يكافئ ببساطة العدّ من 0 إلى 15 في كلا النظامين، فلا حاجة إلى حفظه. وبهذا الربط، يمكنك تحويل أي عدد من البتّات أو الأرقام الست عشرية المتتالية في أي من الاتجاهين:

لتحويل 0xB491 إلى ثنائي، استبدل ببساطة القيمة الثنائية المقابلة بكل رقم ست عشري:

  B    4    9    1
1011 0100 1001 0001  ->  0b1011010010010001

لتحويل 0b1111011001 إلى ست عشري، قسّم البتّات أولاً إلى مجموعات من أربع، من اليمين إلى اليسار. وإذا لم تكن المجموعة اليسرى تحتوي أربع بتّات، فيمكنك الحشو بأصفار بادئة. ثم استبدل القيم الست عشرية المقابلة:

1111011001  ->  11 1101 1001  ->  0011 1101 1001
                                  ^ padding

0011 1101 1001
  3    D    9  ->  0x3D9

4.2.2. التحويل إلى العشري

لحسن الحظ، التحويل إلى العشري هو ما كنا نفعله طوال الأقسام السابقة من هذا الفصل. وبإعطاء عدد في أي أساس B، فإن ترقيم الأرقام من اليمين إلى اليسار d0 وd1 وd2 إلخ يتيح صيغة عامة لتحويل القيم إلى العشري:

(dN-1 × BN-1) + (dN-2 × BN-2) + … + (d2 × B2) + (d1 × B1) + (d0 × B0)

4.2.3. التحويل من العشري

يتطلب التحويل من العشري إلى أنظمة أخرى جهداً أكبر قليلاً. وغير رسمياً، الهدف هو عكس الصيغة السابقة: تحديد قيمة كل رقم بحيث يؤدي جمع كل حد، بناءً على موضع الرقم، إلى العدد العشري المصدر. وقد يساعد التفكير في كل رقم في نظام الأساس الهدف بالطريقة نفسها التي وصفنا بها الخانات (خانة «الآحاد» وخانة «العشرات» وغيرها) للعشري. فمثلاً، تأمّل التحويل من العشري إلى الست عشري. يقابل كل رقم في عدد ست عشري قوة متزايدة للعدد 16، ويسرد الجدول 2 القوى الأولى.

164 163 162 161 160
65536 4096 256 16 1

فمثلاً، لتحويل 9742 إلى ست عشري، تأمّل:

  • كم مضاعفاً للعدد 65536 يتسع في 9742؟ (وبعبارة أخرى، ما قيمة خانة «65536»؟) لا تحتاج القيمة الست عشرية الناتجة إلى أي مضاعفات للعدد 65536، لأن القيمة (9742) أصغر من 65536، لذا ينبغي ضبط d4 على 0. ولاحظ أنه بالمنطق نفسه، ستكون كل الأرقام الأعلى أيضاً 0، لأن كل رقم سيسهم بقيم أكبر حتى من 65536. وبذلك لا تحتوي النتيجة حتى الآن إلا على: 0 d4 d3 d2 d1 d0
  • كم مضاعفاً للعدد 4096 يتسع في 9742؟ (وبعبارة أخرى، ما قيمة خانة «4096»؟) يتسع 4096 في 9742 مرتين (2 × 4096 = 8192)، لذا ينبغي أن تكون قيمة d3 هي 2. وبذلك سيسهم d3 بـ 8192 في القيمة الكلية، فيجب أن تحسب النتيجة حتى الآن 9742 - 8192 = 1550. 0 2 d4 d3 d2 d1 d0
  • كم مضاعفاً للعدد 256 يتسع في 1550؟ (وبعبارة أخرى، ما قيمة خانة «256»؟) يتسع 256 في 1550 ست مرات (6 × 256 = 1536)، لذا ينبغي أن تكون قيمة d2 هي 6، ما يترك 1550 - 1536 = 14. 0 2 6 d4 d3 d2 d1 d0
  • كم مضاعفاً للعدد 16 يتسع في 14؟ (وبعبارة أخرى، ما قيمة خانة «العشرات الست عشرية»؟) لا شيء، لذا يجب أن يكون d1 هو 0. 0 2 6 0 d4 d3 d2 d1 d0
  • وأخيراً، كم مضاعفاً للعدد 1 يتسع في 14؟ (وبعبارة أخرى، ما قيمة خانة «الآحاد»؟) الجواب 14 بالطبع، ويمثلها النظام الست عشري بالرقم E. 0 2 6 0 E d4 d3 d2 d1 d0

وبذلك يقابل العدد العشري 9742 القيمة 0x260E.

من العشري إلى الثنائي: قوى العدد اثنين

تعمل الطريقة نفسها مع الثنائي أيضاً (أو أي نظام أعداد آخر)، بشرط استخدام قوى الأساس المناسب. ويسرد الجدول 3 قوى العدد اثنين الأولى، ما سيساعد في تحويل القيمة العشرية المثالية 422 إلى ثنائي.

28 27 26 25 24 23 22 21 20
256 128 64 32 16 8 4 2 1

ولأن البتّة الفردية لا يُسمح لها بتخزين إلا 0 أو 1، لم يعد السؤال «كم مضاعفاً من كل قوة يتسع في قيمة؟» عند التحويل إلى ثنائي. بل اطرح سؤالاً أبسط: «هل تتسع القوة التالية للعدد اثنين؟» فمثلاً، عند تحويل 422:

  • يتسع 256 في 422، لذا ينبغي أن يكون d8 هو 1. ويتبقى 422 - 256 = 166.
  • يتسع 128 في 166، لذا ينبغي أن يكون d7 هو 1. ويتبقى 166 - 128 = 38.
  • لا يتسع 64 في 38، لذا ينبغي أن يكون d6 هو 0.
  • يتسع 32 في 38، لذا ينبغي أن يكون d5 هو 1. ويتبقى 38 - 32 = 6.
  • لا يتسع 16 في 6، لذا ينبغي أن يكون d4 هو 0.
  • لا يتسع 8 في 6، لذا ينبغي أن يكون d3 هو 0.
  • يتسع 4 في 6، لذا ينبغي أن يكون d2 هو 1. ويتبقى 6 - 4 = 2.
  • يتسع 2 في 2، لذا ينبغي أن يكون d1 هو 1. ويتبقى 2 - 2 = 0. (ملاحظة: عند الوصول إلى 0، ستكون كل الأرقام المتبقية دائماً 0.)
  • لا يتسع 1 في 0، لذا ينبغي أن يكون d0 هو 0.

وبذلك يقابل العدد العشري 422 القيمة 0b110100110.

من العشري إلى الثنائي: القسمة المتكررة

تعمل الطريقة التي وصفناها للتو جيداً عموماً مع الطلاب الملمّين بقوى العدد اثنين ذات الصلة (فمثلاً، في حالة 422، يجب أن يدرك المحوِّل أنه ينبغي البدء من d8 لأن 29 = 512 كبيرة جداً).

وهناك طريقة بديلة لا تتطلب معرفة قوى العدد اثنين. وبدلاً من ذلك، تبني هذه الطريقة نتيجة ثنائية بفحص حالة زوجية العدد العشري (زوجي أو فردي) وقسمته مراراً على اثنين (مع تقريب الأنصاف إلى أسفل) لتحديد كل بتّة متتالية. ولاحظ أنها تبني تسلسل البتّات الناتج من اليمين إلى اليسار. فإذا كانت القيمة العشرية زوجية، ينبغي أن تكون البتّة التالية صفراً؛ وإذا كانت فردية، ينبغي أن تكون البتّة التالية واحداً. وعندما تبلغ القسمة الصفر، يكتمل التحويل.

فمثلاً، عند تحويل 422:

  • 422 زوجي، لذا ينبغي أن يكون d0 هو 0. (وهذه البتّة الواقعة في أقصى اليمين.)
  • 422 / 2 = 211، وهو فردي، لذا ينبغي أن يكون d1 هو 1.
  • 211 / 2 = 105، وهو فردي، لذا ينبغي أن يكون d2 هو 1.
  • 105 / 2 = 52، وهو زوجي، لذا ينبغي أن يكون d3 هو 0.
  • 52 / 2 = 26، وهو زوجي، لذا ينبغي أن يكون d4 هو 0.
  • 26 / 2 = 13، وهو فردي، لذا ينبغي أن يكون d5 هو 1.
  • 13 / 2 = 6، وهو زوجي، لذا ينبغي أن يكون d6 هو 0.
  • 6 / 2 = 3، وهو فردي، لذا ينبغي أن يكون d7 هو 1.
  • 3 / 2 = 1، وهو فردي، لذا ينبغي أن يكون d8 هو 1.
  • 1 / 2 = 0، لذا سيكون أي رقم تسعة أو أعلى هو 0، وتنتهي الخوارزمية.

وكما هو متوقع، تنتج هذه الطريقة التسلسل الثنائي نفسه: 0b110100110.

حتى الآن، حصرنا مناقشة الأعداد الثنائية في الأعداد الصحيحة غير المؤشّرة (غير السالبة حصراً). ويعرض هذا القسم تفسيراً بديلاً للثنائي يتضمن الأعداد السالبة. وبما أن للمتغيّر مساحة تخزين محدودة، يجب أن يميّز الترميز الثنائي المؤشّر بين القيم السالبة والصفر والقيم الموجبة. كما يتطلب التلاعب بالأعداد المؤشّرة إجراءً لسلب عدد ما. يجب أن يميّز ترميز ثنائي مؤشّر بين تسلسلات البتّات للقيم السالبة وغير السالبة. وعملياً، يصمّم مهندسو الأنظمة أنظمة عامة الغرض، لذا يكون التقسيم 50% / 50% خياراً وسطياً جيداً. ولذلك، تمثّل ترميزات الأعداد المؤشّرة التي يعرضها هذا الفصل عدداً متساوياً من القيم السالبة وغير السالبة.

ملاحظة — غير السالبة مقابل الموجبة

لاحظ أن هناك فرقاً دقيقاً لكنه مهم بين غير السالبة والموجبة. فمجموعة القيم الموجبة حصراً تستبعد الصفر، بينما مجموعة غير السالبة تتضمن الصفر. وحتى بعد تقسيم تسلسلات البتّات المتاحة بنسبة 50% / 50% بين القيم السالبة وغير السالبة، يجب أن يظل أحد القيم غير السالبة محجوزاً للصفر. وبذلك، مع عدد ثابت من البتّات، قد ينتهي نظام الأعداد إلى تمثيل قيم سالبة أكثر من الموجبة (مثل نظام المتمّم الثنائي).

تستخدم ترميزات الأعداد المؤشّرة بتّة واحدة للتمييز بين مجموعتي الأعداد السالبة وغير السالبة. وبالاصطلاح، تشير البتّة الواقعة في أقصى اليسار إلى ما إذا كان العدد سالباً (1) أم غير سالب (0). وتُعرف هذه البتّة الواقعة في أقصى اليسار بـالبتّة عالية الرتبة أو البتّة الأكثر أهمية (most significant bit).

يعرض هذا الفصل ترميزين محتملين للثنائي المؤشّر — المقدار المؤشّر والمتمّم الثنائي. ورغم أن ترميزاً واحداً فقط من هذين (المتمّم الثنائي) لا يزال مستخدماً عملياً، فإن مقارنتهما ستساعد في توضيح خصائصهما المهمة.

4.3.1. المقدار المؤشّر

يعامل تمثيل المقدار المؤشّر (signed magnitude) البتّة عالية الرتبة حصراً كبتّة إشارة. أي أن كون البتّة عالية الرتبة 0 أو 1 لا يؤثر في القيمة المطلقة للعدد، بل يحدد فقط ما إذا كانت القيمة موجبة (البتّة عالية الرتبة 0) أو سالبة (البتّة عالية الرتبة 1). ومقارنةً بالمتمّم الثنائي، يجعل المقدار المؤشّر إجراءَي التحويل إلى العشري والسلب مباشرين نسبياً:

  • لحساب قيمة عشرية لتسلسل مقدار مؤشّر من N بتّة، احسب قيمة الأرقام d0 حتى dN-2 باستخدام الطريقة غير المؤشّرة المألوفة. ثم افحص البتّة الأكثر أهمية dN-1: إذا كانت 1 فالقيمة سالبة، وإلا فليست كذلك.
  • لسلب قيمة، اقلب ببساطة البتّة الأكثر أهمية لتغيير إشارتها.

تحذير — مفهوم خاطئ عن السلب

يُعرض المقدار المؤشّر لأغراض تعليمية بحتة. ورغم أن بعض الآلات استخدمته في الماضي (مثل IBM 7090 في الستينيات)، فلا تستخدم الأنظمة الحديثة المقدار المؤشّر لتمثيل الأعداد الصحيحة (وإن كانت آلية مشابهة جزءاً من المعيار لتخزين قيم الفاصلة العائمة).

وما لم يُطلب منك صراحةً النظر في المقدار المؤشّر، فلا تفترض أن قلب البتّة الأولى في عدد ثنائي سيسلب قيمة ذلك العدد على نظام حديث.

يوضح الشكل 1 كيف تقابل تسلسلات المقدار المؤشّر الرباعية البتّات القيم العشرية. وقد يبدو المقدار المؤشّر للوهلة الأولى جاذباً لبساطته. ولسوء الحظ، فإنه يعاني من عيبين رئيسيين يجعلانه غير جاذب. الأول أنه يقدّم تمثيلين للصفر. فمثلاً، بأربع بتّات، يمثّل المقدار المؤشّر الصفر (0b0000) والصفر السالب (0b1000) معاً. وبالتالي، فإنه يمثّل تحدياً لمصممي العتاد لأن عليهم مراعاة تسلسلين ثنائيين محتملين متساويين عددياً رغم اختلاف قيم بتّاتهما. ومهمة مصمم العتاد أسهل بكثير بوجود طريقة واحدة فقط لتمثيل عدد بهذه الأهمية.

دائرة يحمل أحد جانبيها قيماً غير سالبة تتراوح من 0b0000 (0) إلى 0b0111 (7)، بينما يحمل الجانب الآخر من 0b1000 (-0) إلى 0b1111 (-7). الشكل 1. تخطيط منطقي لقيم المقدار المؤشّر لتسلسلات بتّات بطول أربعة.

والعيب الآخر في المقدار المؤشّر أنه يُظهر انقطاعاً غير ملائم بين القيم السالبة والصفر. ورغم أننا سنغطي التجاوز بمزيد من التفصيل لاحقاً، فإن إضافة 1 إلى التسلسل الرباعي 0b1111 «تلتف» عائدةً إلى 0b0000. وفي المقدار المؤشّر، يعني هذا الأثر أن 0b1111 (-7) + 1 قد يُفهم خطأً على أنه 0 بدلاً من -6 المتوقعة. وهذه المشكلة قابلة للحل، لكن الحل يعقّد تصميم العتاد مجدداً، فيحوّل فعلياً أي انتقال بين الأعداد السالبة وغير السالبة إلى حالة خاصة تحتاج إلى عناية إضافية.

ولهذه الأسباب، اختفى المقدار المؤشّر إلى حد كبير عملياً، وساد المتمّم الثنائي.

4.3.2. المتمّم الثنائي

يحل ترميز المتمّم الثنائي (two's complement) مشاكل المقدار المؤشّر بطريقة أنيقة. فمثل المقدار المؤشّر، تشير البتّة عالية الرتبة في عدد المتمّم الثنائي إلى ما إذا كان ينبغي تفسير القيمة كسالبة أم لا. لكن على عكس المقدار المؤشّر، تؤثر البتّة عالية الرتبة أيضاً في قيمة العدد. فكيف تستطيع فعل الأمرين معاً؟

حساب قيمة عشرية لعدد متمّم ثنائي من N بتّة يشبه الطريقة غير المؤشّرة المألوفة، إلا أن مساهمة البتّة عالية الرتبة في القيمة الكلية تكون مسلوبة. أي أنه بالنسبة لتسلسل متمّم ثنائي من N بتّة، بدلاً من إسهام البتّة الأولى بـ dN-1 × 2N-1 في المجموع، تسهم بـ -dN-1 × 2N-1 (لاحظ الإشارة السالبة). وبالتالي، إذا كانت البتّة الأكثر أهمية 1، فستكون القيمة الكلية سالبة لأن تلك البتّة الأولى تسهم بأكبر قيمة مطلقة في المجموع. وإلا فلا تسهم البتّة الأولى بشيء في المجموع، وتكون النتيجة غير سالبة. والصيغة الكاملة هي:

- (dN-1 × 2N-1) + (dN-2 × 2N-2) + … + (d2 × 22) + (d1 × 21) + (d0 × 20)

^ لاحظ إشارة السلب البادئة للحد الأول فقط!

يوضح الشكل 2 تخطيط التسلسلات الرباعية في المتمّم الثنائي. ويشفّر هذا التعريف تمثيلاً واحداً فقط للصفر — تسلسل بتّات كلها 0. وبوجود تسلسل صفر واحد فقط، يمثّل المتمّم الثنائي قيمة سالبة واحدة أكثر من الموجبة. وباستخدام تسلسلات أربع بتّات كمثال، يمثّل المتمّم الثنائي قيمة دنيا هي 0b1000 (-8)، لكن قيمة عظمى لا تتجاوز 0b0111 (7). ولحسن الحظ، لا تعيق هذه الشذوذة تصميم العتاد ونادراً ما تسبب مشاكل للتطبيقات.

دائرة يحمل أحد جانبيها قيماً غير سالبة تتراوح من 0b0000 (0) إلى 0b0111 (7)، بينما يحمل الجانب الآخر من 0b1111 (-1) إلى 0b1000 (-8). الشكل 2. تخطيط منطقي لقيم المتمّم الثنائي لتسلسلات بتّات بطول أربعة.

ومقارنةً بالمقدار المؤشّر، يبسّط المتمّم الثنائي أيضاً الانتقال بين الأعداد السالبة والصفر. فبصرف النظر عن عدد البتّات المستخدمة لتخزينه، سيحمل عدد متمّم ثنائي مكوّن من آحاد كلها القيمة -1 دائماً. وإضافة 1 إلى تسلسل بتّات كلها 1 «تلتف» إلى صفر، ما يجعل المتمّم الثنائي ملائماً، لأن -1 + 1 ينبغي أن ينتج صفراً.

السلب

سلب عدد متمّم ثنائي أعقد قليلاً من سلب قيمة مقدار مؤشّر. ولسلب قيمة من N بتّة، حدد متمّمها (complement) بالنسبة إلى 2N (ومن هنا جاء اسم الترميز). وبعبارة أخرى، لسلب قيمة X من N بتّة، جد تسلسل بتّات Y (متمّم X) بحيث يكون X + Y = 2N.

ولحسن الحظ، يوجد اختصار سريع لسلب عدد متمّم ثنائي عملياً: اقلب كل البتّات وأضف واحداً. فمثلاً، لسلب القيمة الثمانية 13، حدد أولاً القيمة الثنائية للعدد 13. ولأن 13 هو مجموع 8 و4 و1، اضبط البتّات في المواضع 3 و2 و0:

00001101  (decimal 13)

بعد ذلك، «اقلب البتّات» (غيّر كل الأصفار إلى آحاد والعكس):

11110010

وأخيراً، إضافة واحد تعطي 0b11110011. وبالفعل، يُظهر تطبيق صيغة تفسير تسلسل متمّم ثنائي أن القيمة هي -13:

-(1 × 27) + (1 × 26) + (1 × 25) + (1 × 24) + (0 × 23) + (0 × 22) + (1 × 21) + (1 × 20)

= -128 + 64 + 32 + 16 + 0 + 0 + 2 + 1 = -13

وإذا كنت فضولياً لمعرفة سبب نجاح هذا الاختصار الذي يبدو سحرياً، فتأمّل سلب العدد 13 من ثماني بتّات بصيغة أكثر رسمية. لإيجاد متمّم 13، حُل المعادلة 0b00001101 (13) + Y = 0b100000000 (28، وهو ما يتطلب بتّة إضافية للتمثيل). ويمكن إعادة ترتيب المعادلة لتصبح Y = 0b100000000 - 0b00001101. ويتضح الآن أن هذه مسألة طرح:

 100000000  (256)
- 00001101   (13)

ورغم أن هذا الطرح قد يبدو مخيفاً، يمكننا التعبير عنه بصيغة أسهل حساباً هي (0b011111111 + 1) - 0b00001101. لاحظ أن هذا التغيير يعبّر ببساطة عن 28 (256) بالصيغة (255 + 1). وبعد هذا التغيير، يصبح الحساب هكذا:

 011111111  (255)  + 00000001  (1)
- 00001101   (13)

وكما يتبيّن، فإنه لأي قيمة بتّة b، يكون 1 - b مكافئاً لـ«قلب» تلك البتّة. وبذلك يمكن اختزال الطرح كاملاً في المثال السابق إلى قلب جميع بتّات العدد الأدنى فحسب. ولم يبق إلا إضافة +1 المتبقية من التعبير عن 256 بالصيغة 255 + 1. وبتجميع ذلك كله، يمكننا ببساطة قلب بتّات قيمة وإضافة واحد لحساب متمّمها!

تحذير — البرمجة بلغة C بالأعداد الصحيحة المؤشّرة مقابل غير المؤشّرة

إضافةً إلى تخصيص المساحة، يخبر الإعلان عن متغيّرات في C المصرّف أيضاً بالطريقة التي تريد تفسير المتغيّر بها. فعندما تعلن int، يفسّر المصرّف المتغيّر كعدد صحيح مؤشّر بتمثيل المتمّم الثنائي. ولتخصيص قيمة غير مؤشّرة، أعلن unsigned int.

والتمييز يهم أيضاً في C في مواضع أخرى، مثل الدالة printf. وكما شدّد هذا الفصل طواله، يمكن تفسير تسلسل البتّات بطرق مختلفة! فمع printf، يعتمد التفسير على العنصر النائب للتنسيق الذي تستخدمه. فمثلاً:

#include <stdio.h>

int main(void) {
    int example = -100;

    /* Print example int using both signed and unsigned placeholders. */
    printf("%d  %u\n", example, example);

    return 0;
}

ورغم أن هذه الشيفرة تمرّر printf المتغيّر نفسه (example) مرتين، فإنها تطبع -100 4294967196. كن حذراً في تفسير قيمك تفسيراً صحيحاً!

امتداد الإشارة

قد تجد نفسك أحياناً تريد إجراء عملية حسابية على عددين مخزّنين بعددين مختلفين من البتّات. فمثلاً، في C قد تريد جمع int من 32 بتّة وshort من 16 بتّة. وفي مثل هذه الحالات، يحتاج العدد الأصغر إلى امتداد الإشارة (sign extension)، وهي طريقة أنيقة للقول إن البتّة الأكثر أهمية تُكرَّر بقدر ما يلزم لتوسيع طول تسلسل البتّات إلى الطول الهدف. ورغم أن المصرّف سيتولى ترتيب البتّات عنك في C، فمن المفيد فهم كيفية عمل العملية.

فمثلاً، لتوسيع التسلسل الرباعي 0b0110 (6) إلى تسلسل من ثماني بتّات، خذ البتّة عالية الرتبة (0) وضعها في المقدمة أربع مرات لإنتاج القيمة الموسّعة: 0b00000110 (لا تزال 6). وتوسيع 0b1011 (-5) إلى تسلسل من ثماني بتّات يأخذ بالمثل البتّة عالية الرتبة (وهذه المرة 1) ويضعها في المقدمة أربع مرات للقيمة الموسّعة الناتجة: 0b11111011 (لا تزال -5). وللتحقق من الصحة، تأمّل كيف تتغير القيمة بعد إضافة كل بتّة جديدة:

    0b1011 =                       -8 + 0 + 2 + 1  =  -5
   0b11011 =                  -16 + 8 + 0 + 2 + 1  =  -5
  0b111011 =             -32 + 16 + 8 + 0 + 2 + 1  =  -5
 0b1111011 =        -64 + 32 + 16 + 8 + 0 + 2 + 1  =  -5
0b11111011 =  -128 + 64 + 32 + 16 + 8 + 0 + 2 + 1  =  -5

وكما تشهد الأمثلة، تبقى الأعداد غير السالبة (البتّة عالية الرتبة صفر) غير سالبة بعد إضافة أصفار في المقدمة. وبالمثل، تبقى الأعداد السالبة (البتّة عالية الرتبة واحد) سالبة بعد وضع آحاد في مقدمة القيم الموسّعة.

ملاحظة — امتداد الصفر غير المؤشّر

بالنسبة لقيمة غير مؤشّرة (مثل متغيّر C معلَن صراحةً بمحدد unsigned)، يتطلب توسيعها إلى تسلسل بتّات أطول امتداد الصفر (zero extension) بدلاً من ذلك، لأن المحدد unsigned يمنع تفسير القيمة كسالبة أبداً. ويضع امتداد الصفر ببساطة أصفاراً في مقدمة البتّات عالية الرتبة في تسلسل البتّات الموسّع. فمثلاً، يمتد 0b1110 (14 عند تفسيره كغير مؤشّر!) إلى 0b00001110 رغم الـ1 البادئة الأصلية.

بعد أن عرضنا التمثيلات الثنائية للأعداد الصحيحة غير المؤشّرة والمؤشّرة، صرنا جاهزين لاستخدامها في العمليات الحسابية. ولحسن الحظ، وبسبب ترميزها، لا يهم الإجراءات الحسابية ما إذا اخترنا تفسير المعاملات أو النتيجة كعدد مؤشّر أو غير مؤشّر. وهذه الملاحظة خبر سار لمصممي العتاد لأنها تتيح لهم بناء مجموعة واحدة من مكوّنات العتاد يمكن مشاركتها في العمليات غير المؤشّرة والمؤشّرة معاً. ويصف فصل العتاد الدوائر الخاصة بإجراء العمليات الحسابية بمزيد من التفصيل.

ولحسن الحظ، تعمل خوارزميات القلم والورق نفسها التي تعلمتها في المدرسة الابتدائية لإجراء العمليات الحسابية على الأعداد العشرية مع الأعداد الثنائية أيضاً. ورغم أن العتاد قد لا يحسبها بالطريقة نفسها تماماً، ينبغي أن تكون قادراً على الأقل على استيعاب الحسابات.

تذكّر أنه في العدد الثنائي لا يحمل كل رقم إلا 0 أو 1. وبالتالي، عند جمع بتّتين كلتاهما 1، يُحمَل الناتج إلى الرقم التالي (مثلاً، 1 + 1 = 0b10، وهو ما يتطلب بتّتين للتمثيل). وعملياً، تجمع البرامج متغيّرات متعددة البتّات، حيث يؤثر الحمل الخارج من رقم في الرقم التالي عبر الحمل الداخل.

وبشكل عام، عند جمع أرقام عددين ثنائيين (A وB)، توجد ثماني نتائج محتملة بحسب قيم DigitA وDigitB وCarryin من الرقم السابق. ويعدّد الجدول 1 الاحتمالات الثمانية التي قد تنتج عن جمع زوج بتّات. ويشير عمود Carryin إلى حمل يدخل إلى المجموع من الرقم السابق، ويشير عمود Carryout إلى ما إذا كانت إضافة زوج الأرقام ستُخرج حملاً إلى الرقم التالي.

المدخلات المخرجات
DigitA DigitB Carryin النتيجة (المجموع) Carryout
0 0 0 0 0
0 0 1 1 0
0 1 0 1 0
0 1 1 0 1
1 0 0 1 0
1 0 1 0 1
1 1 0 0 1
1 1 1 1 1

تأمّل جمع عددين ثنائيين من أربع بتّات. ابدأ بمحاذاة العددين بحيث تقابل أرقامهما المتناظرة عمودياً، ثم اجمع كل رقم متناظر بالترتيب، من الرقم الأدنى رتبة (d0) إلى الرقم الأعلى رتبة (d3). فمثلاً، جمع 0b0010 + 0b1011:

إعداد المسألة

 
  0010
+ 1011

مثال محلول

         1    <- Carry the 1 from digit 1 into digit 2
        0010
      + 1011

Result: 1101

يوضح المثال حملاً بمقدار 1 ينتقل من d1 إلى d2. وهذا الوضع مماثل لجمع رقمين عشريين مجموعهما أكبر من 9. فمثلاً، عند جمع 5 + 8 = 13، تحتوي خانة الآحاد الناتجة على 3، وينتقل 1 إلى خانة العشرات.

المعامل الأول (0b0010) يحتوي صفراً بادئاً، لذا يمثّل 2 في تفسيرَي المتمّم الثنائي وغير المؤشّر معاً. ويمثّل المعامل الثاني (0b1011) القيمة -5 إذا فُسِّر كقيمة متمّم ثنائي مؤشّرة. وإلا فإنه يمثّل 11 إذا فُسِّر كقيمة غير مؤشّرة. ولحسن الحظ، لا يؤثر تفسير المعاملات في خطوات حساب النتيجة. أي أن النتيجة المحسوبة (0b1101) تمثّل إما 13 (غير مؤشّر: 2 + 11) أو -3 (مؤشّر: 2 + -5)، وكلاهما صحيح بحسب تفسير المعامل الثاني.

وبشكل أعم، يمثّل تسلسل من أربع بتّات القيم في النطاق [0، 15] عند تفسيره غير مؤشّر. وعند تفسيره مؤشّراً، يمثّل النطاق [-8، 7]. وفي المثال السابق، تقع النتيجة داخل النطاق القابل للتمثيل في الحالتين، لكننا قد لا نكون بهذا الحظ دائماً. فمثلاً، عند جمع 0b1100 (12 غير مؤشّر) + 0b0111 (7)، ينبغي أن يكون الجواب 19، لكن أربع بتّات لا تستطيع تمثيل 19:

إعداد المسألة

 
  1100
+ 0111

مثال محلول

             11     <- Carry a 1 from: digit 2 into digit 3, and
              1100                     digit 3 out of the overall value
           +  0111

   Result:    0011
Carry out:   1

لاحظ أن الجمع في هذا المثال يحمل 1 من البتّة الأكثر أهمية، وهي حالة تُعرف بـالحمل الخارج (carry out) لعملية الجمع الحسابية ككل. وفي هذا المثال، يشير الحمل الخارج إلى أن ناتج الحساب يحتاج بتّة إضافية لتخزين النتيجة المقصودة. غير أنه عند إجراء حساب بأربع بتّات، لا مكان لوضع البتّة الإضافية للحمل الخارج، فيُسقطها العتاد ببساطة أو يقتطعها (truncates)، تاركاً 0b0011 كنتيجة. وبالطبع، إذا كان الهدف جمع 12 + 7، فناتج 3 يُرجَّح أن يكون مفاجئاً. والمفاجأة نتيجة التجاوز (overflow). وسنستكشف كيفية كشف التجاوز ولماذا ينتج هذه النتائج في قسم لاحق.

ملاحظة

تدعم دوائر الجمع متعددة البتّات أيضاً حملاً داخلاً (carry in) يتصرف كحمل إلى الرقم الأقصى يميناً (أي أنه يعمل كمدخل Carryin للرقم d0). ولا يفيد الحمل الداخل عند إجراء الجمع — إذ يُضبط ضمنياً على 0، ولهذا لا يظهر في المثال السابق. غير أن الحمل الداخل يصبح ذا صلة في عمليات أخرى تستخدم دوائر الجمع، وأبرزها الطرح.

يجمع الطرح بين عمليتين مألوفتين: السلب والجمع. وبعبارة أخرى، طرح 7 - 3 يكافئ التعبير عن العملية بصيغة 7 + (-3). ويتماشى هذا التصوير للطرح جيداً مع سلوك العتاد — فالمعالج يحتوي بالفعل على دوائر للسلب والجمع، فمن المنطقي إعادة استخدام تلك الدوائر بدلاً من بناء دائرة طرح جديدة كلياً. تذكّر أن إجراءً بسيطاً لسلب عدد ثنائي هو قلب البتّات وإضافة واحد.

تأمّل المثال 0b0111 (7) - 0b0011 (3)، الذي يبدأ بإرسال العدد 3 إلى دائرة قلب البتّات. وللحصول على «زائد واحد»، يستفيد من الحمل الداخل إلى دائرة الجمع. أي أنه بدلاً من الحمل من رقم إلى آخر، يغذّي الطرح حملاً داخلاً إلى d0 في دائرة الجمع. وضبط الحمل الداخل على 1 يزيد قيمة «خانة الآحاد» الناتجة بمقدار واحد، وهو بالضبط ما يحتاجه للحصول على جزء «زائد واحد» من السلب. وبتجميع ذلك كله، سيبدو المثال كما يلي:

إعداد المسألة

 
  0111
- 0011

محوَّل إلى جمع

     1 (carry in)
  0111
+ 1100 (bits flipped)

مثال محلول

                1 (carry in)
             0111
           + 1100 (bits flipped)

   Result:   0100
Carry out:  1

ورغم أن نتيجة الجمع الكاملة تحمل إلى رقم إضافي، فإن النتيجة المقتطعة (0b0100) تمثّل النتيجة المتوقعة (4). وخلافاً لمثال الجمع السابق، لا يشير الحمل الخارج من البتّة عالية الرتبة بالضرورة إلى مشكلة تجاوز في الطرح.

كما يعمل إجراء الطرح كسلب متبوع بجمع عند طرح قيمة سالبة. فمثلاً، ينتج 7 - (-3) القيمة 10:

إعداد المسألة

 
  0111
- 1101

محوَّل إلى جمع

     1 (carry in)
  0111
+ 0010 (bits flipped)

مثال محلول

                1 (carry in)
             0111
           + 0010 (bits flipped)

   Result:   1010
Carry out:  0

نستكشف بمزيد من التفصيل دلالات الحمل الخارج (أو عدمه) في قسم التجاوز.

يصف هذا القسم بإيجاز ضرب الأعداد الثنائية وقسمتها على الأعداد الصحيحة. وتحديداً، يعرض طرقاً لحساب النتائج يدوياً ولا يعكس سلوك العتاد الحديث. وهذا الوصف ليس شاملاً، إذ يركّز ما تبقى من الفصل أساساً على الجمع والطرح.

الضرب

لضرب الأعداد الثنائية، استخدم استراتيجية القلم والورق الشائعة بالنظر إلى رقم واحد في كل مرة وجمع النتائج. فمثلاً، ضرب 0b0101 (5) و0b0011 (3) يكافئ جمع:

  • نتيجة ضرب d0 في 0b101 (5): 0b0101 (5)
  • نتيجة ضرب d1 في 0b101 (5) وإزاحة الناتج إلى اليسار بمقدار رقم واحد: 0b1010 (10).
  0101       0101       0101
x 0011  =  x    1  +  x   10  =  101 + 1010  =  1111 (15)

القسمة (الصحيحة)

وخلافاً للعمليات الأخرى الموصوفة للتو، قد ينتج عن القسمة ناتج غير صحيح. والشيء الأساسي الذي يجب تذكّره عند قسمة الأعداد الصحيحة أن الجزء الكسري من الناتج يُقتطع في معظم اللغات (مثل C وPython 2 وJava). وبخلاف ذلك، تستخدم القسمة الثنائية طريقة القسمة المطولة نفسها التي يتعلمها معظم الطلاب في المدرسة الابتدائية. فمثلاً، إليك كيف ينتج حساب 11 / 3 ناتجاً صحيحاً هو 3:

    ____
11 |1011

    00__   11 (3) doesn't fit into 1 (1) or 10 (2),
11 |1011   so the first two digits of the result are 00.

    001_   11 (3) fits into 101 (5) once.
11 |1011

    101    101 (5) - 11 (3) leaves 10 (2).
  -  11
     10

    0011
11 |1011   11 (3) fits into 101 (5) once again.
     101

عند هذه النقطة، أنتج الحساب الناتج الصحيح المتوقع، 0011 (3)، ويقتطع العتاد أي أجزاء كسرية. وإذا كنت مهتماً بتحديد الباقي الصحيح، فاستخدم معامل الباقي (%). فمثلاً، 11 % 3 = 2.

رغم أن عدد الأعداد الصحيحة لا نهائي رياضياً، فإن الأنواع العددية في ذاكرة الحاسوب تشغل عدداً ثابتاً من البتّات عملياً. وكما أشرنا طوال هذا الفصل، يعني استخدام عدد ثابت من البتّات أن البرامج قد تكون غير قادرة على تمثيل قيم تريد تخزينها. فمثلاً، أوضحت مناقشة الجمع أن جمع قيمتين مشروعتين قد ينتج نتيجة لا يمكن تمثيلها. والحساب الذي تفتقر نتيجته إلى مساحة التخزين اللازمة يتجاوز (overflowed).

4.5.1. تشبيه عدّاد المسافات

لوصف التجاوز، تأمّل مثالاً من العالم غير الحاسوبي: عدّاد مسافات سيارة. يعدّ عدّاد المسافات عدد الأميال التي قطعتها السيارة، وسواء أكان رقمياً أم تماثلياً، فهو لا يستطيع عرض إلا عدداً محدوداً من الأرقام (بالأساس 10). وإذا قطعت السيارة أميالاً أكثر مما يستطيع العدّاد تمثيله، «يلتف» العدّاد عائداً إلى الصفر، لأن القيمة الحقيقية لا يمكن التعبير عنها. فمثلاً، بعدّاد قياسي من ستة أرقام، القيمة العظمى التي يمثّلها 999999. وقطع ميل إضافي واحد ينبغي أن يعرض 1000000، لكن كما في مثال الجمع المتجاوز، يحمل 1 خارج الأرقام الستة المتاحة، فلا يبقى إلا 000000.

وللتبسيط، لنواصل تحليل عدّاد مسافات محدود برقم عشري واحد فقط. أي أن العدّاد يمثّل النطاق [0، 9]، فيعود إلى الصفر بعد كل 10 أميال. وبتصوير نطاق العدّاد بصرياً، قد يبدو كما في الشكل 1.

دائرة تُرتَّب حولها القيم من 0 إلى 9. الشكل 1. تصوير بصري للقيم المحتملة لعدّاد مسافات برقم واحد

ولأن عدّاد المسافات ذا الرقم الواحد يلتف عند بلوغ 10، فإن رسم شكل دائري يشدّد على الانقطاع عند أعلى الدائرة (وفقط عند الأعلى). وتحديداً، بإضافة واحد إلى أي قيمة غير التسعة، تهبط النتيجة على القيمة المتوقعة. ومن ناحية أخرى، إضافة واحد إلى تسعة تقفز إلى قيمة لا تتبعها طبيعياً (صفر). وبشكل أعم، عند إجراء أي حساب يعبر الانقطاع بين تسعة وصفر، سيتجاوز الحساب. فمثلاً، تأمّل جمع 8 + 4 كما في الشكل 2.

دائرة تُرتَّب حولها القيم من 0 إلى 9. وفجوة بين 0 و9 مُعلَّمة بوصفها الموقع الذي قد يحدث فيه تجاوز. وتُظهر الأسهم أن جمع 4 على 8 يجعل الحساب يقفز عبر موقع التجاوز المُعلَّم. الشكل 2. نتيجة جمع 8 + 4 برقم عشري واحد فقط. وعبور الانقطاع بين 0 و9 يشير إلى حدوث تجاوز.

هنا يعطي المجموع 2 بدلاً من 12 المتوقعة. ولاحظ أن كثيراً من القيم الأخرى المضافة إلى 8 (مثلاً، 8 + 14) ستهبط أيضاً على اثنين، والفرق الوحيد أن الحسابات ستقطع دورات إضافية حول الدائرة. ونتيجةً لذلك، لا يهم إن قطعت السيارة 2 أم 12 أم 152 ميلاً — ففي النهاية سيقرأ العدّاد 2 في كل الأحوال.

كل جهاز يتصرف كعدّاد مسافات يُجري حساباً معيارياً (modular arithmetic). وفي هذه الحالة، يكون كل الحساب معيارياً بالنسبة إلى معامل مقداره 10، لأن الرقم العشري الواحد لا يمثّل إلا 10 قيم. وبالتالي، بأي عدد من الأميال المقطوعة، يمكننا حساب ما سيقرأه العدّاد بقسمة المسافة على 10 وأخذ الباقي كنتيجة. ولو كان للعدّاد رقمان عشريان بدلاً من واحد، لتغيّر المعامل إلى 100، لأنه سيستطيع تمثيل نطاق أكبر من القيم: [0، 99]. وبالمثل، تُجري الساعات حساباً معيارياً بمعامل ساعات يساوي 12.

4.5.2. تجاوز الأعداد الصحيحة الثنائية

بعد أن رأينا شكلاً مألوفاً من التجاوز، لننتقل إلى ترميزات الأعداد الثنائية. تذكّر أن N بتّة من التخزين تمثّل 2N تسلسل بتّات فريداً، وأن تلك التسلسلات يمكن تفسيرها بطرق مختلفة (كغير مؤشّرة أو مؤشّرة). وبعض العمليات التي تعطي نتائج صحيحة في أحد التفسيرين قد تُظهر تجاوزاً بحسب التفسير الآخر، لذا يحتاج العتاد إلى كشف التجاوز بصورة مختلفة لكل منهما.

فمثلاً، لنفترض أن آلة تستخدم تسلسلات من أربع بتّات لحساب 0b0010 (2) - 0b0101 (5). وبإجراء هذه العملية عبر إجراء الطرح ينتج ناتج ثنائي هو 0b1101. وتفسير هذه النتيجة كقيمة مؤشّرة يعطي -3 (-8 + 4 + 1)، وهي النتيجة المتوقعة لـ 2 - 5 دون تجاوز. أما تفسيرها كقيمة غير مؤشّرة فيعطي 13 (8 + 4 + 1)، وهي نتيجة غير صحيحة وتشير بوضوح إلى تجاوز. وبفحص هذا المثال أكثر، يبدو الأمر منطقياً حدسياً — إذ ينبغي أن تكون النتيجة سالبة، والتفسير المؤشّر يسمح بالسالب بينما غير المؤشّر لا يسمح به.

التجاوز غير المؤشّر

تتصرف الأعداد غير المؤشّرة بصورة مشابهة لأمثلة عدّاد المسافات العشري بما أنهما يمثّلان قيماً غير سالبة فقط. وتمثّل N بتّة قيماً غير مؤشّرة في النطاق [0، 2N - 1]، ما يجعل كل الحساب معيارياً بالنسبة إلى 2N. ويوضح الشكل 3 ترتيب التفسيرات غير المؤشّرة لتسلسلات أربع بتّات في فضاء معياري.

تُرتَّب الأعداد من 0 إلى 15 في دائرة. وفجوة بين 15 و0 (في أعلى الدائرة) مُعلَّمة بوصفها الموقع الذي قد يحدث فيه تجاوز. الشكل 3. ترتيب القيم غير المؤشّرة الرباعية في فضاء معياري. كل الحساب معياري بالنسبة إلى 24 (16).

وبما أن التفسيرات غير المؤشّرة لا تستطيع حمل قيم سالبة، فإن الانقطاع يقع مجدداً بين القيمة العظمى والصفر. ولذلك، ينتج التجاوز غير المؤشّر عن أي عملية تعبر الفاصل بين 2N-1 و0. وبعبارة أوضح، إذا أنتج إجراء الجمع (الذي ينبغي أن يجعل النتيجة أكبر) نتيجة أصغر، فقد سبّب الجمع تجاوزاً غير مؤشّر. وبالتماثل، إذا أنتج إجراء الطرح (الذي ينبغي أن يجعل النتيجة أصغر) نتيجة أكبر، فقد سبّب الطرح تجاوزاً غير مؤشّر.

وكاختصار لكشف التجاوز غير المؤشّر في الجمع والطرح، تذكّر بتّتي الحمل الخارج والحمل الداخل في هاتين العمليتين. والحمل الخارج حمل من البتّة الأكثر أهمية في نتيجة الحساب. وعند ضبطه، يزيد الحمل الداخل قيمة النتيجة بحمل واحد إلى البتّة الأقل أهمية في العملية الحسابية. ولا يُضبط الحمل الداخل على 1 إلا في الطرح كجزء من إجراء السلب.

واختصار الحساب غير المؤشّر هو: يجب أن يطابق الحمل الخارج الحمل الداخل، وإلا سبّبت العملية تجاوزاً. وبديهياً، يعمل هذا الاختصار لأن:

  • بالنسبة للجمع (الحمل الداخل = 0)، ينبغي أن تكون النتيجة أكبر من المعامل الأول (أو مساوية له). غير أنه إذا احتاج المجموع بتّة تخزين إضافية (الحمل الخارج = 1)، فإن اقتطاع تلك البتّة الإضافية من المجموع يعطي نتيجة أصغر (تجاوز). فمثلاً، في فضاء الأعداد غير المؤشّرة الرباعية، يحتاج جمع 0b1100 (12) + 0b1101 (13) إلى خمس بتّات لتخزين النتيجة 0b11001 (25). وعند اقتطاعها إلى أربع بتّات فقط، تمثّل النتيجة 0b1001 (9)، وهي أصغر من المعاملين (وبالتالي تجاوز).
  • بالنسبة للطرح (الحمل الداخل = 1)، ينبغي أن تكون النتيجة أصغر من المعامل الأول (أو مساوية له). ولأن الطرح يُنفَّذ كتركيب من الجمع والسلب، ينبغي أن تنتج مسألة الجمع الفرعية نتيجة أصغر. والسبيل الوحيد لأن ينتهي الجمع بقيمة أصغر هو اقتطاع مجموعه (الحمل الخارج = 1). وإذا لم يتطلب اقتطاعاً (الحمل الخارج = 0)، أعطى الطرح نتيجة أكبر (تجاوز). لنفحص مثالين على الطرح الرباعي: أحدهما يتجاوز والآخر لا يتجاوز. أولاً، تأمّل 0b0111 (7) - 0b1001 (9). يعالج إجراء الطرح هذا الحساب على النحو التالي:

إعداد المسألة

 
  0111
- 1001

محوَّل إلى جمع

     1 (carry in)
  0111
+ 0110 (bits flipped)

مثال محلول

                1 (carry in)
             0111
           + 0110 (bits flipped)

   Result:   1110
Carry out:  0

لم يحمل الحساب خارج d3، فلا يحدث اقتطاع ولا يطابق الحمل الداخل (1) الحمل الخارج (0). والنتيجة 0b1110 (14) أكبر من أي من المعاملين، وبالتالي فهي غير صحيحة بوضوح بالنسبة إلى 7 - 9 (تجاوز).

بعد ذلك، تأمّل 0b0111 (7) - 0b0101 (5). يعالج إجراء الطرح هذا الحساب على النحو التالي:

إعداد المسألة

 
  0111
- 0101

محوَّل إلى جمع

     1 (carry in)
  0111
+ 1010 (bits flipped)

مثال محلول

                1 (carry in)
             0111
           + 1010 (bits flipped)

   Result:   0010
Carry out:  1

يحمل الحساب بتّة إلى d4، ما يجعل الحمل الداخل (1) يطابق الحمل الخارج (1). والنتيجة المقتطعة 0b0010 (2) تمثّل النتيجة المتوقعة لعملية الطرح تمثيلاً صحيحاً (لا تجاوز).

التجاوز المؤشّر

تنطبق الحدسية نفسها وراء التجاوز على التفسيرات الثنائية المؤشّرة: يوجد انقطاع في فضاء الأعداد المعياري. غير أنه لأن التفسير المؤشّر يسمح بالسالب، لا يقع الانقطاع حول 0. تذكّر أن المتمّم الثنائي «يلتف» بسلاسة من -1 (0b1111…111) إلى 0 (0b0000…000). وبذلك يوجد الانقطاع عند الطرف الآخر من فضاء الأعداد، حيث تلتقي أكبر قيمة موجبة بأصغر قيمة سالبة.

يعرض الشكل 4 ترتيب التفسيرات المؤشّرة لتسلسلات أربع بتّات في فضاء معياري. لاحظ أن نصف القيم سالب والنصف الآخر غير سالب، وأن الانقطاع يقع عند الفاصل الأدنى/الأعلى بينهما.

تُرتَّب الأعداد من 0 إلى 7 على النصف الأيمن من دائرة، وتُرتَّب الأعداد من -1 إلى -8 على النصف الأيسر. وفجوة بين 7 و-8 (في أسفل الدائرة) مُعلَّمة بوصفها الموقع الذي قد يحدث فيه تجاوز. الشكل 4. ترتيب القيم الرباعية المؤشّرة في فضاء معياري. ولأن التفسير المؤشّر يسمح بقيم سالبة، لم يعد الانقطاع يقع عند الصفر.

عند إجراء حساب مؤشّر، من الآمن دائماً توليد نتيجة تتحرك نحو الصفر. أي أن أي عملية تقلّل القيمة المطلقة للنتيجة لا يمكن أن تتجاوز، لأن انقطاع التجاوز يقع حيث تكون مقادير القيم القابلة للتمثيل هي الأكبر.

وبالتالي، تكشف الأنظمة التجاوز في الجمع والطرح المؤشّرين بمقارنة البتّة الأكثر أهمية في المعاملات بالبتّة الأكثر أهمية في النتيجة. وبالنسبة للطرح، أعد أولاً ترتيب الحساب بصيغة جمع (مثلاً، أعد كتابة 5 - 2 بصيغة 5 + -2).

  • إذا كانت لمعاملات الجمع قيمتا بتّة عالية الرتبة مختلفتان (أي أن أحد المعاملين سالب والآخر موجب)، فلا يمكن أن يحدث تجاوز مؤشّر، لأن القيمة المطلقة للنتيجة يجب أن تكون أصغر من أي من المعاملين (أو مساوية له). فالنتيجة تتحرك نحو الصفر.
  • وإذا كانت لمعاملات الجمع قيمة البتّة عالية الرتبة نفسها (أي أن كليهما موجبان أو كليهما سالبان)، فيجب أن تكون للنتيجة الصحيحة قيمة البتّة عالية الرتبة نفسها أيضاً. وبذلك، عند جمع معاملين بالإشارة نفسها، يحدث تجاوز مؤشّر إذا اختلفت إشارة النتيجة عن إشارة المعاملين.

تأمّل أمثلة الثنائي المؤشّر الرباعية التالية:

  • 5 - 4 يكافئ 5 + -4. والمعامل الأول (5) موجب، بينما الثاني (-4) سالب، لذا يجب أن تتحرك النتيجة نحو الصفر حيث لا يمكن حدوث تجاوز.
  • 4 + 2 (كلاهما موجب) يعطي 6 (موجب أيضاً)، فلا يحدث تجاوز.
  • -5 - 1 يكافئ -5 + -1 (كلاهما سالب) ويعطي -6 (سالبة أيضاً)، فلا تجاوز.
  • 4 + 5 (كلاهما موجب) يعطي -7 (سالبة). ولأن للمعاملين الإشارة نفسها وهي لا تطابق إشارة النتيجة، فهذه العملية تتجاوز.
  • -3 - 8 يكافئ -3 + -8 (كلاهما سالب) ويعطي 5 (موجبة). ولأن للمعاملين الإشارة نفسها وهي لا تطابق إشارة النتيجة، فهذه العملية تتجاوز.

4.5.3. ملخص التجاوز

بشكل عام، يحدث تجاوز العدد الصحيح عندما تنتقل عملية حسابية بين القيمتين الدنيا والعظمى اللتين تستطيع نتيجتها تمثيلهما. وإذا شككت يوماً في قواعد التجاوز المؤشّر مقابل غير المؤشّر، فتأمّل القيمتين الدنيا والعظمى لتسلسل من N بتّة:

  • أدنى قيمة غير مؤشّرة هي 0 (لأن الترميزات غير المؤشّرة لا تستطيع تمثيل أعداد سالبة) وأعلى قيمة غير مؤشّرة هي 2N-1 (لأن أحد تسلسلات البتّات محجوز للصفر). وبالتالي يقع الانقطاع بين 2N-1 و0.
  • أدنى قيمة مؤشّرة هي -2N-1 (لأن نصف التسلسلات محجوز للقيم السالبة) وأعلى قيمة هي 2N-1-1 (لأنه في النصف الآخر، قيمة واحدة محجوزة للصفر). وبالتالي يقع الانقطاع بين 2N-1-1 و-2N-1.

4.5.4. عواقب التجاوز

قد لا تصادف تجاوز الأعداد الصحيحة كثيراً، لكن للتجاوزات قدرة على إفساد البرامج بطرق ملحوظة (وربما مدمّرة).

فمثلاً، في عام 2014، كاد فيديو PSY الشهير Gangnam Style الموسيقي أن يتجاوز العدّاد 32 بتاً الذي كان YouTube يستخدمه لتتبع مشاهدات الفيديو. ونتيجةً لذلك، تحوّل YouTube إلى استخدام عدّاد 64 بتاً.

ويظهر مثال آخر غير ضار نسبياً في لعبة الأركيد Pac-Man عام 1980. فقد استخدم مطوّرو اللعبة قيمة غير مؤشّرة من ثماني بتّات لتتبع تقدم اللاعب في مستويات اللعبة. ونتيجةً لذلك، إذا تمكن لاعب خبير من تجاوز المستوى 255 (القيمة العظمى لعدد صحيح غير مؤشّر من ثماني بتّات)، فإن نصف اللوحة ينتهي بتعطّل كبير كما في الشكل 5.

النصف الأيمن من لوحة اللعب تالف تماماً بقيم عشوائية غير مفهومة. الشكل 5. لوحة لعبة Pac-Man «تختل» عند الوصول إلى المستوى 256

ويظهر مثال أكثر مأساوية بكثير للتجاوز في تاريخ جهاز العلاج الإشعاعي Therac-25 في منتصف الثمانينيات. وقد عانى Therac-25 من عدة مشاكل تصميم، منها واحدة كانت تزيد متغيّر علم منطقي (flag) بمقدار واحد بدلاً من ضبطه على ثابت. وبعد استخدامات كافية، تجاوز العلم قيمته، فالتف خطأً إلى الصفر (خطأ) وتجاوز آليات الأمان. وقد تسبب Therac-25 في النهاية بضرر جسيم لستة مرضى (وقتل بعضهم في حالات).

إضافةً إلى العمليات الحسابية القياسية الموصوفة سابقاً، تدعم المعالجات أيضاً عمليات غير شائعة خارج النظام الثنائي. وتطبّق المعاملات البتّية (bitwise operators) سلوك البوابات المنطقية مباشرةً على تسلسلات البتّات، ما يجعل تنفيذها بكفاءة في العتاد مباشراً. وخلافاً للجمع والطرح اللذين يستخدمهما المبرمجون عادةً للتلاعب بالتفسير العددي لمتغيّر، يستخدم المبرمجون المعاملات البتّية عادةً لتعديل بتّات محددة في متغيّر. فمثلاً، قد يرمّز برنامج موضع بتّة معيّن في متغيّر ليحمل معنى صحيح/خطأ، وتتيح العمليات البتّية للبرنامج التلاعب ببتّات المتغيّر الفردية لتغيير تلك البتّة المعيّنة.

4.6.1. الضرب البتّي AND

يقيّم معامل الضرب البتّي (&) تسلسلَي بتّات مدخلين. ولكل رقم من أرقام المدخلين، يُخرج 1 في الموضع المقابل من الناتج إذا كان كلا المدخلين 1 في ذلك الموضع. وإلا فإنه يُخرج 0 لذلك الرقم. ويعرض الجدول 1 جدول الحقيقة للضرب البتّي AND لقيمتين A وB.

A B A & B
0 0 0
0 1 0
1 0 0
1 1 1

فمثلاً، لإجراء AND بتّي بين 0b011010 و0b110110، ابدأ بمحاذاة التسلسلين. وبالفحص العمودي لكل رقم، اضبط نتيجة العمود على 1 إذا كان الرقمان كلاهما 1. وإلا فاضبط نتيجة العمود على 0:

        011010
    AND 110110  Only digits 1 and 4 are 1's in BOTH inputs, so
Result: 010010  those are the only digits set to 1 in the output.

لإجراء AND بتّي في C، ضع معامل الضرب البتّي في C (&) بين متغيّري معاملين. وفي ما يلي المثال نفسه مرة أخرى منفَّذاً في C:

int x = 26;
int y = 54;

printf("Result: %d\n", x & y);  // Prints 18

تحذير — العمليات البتّية مقابل عمليات الحقيقة المنطقية

احترس من الخلط بين المعاملات البتّية والمعاملات المنطقية للحقيقة. فرغم تشابه الأسماء (AND وOR وNOT وغيرها)، فإن الاثنين ليسا الشيء نفسه:

  • تراعي المعاملات البتّية كل بتّة من مدخلاتها على حدة وتنتج تسلسل بتّات ناتجاً كدالة للبتّات المدخلة المحددة المضبوطة.
  • أما المعاملات المنطقية فتراعي فقط التفسير الحقيقي لمعاملاتها. فبالنسبة إلى C، قيمة الصفر خطأ، بينما تُعدّ جميع القيم الأخرى صحيحة. وكثيراً ما تُستخدم المعاملات المنطقية عند تقييم الشروط (مثل عبارات if).

لاحظ أن C كثيراً ما تستخدم معاملات متشابهة (لكن مختلفة قليلاً) للتمييز بين الاثنين. فمثلاً، يمكنك الإشارة إلى AND البتّي وOR البتّي بعلامة & واحدة و| واحدة على الترتيب. ويقابل AND المنطقي وOR المنطقي علامتا && و|| المزدوجتان. وأخيراً، يستخدم NOT البتّي ~ بينما يُعبَّر عن NOT المنطقي بـ !.

4.6.2. الجمع البتّي OR

يتصرف معامل الجمع البتّي OR (|) كمعامل الضرب البتّي AND، إلا أنه يُخرج 1 لرقم ما إذا كان أحد المدخلين أو كلاهما 1 في الموضع المقابل. وإلا فإنه يُخرج 0 لذلك الرقم. ويعرض الجدول 2 جدول الحقيقة للجمع البتّي OR لقيمتين A وB.

| A | B | A | B | | --- | --- | --- | | 0 | 0 | 0 | | 0 | 1 | 1 | | 1 | 0 | 1 | | 1 | 1 | 1 |

فمثلاً، لإجراء OR بتّي بين 0b011010 و0b110110، ابدأ بمحاذاة التسلسلين. وبالفحص العمودي لكل رقم، اضبط نتيجة العمود على 1 إذا كان أي من الرقمين 1:

        011010
     OR 110110     Only digit 0 contains a 0 in both inputs, so it's
Result: 111110     the only digit not set to 1 in the result.

لإجراء OR بتّي في C، ضع معامل الجمع البتّي في C (|) بين معاملين. وفي ما يلي المثال نفسه مرة أخرى منفَّذاً في C:

int x = 26;
int y = 54;

printf("Result: %d\n", x | y);  // Prints 62

4.6.3. الـ XOR البتّي (أو الحصري)

يتصرف معامل الـ XOR البتّي (^) كمعامل الجمع البتّي OR، إلا أنه يُخرج 1 لرقم ما فقط إذا كان واحد بالضبط (لا كلاهما) من المدخلين 1 في الموضع المقابل. وإلا فإنه يُخرج 0 لذلك الرقم. ويعرض الجدول 3 جدول الحقيقة للـ XOR البتّي لقيمتين A وB.

A B A ^ B
0 0 0
0 1 1
1 0 1
1 1 0

فمثلاً، لإجراء XOR بتّي بين 0b011010 و0b110110، ابدأ بمحاذاة التسلسلين. وبالفحص العمودي لكل رقم، اضبط نتيجة العمود على 1 إذا كان رقم واحد فقط 1:

        011010
    XOR 110110     Digits 2, 3, and 6 contain a 1 in exactly one of
Result: 101100     the two inputs.

لإجراء XOR بتّي في C، ضع معامل الـ XOR البتّي في C (^) بين معاملين. وفي ما يلي المثال نفسه مرة أخرى منفَّذاً في C:

int x = 26;
int y = 54;

printf("Result: %d\n", x ^ y);  // Prints 44

4.6.4. النفي البتّي NOT

يعمل معامل النفي البتّي (~) على معامل واحد فقط. ولكل بتّة في التسلسل، يقلب البتّة ببساطة بحيث يصبح الصفر واحداً أو العكس. ويعرض الجدول 4 جدول الحقيقة لمعامل النفي البتّي.

A ~ A
0 1
1 0

فمثلاً، لإجراء NOT بتّي على 0b011010، اقلب قيمة كل بتّة:

    NOT 011010
Result: 100101

لإجراء NOT بتّي في C، ضع محرف التلدة (~) أمام معامل. وفي ما يلي المثال نفسه مرة أخرى منفَّذاً في C:

int x = 26;

printf("Result: %d\n", ~x); // Prints -27

تحذير — النفي البتّي مقابل السلب

لاحظ أن جميع الأنظمة الحديثة تمثّل الأعداد الصحيحة بالمتمّم الثنائي، لذا ليس النفي البتّي تماماً كالسلب. فالنفي البتّي يقلب البتّات فقط ولا يضيف واحداً.

4.6.5. إزاحة البتّات

وهناك عملية بتّية مهمة أخرى تتضمن إزاحة موضع بتّات معامل إما إلى اليسار (>). ويأخذ معاملا الإزاحة اليسرى واليمنى وسيطين: تسلسل البتّات المطلوب إزاحته وعدد المواضع التي ينبغي إزاحته بها.

الإزاحة إلى اليسار

إزاحة تسلسل إلى اليسار بمقدار N موضعاً تنقل كل بتّة من بتّاته إلى اليسار N مرة، وتضيف أصفاراً جديدة إلى الجانب الأيمن من التسلسل. فمثلاً، إزاحة التسلسل الثماني 0b00101101 إلى اليسار بمقدار اثنين ينتج 0b10110100. ويُضاف الصفران في اليمين إلى نهاية التسلسل، لأن النتيجة لا بد أن تبقى تسلسلاً من ثماني بتّات.

وفي غياب التجاوز، تزيد الإزاحة إلى اليسار قيمة النتيجة لأن البتّات تنتقل نحو أرقام تسهم بقوى أكبر للعدد اثنين في قيمة العدد. غير أنه مع عدد ثابت من البتّات، تُقتطع أي بتّات تنتقل إلى مواضع تتجاوز السعة العظمى للعدد. فمثلاً، إزاحة التسلسل الثماني 0b11110101 (التفسير غير المؤشّر 245) إلى اليسار بمقدار واحد ينتج 0b11101010 (التفسير غير المؤشّر 234). وهنا يجعل اقتطاع البتّة عالية الرتبة التي أُزيحت خارج التسلسل النتيجة أصغر.

لإجراء إزاحة بتّية يسرى في C، ضع محرفَي أصغر من (<<) بين قيمة وعدد المواضع المطلوب إزاحة تلك القيمة بها:

int x = 13;  // 13 is 0b00001101

printf("Result: %d\n", x << 3);  // Prints 104 (0b01101000)

الإزاحة إلى اليمين

تشبه الإزاحة إلى اليمين الإزاحة إلى اليسار — فأي بتّات تُزاح خارج سعة المتغيّر (مثلاً من الطرف إلى اليمين) تختفي بسبب الاقتطاع. غير أن الإزاحة إلى اليمين تُدخل اعتباراً إضافياً: قد تحتاج البتّات الجديدة التي تُضاف إلى الجانب الأيسر من النتيجة إلى أن تكون كلها أصفار أو كلها آحاد بحسب نوع المتغيّر الجاري إزاحته وقيمة بتّته عالية الرتبة. ومن الناحية المفاهيمية، يشبه اختيار إضافة أصفار أو آحاد اختيار امتداد الإشارة. وبذلك يوجد متغيّران متميزان للإزاحة إلى اليمين:

  • الإزاحة اليمنى المنطقية (logical right shift) تضيف دائماً أصفاراً إلى البتّات عالية الرتبة في النتيجة. وتُستخدم الإزاحة المنطقية لإزاحة المتغيّرات غير المؤشّرة، لأن وجود 1 بادئ في البتّة الأكثر أهمية في قيمة غير مؤشّرة لا يُقصد به أن القيمة سالبة. فمثلاً، إزاحة 0b10110011 إلى اليمين بمقدار اثنين بإزاحة منطقية ينتج 0b00101100.
  • الإزاحة اليمنى الحسابية (arithmetic right shift) تضيف نسخة من البتّة الأكثر أهمية في القيمة المُزاحة إلى كل موضع بتّة جديد. وتنطبق الإزاحة الحسابية على المتغيّرات المؤشّرة التي يهم فيها الحفاظ على إشارة البتّات عالية الرتبة. فمثلاً، إزاحة 0b10110011 إلى اليمين بمقدار اثنين بإزاحة حسابية ينتج 0b11101100.

ولحسن الحظ، عند البرمجة بلغة C، لا تحتاج عادةً إلى القلق بشأن هذا التمييز إذا أعلنت متغيّراتك إعلاناً صحيحاً. فإذا تضمن برنامجك معامل إزاحة يمنى (>>)، فسيُنفّذ كل مصرّف C تقريباً نوع الإزاحة المناسب تلقائياً بحسب نوع المتغيّر المُزاح. أي أنه إذا أُعلن المتغيّر المُزاح بمحدد unsigned، فسينفّذ المصرّف إزاحة منطقية. وإلا فسينفّذ إزاحة حسابية.

ملاحظة — برنامج مثال للإزاحة اليمنى في C

يمكنك اختبار سلوك الإزاحة اليمنى ببرنامج مثال صغير كهذا:

#include <stdio.h>

int main(int argc, char **argv) {
    /* Unsigned integer value: u_val. */
    unsigned int u_val = 0xFF000000;

    /* Signed integer value: s_val. */
    int s_val = 0xFF000000;

    printf("%08X\n", u_val >> 12);  // logical right shift
    printf("%08X\n", s_val >> 12);  // arithmetic right shift

    return 0;
}

يعلن هذا البرنامج عددين صحيحين من 32 بتّة: أحدهما كعدد صحيح غير مؤشّر (u_val) والآخر كعدد صحيح مؤشّر (s_val). ويهيّئ كلا العددين بالقيمة الابتدائية نفسها: تسلسل من ثمانية آحاد تليها 24 صفراً (0b1111111100000000000000000000000000)، ثم يزيح القيمتين 12 موضعاً إلى اليمين. وعند تنفيذه، يطبع:

$ ./a.out
000FF000
FFFFF000

ولأن 1 البادئ لا يشير إلى «سالب» في النوع غير المؤشّر u_val يستخدم المصرّف تعليمات تضيف أصفاراً فقط في المقدمة. وتحتوي النتيجة المُزاحة 12 صفراً وثمانية آحاد و12 صفراً آخر (0b00000000000011111111000000000000). وفي المقابل، يشير 1 البادئ فعلاً إلى «سالب» في s_val، لذا يضيف المصرّف آحاداً إلى مقدمة القيمة المُزاحة، فينتج 20 واحداً تليها 12 صفراً (0b11111111111111111111000000000000).

وصف هذا الفصل حتى الآن عدة مخططات لترميز الأعداد بالبتّات، لكنه لم يذكر كيفية تنظيم القيم في الذاكرة. وفي الأنظمة الحديثة، أصغر وحدة ذاكرة قابلة للعنونة هي البايت، المكوّن من ثماني بتّات. وبالتالي، لتخزين قيمة من بايت واحد (مثلاً متغيّر من النوع char) بدءاً من العنوان X، لا تملك في الحقيقة أي خيارات — ما عليك إلا تخزين البايت في الموقع X.

غير أنه بالنسبة للقيم متعددة البايتات (مثل متغيّرات من النوع short أو int)، يملك العتاد خيارات أكثر لإسناد بايتات القيمة إلى عناوين الذاكرة. فمثلاً، تأمّل متغيّر short من بايتين، s، بايتاه مسمّيان A (يحتوي البتّات عالية الرتبة من s) وB (يحتوي البتّات منخفضة الرتبة من s). وعندما يُطلب من نظام تخزين short مثل s عند العنوان X (أي في العنوانين X وX+1)، يجب أن يحدد أي بايت من المتغيّر (A أم B) ينبغي أن يشغل أي عنوان (X أم X+1). ويعرض الشكل 1 الخيارين لتخزين s في الذاكرة.

في التخطيط الأول يشغل البايت A العنوان X والبايت B العنوان X+1، وفي التخطيط الآخر يتبادلان مكانيهما. الشكل 1. تخطيطان محتملان للذاكرة لمتغيّر short من بايتين بدءاً من عنوان الذاكرة X

يحدد ترتيب البايتات (byte order أو endianness) في النظام كيفية إسناد عتاده بايتات متغيّر متعدد البايتات إلى عناوين ذاكرة متتالية. ورغم أن ترتيب البايتات نادراً ما يمثّل مشكلة للبرامج التي تعمل على نظام واحد فقط، فقد يبدو مفاجئاً إذا حاول أحد برامجك طباعة البايتات واحداً في كل مرة أو إذا كنت تفحص المتغيّرات بمنقّح.

فمثلاً، تأمّل البرنامج التالي:

#include <stdio.h>

int main(int argc, char **argv) {
    // Initialize a four-byte integer with easily distinguishable byte values
    int value = 0xAABBCCDD;

    // Initialize a character pointer to the address of the integer.
    char *p = (char *) &value;

    // For each byte in the integer, print its memory address and value.
    int i;
    for (i = 0; i < sizeof(value); i++) {
        printf("Address: %p, Value: %02hhX\n", p, *p);
        p += 1;
    }

    return 0;
}

يخصّص هذا البرنامج عدداً صحيحاً من أربعة بايتات ويهيّئ البايتات، بالترتيب من الأكثر أهمية إلى الأقل، بالقيم الست عشرية 0xAA و0xBB و0xCC و0xDD. ثم يطبع البايتات واحداً في كل مرة بدءاً من عنوان أساس العدد الصحيح. وقد تُعذر إن توقعت طباعة البايتات بترتيب أبجدي. غير أن معماريات المعالجات الشائعة (أي x86 ومعظم عتاد ARM) تطبع البايتات بترتيب معكوس عند تنفيذ البرنامج المثال:

$ ./a.out
Address: 0x7ffc0a234928, Value: DD
Address: 0x7ffc0a234929, Value: CC
Address: 0x7ffc0a23492a, Value: BB
Address: 0x7ffc0a23492b, Value: AA

تخزّن معالجات x86 الأعداد الصحيحة بصيغة little-endian — من البايت الأقل أهمية («الطرف الصغير») إلى البايت الأكثر أهمية في عناوين متتالية. أما معماريات المعالجات big-endian الأخرى فتخزّن الأعداد الصحيحة متعددة البايتات بالترتيب المعاكس. ويصوّر الشكل 2 عدداً صحيحاً من أربعة بايتات في تخطيطَي (a) big-endian و(b) little-endian.

في صيغة big-endian يشغل البايت AA الموضع X وتتقدّم البايتات بترتيب أبجدي في عناوين متتالية. وفي صيغة little-endian يشغل البايت DD الموضع X وتتقدّم البايتات بترتيب أبجدي عكسي. الشكل 2. تخطيط الذاكرة لعدد صحيح من أربعة بايتات بصيغتي (a) big-endian و(b) little-endian

تعود مصطلحات «endian» الغريبة الظاهر إلى رواية جوناثان سويفت الساخرة رحلات جوليفر (1726)1. ففي القصة يجد جوليفر نفسه بين إمبراطوريتين من أناس بطول ست بوصات يخوضون حرباً حول الطريقة الصحيحة لكسر البيض. فإمبراطورية «big-endian» في بلفسكو تكسر الطرف الكبير من بيضها، بينما يكسر الناس في إمبراطورية «little-endian» في ليليبوت الطرف الصغير.

وفي عالم الحوسبة، يؤثر كون النظام big-endian أو little-endian عادةً في البرامج التي تتواصل عبر الأجهزة فقط (مثلاً عبر شبكة). وعند تبادل البيانات بين الأنظمة، يجب أن يتفق النظامان على ترتيب البايتات ليتمكن المتلقي من تفسير القيمة تفسيراً صحيحاً. وفي عام 1980، كتب داني كوهن ملاحظة إلى فرقة هندسة الإنترنت (IETF) بعنوان حول الحروب المقدسة وطلب السلام 2. وفي تلك الملاحظة يتبنى كوهن مصطلحات سويفت «endian» ويقترح أن تعتمد IETF ترتيب بايتات قياسياً للإرسال عبر الشبكات. وقد اعتمدت IETF في النهاية big-endian كمعيار «ترتيب بايتات الشبكة».

وتوفّر لغة C مكتبتين تتيحان للبرنامج إعادة ترتيب بايتات عدد صحيح3,4 لأغراض الاتصال.

4.7.1. المراجع

  1. جوناثان سويفت. رحلات جوليفر. http://www.gutenberg.org/ebooks/829
  2. داني كوهن. حول الحروب المقدسة وطلب السلام. https://www.ietf.org/rfc/ien/ien137.txt
  3. https://linux.die.net/man/3/byteorder
  4. https://linux.die.net/man/3/endian رغم أن هذا الفصل يركّز أساساً على تمثيلات الأعداد الصحيحة الثنائية، فإن المبرمجين كثيراً ما يحتاجون إلى تخزين الأعداد الحقيقية أيضاً. وتخزين الأعداد الحقيقية صعب بطبيعته، ولا يمثّل أي ترميز ثنائي القيم الحقيقية بدقة تامة. أي أنه لأي ترميز ثنائي للأعداد الحقيقية، توجد قيم لا يمكن تمثيلها تمثيلاً دقيقاً. ومن الواضح أن القيم غير النسبية مثل pi لا يمكن تمثيلها بدقة، لأن تمثيلها لا ينتهي أبداً. ومع عدد ثابت من البتّات، ما تزال الترميزات الثنائية عاجزة عن تمثيل بعض القيم النسبية داخل نطاقها.

وخلافاً للأعداد الصحيحة التي تكون قابلة للعدّ لا نهائياً، فإن مجموعة الأعداد الحقيقية غير قابلة للعدّ. وبعبارة أخرى، حتى لنطاق ضيق من القيم الحقيقية (مثلاً بين صفر وواحد)، تكون مجموعة القيم داخل ذلك النطاق كبيرة إلى حدّ لا يمكننا فيه حتى البدء بتعدادها. وبالتالي، تخزّن ترميزات الأعداد الحقيقية عادةً تقديرات مقرّبة فقط لقيم قُطعت إلى عدد محدد مسبقاً من البتّات. ومع بتّات كافية، تكون التقديرات التقريبية دقيقة بما يكفي لمعظم الأغراض عادةً، لكن كن حذراً عند كتابة تطبيقات لا تتحمل التقريب.

يصف ما تبقى من هذا القسم بإيجاز طريقتين لتمثيل الأعداد الحقيقية ثنائياً: الفاصلة الثابتة (fixed-point) التي توسّع صيغة الأعداد الصحيحة الثنائية، والفاصلة العائمة (floating-point) التي تمثّل نطاقاً واسعاً من القيم بتكلفة بعض التعقيد الإضافي.

4.8.1. تمثيل الفاصلة الثابتة

في تمثيل الفاصلة الثابتة، يبقى موضع الفاصلة الثنائية (binary point) للقيمة ثابتاً ولا يمكن تغييره. ومثل الفاصلة العشرية في العدد العشري، تشير الفاصلة الثنائية إلى المكان الذي يبدأ فيه الجزء الكسري من العدد. وتشبه قواعد ترميز الفاصلة الثابتة تمثيل الأعداد الصحيحة غير المؤشّرة باستثناء رئيسي واحد: تمثّل الأرقام بعد الفاصلة الثنائية قوى للعدد اثنين مرفوعة إلى قيمة سالبة. فمثلاً، تأمّل التسلسل الثماني 0b000101.10 الذي تمثّل بتّاته الست الأولى أعداداً صحيحة وتمثّل بتّتاه المتبقيتان الجزء الكسري. ويسمّي الشكل 1 مواضع الأرقام وتفسيراتها الفردية.

من الأعلى إلى الأدنى تُسمّى الأرقام d5 وd4 وd3 وd2 وd1 وd0 وd-1 وd-2. ويُسهم d-1 بـ0.5 وd-2 بـ0.25 في القيمة. الشكل 1. قيمة كل رقم في عدد من ثماني بتّات ببتّتين بعد الفاصلة الثنائية الثابتة

يُظهر تطبيق صيغة تحويل 0b000101.10 إلى عشري ما يلي:

(0 × 25) + (0 × 24) + (0 × 23) + (1 × 22) + (0 × 21) + (1 × 20) + (1 × 2-1) + (0 × 2-2)

= 0 + 0 + 0 + 4 + 0 + 1 + 0.5 + 0 = 5.5

وبشكل أعم، بوجود بتّتين بعد الفاصلة الثنائية، يحمل الجزء الكسري من العدد أحد أربعة تسلسلات: 00 ‏(.00)، أو 01 ‏(.25)، أو 10 ‏(.50)، أو 11 ‏(.75). وبذلك تتيح بتّتان كسريتان لعدد الفاصلة الثابتة تمثيل قيم كسرية دقيقة حتى 0.25 (2-2). وإضافة بتّة ثالثة تزيد الدقة إلى 0.125 (2-3)، ويستمر النمط على نحو مشابه، حيث تتيح N بتّة بعد الفاصلة الثنائية دقة قدرها 2-N.

ولأن عدد البتّات بعد الفاصلة الثنائية يبقى ثابتاً، فقد تنتج بعض الحسابات بمعاملات دقيقة تماماً نتيجة تتطلب اقتطاعاً (تقريباً). تأمّل ترميز الفاصلة الثابتة الثماني نفسه من المثال السابق. فهو يمثّل بدقة كلاً من 0.75 (0b000000.11) و2 (0b000010.00). غير أنه لا يستطيع تمثيل نتيجة قسمة 0.75 على 2 تمثيلاً دقيقاً: فالحساب ينبغي أن ينتج 0.375، لكن تخزينها سيتطلب بتّة ثالثة بعد الفاصلة الثنائية (0b000000.011). واقتطاع الـ1 الأقصى يميناً يتيح للنتيجة أن تتسع في الصيغة المحددة، لكنه يعطي نتيجة مقرّبة مقدارها 0.75 / 2 = 0.25. وفي هذا المثال، يكون التقريب فادحاً بسبب قلة عدد البتّات المتضمنة، لكن حتى التسلسلات الأطول من البتّات ستتطلب اقتطاعاً في مرحلة ما.

والأسوأ من ذلك أن أخطاء التقريب تتراكم على مدار الحسابات الوسيطة، وقد تختلف في بعض الحالات نتيجة سلسلة من الحسابات بحسب الترتيب الذي تُنفَّذ به. فمثلاً، تأمّل سلسلتي حسابات في ترميز الفاصلة الثابتة الثماني نفسه الموصوف سابقاً:

  1. (0.75 / 2) * 3 = 0.75
  2. (0.75 * 3) / 2 = 1.00

لاحظ أن الفرق الوحيد بين السلسلتين هو ترتيب عمليتي الضرب والقسمة. ولو لم يكن التقريب ضرورياً، لكان ينبغي أن ينتج الحسابان النتيجة نفسها (1.125). غير أنهم، بسبب وقوع الاقتطاع في مواضع مختلفة من الحساب، ينتجان نتيجتين مختلفتين:

  1. بالانتقال من اليسار إلى اليمين، تُقرَّب النتيجة الوسيطة (0.75 / 2) إلى 0.25 وتنتج في النهاية 0.75 عند ضربها في 3.
  2. وبالانتقال من اليسار إلى اليمين، ينتج الحساب الوسيط (0.75 * 3) القيمة 2.25 بدقة دون أي تقريب. وقسمة 2.25 على 2 تُقرَّب إلى نتيجة نهائية مقدارها 1.

في هذا المثال، تتيح بتّة إضافية واحدة لخانة 2-3 نجاح المثال بدقة تامة، لكن موضع الفاصلة الثابتة الذي اخترناه لم يسمح إلا ببتّتين بعد الفاصلة الثنائية. وطوال ذلك، بقيت البتّات عالية الرتبة في المعاملات غير مستخدمة تماماً (فالأرقام d2 حتى d5 لم تُضبط على 1 قط). وبتكلفة تعقيد إضافي، يتيح تمثيل بديل (الفاصلة العائمة) لمجموعة البتّات كاملة أن تسهم في القيمة بصرف النظر عن تقسيمها بين الجزأين الصحيح والكسري.

4.8.2. تمثيل الفاصلة العائمة

في تمثيل الفاصلة العائمة (floating-point)، لا تُثبَّت الفاصلة الثنائية للقيمة في موضع محدد مسبقاً. أي أن تفسير التسلسل الثنائي يجب أن يرمّز كيفية تمثيله التقسيم بين الجزأين الصحيح والكسري للقيمة. ورغم أنه يمكن ترميز موضع الفاصلة الثنائية بطرق محتملة كثيرة، يركّز هذا القسم على طريقة واحدة فقط، هي معيار معهد مهندسي الكهرباء والإلكترونيات (IEEE) رقم 754. ويتبع كل العتاد الحديث تقريباً معيار IEEE 754 لتمثيل قيم الفاصلة العائمة.

يمثّل الرقم الأيسر البتّة الإشارية. والبتّات الثماني التاليات تُمثّل الأسّ، والبتّات الـ23 المتبقية تُمثّل الكوّن. الشكل 2. معيار IEEE 754 للفاصلة العائمة من 32 بتّة

يوضح الشكل 2 تفسير IEEE 754 لعدد فاصلة عائمة من 32 بتّة (نوع float في C). ويقسّم المعيار البتّات إلى ثلاث مناطق:

  1. تمثّل البتّات الـ23 الأقل رتبة (الأرقام d22 حتى d0) الجزء الدال (significand) (ويُسمى أحياناً الجزء العشري mantissa). وباعتبارها أكبر منطقة من البتّات، يعمل الجزء الدال أساساً للقيمة، التي تُعدَّل في النهاية بضربها وفقاً لمناطق البتّات الأخرى. وعند تفسير الجزء الدال، تتبع قيمته ضمنياً 1 وفاصلة ثنائية. ويتصرف الجزء الكسري مثل تمثيل الفاصلة الثابتة الموصوف في القسم السابق. فمثلاً، إذا احتوت بتّات الجزء الدال على 0b110000…0000، تمثّل البتّة الأولى 0.5 (1 × 2-1)، وتمثّل البتّة الثانية 0.25 (1 × 2-2)، وكل البتّات المتبقية أصفار فلا تؤثر في القيمة. وبذلك يسهم الجزء الدال بـ1.(0.5 + 0.25)، أي 1.75.
  2. وتمثّل البتّات الثماني التالية (الأرقام d30 حتى d23) الأس (exponent) الذي يوسّع قيمة الجزء الدال لتوفير نطاق واسع قابل للتمثيل. ويُضرب الجزء الدال في 2exponent - 127، حيث 127 إزاحة (bias) تتيح لعدد الفاصلة العائمة تمثيل قيم كبيرة جداً وصغيرة جداً معاً.
  3. وتمثّل البتّة عالية الرتبة الأخيرة (الرقم d31) بتّة الإشارة (sign bit) التي ترمّز ما إذا كانت القيمة موجبة (0) أو سالبة (1).

وكمثال، تأمّل فك ترميز التسلسل 0b11000001101101000000000000000000. الجزء الدال هو 01101000000000000000000، ويمثّل 2-2 + 2-3 + 2-5 = 0.40625، لذا يسهم الجزء الدال بـ1.40625. والأس هو 10000011، ويمثّل القيمة العشرية 131، لذا يسهم الأس بمعامل 2(131-127) ‏(16). وأخيراً، بتّة الإشارة هي 1، لذا يمثّل التسلسل قيمة سالبة. وبتجميع ذلك كله، يمثّل التسلسل:

1.40625 × 16 × -1  = -22.5

ورغم أن معيار IEEE للفاصلة العائمة أعقد بوضوح من مخطط الفاصلة الثابتة الموصوف سابقاً، فإنه يوفّر مرونة إضافية لتمثيل نطاق واسع من القيم. ورغم هذه المرونة، تظل صيغة الفاصلة العائمة ذات العدد الثابت من البتّات عاجزة عن تمثيل كل قيمة ممكنة بدقة. أي أنه، مثل الفاصلة الثابتة، تؤثر مشاكل التقريب في ترميزات الفاصلة العائمة أيضاً.

4.8.3. عواقب التقريب

رغم أنه ليس مرجحاً أن يفسد التقريب معظم البرامج التي تكتبها، فقد تسببت أخطاء تقريب الأعداد الحقيقية أحياناً في إخفاقات نظامية بارزة. فخلال حرب الخليج عام 1991، تسبب خطأ تقريب في فشل بطارية صاروخ باتريوت أمريكية في اعتراض صاروخ عراقي. وقتل الصاروخ 28 جندياً وأصاب كثيرين آخرين. وفي عام 1996، انفجر صاروخ آريان 5 التابع لوكالة الفضاء الأوروبية بعد 39 ثانية من إقلاعه. وقد أطلق الصاروخ، الذي استعار كثيراً من شيفرته من آريان 4، تجاوزاً عند محاولة تحويل قيمة فاصلة عائمة إلى قيمة صحيحة.

فحص هذا الفصل كيفية تمثيل الحواسيب الحديثة للمعلومات بالبتّات والبايتات. ومن الخلاصات المهمة أن ذاكرة الحاسوب تخزّن جميع المعلومات كأصفار وآحاد ثنائية — ويعود تفسير معنى تلك البتّات إلى البرامج أو الأشخاص الذين يشغّلونها. وركّز هذا الفصل أساساً على تمثيلات الأعداد الصحيحة، بدءاً بالأعداد الصحيحة غير المؤشّرة (غير السالبة) قبل النظر في الأعداد الصحيحة المؤشّرة.

يدعم عتاد الحاسوب مجموعة متنوعة من العمليات على الأعداد الصحيحة، منها الجمع والطرح والضرب والقسمة المألوفة. وتوفّر الأنظمة أيضاً عمليات بتّية مثل AND وOR وNOT البتّية والإزاحة. وعند إجراء أي عملية، تأمّل عدد البتّات المستخدمة لتمثيل المعاملات والنتيجة. وإذا لم تكن مساحة التخزين المخصَّصة للنتيجة كبيرة بما يكفي، فقد يمثّل التجاوز القيمة الناتجة تمثيلاً خاطئاً.

وأخيراً، استكشف هذا الفصل مخططات شائعة لتمثيل الأعداد الحقيقية ثنائياً، بما في ذلك معيار IEEE 754 القياسي. ولاحظ أننا عند تمثيل قيم الفاصلة العائمة نضحي بالدقة مقابل مرونة أكبر (أي القدرة على تحريك الفاصلة العشرية).

تمارين إضافية

  1. ما التمثيلان العشري والست عشري للقيمة 0b01001010؟
  2. ما التمثيلان الثنائي والست عشري للقيمة 389؟
  3. بصفتها مخلوقاً بخمسة أذرع، تفضّل سالي نجمة البحر تمثيل الأعداد باستخدام نظام أعداد بالأساس 5. إذا أعطتك سالي العدد 1423 بالأساس 5، فما قيمته العشرية المكافئة؟

الحلول

إذا كان متصفحك يدعم الحلول المخفية، فانقر هنا لكشفها.

0b01001010 بالعشري هو:

(0 * 27) + (1 * 26) + (0 * 25) + (0 * 24) + (1 * 23) + (0 * 22) + (1 * 21) + (0 * 20)

= 0 + 64 + 0 + 0 + 8 + 0 + 2 + 0 = 74

وبالست عشري هو:

0100 1010
  4    A  ->  0x4A

تحويل 389 إلى النظام الثنائي…

باستخدام قوى العدد اثنين:

يتسع 256 في 389، لذا ينبغي أن يكون d8 هو 1. ويتبقى 389 - 256 = 133.

يتسع 128 في 133، لذا ينبغي أن يكون d7 هو 1. ويتبقى 133 - 128 = 5.

لا يتسع 64 في 5، لذا ينبغي أن يكون d6 هو 0.

لا يتسع 32 في 5، لذا ينبغي أن يكون d5 هو 0.

لا يتسع 16 في 5، لذا ينبغي أن يكون d4 هو 0.

لا يتسع 8 في 5، لذا ينبغي أن يكون d3 هو 0.

يتسع 4 في 5، لذا ينبغي أن يكون d2 هو 1. ويتبقى 6 - 5 = 1.

لا يتسع 2 في 1، لذا ينبغي أن يكون d1 هو 0.

يتسع 1 في 1، لذا ينبغي أن يكون d0 هو 1. ويتبقى 1 - 1 = 0.

وبذلك يقابل العدد العشري 389 القيمة 0b110000101.

باستخدام القسمة المتكررة:

389 فردي، لذا ينبغي أن يكون d0 هو 1.

389 / 2 = 194، وهو زوجي، لذا ينبغي أن يكون d1 هو 0.

194 / 2 = 97، وهو فردي، لذا ينبغي أن يكون d2 هو 1.

97 / 2 = 48، وهو زوجي، لذا ينبغي أن يكون d3 هو 0.

48 / 2 = 24، وهو زوجي، لذا ينبغي أن يكون d4 هو 0.

24 / 2 = 12، وهو زوجي، لذا ينبغي أن يكون d5 هو 0.

12 / 2 = 6، وهو زوجي، لذا ينبغي أن يكون d6 هو 0.

6 / 2 = 3، وهو فردي، لذا ينبغي أن يكون d7 هو 1.

3 / 2 = 1، وهو فردي، لذا ينبغي أن يكون d8 هو 1.

1 / 2 = 0، لذا سيكون أي رقم تسعة أو أعلى هو 0.

وبذلك يقابل العدد العشري 389 القيمة 0b110000101.

التحويل إلى الست عشري:

0001 1000 0101
  1    8    5  ->  0x185

1423 بالأساس 5 يحوَّل إلى العشري كالتالي:

(1 * 53) + (4 * 52) + (2 * 51) + (3 * 50)

= 125 + 100 + 10 + 3 = 238