strcpy()返回值

时间:2010-08-24 22:07:56

标签: c function strcpy

标准C库中的许多函数,尤其是用于字符串操作的函数,最着名的是strcpy(),共享以下原型:

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

这些函数的返回值实际上与提供的destination相同。你为什么要浪费多余的回报价值呢?这样的函数无效或返回有用的东西更有意义。

我唯一的猜测是,为什么将函数调用嵌套在另一个表达式中更容易,更方便,例如:

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

还有其他合理的理由来证明这个成语吗?

6 个答案:

答案 0 :(得分:20)

正如埃文指出的那样,可以做类似

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

e.g。为malloc()ed内存赋值,而不使用辅助变量。

(这个例子不是最好的,它会因内存不足而崩溃,但这个想法很明显)

答案 1 :(得分:5)

我相信你的猜测是正确的,它可以让你更容易嵌入电话。

答案 2 :(得分:2)

char *stpcpy(char *dest, const char *src);返回指向字符串 end 的指针,并且是POSIX.1-2008 的一部分。在此之前,它是自1992年以来的GNU libc扩展。如果在1986年首次出现在Lattice C AmigaDOS中。

gcc -O3在某些情况下将优化strcpy + strcat以使用stpcpystrlen +内联复制,请参见下文。


C的标准库是很早设计的,很容易争论str*函数的设计不是最优的。绝对早在1972年C甚至还没有一个预处理器why fopen(3) takes a mode string instead of a flag bitmap like Unix open(2)的情况下,就非常设计了I / O函数。

我无法找到Mike Lesk的“便携式I / O包”中包含的功能列表,所以我不知道当前形式的strcpy是否一直追溯到现在或者以后添加了这些功能。 (我发现的唯一真实的源代码是Dennis Ritchie's widely-known C History article,它非常好,但是深度上还不尽人意。)我没有找到任何有关实际I / O包本身的文档或源代码。 )

它们确实以1978年K&R first edition的当前形式出现。


如果函数可能对调用方有用,则函数应返回其执行的计算结果,而不是将其丢弃。可以作为指向字符串末尾的指针,也可以是整数长度。 (指针是自然的。)

@R说:

  

我们都希望这些函数返回一个指向终止空字节的指针(这会将许多O(n)操作减少到O(1)

例如循环调用strcat(bigstr, newstr[i])以从许多短(O(1)长度)字符串中构建一个长字符串大约具有O(n^2)的复杂度,但是strlen / memcpy只会看起来每个字符两次(一次在strlen中,一次在memcpy中)。

仅使用ANSI C标准库,无法高效地仅一次查看每个字符。您可以手动编写一个字节的循环,但是对于长度超过几个字节的字符串,这比在现代硬件上使用当前的编译器(不会自动向量化搜索循环)两次查看每个字符要糟糕得多,给定了由libc提供的高效SIMD strlen和memcpy。您可以使用length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length;,但是sprintf()必须解析其格式字符串,并且不是快速。

甚至没有strcmpmemcmp的版本返回差异的位置 。如果这是您想要的,您将遇到与Why is string comparison so fast in python?相同的问题:一种优化的库函数,其运行速度比您使用编译循环所能完成的任何事情都要快(除非您为自己关心的每个目标平台都进行了手工优化的asm) ,您可以使用它来接近不同的字节,然后在接近后回到常规循环。

似乎C的字符串库的设计没有考虑任何操作的O(n)开销,而不仅仅是查找隐式长度字符串的结尾,并且strcpy的行为绝对不是唯一的例子。

基本上,它们将隐式长度的字符串视为整个不透明的对象,在搜索或追加之后,始终将指针返回到起点,从不返回终点或指向指针内部的位置。


历史猜测

在C早期的PDP-11 上,我怀疑strcpy的效率不及while(*dst++ = *src++) {}(并且可能以这种方式实现)。

实际上,K&R first edition (page 101)显示了strcpy的实现,并说:

  

尽管乍看之下这似乎有些晦涩难懂,但符号上的便利性却相当可观,如果不是出于其他原因(您会经常在C程序中看到它),就应该熟练掌握这种习惯用法。

这意味着在您希望最终值dstsrc 的情况下,他们完全希望程序员编写自己的循环。因此,也许他们没有必要重新设计标准库API,直到为时已晚,无法为手动优化的asm库功能提供更多有用的API。


但是返回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对于古代编译器来说不是一个很好的模型,因为它反优化是为了进行一致的调试。


可能的编译器生成的asm动机

鉴于在C字符串库的常规API设计中对效率缺乏关注,这可能不太可能。但是也许有代码大小的好处。 (在早期的计算机上,代码大小比CPU时间更多是硬性限制。

我对早期C编译器的质量了解不多,但是可以肯定的是,即使对于像PDP-11这样的简单/正交体系结构,它们也不擅长优化。

通常在函数调用之后需要字符串指针 。在asm级别,您(编译器)可能在调用之前将其保存在寄存器中。根据调用约定,您可以将其压入堆栈,也可以将其复制到正确的寄存器中,在该寄存器中调用约定表示第一个arg进入。 (即strcpy所期望的位置)。或者,如果您正在计划,那么您已经在调用约定的正确寄存器中有了指针。

但是函数会调用Clobber的某些寄存器,包括所有传递arg的寄存器。 (因此,当函数在寄存器中获取arg时,可以在其中递增该参数,而不必复制到暂存寄存器中。)

因此,作为调用方,用于在函数调用中保留某些内容的代码生成选项包括:

  • 将其存储/重新加载到本地堆栈内存。 (或者如果最新副本仍在内存中,则重新加载它。)
  • 在整个函数的开始/结尾处保存/恢复保留调用的寄存器,并在函数调用之前将指针复制到这些寄存器之一。
  • 该函数为您返回寄存器中的值。 (当然,这仅在编写C源代码以使用输入变量的返回值 的情况下才有效。例如,dst = strcpy(dst, src);,如果您不嵌套它)。

所有架构上的所有调用约定我都知道寄存器中的返回指针大小的返回值,因此在库函数中添加一条额外的指令可以节省所有希望使用该返回值的调用者的代码大小。

通过使用strcpy(已经在寄存器中)的返回值,您可能会从原始的早期C编译器获得更好的asm,而不是使编译器将调用的指针保存在调用保留的寄存器中或将其溢出到堆栈。情况仍然如此。

顺便说一句,在许多ISA上,返回值寄存器不是第一个arg传递寄存器。而且,除非您使用base + index寻址模式,否则它确实会花费额外的指令(并占用另一个reg)供strcpy复制指针递增循环的寄存器。

PDP-11工具链normally used some kind of stack-args calling convention,始终将args推入堆栈。我不确定正常情况下有多少个保留呼叫和保留呼叫寄存器,但是只有5或6个GP寄存器可用(R7 being the program counter, R6 being the stack pointer, R5 often used as a frame pointer)。因此,它类似于32位x86,但比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。 (请参见on the Godbolt compiler explorer。)

-O3的输出是非常不同的:不使用返回值的版本的gcc使用stpcpy(返回指向尾部的指针),然后mov-立即转换为将文字字符串数据直接存储到正确的位置。

但是不幸的是,dst = strcpy(dst, src) -O3版本仍然使用常规的strcpy,然后以strcat + strlen的形式内联mov


是C字串还是不是C字串

C隐式长度的字符串并不总是固有的错误,并且具有有趣的优点(例如,后缀也是有效的字符串,无需复制)。

但是C字符串库的设计方式并没有使高效代码成为可能,因为一次char循环通常不会自动矢量化,并且库函数会丢弃它们的工作结果必须做。

gcc和clang永远不会自动向量化循环,除非在第一次迭代之前就知道迭代计数,例如for(int i=0; i<n ;i++)。 ICC可以对搜索循环进行矢量化处理,但仍不如手写的asm来做。


strncpy等基本上是一场灾难。例如如果strncpy达到缓冲区大小限制,则不会复制终止的'\0'。它似乎是为写入较大字符串的中间而设计的,而 not 则是为了避免缓冲区溢出。不返回末尾的指针意味着您必须在arr[n] = 0;之前或之后进行操作,这有可能会触及一个不需要触及的内存页面。

一些功能,例如snprintf可用,并且总是以n终止。记住哪一项很难做到,如果记住错了,将带来巨大的风险,因此,在每次检查正确性的情况下,您都必须进行检查。

正如布鲁斯·道森(Bruce Dawson)所说:Stop using strncpy already!。显然,某些_snprintf之类的MSVC扩展甚至更糟。

答案 3 :(得分:1)

它也非常容易编码。

返回值通常保留在AX寄存器中(它不是强制性的,但通常是这种情况)。当函数启动时,目标位于AX寄存器中。 要返回目的地,程序员需要做....完全没有!只需将值保留在原来的位置即可。

程序员可以将函数声明为void。但是那个返回值已经在正确的位置,只是等待返回,它甚至不需要额外的指令来返回它!无论改进多么小,在某些情况下都很方便。

答案 4 :(得分:0)

Fluent Interfaces相同的概念。只需使代码更快/更容易阅读。

答案 5 :(得分:0)

我认为这不是为嵌套目的而设置的,而是更多用于错误检查。如果内存没有提供c标准库函数,那么就自己进行大量的错误检查,因此更有意义的是确定在strcpy调用期间是否出现了错误。

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