从0开始学c语言-23-如何写出好(易于调试)的代码、模拟实现库函数:strcpy、strlen 、编程常见错误

本文介绍了如何写出易于调试的C语言代码,包括模拟实现库函数strcpy和strlen,讨论了代码优化和错误处理。通过实例展示了如何避免编译型、链接型和运行时错误,强调了const修饰指针的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

优快云话题挑战赛第2期
参赛话题:学习笔记

本人0基础开始学编程,我能学会的,你也一定可以,学会多少写多少。

下载安装请从官网入手,社区版本即可,这里主要使用的软件是VS2019,图标如下。

上一篇:从0开始学c语言-22-结构体声明和初始化、结构体大小、结构体成员访问、结构体传参_阿秋的阿秋不是阿秋的博客-优快云博客

目录

1·如何写出好(易于调试)的代码

优秀代码

常见的coding技巧:

模拟实现库函数:strcpy

查看功能

模拟实现

优化代码

断言assert 

const修饰

2·const修饰指针

总结

3·模拟实现一个strlen函数

功能

现在写一个我们自己的strlen函数。

函数返回类型和参数类型确定

确定实现函数功能的过程

4·编程常见的错误

.1 编译型错误

.2 链接型错误

.3 运行时错误


1·如何写出好(易于调试)的代码

优秀代码

1. 代码运行正常
2. bug 很少
3. 效率高
4. 可读性高
5. 可维护性高
6. 注释清晰
7. 文档齐全

常见的coding技巧:

1. 使用 assert
2. 尽量使用 const
3. 养成良好的编码风格(看一本书,高质量的c/c++编程)
4. 添加必要的注释
5. 避免编码的陷阱(看一本书,c陷阱和缺陷)

模拟实现库函数:strcpy

查看功能

 简单来说,就是把source位置的字符串拷贝到destination中。

效果就像这样。

可以看到,会把结束标志的 \0 也拷贝进去。

模拟实现

首先模仿strcpy,起个自己的函数名,然后确定要传递的函数参数。

my_strcpy(arr1, arr2);

然后考虑函数的返回类型和函数参数的类型

因为我们的arr1和arr2都是字符数组,数组的名字是首元素的地址,所以我们在传参的时候传递的是首元素的地址,那么函数参数的类型就要用指针进行接收。(因为指针变量是用来存地址的啊!)

那么函数就会长这样。

void my_strcpy(char* dest, char* source)
{	
//dest表示目的地(copy的目的地) 
//source表示源(需要copy的字符串)
}

 这就是传过来的dest和source指针。

为了实现copy这个功能,就需要让

*dest=*source;

 这样便可以实现copy,接下来就需要指针向后挪动指向下一个内容进行copy。

dest++;
source++;

那么为了实现所有字符的copy,我们需要循环,循环便需要确定什么时候不用循环

当然是遇到 \0 便停止循环了。那代码就是这样。

while (*source != '\0')
	{
		*dest = *source;
		dest++;
		source++;
	}

可这还没完,因为我们没有把arr2中的结束字符 \0 拷贝过去,就不算完全copy了arr2字符数组。

想想走出循环的时候,dest和source指针都指向什么位置呢?

 source指针正好指向 \0。

那么这就是我们设想的代码了。

void my_strcpy(char* dest, char* source)
{
	while (*source != '\0')
	{
		*dest = *source;
		dest++;
		source++;
	}
	*dest = *source;
}

现在可以实现我们想要的功能了,但它不是一个优秀的代码。

优化代码

首先是while语句中的三个语句太多,且可以合并为
*dest++ = *source++;

这还不够,出了循环后又让*dest = *source显得不是很简洁,我们需要想办法避免这个语句。

我们知道的是出了循环之后,可以直接让指向的内容相等。

那么有没有什么办法能够让这个语句在循环内就实现交换,并且copy后就跳出循环呢

有!放到循环条件中

while (*dest++ = *source++)
	{
		;
	}

*dest++ = *source++这个表达式的结果是什么呢?

*source是谁,这个表达式的结果就是谁,比如第一个*source是字符a,那么这个表达式的结果就是字符a,如果*source指向\0,那么这个表达式的结果就是0,此时判断条件为假,跳出循环,但是后置++的操作是必须执行的,因此是判断为假后,进行++,然后跳出循环。

但是还不够!

断言assert 

如果我们的arr2传过来是一个空值,那么接收的指针应该是个空指针,空指针不能被解引用实现字符串的copy,所以我们要避免这种情况,那就需要assert来判断是否是空值。 

assert(source!=NULL);

这代表我们不允许source指针是空指针,如果是空指针那便会程序崩溃并提示你。

可以把assert理解为一个条件判断语句,条件为真,照常执行。条件为假,它会告诉你有问题,还会告诉你它在第几行,你就可以根据这个找到程序的问题在哪。

如果你没有assert的情况下传过来了空指针,那你连问题在哪都不知道。

这东西真是个好玩意,帮助我们快速找到问题。

同样的道理,我们还需要判断一下arr1是不是空值。

那代码就是这样

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

const修饰

但还不够好,我们看看strcpy在库函数中的定义。

char *strcpy( char *strDestination, const char *strSource );

再放上我们自己定义的

void my_strcpy(char* dest, char* source)

1·可以看到,source当中有个const来修饰。

它是指souce字符串不能被修改的意思,也就是说用来防止把dest和source写反了的情况,

2·函数的返回类型是char*

我们通过学习网址

https://cplusplus.com/reference/

找到strcpy,看到它返回的是目标空间的起始地址(也就是dest的起始地址)

之前我们的代码是这样

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

我们先把函数返回类型改为 char*

然后写一个返回值

return dest;

你是不是觉得完美了?

错!

dest指针早就被你不知道向后挪了多少步,所以为了能够返回起始地址,我们需要用一个变量来存储dest最开始的地址,就像这样。

char* my_strcpy(char* dest,const char* source)
{
	assert(dest != NULL);
	assert(source != NULL);
	char*const ret = dest; //加了这个
	while (*dest++ = *source++)
	{
		;
	}
	return ret; //加了这个
}

可以看到我加了个const修饰,后面我们深入讲一下const。

上面的代码还可以继续优化成这样

char * strcpy(char * dst, const char * src) {
        char * cp = dst;
        assert(dst && src);
        while( *cp++ = *src++ )
               ;     /* Copy src over dst */
        return( dst );
}

2·const修饰指针

const 修饰变量,这个变量就被称为常变量,不能被修改,本质上还是变量。

const int num=10;
num=20; //err

就像这样,num没办法被修改。

现在我们把num地址存放到指针p中

const int num=10;
int *p=#
*p=20;

此时,num的值可以被修改。

你可以理解为每次修改都需要访问内存,num是直接从内存门口走进去修改值的,而指针直接翻窗户进来修改,所以这时候的num可以被修改。

那么这个const就对num不是那么有作用了,我们就需要在指针前加一个const

const int *p=#
*p=20;//err

这时候*p没办法改变num

const修饰指针变量的时候,

放在*左边,修饰的是*p,*p表示指针指向的内容,也就是说不能通过*p来改变指针指向的内容了,但是指针变量p可以改变。

*p——指针指向的内容

p——指针变量,存放地址

上面这段话,在代码中就会体现成这样

const int *p=#
*p=20;//err
p=&n;//ok

现在我们把const放在*右边,那代码就是这样

int * const p=#
*p=20;//ok
p=&n;//err

const修饰指针变量的时候,

放在*右边,修饰的是指针变量p,指针变量存储的地址不能被改变,但是*p(指针指向的内容)可以改变。

总结

const修饰指针变量的时候,

放在*左边,修饰的是*p,*p表示指针指向的内容,也就是说不能通过*p来改变指针指向的内容了,但是指针变量p可以改变。

放在*右边,修饰的是指针变量p,指针变量存储的地址不能被改变,但是*p(指针指向的内容)可以改变。

可以都放const

//*左边
const int *p=#
*p=20;//err
p=&n;//ok

//*右边
int * const p=#
*p=20;//ok
p=&n;//err
《高质量C/C++编程》一书中最后章节试卷中有关 strcpy 模拟实现的题目。
有了上面的书写思路,我们试试

3·模拟实现一个strlen函数

功能

int main()
{
	char arr[] = "asdfg";
	int len = strlen(arr);
	printf("%d\n", len);
    return 0;
}

我们是这样使用strlen函数的,求字符串长度。

现在写一个我们自己的strlen函数。

函数返回类型和参数类型确定

my_strlen(arr);

然后进行函数返回类型和函数参数的类型确定。

1·函数参数的类型确定

因为我们传过去的是arr字符数组的名字,而数组名字又是首元素的地址,所以我们的接收参数的函数参数类型应该是char*类型的指针。

2·函数返回类型

因为我们计算的是字符串的长度,所以返回的应该是一个整数,int类型。

函数现在长这样。

int my_strlen(char* start)
{

}

确定实现函数功能的过程

本来一开始打算设置个变量来储存长度,当做函数的返回值。

后来我突然想起来,咱们接收的是char*指针,然后我就想起来了 指针 - 指针 是 指针之间的元素个数。

那么现在只有一个指针变量,所以我们需要再来一个指针,才能进行 指针 - 指针 的操作。

char* end = start;

然后我们需要让指针向后移动,一直移动到 \0

那就需要循环来让指针向后移动,循环就需要考虑循环条件,本来我想写成这样

while (*end)
{
    end++;
}

后来我想起来strcpy函数的优化过程,就改良成了这样。

while (end++);

然后返回两个指针相减的值

int my_strlen(char* start)
{
	char* end = start;
	while (*end++);
	return (end - start);
}

但是不对,我画图示意一下

每次先进行判断后进行+1,同一颜色的是判断后+1后的指针。

虽然判断为假,但是指针依旧向后跳了一步,只不过不会进行下一次判断了而已。

那么我们 返回的值 就比 本来的字符串长度 多1

代码应该是这

int my_strlen(char* start)
{
	char* end = start;
	while (*end++);
	return (end - start-1);
}

最后进行assert和const的点睛之笔

int my_strlen(const char* start)
{
    assert(start);
	const char* end = start;
	while (*end++);
	return (end - start-1);
}

编程常见的错误

.1 编译型错误

直接看错误提示信息(双击),解决问题。或者凭借经验就可以搞定。相对来说简单。

.2 链接型错误

看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在。一般是标识符名不
存在或者拼写错误

.3 运行时错误

借助调试,逐步定位问题。最难搞。

下一篇:从0开始学c语言-24- 剖析数据在内存中的存储(整型在内存中的储存和练习)、char的取值范围、大端小端储存_阿秋的阿秋不是阿秋的博客-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值