C语言——复刻一个strcpy()函数,体验代码编写和优化的过程-创新互联
项目需求:自己实现一个strcpy()
初始代码实现如下:
void my_strcpy(char* dest, char* src); // 函数声明
// 测试
void test()
{char str1[] = "################################";
char str2[] = "我爱华农!!!华农加油!!!";
my_strcpy(str1, str2); // 自己实现strcpy
printf("%s\n", str1);
}
////// 自己实现的strcpy函数
/// ////// 字符串复制的目的地(char*)
///////// 待复制的字符串(char*)
///void my_strcpy(char* dest, char* src)
{while (*src != '\0') // src中的'\0'前的字符都要进行复制
{*dest = *src; // 将待复制字符串的字符复制到目的字符串中
dest++; // 指针向后移位,以便进行下一个字符的赋值
src++; // 同上
}
*dest = *src; // 最后的'\0'也要复制过去
}
注意到my_strcpy()
函数的函数体中实际上是可以把解引用*
和自增++
放在同一行的。于是对strcpy()的优化如下:
void my_strcpy(char* dest, char* src)
{while (*src != '\0') // src中的'\0'前的字符都要进行复制
{*dest++ = *src++; // 优化
}
*dest = *src; // 最后的'\0'也要复制过去
}
此时代码中的
*dest++ = *src++
执行过程就是:先执行解引用*,然后进行赋值=,然后执行自增++。从表面看这行代码还是比较好直接理解的,就是我们所看到的
*
在前面,++
又是后置的,所以肯定是先执行了解引用*
,然后执行后置++
,而后置++
执行自增又是在整个语句结束后,也就是赋值语句=
结束之后才++
。所以整个语句的执行顺序就是先解引用*
,然后赋值=
,然后指针自增。但实际的执行顺序是这样的,我先查阅了以下运算符优先级表,表格如下:
*
和++
运算符的优先级是相同的,所以此时的结合先后顺序要考虑的是结合性(=
运算符优先级是较低的,肯定是在最后才执行\结合),由于是这两个运算符是右结合(右到左),即结合顺序是从右到左,所以是++先结合,然后才是*,也就是说实际上
*dest++ = *src++
是++
先执行,然后才是*
执行,但是由于++
是后置的,所以尽管++
先结合或者说先执行(以上表述中的“结合”、“执行”不严格区分,认为是一个意思)了,但是它的自增效果实现是在整个语句结束之后,也就是最后的=
赋值结束之后才++
,所以最终语句的执行顺序还是:先解引用*
,然后赋值=
,然后自增。但我们要知道该语句本质的结合\执行顺序是:先后置++
,后*
,然后=
。只不过++
的自增效果实现是在最后而已。
以上是第一步优化,实际上还可以继续优化:
我们注意到此时my_strcpy()
函数的实现是把字符串内容的拷贝和'\0'
的拷贝分为了两个部分,其实没有必要。我们将函数进一步优化后如下:
void my_strcpy(char* dest, char* src)
{while (*dest++ = *src++) {} // 优化
}
此时的代码不仅能够继续实现将字符串中的内容进行复制,并且还能将字符串最后的
'\0'
也进行复制,而且在'\0'
复制之后while
循环就会停下来,函数执行完毕。这是因为在复制完字符串的最后一个字符后,循环条件处的整个
*dest++ = *src++
表达式的值就是最后一个字符(非'\0'
),此时相当于循环条件仍是真,会进入空循环体执行,空循环体执行完之后再次来到循环条件处(此时的src
相较于上一次循环已经是++
了,其解引用*
后指向的是'\0'
,于是在完成赋值操作后,整个表达式的值就是'\0'
,此时循环条件就变为假,循环结束。这里我们注意到'\0'
是在完成了赋值操作后,循环才结束的。所以上面的代码就很好地将字符串内容的拷贝和'\0'
的拷贝合成一个部分\语句,并且能够保证'\0'
也被拷贝)
以上是第二步优化,如果要考虑代码的健壮性,我们可以再继续优化:
我们考虑这样的情况,如果我们在调用这个函数的时候,不小心给dest
和src
传了一个空指针:
void test()
{char str1[] = "################################";
char str2[] = "我爱华农!!!华农加油!!!";
my_strcpy(str1, NULL); // 传了一个NULL指针
printf("%s\n", str1);
}
此时代码运行结果如下:
程序挂掉了!!!因为我们此时在函数中会访问这个
test()
中传过来的空指针NULL
,这是非法的!
这就说明我们的代码在健壮性方面存在欠缺,原因就是我们的my_strcpy()
函数中缺乏对形参变量的合法性判断:我们对主调函数传过来的参数,不管三七二十一就直接开始解引用了。
正确的做法是我们应该对这些形参使用前先进行一个判断:
void my_strcpy(char* dest, char* src)
{if (dest != NULL && src != NULL)
{while (*dest++ = *src++) {}
}
}
此时就算我们的test()
函数在调用my_strcpy()
的时候给这个函数传递了一个NULL
指针,程序运行效果如下:
可以看到,现在程序至少能够规避掉这个错误而不会挂掉(这是很重要的!因为程序如果其他部分的功能正常,但是因为你这个地方的问题而程序挂掉了,那么其他部分的功能也执行不了!所以我们至少至少的!应该要保证程序不会挂掉!)
这个时候程序就健壮了一些,但是我们还要考虑到,程序更改成这样之后,其实我们是不容易去发现这个错误(或者说是BUG
)的。
为了让我们的程序更健壮,并且容易进行Debug,我们调用一个函数:assert()
(我们称之为:断言),其使用必须先包含一个头文件:
assert()
这个函数的功能是:我们在这个函数()内传入一个表达式
- 如果这个表达式的执行结果为真,那么该语句就相当于一个空语句(效果上"相当",不是执行的时间和空间效率上的"相当");
- 如果这个表达式的执行结果为假,那么程序就会在这里报错,并且会显示程序出错的位置信息
那么我们利用这个函数,对my_strcpy()
进行进一步的改进:
void my_strcpy(char* dest, char* src)
{assert( dest != NULL ); // 一旦dest为NULL,该函数就会报错
assert( src != NULL ); // 一旦src为NULL,该函数就会报错
while ( *dest++ = *src++ ) {}
}
此时我们还是在test()
函数中给src
传一个NULL
,程序运行效果如下:
可以看到此时DOS窗口很好地将代码出错的文件路径,出错的位置(行数)都显示了出来
这样的做法将有利于我们编写程序时去发现BUG。修改后的整体代码如下:
void my_strcpy(char* dest, char* src); // 函数声明
// 测试
void test()
{char str1[] = "################################";
char str2[] = "我爱华农!!!华农加油!!!";
// my_strcpy(str1, NULL);
// my_strcpy(NULL, str2);
my_strcpy(str1, str2);
printf("%s\n", str1);
}
void my_strcpy(char* dest, char* src)
{assert( dest != NULL ); // 一旦dest为NULL,该函数就会报错
assert( src != NULL ); // 一旦src为NULL,该函数就会报错
while ( *dest++ = *src++ ) {} // 优化
}
此时当我们把test()
在调用时传入正常的参数,代码也能够正常运行:
程序优化到这一步就差不多了,我们回想一下:我们写my_strcpy()
这个函数的目的是要自己实现跟库函数中strcpy()
的一样的功能。功能现在是实现得差不多了,我们来看看我们写的这个my_strcpy()
与库函数中的strcpy()
又有什么不同呢?
通过MSDN
(微软提供提供给广大程序员的开发大全,是一个帮助文档)我们查阅一下关于strcpy()
函数的声明:
再对比我们写的my_strcpy()
函数的声明:
void my_strcpy(char* dest, char* src);
可以发现有两个区别:
strcpy()
与my_strcpy()
的区别
strcpy()
的返回值是char*
类型,strcpy()
返回值是void
类型strcpy()
的第二个形参(src
)有const
修饰,strcpy()
则没有
strcpy()
比我们多的这两个地方,有什么作用呢?接下来我们剖析一下:
第一个区别:char *strSource
中char
前多了一个const
修饰:
有什么用呢?
我们先看我们自己写的my_strcpy()
函数的代码:
void my_strcpy(char* dest, char* src)
{assert( dest != NULL );
assert( src != NULL );
while ( *dest++ = *src++ ) {}
}
我们写这个函数的目的是将源字符串src
中的内容赋值到目标字符串dest
中,实现这个过程的语句是上面的第五行代码
`while ( *dest++ = *src++ ) {}`
设想如果我们哪天在复刻这个代码的时候,不小心把dest
和src
的位置写反了,写成了这样:
`while ( *src++ = *dest++ ) {}`
程序肯定会出问题!
或者说我们在函数中对src
的内容*src
不小心进行了修改(src
是我们要复制的字符串,其内容是我们在调用这个函数的时候不希望会被修改的)
这些都是不允许的,最根本的原因就是我们调用这个函数的初衷是为了复制,并不希望待复制的字符串反过来被莫名其妙地修改。但是项目开发的过程中,犯这样这样的错误在所难免(变量用着用着,不小心就把它改掉了…)
所以为了避免这样的问题发生我们可以在my_strcpy()
形参src
的位置利用const
修饰:
这里涉及到指针常量和常量指针的问题的知识,大家可以看我的这篇博客:C/C++中指针常量和常量指针的一些小见解
void my_strcpy(char* dest, const char* src) // const常量指针,src所指向的内容*src不可被修改
{assert( dest != NULL );
assert( src != NULL );
while ( *dest++ = *src++ ) {}
}
这样让src
变成一个常量指针之后,在函数中就使得其指向的内容*src
不会被错误修改,即使程序员不小心写错了,想修改src中的内容,编译器也会及时地报错:表达式必须是可修改的左值。
所以代码到了这里,第一个区别存在的原因我们就清楚了,代码的优化就又进了一步,代码产生BUG的可能性就更小了。
第二个区别接下里我们继续看第二个区别:strcpy()
的返回值是char*
类型,strcpy()
返回值是void
类型
那么返回的这char*
类型有什么用呢?我们查阅帮助文档:
可以获知strcpy()
的返回值是目标字符串strDestination
的地址,这样做有什么用呢?
用途:
可以用于直接作为
printf
对字符串的输出:printf("%s\n", my_strcpy(str1, str2))
,这里也体现了链式访问的思想(函数(的返回值)直接作为另一个函数参数)
要使得我们的my_strcpy()
函数返回目标字符串strDestination
的地址,我们将函数更改后如下:
char* my_strcpy(char* dest, const char* src) // 返回值类型修改为char*
{char* tempSave_Addr = dest; // 保存目标字符串strDestination的起始地址
assert( dest != NULL );
assert( src != NULL );
while ( *dest++ = *src++ ) {}
return tempSave_Addr; // 返回值目标字符串strDestination的起始地址
}
此时我们可以验证刚才提到的第一个用途,直接用于printf
打印输出复制后的目标字符串strDestination
:
void test()
{char str1[] = "################################";
char str2[] = "我爱华农!!!华农加油!!!";
// my_strcpy(str1, str2);
// printf( "%s\n", str1);
// 上面的两行代码合为下面一行:
printf( "%s\n", my_strcpy(str1, str2));
}
程序运行后输出了被复制后的str1
,跟我们修改之前的运行效果一致。
这个地方也体现了链式访问(函数的返回值作为另一个函数的参数),程序的这一步修改,其多了一个功能:这个函数可以直接作为其他函数的参数。功能进一步丰富了。
最后我们加上必要的注释之后,整体代码如下:
#include#include#include
char* my_strcpy(char* dest, char* src); // 函数声明
// 测试
void test()
{char str1[] = "################################";
char str2[] = "我爱华农!!!华农加油!!!";
printf("%s\n", my_strcpy(str1, str2)); // 打印被复制后的目标字符串
}
////// 将src指向的字符串拷贝到dest指向的空间,包括'\0'字符
/// ////// 字符串复制的目的地(char*)
///////// 要复制的字符串(char*)
///char* my_strcpy(char* dest, const char* src)
{char* tempSave_Addr = dest; // 保存目标字符串strDestination的起始地址
// 以下两行代码是断言,用于保证指针的有效性
assert( dest != NULL ); // 一旦dest为NULL,该函数就会浮窗报错
assert( src != NULL ); // 一旦src为NULL,该函数就会浮窗报错
while ( *dest++ = *src++ ) {} // 将src指向的字符串拷贝到dest指向的空间,包括'\0'字符
return tempSave_Addr; // 返回值目标字符串strDestination的起始地址
}
int main()
{test();
system("pause");
return 0;
}
程序运行结果:
以上就是我们关于自己编写一个类似strcpy()
函数my_strcpy()的过程,体验代码优化的一个过程。目的在于提高我们的coding
技巧,减少BUG
的产生,即使产生BUG
不可用避免,也要学会如何编写程序使得我们Debug
的难度降低。
顺便分享比特鹏哥给我们的coding
技巧建议:
- 尽量使用
assert
- 尽量使用
const
- 养成良好的编码风格
- 添加必要的注释
- 避免编码的陷阱(野指针滥用,如上面代码实现过程中的NULL)
以上内容总结自鹏哥的C语言教学视频25.VS环境-C语言实用调试技巧(2)哔哩哔哩_bilibili并加上了一些个人的思考。
欢迎大家批评指正,交流想法~
最后偏心一下我的学校:
(以下图片来自华南农业大学官方公众号,非个人拍摄!!!)
(以下图片来自华南农业大学官方公众号,非个人拍摄!!!)
(以下图片来自华南农业大学官方公众号,非个人拍摄!!!)
我大华农:抗疫必须胜利!!!
我大华农:抗疫必须胜利!!!
我大华农:抗疫必须胜利!!!
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
名称栏目:C语言——复刻一个strcpy()函数,体验代码编写和优化的过程-创新互联
分享地址:http://myzitong.com/article/diopsd.html