C: تطبيق: الأنواع المخصصة
لقد تعلمت ما يكفي — حان وقت البناء. كتعلم الطبخ: قراءة الوصفات لا تكفي؛ يجب أن تدخل المطبخ. ادمج البنى والذاكرة الديناميكية وإدخال/إخراج الملفات لإنشاء برامج تعمل.
1. المشروع 1: نظام إدارة درجات الطلاب
(1) المتطلبات
- إضافة طلاب (الاسم، الرقم الجامعي، ثلاث درجات مواد)
- عرض معلومات جميع الطلاب
- البحث بالرقم الجامعي
- حساب المجموع والمعدل لكل طالب
- الترتيب حسب المجموع الكلي
- حفظ في ملف / تحميل من ملف
- إدارة ذاكرة ديناميكية — عدد الطلاب غير ثابت
(2) تصميم هيكل البيانات
typedef struct {
char name[32];
char id[16];
int chinese;
int math;
int english;
} Student;
typedef struct {
Student *data;
int count;
int capacity;
} StudentList;
يُدير StudentList الطلاب بمصفوفة ديناميكية — السعة الأولية 4، تتوسع عند الامتلاء.
(3) التنفيذ الأساسي
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[32];
char id[16];
int chinese;
int math;
int english;
} Student;
typedef struct {
Student *data;
int count;
int capacity;
} StudentList;
void list_init(StudentList *list) {
list->capacity = 4;
list->count = 0;
list->data = (Student *)malloc(sizeof(Student) * list->capacity);
}
void list_free(StudentList *list) {
free(list->data);
list->data = NULL;
list->count = 0;
list->capacity = 0;
}
void list_expand(StudentList *list) {
if (list->count < list->capacity) return;
list->capacity *= 2;
Student *tmp = (Student *)realloc(list->data, sizeof(Student) * list->capacity);
if (tmp) list->data = tmp;
}
void list_add(StudentList *list, const Student *s) {
list_expand(list);
list->data[list->count++] = *s;
}
int total_score(const Student *s) {
return s->chinese + s->math + s->english;
}
double avg_score(const Student *s) {
return total_score(s) / 3.0;
}
Student *find_by_id(StudentList *list, const char *id) {
for (int i = 0; i < list->count; i++) {
if (strcmp(list->data[i].id, id) == 0) {
return &list->data[i];
}
}
return NULL;
}
int cmp_by_total(const void *a, const void *b) {
int ta = total_score((const Student *)a);
int tb = total_score((const Student *)b);
return tb - ta;
}
void sort_by_total(StudentList *list) {
qsort(list->data, list->count, sizeof(Student), cmp_by_total);
}
▶ مثال
int main(void) {
StudentList list;
list_init(&list);
Student s1 = {"001", "Zhang", 85, 92, 78};
Student s2 = {"002", "Li", 90, 88, 95};
Student s3 = {"003", "Wang", 72, 65, 80};
list_add(&list, &s1);
list_add(&list, &s2);
list_add(&list, &s3);
sort_by_total(&list);
Student *found = find_by_id(&list, "001");
if (found) {
printf("Found 001: %s Total=%d\n", found->name, total_score(found));
}
list_free(&list);
return 0;
}
Found 001: Zhang Total=255
2. المشروع 2: إدارة جهات الاتصال
(1) المتطلبات
- إضافة جهات اتصال (الاسم، الهاتف، البريد الإلكتروني، المجموعة)
- البحث بالاسم
- التصفية حسب المجموعة
- حذف جهة اتصال
- تعديل معلومات جهة الاتصال
- حفظ في ملف / تحميل من ملف
(2) تصميم هيكل البيانات
typedef struct {
char name[32];
char phone[16];
char email[40];
char group[16];
} Contact;
typedef struct {
Contact *data;
int count;
int capacity;
} ContactList;
(3) التنفيذ الأساسي
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[32];
char phone[16];
char email[40];
char group[16];
} Contact;
typedef struct {
Contact *data;
int count;
int capacity;
} ContactList;
void clist_init(ContactList *list) {
list->capacity = 4;
list->count = 0;
list->data = (Contact *)malloc(sizeof(Contact) * list->capacity);
}
void clist_free(ContactList *list) {
free(list->data);
list->data = NULL;
list->count = 0;
list->capacity = 0;
}
void clist_expand(ContactList *list) {
if (list->count < list->capacity) return;
list->capacity *= 2;
Contact *tmp = (Contact *)realloc(list->data, sizeof(Contact) * list->capacity);
if (tmp) list->data = tmp;
}
void clist_add(ContactList *list, const Contact *c) {
clist_expand(list);
list->data[list->count++] = *c;
}
int clist_remove(ContactList *list, const char *name) {
for (int i = 0; i < list->count; i++) {
if (strcmp(list->data[i].name, name) == 0) {
list->data[i] = list->data[list->count - 1];
list->count--;
return 0;
}
}
return -1;
}
Contact *clist_find(ContactList *list, const char *name) {
for (int i = 0; i < list->count; i++) {
if (strcmp(list->data[i].name, name) == 0) {
return &list->data[i];
}
}
return NULL;
}
▶ مثال
int main(void) {
ContactList list;
clist_init(&list);
Contact c1 = {"Zhang", "13800001111", "zhangsan@mail.com", "Coworker"};
Contact c2 = {"Li", "13900002222", "lisi@mail.com", "Friend"};
Contact c3 = {"Wang", "15000003333", "wangwu@mail.com", "Coworker"};
Contact c4 = {"Zhao", "18600004444", "zhaoliu@mail.com", "Family"};
clist_add(&list, &c1);
clist_add(&list, &c2);
clist_add(&list, &c3);
clist_add(&list, &c4);
Contact *found = clist_find(&list, "Li");
if (found) {
printf("Found Li: %s %s\n", found->phone, found->email);
}
clist_remove(&list, "Wang");
clist_free(&list);
return 0;
}
Found Li: 13900002222 lisi@mail.com
3. مراجعة الأنماط الرئيسية
(1) نمط المصفوفة الديناميكية
كلا المشروعين يستخدمان نفس نمط المصفوفة الديناميكية:
- البنية تحتوي
مؤشر بيانات + عداد + سعة initتُخصص الذاكرة الأوليةaddتفحص السعة وتستخدمreallocللتوسيع عند الامتلاءfreeتُحرّر الذاكرة وتضبط إلى NULL
هذه هي الطريقة الأكثر شيوعًا لتنفيذ المجموعات الديناميكية في C. أتقنها مرة، وأعد استخدامها في أي سيناريو.
(2) نمط إدخال/إخراج الملفات
fprintf(fp, "%d\n", count);
for (int i = 0; i < count; i++) {
fprintf(fp, "field1 field2 ...\n", ...);
}
عند القراءة، اقرأ العدد أولًا، ثم مر عبر السجلات. لاحظ أن %s تتوقف عند المسافة — إما تأكد أن الحقول لا تحتوي مسافات، أو استخدم fscanf بتنسيق ثابت.
(3) حيلة الحذف
عند حذف عنصر وسط من مصفوفة، اكتب فوقه بالعنصر الأخير، ثم count--. هذا يتجنب نقل عناصر كثيرة لكن لا يحافظ على الترتيب. إذا كان الترتيب مهمًا، استخدم memmove لإزاحة العناصر اللاحقة للأمام.
❓ أسئلة شائعة
س: لماذا مضاعفة السعة بدلًا من إضافة 1؟ ج: المضاعفة استراتيجية بمعدل استهلاك
O(1)— إجمالي عدد النسخ أقل بكثير من الزيادة بواحد في كل مرة. هذه نتيجة كلاسيكية من تحليل الخوارزميات، وvectorفي C++ يستخدم نفس الاستراتيجية.
س: هل أحفظ الملفات بصيغة ثنائية أم نصية؟ ج: الصيغة النصية قابلة للقراءة والتعديل يدويًا وعبر المنصات. الثنائية أكثر إحكامًا وأسرع في القراءة/الكتابة لكنها غير مقروءة بشريًا. النصية أكثر بديهية لمشاريع التعلم.
س: ماذا لو احتجت الحفاظ على الترتيب عند حذف بتبديل الأخير؟ ج: استخدم
memmoveلإزاحة العناصر للأمام:memmove(&data[i], &data[i+1], (count-i-1)*sizeof(T)). أو انتقل لبنية قائمة مرتبطة.
س: ماذا لو كان الاسم يحتوي مسافات؟ ج:
scanf/fscanfبـ%sتتوقف عند المسافة. استخدمfgetsلقراءة سطر كامل، أو اتفق على استخدام شرطات سفلية بدل المسافات، أو استخدم تنسيقًا ذو عرض ثابت مثل%31[^\n].
📖 ملخص
- المصفوفات الديناميكية تستخدم بنية
مؤشر + عداد + سعة، باستراتيجية توسع بمقدار 2x - CRUD (إنشاء، قراءة، تحديث، حذف) هو جوهر إدارة البيانات — كل مشروع يدور حول هذه العمليات الأربع
- حفظ الملفات يكتب عدد السجلات أولًا، ثم كل سجل؛ القراءة تحمل العدد أولًا، ثم كل سجل
- حذف عنصر وسط يمكن استخدام تبديل الأخير (سريع لكن بلا ترتيب) أو الإزاحة (يحافظ على الترتيب)
- دمج البنى والذاكرة الديناميكية وإدخال/إخراج الملفات هو النمط الأساسي لمشاريع C
📝 تمارين
- أضف ميزة "تعديل الدرجات" لنظام إدارة درجات الطلاب: أدخل رقمًا جامعيًا ودرجات جديدة لتحديث سجل الطالب المُقابل
- أضف ميزة "البحث برقم الهاتف" لمدير جهات الاتصال، وغيّر التخزين ليحتفظ بجهات الاتصال مرتبة حسب الاسم (حافظ على الترتيب عند الإدراج)
- قسّم كلا المشروعين لهيكل متعدد الملفات: يجب أن يكون لكل مشروع ملف إعلان
.hوملف تنفيذ.cعلى الأقل، مع Makefile مُقابل