سؤال

الكثير من الوظائف من مكتبة C القياسية ، وخاصة تلك الخاصة بمعالجة السلسلة ، وأبرزها strcpy () ، تشترك في النموذج الأولي التالي:

char *the_function (char *destination, ...)

قيمة إرجاع هذه الوظائف هي في الواقع نفس المقدمة destination. لماذا تضيع قيمة الإرجاع لشيء زائدة عن الحاجة؟ من المنطقي أن تكون هذه الوظيفة باطلة أو إرجاع شيء مفيد.

تخميني الوحيد لسبب هذا هو أنه أسهل وأكثر ملاءمة لاستدعاء الوظيفة في تعبير آخر ، على سبيل المثال:

printf("%s\n", strcpy(dst, src));

هل هناك أي أسباب أخرى معقولة لتبرير هذا المصطلح؟

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

المحلول

كما أشار إيفان ، من الممكن فعل شيء مثل

char* s = strcpy(malloc(10), "test");

على سبيل المثال malloc()ed الذاكرة قيمة ، دون استخدام متغير المساعد.

(هذا المثال ليس هو الأفضل ، فسيتم تعطله في ظروف الذاكرة ، لكن الفكرة واضحة)

نصائح أخرى

أعتقد أن تخمينك صحيح ، فإنه يجعل من السهل عش المكالمة.

char *stpcpy(char *dest, const char *src); يعيد مؤشر إلى نهاية من السلسلة ، وهو جزء من Posix.1-2008. قبل ذلك ، كان امتداد GNU LIBC منذ عام 1992. إذا ظهر لأول مرة في Lattice C Amigados في عام 1986.

gcc -O3 سوف يتحسن في بعض الحالات strcpy + strcat ليستخدم stpcpy أو strlen + نسخ مضمّن ، انظر أدناه.


تم تصميم مكتبة C القياسية في وقت مبكر جدًا ، ومن السهل جدًا القول بأن str* الوظائف ليست مصممة على النحو الأمثل. تم تصميم وظائف الإدخال/الإخراج بالتأكيد جداً في وقت مبكر ، في عام 1972 قبل أن كان C حتى معالج مسبق ، وهو لماذا fopen(3) يأخذ سلسلة وضع بدلاً من صورة نقطية مثل UNIX open(2).

لم أتمكن من العثور على قائمة بالوظائف المدرجة في "حزمة I/O المحمولة" لـ Mike Lesk ، لذلك لا أعرف ما إذا كانت strcpy في شكله الحالي يعود إلى هناك أو إذا تمت إضافة هذه الوظائف لاحقًا. (المصدر الحقيقي الوحيد الذي وجدته هو مقال دينيس ريتشي على نطاق واسع التاريخ, ، وهو ممتاز ولكن ليس الذي - التي في الصميم. لم أجد أي وثائق أو رمز مصدر لحزمة الإدخال/الإخراج الفعلية نفسها.)

تظهر في شكلها الحالي في الطبعة الأولى K&R, 1978.


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

كما يقول R:

نتمنى جميعًا أن هذه الوظائف قد أعادت مؤشرًا إلى بايت لاخار O(n) العمليات ل O(1))

على سبيل المثال الاتصال strcat(bigstr, newstr[i]) في حلقة لبناء سلسلة طويلة من سلاسل قصيرة (O (1) الطول) تقريبًا O(n^2) التعقيد ، ولكن strlen/memcpy سوف ننظر فقط إلى كل حرف مرتين (مرة واحدة في Strlen ، مرة واحدة في memcpy).

باستخدام مكتبة ANSI C القياسية فقط ، لا توجد طريقة لإلقاء نظرة على كل حرف فقط ذات مرة. يمكنك كتابة حلقة بايت في وقت واحد يدويًا ، ولكن بالنسبة للسلاسل أطول من بضع بايت ، فهذا أسوأ من النظر إلى كل حرف مرتين مع المترجمين الحاليين (والتي لن تقوم بتثبيت حلقة بحث تلقائية) على HW الحديث ، بالنظر إلى SIMD Strlen و MEMCPY بفعالية. يمكنك استخدام length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length;, ، لكن sprintf() يجب أن تتحمل سلسلة تنسيقها وهي ليس سريع.

لا يوجد حتى نسخة من strcmp أو memcmp هذا يعيد موقع من الفرق. إذا كان هذا هو ما تريده ، فلديك نفس المشكلة لماذا تقارن السلسلة بسرعة كبيرة في بيثون؟: وظيفة مكتبة محسّنة تعمل بشكل أسرع من أي شيء يمكنك القيام به مع حلقة مجمعة (ما لم تكن قد قمت بتصنيع ASM محسّنة يدويًا لكل منصة مستهدفة تهتم بها) ، والتي يمكنك استخدامها للاقتراب من البايت المختلفة قبل التراجع إلى أ حلقة منتظمة بمجرد الاقتراب.

يبدو أن مكتبة سلسلة C تم تصميمها دون النظر إلى تكلفة O (n) لأي عملية ، وليس فقط العثور على نهاية السلاسل الضمنية ، و strcpyسلوك S بالتأكيد ليس المثال الوحيد.

إنهم يتعاملون بشكل أساسي مع الأوتار الضمنية ككائنات كاملة كاملة ، ويعودون دائمًا المؤشرات إلى البداية ، أو حتى النهاية أو إلى وضع داخل واحد بعد البحث أو الإلحاق.


التخمين التاريخ

في وقت مبكر C على PDP-11, ، وأظن أن strcpy لم يكن أكثر كفاءة من while(*dst++ = *src++) {} (وربما تم تنفيذها بهذه الطريقة).

في الواقع، الطبعة الأولى K&R (صفحة 101) يوضح أن تنفيذ strcpy ويقول:

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

هذا يعني لقد توقعوا تمامًا أن يكتب المبرمجون حلقاتهم الخاصة في الحالات التي تريد فيها القيمة النهائية dst أو src. وبالتالي ربما لم يروا حاجة إلى إعادة تصميم واجهة برمجة تطبيقات المكتبة القياسية حتى فات الأوان لفضح واجهات برمجة التطبيقات الأكثر فائدة لوظائف مكتبة ASM المحسنة يدويًا.


ولكن هل إرجاع القيمة الأصلية لـ dst جد اية منطقية؟

strcpy(dst, src) عودة dst مماثل ل x=y التقييم إلى x. لذلك يجعل STRCPY يعمل مثل مشغل تعيين السلسلة.

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

وكان المترجمون القدامى القشور أيضا عامل. مع المترجمين التحسين الحديثة ، char *tmp = foo(); / bar(tmp); ليس أبطأ من bar(foo());, ، لكنه مع gcc -O0. لا أعرف ما إذا كان يمكن للمترجمين الأوائل جدًا تحسين المتغيرات تمامًا (لا يحتفظون بحجز مساحة المكدس لهم) ، ولكن نأمل أن يتمكنوا على الأقل من الاحتفاظ بها في السجلات في حالات بسيطة (على عكس الحديثة gcc -O0 الذي ينسجم عن قصد/إعادة تحميل كل شيء لتصحيح الأخطاء المتسقة). بمعنى آخر gcc -O0 ليس نموذجًا جيدًا للمترجمين القدامى ، لأنه مكافحة التحسين عن قصد لتصحيح الأخطاء المتسقة.


الدافع المولود المولد المولد

بالنظر إلى الافتقار إلى الرعاية حول الكفاءة في تصميم API العام لمكتبة C string ، قد يكون هذا غير مرجح. ولكن ربما كان هناك فائدة بحجم الكود. (على أجهزة الكمبيوتر المبكرة ، كانت حجم الكود أكثر من حد كبير من وقت وحدة المعالجة المركزية).

لا أعرف الكثير عن جودة المترجمين المبكرة ، ولكن من الرهان الآمن أنهم لم يكونوا رائعين في التحسين ، حتى بالنسبة للهندسة المعمارية البسيطة / المتعامدة مثل PDP-11.

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

لكن الوظيفة تستدعي Clobber بعض السجلات ، بما في ذلك جميع سجلات تماس Arg. (لذلك عندما تحصل الوظيفة على ARG في السجل ، يمكنها زيادة ذلك بدلاً من النسخ إلى سجل خدش.)

لذا ، بصفتك المتصل ، يتضمن خيار الكود العام الخاص بك للحفاظ على شيء ما عبر استدعاء الوظيفة:

  • تخزين/إعادة تحميلها إلى ذاكرة المكدس المحلية. (أو مجرد إعادة تحميله إذا كانت نسخة محدثة لا تزال في الذاكرة).
  • حفظ/استعادة سجل محفوظ للمكالمات في بداية/نهاية وظيفتك بأكملها ، ونسخ المؤشر إلى أحد هذه السجلات قبل استدعاء الوظيفة.
  • تقوم الوظيفة بإرجاع القيمة في سجل لك. (بالطبع ، يعمل هذا فقط إذا تم كتابة مصدر C لاستخدام قيمة الإرجاع في حين أن من متغير الإدخال. على سبيل المثال dst = strcpy(dst, src); إذا كنت لا تعششها).

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

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

راجع للشغل ، في العديد من ISAs ، ليس سجل قيمة العودة هو أول سجل لتمرير ARG. وما لم تستخدم أوضاع معالجة الفهرس BASE+، فإنه يكلف تعليمات إضافية (وربط Reg آخر) لـ StrCPY لنسخ السجل لحلقة المؤشر.

PDP-11 أدوات أدوات عادة ما تستخدم نوعا من اتفاقية مكدس args, ، دائما دفع args على المكدس. لست متأكدًا من عدد السجلات المحفوظة بالمكالمات مقابل الاتصالات التي كانت طبيعية ، ولكن تم توفير 5 أو 6 جيم فقط من ريجس (R7 كونه عداد البرنامج ، R6 كونه مؤشر المكدس ، R5 غالبًا ما يستخدم كمؤشر إطار). لذلك فهو مشابه ولكن أكثر ضيقة من 32 بت x86.

char *bar(char *dst, const char *str1, const char *str2)
{
    //return strcat(strcat(strcpy(dst, str1), "separator"), str2);

    // more readable to modern eyes:
    dst = strcpy(dst, str1);
    dst = strcat(dst, "separator");
//    dst = strcat(dst, str2);

    return dst;  // simulates further use of dst
}

  # x86 32-bit gcc output, optimized for size (not speed)
  # gcc8.1 -Os  -fverbose-asm -m32
  # input args are on the stack, above the return address

    push    ebp     #
    mov     ebp, esp  #,      Create a stack frame.

    sub     esp, 16   #,      This looks like a missed optimization, wasted insn
    push    DWORD PTR [ebp+12]      # str1
    push    DWORD PTR [ebp+8]       # dst
    call    strcpy  #
    add     esp, 16   #,

    mov     DWORD PTR [ebp+12], OFFSET FLAT:.LC0      # store new args over our incoming args
    mov     DWORD PTR [ebp+8], eax    #  EAX = dst.
    leave   
    jmp     strcat                  # optimized tailcall of the last strcat

هذا أكثر إحكاما بكثير من الإصدار الذي لا يستخدم dst =, وبدلاً من ذلك يعيد استخدام ARG للمدخلات لـ strcat. (انظر كلاهما على مستكشف برنامج التحويل البرمجي Godbolt.)

ال -O3 الإخراج مختلف تمامًا: GCC للإصدار الذي لا يستخدم قيمة الإرجاع stpcpy (يعيد مؤشر إلى الذيل) ثم mov-هيات لتخزين بيانات السلسلة الحرفية مباشرة إلى المكان الصحيح.

لكن لسوء الحظ ، dst = strcpy(dst, src) -O3 إصدار لا يزال يستخدم العادية strcpy, ، ثم تطبع strcat كما strlen + mov-فوري.


إلى C-bring أو لا لسلسلة C

ج السلاسل الضمنية الطول ليست دائما متأصل سيئة ، ولها مزايا مثيرة للاهتمام (مثل اللاحقة هي أيضا سلسلة صالحة ، دون الحاجة إلى نسخها).

لكن مكتبة سلسلة C غير مصممة بطريقة تجعل الكود الفعال ممكنًا ، لأنه char-لا تلقائيًا في حلقات الوقت ، لا تلقائيًا وتتخلص من وظائف المكتبة نتائج العمل التي يتعين عليهم القيام بها.

GCC و CLANG لا تلقائيًا أبدًا حلقات تلقائية ما لم يكن عدد التكرار معروفًا قبل التكرار الأول ، على سبيل المثال for(int i=0; i<n ;i++). يمكن أن تقوم ICC بإعداد حلقات البحث ، ولكن لا يزال من غير المرجح أن تفعل ذلك وكذلك ASM المكتوب يدويًا.


strncpy وهكذا في الأساس كارثة. على سبيل المثال strncpy لا نسخ الإنهاء '\0' إذا وصل إلى حد حجم المخزن المؤقت. يبدو أنه تم تصميمه للكتابة في وسط الأوتار الكبيرة ، ليس لتجنب الفائض العازلة. عدم إعادة مؤشر إلى النهاية يعني أنه يجب عليك ذلك arr[n] = 0; قبل أو بعد ذلك ، يحتمل أن تلمس صفحة من الذاكرة التي لا تحتاج إلى لمسها.

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

كما يقول بروس داوسون: توقف عن استخدام strncpy بالفعل!. يبدو أن بعض ملحقات MSVC مثل _snprintf أسوأ.

كما أنه من السهل للغاية رمز.

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

يمكن للمبرمجي إعلان الوظيفة void. لكن قيمة الإرجاع هذه موجودة بالفعل في المكان الصحيح ، فقط في انتظار العودة ، ولا تكلف حتى تعليمات إضافية لإعادتها! بغض النظر عن حجم التحسن ، فهو مفيد في بعض الحالات.

نفس المفهوم كما واجهات بطلاقة. مجرد جعل الكود أسرع/أسهل للقراءة.

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

if(strcpy(dest, source) == NULL) {
  // Something went horribly wrong, now we deal with it
}
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top