C語言strcpy,strncpy和strlcpy講解


前言

C風格的字符串處理函數有很多,如strcpy()、strcat()等等。

strcpy與strcat

char* strcpy (char* dest, const char* src); char* strcat (char* dest, const char* src);

strcpy將'src'中的字符串按字符拷貝到'dest'中,遇到'0x00'時不拷貝此字符並結束函數,返回"dest"地址。

當"sizeof(dest) > sizeof(src)"時,'src'能成功復制到'dest'中;反之會出現緩沖區溢出的問題,如下代碼:

 char dest[5]; memset(dest, 0x0, sizeof(dest)); char src[10] = "012345678"; strcpy(dest, src); //拷貝大於dest數組大小的長度
 printf("%s\n", dest);

輸出結果:“012345678”。我們可以用 "dest[5] = '\0';" 來截斷數組,輸出正確結果,但是接下來程序會發生未定義行為——

  1. 如果上述代碼是被調用函數,且恰巧當前函數棧中位於‘dest’數組最后元素之后的四字節地址記錄了上一個函數棧的棧底指針,那么這部分地址信息會被‘src’后面的數據覆蓋寫,最后彈出棧的不是正確的記錄棧底指針的信息,返回的函數棧的棧底位置會是'0x38373635(主機是小端字節序)'。如果此時用戶棧訪問的是內核信息所在內存地址,程序將會崩潰。

  2. 另外如果被調用函數的返回地址被修改成無權訪問的地址,CPU訪問這個地址取指令時程序也會崩潰。這就是棧溢出的嚴重后果。

  3. 如果沒有覆蓋棧底指針和返回地址,CPU正常把控制權返回給調用者繼續執行。

  由此當我們使用strcpy()函數時,必須明確’src‘的大小可控,不可來自用戶輸入。或者使用動態內存給’dest‘分配大小,來防止緩沖區溢出。

strcat將’src‘按字符追加寫到’dest’后面,遇到'0x00'時不拷貝此字符並結束函數,返回dest地址。它同樣存在緩沖區溢出問題。

strncpy與strncat

為了防止緩沖區溢出,就需要程序員能夠控制拷貝的字節數,可以使用strncpy()和strncat():

char* strncpy (char* dest, const char* src, size_t copySize); char* strncat (char* dest, const char* src, size_t copySize);

strncpy將從‘src’按字符拷貝‘copySize’個字符到‘dest’,如果中途遇到‘0x00’不拷貝此字符並提前結束函數,返回‘dest’地址。

strncpy源代碼:

char * strncpy(char *dest, const char *src, size_t copySize) { size_t size = __strnlen(src, copySize); if (size != copySize) memset(dest + size, '\0', copySize - size); return memcpy(dest, src, size); }

其中__strnlen()也是'GNU libc'里的庫函數——返回'strlen(src)'和'copySize'的最小值(百度’GNU libc‘可以自行下載它的庫函數實現源代碼)。不同編譯器的實現略有不同,但是功能是相同的。strncpy的實現不考慮‘dest’緩沖區的長度,所以在函數體中它只是比較‘src’長度和‘copySize’大小,取二者最小值的長度拷貝至‘dest’中。它的特點是如果拷貝至‘dest’的數據沒有‘0x00’字符,則不會在‘dest’末尾添加結束符,需要程序員手動分配。另外程序員需要根據拷貝至‘dest’字符串的大小和‘src’長度進行比較,來判斷是否發生字符串截斷。以上是使用此函數時需要程序員做的工作。它的優點是拷貝字符數可供程序員選擇,缺點是可能造成緩沖區溢出和需要手動處理截斷的問題。為防止緩沖區溢出,一般做法是將‘copySize’置為‘dest’的長度。

strncat源代碼:

char * strncat(char *dest, const char *src, size_t copySize) { char *s = dest; /* Find the end of dest. */ dest += strlen (dest); size_t ss = __strnlen (src, copySize); dest[ss] = '\0'; memcpy (dest, src, ss); return s; }

 

strncat會覆寫’dest‘的首個’0x00‘字符,將’src‘按字符拷貝追加寫到’dest‘后面,如果中途遇到’0x00‘或者’copySize‘個字符拷貝完畢,在’dest‘末尾添加結束符最后函數結束,返回’dest‘地址。為防止緩沖區溢出,一般做法是將’copySize‘置為’dest‘剩余空間大小。

strlcpy和strlcat

 為了減輕程序員添加結束符和處理字符串截斷的負擔,可以使用strlcpy和strlcat函數。

strlcpy源代碼:

size_t strlcpy(char *dst, const char *src, size_t dsize) { const char *osrc = src; size_t nleft = dsize; /* Copy as many bytes as will fit. */
    if (nleft != 0) { while (--nleft != 0) { if ((*dst++ = *src++) == '\0') break; } } /* Not enough room in dst, add NUL and traverse rest of src. */
    if (nleft == 0) { if (dsize != 0) *dst = '\0';        /* NUL-terminate dst */
        while (*src++) ; } return(src - osrc - 1);    /* count does not include NUL */ }

 strlcpy將‘src’按字符拷貝到‘dst’中,最多拷貝(dszie-1)個字符,拷貝結束后在‘dst’末尾添加'0x00'結束符,返回值是‘src’的長度。一般將‘dsize’置為‘dst’的大小。相較於strncpy,strlcpy有兩個優點:(1)當strlen(src)大於等於‘dsize’時自動在‘dst’末尾添加結束符;(2)返回值大於等於‘dsize’時確定發生字符串截斷。以上兩點幫助程序員進行判斷,方便后續處理。

strlcat源代碼:

size_t strlcat(char *dst, const char *src, size_t dsize) { const char *odst = dst; const char *osrc = src; size_t n = dsize; size_t dlen; /* Find the end of dst and adjust bytes left but don't go past end. */
    while (n-- != 0 && *dst != '\0') dst++; dlen = dst - odst; n = dsize - dlen; if (n-- == 0) return(dlen + strlen(src)); while (*src != '\0') { if (n != 0) { *dst++ = *src; n--; } src++; } *dst = '\0'; return(dlen + (src - osrc));    /* count does not include NUL */ }

其中,‘dlen‘是’dst‘中原有字符串的長度(不包含’0x00‘結束符);’dsize‘是’dst‘緩沖區的總大小(包含結束符)。 它首先找到’dst‘中源字符串的結束符,然后計算剩余空間大小,如果為0,則返回(這里不懂’n-- == 0‘的判斷,當’dst‘的長度和參數'dsize'的大小一樣時’n‘才等於0,不過這種情況只有在程序員沒有分配好‘dst’緩沖區大小和‘dst’內原字符串的大小才會發生的吧?如果小伙伴看出點門道,務必評論指點迷津)。如果有剩余空間,就依次往其中添加‘src’字符串,直至‘dst’緩沖區填滿,返回值是‘dst’原字符串長度與‘src’長度之和。相比較strncat,strlcat的優點是:(1)第三個參數大小直接帶入‘dst’的大小,不需要計算剩余空間;(2)返回值可以用來判斷是否發生字符串截斷。

對以上源代碼不理解的地方稍作修改,有了以下函數:

size_t strlcat(char *dst, const char *src, size_t dsize) { const char *odst = dst; const char *osrc = src; size_t n = dsize; size_t dlen; /* Find the end of dst and adjust bytes left but don't go past end. */
    while (n-- != 0 && *dst != '\0') dst++; dlen = dst - odst; if (n == 0 && *dst == '\0'){ return dlen + strlen(src); } while (*src != '\0') { if (n != 0) { *dst++ = *src; n--; } src++; } *dst = '\0'; return(dlen + (src - osrc));    /* count does not include NUL */ }

 上述兩個源代碼最早出現在FreeBSD系統的標准庫函數中,可以在'http://ftp.openbsd.org/pub/OpenBSD/'官網中找到其實現。在Window或者Censos標准庫中沒有其實現方式。windows使用strcpy_s、strncpy_s。

其他

如果編譯平台是多個的話,由於strlcpy和strncpy_s的平台局限性,我們可以編寫函數實現類似的功能,或者使用NSPR庫來實現跨平台編譯。NSPR庫的知識和下載方式見‘https://www.jianshu.com/p/5e3d762981dd’。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM