strcpy () возвращаемое значение - PullRequest
25 голосов
/ 25 августа 2010

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

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

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

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

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

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

Ответы [ 6 ]

20 голосов
/ 25 августа 2010

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

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

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

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

5 голосов
/ 25 августа 2010

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

2 голосов
/ 27 июля 2018

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 году, еще до того, как в С даже появился препроцессор, а именно , почему fopen(3) принимает строку режима вместо битовой карты флага, такой как Unix open(2).

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

Они появляются в их текущем виде в K & R first edition , 1978.


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

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

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

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

Используя только стандартную библиотеку ANSI C, невозможно эффективно просматривать только каждый символ один раз .Вы можете вручную написать цикл byte-a-a-time, но для строк длиннее нескольких байтов это хуже, чем смотреть на каждый символ дважды с помощью текущих компиляторов (которые не будут автоматически векторизовывать цикл поиска) в современном 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 first edition (стр. 101) показывает, что реализация strcpy и говорит:

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

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


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

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-строк, это может быть маловероятным. Но, возможно, было преимущество размера кода. (На ранних компьютерах размер кода был более жестким ограничением, чем время процессора).

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

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

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

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

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

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

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

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

Набор инструментов PDP-11 обычноиспользовал какое-то соглашение о вызове стековых аргументов , всегда помещающее аргументы в стек.Я не уверен, сколько регистров с сохраненным вызовом и с закрытым вызовом было нормальным, но было доступно только 5 или 6 регистров GP ( 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 =, и вместо этого повторно используется входной аргумент для strcat.(См. Оба в проводнике компилятора Godbolt .)

Вывод -O3 очень отличается: gcc для версии, которая не использует возвращаемое значение, используетstpcpy (возвращает указатель на хвост), а затем mov - немедленно, чтобы сохранить данные строковых литералов прямо в нужном месте.

Но, к сожалению, версия dst = strcpy(dst, src) -O3 все еще использует обычные strcpy, затем встраивает strcat как strlen + mov -mmediate.


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

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

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

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


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

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

Как говорит Брюс Доусон: Прекратите использовать strncpy уже!.Очевидно, что некоторые расширения MSVC, такие как _snprintf, еще хуже.

1 голос
/ 25 августа 2010

Его также очень легко кодировать.

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

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

0 голосов
/ 29 февраля 2016

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

if(strcpy(dest, source) == NULL) {
  // Something went horribly wrong, now we deal with it
}
0 голосов
/ 25 августа 2010

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

...