前不久去腾讯一面,又遇到这个老生常谈的strcpy函数的实现,一听到HR说了这个函数,心里就暗暗吐槽,前一天还想到这个函数,觉得应该不会考就没细究。回来查了很多篇博客,测试了一下,发现还是有些问题,总结一下。
先从这篇博文开始,http://blog.csdn.net/gpengtao/article/details/7464061,“strcpy函数的实现”。
char *my_strcpy(char *dst,const char *src)
{
assert(dst != NULL);
assert(src != NULL);
char *ret = dst;
while((* dst++ = * src++) != '\0')
;
return ret;
}
这个基本的函数包括了指针有效性检测、目标指针返回和末尾添'\0',但是没有考虑内存覆盖问题。
使用下面的测试用例,程序就会因为覆盖了原指针字符串的'\0',导致while循环访问到数组外面的内存而崩溃:
char str[10]="abc";
my_strcpy(str + 1, str);
文中说了“调用系统的strcpy函数程序正常运行”,于是我在VS2010上测试了一下,结果如下:
int main()
{
char str[20] = "abcdefgh";
printf("before: %s\n", str);
strcpy(str + 1, str);
printf("after: %s\n", str);
}
输出:
before: abcdefgh
after: aabcddfghh
虽然程序没有崩溃,但输出也不是正常的,其中出现了两个d和两个h,所以库函数中的strcpy也不能处理内存重叠的问题。接着就讲到用memcpy函数解决内存重叠,但是《C和指针》中写着“如果src和dst以任何形式重叠时,它的结果是未定义的”,那就再测试一下吧。
char *my_strcpy(char *dst,const char *src)
{
assert(dst != NULL);
assert(src != NULL);
char *ret = dst;
memcpy(dst, src, strlen(src) + 1);
return ret;
}
对于这个程序,用如下测试用例:
int main()
{
char str[20] = "abcdefgh";
printf("before: %s\n", str);
my_strcpy(str + 1, str);
printf("after: %s\n", str);
}
VS2010的输出:
before: abcdefgh
after: aabcdefgh
是正确的。
Debian-GCC4.6.3中的输出:
before: abcdefgh
after: aaaaaefgg
是错误的。
VS2010上的memcpy看结果应该是考虑了内存重叠的问题,但Debian的gcc下输出则没考虑内存重叠导致重复输出a的效果,看来这就是结果未定义的表现。
在这儿http://bbs.chinaunix.net/thread-1305706-1-1.html也提到了,“cpy类的函数对于内存重叠都是未定义”。
插播一句,在vs2010目录的src中的memcpy源码中,也没有内存重叠的处理,写着“This routine does NOT recognize overlapping buffers, and thus can lead to propogation.” 会不会是因为安装了MinGW的原因,但是在MinGW中找不着源码,那就只能先搁着了。
那还是老老实实用memmove函数吧,memmove函数的实现方式就是第一篇文中的my_memcpy函数,当源地址和目的地址不重叠,低字节向高字节拷贝,否则高字节向低字节拷贝。于是strcpy的实现方式为:
char *my_strcpy(char *dst,const char *src)
{
if(NULL == dst || NULL == src)
{
return NULL;
}
if(src == dst)
{
return dst;
}
int length = strlen(src);
char *ret = dst;
if(src < dst || src > dst + length)//注意'\0'
{
while((*dst++ = *src++) != '\0');
}
else //内存重叠
{
while(length >= 0)
{
*(dst + length) = *(src + length);
length--;
}
}
return ret;
}
当然,这个程序还是不能保证目标指针指向的内存区间是否会越界,再加个size变量做越界检查貌似是画蛇添足了,在这儿也做个记号,有空在看一下strcpy_s、memcpy_s一类的函数。
最后,”卓越的教练是如何训练高手的?”这的确是一篇非常好的文章,从一个最简单的内存移动函数开始不断完善到完成memcpy的各个细节,以及测试用例的讲解,像我这样的初学者还是得多多学习,在这也姑且总结一下:实现最基础的功能->用void指针提高通用性->使用const修饰减少犯错->NULL指针检查->使用assert调试->内存重叠处理->完善测试用例。还真是写10分钟程序,先思考20分钟,多借鉴优秀程序。
绕了一个大圈,小结到这儿,以后再来自检。