char *stpcpy(char *dest, const char *src);
返回指向字符串结尾的指针,是POSIX.1-2008的一部分。在此之前,它是自1992年以来的GNU libc扩展。它最初出现在Lattice C AmigaDOS中,时间为1986年。
gcc -O3
在某些情况下会优化 strcpy
+ strcat
以使用 stpcpy
或 strlen
+ 内联复制,请参见下文。
C的标准库设计非常早期,很容易争论str*
函数并不是最优秀的设计。I/O函数在1972年C甚至没有预处理器之前就被设计出来了,这就是为什么fopen(3)
采用模式字符串而不是像Unix的open(2)
一样采用标志位图。
我找不到Mike Lesk的“可移植I/O包”中包含的函数列表,因此我不知道strcpy
是否以其当前形式一直存在于那里,或者这些函数是后来添加的。(我找到的唯一真正的来源是Dennis Ritchie广为人知的C历史文章,它非常好,但不是那么深入。我没有找到有关实际I/O包本身的文档或源代码。)
它们以其当前形式出现在K&R第一版,1978年。
函数应该返回它们所做的计算结果,如果对调用者有潜在用处,而不是将其丢弃。可以返回指向字符串末尾的指针或整数长度(指针更自然)。
正如@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()
必须解析其格式字符串,并且速度不快。
甚至没有返回差异位置的 strcmp
或 memcmp
版本。如果这是你想要的,你面临着和 Why is string comparison so fast in python? 一样的问题:优化的库函数运行速度比你用编译循环做的任何事情都要快(除非你为每个目标平台都有经过手动优化的汇编代码),你可以使用它接近差异字节,然后在接近后退回到正常循环。
似乎 C 的字符串库在设计时没有考虑到任何操作的 O(n) 成本,不仅是查找隐式长度字符串的结尾,而且 strcpy
的行为绝对不是唯一的例子。
它们基本上将隐式长度字符串视为整个不透明对象,始终返回指向开头的指针,从未返回指向结尾或搜索或附加后的位置。
历史猜测
在 PDP-11 上的早期 C 语言中,我怀疑 strcpy
的效率不会比 while(*dst++ = *src++) {}
更高(并且可能是这样实现的)。
事实上,K&R 第一版(第101页) 展示了 strcpy
的实现,并表示:
虽然这乍一看似乎有点神秘,但它确实非常方便,而且这种用法应该掌握,即使出于其他原因,你也会经常在 C 程序中看到它。
这意味着他们完全希望程序员在需要 dst
或 src
的最终值的情况下编写自己的循环。因此,也许他们没有看到需要重新设计标准库 API 的必要性,直到为手动优化的汇编库函数暴露更有用的 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
不是古老编译器的好模型,因为它故意进行反优化以便进行一致的调试。
可能是编译器生成汇编代码的动机
考虑到 C 字符串库的通用 API 设计缺乏效率方面的关注,这种可能性不大。但也许有一些代码大小上的好处。(在早期计算机上,代码大小更多地是一个硬性限制,而不是 CPU 时间)。
我对早期 C 编译器的质量了解不多,但可以肯定的是,它们在优化方面并不出色,即使是像 PDP-11 这样简单 / 正交的架构。
通常希望在函数调用后获得字符串指针。在汇编级别上,编译器可能已经将其存储在寄存器中。根据调用约定,您可以将其推送到堆栈上或将其复制到正确的寄存器中,该寄存器是调用约定规定的第一个参数所在的位置(即 strcpy
所期望的位置)。或者,如果您提前计划,您已经将指针存储在符合调用约定的正确寄存器中。
但是函数调用会破坏一些寄存器,包括所有传递参数的寄存器。(因此,当函数在寄存器中获取参数时,它可以在那里递增,而不是复制到临时寄存器中。)
因此,作为调用者,在函数调用中保留某些内容的代码生成选项包括:
- 将其存储/重新加载到本地堆栈内存中(如果仍在内存中,则只需重新加载)。
- 在整个函数的开头/结尾保存/恢复调用保留的寄存器,并在函数调用之前将指针复制到这些寄存器中的一个。
- 该函数会为您在一个寄存器中返回值。 (当然,这仅适用于C源编写成使用返回值而不是输入变量的情况。例如,如果您没有将其嵌套,则
dst = strcpy(dst,src);
)。
我所知道的所有体系结构上的所有调用约定都会在寄存器中返回指针大小的返回值,因此库函数中可能有一个额外的指令可以节省所有想要使用该返回值的调用者的代码大小。
通过使用 strcpy
的返回值(已经在寄存器中)可能会从早期原始C编译器获得更好的汇编代码,而不是让编译器将指针保存在调用保留的寄存器中或将其溢出到堆栈中。 这种情况可能仍然存在。
顺便说一句,在许多 ISA 中,返回值寄存器不是第一个参数传递寄存器。除非您使用基址+索引寻址模式,否则执行strcpy操作时需要额外一个指令(并占用另一个寄存器)来复制指针增量循环的寄存器。
PDP-11工具链通常使用某种堆栈参数调用约定,始终将参数推入堆栈。我不确定正常情况下保留调用与破坏调用寄存器的数量有多少,但只有5或6个GP寄存器可用({{link2: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
}
# 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
-immediate 直接将字面字符串数据存储到正确的位置。
但不幸的是,dst = strcpy(dst, src)
的 -O3 版本仍然使用常规的 strcpy
,然后将 strcat
内联为 strlen
+ mov
-immediate。
使用C字符串还是不使用C字符串
C语言隐式长度字符串并非总是本质上不好,而且具有一些有趣的优点(例如,后缀也是一个有效的字符串,无需复制它)。
但是,C字符串库的设计方式并不利于编写高效的代码,因为逐个字符循环通常无法自动向量化,并且库函数会丢弃它们必须执行的工作结果。
除非在第一次迭代之前已知迭代次数(例如,for(int i=0; i<n ;i++)
),否则gcc和clang永远不会自动向量化循环。ICC可以向量化搜索循环,但仍然不太可能像手写汇编代码那样做得好。
strncpy
等函数基本上是灾难性的。例如,如果达到缓冲区大小限制,strncpy
不会复制终止符'\0'
,因此您需要在之前或之后手动添加arr[n] = 0;
。但是,如果源字符串较短,则填充0
字节以达到指定长度,可能会触及从未被触及的内存页。(这也使得将短字符串复制到仍有大量空间的大缓冲区中非常低效。)
它似乎是为了写入更大字符串的中间而设计的,而不是为了避免缓冲区溢出。
一些函数如snprintf
可用并始终以空字符结尾。记住哪个函数做什么很困难,如果记错了,风险很大,因此在涉及正确性的情况下,您必须每次都进行检查。
正如Bruce Dawson所说:别再使用strncpy了!。显然,一些MSVC扩展如_snprintf
甚至更糟。
strncat
也存在于POSIX.2001中,与strcpy
无关;它实现了你所期望的功能,即一个带边界检查的strcpy
,总是以0结尾。但像strcat
一样,它仍然返回原始指针,因此不适用于将字符串有效地追加到缓冲区中;如果你在同一缓冲区上重复调用它,它必须每次重新扫描前面的部分以找到当前的结尾。手册中提到了“Shlemiel the painter”。
stpcpy(3)
,它与strcpy(3)
相同,但返回指向空字符终止符的指针。 - alx - recommends codidact