يشير المؤشر إلى خلية ذاكرة ، ويعني إلغاء
الإشارة إلى المؤشر قراءة قيمة الخلية المحددة. قيمة المؤشر نفسه هو عنوان خلية الذاكرة. لا يحدد معيار لغة C النموذج لتمثيل عناوين الذاكرة. هذه نقطة مهمة جدًا ، نظرًا لأن البنى المختلفة قد تستخدم نماذج عنونة مختلفة. تستخدم معظم البنى الحديثة مساحة العنوان الخطي أو ما شابه ذلك. ومع ذلك ، حتى هذا السؤال غير محدد بدقة ، حيث يمكن أن تكون العناوين مادية أو افتراضية. تستخدم بعض المعماريات تمثيلًا غير رقمي على الإطلاق. لذلك ، تعمل Symbolics Lisp Machine مع الصفوف من الشكل
(الكائن ، الإزاحة) كعناوين.
بعد مرور بعض الوقت ، بعد نشر الترجمة على حبري ، أدخل المؤلف تعديلات كبيرة على نص المقالة. تحديث الترجمة على حبري ليس فكرة جيدة ، لأن بعض التعليقات ستفقد معناها أو ستبدو خارج المكان. لا أريد نشر النص كمقالة جديدة. لذلك ، قمنا بتحديث ترجمة المقال على viva64.com ، وهنا تركنا كل شيء كما هو. إذا كنت قارئًا جديدًا ، أقترح قراءة ترجمة أحدث على موقعنا من خلال النقر على الرابط أعلاه. |
لا ينص المعيار على شكل عرض المؤشرات ، ولكنه ينص - إلى حد أكبر أو أقل - على العمليات معهم. أدناه نعتبر هذه العمليات وميزات تعريفها في المعيار. لنبدأ بالمثال التالي:
#include <stdio.h> int main(void) { int a, b; int *p = &a; int *q = &b + 1; printf("%p %p %d\n", (void *)p, (void *)q, p == q); return 0; }
إذا جمعنا كود GCC هذا بمستوى التحسين 1 وقمنا بتشغيل البرنامج في Linux x86-64 ، فسيتم طباعة ما يلي:
0x7fff4a35b19c 0x7fff4a35b19c 0
لاحظ أن المؤشرات
p و
q تشير إلى نفس العنوان. ومع ذلك ، فإن نتيجة التعبير
p == q خطأ ، وهذا يبدو للوهلة الأولى غريبًا. ألا يجب أن تكون نقطتان إلى نفس العنوان متساويين؟
إليك كيفية تحديد المعيار C نتيجة فحص مؤشرين للمساواة:
C11 § 6.5.9 الفقرة 6
يتساوى المؤشران إذا وفقط إذا كان كلاهما صفرًا ، فإما أن يشير إلى نفس الكائن (بما في ذلك مؤشر إلى الكائن والمشروع الفرعي الأول في بنية الكائن) أو دالة ، أو يشير إلى الموضع بعد العنصر الأخير للصفيف ، أو مؤشر واحد يشير إلى الموضع بعد العنصر الأخير من الصفيف ، ويشير الآخر إلى بداية صفيف آخر يتبع الأول مباشرة في نفس مساحة العنوان. |
بادئ ذي بدء ، يطرح السؤال: ما هو "الشيء
" ؟ بما أننا نتحدث عن لغة C ، فمن الواضح أن الكائنات هنا لا علاقة لها بالكائنات بلغات OOP مثل C ++. في المعيار C ، لم يتم تعريف هذا المفهوم بالكامل:
C11 § 3.15
الكائن هو منطقة تخزين وقت التشغيل التي يمكن استخدام محتوياتها لتمثيل القيم
ملاحظة: عندما يذكر كائن يمكن أن يكون له نوع معين. انظر 6.3.2.1. |
دعنا نحصل على حق. متغير عدد صحيح 16 بت هو مجموعة من البيانات في الذاكرة التي يمكن أن تمثل قيم عدد صحيح 16 بت. لذلك ، مثل هذا المتغير هو كائن. هل ستكون نقطتان متساويتان إذا كان أحدهما يشير إلى البايت الأول من عدد صحيح معين ، والثاني إلى البايت الثاني من نفس الرقم؟ بالطبع ، لم تقصد لجنة توحيد اللغة هذا على الإطلاق. ولكن هنا تجدر الإشارة إلى أنه ليس لديه تفسيرات واضحة في هذا الصدد ، ونحن مضطرون إلى تخمين ما كان يعنيه حقًا.
عندما يعترض المترجم الطريق
دعونا نعود إلى مثالنا الأول.
يتم الحصول على المؤشر
p من الكائن
a ، والمؤشر
q من الكائن
b . في الحالة الثانية ، يتم استخدام حساب العنوان ، والذي يتم تعريفه لعوامل الجمع زائد وناقص على النحو التالي:
البند C.6 ، الفقرة 6.5.6 ، البند 7
عند استخدامها مع هذه العوامل ، يتصرف مؤشر كائن ليس عنصرًا من الصفيف مثل مؤشر إلى بداية صفيف بطول عنصر واحد ، يتوافق نوعه مع نوع الكائن الأصلي. |
نظرًا لأن أي مؤشر لعنصر ليس صفيفًا يصبح في
الواقع مؤشرًا لصفيف بطول عنصر واحد ، فإن المعيار يحدد الحساب الحسابي فقط للمؤشرات للصفائف - هذه هي النقطة 8. نحن مهتمون بالجزء التالي:
البند C.6 ، الفقرة 6.5.6 ، البند 8
إذا تمت إضافة تعبير صحيح إلى المؤشر أو طرحه منه ، فإن المؤشر الناتج يكون من نفس نوع المؤشر الأصلي. إذا كان المؤشر المصدر يشير إلى عنصر صفيف وكان الصفيف بطول كافٍ ، فسيتم فصل المصدر والعناصر الناتجة عن بعضها البعض بحيث يكون الفرق بين فهارسهم مساويًا لقيمة التعبير الصحيح. بمعنى آخر ، إذا كان التعبير P يشير إلى العنصر ith للصفيف ، فإن التعبيرات (P) + N (أو ما يعادلها N + (P) ) و (P) -N (حيث N لها القيمة n) تشير على التوالي (i + n) عناصر المصفوفة th و (i - n) th بشرط وجودها. علاوة على ذلك ، إذا كان التعبير P يشير إلى العنصر الأخير في المصفوفة ، فإن التعبير (P) +1 يشير إلى الموضع بعد العنصر الأخير للصفيف ، وإذا كان التعبير Q يشير إلى الموضع بعد العنصر الأخير للصفيف ، فإن التعبير (Q) -1 يشير إلى العنصر الأخير صفيف. إذا كان كل من المصدر والمؤشرات الناتجة يشيران إلى عناصر من نفس المصفوفة أو إلى الموضع بعد العنصر الأخير من المصفوفة ، فسيتم استبعاد الفائض ؛ وإلا ، فإن السلوك غير محدد. إذا كان المؤشر الناتج يشير إلى الموضع بعد العنصر الأخير للصفيف ، فلا يمكن تطبيق العامل الأحادي * عليه. |
ويترتب على ذلك أن نتيجة التعبير
& b + 1 يجب أن تكون بالتأكيد عنوانًا ، وبالتالي تعتبر
p و
q مؤشرات صالحة. دعني أذكرك بكيفية تعريف المساواة بين مؤشرين في المعيار: "
يتساوى المؤشران إذا وفقط [...] يشير أحد المؤشرات إلى الموضع بعد العنصر الأخير للصفيف ، والآخر إلى بداية مصفوفة أخرى مباشرة بعد الأول في نفس المصفوفة مساحة العنوان " (البند C11 § 6.5.9 البند 6). هذا هو بالضبط ما نلاحظه في مثالنا. يشير المؤشر q إلى الموضع بعد الكائن b ، يليه مباشرة الكائن a ، والذي يشير إليه المؤشر p. لذا ، هل هناك خلل في دول مجلس التعاون الخليجي؟ تم وصف هذا التناقض في عام 2014 على أنه
الخطأ رقم 61502 ، لكن مطوري دول مجلس التعاون الخليجي لا يعتبرونه خطأ وبالتالي لن يقوموا بإصلاحه.
تمت مواجهة مشكلة مماثلة في عام 2016 من قبل مبرمجي لينكس. خذ بعين الاعتبار الرمز التالي:
extern int _start[]; extern int _end[]; void foo(void) { for (int *i = _start; i != _end; ++i) { } }
تحدد الرموز
_start و
_end حدود منطقة الذاكرة. نظرًا لنقلها إلى ملف خارجي ، فإن المترجم لا يعرف كيف توجد الصفائف بالفعل في الذاكرة. لهذا السبب ، يجب أن يكون حذرًا هنا وينطلق من افتراض أنهم يتبعون بعضهم البعض في مساحة العنوان. ومع ذلك ، يجمع GCC حالة الحلقة بحيث تكون دائمًا صحيحة ، مما يجعل الحلقة لا نهائية. يتم وصف هذه المشكلة هنا في هذا
المنشور على LKML - يتم استخدام جزء رمز مشابه هناك. يبدو أنه في هذه الحالة ، مع ذلك ، أخذ مؤلفو دول مجلس التعاون الخليجي بعين الاعتبار التعليقات وغيروا سلوك المترجم. لم أستطع على الأقل إعادة إنتاج هذا الخطأ في إصدار GCC 7.3.1 تحت Linux x86_64.
الحل - في تقرير الخطأ رقم 260؟
قد توضح قضيتنا تقرير الخطأ
رقم 260 . يتعلق الأمر أكثر بالقيم غير المؤكدة ، ولكن يمكنك العثور على تعليق غريب من اللجنة فيه:
يمكن لتطبيقات المترجم [...] أيضًا التمييز بين المؤشرات التي تم الحصول عليها من كائنات مختلفة ، حتى إذا كانت هذه المؤشرات لها نفس مجموعة البتات.إذا أخذنا هذا التعليق حرفيًا ، فمن المنطقي أن تكون نتيجة التعبير
p == q "خاطئة" ، حيث
يتم الحصول على
p و
q من كائنات مختلفة غير متصلة بأي شكل من الأشكال. يبدو أننا نقترب من الحقيقة - أم لا؟ حتى الآن ، تعاملنا مع مشغلي المساواة ، ولكن ماذا عن مشغلي العلاقات؟
الدليل النهائي يتعلق بالعاملين؟
يحتوي تعريف عوامل العلاقة
< و
<= و
> و
> = في سياق مقارنات المؤشر على فكرة غريبة واحدة:
C11 § 6.5.8 الفقرة 5
تعتمد نتيجة مقارنة مؤشرين على الموضع النسبي للكائنات المشار إليها في مساحة العنوان. إذا كانت مؤشرين لأنواع الكائنات يشيران إلى نفس الكائن ، أو كلاهما يشير إلى الموضع بعد العنصر الأخير من نفس المصفوفة ، فإن هذه المؤشرات متساوية. إذا كانت الكائنات المشار إليها أعضاء في نفس الكائن المركب ، فإن المؤشرات إلى أعضاء الهيكل المعلن عنها لاحقًا هي أكثر من مؤشرات للأعضاء المعلن عنها سابقًا ، والمؤشرات إلى عناصر مصفوفة بمؤشرات أعلى هي أكثر من مؤشرات لعناصر من نفس المصفوفة بمؤشرات أقل. جميع المؤشرات لأعضاء نفس الرابطة متساوية. إذا كان التعبير P يشير إلى عنصر من الصفيف ، وكان التعبير Q يشير إلى العنصر الأخير من نفس الصفيف ، فإن قيمة تعبير المؤشر Q + 1 أكبر من قيمة التعبير P. في جميع الحالات الأخرى ، لم يتم تعريف السلوك. |
وفقًا لهذا التعريف ، يتم تحديد نتيجة مقارنة المؤشرات فقط إذا تم الحصول على المؤشرات من
نفس الكائن. نعرض هذا مع مثالين.
int *p = malloc(64 * sizeof(int)); int *q = malloc(64 * sizeof(int)); if (p < q)
هنا ، تشير المؤشرات
p و
q إلى كائنين مختلفين غير مترابطين. لذلك ، لم يتم تحديد نتيجة مقارنتها. لكن في المثال التالي:
int *p = malloc(64 * sizeof(int)); int *q = p + 42; if (p < q) foo();
تشير المؤشرات
p و
q إلى نفس الكائن ، وبالتالي فهي مترابطة. لذا ، يمكن مقارنتها - ما لم تُرجع
malloc قيمة فارغة.
الملخص
لا يصف معيار C11 مقارنات المؤشر بشكل كاف. النقطة الأكثر إشكالية التي واجهناها كانت الفقرة 6 § 6.5.9 ، حيث يُسمح صراحة بمقارنة مؤشرين يشيران إلى مصفوفين مختلفين. هذا يتناقض مع التعليق من تقرير الخطأ رقم 260. ومع ذلك ، نحن نتحدث عن معاني غير محددة ، ولا أريد أن أبني منطقتي على أساس هذا التعليق وحده وتفسيره في سياق آخر. عند مقارنة المؤشرات ، يتم تعريف عوامل العلاقة بشكل مختلف قليلاً عن عوامل المساواة - أي يتم تحديد عوامل العلاقة فقط إذا تم الحصول على كلتا المؤشرين من
نفس الكائن.
إذا تجاهلنا نص المعيار وسألنا عما إذا كان من الممكن مقارنة مؤشرين تم الحصول عليهما من شيئين مختلفين ، فإن الإجابة ستكون على الأرجح "لا". يوضح المثال في بداية المقال مشكلة نظرية. نظرًا لأن المتغيرات
أ و
ب لها فترات تخزين تلقائية ، فإن افتراضاتنا حول وضعها في الذاكرة ستكون غير موثوقة. في بعض الحالات ، يمكننا التخمين ، ولكن من الواضح أنه لا يمكن نقل هذه الشفرة بأمان ، ولا يمكنك معرفة معنى البرنامج إلا من خلال تجميع التعليمات البرمجية وتشغيلها أو تفكيكها ، وهذا يتعارض مع أي نموذج برمجة جاد.
ومع ذلك ، بشكل عام ، لست راضيًا عن الصياغة في معيار C11 ، وبما أن العديد من الأشخاص قد واجهوا هذه المشكلة بالفعل ، يبقى السؤال: لماذا لا تصيغ القواعد بشكل أكثر وضوحًا؟
إضافة
يشير إلى الموضع بعد العنصر الأخير للصفيف
أما بالنسبة للقاعدة المتعلقة بمقارنة ومعالجة العمليات الحسابية للمؤشرات إلى الموضع بعد العنصر الأخير من المصفوفة ، فيمكنك غالبًا العثور على استثناءات له. افترض أن المعيار لن يسمح بمقارنة مؤشرين تم الحصول عليهما من
نفس المصفوفة ، على الرغم من أن واحدة منهما على الأقل تشير إلى الموضع بعد نهاية المصفوفة. ثم لن يعمل الكود التالي:
const int num = 64; int x[num]; for (int *i = x; i < &x[num]; ++i) { }
باستخدام حلقة ، نتجول في مصفوفة
x بأكملها ، والتي تتكون من 64 عنصرًا ، أي يجب أن ينفذ جسم الحلقة 64 مرة بالضبط. ولكن في الواقع ، يتم التحقق من الحالة 65 مرة - مرة واحدة أكثر من عدد العناصر في الصفيف. في التكرارات الـ 64 الأولى ، يشير المؤشر
i دائمًا إلى داخل المصفوفة
x ، بينما يشير التعبير
& x [num] دائمًا إلى الموضع بعد العنصر الأخير من المصفوفة. عند التكرار رقم 65 ، سيشير المؤشر
i أيضًا إلى الموضع بعد نهاية المصفوفة
x ، والذي يصبح شرط الحلقة فيه خطأ. هذه طريقة ملائمة لتجاوز الصفيف بأكمله ، وتعتمد على استثناء لقاعدة عدم اليقين في السلوك عند مقارنة هذه المؤشرات. لاحظ أن المعيار يصف السلوك فقط عند مقارنة المؤشرات ؛ يعد إلغاء الإشارة مسألة منفصلة.
هل من الممكن تغيير مثالنا بحيث لا يشير مؤشر واحد إلى الموضع بعد العنصر الأخير في المصفوفة
x ؟ هذا ممكن ، لكنه سيكون أكثر صعوبة. سيتعين علينا تغيير حالة الحلقة وحظر زيادة المتغير
i في التكرار الأخير.
const int num = 64; int x[num]; for (int *i = x; i <= &x[num-1]; ++i) { if (i == &x[num-1]) break; }
هذا الكود مليء بالبراعة الفنية ، التي تثير الضجيج والتي تصرف الانتباه عن المهمة الرئيسية. بالإضافة إلى ذلك ، ظهر فرع إضافي في جسم الحلقة. لذا أجد أنه من المعقول أن المعيار يسمح بالاستثناءات عند مقارنة مؤشرات الموضع بعد العنصر الأخير في المصفوفة.
ملاحظة فريق PVS-Studioعند تطوير محلل رمز PVS-Studio ، يتعين علينا في بعض الأحيان التعامل مع المشكلات الدقيقة من أجل جعل التشخيص أكثر دقة أو تقديم استشارات تفصيلية لعملائنا. بدت لنا هذه المقالة مثيرة للاهتمام ، لأنها تتطرق إلى القضايا التي لا نشعر فيها بأنفسنا. لذلك ، طلبنا من المؤلف نشر ترجمتها. نأمل أن يتعرف عليها المزيد من المبرمجين C و C ++ ويفهمون أن الأمر ليس بهذه البساطة وأنه عندما يعرض المحلل رسالة غريبة فجأة ، يجب ألا تتسرع في اعتبارها إيجابية كاذبة :).نُشر المقال لأول مرة باللغة الإنجليزية على موقع stefansf.de. يتم نشر الترجمات بإذن من المؤلف.