Bla*_*iev 26 c function strcpy
标准C库中的许多函数,尤其是用于字符串操作的函数,最显着的是strcpy(),共享以下原型:
char *the_function (char *destination, ...)
Run Code Online (Sandbox Code Playgroud)
这些函数的返回值实际上与提供的相同destination.你为什么要浪费多余的回报价值呢?这样的函数无效或返回有用的东西更有意义.
我唯一的猜测是,为什么将函数调用嵌套在另一个表达式中更容易,更方便,例如:
printf("%s\n", strcpy(dst, src));
Run Code Online (Sandbox Code Playgroud)
还有其他合理的理由来证明这个成语吗?
not*_*row 22
正如埃文指出的那样,有可能做类似的事情
char* s = strcpy(malloc(10), "test");
Run Code Online (Sandbox Code Playgroud)
例如,为malloc()ed内存分配一个值,而不使用辅助变量.
(这个例子不是最好的,它会因内存不足而崩溃,但这个想法很明显)
Pet*_*des 14
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 History 文章,它非常出色但没有那么深入。我没有找到任何实际 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()必须分析它的格式字符串,是不是快。
甚至没有一个版本的strcmpormemcmp返回差异的位置。如果这就是您想要的,那么您会遇到与为什么 Python 中的字符串比较如此之快一样的问题?: 一个优化的库函数,它的运行速度比编译循环所能做的任何事情都要快(除非你为你关心的每个目标平台手动优化了 asm),你可以用它来接近不同的字节,然后再回到一个一旦你接近,常规循环。
似乎 C 的字符串库的设计没有考虑任何操作的 O(n) 成本,而不仅仅是找到隐式长度字符串的结尾,而且strcpy的行为绝对不是唯一的例子。
他们基本上将隐式长度字符串视为整个不透明对象,在搜索或追加后始终返回指向开头的指针,从不返回结尾或指向内部位置的指针。
在 PDP-11 上的早期 C 中,我怀疑这strcpy并不比while(*dst++ = *src++) {}(并且可能以这种方式实现)更有效。
事实上,K&R 第一版(第 101 页)显示了strcpy和 的实现:
虽然这乍一看似乎很神秘,但符号的便利性是相当大的,并且应该掌握这个习惯用法,如果不是因为其他原因,您会经常在 C 程序中看到它。
这意味着他们完全希望程序员在您想要dstor的最终值的情况下编写自己的循环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不是古代编译器的好模型,因为它是为了一致的调试而故意进行反优化的。
鉴于在 C 字符串库的通用 API 设计中缺乏对效率的关注,这可能不太可能。但也许有代码大小的好处。(在早期的计算机上,代码大小比 CPU 时间更具有硬性限制)。
我不太了解早期 C 编译器的质量,但可以肯定的是,它们在优化方面并不出色,即使对于像 PDP-11 这样的简单/正交架构也是如此。
在函数调用之后需要字符串指针是很常见的。在 asm 级别,您(编译器)可能在调用之前将它放在寄存器中。根据调用约定,您要么将其压入堆栈,要么将其复制到调用约定表示第一个 arg 所在的正确寄存器中。(即在哪里strcpy期待它)。或者,如果您提前计划,您已经在调用约定的正确寄存器中拥有指针。
但是函数调用会破坏一些寄存器,包括所有传递参数的寄存器。(因此,当函数在寄存器中获取 arg 时,它可以在那里增加它而不是复制到临时寄存器。)
因此,作为调用者,用于在函数调用中保留某些内容的代码生成选项包括:
dst = strcpy(dst, src);如果您没有嵌套它)。所有架构上的所有调用约定我都知道在寄存器中返回指针大小的返回值,因此在库函数中可能有一个额外的指令可以节省所有想要使用该返回值的调用者的代码大小。
通过使用strcpy(已经在寄存器中)的返回值,您可能会从原始的早期 C 编译器中获得更好的 asm,而不是让编译器将调用周围的指针保存在调用保留的寄存器中或将其溢出到堆栈中。情况可能仍然如此。
顺便说一句,在许多 ISA 上,返回值寄存器不是第一个传递参数的寄存器。除非您使用基址+索引寻址模式,否则 strcpy 确实需要额外的指令(并占用另一个 reg)来复制指针增量循环的寄存器。
PDP-11 工具链通常使用某种 stack-args 调用约定,总是将 args 压入堆栈。我不确定有多少 call-preserved vs. call-clobbered 寄存器是正常的,但只有 5 或 6 个 GP regs 可用(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
Run Code Online (Sandbox Code Playgroud)
这比不使用 的版本要紧凑得多dst =,而是为strcat. (请参阅Godbolt 编译器资源管理器中的两者。)
该-O3输出有很大的不同:对不使用的返回值使用的版本的GCC stpcpy(返回一个指针到尾),然后mov-immediate字面字符串数据直接存储到正确的位置。
但不幸的是,dst = strcpy(dst, src)-O3 版本仍然使用常规strcpy,然后内联strcat为strlen+ mov-immediate。
C 隐式长度的字符串并不总是天生不好,并且具有有趣的优点(例如,后缀也是有效的字符串,无需复制它)。
但是 C 字符串库的设计方式并没有使高效的代码成为可能,因为char每次循环通常不会自动矢量化,并且库函数会丢弃它们必须执行的工作的结果。
gcc 和 clang 从不自动矢量化循环,除非在第一次迭代之前就知道迭代次数,例如for(int i=0; i<n ;i++). ICC 可以矢量化搜索循环,但它仍然不太可能像手写 asm 那样好。
strncpy等等基本上都是灾难。例如strncpy,'\0'如果达到缓冲区大小限制,则不会复制终止。它似乎是为写入较大字符串的中间而设计的,而不是为了避免缓冲区溢出。不返回指向末尾的指针意味着您必须在arr[n] = 0;之前或之后返回,可能会触及永远不需要触及的内存页面。
一些函数像是snprintf可用的并且总是空终止。记住哪个做哪个很难,如果你记错了,风险很大,所以你必须每次检查正确性。
正如布鲁斯道森所说:停止使用 strncpy 了!. 显然,一些 MSVC 扩展_snprintf更糟糕。
| 归档时间: |
|
| 查看次数: |
15934 次 |
| 最近记录: |