char *stpcpy(char *dest, const char *src);
返回指向字符串末尾的指针,并且是 POSIX.1-2008 的一部分。在此之前,它是 1992 年以来的 GNU libc 扩展。它于 1986 年首次出现在 Lattice C AmigaDOS 中。
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 包本身的任何文档或源代码。)
它们确实以目前的形式出现在1978 年的K&R 第一版中。
函数应该返回它们所做的计算结果,如果它可能对调用者有用,而不是把它扔掉。作为指向字符串末尾的指针,或整数长度。(指针很自然。)
正如@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
的ormemcmp
的版本。如果这就是您想要的,那么您遇到的问题与为什么 python 中的字符串比较如此之快?: 一个优化的库函数,运行速度比使用编译循环执行的任何操作都快(除非您为您关心的每个目标平台手动优化 asm),您可以使用它来接近不同的字节,然后再回退到一个一旦你接近,定期循环。
似乎 C 的字符串库的设计没有考虑任何操作的 O(n) 成本,而不仅仅是找到隐式长度字符串的结尾,而且strcpy
' 的行为绝对不是唯一的例子。
它们基本上将隐式长度字符串视为整个不透明对象,始终返回指向开头的指针,从不返回结尾或在搜索或追加后返回到一个内部的位置。
历史猜测
在 PDP-11 的早期 C 中,我怀疑这strcpy
并不比while(*dst++ = *src++) {}
(并且可能以这种方式实现)更有效。
事实上,K&R 第一版(第 101 页)显示了该实现strcpy
并说:
尽管乍一看这似乎很神秘,但符号的便利性是相当可观的,并且应该掌握这个习语,如果没有其他原因,你会在 C 程序中经常看到它。
这意味着他们完全期望程序员在您想要or的最终值的情况下编写自己的循环dst
src
。因此,也许他们认为没有必要重新设计标准库 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
故意溢出/重新加载所有内容以进行一致的调试) . iegcc -O0
对于古代编译器来说不是一个好的模型,因为它是为了一致的调试而故意反优化的。
可能的编译器生成的 asm 动机
鉴于 C 字符串库的通用 API 设计缺乏对效率的关注,这可能不太可能。但也许有代码大小的好处。(在早期的计算机上,代码大小比 CPU 时间更受硬限制)。
我对早期 C 编译器的质量了解不多,但可以肯定的是,它们在优化方面并不出色,即使对于像 PDP-11 这样的简单/正交架构也是如此。
在函数调用之后需要字符串指针是很常见的。在 asm 级别,您(编译器)可能在调用之前将它放在寄存器中。根据调用约定,您可以将其推送到堆栈上,也可以将其复制到调用约定说第一个 arg 所在的正确寄存器。(即在哪里strcpy
期待它)。或者,如果您提前计划,您已经在正确的寄存器中为调用约定找到了指针。
但是函数调用会破坏一些寄存器,包括所有传递参数的寄存器。(因此,当函数在寄存器中获取 arg 时,它可以在那里增加它而不是复制到临时寄存器。)
因此,作为调用者,您在函数调用中保留某些内容的代码生成选项包括:
- 将其存储/重新加载到本地堆栈内存。(或者如果最新的副本仍在内存中,则只需重新加载它)。
- 在整个函数的开始/结束处保存/恢复调用保留寄存器,并在函数调用之前将指针复制到这些寄存器之一。
- 该函数为您返回寄存器中的值。(当然,这仅在 C 源代码被编写为使用返回值而不是输入变量时才有效。例如
dst = strcpy(dst, src);
,如果您不嵌套它)。
所有架构上的所有调用约定我都知道在寄存器中返回指针大小的返回值,因此库函数中可能有一条额外的指令可以节省所有想要使用该返回值的调用者的代码大小。
通过使用(已经在寄存器中)的返回值,您可能会从原始早期 C 编译器中获得更好的 asm,而strcpy
不是让编译器将调用周围的指针保存在调用保留寄存器中或将其溢出到堆栈中。情况可能仍然如此。
顺便说一句,在许多 ISA 上,返回值寄存器不是第一个 arg 传递寄存器。除非您使用基址 + 索引寻址模式,否则 strcpy 需要额外的指令(并占用另一个 reg)来复制指针增量循环的寄存器。
PDP-11 工具链通常使用某种堆栈参数调用约定,总是将参数压入堆栈。我不确定有多少调用保留寄存器和调用破坏寄存器是正常的,但只有 5 或 6 个 GP 寄存器可用(R7 是程序计数器,R6 是堆栈指针,R5 经常用作帧指针)。所以它类似于 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
. (参见Godbolt 编译器资源管理器。)
-O3
输出非常不同:不使用返回值的版本使用 gcc (stpcpy
返回指向尾部的指针),然后mov
-immediate 将文字字符串数据直接存储到正确的位置。
但不幸的是,dst = strcpy(dst, src)
-O3 版本仍然使用常规strcpy
,然后内联strcat
为strlen
+ mov
-immediate。
到 C-string 或不到 C-string
C 隐式长度字符串并不总是天生不好,并且具有有趣的优点(例如,后缀也是有效的字符串,无需复制它)。
但是 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
更糟糕。