مقدمة لتركيبات الدمج Unions

This article is available in English too, check it out here.

مقدمة

نتكلم عن واحدة من التركيبات الهامة جدا في برمجة الويندوز وهي تركيبات الدمج Unions. سنبدأ بتعريفها، ثم سوف نقوم بالتعرف عليها وشرحها من خلال الأمثلة والتجارب على الكود.

التعريف

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

تركيبات الدمج Unions هي عبارة عن تركيبات مشابهة كثيرا للتركيبات العادية Structures مع الفارق أن في التركيبات العادية Structures كل متغير يشير إلى جزء معين من الذاكرة وله القيمة الخاصة به، بينما في تركيبات الدمج Unions فإنه هناك أكثر من متغير يشير إلى نفس الجزء من الذاكرة ونفس القيمة. فالفكرة هي أننا نقوم بالوصول إلى نفس الجزء باستخدام أكثر من متغير وباختلاف أنواع هذه المتغيرات.

الشرح

كي نستطيع فهم تركيبات الدمج Unions سوف نقوم بأخذ مثال بسيط على هذه التركيبات، سوف نقوم بتعريف أحدها:

typedef union CHARACTER
{
	int i;
	char c;
};

في التعريف السابق قمنا بإنشاء تركيب دمج Union وقمنا بتسميته CHARACTER وقمنا بإضافة متغيرين إلى هذا التركيب أحدها رقمي والآخر حرفي. لاحظ أنك تقوم بتعريف تركيبات الدمج بنفس الطريقة التي تقوم بتعريف التركيبات العادية بها مع اختلاف بسيط، فقط تقوم بتحديد union بدلا من struct في سطر التعريف.

الآن، نقوم بتجربة إنشاء تركيب دمج والتعامل معه. لاحظ الكود التالي:

int main()
{
	union CHARACTER ch;

	ch.i = 65;				// 65 for A
	printf("c = %c", ch.c);	// prints 'A'
	printf("n");

	ch.c += 32;				// 97 for a
	printf("i = %d", ch.i);	// prints '97'
	printf("n");

	return 0;
}

لاحظ في الكود السابق أنك عندما تقوم بتحديد أي قيمة في المتغير i فإنها تنطبق  تلقائيا على المتغير c، والعكس بالعكس.

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

عند تعريفك لتركيب دمج جديد Union فإنك تقوم بتعريف بداخله جميع المتغيرات التي تريدها أن تتشارك بنفس الجزء من الذاكرة. بمعنى آخر، فإنك تقوم بتعريف المتغيرات التي تريد أن تنطبق عليها جميعها أي قيمة تقوم بتحديدها في أحد هذه المتغيرات.

تتحدد المساحة الكلية لهذا التركيب، المساحة التي سوف يستغلها من الذاكرة، تتحدد بحسب مساحة أكبر متغير فيه. بمعنى أنه في تركيبنا السابق فإن مساحة المتغير الرقمي من نوع int هي 4 بايت (في Win32) بينما مساحة المتغير الآخر وهو متغير حرفي من نوع char هي فقط 1 بايت. لهذا فإن المساحة الكلية لتركيبنا هذا هي 4 بايت يستغلها جميعها المتغير الأول بينما يستغل أول بايت منها فقط المتغير الآخر (لاحظ شكل 1.) يسمى المتغير الأكبر في تركيبات الدمج المتغير الأب Parent أو الحاوية Container.

شكل 1 - مشاركة الذاكرة للتركيب CHARACTER

معرفة المساحة التي يستغلها كل متغير من المساحة الكلية للتركيب مهمة للغاية. لاحظ مثلا المثال التالي والذي يقوم بتسجيل قيمة أعلى من 1 في المتغير الرقمي. لاحظ كيف سيتعامل معها المتغير الرقمي وأيضا المتغير الحرفي الصغير:

int main()
{
	union CHARACTER ch;

	ch.i = 0xAA61; 		// 43617

	printf("c = %c, %x", ch.c, ch.c); // prints 'a, 0x61'!!!!
	printf("n");				// 0x61 = 97

	printf("i = %x", ch.i);	// prints '0xAA61'
	printf("n");			

	return 0;
}

قمنا بتسجيل القيمة 0xAA61 (أو 43617) باستخدام المتغير الرقمي، وبما أن هذا المتغير حجمه 4 بايت فإن هذه البيانات تخزن وتظهر بشكل كامل. عند محاولة طباعة محتويات المتغير الحرفي والذي حجمه فقط 1 بايت فإن هذا المتغير يقوم بإرجاع أول بايت فقط من الذاكرة الكلية للتركيب والذي يحوي أول بايت من القيمة المخزنة وهو 0x61 (أي 97) بما يساوي ترميز حرف الـ ’a‘.

وهذا مثال آخر يوضح نفس الفكرة. نقوم بإنشاء تركيب يقوم بدمج 3 متغيرات في نفس الجزء من الذاكرة:

int main()
{
	union {
		int i;
		short n;
		char c;
	} ch;

	ch.i = 0xAABBCC;

	printf("i = %x", ch.i); // 0xAABBCC
	printf("n");

	printf("n = %x", ch.n); // 0xBBCC
	printf("n");

	printf("c = %x",
		(unsigned char)ch.c); // 0xCC
	printf("n");

	return 0;
}

قمنا بتسجيل قيمة تساوي 3 بايت وهي 0xAABBCC. قام المتغير int بعرضها بطريقة صحيحة (حيث أنه يستغل الـ 4 بايت كاملة مساحة التركيب.) قام المتغير short بعرض أول 2 بايت منها فقط (حيث أنه يشارك int في أول 2 بايت من المساحة.) أخيرا قام المتغير char بعرض فقط أول بايت منها (حيث أنه يشارك int و short في أول بايت من المساحة.)

فائدة تركيبات الدمج

أفترض الآن أنه لديك سؤال: ماذا أستفيد من كل هذا؟ ما الفائدة التي ترجع إلى المبرمج من استخدام تركيبات الدمج؟

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

نفترض أيضا أنك تريد أن تقوم بكتابة قيمة int في ملف، فإذا كنت تريد استخدام الدوال الأساسية في C فإنك لن تجد دالة تقوم بذلك، وإذا أردت استخدام الدالة fwrite() فإنه سيكون من التعقيد الكافي للكود الخاص بك. فالحل هو استخدام تركيب دمج Union يمكنك من خلاله الوصول إلى القيمة الخاصة بك بصورة char مثلا وبالتالي تحافظ على أداء الكود وبساطته. انظر التركيب التالي:

union myval{
	int i;
	char str[4];
};

أخيرا، فإنه يمكنك استخدام تركيبات الدمج لتسهيل الوصول إلى مجموعة من الخيارات Flags وهذا ما ستلاحظ أثناء تعاملك مع واجهة برمجة الويندوز Windows API حيث أنه هناك العديد والعديد من تركيبات الدمج والتي تستخدم لتسهيل عملية الوصول إلى الخيارات Flags وتبسيطها، وليس لدينا مثال أقوى ولا أفضل من التركيب DEVMODE.

خاتمة

أخيرا، نرجوا أن نكون قد وفقنا في عرض الموضوع. وننتظر أسئلتكم ومناقشتكم معنا في هذا الموضوع.

اترك تعليقا