簡體   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