简体   繁体   English

strcpy() 返回值

[英]strcpy() return value

A lot of the functions from the standard C library, especially the ones for string manipulation, and most notably strcpy(), share the following prototype:标准 C 库中的许多函数,尤其是用于字符串操作的函数,尤其是 strcpy(),共享以下原型:

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

The return value of these functions is in fact the same as the provided destination .这些函数的返回值实际上与提供的destination相同。 Why would you waste the return value for something redundant?为什么要为多余的东西浪费返回值? It makes more sense for such a function to be void or return something useful.将这样的函数设为 void 或返回有用的东西更有意义。

My only guess as to why this is is that it's easier and more convenient to nest the function call in another expression, for example:对于为什么会这样,我唯一的猜测是将函数调用嵌套在另一个表达式中更容易、更方便,例如:

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

Are there any other sensible reasons to justify this idiom?是否有任何其他合理的理由来证明这个习语是合理的?

as Evan pointed out, it is possible to do something like正如埃文指出的那样,可以做类似的事情

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

eg assign malloc()ed memory a value, without using helper variable.例如,分配malloc()ed内存一个值,而不使用辅助变量。

(this example isn't the best one, it will crash on out of memory conditions, but the idea is obvious) (这个例子不是最好的,它会在内存不足的情况下崩溃,但这个想法很明显)

char *stpcpy(char *dest, const char *src); returns a pointer to the end of the string, and is part of POSIX.1-2008 .返回指向字符串末尾的指针,并且是 POSIX.1-2008 的一部分 Before that, it was a GNU libc extension since 1992. It first appeared in Lattice C AmigaDOS in 1986.在此之前,它是 1992 年以来的 GNU libc 扩展。它于 1986 年首次出现在 Lattice C AmigaDOS 中。

gcc -O3 will in some cases optimize strcpy + strcat to use stpcpy or strlen + inline copying, see below. gcc -O3在某些情况下会优化strcpy + strcat以使用stpcpystrlen + 内联复制,见下文。


C's standard library was designed very early, and it's very easy to argue that the str* functions are not optimally designed. C 的标准库很早就设计好了,很容易争辩说str*函数没有经过优化设计。 The I/O functions were definitely designed very early, in 1972 before C even had a preprocessor, which is why fopen(3) takes a mode string instead of a flag bitmap like Unix open(2) .在I / O功能进行了明确设计得很早,在1972年前的C甚至有一个预处理器,这是为什么fopen(3)需要一个模式字符串,而不是一个标志位类似Unix的open(2)

I haven't been able to find a list of functions included in Mike Lesk's "portable I/O package", so I don't know whether strcpy in its current form dates all the way back to there or if those functions were added later.我找不到 Mike Lesk 的“便携式 I/O 包”中包含的函数列表,所以我不知道当前形式的strcpy是否可以追溯到那里,或者这些函数是否是后来添加的. (The only real source I've found is Dennis Ritchie's widely-known C History article , which is excellent but not that in depth. I didn't find any documentation or source code for the actual I/O package itself.) (我找到的唯一真正的来源是Dennis Ritchie 广为人知的 C History 文章这篇文章非常好,但没有那么深入。我没有找到实际 I/O 包本身的任何文档或源代码。)

They do appear in their current form in K&R first edition , 1978.它们确实以目前的形式出现在 1978 年的K&R 第一版中


Functions should return the result of computation they do, if it's potentially useful to the caller, instead of throwing it away .函数应该返回它们所做的计算结果,如果它对调用者可能有用,而不是扔掉它 Either as a pointer to the end of the string, or an integer length.作为指向字符串末尾的指针,或整数长度。 (A pointer would be natural.) (指针很自然。)

As @R says:正如@R 所说:

We all wish these functions returned a pointer to the terminating null byte (which would reduce a lot of O(n) operations to O(1) )我们都希望这些函数返回一个指向终止空字节的指针(这会将很多O(n)操作减少到O(1)

eg calling strcat(bigstr, newstr[i]) in a loop to build up a long string from many short (O(1) length) strings has approximately O(n^2) complexity, but strlen / memcpy will only look at each character twice (once in strlen, once in memcpy).例如strcat(bigstr, newstr[i])在循环中调用strcat(bigstr, newstr[i])以从许多短(O(1) 长度)字符串构建一个长字符串具有大约O(n^2)复杂度,但strlen / memcpy只会查看每个字符两次(一次在 strlen 中,一次在 memcpy 中)。

Using only the ANSI C standard library, there's no way to efficiently only look at every character once .仅使用 ANSI C 标准库,无法有效地只查看每个字符一次 You could manually write a byte-at-a-time loop, but for strings longer than a few bytes, that's worse than looking at each character twice with current compilers (which won't auto-vectorize a search loop) on modern HW, given efficient libc-provided SIMD strlen and memcpy.您可以手动编写一个字节一次的循环,但对于长度超过几个字节的字符串,这比使用现代硬件上的当前编译器(不会自动矢量化搜索循环)两次查看每个字符更糟糕,给定高效的 libc 提供的 SIMD strlen 和 memcpy。 You could use length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length;您可以使用length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length; length = sprintf(bigstr, "%s", newstr[i]); bigstr+=length; , but sprintf() has to parse its format string and is not fast.sprintf()必须分析它的格式字符串和并不快。

There isn't even a version of strcmp or memcmp that returns the position of the difference .甚至没有一个版本的strcmpmemcmp返回差异的位置 If that's what you want, you have the same problem as Why is string comparison so fast in python?如果这就是您想要的,那么您会遇到与为什么 Python 中的字符串比较如此之快一样的问题 : an optimized library function that runs faster than anything you can do with a compiled loop (unless you have hand-optimized asm for every target platform you care about), which you can use to get close to the differing byte before falling back to a regular loop once you get close. : 一个优化的库函数,它的运行速度比编译循环所能做的任何事情都要快(除非你为你关心的每个目标平台手动优化了 asm),你可以用它来接近不同的字节,然后再回到一个一旦你接近,常规循环。

It seems that C's string library was designed without regard to the O(n) cost of any operation, not just finding the end of implicit-length strings, and strcpy 's behaviour is definitely not the only example.似乎 C 的字符串库的设计没有考虑任何操作的 O(n) 成本,而不仅仅是找到隐式长度字符串的结尾,而且strcpy的行为绝对不是唯一的例子。

They basically treat implicit-length strings as whole opaque objects, always returning pointers to the start, never to the end or to a position inside one after searching or appending.他们基本上将隐式长度字符串视为整个不透明对象,在搜索或追加后始终返回指向开头的指针,从不返回结尾或指向内部位置的指针。


History guesswork历史猜测

In early C on a PDP-11 , I suspect that strcpy was no more efficient than while(*dst++ = *src++) {} (and was probably implemented that way).在 PDP-11 上的早期 C 中,我怀疑strcpy效率并不比while(*dst++ = *src++) {} (并且可能是这样实现的)。

In fact, K&R first edition (page 101) shows that implementation of strcpy and says:事实上, K&R 第一版(第 101 页)显示了strcpy实现并说:

Although this may seem cryptic at first sight, the notational convenience is considerable, and the idiom should be mastered, if for no other reason than that you will see it frequently in C programs.虽然这乍一看似乎很神秘,但符号的便利性是相当大的,并且应该掌握这个习惯用法,如果不是因为其他原因,您会经常在 C 程序中看到它。

This implies they fully expected programmers to write their own loops in cases where you wanted the final value of dst or src .这意味着他们完全希望程序员在您想要dstsrc的最终值的情况下编写自己的循环 And thus maybe they didn't see a need to redesign the standard library API until it was too late to expose more useful APIs for hand-optimized asm library functions.因此,也许他们没有看到需要重新设计标准库 API,直到为手动优化的 asm 库函数公开更多有用的 API 为时已晚。


But does returning the original value of dst make any sense?但是返回dst的原始值有意义吗?

strcpy(dst, src) returning dst is analogous to x=y evaluating to the x . strcpy(dst, src)返回dst类似于x=y评估到x So it makes strcpy work like a string assignment operator.所以它使 strcpy 像字符串赋值运算符一样工作。

As other answers point out, this allows nesting, like foo( strcpy(buf,input) );正如其他答案指出的那样,这允许嵌套,例如foo( strcpy(buf,input) ); . . Early computers were very memory-constrained.早期的计算机内存非常有限。 Keeping your source code compact was common practice .保持源代码紧凑是常见的做法 Punch cards and slow terminals were probably a factor in this.打孔卡和慢速终端可能是其中的一个因素。 I don't know historical coding standards or style guides or what was considered too much to put on one line.我不知道历史编码标准或风格指南,也不知道什么东西被认为太多而不能放在一行中。

Crusty old compilers were also maybe a factor.生硬的旧编译器也可能是一个因素。 With modern optimizing compilers, char *tmp = foo();使用现代优化编译器, char *tmp = foo(); / bar(tmp); / bar(tmp); is no slower than bar(foo());不比bar(foo());bar(foo()); , but it is with gcc -O0 . ,但它与gcc -O0 I don't know if very early compilers could optimize variables away completely (not reserving stack space for them), but hopefully they could at least keep them in registers in simple cases (unlike modern gcc -O0 which on purpose spills/reloads everything for consistent debugging).我不知道非常早期的编译器是否可以完全优化变量(不为它们保留堆栈空间),但希望他们至少可以在简单的情况下将它们保存在寄存器中(不像现代gcc -O0故意溢出/重新加载所有内容)一致调试)。 ie gcc -O0 isn't a good model for ancient compilers, because it's anti-optimizing on purpose for consistent debugging.gcc -O0对于古代编译器来说不是一个好的模型,因为它是为了一致的调试而故意进行反优化的


Possible compiler-generated-asm motivation可能的编译器生成的 asm 动机

Given the lack of care about efficiency in the general API design of the C string library, this might be unlikely.鉴于在 C 字符串库的通用 API 设计中缺乏对效率的关注,这可能不太可能。 But perhaps there was a code-size benefit.但也许有代码大小的好处。 (On early computers, code-size was more of a hard limit than CPU time). (在早期的计算机上,代码大小比 CPU 时间更具有硬性限制)。

I don't know much about the quality of early C compilers, but it's a safe bet that they were not awesome at optimizing, even for a nice simple / orthogonal architecture like PDP-11.我不太了解早期 C 编译器的质量,但可以肯定的是,它们在优化方面并不出色,即使对于像 PDP-11 这样的简单/正交架构也是如此。

It's common to want the string pointer after the function call.在函数调用之后需要字符串指针是很常见的。 At an asm level, you (the compiler) probably has it in a register before the call.在 asm 级别,您(编译器)可能在调用之前将它放在寄存器中。 Depending on calling convention, you either push it on the stack or you copy it to the right register where the calling convention says the first arg goes.根据调用约定,您要么将其压入堆栈,要么将其复制到调用约定表示第一个 arg 所在的正确寄存器中。 (ie where strcpy is expecting it). (即strcpy期待它的地方)。 Or if you're planning ahead, you already had the pointer in the right register for the calling convention.或者,如果您提前计划,您已经在调用约定的正确寄存器中拥有指针。

But function calls clobber some registers, including all the arg-passing registers.但是函数调用会破坏一些寄存器,包括所有传递参数的寄存器。 (So when a function gets an arg in a register, it can increment it there instead of copying to a scratch register.) (因此,当函数在寄存器中获取 arg 时,它可以在那里增加它而不是复制到临时寄存器。)

So as the caller, your code-gen option for keeping something across a function call include:因此,作为调用者,用于在函数调用中保留某些内容的代码生成选项包括:

  • store/reload it to local stack memory.将其存储/重新加载到本地堆栈内存。 (Or just reload it if an up-to-date copy is still in memory). (或者,如果最新副本仍在内存中,则只需重新加载它)。
  • save/restore a call-preserved register at the start/end of your whole function, and copy the pointer to one of those registers before the function call.在整个函数的开始/结束时保存/恢复调用保留的寄存器,并在函数调用之前将指针复制到这些寄存器之一。
  • the function returns the value in a register for you.该函数为您返回寄存器中的值。 (Of course, this only works if the C source is written to use the return value instead of the input variable. eg dst = strcpy(dst, src); if you aren't nesting it). (当然,这仅在编写 C 源代码以使用返回值而不是输入变量时才有效。例如dst = strcpy(dst, src);如果您没有嵌套它)。

All calling conventions on all architectures I'm aware of return pointer-sized return values in a register, so having maybe one extra instruction in the library function can save code-size in all callers that want to use that return value.所有架构上的所有调用约定我都知道在寄存器中返回指针大小的返回值,因此在库函数中可能有一个额外的指令可以节省所有想要使用该返回值的调用者的代码大小。

You probably got better asm from primitive early C compilers by using the return value of strcpy (already in a register) than by making the compiler save the pointer around the call in a call-preserved register or spill it to the stack.通过使用strcpy的返回值(已经在寄存器中),您可能会从原始的早期 C 编译器中获得更好的 asm,而不是让编译器将调用周围的指针保存在调用保留的寄存器中或将其溢出到堆栈中。 This may still be the case.情况可能仍然如此。

BTW, on many ISAs, the return-value register is not the first arg-passing register.顺便说一句,在许多 ISA 上,返回值寄存器不是第一个传递参数的寄存器。 And unless you use base+index addressing modes, it does cost an extra instruction (and tie up another reg) for strcpy to copy the register for a pointer-increment loop.除非您使用基址+索引寻址模式,否则 strcpy 确实需要额外的指令(并占用另一个 reg)来复制指针增量循环的寄存器。

PDP-11 toolchains normally used some kind of stack-args calling convention , always pushing args on the stack. PDP-11 工具链通常使用某种 stack-args 调用约定,总是将 args 压入堆栈。 I'm not sure how many call-preserved vs. call-clobbered registers were normal, but only 5 or 6 GP regs were available ( R7 being the program counter, R6 being the stack pointer, R5 often used as a frame pointer ).我不确定有多少调用保留寄存器和调用破坏寄存器是正常的,但只有 5 或 6 个 GP 寄存器可用( R7 是程序计数器,R6 是堆栈指针,R5 通常用作帧指针)。 So it's similar to but even more cramped than 32-bit x86.因此它与 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

This is significantly more compact than a version which doesn't use dst = , and instead reuses the input arg for the strcat .这比不使用dst =而是为strcat重用输入 arg 的版本要紧凑得多。 (See both on the Godbolt compiler explorer .) (请参阅Godbolt 编译器资源管理器中的两者。)

The -O3 output is very different: gcc for the version that doesn't use the return value uses stpcpy (returns a pointer to the tail) and then mov -immediate to store the literal string data directly to the right place. -O3输出非常不同:不使用返回值的版本的 gcc 使用stpcpy (返回指向尾部的指针),然后mov -immediate 将文字字符串数据直接存储到正确的位置。

But unfortunately, the dst = strcpy(dst, src) -O3 version still uses regular strcpy , then inlines strcat as strlen + mov -immediate.但不幸的是, dst = strcpy(dst, src) -O3 版本仍然使用常规strcpy ,然后将strcat联为strlen + mov -immediate。


To C-string or not to C-string到 C 串或不到 C 串

C implicit-length strings aren't always inherently bad, and have interesting advantages (eg a suffix is also a valid string, without having to copy it). C 隐式长度的字符串并不总是天生不好,并且具有有趣的优点(例如,后缀也是有效的字符串,无需复制它)。

But the C string library is not designed in a way that makes efficient code possible, because char -at-a-time loops typically don't auto-vectorize and the library functions throw away results of work they have to do.但是 C 字符串库的设计方式并没有使高效的代码成为可能,因为每次char循环通常不会自动矢量化,并且库函数会丢弃它们必须执行的工作的结果。

gcc and clang never auto-vectorize loops unless the iteration count is known before the first iteration, eg for(int i=0; i<n ;i++) . gcc 和 clang 从不自动矢量化循环,除非在第一次迭代之前知道迭代计数,例如for(int i=0; i<n ;i++) ICC can vectorize search loops, but it's still unlikely to do as well as hand-written asm. ICC 可以矢量化搜索循环,但它仍然不太可能像手写 asm 那样好。


strncpy and so on are basically a disaster . strncpy等基本上都是灾难 eg strncpy doesn't copy the terminating '\\0' if it reaches the buffer size limit.例如,如果strncpy达到缓冲区大小限制,则它不会复制终止的'\\0' It appears to have been designed for writing into the middle of larger strings, not for avoiding buffer overflows.它似乎是为写入较大字符串的中间而设计的,而不是为了避免缓冲区溢出。 Not returning a pointer to the end means you have to arr[n] = 0;不返回指向末尾的指针意味着您必须arr[n] = 0; before or afterwards, potentially touching a page of memory that never needed to be touched.在之前或之后,可能会触及永远不需要触及的内存页面。

A few functions like snprintf are usable and do always nul-terminate.一些像snprintf这样的函数是可用的,并且总是以空结尾。 Remembering which does which is hard, and a huge risk if you remember wrong, so you have to check every time in cases where it matters for correctness.记住哪个做哪个很难,如果你记错了,风险很大,所以你必须每次检查正确性。

As Bruce Dawson says: Stop using strncpy already!正如布鲁斯道森所说: 停止使用 strncpy 了! . . Apparently some MSVC extensions like _snprintf are even worse.显然,像_snprintf这样的一些 MSVC 扩展甚至更糟。

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

Its also extremely easy to code.它也非常容易编码。

The return value is typically left in the AX register (it is not mandatory, but it is frequently the case).返回值通常保留在 AX 寄存器中(这不是强制性的,但情况经常如此)。 And the destination is put in the AX register when the function starts.并且在函数启动时将目标放入 AX 寄存器。 To return the destination, the programmer needs to do.... exactly nothing!要返回目的地,程序员需要做....什么都不做! Just leave the value where it is.只需将值留在原处。

The programmer could declare the function as void .程序员可以将该函数声明为void But that return value is already in the right spot, just waiting to be returned, and it doesn't even cost an extra instruction to return it!但是那个返回值已经在正确的位置,只是在等待返回,甚至不需要额外的指令来返回它! No matter how small the improvement, it is handy in some cases.无论改进有多小,在某些情况下它都很方便。

Same concept as Fluent Interfaces .Fluent Interfaces相同的概念。 Just making code quicker/easier to read.只是让代码更快/更容易阅读。

I don't think this is really set up this way for nesting purposes, but more for error checking.我不认为这真的是为了嵌套目的而设置的,而是更多用于错误检查。 If memory serves none of the c standard library functions do much error checking on their own and therefor it makes more sense that this would be to determine if something went awry during the strcpy call.如果内存没有任何作用,那么 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