strcpy()返回值

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

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

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

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

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

还有其他合理的理由certificate这个成语是正确的吗?

正如埃文所指出的那样,有可能做类似的事情

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

例如,为malloc()ed memory分配一个值,而不使用辅助变量。

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

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

它也非常容易编码。

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

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

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

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


C的标准库是很早就设计出来的,并且很容易认为str*函数没有经过优化设计。 I / O函数早就设计得很早,在1972年之前C甚至有一个预处理器,这就是为什么fopen(3)采用模式字符串而不是像Unix open(2)那样的标志位图 。

我无法找到Mike Lesk的“便携式I / O包”中包含的function列表,因此我不知道当前forms的strcpy是否可以追溯到那里,或者是否稍后添加了这些function。 (我发现的唯一真正的来源是Dennis Ritchie广为人知的C History文章 , 这篇文章非常出色但不深入。我没有找到任何实际I / O包本身的文档或源代码。)

它们确实以1978年K&R第一版的forms出现。


函数应返回它们执行的计算结果,如果它对调用者有用,而不是丢弃它 。 作为指向字符串结尾的指针,或整数长度。 (指针很自然。)

正如@R所说:

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

例如strcat(bigstr, newstr[i])在一个循环中调用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; length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length; ,但是sprintf()必须解析其格式字符串并且速度不快。

甚至没有strcmpmemcmp版本返回差异的位置 。 如果那就是你想要的,那么你遇到的问题就是为什么python中的字符串比较如此之快? :一个优化的库函数,运行速度比使用编译循环可以执行的任何操作都快(除非您为所关注的每个目标平台都有手动优化的asm),您可以使用它来接近不同的字节,然后再回退到一旦你接近,定期循环。

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

它们基本上将隐式长度字符串视为整个不透明对象,总是在搜索或追加后返回指向开头的指针,从不返回结尾或指向内部的位置。


历史猜测

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

事实上, K&R第一版(第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());bar(foo()); ,但它与gcc -O0 。 我不知道早期的编译器是否可以完全优化变量(不为它们保留堆栈空间),但希望它们至少可以在简单的情况下将它们保存在寄存器中(不像现代的gcc -O0 ,它会故意溢出/重新加载所有内容)一致的调试)。 即gcc -O0对于古代编译器来说不是一个好模型,因为它是为了一致的调试而进行反优化的


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

鉴于C字符串库的一般API设计缺乏对效率的关注,这可能不太可能。 但也许有代码大小的好处。 (在早期的计算机上,代码大小比CPU时间更严格)。

我不太了解早期C编译器的质量,但可以肯定的是,即使对于像PDP-11这样漂亮的简单/正交架构,它们在优化方面也不是很棒。

在函数调用之后需要字符串指针是很常见的。 在asm级别,您(编译器)可能在调用之前将它放在寄存器中。 根据调用约定,您可以将其推送到堆栈上,也可以将其复制到调用约定所示的第一个arg所在的右侧寄存器中。 (即strcpy期待的地方)。 或者,如果您正在提前计划,那么您已经将指针放在调用约定的右侧寄存器中。

但函数调用会破坏一些寄存器,包括所有arg传递寄存器。 (因此,当函数在寄存器中获取arg时,它可以在那里递增它而不是复制到临时寄存器。)

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

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

我知道所有体系结构上的所有调用约定都会在寄存器中返回指针大小的返回值,因此在库函数中可能有一条额外的指令可以在所有想要使用该返回值的调用者中保存代码大小。

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

顺便说一下,在许多ISA上,返回值寄存器不是第一个arg-passing寄存器。 除非你使用基本+索引寻址模式,否则它会花费额外的指令(并占用另一个寄存器)来使strcpy复制寄存器以获得指针增量循环。

PDP-11工具链通常使用某种stack-args调用约定 ,总是在堆栈上推动args。 我不确定有多少呼叫保留与呼叫被破坏的寄存器是正常的,但只有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的输入arg。 (参见Godbolt编译器浏览器 。)

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

但不幸的是, dst = strcpy(dst, src) -O3版本仍然使用常规strcpy ,然后将strcat联为strlen + mov -immediate。


要C-string还是不要C-string

C隐式长度字符串并不总是坏的,并且具有有趣的优点(例如,后缀也是一个有效的字符串,而不必复制它)。

但C字符串库的设计并不能使高效代码成为可能,因为char -at-a-time循环通常不会自动向量化,并且库函数会丢弃它们必须完成的工作结果。

gcc和clang从不自动向量化循环,除非迭代计数在第一次迭代之前已知,例如for(int i=0; i 。 ICC可以对搜索循环进行矢量化,但它仍然不太可能像手写的asm一样好。


strncpy等基本上都是一场灾难 。 例如,如果strncpy达到缓冲区大小限制,则不会复制终止'\0' 。 它似乎被设计用于写入较大字符串的中间, 而不是用于避免缓冲区溢出。 不返回指向结尾的指针意味着你必须arr[n] = 0; 之前或之后,可能触及永远不需要触摸的内存页面。

snprintf这样的一些函数是可用的,并且总是nul-terminate。 记住哪些是困难的,如果你记得错误则存在巨大的风险,所以你必须每次检查它对正确性的重要性。

正如布鲁斯道森所说: 已经停止使用strncpy了! 。 显然像_snprintf这样的一些MSVC扩展更糟糕。

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

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

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