المقالات العلمية

مقالات الشبكة العربية لمطوري الألعاب

تحويل أنواع المتغيرات في ++C، سلاح ذو حدين

هذه المقالة محمية ضمن الحقوق الفكرية لشركة In|Framez Technology Corp.، ومرخصة للعرض فقط ضمن الشبكة العربية لمطوري الألعاب مع الموافقة الصريحة من المؤلف وشركة In|Framez Technology Corp.. لا يسمح بإعادة نشر هذه المقالة أو تعديلها دون الرجوع للمؤلف. يمنع النسخ والاقتباس دون ذكر المصدر والموافقة من المؤلف.

تتعرف ++C بشكل طبيعي على مجموعة من أنواع البيانات (data types)، مثل int، float، char وَ void. و لكن بعد أن تقوم بتضمين (include) أي من المكتبات الملحقة (أي header files)، فإنك ستواجه عدداً كبيراً من أنواع المتغيرات الجديدة. من أشهر هذه الـ headers، ملف مكتبة Win32 API الرئيسي (windows.h). فيما يلي قائمة مختصرة للأنواع التي تتعرف (typedef) مع windows.h:

نوع المتغير معناه
BOOL متغيّـر bool (نسبة للعالم Bool). يحتمل قيمتين : (صح) TRUE أو (خطأ) FALSE .
BYTE بايت (byte) . يتكون من 8 بتــّات (bits) .
COLORREF قيمة لونية من ثلاث عناصر (32 bits) : أحمر، أخضر و أزرق .
DWORD عدد صحيح بدون إشارة (unsigned) (32 bits) .
HANDLE . Handle to an object
LPSTR مؤشر (pointer) إلى مصفوفة حروف (string) تتكون من محارف (characters) بثماني بتات (ANSI) .
WINAPI نظام نداء (calling convention) لإجراءات النظام .
WORD عدد صحيح بدون إشارة (unsigned) (16 bits) .

عبر code البرنامج، تقوم بنداء إجراءات تـُعيد قيم من نوع معين. مثلاً، الإجراء LoadImage()، يعيد قيمة من نوع HANDLE. و لكن الإجراء المقابل (الذي يقوم بتحرير الذاكرة المحجوزة للصورة) DeleteObject() يأخذ متغير من نوع HGDIOBJ . كيف تتصرف؟

هنا يأتي دور الـ typecasting، أو تحويل نوع المتغير.

عملية التحويل هذه هي عملية مؤقتة، و تستخدم في الحالات التي تملك فيها بيانات ضمن نوع من المتغيرات، و أنت تريد تطبيق عمليات على هذه البيانات. العمليات تستلزم أن تكون هذه البيانات منظمة و معبر عنها بطريقة غير تلك التي يؤمنها النوع الحالي للمتغير.

تتبع عملية التحويل الطريقة الهجائية التالية:

cast-expression :
unary expression
( type-name ) cast-expression 
type-name :
specifier-qualifier-list abstract-declarator opt

فمثلاً:

char cChar = 'a';
BYTE uChar;
 
// Cast char to BYTE
uChar = (BYTE)cChar;

في المثال السابق حولنا قيمة من نوع char إلى BYTE. إذن يمكننا الآن:

HANDLE handle;
handle = LoadImage(NULL,"mypic.bmp",IMAGE_BITMAP,0,0,LR_LOADFROMFILE);
 
// .. Do something with the image
 
// Done, we must release the image's memory
DeleteObject((HGIOBJ)handle);    // Cast to HGDIOBJ then pass to function

إلى الآن و كل شيء بخير، و لكن أنظر المثال الآتي:

float fPI = 3.14159265f;
int iPI;
iPi = (int)fPI;    // Cast float to int

كيف سيتم التحويل الآن؟ إن int لا يستطيع الحفاظ على معلومات الفاصلة العائمة (floating point). إن هذا التحويل هو تحويل هدام أو غير سالم لأن فيه فقد للمعلومات. إن iPI بعد عملية التحويل سيحتوي على 3 فقط! أي أن الكسر قد تم تجاهله. المثال يتحدث:

// Example of a non-safe (destructive) typecast
 
float fPI = 3.14159265f;
int iPI;
iPi = (int)fPI;    // Cast float to int
 
// iPI now equals 3
 
fPI = (float)iPI;    // Cast int to float
 
// fPI new equals 3.00000000

أما هنا:

// Example of a safe typecast
 
float fPI = 3.14159265f;
double dPI;
dPi = (double)fPI;    // Cast float to double
 
// dPi now equals 3.1415926500000000
 
fPI = (float)dPI;    // Cast double to float
 
// fPI new equals 3.14159265 again

يمكن حصر التحويل الهدام بحالتين رئيسيتين : الأولى هي تحويل عدد كسري إلى صحيح (يضيع الكسر)، الثانية هي تحويل البيانات من متغير ذو حجم معين إلى متغير ذو حجم أصغر، مثل:

int iNumber = 1000;
BYTE bNumber;
 
bNumber = (BYTE)iNumber;    // BYTE can hold 255 max

أما في حالة المؤشرات، فإن الموضوع يأخذ أبعاداً جديدة، فالمصفوفة من نوع char* يمكن تحويلها إلى مصفوفة من نوع BYTE*. و الوصول من ثم إلى عناصرها كـ BYTE:

char *szText = "I am dumb";
BYTE *pBytes;
 
pBytes = (BYTE*)szText;    // Type cast the pointer to BYTE*
 
// Access the elements as bytes (The index of the characters
// in the ASCII table)
for (int i=0; i < 9; i++) 9 printf("Character %c in ASCII table is %d\n",szText[i],pBytes[i]);

ال��ديد في الموضوع أن التحويل بين المؤشرات لا يـُـفقِد أي معلومات. إنه تحويل سالم تماماً. فلنعقد المسألة أكثر : إن مصفوفة من نوع BYTE (أو أي نوع آخر) يتم الوصول إلى عناصرها بالمعادلة الآتية:

Address of Element at index (i) = i * (size of base type) + (array's base address)

دقق على المعامل (size of base type) ضمن المعادلة. إن متغير BYTE حجمه بايت واحد، لذا إذا أردت أن أصل للعنصر الخامس من المصفوفة، أكتب:

Address of Element at index (4) = 4 * (1) + (array's base address)

الآن ماذا لو حولت المصفوفة من نوع BYTE* إلى DWORD*؟ من الجدول الذي ذكرته في بداية المقال، نلاحظ أن DWORD هو متغير بحجم أربعة بايتات! إذن أنظر إلى المعادلة الخاصة بالوصول إلى عناصر مصفوفة DWORD:

Address of Element at index (i) = i * (4) + (array's base address)
Address of Element at index (4) = 4 * (4) + (array's base address)

العنصر الخامس من مصفوفة DWORD يبتعد 16 بايت عن أساس المصفوفة. خذ وقتك لفهم هذه النقطة لأن المثال الآتي قد يصيبك بالصداع إن لم تستوعب الوضع جيداً:

// A simple password generator, from a 4-character string,
// we build a DWORD representation then show it to the user
 
char *szName = "PASS";    // 4-characters password
DWORD *pData;
 
pData = (DWORD*)szName;    // Cast (char*) to (DWORD*)
 
// Get the first element of the DWORD array, since
// a DWORD is 4 bytes long, then we will be reading
// 4 bytes in one shot. These 4 bytes are actually
// the 4 characters of the password
DWORD dwPassword = pData[0];
 
// Show the user the password generated from his entry
printf("User Name: %s\nYour Password is: %u",szName,dwPassword);

الجدول الآتي قد يساعد في توضيح اللعبة من وراء المثال السابق:

4 3 2 1 Memory Block (Bytes)
S S A P char[4]
0x53 0x53 0x41 0x50 char[4] (Hex)
0x53 53 41 50 DWORD[1] (Hex)
1397965136 DWORD[1]

يمكن استخدام التحويل للتغلب على تنبيهات المترجم (compiler warnings) التي قد تنتج عن القيام بعمليات بين أنواع غير متوافقة من المتغيرات. مثلاً:

// Comparison between incompatible types
 
int iNumber1 = 100;
unsigned int uNumber2 = 200;
 
// This will generate compiler warning C4018: '<' : signed/unsigned mismatch
if ( iNumber1 < uNumber2 )
{
    // Do something
}
 
// This will suppress the warning
if ( iNumber1 < (int)uNumber2 )
{
    // Do something
}

الفكرة هنا أنك تضمن أن المتغير uNumber2 سيحافظ على القيمة المحفوظة به بعد التحويل (تحويل سالم). لأنه (كما تعلم) في حالة المتغير بدون إشارة (unsigned)، يتم استخدام جميع البتات لتمثيل الرقم. أما في حالة المتغير ذو الإشارة (signed) فإن البت الأعلى (Highest order bit) يتم استخدامه لحفظ الإشارة، و بذلك تقل قيمة أكبر عدد يستطيع هذا المتغير تمثيله. فإذا حولنا من unsigned إلى signed و كان المتغير يحمل قيمة كبيرة (البت الأخير مضاء) ، فإن العدد بعد التحويل سيصبح سالباً! هذا هو السبب الذي يجعل المترجم يعطي تنبيهاً عندما يواجه مثل هذه الحالة. إن المترجم بإمكانه القيام بعملية التحويل داخلياً (implicit) و لكن في هذه العملية خطورة، لذا هو يسألك أن تتأكد من كونك تريد فعلاً أن تقوم بعملية التحويل، فتجيبه أنت بنعم عن طريق كتابة التحويل بشكل صريح (explicit) كما في القسم الثاني من المثال السابق.

سأنهي الموضوع بذكر مثال أخير على التحويلات. الإجراء التالي يستخدم التحويلات الهدامة لإجراء عملية تدوير للأرقام الكسرية بسرعة و بساطة:

inline int RoundFloat(float fNum)
{
    if (fNum >= ((int)fNum + 0.5f))
        return ((int)fNum) + 1;
    return (int)fNum;
}

مختصر جداً، أليس كذلك ؟

في السطر الأول نقوم بعملية مقارنة بين المتغير الأصلي، و المتغير الأصلي بدون كسر. إذا كان الفرق بينهما أكبر من أو يساوي 0.5 فإن النتيجة النهائية هي تدوير الرقم إلى الأعلى (القيمة بدون كسر + 1) . و إلا فإن النتيجة النهائية هي العدد نفسه بدون كسر.

هنا سأختم هذا المقال العجيب. إن الموضوع ما زال يحتوي على المزيد من الحيل و الأمور التي لم أذكرها. مثل التعليمات reinterpret_cast وَ static_cast وَ dynamic_cast. و لكنني سأكتفي بهذا إلى الآن.
إلى اللقاء.

لمزيد من المعلومات

  • C/C++ Languages Documentation: type casts conversion
  • MSDN Library, Platform SDK: Windows API

المصطلحات المستخدمة

المصطلح معناه
typecast عملية تحويل (مقصودة explicit أو غير مقصودة implicit) لنوع المتغير. قد تؤثر على البيانات المخزنة في المتغير.
floating point الفاصلة العائمة، أي الفاصلة الموجودة في العدد الكسري.

أضف تعليقاً

Loading