سؤال

لقد كتبت ملف مكتبة بسيط مع وظيفة لقراءة الخطوط من ملف من أي حجم. يتم استدعاء الوظيفة من خلال تمريرها في المخزن المؤقت والحجم الملقح للمكدس ، ولكن إذا كان الخط كبيرًا جدًا ، يتم تهيئة المخزن المؤقت الخاص بالمواد الملقحة واستخدامه لتمرير خط أكبر.

هذا المخزن المؤقت الملقح في كومة هو محدد الوظيفة وإعلانه ثابت ، ويتم تهيئته إلى فارغ في البداية بالطبع. لقد كتبت في بعض الشيكات في بداية الوظيفة ، للتحقق مما إذا كان المخزن المؤقت للكومة غير خاطئ ؛ إذا كان هذا هو الحال ، فإن قراءة السطر السابق كانت طويلة جدًا. وبطبيعة الحال ، أقوم بتحرير المخزن المؤقت للكومة وأعيده إلى NULL ، معتقدين أن القراءة التالية لن تحتاج إلا إلى ملء المخزن المؤقت الذي تم تخصيصه (يجب أن يكون من النادر جدًا رؤية خطوط تزيد عن 1 ميجابايت ، حتى في تطبيقنا!).

لقد تجاوزت الكود واختبرته جيدًا إلى حد ما ، من خلال قراءته بعناية وعن طريق إجراء بعض الاختبارات. أنا واثق بشكل معقول من الحفاظ على الثابتة التالية:

  • سيكون المخزن المؤقت للكومة فارغًا (ولن يتسرب أي ذاكرة) عند إرجاع الوظيفة إذا كان المخزن المؤقت المكدس هو كل ما هو مطلوب.
  • إذا لم يكن المخزن المؤقت للكومة لا ، لأنه كان مطلوبًا ، فسيتم تحريره على مكالمة الوظيفة التالية (وربما إعادة استخدامها إذا لزم الأمر على هذا السطر التالي).

لكنني فكرت في مشكلة محتملة: إذا كان السطر الأخير في ملف طويل جدًا ، فمن المفترض أن الوظيفة لا يتم استدعاؤها مرة أخرى ، لست متأكدًا من أن لدي أي طريقة لتحرير المخزن المؤقت للكومة- إنها وظيفة -Scoped ، بعد كل شيء.

لذا فإن سؤالي هو ، كيف يمكنني تحرير الذاكرة المخصصة ديناميكيًا في مؤشر ثابت للوظائف ، من الناحية المثالية دون الاتصال بالوظيفة مرة أخرى؟ (ومن الناحية المثالية دون جعلها متغيرًا عالميًا ، أيضًا!)

الرمز المتاح عند الطلب. (لم أتمكن من الوصول الآن ، آسف. وآمل أن يكون السؤال عامًا كافيًا ومستمرًا جيدًا لعدم الحاجة إليه ، ولكن بكل الوسائل لا تتردد في رفضي من هذه الفكرة!)


تحرير: أشعر أنني يجب أن أضيف بعض الملاحظات حول استخدام الوظيفة.

يتم استخدام هذه الوظيفة بالذات في شكل خطوط يتم قراءتها بشكل تسلسلي من ملف ، ثم يتم نسخها على الفور إلى هياكل POD ، سطر واحد لكل بنية. يتم إنشاء تلك على الكومة عند قراءة الملف ، ولكل واحد من هذه الهياكل مؤشر char يحتوي على سطر من الملف. من أجل أن تستمر هذه ، يجب أن تحدث نسخة بالفعل. (كانت تلك واحدة من الحجج المضادة الكبيرة التي ظهرت في العديد من الإجابات- أوه لا ، يجب نسخ الخط ، يا عزيزي).

أما بالنسبة إلى Multithreading ، كما قلت هذا تم تصميمه ليتم استخدامه بشكل متسلسل. لا ، إنه ليس آمنًا ، لكنني لا أهتم.

شكرا على العديد من الردود ، على الرغم من! سأقرأهم بشكل أكثر شمولية عندما أحصل على الوقت. حاليًا ، أميل نحو تمرير مؤشر إضافي حوله أو إعادة تصميم الوظيفة حتى عندما تكون عندما fgets يظهر EOF ، ثم قد أقوم فقط ببناء منطق الحرية هناك بدلاً من ذلك ، ونأمل أن يلزم المستخدم القلق بشأنه.

هل كانت مفيدة؟

المحلول

إذا تمكنت من تغيير الوظيفة ، فإنني أوصي بتغيير واجهة الوظيفة نفسها. أعلم أنك قضيت الكثير من الوقت في تصحيح الأخطاء واختباره ، ولكن هناك بعض المشكلات في تطبيقك الحالي:

  • انها ليست آمنة الخيط ،
  • لا يتحكم المستخدم في البيانات ، لذلك يجب عليه نسخها إذا احتاج إليها لاحقًا ، على الأرجح في المخزن المؤقت الذي سيكون عليه malloc()إد ، وبالتالي إلغاء أي ميزة حصلت عليها من خلال الاستخدام الانتقائي لـ malloc() في وظيفتك ،
  • الأهم من ذلك ، كما اكتشفت ، يجب اتخاذ إجراء خاص من قبل المستخدم لخط آخر طويل.

يجب ألا يقلق المستخدمون الخاص بك من التنفيذ الغريب في وظيفتك ، يجب أن يكونوا قادرين على "استخدامه فقط".

ما لم تكن تفعل ذلك لأغراض تعليمية ، أوصي بالبحث في هذه الصفحة, ، والذي يحتوي على تنفيذ واحد لـ "قراءة خط طويل بشكل تعسفي من دفق" ، وروابط إلى تطبيقات أخرى من هذا القبيل (كل تطبيق يختلف قليلاً عن الآخرين ، لذلك يجب أن تكون قادرًا على العثور على خط يعجبك).

بناءً على تحريرك ، فإن MT-Safe ليس شرطًا ، وسيحدث نسخة دائمًا. لذا ، فإن التصميم الأكثر وضوحًا هو واحد من الاثنين:

  • دع المستخدم يوفر أ char **, الذي يشير إلى المخزن المؤقت الذي ستخصصه وظيفتك ، باستخدام مزيج من malloc() و realloc() (إذا لزم الأمر). تقع على عاتق المستخدم مسؤولية free() عند القيام به. وبهذه الطريقة ، لا يتعين على المستخدم نسخ البيانات مرة أخرى ، لأنه يمكنه تمرير مؤشر إلى أي مكان وجود الوجهة النهائية للبيانات.
  • إرجاع أ char * يتم تخصيصها من خلال وظيفتك. مرة أخرى ، تقع على عاتق المستخدم مسؤولية free() هو - هي.

كلاهما مكافئ إلى حد كبير.

لتنفيذك الحالي ، يمكنك دائمًا إرجاع "لا نهاية الملف" إذا كان السطر الأخير طويلًا جدًا ، ولا ينتهي في خط جديد. بعد ذلك ، سيقوم المستخدم بالاتصال بوظيفتك مرة أخرى ، وبعد ذلك يمكنك تحرير المخزن المؤقت. أنا شخصياً سأكون أكثر سعادة بوظيفة تسمح لي بقراءة العديد من الخطوط كما أريد ، ولا أجبرني على الذهاب إلى نهاية الملف.

نصائح أخرى

بصرف النظر عن صعوبة تحرير هذا المخزن المؤقت المخصص ديناميكيًا ، هناك مشكلة محتملة أخرى. انها ليست مؤلمة آمنة. نظرًا لأنها وظيفة مكتبة ، فهناك دائمًا احتمال استخدامها في بيئة متعددة الخيوط في المستقبل.

من المحتمل أن يكون من الأفضل طلب وظيفة الاتصال لتحرير المخزن المؤقت عبر وظيفة المكتبة ذات الصلة.

قد لا يزال ذلك على ما يرام إذا كنت تستخدم التقنية القياسية للإشارة إلى نهاية الملف (أي هل قمت بإرجاع وظيفة الخط الفارغ).

ما يحدث في هذه الحالة هو أنه بعد قراءة السطر النهائي ، ستكون هناك حاجة إلى مكالمة أخرى إلى وظيفة خط القراءة الخاص بك بحيث يمكن إرجاع NULL للإشارة إلى أنه تم الوصول إلى نهاية الملف. في هذه المكالمة الأخيرة ، يمكنك بعد ذلك تحرير المخزن المؤقت.

خياران يحدثان على الفور:

  1. اصنع المؤشر إلى المخزن المؤقت المخصص للموقد ثابتًا ولكنه تم تحديد موقعه. أضف وظيفة (ثابتة) تتحقق إذا لم تكن فارغة وإذا لم تكن خالية من الفرق (). اتصل على atexit (Free_func) في بداية البرنامج ، حيث تكون Free_func هي الوظيفة الثابتة. يمكنك الحصول على بعض روتين الإعداد العالمي (caled by main () حيث يتم ذلك.

  2. لا تقلق بشأن هذا؛ يتم إصدار الذاكرة المخصصة للكومة بواسطة نظام التشغيل عندما تخرج العملية الخاصة بك ، وتسرب الذاكرة ليس تراكميًا ، لذلك حتى لو كان برنامجك عمره طويلًا ، فلن يثير استثناء OOM (ما لم يكن لديك بعض الأخطاء الأخرى).

أفترض أن تطبيقك ليس متعدد مؤشرات الترابط. في هذه الحالة ، يجب ألا تستخدم عازلة ثابتة على الإطلاق ، أو يجب عليك استخدام بيانات مؤشر الترابط المحلي.

الواجهة التي اخترتها تجعل هذه مشكلة لا يمكن حلها:

  • يجب ألا يعرف العميل ما إذا كانت قيمة الإرجاع تشير إلى ذاكرة ثابتة أو ديناميكية.

  • يجب أن تشير قيمة الإرجاع إلى الذاكرة التي تفوق المكالمة.

  • قد يكون أي مكالمة هو الأخير.

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

إذا لم تتمكن من التعايش مع التسرب ، فإن أبسط شيء يجب فعله هو توسيع الواجهة حتى يتمكن العميل من إخطار وظيفتك عند الانتهاء من العميل بالذاكرة. (في الوقت الحالي ، يقول العقد مع العميل أن العميل يمتلك الذاكرة حتى يتصل بوظيفتك مرة أخرى ، وعند هذه النقطة تعود ملكية إلى وظيفتك.) بالطبع ، لتغيير الواجهة تعني أيضًا أيضًا

  • إضافة وظيفة جديدة ، والتي تتطلب منك تعزيز مؤشرك لتكون static لكن محليًا إلى وحدة التجميع ، أو

  • إضافة بعض الوسيطة إلى الوظيفة الحالية (أو التحميل الزائد للوسيطة) بحيث يكون لديك مكالمة تعني "لقد انتهيت من ذاكرتك الآن ، لكنني لا أريد سطرًا آخر".

يتمثل التغيير الأكثر جذرية في إعادة كتابة الوظيفة لاستخدام الذاكرة المخصصة ديناميكيًا طوال حياتها ، وتوسع الكتلة تدريجياً حسب الحاجة حتى تصبح أكبر قدر من القراءة على الإطلاق (أو ربما يتم تقريبها إلى القوة التالية لشخصين). اعتمادًا على الحالات الفعلية التي قد تستهلكها هذه الاستراتيجية أقل مساحة العنوان من الحفاظ على مخزن مؤقت ثابت كبير.

على أي حال ، لست مقتنعًا بأنك يجب أن تقلق بشأن هذه الحالة الزاوية. إذا كنت تعتقد أن هذه القضية مهمة ، فيرجى تحرير سؤالك لتظهر لنا الأدلة.

بدلاً من نطاق الوظيفة ، امنحها نطاق الوحدة (أي في نطاق الملف ، ولكن ثابتًا ، لذلك ليس مرئيًا خارج هذا الملف. أضف وظيفة صغيرة تحرر المخزن المؤقت ، واستخدامها atexit() للتأكد من أن هذا يسمى قبل خروج البرنامج. بديل ، لا تقلق بشأن ذلك - تسرب يحدث مرة واحدة فقط ، ويتم تحريره تلقائيًا مع خروج البرنامج ليس ضارًا بشكل خاص.

أشعر بأنني مضطر إلى القول إن التصميم يبدو لي مثل وصفة لكارثة. عندما تحرر المخزن المؤقت ، لا توجد طريقة تقريبًا لتخمين ما إذا كان قد لا يزال قيد الاستخدام. يتعين على المستخدم (على ما يبدو) تتبع المكان الذي تم فيه إرجاع البيانات ، ونسخ البيانات إلى مخزن مؤقت جديد إذا (وفقط) قمت بتخصيص واحدة ديناميكيًا. في بيئة متعددة الخيوط ، تحتاج إلى جعل مؤشر المؤشر الداخلي المحلي ليكون لديه أي فرصة للعمل بشكل صحيح على الإطلاق. للمستخدم ، قد تقوم الوظيفة بأحد أمرين مختلفين تمامًا - إما إرجاع مخزن مؤقت مملوك للمستخدم ، أو إرجاع مخزن مؤقت مملوك للوظيفة ، ولا يمكن استخدامه إلا بأمان من خلال تخصيص مخزن مؤقت آخر ، ونسخ يتم استدعاء البيانات في المخزن المؤقت الآخر قبل الوظيفة مرة أخرى.

هناك بعض الاختراقات التي يمكنني التفكير فيها ، على الرغم من أن كلاهما يتطلب نقل الإعلان الثابت من الوظيفة. لا أستطيع أن أتخيل لماذا ستكون هذه مشكلة.

باستخدام امتداد GCC,

static char *buffer;
void use_buffer(size_t n) {
    buffer = realloc(buffer, n);
}
void cleanup_buffer() __attribute__((destructor)) {
    free(buffer);
}

باستخدام C ++ ،

static char *buffer;
static class buffer_guard {
    ~buffer_guard() { free(buffer); }
} my_buffer_guard;

في أي حال ، أنا لا أحب التصميم حقًا. في C ، عادةً ما يكون المتصل مسؤولاً عن تخصيص/تحرير الذاكرة التي يحتاجها إلى استخدامها ، حتى لو تم ملؤها بواسطة Callee.

راجع للشغل ، قارن مع Glibc nonstandard الحصول على خط. لا يستخدم ذاكرة ثابتة أبدًا.

كنت سأعلق فقط أسفل إجابة مارك ، لكن قد يشعر الأمر بالضيق قليلاً. ومع ذلك ، فإن هذه الإجابة هي في جوهرها تعليق على إجابته ، والتي أجدها جيدة جدًا بالإضافة إلى أن أكون سريعًا :).

ليس فقط وظيفتك ليست آمنة ، ولكن حتى بدون مؤشرات الترابط ، فإن الواجهة لاستخدامها بشكل صحيح معقدة. يجب أن يكون المتصل قد انتهى بالنتيجة السابقة قبل الاتصال بالوظيفة مرة أخرى. إذا كان هذا الرمز لا يزال قيد الاستخدام بعد عامين من الآن ، فسوف يخدش شخص ما رأسه في محاولة لاستخدامه بشكل صحيح ... أو ما هو أسوأ ، استخدمه خطأ حتى حتى التفكير فيه. هذا الشخص يمكن أن يكون أنت ...

اقتراح مارك (يتطلب من المتصل تحرير المخزن المؤقت) هو IMHO الأكثر منطقية. ولكن ربما لا تثق malloc و free لا تتسبب في تجزئة على المدى الطويل ، أو لديك سبب آخر لتفضيل محلول المخزن المؤقت الثابت. في هذه الحالة ، يمكنك الاحتفاظ بالمخزن المؤقت الثابت لخطوط الطول العادية ، وتحديد علامة منطقية تشير إلى ما إذا كان المخزن المؤقت الثابت مشغولًا حاليًا ، وتوثيق الوظيفة التالية (وليس free) يجب استدعاؤها بعنوان المخزن المؤقت عندما لم يعد المتصل يستخدمه:

char static_buffer[512];
int buffer_busy;

void free_buffer(char *p)
{
  if (p == static_buffer)
  {
     assert(buffer_busy);
     buffer_busy=0;
  }
  else free(p);
}

char *get_line(...)
{
  char *result;
  if (..short line..)
  {
     result = static_buffer;
     assert(!buffer_busy);
     buffer_busy=1;
  }
  else result = malloc(...);
  ...
  return result;
}

الظروف الوحيدة التي ستؤدي فيها التأكيدات هي الظروف التي سيكون فيها تطبيقك السابق قد أخطأت بصمت ، والنفقات العامة منخفضة للغاية مقارنة بحلك الحالي (فقط تبديل العلم ، وطلب من المتصل الاتصال free_buffer عندما ينتهي ، وهو أنظف). إذا كان التأكيد في get_line على وجه الخصوص ، فهذا يعني أنك بحاجة إلى تخصيص ديناميكي بعد كل شيء ، لأنه لا يمكن الانتهاء من المتصل مع المخزن المؤقت في الوقت الذي كان يطلب فيه آخر.

ملاحظة: هذا لا يزال غير آمن.

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top