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);
 }


/// <summary>
/// 自己实现的strcpy函数
/// </summary>
/// <param name="dest">
/// 字符串复制的目的地(char*)
/// </param>
/// <param name="src">
/// 待复制的字符串(char*)
/// </param>
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'也被拷贝)

以上是第二步优化,如果要考虑代码的健壮性,我们可以再继续优化:

我们考虑这样的情况,如果我们在调用这个函数的时候,不小心给destsrc传了一个空指针:

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.h>

assert()这个函数的功能是:我们在这个函数()内传入一个表达式

  1. 如果这个表达式的执行结果为真,那么该语句就相当于一个空语句(效果上"相当",不是执行的时间和空间效率上的"相当");
  2. 如果这个表达式的执行结果为假,那么程序就会在这里报错,并且会显示程序出错的位置信息

那么我们利用这个函数,对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()的区别

  1. strcpy()的返回值是char*类型,strcpy()返回值是void类型
  2. strcpy()的第二个形参(src)有const修饰,strcpy()则没有

strcpy()比我们多的这两个地方,有什么作用呢?接下来我们剖析一下:

第一个区别

第一个区别:char *strSourcechar前多了一个const修饰:

有什么用呢?

我们先看我们自己写的my_strcpy()函数的代码:

void my_strcpy(char* dest, char* src)
{
	assert( dest != NULL );
	assert( src != NULL );
	while ( *dest++ = *src++ ) {}
}

我们写这个函数的目的是将源字符串src中的内容赋值到目标字符串dest中,实现这个过程的语句是上面的第五行代码

`while ( *dest++ = *src++ ) {}`

设想如果我们哪天在复刻这个代码的时候,不小心把destsrc的位置写反了,写成了这样:

`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<stdio.h>
#include<stdlib.h>
#include<assert.h>

char* my_strcpy(char* dest, char* src);		// 函数声明

// 测试
void test()
{
	char str1[] = "################################";
	char str2[] = "我爱华农!!!华农加油!!!";

	printf("%s\n", my_strcpy(str1, str2));	// 打印被复制后的目标字符串
}


/// <summary>
/// 将src指向的字符串拷贝到dest指向的空间,包括'\0'字符
/// </summary>
/// <param name="dest">
/// 字符串复制的目的地(char*)
/// </param>
/// <param name="src">
/// 要复制的字符串(char*)
/// </param>
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技巧建议:

  1. 尽量使用assert
  2. 尽量使用const
  3. 养成良好的编码风格
  4. 添加必要的注释
  5. 避免编码的陷阱(野指针滥用,如上面代码实现过程中的NULL)

以上内容总结自鹏哥的C语言教学视频25.VS环境-C语言实用调试技巧(2)哔哩哔哩_bilibili并加上了一些个人的思考。
欢迎大家批评指正,交流想法~
最后偏心一下我的学校:
(以下图片来自华南农业大学官方公众号,非个人拍摄!!!)
(以下图片来自华南农业大学官方公众号,非个人拍摄!!!)
(以下图片来自华南农业大学官方公众号,非个人拍摄!!!)

在这里插入图片描述在这里插入图片描述
在这里插入图片描述
我大华农:抗疫必须胜利!!!
我大华农:抗疫必须胜利!!!
我大华农:抗疫必须胜利!!!

  • 12
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值