C / المتقدم: حدود اللغة والأنظمة / الدرس 14
الاستدعاءات الراجعة والواجهات العامة
مرر السلوك بمؤشرات دوال ذات أنواع صحيحة واربط الوصول العام للبيانات بعقود حجم وسياق صريحة.
في هذه الصفحة
أهداف الدرس
- قراءة أنواع مؤشرات الدوال واستدعاؤها.
- كتابة ترتيب qsort صالح دون تجاوز حسابي.
- تحديد عمر سياق الاستدعاء الراجع وتعديله والاحتفاظ به.
للسلوك نوع أيضا
يحدد مؤشر الدالة سلوكا قابلا للاستدعاء بنوعي إرجاع ومعاملات معينين. تمثل int (*operation)(int) مؤشرا إلى دالة تقبل int وتعيد int؛ وتميز الأقواس ذلك عن دالة تعيد مؤشرا. الاستدعاء عبر نوع مؤشر دالة غير متوافق سلوك غير معرّف حتى لو أخفى التحويل تشخيص المترجم.
استخدم typedef عندما يحسن وضوح الواجهة، وانشر تصريحا موحدا للاستدعاء الراجع. لا تضمن ISO C تبادلا عشوائيا بين مؤشرات الدوال ومؤشرات الكائنات void *. وقد تعرف واجهة منصية اصطلاحا منفصلا لذلك، لكن يجب عرضه كعقد لتلك المنصة.
المقارن يحدد ترتيبا متسقا
تتلقى qsort مؤشر البداية وعدد العناصر وحجم العنصر واستدعاء مقارنة راجعا. يقرأ المقارن عنصرين ويعيد سالبا أو صفرا أو موجبا حسب الترتيب. قد يتجاوز left - right لقيم int المدى، فاستخدم مقارنات تنتج نتائج صغيرة فقط. ويجب أن تبقى المقارنة متسقة طوال الفرز.
يعيد المثال تحويل كل مؤشر كائن عام إلى نوع العنصر الفعلي ويقرأه دون تعديل. لا يلزم qsort حفظ ترتيب العناصر المتساوية الأصلي، ولا يعد اسمها بخوارزمية أو تعقيد محدد. إذا احتجت الثبات فاختر أو نفذ فرزا بضمان ثبات صريح.
#include <stdio.h>
#include <stdlib.h>
static int compare_ints(const void *left, const void *right)
{
int a = *(const int *)left;
int b = *(const int *)right;
return (a > b) - (a < b);
}
int main(void)
{
int values[] = {4, 1, 3, 2};
size_t count = sizeof values / sizeof values[0];
qsort(values, count, sizeof values[0], compare_ints);
for (size_t i = 0; i < count; ++i) printf("%d%s", values[i], i + 1 == count ? "\n" : " ");
return 0;
}
1 2 3 4
نتائج المقارنة صفر أو واحد، لذلك فرقها دائما -1 أو 0 أو 1.
تمرير الحالة كسياق صريح
يحتاج الاستدعاء الراجع غالبا حالة إضافية إلى العنصر الحالي. يستطيع معامل سياق void * حمل مؤشر إلى سجل يملكه المستدعي بشرط استعادة النوع الصحيح وبقاء السجل حيا لكل الاستدعاءات. هذا نمط تحويل مؤشرات كائنات، وليس إذنا بتخزين أي مؤشر دالة داخل السياق.
الاجتياز التالي متزامن زمنيا ولا يحتفظ بأي مؤشر. يضيف استدعاؤه الراجع كل قيمة صغيرة ثابتة إلى مجمع int حي. يجب أن تصف الواجهة الحقيقية هل يجوز إيقاف التكرار أو الفشل أو تعديل الحاوية أو استدعاء الواجهة ذاتيا أو الاحتفاظ بالعناصر بعد الإرجاع. تؤثر تلك الخيارات في السلامة أكثر من صياغة مؤشر الدالة.
#include <stddef.h>
#include <stdio.h>
typedef void (*visitor)(int value, void *context);
static void visit(const int *data, size_t count, visitor action, void *context)
{
for (size_t i = 0; i < count; ++i) action(data[i], context);
}
static void add_value(int value, void *context)
{
int *total = context;
*total += value;
}
int main(void)
{
const int data[] = {2, 3, 4};
int total = 0;
visit(data, 3, add_value, &total);
printf("total=%d\n", total);
return 0;
}
total=9
السياق int حي ويُستخدم أثناء visit فقط؛ والمجموع الثابت الآمن تسعة.
العمومية لا تلغي الأنواع الفعلية
تتيح void * نقل مؤشر كائن دون معرفة نوعه المحدد. يظل للتخزين الفعلي حدود ومحاذاة وعمر وعقد وصول. يستخدم التقدم البايتي العام unsigned char * بحجم عنصر متحقق منه، لا حسابات على void * التي ليست حساب مؤشرات ISO C17.
قبل حساب count * element_size أثبت ملاءمة الضرب لـsize_t وكفاية المنطقة المقدمة. حدد نسخ العنصر وتدميره عندما تحتوي القيم مؤشرات مالكة. قد يصح النسخ الخام لسجل بسيط النسخ ويخطئ لسجل مالك يجب تكرار موارده أو نقلها.
تحديد العودة من الاستدعاء الراجع
تتخلى الواجهة التي تستدعي سلوكا يوفره المستخدم عن التحكم مؤقتا. إذا عدل الاستدعاء الراجع البنية الجاري اجتيازها فقد تبطل الفهارس أو المؤشرات المخزنة قبل استئناف الواجهة. إما أن تحظر التعديل أو تصمم بروتوكولا آمنا له وتوثق العمليات المسموحة.
للاستدعاءات القابلة للفشل أعد حالة وحدد هل يتوقف الاجتياز فورا وهل تبقى الآثار السابقة معتمدة. يحتاج الاحتفاظ بالسياق لاستعمال غير متزامن إلى اتفاق عمر أو ملكية يتجاوز الاستدعاء الأول. لا تمرر عنوان كائن محلي قصير العمر إلى خدمة قد تستدعيه لاحقا.
الأخطاء الشائعة وإصلاحها
يفشل المقارن مع الأعداد القصوى.
- لماذا يحدث؟
- يطرح المعاملين وقد يتجاوز المدى.
- كيف تصلحه؟
- أعد (a > b) - (a < b) لترتيب الأعداد.
- كيف تتجنب تكراره؟
- اختبر INT_MIN وصفرا وINT_MAX والتساوي.
يخفي تحويل الاستدعاء الراجع التحذير لكن الاستدعاءات تفشل.
- لماذا يحدث؟
- نوع الدالة الفعلي غير متوافق.
- كيف تصلحه؟
- اكتب محولا بتوقيع الاستدعاء الراجع المعلن تماما.
- كيف تتجنب تكراره؟
- لا تجعل التحويل بديلا عن مطابقة أنواع الدوال.
يصل استدعاء مؤجل إلى سياق انتهى عمره.
- لماذا يحدث؟
- استُعير كائن محلي بعد عمره.
- كيف تصلحه؟
- مدد الملكية أو ألغ الاستدعاءات وأكملها قبل الخروج من النطاق.
- كيف تتجنب تكراره؟
- وثق الاحتفاظ بالسياقات وموعد انتهاء الاستعارة.
اختبر فهمك
1. صمم اصطلاح الإرجاع لزائر يستطيع التوقف مبكرا بعد العثور على قيمة.
اعرض الحل المشروح
استخدم enum مثل CONTINUE وSTOP وERROR. حدد أن STOP توقف مبكر ناجح وأن ERROR فشل، وأن آثار الاستدعاءات السابقة تبقى ما لم تنفذ معاملة صريحة. يعيد الاجتياز سبب الانتهاء. حدد هل يُستعار العنصر حتى الإرجاع فقط، واحظر تعديل البنية أثناء الزيارة إلا إذا كان مدعوما.
2. اشرح لماذا لا يستطيع متجه عام لسجلات نصية مالكة تعريف النسخ بأنه memcpy وحدها.
اعرض الحل المشروح
تكرر memcpy قيم المؤشرات فتجعل السجلين يشيران إلى التخصيصات نفسها. إذا دمر كلاهما النصوص تتحرر الملكية مرتين. يحتاج المتجه استدعاء نسخ يخصص وينسخ كل نص، أو نقل يفرغ المصدر، أو نموذج استعارة أو مشاركة موثقا. ويجب أن يدمر مسار الفشل عناصر الوجهة المنشأة بنجاح فقط.
أهم ما تعلمته
- توافق مؤشرات الدوال شرط سلامة.
- تحتاج مؤشرات السياق نوعا فعليا وعقد عمر.
- تحتاج الحاويات العامة قواعد حجم ونسخ وتدمير وتعديل.