繁体   English   中英

strcpy() 返回值

[英]strcpy() return value

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

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

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

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

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

是否有任何其他合理的理由来证明这个习语是合理的?

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

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

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

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

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 包”中包含的函数列表,所以我不知道当前形式的strcpy是否可以追溯到那里,或者这些函数是否是后来添加的. (我找到的唯一真正的来源是Dennis Ritchie 广为人知的 C History 文章这篇文章非常好,但没有那么深入。我没有找到实际 I/O 包本身的任何文档或源代码。)

它们确实以目前的形式出现在 1978 年的K&R 第一版中


函数应该返回它们所做的计算结果,如果它对调用者可能有用,而不是扔掉它 作为指向字符串末尾的指针,或整数长度。 (指针很自然。)

正如@R 所说:

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

例如strcat(bigstr, newstr[i])在循环中调用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; 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 时,它可以在那里增加它而不是复制到临时寄存器。)

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

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

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

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

顺便说一句,在许多 ISA 上,返回值寄存器不是第一个传递参数的寄存器。 除非您使用基址+索引寻址模式,否则 strcpy 确实需要额外的指令(并占用另一个 reg)来复制指针增量循环的寄存器。

PDP-11 工具链通常使用某种 stack-args 调用约定,总是将 args 压入堆栈。 我不确定有多少调用保留寄存器和调用破坏寄存器是正常的,但只有 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 =而是为strcat重用输入 arg 的版本要紧凑得多。 (请参阅Godbolt 编译器资源管理器中的两者。)

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

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


到 C 串或不到 C 串

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

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

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


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

一些像snprintf这样的函数是可用的,并且总是以空结尾。 记住哪个做哪个很难,如果你记错了,风险很大,所以你必须每次检查正确性。

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

我相信你的猜测是正确的,它更容易嵌套调用。

它也非常容易编码。

返回值通常保留在 AX 寄存器中(这不是强制性的,但情况经常如此)。 并且在函数启动时将目标放入 AX 寄存器。 要返回目的地,程序员需要做....什么都不做! 只需将值留在原处。

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

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

我不认为这真的是为了嵌套目的而设置的,而是更多用于错误检查。 如果内存没有任何作用,那么 c 标准库函数都不会自己做很多错误检查,因此更有意义的是,确定在 strcpy 调用期间是否出现问题。

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM