代码改变世界

关于strcpy的一些想法

2013-10-21 21:37  Ross Wang  阅读(368)  评论(0编辑  收藏  举报

标准库对strcpy的说明如下:

char * strcpy ( char * destination, const char * source );
Copy string

Copies the C string pointed by source into the array pointed by destination, including the terminating null character (and stopping at that point).

To avoid overflows, the size of the array pointed by destination shall be long enough to contain the same C string as source (including the terminating null character), and should not overlap in memory with source.
一些公司在笔试面试的时候很喜欢让候选人自己现场实现一个strcpy函数,比较好的实现是这样的:
//定义一个strcpy函数,实现字符数组的拷贝
char * strcpy_simulator(char *dst,const char *src)
{
    assert(dst != NULL);
    assert(src != NULL);
    char *ret = dst;
    while((* dst++ = * src++) != '\0')
        NULL;
    return ret;
}

网上也有朋友提到目标字符数组可以为空,我们在函数内部手动分配空间:

//这种做法是错误的,而且会出现返回的dst和实参dst不一致的情况。
char * strcpy_simulator_allow_null_dst(char *dst,const char *src)
{
    if(dst == NULL)
    {
        dst = (char *)malloc(strlen(src)+1);
    }
    assert(src != NULL);
    char *ret = dst;
    while((* dst++ = * src++) != '\0')
        NULL;
    return ret;
}

遗憾的是,这种实现是错误的。

为了和库函数strcpy保持一致,上面函数的签名(返回值类型+函数名+形参表)和库函数保持一致,而且的确在目标指针为空的时候手动分配了内存。

但是这样做的问题在于如果dst为NULL来调用这个函数,作为实参的dst在函数返回后还是NULL,而该函数的返回值却正确返回了新分配的内存地址。

这毫无疑问会导致困惑和潜在的风险。

这种实现实质的问题就是错误认为指针传入函数的时候是地址传递,在函数内部对该指针的修改会在函数返回后保留。

真实情况是:

指针作为实参传入函数对于指针所指对象而言是地址传递,也就是说传入函数的是指针所指对象的地址。

指针作为实参传入函数对于该指针来说是值传递,也就是说指针本身表示内存地址的值被拷贝了一个副本进去。

所以函数内部对指针本身的修改在函数返回后都无法保留,而对指针所指对象的修改是有效的。

下面用完整的例子来说明一下:

#include <iostream>
#include <assert.h>
using namespace std;

//定义一个strcpy函数,实现字符数组的拷贝
char * strcpy_simulator(char *dst,const char *src)
{
    assert(dst != NULL);
    assert(src != NULL);
    char *ret = dst;
    while((* dst++ = * src++) != '\0')
        NULL;
    return ret;
}

//曾经看到有人这么写strcpy的实现,允许目标指针为空,在函数内部分配内存
//这种做法是错误的,而且会出现返回的dst和实参dst不一致的情况。
char * strcpy_simulator_allow_null_dst(char *dst,const char *src)
{
    if(dst == NULL)
    {
        dst = (char *)malloc(strlen(src)+1);
    }
    assert(src != NULL);
    char *ret = dst;
    while((* dst++ = * src++) != '\0')
        NULL;
    return ret;
}

int main()
{
    char str[4]="abc";
    cout<<str<<endl;
    //如果源字符数组和目标字符数组有内存重叠的情况
    strcpy(str+2,str);
    cout<<str<<endl; //输出结果ababc
    cout<<str[4]<<endl;  //输出结果c

    /*************
    调用我们自己的strcpy_simulator报错。
    说明库函数strcpy的实现和我们的模拟strcpy函数在执行
    过程中的行为是不同的,至少在vs2010中如此
    测试strcpy_simulator_allow_null_dst的时候请将下一行注释掉
    *************/
    strcpy_simulator(str+1,str);
    cout<<str<<endl;

    char * nullDst = NULL;
    char * newDst = strcpy_simulator_allow_null_dst(nullDst,str);
    if(nullDst == NULL)//判断肯定为true
        cout<<"nullDst is still NULL."<<endl;

    if(newDst == NULL)
        cout<<"nullDst is still NULL."<<endl;
    else
    {
        cout<<newDst<<endl;
        //newDst是我们在堆上手动申请的内存,需要释放。
        free(newDst);
    }

    return 0; 
} 

可以看到我们的simulator和微软提供的库函数在运行时的行为是不一样的,后者在源字符数组和目标字符数组存在内存重叠的时候是不报错并执行了拷贝的。

但是这个不是确定的行为,MSDN也确认了这一点

The strcpy function copies strSource, including the terminating null character, to the location specified by strDestination. The behavior of strcpy is undefined if the source and destination strings overlap.

这种情况下,执行成功后超出数组容量部分的数据也泄漏了,bug在偷笑。

欢迎大家讨论交流。