每一次写代码时,都要想好思路,理清逻辑顺序,而不是直接开写,到最后这改改那改改完事,拒绝-迷信式调试
目录
实例一:实现代码:求 1!+2!+3! ...+ n! ;不考虑溢出。
Debug和Release的介绍。
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优
的,以便用户很好地使用
最常使用的几个快捷键:
F5
启动调试,经常用来直接跳到下一个断点处。
F9
创建断点和取消断点
断点的重要作用,可以在程序的任意位置设置断点。
这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。
F5通常与F9搭配使用
F10
逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11
逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(这是
长用的)。
CTRL + F5
开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。
实例一:实现代码:求 1!+2!+3! ...+ n! ;不考虑溢出。
int main()
{
int i = 0;
int sum = 0;//保存最终结果
int n = 0;
int ret = 1;//保存n的阶乘
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
int j = 0;
for (j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
我们会发现, 我们输入3的话,结果等于15,而不等于9。问题出在哪里呢?
经过我们调试可得,是保存到ret的值,没有初始化为1,把上一次结果也存下来了。所以我可以改写成:
int main()
{
int i = 0;
int sum = 0;//保存最终结果
int n = 0;
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
int ret = 1;//保存n的阶乘
int j = 0;
for (j = 1; j <= i; j++)
{
ret *= j;
}
sum += ret;
}
printf("%d\n", sum);
return 0;
}
当然,这也不是我最优写法。我可以在2!上乘以3,就是3的阶乘,没必要从1开始依次乘以。
这样一来:我就可以这样写:
int main()
{
int i = 0;
int sum = 0;//保存最终结果
int n = 0;
int ret = 1;
scanf("%d", &n);
for (i = 1; i <= n; i++)
{
ret = ret * i;
sum += ret;
}
printf("%d\n", sum);
return 0;
}
这样一层循环就可以搞定。
再来看这道题,结果是什么?
int main()
{
int i = 0;
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (i = 0; i <= 12; i++)
{
arr[i] = 0;
printf("hehe");
}
return 0;
}
在Vs2019的环境中,结果竟然是死循环,为什么?
经过调试可得:我i=11时,即便越界了,但是没有报错,并强制把arr[11]改成了0。最后改arr[12]的时候,竟然就把i也改掉了,为什么。
原来是i的地址,和arr[12]的地址是一样的,我改arr[12],也就改了i的值。所以i永远不会>12,所以造成了死循环。
为什么?
因为在Debug,X86的编译器环境下,我们先创建了i,再创建了数组。数组都是局部变量,都是在栈区创建的。栈区的使用习惯是先使用高地址,再使用低地址。数组却又是随着下标的增长,地址是由低到高变化的。如果arr数组和i之间刚好空了两个整形的空间的话,也就是说,当我数组越界访问,就很有可能使用arr[12]的时候,其实就是i,就改掉了我的i,造成了死循环。
在Vc6.0的环境中,可能写成10就死循环了,中间没有空出一个整形的空间。所以他非常依赖环境。不同的环境也是不同的。而且release版本会对代码进行一定优化,可能就不会出现死循环的情况。
但最终造成这个原因的是越界访问。
优秀代码的特点:1. 代码运行正常 2. bug很少 3. 效率高 4. 可读性高 5. 可维护性高 6. 注释清晰 7. 文档齐全。
常见的coding技巧: 1. 使用assert 2. 尽量使用const 3. 养成良好的编码风格 4. 添加必要的注释 5. 避免编码的陷阱。
示范:
模拟实现库函数strcpy
两个参数,一个目标字符串,一个源头。而且“including the termination null character”,包括结束的空字符串 \0 。
注:传参传的都是首元素的地址。
Null都是指\0
接下来模拟实现:
void my_strcpy(char*dest, char*src)
{
while (*src!='\0')
{
*dest = *src;
dest++;
src++;
}
*dest = *src;
}
int main()
{
char arr1[] = "hello world";
char arr2[] = "xxxxxxxxxxxxxxxxx";
my_strcpy(arr2, arr1);
printf("%s", arr2);
return 0;
}
这样写可以。我还能再改进一下吗?
这样写,就非常的巧妙:
void my_strcpy(char*dest, char*src)
{
while (*dest++ = *src++)
{
;
}
}
int main()
{
char arr1[] = "hello world";
char arr2[] = "xxxxxxxxxxxxxxxxx";
my_strcpy(arr2, arr1);
printf("%s", arr2);
return 0;
}
解析:因为第一次当我进入循环时,h的ascii码值不可能为0,赋值后进入循环体内什么事也没发生。直到最后复制了\0,0为假,循环体结束。
我还能怎么改进呢?万一传来的是没有赋值的野指针呢?所以我们断言一样:
#include<assert.h>
void my_strcpy(char*dest, char*src)
{
assert(dest != NULL);//断言,如果条件为假就报错
assert(src != NULL);
//这两句也可以写成 assert(dest && src);因为NULL就是0,就是假
while (*dest++ = *src++)
{
;
}
}
int main()
{
char arr1[] = "hello world";
char arr2[] = "xxxxxxxxxxxxxxxxx";
char* p = NULL;
my_strcpy(p, arr1);
printf("%s", arr2);
return 0;
}
它可以直接告诉我们哪里出了问题。
我们还能如何改进呢?
我们写的,和官方的有两个不同,有何区别呢?
为什么要加个const呢?就可以防止我src和dest写反了
因为const修饰的变量,叫常变量,是无法被修改的。但是我*p地址来进行间接修改,就真的改掉了
这样就破坏了这种规则,const就没有效果了。所以我对p也进行一定限制
这样就无法修改了
const的使用方法:
总结:const可以修饰指针,const放到*的左边(const int*p),修饰的是*p,表示p指向的对象,不能通过*p指针来间接修改。但是p自己本身存放的变量的地址是可以修改的
const放在*的右边(int* const p),修饰的是p变量本身,表示p的内容不能被改变,但是p指向的对象,是可以通过*p指针来改变的。
简单来说:
如果 const 位于 * 的左侧,则 const 就是用来修饰指针所指向的变量,即指针指向为常量;
如果 const 位于 * 的右侧,则 const 就是修饰指针本身,即指针本身是常量;
const int * const p,就什么也不能改了
也就是说const后面跟着是什么,就限制什么。是指针p,那就无法通过指针进行间接修改。是变量p,那就无法通过变量p进行修改,但是可以用指针进行修改。
所以最后我加了个const,就避免出现一些写反了的问题
库里写的是返回的char*,我写的是void,我还能怎么改进呢?
返回的就是目标的起始地址。
这样的作用呢,就方便我实现链式访问
这就是非常完美的代码写法了:
#include<assert.h>
char* my_strcpy(char*dest,const char*src)
{
assert(dest != NULL);//断言,如果条件为假就报错
assert(src != NULL);
char* ret = dest;
while (*dest++ = *src++)
{
;
}
return ret;//实现链式访问
}
int main()
{
char arr1[] = "hello world";
char arr2[] = "xxxxxxxxxxxxxxxxx";
my_strcpy(arr2, arr1);
printf("%s", my_strcpy(arr2, arr1));
return 0;
}
练习2:模拟strlen
#include<assert.h>
int my_strlen(const char* p)
{
assert(p != NULL);//一定不是*p !=NULL,这样我就解引用了,我是防止出现野指针的情况
int i = 0;
while (*p != '\0')
{
i++;
p++;
}
return i;
}
int main()
{
char arr[20] = "Hello,world";
printf("%d", my_strlen(arr));
return 0;
}
编程的常见错误
1.编译型错误
基本是属于语法错误,直接就能看到报错位置
2.链接型错误
基本是标识符名不存在或者拼写错误。看错误提示信息,然后定位问题所在。
3.运行时错误
基本是逻辑问题,需要通过调试进行修改。