تخطَّ إلى المحتوى

الهياكل في C: struct و typedef و enum

الدرس 9 من 10 · C

حتى الآن كل متغير عندك يخزن معلومة واحدة: رقم أو حرف. لكن «الطالب» له اسم وعمر ودرجة معاً. هل ستنشئ ثلاث مصفوفات منفصلة وتحاول إبقاءها متطابقة؟ الحل الأنظف هو الهيكل (struct): نوع جديد تصممه أنت، يجمع عدة معلومات مترابطة تحت اسم واحد. وهو أقرب شيء في C إلى الكلاسات التي ستراها في اللغات الأحدث.

struct Student {
char name[30];
int age;
float grade;
};

هذا التعريف قالب فقط ولا يحجز ذاكرة. يُكتب عادة في أعلى الملف قبل الدوال. والمعلومات بداخله تسمى الحقول (Fields)، ولا تنسَ الفاصلة المنقوطة بعد القوس الأخير.

الهيكل مثل استمارة تسجيل فارغة: فيها خانات محددة، وكل طالب يملأ نسخته الخاصة.

struct Student s1 = {"Sara", 14, 91.5f};
printf("%s %d %.1f\n", s1.name, s1.age, s1.grade);
s1.age = 15;
strcpy(s1.name, "Sara Ali");
printf("%s %d\n", s1.name, s1.age);
الناتج
Sara 14 91.5
Sara Ali 15
  • struct Student s1: متغير من النوع الجديد.
  • {"Sara", 14, 91.5f}: القيم الأولية بترتيب الحقول.
  • النقطة s1.age: الوصول إلى حقل للقراءة أو التعديل.
  • الحقل name مصفوفة حروف، فتعديله بـ strcpy وليس بعلامة =.

يمكنك أيضاً تسمية الحقول عند التهيئة، وهذا أوضح ولا يعتمد على الترتيب:

struct Student s2 = {.name = "Omar", .grade = 78.0f};
printf("%s %d %.1f\n", s2.name, s2.age, s2.grade);
الناتج
Omar 0 78.0

الحقل الذي لم تذكره (age) أخذ القيمة صفراً تلقائياً.

كتابة struct Student في كل مرة متعبة. typedef تعطي النوع اسماً مختصراً:

typedef struct {
char title[40];
int pages;
float price;
} Book;
Book b = {"C Basics", 250, 120.0f};
printf("%s: %d صفحة\n", b.title, b.pages);
الناتج
C Basics: 250 صفحة

الآن Book اسم نوع كامل مثل int، وتكتبه بدون كلمة struct.

بعكس المصفوفات، الهياكل تُنسخ بعلامة = مباشرة، وتُنسخ معها كل الحقول:

Book original = {"C Basics", 250, 120.0f};
Book copy = original;
copy.pages = 300;
printf("%d %d\n", original.pages, copy.pages);
الناتج
250 300

النسخة مستقلة تماماً عن الأصل. لكن المقارنة بـ == بين هيكلين غير مسموحة في C: قارن الحقول واحداً واحداً.

هنا تظهر القوة الحقيقية: قائمة كاملة من السجلات في متغير واحد.

Book library[3] = {
{"C Basics", 250, 120.0f},
{"Pointers", 180, 95.5f},
{"Algorithms", 420, 210.0f},
};
float total = 0;
int longest = 0;
for (int i = 0; i < 3; i++) {
total += library[i].price;
if (library[i].pages > library[longest].pages) {
longest = i;
}
}
printf("المجموع: %.2f\n", total);
printf("الأطول: %s\n", library[longest].title);
الناتج
المجموع: 425.50
الأطول: Algorithms

library[i].price: العنصر رقم i ثم الحقل price. مثل جدول بيانات: كل صف كتاب، وكل عمود حقل.

الهياكل والدوال: المؤشر والسهم

Section titled “الهياكل والدوال: المؤشر والسهم”

عند تمرير هيكل لدالة تُنسخ كل حقوله، فأي تعديل داخل الدالة يضيع. وإذا كان الهيكل كبيراً فالنسخ يضيّع وقتاً وذاكرة. الحل الذي تعلمته: مرّر عنوانه.

void applyDiscount(Book *book, float percent) {
book->price = book->price * (1 - percent / 100);
}
void printBook(const Book *book) {
printf("%s - %d صفحة - %.2f\n", book->title, book->pages, book->price);
}
Book b = {"C Basics", 250, 120.0f};
printBook(&b);
applyDiscount(&b, 25);
printBook(&b);
الناتج
C Basics - 250 صفحة - 120.00
C Basics - 250 صفحة - 90.00
  • Book *book: الدالة تستقبل مؤشراً إلى هيكل.
  • السهم book->price: الوصول إلى حقل عبر مؤشر. هو اختصار للكتابة الثقيلة (*book).price.
  • const Book *book: وعد بأن الدالة ستقرأ فقط ولن تعدّل. استخدمها مع كل دالة لا تغيّر الهيكل.

حقل الهيكل يمكن أن يكون هيكلاً آخر:

typedef struct {
int day;
int month;
int year;
} Date;
typedef struct {
char name[30];
Date birthday;
} Person;
Person p = {"Laila", {12, 5, 2011}};
printf("%s: %d/%d/%d\n", p.name, p.birthday.day, p.birthday.month, p.birthday.year);
الناتج
Laila: 12/5/2011

التعداد (enum) نوع قيمه قائمة محددة من الأسماء. بدل أن تتذكر أن 0 تعني «جديد» و 1 تعني «قيد الشحن»، تكتب الأسماء مباشرة:

typedef enum {
NEW,
SHIPPED,
DELIVERED
} Status;
typedef struct {
int id;
Status status;
} Order;
Order order = {1024, NEW};
order.status = SHIPPED;
switch (order.status) {
case NEW:
printf("الطلب %d جديد\n", order.id);
break;
case SHIPPED:
printf("الطلب %d في الطريق\n", order.id);
break;
case DELIVERED:
printf("الطلب %d وصل\n", order.id);
break;
}
printf("%d\n", DELIVERED);
الناتج
الطلب 1024 في الطريق
2

الأسماء في الحقيقة أعداد صحيحة تبدأ من صفر: NEW تساوي 0 و SHIPPED تساوي 1 و DELIVERED تساوي 2. لكن الكود صار يُقرأ كجملة مفهومة.

  1. عرّف typedef struct باسم Product فيه الاسم والسعر والكمية.
  2. أنشئ مصفوفة من أربعة منتجات، واطبع القيمة الإجمالية للمخزون (السعر × الكمية لكل منتج).
  3. اكتب دالة void restock(Product *p, int amount) تزيد الكمية، وجرّبها.
  4. تحدٍّ: اكتب دالة const Product *cheapest(const Product items[], int size) تُرجع مؤشراً إلى أرخص منتج في المصفوفة.
  • نسيان الفاصلة المنقوطة: بعد القوس الأخير في تعريف الهيكل };. نسيانها يعطي أخطاء غريبة في السطور التالية.
  • النقطة مع المؤشر: book.price و book مؤشر خطأ ترجمة. استخدم السهم book->price.
  • إسناد نص لحقل مصفوفة: s1.name = "Ali"; خطأ. استخدم strcpy أو snprintf.
  • تمرير الهيكل بالقيمة وانتظار التعديل: الدالة تعدّل نسختها فقط. مرّر العنوان بـ &.

سؤال

إذا كان p مؤشراً إلى هيكل فيه الحقل age، كيف نصل إلى الحقل؟

سؤال

ماذا يحدث عند تنفيذ b2 = b1; بين هيكلين من النوع نفسه؟

  • struct يجمع حقولاً مترابطة في نوع واحد، والوصول إليها بالنقطة.
  • typedef يعطي الهيكل اسماً مختصراً يُستخدم بدون كلمة struct.
  • الهياكل تُنسخ بـ =، وتوضع في مصفوفات لتمثيل قوائم السجلات.
  • مرّر الهيكل للدوال بعنوانه، واستخدم السهم -> و const عند القراءة فقط.
  • enum يعطي الأعداد أسماء واضحة تجعل الكود مقروءاً.