أنواع البيانات المهيكلة في لغة C: دليل شامل للمطورين
int و char و float، بالإضافة إلى المصفوفات لتجميع بيانات من نفس النوع، فإنها لا تقدم حلاً مباشرًا لتجميع بيانات من أنواع مختلفة. هنا يأتي دور أنواع البيانات المهيكلة (Structures) في لغة C، والتي توفر حلاً فعالاً لتنظيم البيانات المتنوعة ومعالجتها كوحدة واحدة. سيتناول هذا المقال أنواع البيانات المهيكلة بالتفصيل، بدءًا من أساسياتها وصولاً إلى مفاهيم متقدمة مثل تخصيص الذاكرة والمؤشرات والهياكل المرجعية الذاتية.
جدول المحتويات
الأساسيات
1. التعريف والإعلان
الهيكل (Structure) هو مجموعة من متغير واحد أو أكثر، يمكن أن تكون من أنواع مختلفة، مجمعة تحت اسم واحد. يعتبر الهيكل نوع بيانات يحدده المستخدم (user-defined data type). تساعد الهياكل في تنظيم البيانات المعقدة ضمن البرامج الكبيرة، حيث تسمح بمعاملة مجموعة من المتغيرات ذات الصلة المنطقية كوحدة واحدة.
على سبيل المثال، يمكن أن يمتلك الطالب خصائص مثل الاسم والعمر والجنس والدرجات. يمكننا إنشاء مصفوفة أحرف لـ name، ومتغير عدد صحيح لـ roll، ومتغير حرف لـ gender، ومصفوفة أعداد صحيحة لـ marks. ولكن إذا كان هناك 20 أو 100 طالب، فسيكون من الصعب التعامل مع كل هذه المتغيرات بشكل منفصل. يمكننا تعريف هيكل باستخدام الكلمة المفتاحية struct باتباع الصيغة التالية:
/* Syntax */
struct structureName {
dataType memberVariable1;
dataType memberVariable2;
...
};
/* Example */
struct student {
char name[20];
int roll;
char gender;
int marks[5];
};
التعريف أعلاه ينشئ نوع بيانات جديد هو struct student. كل متغير من هذا النوع سيحتوي على name[20] و roll و gender و marks[5]. تُعرف هذه المتغيرات بأعضاء الهيكل (members of the structure). بمجرد تعريف الهيكل كنوع بيانات جديد، يمكن إنشاء متغيرات من هذا النوع:
/* Variable declaration */
struct structureName structureVariable;
/* Example */
struct student st1;
struct student st2, st3, st4;
كل متغير من نوع struct student يمتلك نسخًا خاصة به من الأعضاء. إليك بعض الملاحظات الهامة:
- لا تشغل أعضاء الهيكل ذاكرة إلا بعد إنشاء متغير هيكل.
- قد تلاحظ أننا نستخدم
structعند تعريف المتغير أيضًا. أليس هذا مملاً؟ باستخدام الكلمة المفتاحيةtypedefفي تعريف الهيكل، يمكننا تجنب كتابةstructمرة أخرى:
typedef struct students {
char name[20];
int roll;
char gender;
int marks[5];
} STUDENT;
/* أو */
typedef struct {
char name[20];
int roll;
char gender;
int marks[5];
} STUDENT;
STUDENT st1, st2, st3, st4;
وفقًا للتقاليد، تُستخدم الأحرف الكبيرة لتعريفات الأنواع (مثل STUDENT). يمكن دمج تعريف الهيكل وإعلان المتغيرات كما يلي:
struct student {
char name[20];
int roll;
char gender;
int marks[5];
} st1, st2, st3, st4;
استخدام structureName اختياري. الكود التالي صحيح تمامًا:
struct {
char name[20];
int roll;
char gender;
int marks[5];
} st1, st2, st3, st4;
عادةً ما تُعلن الهياكل في الجزء العلوي من ملف الشيفرة المصدرية، حتى قبل تعريف الدوال (ستعرف السبب لاحقًا). لا تسمح لغة C بتهيئة المتغيرات داخل تعريف الهيكل.
2. تهيئة أعضاء الهيكل والوصول إليها
مثل أي متغير آخر، يمكن تهيئة متغير الهيكل عند إعلانه. توجد علاقة واحد لواحد بين الأعضاء وقيم التهيئة الخاصة بها.
/* تهيئة المتغيرات */
struct structureName = { value1, value2,...};
/* مثال */
typedef struct {
char name[20];
int roll;
char gender;
int marks[5];
} STUDENT;
void main () {
STUDENT st1 = { "Alex", 43, 'M', { 76, 78, 56, 98, 92 }};
STUDENT st2 = { "Max", 33, 'M', { 87, 84, 82, 96, 78 }};
}
للوصول إلى أعضاء الهيكل، يجب استخدام عامل النقطة (.) المعروف بـ dot operator.
/* الوصول إلى أعضاء الهيكل */
structureVariable.memberVariable
/* مثال */
printf("Name: %s\n", st1.name);
printf("Roll: %d\n", st1.roll);
printf("Gender: %c\n", st1.gender);
for (int i = 0; i < 5; i++)
printf("Marks in %dth subject: %d\n", i, st1.marks[i]);
/* الناتج */
Name: Alex
Roll: 43
Gender: M
Marks in 0th subject: 76
Marks in 1th subject: 78
Marks in 2th subject: 56
Marks in 3th subject: 98
Marks in 4th subject: 92
يمكن تهيئة الأعضاء عند إعلان المتغير بأي ترتيب باستخدام عامل النقطة . (designated initializers).
STUDENT st3 = { .gender = 'M', .roll = 23, .name = "Gasly", .marks = { 99, 45, 67, 78, 94 }};
يمكننا أيضًا تهيئة الأعضاء القليلة الأولى وترك الباقي فارغًا. ومع ذلك، يجب أن تكون الأعضاء غير المهيأة في نهاية القائمة فقط. الأعضاء من نوع الأعداد الصحيحة (int) والأعداد العشرية (floating-point numbers) غير المهيأة تأخذ قيمة افتراضية 0. أما بالنسبة للأحرف (char) والسلاسل النصية (strings)، فتكون القيمة الافتراضية \0 (NULL).
STUDENT st4 = { "Kviyat", 65 }; /* نفس { "Kviyat", 65, '\0', { 0, 0, 0, 0, 0} } */
3. التعامل مع متغيرات الهيكل
على عكس المتغيرات من أنواع البيانات البدائية، لا يمكننا إجراء عمليات حسابية مثل + و - و * و / وما إلى ذلك على متغيرات الهيكل مباشرةً. وبالمثل، لا يمكن استخدام عوامل المقارنة (relational operators) وعوامل المساواة (equality operators) مع متغيرات الهيكل. ولكن يمكننا نسخ متغير هيكل إلى آخر، بشرط أن يكونا من نفس نوع الهيكل.
/* عمليات غير صالحة */
st1 + st2
st1 - st2
st1 == st2
st1 != st2
إلخ.
/* عملية صالحة */
st1 = st2
لمقارنة متغيرات الهيكل، يجب علينا مقارنة أعضاء الهيكل بشكل فردي.
#include <stdio.h>
#include <string.h>
struct student {
char name[20];
double roll;
char gender;
int marks[5];
} st1, st2;
void main () {
struct student st1 = { "Alex", 43, 'M', { 76, 78, 56, 98, 92 }};
struct student st2 = { "Max", 33, 'M', { 87, 84, 82, 96, 78 }};
if (strcmp(st1.name, st2.name) == 0 && st1.roll == st2.roll)
printf("Both are the records of the same student.\n");
else
printf("Different records, different students.\n");
/* نسخ متغير الهيكل */
st2 = st1;
if (strcmp(st1.name, st2.name) == 0 && st1.roll == st2.roll)
printf("\nBoth are the records of the same student.\n");
else
printf("\nDifferent records, different students.\n");
}
/* الناتج */
Different records, different students.
Both are the records of the same student.
4. مصفوفة من الهياكل
لقد رأيت بالفعل كيف اضطررنا إلى إنشاء 4 متغيرات مختلفة من نوع struct student لتخزين سجلات 4 طلاب. طريقة أفضل هي إنشاء مصفوفة من نوع struct student (تمامًا مثل مصفوفة من نوع int).
struct student {
char name[20];
double roll;
char gender;
int marks[5];
};
struct student stu[4];
للوصول إلى عناصر المصفوفة stu وأعضاء كل عنصر، يمكننا استخدام الحلقات التكرارية (loops).
/* أخذ القيم من المستخدم */
for (int i = 0; i < 4; i++) {
printf("Enter name:\n");
scanf("%s", &stu[i].name);
printf("Enter roll:\n");
scanf("%d", &stu[i].roll);
printf("Enter gender:\n");
scanf(" %c", &stu[i].gender);
for (int j = 0; j < 5; j++) {
printf("Enter marks of %dth subject:\n", j);
scanf("%d", &stu[i].marks[j]);
}
printf("\n-------------------\n\n");
}
/* إيجاد متوسط الدرجات وطباعته */
for (int i = 0; i < 4; i++) {
float sum = 0;
for (int j = 0; j < 5; j++) {
sum += stu[i].marks[j];
}
printf("Name: %s\nAverage Marks = %.2f\n\n", stu[i].name, sum / 5);
}
5. الهياكل المتداخلة (Nested Structure)
يعني تداخل الهياكل وجود متغير هيكل واحد أو أكثر داخل هيكل آخر. تمامًا كما نعلن عن عضو من نوع int أو char، يمكننا أيضًا إعلان متغير هيكل كعضو.
struct date {
int date;
int month;
int year;
};
struct student {
char name[20];
int roll;
char gender;
int marks[5];
struct date birthday;
};
void main () {
struct student stu1;
}
متغير الهيكل birthday من نوع struct date متداخل داخل struct student. يجب أن يكون واضحًا أنه لا يمكنك تداخل متغير هيكل من نوع struct student داخل struct student نفسه. لاحظ أن الهيكل المراد تداخله يجب أن يُعلن أولاً. باستخدام عامل النقطة .، يمكننا الوصول إلى الأعضاء الموجودة داخل الهيكل الداخلي وكذلك الأعضاء الأخرى.
/* مثال */
stu1.birthday.date
stu1.birthday.month
stu1.birthday.year
stu1.name
يمكن تداخل متغيرات الهياكل من أنواع مختلفة أيضًا.
struct birth {
int date;
int month;
int year;
};
struct relation {
char fathersName[20];
char mothersName[20];
};
struct student {
char name[20];
int roll;
char gender;
int marks[5];
struct birth birthday;
struct relation parents;
};
تخصيص الذاكرة
عند إعلان متغير هيكل من نوع معين، تُخصص لأعضاء الهيكل مواقع ذاكرة متجاورة (contiguous).
struct student {
char name[20];
int roll;
char gender;
int marks[5];
} stu1;
هنا، ستُخصص الذاكرة لـ name[20]، يتبعها roll، ثم gender، وأخيرًا marks[5]. هذا يعني أن حجم stu1 أو struct student سيكون مجموع أحجام أعضائها، أليس كذلك؟ دعنا نتحقق.
void main () {
printf("Sum of the size of members = %I64d bytes\n", sizeof(stu1.name) + sizeof(stu1.roll) + sizeof(stu1.gender) + sizeof(stu1.marks));
printf("Using sizeof() operator = %I64d bytes\n", sizeof(stu1));
}
/* الناتج */
Sum of the size of members = 45 bytes
Using sizeof() operator = 48 bytes
نظرًا لأن عامل sizeof() يُرجع قيمة من نوع long long unsigned int، استخدم %I64d كمحدد تنسيق. قد تحتاج إلى استخدام %llu أو %lld اعتمادًا على المترجم الخاص بك. استخدام %d سيؤدي إلى تحذير - format '%d' expects argument of type 'int', but argument 2 has type 'long long unsigned int'.
باستخدام عامل sizeof()، نحصل على 3 بايتات إضافية أكثر من مجموع أحجام الأعضاء. لماذا؟ أين توجد هذه البايتات الثلاثة في الذاكرة؟ دعنا نجيب على السؤال الثاني أولاً. يمكننا طباعة عناوين الأعضاء لمعرفة عناوين تلك البايتات الثلاثة.
void main () {
printf("Address of member name = %d\n", &stu1.name);
printf("Address of member roll = %d\n", &stu1.roll);
printf("Address of member gender = %d\n", &stu1.gender);
printf("Address of member marks = %d\n", &stu1.marks);
}
/* الناتج */
Address of member name = 4225408
Address of member roll = 4225428
Address of member gender = 4225432
Address of member marks = 4225436

نلاحظ أن مصفوفة marks[5] بدلاً من أن تُخصص من العنوان 4225433، تم تخصيصها من العنوان 4225436. ولكن لماذا؟
1. محاذاة البيانات (Data Alignment)
قبل النظر في محاذاة البيانات، من المهم معرفة كيف يقرأ المعالج البيانات من الذاكرة. يقرأ المعالج كلمة واحدة في دورة واحدة. هذه الكلمة هي 4 بايتات لمعالج 32 بت و 8 بايتات لمعالج 64 بت. كلما انخفض عدد الدورات، كان أداء وحدة المعالجة المركزية أفضل. إحدى الطرق لتحقيق ذلك هي محاذاة البيانات (aligning the data). تعني المحاذاة أن متغيرًا من أي نوع بيانات بدائي بحجم t سيكون له دائمًا (افتراضيًا) عنوان هو مضاعف لـ t. هذا هو جوهر محاذاة البيانات، ويحدث في كل مرة.
| أنواع البيانات | الحجم (بالبايت) | العنوان |
|---|---|---|
char |
1 | مضاعف للعدد 1 |
short |
2 | مضاعف للعدد 2 |
int, float |
4 | مضاعف للعدد 4 |
double, long, * (المؤشرات) |
8 | مضاعف للعدد 8 |
long double |
16 | مضاعف للعدد 16 |
2. حشو الهياكل (Structure Padding)
قد تحتاج إلى إدراج بعض البايتات الإضافية بين أعضاء الهيكل لمحاذاة البيانات. تُعرف هذه البايتات الإضافية باسم padding (الحشو). في مثالنا أعلاه، عملت البايتات الثلاثة كحشو. بدونها، فإن marks[0]، وهو من نوع int (عنوان مضاعف للعدد 4)، كان سيكون عنوانه الأساسي 4225433 (وهو ليس مضاعفًا للعدد 4). يمكنك الآن على الأرجح أن تفهم لماذا لا يمكن مقارنة الهياكل مباشرة.

3. محاذاة أعضاء الهيكل (Structure Member Alignment)
لشرح هذا، سنأخذ مثالًا آخر (ستفهم السبب لاحقًا).
struct example {
int i1;
double d1;
char c1;
} example1;
void main () {
printf("size = %I64d bytes\n", sizeof(example1));
}
ماذا سيكون الناتج؟ دعنا نطبق ما نعرفه. i1 بحجم 4 بايتات. سيتبعه حشو (padding) بحجم 4 بايتات لأن عنوان d1 يجب أن يكون قابلاً للقسمة على 8. سيتبع ذلك 8 و 1 بايت على التوالي لـ d1 و c1. وبالتالي، يجب أن يكون الناتج 4 + 4 + 8 + 1 = 17 بايتًا.
/* الناتج */
size = 24 bytes
ماذا؟ خطأ مرة أخرى! كيف؟ من خلال مصفوفة من struct example، يمكننا فهم الأمر بشكل أفضل. سنقوم أيضًا بطباعة عنوان أعضاء example2[0].
void main () {
struct example example2[2];
printf("Address of example2[0].i1 = %d\n", &example2[0].i1);
printf("Address of example2[0].d1 = %d\n", &example2[0].d1);
printf("Address of example2[0].c1 = %d\n", &example2[0].c1);
}
/* الناتج */
Address of example2[0].i1 = 4225408
Address of example2[0].d1 = 4225416
Address of example2[0].c1 = 4225424

لنفترض أن حجم example2[0] هو 17 بايتًا. هذا يعني أن عنوان example2[1].i1 سيكون 4225425. هذا غير ممكن لأن عنوان int يجب أن يكون مضاعفًا للعدد 4. منطقيًا، يبدو أن العنوان المحتمل لـ example2[1].i1 هو 4225428، وهو مضاعف للعدد 4. هذا خطأ أيضًا. هل تعرف لماذا؟ سيكون عنوان example2[1].d1 الآن (28 + 4 (i1) + 3 (حشو)) 4225436 وهو ليس مضاعفًا للعدد 8.
لتجنب مثل هذا عدم المحاذاة، يقدم المترجم محاذاة لكل هيكل. يتم ذلك عن طريق إضافة بايتات إضافية بعد العضو الأخير، تُعرف باسم محاذاة أعضاء الهيكل (structure member alignment). في المثال الذي نوقش في بداية هذا القسم، لم يكن هذا مطلوبًا (وهذا هو سبب حاجتنا إلى هذا المثال الآخر).
طريقة بسيطة للتذكر هي من خلال هذه القاعدة:
**يجب أن يكون عنوان الهيكل وطول الهيكل مضاعفات لـ t_max.** هنا، t_max هو الحد الأقصى للحجم الذي يشغله عضو في الهيكل.

بالنسبة لـ struct example، فإن 8 بايتات هو الحد الأقصى لحجم d1. لذلك، يوجد حشو (padding) بحجم 7 بايتات في نهاية الهيكل، مما يجعل حجمه 24 بايتًا. باتباع هاتين القاعدتين، يمكنك بسهولة العثور على حجم أي هيكل:
- يخزن أي نوع بيانات قيمته في عنوان هو مضاعف لحجمه.
- يأخذ أي هيكل حجمًا هو مضاعف لأقصى عدد من البايتات التي يشغلها عضو.
على الرغم من أننا قادرون على تقليل دورات وحدة المعالجة المركزية، إلا أن هناك قدرًا كبيرًا من الذاكرة يذهب هباءً. إحدى الطرق لتقليل مقدار الحشو إلى الحد الأدنى الممكن هي عن طريق إعلان متغيرات الأعضاء بترتيب تنازلي لحجمها. إذا اتبعنا هذا في struct example، يقل حجم الهيكل إلى 16 بايتًا. يقل الحشو من 7 إلى 3 بايتات.

struct example {
double d1;
int i1;
char c1;
} example3;
void main () {
printf("size = %I64d bytes\n", sizeof(example3));
}
/* الناتج */
size = 16 bytes
4. تعبئة الهياكل (Structure Packing)
التعبئة (Packing) هي عكس الحشو (padding). تمنع المترجم من إضافة الحشو وتزيل الذاكرة غير المخصصة. في حالة أنظمة التشغيل Windows، نستخدم توجيه #pragma pack، الذي يحدد محاذاة التعبئة لأعضاء الهيكل.
#pragma pack(1)
struct example {
double d1;
int i1;
char c1;
} example4;
void main () {
printf("size = %I64d bytes\n", sizeof(example4));
}
/* الناتج */
size = 13 bytes

هذا يضمن أن الأعضاء محاذية على حدود 1 بايت. بعبارة أخرى، يجب أن يكون عنوان أي نوع بيانات مضاعفًا لـ 1 بايت أو حجمه (أيهما أقل).
المؤشرات
إذا كنت ترغب في مراجعة المؤشرات قبل المتابعة، إليك رابط لمقال يغطي المؤشرات بعمق.
1. مؤشر كعضو في الهيكل
يمكن أن يحتوي الهيكل على مؤشرات كأعضاء أيضًا.
struct student {
char *name;
int *roll;
char gender;
int marks[5];
};
void main () {
int alexRoll = 44;
struct student stu1 = { "Alex", &alexRoll, 'M', { 76, 78, 56, 98, 92 }};
}
باستخدام عامل النقطة (.)، يمكننا مرة أخرى الوصول إلى الأعضاء. بما أن roll يحتوي الآن على عنوان alexRoll، سيتعين علينا فك إشارة stu1.roll للحصول على القيمة (وليس stu1.(*roll)).
printf("Name: %s\n", stu1.name);
printf("Roll: %d\n", *(stu1.roll));
printf("Gender: %c\n", stu1.gender);
for (int i = 0; i < 5; i++)
printf("Marks in %dth subject: %d\n", i, stu1.marks[i]);
/* الناتج */
Name: Alex
Roll: 43
Gender: M
Marks in 0th subject: 76
Marks in 1th subject: 78
Marks in 2th subject: 56
Marks in 3th subject: 98
Marks in 4th subject: 92
2. مؤشر إلى هيكل (Pointer to Structure)
مثل مؤشرات الأعداد الصحيحة، ومؤشرات المصفوفات، ومؤشرات الدوال، لدينا مؤشرات إلى هياكل أو مؤشرات الهياكل أيضًا.
struct student {
char name[20];
int roll;
char gender;
int marks[5];
};
struct student stu1 = { "Alex", 43, 'M', { 76, 98, 68, 87, 93 }};
struct student *ptrStu1 = &stu1;
هنا، قمنا بإعلان مؤشر ptrStu1 من نوع struct student. لقد قمنا بتعيين عنوان stu1 إلى ptrStu1. يخزن ptrStu1 العنوان الأساسي لـ stu1، وهو العنوان الأساسي للعضو الأول من الهيكل. زيادة المؤشر بمقدار 1 ستزيد العنوان بمقدار sizeof(stu1) بايت.
printf("Address of structure = %d\n", ptrStu1);
printf("Address of member `name` = %d\n", &stu1.name);
printf("Increment by 1 results in %d\n", ptrStu1 + 1);
/* الناتج */
Address of structure = 6421968
Address of member 'name' = 6421968
Increment by 1 results in 6422016
يمكننا الوصول إلى أعضاء stu1 باستخدام ptrStu1 بطريقتين: باستخدام عامل فك الإشارة (*) أو باستخدام عامل السهم (->). مع *، سنستمر في استخدام عامل النقطة (.)، بينما مع -> لن نحتاج إلى عامل النقطة.
printf("Name w.o using ptrStu1 : %s\n", stu1.name);
printf("Name using ptrStu1 and * : %s\n", (*ptrStu1).name);
printf("Name using ptrStu1 and -> : %s\n", ptrStu1->name);
/* الناتج */
Name without using ptrStu1: Alex
Name using ptrStu1 and *: Alex
Name using ptrStu1 and ->: Alex
وبالمثل، يمكننا الوصول إلى الأعضاء الأخرى وتعديلها. لاحظ أن الأقواس ضرورية عند استخدام * لأن عامل النقطة (.) له أسبقية أعلى من *.
3. المؤشر ومصفوفة الهياكل
يمكننا إنشاء مصفوفة من نوع struct student واستخدام مؤشر للوصول إلى العناصر وأعضائها.
struct student stu[10];
/* مؤشر إلى العنصر الأول (الهيكل) في المصفوفة */
struct student *ptrStu_type1 = stu;
/* مؤشر إلى مصفوفة من 10 هياكل من نوع struct student */
struct student (*ptrStu_type2)[10] = &stu;
لاحظ أن ptrStu_type1 هو مؤشر إلى stu[0] بينما ptrStu_type2 هو مؤشر إلى المصفوفة بأكملها المكونة من 10 هياكل struct student. إضافة 1 إلى ptrStu_type1 سيشير إلى stu[1]. يمكننا استخدام ptrStu_type1 مع حلقة تكرارية للتنقل عبر العناصر وأعضائها.
for (int i = 0; i < 10; i++)
printf("%s, %d\n", (ptrStu_type1 + i)->name, (ptrStu_type1 + i)->roll);
الدوال
1. دالة كعضو في الهيكل
لا يمكن أن تكون الدوال عضوًا في هيكل بشكل مباشر. ومع ذلك، باستخدام مؤشرات الدوال (function pointers)، يمكننا استدعاء الدوال باستخدام عامل النقطة .. فقط تذكر أن هذا ليس موصى به.
struct example {
int i;
void (*ptrMessage)(int i);
};
void message (int);
void message (int i) {
printf("Hello, I'm a member of a structure. This structure also has an integer with value %d", i);
}
void main () {
struct example eg1 = { 6, message};
eg1.ptrMessage(eg1.i);
}
لقد قمنا بإعلان عضوين، عدد صحيح i ومؤشر دالة ptrMessage داخل struct example. يشير مؤشر الدالة إلى دالة تأخذ عددًا صحيحًا وتُرجع void. message هي إحدى هذه الدوال. قمنا بتهيئة eg1 بالقيمة 6 والدالة message. ثم نستخدم . لاستدعاء الدالة باستخدام ptrMessage ونمرر eg1.i.
2. هيكل كوسيط دالة (Structure as a Function Argument)
مثل المتغيرات، يمكننا تمرير أعضاء الهيكل الفردية كوسائط لدالة.
#include <stdio.h>
struct student {
char name[20];
int roll;
char gender;
int marks[5];
};
void display (char a[], int b, char c, int marks[]) {
printf("Name: %s\n", a);
printf("Roll: %d\n", b);
printf("Gender: %c\n", c);
for (int i = 0; i < 5; i++)
printf("Marks in %dth subject: %d\n", i, marks[i]);
}
void main () {
struct student stu1 = { "Alex", 43, 'M', { 76, 98, 68, 87, 93 }};
display(stu1.name, stu1.roll, stu1.gender, stu1.marks);
}
/* الناتج */
Name: Alex
Roll: 43
Gender: M
Marks in 0th subject: 76
Marks in 1th subject: 98
Marks in 2th subject: 68
Marks in 3th subject: 87
Marks in 4th subject: 93
لاحظ أن الهيكل struct student مُعلن خارج دالة main()، في الجزء العلوي تمامًا. هذا لضمان أنه متاح عالميًا ويمكن لدالة display() استخدامه. إذا تم تعريف الهيكل داخل main()، فسيكون نطاقه محدودًا بـ main().
تمرير أعضاء الهيكل ليس فعالًا عندما يكون هناك عدد كبير منها. في هذه الحالة، يمكن تمرير متغيرات الهيكل إلى دالة.
void display (struct student a) {
printf("Name: %s\n", a.name);
printf("Roll: %d\n", a.roll);
printf("Gender: %c\n", a.gender);
for (int i = 0; i < 5; i++)
printf("Marks in %dth subject: %d\n", i, a.marks[i]);
}
void main () {
struct student stu1 = { "Alex", 43, 'M', { 76, 98, 68, 87, 93 }};
display(stu1);
}
إذا كان حجم الهيكل كبيرًا، فإن تمرير نسخة منه لن يكون فعالًا جدًا. يمكننا تمرير مؤشر هيكل إلى دالة. في هذه الحالة، يتم تمرير عنوان الهيكل كوسيط فعلي.
void display (struct student *p) {
printf("Name: %s\n", p->name);
printf("Roll: %d\n", p->roll);
printf("Gender: %c\n", p->gender);
for (int i = 0; i < 5; i++)
printf("Marks in %dth subject: %d\n", i, p->marks[i]);
}
void main () {
struct student stu1 = { "Alex", 43, 'M', { 76, 98, 68, 87, 93 }};
struct student *ptrStu1 = &stu1;
display(ptrStu1);
}
تمرير مصفوفة من الهياكل إلى دالة يشبه تمرير مصفوفة من أي نوع إلى دالة. يتم تمرير اسم المصفوفة، وهو العنوان الأساسي لمصفوفة الهيكل، إلى الدالة.
void display (struct student *p) {
for (int j = 0; j < 10; j++) {
printf("Name: %s\n", (p+j)->name);
printf("Roll: %d\n", (p+j)->roll);
printf("Gender: %c\n", (p+j)->gender);
for (int i = 0; i < 5; i++)
printf("Marks in %dth subject: %d\n", i, (p+j)->marks[i]);
}
}
void main () {
struct student stu1[10];
display(stu1);
}
3. هيكل كقيمة مرجعة من دالة (Structure as a Function Return)
يمكننا إرجاع متغير هيكل، تمامًا مثل أي متغير آخر.
#include <stdio.h>
struct student {
char name[20];
int roll;
char gender;
int marks[5];
};
struct student increaseBy5 (struct student p) {
for (int i = 0; i < 5; i++)
if (p.marks[i] + 5 <= 100) {
p.marks[i]+= 5;
}
return p;
}
void main () {
struct student stu1 = { "Alex", 43, 'M', { 76, 98, 68, 87, 93 }};
stu1 = increaseBy5(stu1);
printf("Name: %s\n", stu1.name);
printf("Roll: %d\n", stu1.roll);
printf("Gender: %c\n", stu1.gender);
for (int i = 0; i < 5; i++)
printf("Marks in %dth subject: %d\n", i, stu1.marks[i]);
}
/* الناتج */
Name: Alex
Roll: 43
Gender: M
Marks in 0th subject: 81
Marks in 1th subject: 98
Marks in 2th subject: 73
Marks in 3th subject: 92
Marks in 4th subject: 98
تقوم الدالة increaseBy5() بزيادة الدرجات بمقدار 5 للمواد التي، بعد زيادة الدرجات، تكون أقل من أو تساوي 100. لاحظ أن نوع الإرجاع هو متغير هيكل من نوع struct student. عند إرجاع عضو هيكل، يجب أن يكون نوع الإرجاع هو نوع هذا العضو.
يمكن أيضًا إرجاع مؤشر هيكل بواسطة دالة.
#include <stdio.h>
#include <stdlib.h>
struct rectangle {
int length;
int breadth;
};
struct rectangle* function (int length, int breadth) {
struct rectangle *p = (struct rectangle *) malloc(sizeof(struct rectangle));
p->length = length;
p->breadth = breadth;
return p;
}
void main () {
struct rectangle *rectangle1 = function(5, 4);
printf("Length of rectangle = %d units\n", rectangle1->length);
printf("Breadth of rectangle = %d units\n", rectangle1->breadth);
printf("Area of rectangle = %d square units\n", rectangle1->length * rectangle1->breadth);
}
/* الناتج */
Length of rectangle = 5 units
Breadth of rectangle = 4 units
Area of rectangle = 20 square units
لاحظ أننا خصصنا ذاكرة بحجم struct rectangle ديناميكيًا باستخدام malloc(). نظرًا لأنها تُرجع مؤشر void، يجب علينا تحويل نوعها إلى مؤشر struct rectangle.
الهياكل المرجعية الذاتية (Self-Referential Structures)
لقد ناقشنا أن المؤشرات يمكن أن تكون عضوًا في هيكل أيضًا. ماذا لو كان المؤشر هو مؤشر هيكل؟ يمكن أن يكون مؤشر الهيكل من نفس نوع الهيكل أو مختلفًا. الهياكل المرجعية الذاتية هي تلك التي تحتوي على مؤشر (مؤشرات) هيكل من نفس النوع كأعضاء لها.
struct student {
char name[20];
int roll;
char gender;
int marks[5];
struct student *next;
};
هذا هيكل مرجعي ذاتي حيث next هو مؤشر هيكل من نوع struct student. سنقوم الآن بإنشاء متغيري هيكل stu1 و stu2 وتهيئتهما بالقيم. ثم سنخزن عنوان stu2 في العضو next لـ stu1.
void main () {
struct student stu1 = { "Alex", 43, 'M', { 76, 98, 68, 87, 93 }, NULL };
struct student stu2 = { "Max", 33, 'M', { 87, 84, 82, 96, 78 }, NULL };
stu1.next = &stu2;
}

يمكننا الآن الوصول إلى أعضاء stu2 باستخدام stu1 و next.
void main () {
printf("Name: %s\n", stu1.next->name);
printf("Roll: %d\n", stu1.next->roll);
printf("Gender: %c\n", stu1.next->gender);
for (int i = 0; i < 5; i++)
printf("Marks in %dth subject: %d\n", i, stu1.next->marks[i]);
}
/* الناتج */
Name: Max
Roll: 33
Gender: M
Marks in 0th subject: 87
Marks in 1th subject: 84
Marks in 2th subject: 82
Marks in 3th subject: 96
Marks in 4th subject: 78
لنفترض أننا نريد متغير هيكل مختلفًا بعد stu1، أي إدراج متغير هيكل آخر بين stu1 و stu2. يمكن القيام بذلك بسهولة.
void main () {
struct student stuBetween = { "Gasly", 23, 'M', { 83, 64, 88, 79, 91 }, NULL };
stu1.next = &stuBetween;
stuBetween.next = &stu2;
}
الآن stu1.next يخزن عنوان stuBetween. و stuBetween.next يحتوي على عنوان stu2. يمكننا الآن الوصول إلى جميع الهياكل الثلاثة باستخدام stu1.
printf("Roll Of %s: %d\n", stu1.next->name, stu1.next->roll);
printf("Gender Of %s: %c\n", stu1.next->next->name, stu1.next->next->gender);
/* الناتج */
Roll Of Gasly: 23
Gender Of Max: M

لاحظ كيف قمنا بتشكيل رابط بين stu1 و stuBetween و stu2. ما ناقشناه هنا هو نقطة البداية لمفهوم القائمة المرتبطة (Linked List). الهياكل المرجعية الذاتية مفيدة جدًا في إنشاء هياكل البيانات مثل القوائم المرتبطة (linked list)، المكدسات (stacks)، الطوابير (queues)، الرسوم البيانية (graphs) وما إلى ذلك.
الخلاصة التقنية
لقد استعرضنا في هذا المقال الجوانب الأساسية والمتقدمة لأنواع البيانات المهيكلة في لغة C. من تعريفها البسيط كأداة لتجميع البيانات المتنوعة، إلى تعقيدات إدارة الذاكرة مثل محاذاة البيانات والحشو والتعبئة، وصولاً إلى استخدام المؤشرات والهياكل المرجعية الذاتية التي تشكل اللبنة الأساسية لهياكل البيانات المعقدة. إن فهم هذه المفاهيم بعمق لا يعزز فقط قدرة المطور على كتابة كود منظم وفعال، بل يمكنه أيضًا من تحسين أداء التطبيقات من خلال إدارة الذاكرة بشكل أمثل. الهياكل هي حجر الزاوية في بناء أنظمة برمجية قوية ومرنة في لغة C، وإتقانها يفتح الأبواب أمام فهم أعمق للبرمجة منخفضة المستوى وتصميم هياكل البيانات المتقدمة. نأمل أن يكون هذا الدليل قد أضاف قيمة حقيقية لمعرفتك البرمجية.