اذهب إلى المحتوى

الفصل الخامس: تمثيل الأعداد والنصوص بالبتات وإجراء العمليات على مستوى البت


Ola Abbas

تمثيل الأعداد الصحيحة (Representing integers)

لا بدّ أنك تعلم أن الحواسيب تمثّل الأعداد بنظام العد ذو الأساس 2 (base 2) والمعروف أيضًا بالنظام الثنائي (binary). التمثيل الثنائي للأعداد الموجبة واضحٌ فعلى سبيل المثال التمثيل الثنائي للعدد 510 (أي للعدد 5 في نظام العد العشري) هو b101 (أي 101 بنظام العد الثنائي (binary)). بينما يستخدم التمثيلُ الأوضح للأعداد السالبة بتًا للإشارة (sign bit) لتحديد فيما إذا كان العدد موجبًا أو سالبًا، ولكن يوجد تمثيلٌ آخر يدعى بالمتمّم الثنائي (two's complement) وهو التمثيل الأكثر شيوعًا لأن العمل معه أسهل ضمن العتاد. لإيجاد المتمم الثنائي للعدد السالب ‎-x تجد التمثيل الثنائي للعدد x أولًا، ثم تقلب (flip) كل البتات أي تقلب الأصفار واحداتٍ والواحدات أصفارًا، ثم تجمع 1 لناتج القلب، فلتمثيل العدد ‎-510 بالنظام الثنائي على سبيل المثال، تبدأ بتمثيل العدد 510 بالنظام الثنائي بكتابته بنسخة 8 بت (8bit version) وهو b00000101، ثم تقلب (flip) كل البتات وتضيف له 1 فينتج b11111011. يتصرف البت الموجود أقصى اليسار في المتمم الثنائي كبت إشارة، فهو 0 في الأعداد الموجبة و1 في الأعداد السالبة. يجب إضافة أصفار للعدد الموجب وواحدات للعدد السالب عند تحويل عدد من النوع 8 بت إلى 16 بتًا، أي يجب نسخ قيمة بت الإشارة إلى البتات الجديدة وتدعى هذه العملية بامتداد الإشارة (sign extension). كل أنواع الأعداد الصحيحة في لغة البرمجة C لها إشارة أي أنها قادرة على تمثيل الأعداد السالبة والموجبة إلّا إذا صرّحت عنهم كأعداد صحيحة بِلا إشارة (unsigned)، والاختلاف الذي يجعل هذا التصريح (declaration) مهمًا هو أن عمليات الأعداد الصحيحة التي لا تملك إشارة (unsigned integers) لا تستخدم امتداد الإشارة (sign extension).

العاملات الثنائية (Bitwise operators)

يُصاب متعلمو لغة البرمجة C بالارتباك أحيانًا بالنسبة للعاملين الثنائيين (& و |)، حيث يعامِل هذان العامِلان الأعدادَ الصحيحة (integers) كمتجهات من البتات (bit vectors) وتُحسَب العمليات المنطقية (logical operations) بتطبيقها على البتات المتناظرة (corresponding bits).، حيث تَحسب & عملية AND بحيث ينتج 1 إذا كانت قيمة كلا المُعامَلين (operands) هي 1 وينتج 0 بخلاف ذلك. المثال التالي هو تطبيق العامِل & على عددين مكونين من 4 بتات:

    1100
  & 1010
    ----
    1000

وهذا يعني في لغة البرمجة C أن قيمة التعبير 12 & 10 هي 8.

ويحسب العامِل | عملية OR بحيث ينتج 1 إذا كانت قيمة أحد المعامَلين 1 وينتج 0 بخلاف ذلك كما في المثال التالي:

  1100   
| 1010
   ----
  1110

أي قيمة التعبير 12 | 10 هي 14.

ويحسب العامل ^ عملية XOR بحيث ينتج 1 إذا كانت قيمة أحد المعامَلين 1 وليس كلاهما كما في المثال التالي:

   1100
 ^ 1010
   ----
   0110

أي قيمة التعبير 12 ^ 10 هي 6.

يُستخدَم العامل & لتصفير (clear) مجموعة بتات من متجهة بتات، بينما يُستخدم العامل | لضبط (set) البتات، ويُستخدم العامل ^ لقلب (flip) أو تبديل (toggle) البتات كما يلي:

  • تصفير البتات (Clearing bits): قيمة x&0 هي 0 و x&1 هي x مهما كانت قيمة x، لذلك عند تطبيق العملية AND على متجهة مع العدد 3 فسيُختار البتان الموجودان أقصى اليمين فقط وتُضبَط بقية البتات بالقيمة 0 كما يلي:
    xxxx
  & 0011
    ----
    00xx

حيث تدعى القيمة 3 العشرية أو 0011 الثنائية بقناع (mask) لأنها تختار بعض البتات وتقنّع البتات الباقية.

  • ضبط البتات (Setting bits): قيمة x|0 هي x و x|1 هي 1 مهما كانت قيمة x، لذلك عند تطبيق OR على متجهة مع العدد 3 فستُضبط البتات الموجودة أقصى اليمين بينما ستُترَك بقية البتات كما هي:
  xxxx
| 0011
  ----
  xx11
  • تبديل أو عكس البتات (Toggling bits): إذا طبّقت XOR مع العدد 3 فستُقلب البتات الموجودة أقصى اليمين وتُترك بقية البتات كما هي. جرّب حساب المتمم الثنائي للعدد 12 باستخدام ^ كتمرينٍ لك، تلميح: ما هو تمثيل المتمم الثنائي للعدد ‎-1؟

توفر لغة البرمجة C عاملات إزاحة أيضًا مثل << و >> التي تزيح البتات يمينًا ويسارًا، حيث تضاعف الإزاحةُ لليسار العددَ، فناتج 5 << 1 هو 10 أما ناتج 5 << 2 هو 20، وتقسم الإزاحة لليمين العدد على 2 (ويكون الناتج مُقرّبًا (rounding down)) حيث ناتج 5 >> 1 هو 2 وناتج 2 >> 1 هو 1.

تمثيل الأعداد العشرية (Representing floating-point numbers)

تُمثَّل الأعداد العشرية باستخدام النسخة الثنائية (binary version) للصيغة العلمية (scientific notation)، وتُكتَب الأعداد الكبيرة في الصيغة العشرية (decimal notation) كحاصل ضرب معامِلٍ (coefficient) مع 10 مرفوعة لأسّ، فسرعة الضوء المقدّرة بالمتر/ ثانية تساوي تقريبًا ‎2.998 . 108 على سبيل المثال.

تستخدم معظم الحواسيب معيار (IEEE standard) لحساب الأعداد العشرية (معيار IEEE للأعداد العشرية)، حيث يقابل النوع float في C المعيار IEEE المكون من 32 بتًا أما النوع double يقابل المعيار 64 بتًا. البت الموجود أقصى اليسار (leftmost bit) هو بت الإشارة (sign bit) ويرمز له s في معيار 32 بت، و تمثل ال8 بتات التالية الأسَّ (exponent) و يرمز له q وآخر 23 بت هي المعامِل (coefficient) ويرمز له c، وبالتالي قيمة العدد العشري هي ‎(-1)sc.2q. هذه القيمة صحيحة تقريبًا ولكن هناك شيء بسيط أيضًا، فالأعداد العشرية موحّدة بحيث يوجد رقمٌ واحد قبل الفاصلة، لذلك يُفضَّل في النظام العشري على سبيل المثال الصيغة ‎2.998 . 108 على الصيغة ‎2998 . 105 أو على أي تعبيرٍ آخر مكافئ. أما بالنظام الثنائي فالأعداد موحّدة بحيث يوجد الرقم 1 قبل الفاصلة الثنائية دومًا، وبما أن الرقم في هذا الموقع هو 1 دومًا لذلك يمكن توفير مساحة وذلك بعدم إدخال هذا الرقم ضمن التمثيل. فتمثيل العدد الصحيح 1310 هو b1101 على سبيل المثال، أما التمثيل العشري هو ‎1.101 . 23 حيث 3 هو الأس (exponent) وجزء المعامِل الذي يمكن أن يُخزَّن هو 101 (متبوعًا ب 20 صفرًا). هذا صحيحٌ تقريبًا ولكن يوجد شيءٌ آخر أيضًا، وهو أن الأس يُخزّن مع معدل انحياز (bias)، وقيمة معدل الانحياز في معيار 32 بت هي 127، وبالتالي يمكن تخزين الأس 3 كَ 130. تُستخدَم عمليات الاتحاد (union operations) والعمليات الثنائية (bitwise operations) لضغط (pack) وفك ضغط (unpack) الأعداد العشرية في C كما في المثال التالي:

union
{
    float f;
    unsigned int u;
} p;

p.f = -13.0;
unsigned int sign = (p.u >> 31) & 1;
unsigned int exp = (p.u >> 23) & 0xff;

unsigned int coef_mask = (1 << 23) - 1;
unsigned int coef = p.u & coef_mask;

printf("%d\n", sign);
printf("%d\n", exp);
printf("0x%x\n", coef);

يسمح النوع union بتخزين القيمة العشرية باستخدام المتغير p.f ثم قراءته كعدد صحيح بلا إشارة (unsigned integer) باستخدام المتغير p.u. وللحصول على بت الإشارة تُزاح البتات يمينًا بمقدار 31 ثم يُستخدم قناع 1 بت (أي تطبيق & على ناتج الإزاحة مع العدد 1) وذلك لاختيار البت الموجود أقصى اليمين فقط. وللحصول على الأس تُزاح البتات بمقدار 23 ثم تُختار ال8 بتات الموجودة أقصى اليمين، حيث تملك القيمة الست عشرية 0xff ثمانية واحدات. وللحصول على المعامِل (coefficient) تحتاج لإزالة 23 بتًا الموجودين أقصى اليمين وتجاهل بقية البتات، ويمكن تحقيق ذلك من خلال إنشاء قناع مكون من واحدات في البتات ال23 الموجودة أقصى اليمين وأصفارًا في البتات الموجودة على اليسار، والطريقة الأسهل لإنشاء هذا القناع هي إزاحة 1 يسارًا بمقدار 23 ثم يُطرَح من ناتج الإزاحة 1. خرج البرنامج السابق هو كما يلي:

1
130
0x500000

بت الإشارة للعدد السالب هو 1 كما هو متوقع، والأس هو 130 متضمنًا معدل الانحياز، والمعامل هو 101 متبوعًا ب20 صفرًا ولكنه طُبع بالنظام الست عشري (0x500000).

أخطاء الاتحادات وأخطاء الذاكرة (Unions and memory errors)

يوجد استخدامان شائعان لاتحادات لغة البرمجة C أحدهما هو الوصول إلى التمثيل الثنائي للبيانات كما ذُكر سابقًا. والاستخدام الآخر هو تخزين البيانات غير المتجانسة (heterogeneous data)، فيمكنك استخدام اتحاد (union) لتمثيل عددٍ والذي من الممكن أن يكون عددًا صحيحًا (integer) أو عشريًا (float) أو مركبًا (complex) أو كسريًا (rational). الاتحادات معرضةٌ للخطأ على كل حال، ويعود الأمر للمبرمج لتتبع نوع البيانات الموجودة في الاتحاد، فإذا كتبت قيمة عشرية ثم فُسِّرت كعدد صحيح فستكون النتيجة لامعنىً لها، وهو أمرٌ مماثل لقراءة موقع من الذاكرة بصورة خاطئة مثل قراءة قيمة موقع من مصفوفة في حين تكون هذه المصفوفة انتهت أي قراءة قيمة من خارج حدود المصفوفة. تخصص الدالة التالية مكانًا للمصفوفة في المكدس وتملؤه بأعداد من 0 إلى 99:

void f1() {
int i;
int array[100];

for (i=0; i<100; i++) {
array[i] = i;
}
}

ثم تعرّف الدالة التالية مصفوفةً أصغر وتدخل عناصرًا قبل بداية المصفوفة وبعد نهايتها عمدًا:

void f2() {
int x = 17;
int array[10];
int y = 123;

printf("%d\n", array[-2]);
printf("%d\n", array[-1]);
printf("%d\n", array[10]);
printf("%d\n", array[11]);
}

تكون نتيجة استدعاء الدالة f1 ثم استدعاء الدالة f2 ما يلي:

17
123
98
99

تعتمد التفاصيل على المصرّف (compiler) الذي يرتّب المتغيرات في المكدّس، ويمكنك من خلال النتائج السابقة استنتاج أن المصرّف يضع المتغيرين x و y قرب بعضهما البعض أسفل المصفوفة (أي في عناوين أسفل عنوان المصفوفة)، وعندما تقرأ قيمةً خارج المصفوفة فكأنك تريد الحصول على قيمٍ متروكة من استدعاء دالة سابقة في المكدس. كل المتغيرات في المثال السابق أعدادٌ صحيحة (integers) لذلك سيكون سهلًا معرفة ما يحدث إلى حدٍ ما، ولكن يمكن أن يكون للقيم التي تقرؤها من خارج حدود المصفوفة أيّ نوع. إذا غيّرت في الدالة f1 بحيث تستخدم مصفوفة أعداد عشرية (array of floats) فالنتيجة هي:

17
123
1120141312
1120272384

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

تمثيل السلاسل (Representing strings)

سلاسل لغة البرمجة C هي سلاسلٌ منتهية بالقيمة الخالية (null-terminated)، لذلك لا تنسَ البايت الإضافي في نهاية السلسلة وذلك عند تخصيص مكان لهذه السلسلة. تُرمّز الحروف والأرقام في سلاسل C بواسطة ترميز ASCII، فترميز ASCII للأرقام من 0 إلى 9 هو من 48 إلى 57 وليس ترميزها من 0 إلى 9، فالرمز الآسكي 0 يمثل الحرف الخالي (NUL) الذي يحدد نهاية السلسلة، أما الرموز الآسكية من 1 إلى 9 فهي محارف خاصة تُستخدم في بعض بروتوكولات الاتصالات، والرمز الآسكي 7 هو جرس (bell) فينتج عن طباعته إصدارُ صوت في بعض الطرفيات. 65 هو الرمز الآسكي للحرف A وللحرف a هو 97 والتي تُكتب ثنائيًا كما يلي :

65 = b0100 0001
97 = b0110 0001

حيث ستلاحظ أنهما مختلفان فقط ببتٍ واحد إذا تمعّنت النظر قليلًا، ويُستخدم هذا النمط أيضًا لبقية الحروف، فيتصرّف البت السادس (إذا ابتدأت العد من اليمين) كبت حالة الحرف (case bit) فهو 0 للحروف الكبيرة و 1 للحروف الصغيرة. جرّب كتابة دالة تحوّل الحرف الصغير إلى حرفٍ كبير وذلك من خلال قلب (flipping) البت السادس فقط، ويمكنك أيضًا صنع نسخة أسرع من الدالة وذلك من خلال قراءة سلسلة مكونة من 32 بتًا أو 64 بتًا وهذا أفضل من قراءة حرفٍ واحد فقط في كل مرة، حيث ينشَأ هذا التحسين بسهولة أكثر إذا كان طول السلسلة من مضاعفات 4 أو 8 بايتات. إذا قرأت قيمةً خارج حدود المصفوفة ستظهر لك محارف غريبة، ولكن إذا كتبت سلسلةً ثم قرأتها كعدد صحيح (int) أو عشري (float) فسيكون تفسير (interpret) النتيجة صعبًا. وإذا شغلّت البرنامج التالي:

char array[] = "allen";
float *p = array;
printf("%f\n", *p);

ستجد أن التمثيل الآسكي لأول 8 حروف من اسمي، يقول الكاتب، التي فُسّرت كعدد عشري مضبوط بالنوع double (double-precision floating point number) هو 69779713878800585457664.

ترجمة -وبتصرّف- للفصل More bits and bytes من كتاب Think OS A Brief Introduction to Operating Systems


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...