سلسلة ++c للمحترفين الدرس 5: العمليات البتية (Bitwise Operators) في ++C


محمد الميداوي

تُنفَّذ العمليات البتية (أو الثنائية) على مستوى البت من البيانات وذلك باستعمال العوامل التالية:

| - عامِل OR البِتِّي (Bitwise OR)

إليك المثال التالي:

int a = 5; // 0101b  (0x05)
int b = 12; // 1100b  (0x0C)
int c = a | b; // 1101b  (0x0D)

std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;

يكون ناتج الشيفرة السابقة هو:

a = 5, b = 12, c = 13

وسبب ذلك أن عامِل OR يعمل على مستوى البِتّْ (bit level)، ويستخدم جدول الصحة البوليني أو المنطقي (Boolean Truth Table) التالي:

true OR true = true
true OR false = true
false OR false = false

عندما يُطبق عامل OR على القيمتين البتّيّتين ‎a ‎ (أي 0101)‎ و ‎b ‎ (أي 1100)‎ ، سنحصل على القيمة البتّيّة 1101:

int a = 0 1 0 1
int b = 1 1 0 0 |
---------
int c = 1 1 0 1

لا يغير عامِل OR القيم الأصلية للمعامَلات إلا في حالة استخدام عامِل الإسناد البِتِّي المركَّب =| (Bitwise Assignment Compound Operator) لإسناد تغيير تلك القيم:

int a = 5; // 0101b  (0x05)
a |= 12; // a = 0101b | 1101b

^ - عامل XOR البِتِّي (Bitwise XOR)

إليك المثال التالي:

int a = 5; // 0101b  (0x05)
int b = 9; // 1001b  (0x09)
int c = a ^ b; // 1100b  (0x0C)

std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;

يكون ناتج الشيفرة السابقة:

a = 5, b = 9, c = 12

وسبب ذلك أن عامِل XOR يعمل على مستوى البِتّْ (bit level)، ويستخدم جدول الصحة البولياني التالي:

true OR true = false
true OR false = true
false OR false = false

لاحظ أنه في عملية XOR، يكون لدينا true XOR true = false، على خلاف العمليتين AND و OR، إذ تحققان: true AND/OR true = true، وذلك هو وصف الطبيعة الحصرية لعملية XOR. عندما يُطبّق عامِل XOR على القيمتين البتّيّتين ‎a ‎ (أي 0101)‎ و ‎b ‎ (أي 1001)‎ ، سنحصل على القيمة البتّيّة 1101:

int a = 0 1 0 1
int b = 1 0 0 1 ^
---------
int c = 1 1 0 0

لا يغير عامِل XOR القيم الأصلية للمعامَلات إلا في حالة استخدام عامِل الإسناد البِتِّي المركَّب =^ (Bitwise Assignment Compound Operator) لإسناد تغيير تلك القيم:

int a = 5; // 0101b  (0x05)
a ^= 9; // a = 0101b ^ 1001b

يمكن استخدام عامِل XOR بعدة طرق، وغالبًا ما يُستخدَم في عمليات البتات المُقنّعة (bit mask operations) لتشفير البيانات وضغطها.

لاحظ أن المثال التالي توضيحي، ولا ينبغي أن يُستخدم في الشيفرة النهائية (هناك طرق أفضل لتحقيق نفس النتيجة، مثل std::swap()‎). تستطيع استخدام عملية XOR لمبادلة متغيرين:

int a = 42;
int b = 64;

// XOR المبادلة عبر
a ^= b;
b ^= a;
a ^= b;

std::cout << "a = " << a << ", b = " << b << "\n";

لإنجاز هذا، تحتاج إلى التحقق من إمكانية استخدامه.

void doXORSwap(int & a, int & b) {
    // ينبغي إضافة عبارة للتحقق من أنك لا تبادل المتغير مع نفسه
    // وإلا ستجعل القيمة صفرية
  
    if ( & a != & b) {
        // XOR مبادلة
        a ^= b;
        b ^= a;
        a ^= b;
    }
}

ورغم أنها تبدو طريقة مناسبة للعزل إلا أنها ليست مفيدة في شيفرة الإنتاج، ذلك أن xor ليس عملية منطقية أساسية، بل مزيج من عمليات أخرى: a^c=~(a&c)&(a|c)‎.

كذلك ، فإن المُصرفات التي صدرت بعد 2015 أتاحت إسناد قيم ثنائية إلى المتغيرات:

int cn=0b0111;

& - عامل AND البتّيّ

إليك المثال التالي:

int a = 6; // 0110b  (0x06)
int b = 10; // 1010b  (0x0A)
int c = a & b; // 0010b  (0x02)

std::cout << "a = " << a << ", b = " << b << ", c = " << c << std::endl;

يكون ناتج الشيفرة السابقة:

a = 6, b = 10, c = 2

وسبب ذلك أن عامل AND يعمل على مستوى البت، ويستخدم جدول الصحة البوليني التالي:

TRUE  AND TRUE  = TRUE
TRUE  AND FALSE = FALSE
FALSE AND FALSE = FALSE

عندما يُطبق عامِل AND على القيمتين البتّيّتين ‎a ‎ (أي 0110)‎ و ‎b ‎ (أي 1010)‎ ، سنحصل على القيمة البتّيّة 0010:

int a = 0 1 1 0
int b = 1 0 1 0 &
---------
int c = 0 0 1 0

لا يغير عامِل AND قيمة المعامَل الأصلي إلا عند استخدام عامِل الإسناد البِتِّي المركب ‎&=‎:

int a = 5; // 0101b  (0x05)
a &= 10; // a = 0101b & 1010b

<< - عملية الإزاحة اليسارية (left shift)

إليك المثال التالي:

int a = 1; // 0001b
int b = a << 1; // 0010b

std::cout << "a = " << a << ", b = " << b << std::endl;

يكون ناتج ذلك هو:

a = 1, b = 2

وتفسير ذلك أن عامِل الإزاحة اليساري يزيح بتَّات القيمة الموجودة على الجانب الأيسر (a) بالعدد المحدد على اليمين (1)، مع ملأ البتات الأقل أهمية (least significant bits) بأصفار، فمثلًا في حال إزاحة قيمة العدد 5 (تمثيله الثنائي 0000 0101) إلى اليسار أربعة منازل (على سبيل المثال ‎5 <<‎4)، سوف نحصل على القيمة 80 (تمثيلها الثنائي 0101 0000). قد تلاحظ أنّ إزاحة قيمة إلى اليسار بمنزلة واحدة يكافئ ضرب تلك القيمة بالعدد 2، على سبيل المثال:

int a = 7;

while (a < 200) {
    std::cout << "a = " << a << std::endl;
    a <<= 1;
}

a = 7;

while (a < 200) {
    std::cout << "a = " << a << std::endl;
    a *= 2;
}

ولكن تجدر الإشارة إلى أنّ عملية الإزاحة اليسارية سوف تزيح جميع البتات إلى اليسار، بما في ذلك بتّ الإشارة، انظر إلى المثال التالي:

int a = 2147483647; // 0111 1111 1111 1111 1111 1111 1111 1111
int b = a << 1; // 1111 1111 1111 1111 1111 1111 1111 1110

std::cout << "a = " << a << ", b = " << b << std::endl;

سيكون الناتج:

a = 2147483647, b = -2

وفي حين أنّ بعض المُصرِّفات ستعيد نتائج قد تبدو متوقعة غير أنه يجب ملاحظة أن إزاحة عددٍ ذا إشارة (signed number) بحيث تتأثر بتة الإشارة (sign bit)، فستكون النتيجة غير محددة (undefined). كذلك في حال كان عدد البتات التي ترغب في إزاحتها عددًا سالبًا، أو كان أكبر من عدد البتات التي يمكن أن يحملها النوع الموجود على اليسار، انظر:

int a = 1;

int b = a << -1; // سلوك غير محدد
char c = a << 20; // سلوك غير محدد

لا تغير الإزاحة البتّيّة اليسارية قيمة المعامَلات إلا في حال استخدام عامِل الإسناد البِتِّي المركب ‎<<=‎:

int a = 5; // 0101b
a <<= 1; // a = a << 1;

>> - الإزاحة اليمينية (right shift)

إليك المثال التالي:

int a = 2;        // 0010b
int b = a >> 1;     // 0001b

std::cout << "a = " << a << ", b = " << b << std::endl;

سيكون الناتج:

a = 2, b = 1

وتفسير ذلك أن عامِل الإزاحة اليميني يزيح بتِّات القيمة الموجودة على الجانب الأيسر (a) بالعدد المحدد على اليمين (1)؛ وتجدر الإشارة إلى أنه رغم أنّ عملية الإزاحة اليمينية قياسية، فإنّ ما يحدث لبتات الإزاحة اليمينية في الأعداد السالبة ذات الإشارة (signed negative number) يختلف بحسب الاستخدام، ومن ثم لا يمكن ضمان كونها قابلة للنقل (portable)، انظر:

int a = -2;
int b = a >> 1; // على المصرّف b تعتمد قيمة

كذلك سيكون ذلك السلوك غير محدد إن كان عدد البتات التي ترغب في إزاحتها سالبًا، على سبيل المثال:

int a = 1;
int b = a >> -1; // سلوك غير محدد

لا يغيّر عامل الإزاحة اليميني قيمة المعامَل الأصلي إلا في حال استخدام عامِل الإسناد البِتِّي المركب ‎>>=‎:

int a = 2; // 0010b
a >>= 1; // a = a >> 1;

هذا الدرس جزء من سلسلة دروس عن C++‎.

ترجمة -وبتصرّف- للفصل Chapter 5: Bit Operators من كتاب C++ Notes for Professionals

اقرأ أيضا





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


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



يجب أن تكون عضوًا لدينا لتتمكّن من التعليق

انشاء حساب جديد

يستغرق التسجيل بضع ثوان فقط


سجّل حسابًا جديدًا

تسجيل الدخول

تملك حسابا مسجّلا بالفعل؟


سجّل دخولك الآن