C标准库函数strncpy用法解析

来源:互联网 发布:mysql查看数据库表 编辑:程序博客网 时间:2024/06/06 02:10

C库函数中,使用的最频繁的莫过于string库中的strcpy和strncpy这两个函数了,很多场合都需要用到,但是对这两个函数的使用上,还是存在很多问题,本文整理下这个点,并结合标准库的实现来说明C语言编程中可能存在的陷阱。

1. strcpy:

下面是从《C标准库》中摘录的对这两个函数的说明:

strcpy()函数:
概述:

    #include <string.h>    char * strcpy(char *s1, const char *s2);

说明:
函数strcpy把s2指向的串(包含终止的空字符)复制到s1指向的数组中,如果复制发生在两个重叠的对象中,则行为未定义。
返回值:
函数strcpy返回s1的值。

通过上面的描述,我们可以得到以下三点:
a. strcpy不会对地址是否发生重叠进行检测,如果可能有重叠的部分,这个接口的行为是未定义的,这个属性制约了该接口的使用场景
b. strcpy不会检测目的串的空间能否容纳下源串,所以使用这个接口的时候,必须保证目的串的存储空间足以容纳下源串
c. 返回值是源串的地址,而不是一个size_t的值,来描述真实发生的拷贝字符个数
本文主要关注第二点,从函数说明中可以看到, strcpy执行的真实拷贝长度是s2.len+1,既然strcpy不能保证目的存储空间一定能够容纳下源串,那么自然而然就想到了strncpy函数.

2.strncpy:

strncpy()函数:
概述:

    #include <string.h>    char *strncpy(char *s1, const char *s2, size_t n);

说明:
函数strncpy从s2指向的数组中复制最多n个字符(不复制空字符后面的字符)到s1指向的数组中。如果复制发生在两个重叠的对象中,则行为未定义。
如果s2指向的数组是一个长度比n短的字符串,则在s1指向的数组后面添加空字符,直到写入了n个字符。
返回值:
函数strncpy返回s1的值。

从上面的描述可以发现,这个函数的行为和strcpy有比较大的差别,虽然能够通过设置n的值为s1对应的存储空间来弥补strcpy的不足,但是这个函数的行为却比strcpy复杂了很多,这个函数的行为取决于strlen(s2)和n的大小关系:

if strlen(s2) < n:
这个函数执行两部分操作,先拷贝全部的s2的内容到s1,然后把strlen(s2) – n的长度用空字符填满,后面这个操作存在潜在的性能问题。
if strlen(s2) >= n:
这个函数拷贝s2的前n个字符到s1中
从上面的分析可以得到结论:strncpy不会一直给s1的结尾补上空字符,这个点是这个函数被误用的根源,比如像下面的代码:

char dst[8] = “abcdefg”;char src[16] = “ABCDEFGHIJKLMN”;strncpy(dst, srs, 8);

这个时候,dst的内容按次序分别为ABCDEFGH,结尾不是空字符,这个时候,如果只是读取dst的内容,则一般情况下不会导致程序崩溃,如果根据dst的空字符来控制数据写入,则会引起缓冲区溢出问题,会破坏掉dst地址增长方向的内存空间,引起不可预知的错误,具体发生哪类错误取决于dst所在的地址空间中的具体位置。


下面是在Ubuntu 16.04.2 LTS (GNU/Linux 4.8.0-36-generic x86_64)的测试结果。
程序源代码:

#include <stdio.h>#include <string.h>#define DST_LEN (8)#define SRC_LEN (16)int test_strncpy(){    char dst[DST_LEN] = "abcdefg";     char src[SRC_LEN] = "ABCDEFGHIGKLMN";      strncpy(dst, src, DST_LEN);    size_t len = strlen(dst);    printf("The dst len is %u!\n", len);    printf("The dst string: %s!\n", dst);    return 0;}int main(){    test_strncpy();    return 0;}

执行结果如下:
The dst len is 11!
The dst string: ABCDEFGHM@!——– H后面的值是随机的,H代替了应该出现的空字符。
从这个测试可以看到,这个函数使用上具有一定的迷糊性:不会一直在结尾补空字符,完全取决于strlen(src)和n的关系。
上面的这种情形下, dst退化成字符数组,不再具有C字符串的行为,为了使程序能够正常工作,修改程序为只拷贝前面的7个字节,数组的最后一个字节设置为空字符,这样程序就能正常工作。
修改后的test_strncpy函数:

int test_strncpy(){    char dst[DST_LEN] = "abcdefg";     char src[SRC_LEN] = "ABCDEFGHIGKLMN";     strncpy(dst, src, DST_LEN - 1); // use DST_LEN - 1 instead    dst[DST_LEN - 1] = '\0';  // guarantee  NUL-terminate    size_t len = strlen(dst);    printf("The dst len is %u!\n", len);     printf("The dst string: %s!\n", dst);    return 0;}

执行结果如下:
The dst len is 7!
The dst string: ABCDEFG!
通过这个例子,可以看到,使用strncpy的时候,必须牢记这个接口不会保证最后的字符是空字符,下面是来自《C标准库》的源代码,从源代码,我们可以看到函数的实现细节,正好印证了测试程序中的行为。

char * (strncpy)(char *s1, const char *s2, size_t n){    char *s;    for (s=s1; 0 < n && *s2 != ‘\0’; --n)        *s++ = *s2++;    for (; 0 < n; --n)        *s++ = ‘\0’;    return (s1);}

从函数的实现可以看到,strncpy可以保证目的串能够容纳源串,但是不能保证拷贝后目的串还是标准的C字符串(末尾的空字符0有可能被覆盖),同时引入了性能问题,如果源串小,目的串的存储空间比较大,strncpy的效率不如strcpy。为了解决这些问题,openbsd引入了函数strlcpy.

3. strlcpy

strlcpy()函数:
概述:

size_t strlcpy(char *dst, const char *src, size_t size);

说明:
函数strcpy把s2指向的串(包含终止的空字符)复制到s1指向的数组中,最多复制size个字符,如果第size-1个字符不是空字符,则设置dst中的size-1个字符为空字符;如果复制发生在两个重叠的对象中,则行为未定义。
如果str的长度小于size,则不会在dst后面写入空字符。
返回值:
函数返回src的长度。
这个函数弥补了strcpy和strncpy的问题,同时返回了src的长度,通过这个长度和拷贝后的dst的长度进行比较,可以判断是否发生截断,以方便后续程序处理。这个函数没有进入标准库,在linux下也没有对应的版本,但是在BSD系统中广泛存在,也有很多开源代码在使用,甚至进入了linux内核,但是到目前为止,还未进入glibc库和C标准库,至于这后面的原因,需要继续探索,这篇文章给出了一个参考:strlcpy and strlcat - consistent, safe, string copy and concatenation.

下面代码是openbsd对应的源代码:

#include <sys/types.h>#include <string.h>/* * Copy string src to buffer dst of size dsize.  At most dsize-1 * chars will be copied.  Always NUL terminates (unless dsize == 0). * Returns strlen(src); if retval >= dsize, truncation occurred. */size_tstrlcpy(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 */}DEF_WEAK(strlcpy);

通过对strncpy函数的剖析,对标准库的理解需要深入到实现细节这一步,才能驾轻就熟地使用它们,这也是高质量C编程的基石。这篇文章重点在防止buff overflow上,地址重叠和性能问题,后面的文章会继续讨论。

参考:
《C标准库》(美.P.J.Plauger)
《C primer plus the 5th》(Prata S)
https://en.wikipedia.org/wiki/C_string_handling#strlcpy

原创粉丝点击