и strcpy() возвращаемое значение

многие функции из стандартной библиотеки C, особенно для строковых манипуляций, и, в частности, strcpy (), имеют следующий прототип:

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

возвращаемое значение этих функций фактически совпадает с предоставленным destination. Почему вы тратите возвращаемое значение на что-то избыточное? Имеет смысл, чтобы такая функция была пустой или возвращала что-то полезное.

мое единственное предположение о том, почему это так, что это проще и больше удобно вложить вызов функции в другое выражение, например:

printf("%sn", strcpy(dst, src));

есть ли другие разумные причины, чтобы оправдать эту идиому?

6 ответов


Как отметил Эван, можно сделать что-то вроде

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

например, назначить malloc()ed memory значение, без использования вспомогательной переменной.

(этот пример не лучший, он будет разбиваться на условиях нехватки памяти, но идея очевидна)


Я считаю, что ваша догадка верна, это облегчает гнездо вызова.


его также очень легко получить код.

возвращаемое значение обычно остается в регистре AX (это не обязательно, но это часто бывает). И назначение помещается в регистр AX при запуске функции. Чтобы вернуть пункт назначения, программист должен это сделать.... ровно ничего! Просто оставьте ценность там, где она есть.

программист может объявить функцию как void. Но это возвращаемое значение уже находится в нужном месте, просто ожидая, чтобы быть вернулся, и это даже не стоит дополнительной инструкции, чтобы вернуть его! Независимо от того, насколько небольшое улучшение, это удобно в некоторых случаях.


char *stpcpy(char *dest, const char *src); возвращает указатель на конец строки и является частью POSIX.1-2008. До этого это было расширение GNU libc с 1992 года. Если впервые появился в решетке C AmigaDOS в 1986 году.

gcc -O3 в некоторых случаях оптимизировать strcpy + strcat использовать stpcpy или strlen + встроенное копирование, см. ниже.


стандартная библиотека C была разработана очень рано, и очень легко утверждать, что str* функции не рассчитаны. Функции ввода-вывода были определенно очень рано, в 1972 году до C даже был препроцессор, который почему fopen(3) принимает строку режима вместо растрового изображения флага, такого как Unix open(2).

я не смог найти список функций, включенных в "портативный пакет ввода-вывода" Майка леска, поэтому я не знаю,strcpy в своей текущей форме датируется полностью туда или если эти функции были добавлено позже. (Единственный реальный источник, который я нашел, -широко известная статья Денниса Ричи по истории C, что отлично, но не это в глубину. Я не нашел никакой документации или исходного кода для самого пакета ввода-вывода.)

они появляются в их нынешнем виде в K&R первое издание, 1978.


функции должны возвращать результат вычисления, если это потенциально полезно для звонящий, вместо того, чтобы выбросить его. Либо как указатель на конец строки, либо как целочисленная длина. (Указатель был бы естественным.)

как @R говорит:

мы все хотим, чтобы эти функции вернули указатель на завершающий нулевой байт (что уменьшило бы много O(n) операции O(1))

например, вызов strcat(bigstr, newstr[i]) в цикле для создания длинной строки из многих коротких (O (1) длина) строк имеет приблизительно O(n^2) сложности, но strlen/memcpy будет смотреть только на каждый символ дважды (один раз в strlen, один раз в memcpy).

используя только стандартную библиотеку ANSI C, нет способа эффективно смотреть только на каждый символ после. Вы можете вручную написать цикл байт-за-раз, но для строк длиннее, чем несколько байтов, это хуже, чем смотреть на каждый символ дважды с помощью текущих компиляторов (которые не будут автоматически векторизовать цикл поиска) на современном HW, учитывая эффективность libc-предоставляет SIMD strlen и memcpy. Вы могли бы использовать length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length;, а sprintf() должен разобрать строку формата и не быстро.

нет даже версии strcmp или memcmp возвращает позиция разница. Если это то, что вы хотите, у вас такая же проблема как почему сравнение строк так быстро в python?: оптимизированная функция библиотеки, которая работает быстрее, чем все, что вы можете сделать с скомпилированный цикл (если у вас нет оптимизированного вручную asm для каждой целевой платформы, о которой вы заботитесь), который вы можете использовать, чтобы приблизиться к различающемуся байту, прежде чем вернуться к регулярному циклу, как только вы приблизитесь.

похоже, что библиотека строк C была разработана без учета стоимости O(n) любой операции, а не только нахождения конца строк неявной длины, и strcpyповедение определенно не единственный пример.

они в основном лечат строки неявной длины как целые непрозрачные объекты, всегда возвращающие указатели в начало, никогда в конец или в позицию внутри одного из них после поиска или добавления.


история догадки

в начале C на PDP-11, я подозреваю, что strcpy был не более эффективен, чем while(*dst++ = *src++) {} (и, вероятно, был реализован именно так).

в самом деле K&R первое издание (стр. 101) показывает, что реализация strcpy и говорит:

хотя это может показаться загадочным, на первый взгляд, запись значительно удобнее, и следует освоить, если ни по какой другой причине, чем, что вы будете видеть его часто в программах на Си.

это означает они полностью ожидали, что программисты напишут свои собственные циклы в случаях, когда вам нужно конечное значение dst или src. И поэтому, возможно, они не видели необходимости перепроектировать стандартный API библиотеки пока не стало слишком поздно предоставлять более полезные API для оптимизированных вручную функций библиотеки asm.


но возвращает исходное значение dst есть смысл?

strcpy(dst, src) возвращение dst аналогично x=y оценка для x. Таким образом, strcpy работает как оператор присваивания строк.

как указывают другие ответы, это позволяет вложенности, как foo( strcpy(buf,input) );. Ранние компьютеры были очень память ограничена. сохранение компактного исходного кода было обычной практикой. Перфокарты и медленные терминалы, вероятно, были фактором в этом. Я не знаю исторических стандартов кодирования или руководств по стилю или того, что считалось слишком большим, чтобы поставить на одну строку.

Crusty старые компиляторы также были, возможно, фактором. С современными оптимизирующими компиляторами,char *tmp = foo(); / bar(tmp); не медленнее, чем bar(foo());, но с gcc -O0. Я не знаю, могут ли очень ранние компиляторы оптимизировать переменные полностью отсутствуют (не резервируя для них пространство стека), но, надеюсь, они могли бы по крайней мере хранить их в регистрах в простых случаях (в отличие от modern gcc -O0 который специально разливает / перезагружает все для последовательной отладки). т. е. gcc -O0 не является хорошей моделью для древних компиляторов, потому что это анти-оптимизации специально для последовательной отладки.


возможная мотивация ASM, сгенерированная компилятором

учитывая отсутствие заботы о эффективность в общем дизайне API библиотеки строк C это может быть маловероятным. Но, возможно, было преимущество в размере кода. (На ранних компьютерах размер кода был более жестким ограничением, чем время процессора).

я мало знаю о качестве ранних компиляторов C, но можно с уверенностью сказать, что они не были потрясающими в оптимизации, даже для хорошей простой / ортогональной архитектуры, такой как PDP-11.

обычно требуется строковый указатель после в вызов функции. На уровне asm вы (компилятор), вероятно, имеете его в регистре перед вызовом. В зависимости от соглашения о вызове вы либо нажимаете его в стеке, либо копируете его в правый регистр, где соглашение о вызове говорит, что первый arg идет. (то есть где strcpy ждет его). Или, если вы планируете заранее, у вас уже был указатель в правом регистре для соглашения о вызове.

но функция вызывает clobber некоторые регистры, включая все arg-проходя реестры. (Поэтому, когда функция получает arg в регистре, она может увеличить его там вместо копирования в регистр нуля.)

так как вызывающий абонент, ваш вариант кода для хранения чего-то через вызов функции включает:

  • сохранить / перезагрузить его в локальную память стека. (Или просто перезагрузите его, если обновленная копия все еще находится в памяти).
  • сохранить / восстановить регистр с сохраненным вызовом в начале / конце всей функции и скопировать указатель на один из этих регистров перед вызовом функции.
  • функция возвращает значение в регистре для вас. (Конечно, это работает только в том случае, если источник C записан для использования возвращаемого значения вместо входной переменной. например,dst = strcpy(dst, src); если вы не вложенности).

все соглашения о вызовах на всех архитектурах я знаю о возвращаемых значениях размера указателя в регистре, поэтому, возможно, одна дополнительная инструкция в функции библиотеки может сохраните размер кода во всех абонентах, которые хотят использовать это возвращаемое значение.

вы, вероятно, получили лучший asm от примитивных ранних компиляторов C, используя возвращаемое значение strcpy (уже в регистре), чем заставляя компилятор сохранить указатель вокруг вызова в регистре, сохраненном вызовом, или разлить его в стек. Это все еще может быть так.

кстати, на многих ISAs регистр возвращаемого значения не является первым регистром ARG-передачи. И если вы не используете адресацию base+index режимы, это стоит дополнительной инструкции (и связать другой reg) для strcpy, чтобы скопировать регистр для цикла приращения указателя.

PDP-11 toolchains обычно используется какой-то стек-args, вызывающий соглашение, всегда нажимая args на стеке. Я не уверен, сколько регистров Call-preserved vs. call-clobbered были нормальными, но только 5 или 6 GP regs были доступны (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, затем inlines strcat as strlen + mov-немедленно.


в C-строку или не в C-строку

C строки неявной длины не всегда по сути плохо, и имеют интересные преимущества (например, суффикс также является допустимой строкой, без необходимости копировать он.)

но библиотека строк C не разработана таким образом, чтобы сделать возможным эффективный код, потому что -at-a-time обычно не векторизуются автоматически, а библиотечные функции отбрасывают результаты работы, которую они должны делать.

GCC и clang никогда не автоматически векторизуют циклы, если количество итераций не известно до первой итерации, например for(int i=0; i<n ;i++). ICC может векторизовать циклы поиска, но это все равно вряд ли будет так же хорошо, как написано от руки ассемблер.


strncpy и так далее-это катастрофа. например,strncpy не копирует завершающий '' если он достигает предела размера буфера. Похоже, он был предназначен для записи в середину больших строк,не для предотвращения переполнения буфера. Не возвращая указатель на конец, вы должны arr[n] = 0; до или после, потенциально касаясь страницы памяти, которая никогда не должна быть тронутый.

несколько функций, таких как snprintf можно использовать и всегда nul-terminate. Запоминание того, что делает то, что трудно, и огромный риск, если вы помните неправильно, поэтому вы должны проверять каждый раз в случаях, когда это имеет значение для правильности.

как говорит Брюс Доусон:прекратите использование функції strncpy уже!. Видимо, некоторые расширения индекса MSVC как _snprintf еще хуже.


та же концепция, что и Интерфейсы Свободно. Просто делает код быстрее/легче читать.


Я не думаю, что это действительно настроено таким образом для целей вложенности, но больше для проверки ошибок. Если память не обслуживает ни одну из стандартных библиотечных функций c, сделайте большую проверку ошибок самостоятельно, и поэтому имеет смысл, что это будет определять, если что-то пошло не так во время вызова strcpy.

if(strcpy(dest, source) == NULL) {
  // Something went horribly wrong, now we deal with it
}