此篇笔记也会插入一些代码。调试需要实际操作去调试,观察效果。
目录
一、Debug、Release
Debug版本为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。
Release版本为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
二、vs里面的一些快捷键
F5启动调试,与F9配合。F9为断点,程序执行时在这个断点处停止 ,并且表示可能程序的错误出现这部分代码
F5:经常用来直接调到下一个断点处。
F9:创建断点和取消断点。断点的重要作用,可以在程序的任意位置设置断点,这样就可以使得程序在想要的位置随意停止执行,继而一步步执行下去。
F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句
F11:逐语句,都是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部(经常用)
Ctrl + F5:开始执行不调试,如果你想让程序直接运行起来而不调试就可以直接使用。
三、调试的时候查看程序当前信息
查看临时变量的值
自动窗口:程序进行过程中,打开自动窗口,就会自动打开程序进行到这的要观察的一些变量列出来观察。
局部变量:和自动窗口一样的外观。但是会把程序中所有局部变量都加起来观察,当经过一块程序后,其中的局部变量就不能看了。不能像自动窗口,随心所欲监视谁。
监视:手动添加监视对象
内存:可以观察内存存储的东西
反汇编:观察每一句c语言对应的汇编代码
寄存器:观察寄存器
调用堆栈:
void test2()
{
printf("hehe\n");
}
void test1()
{
test2();
}
void test()
{
test1();
}
int main()
{
test();
return 0;
}
调用堆栈后,先出main,然后test,依次出现,从下往上出现。而到达test2函数后,实现test2函数后,test2就消失了,然后test1也消失,就像是栈区传入参数一样,栈顶传入,传出时依次向下消失。
看一个小题
int main()
{
int i = 0;
int arr[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
for (i = 0; i <= 12; i++)
{
printf("hehe\n");
arr[i] = 0;
}
system("pause");
return 0;
}
结果是死循环。调用调试后,当i = 10时,程序会继续运行,此时其实也就是越界访问了,出现arr[10], arr[11],arr[12], 之后开始继续循环,arr[12]为0,i也变为0。打印i和arr[12]的地址,是一样的。这其中的原因还需要写一写
内存中的栈区被分成许多个小内存块,这些内存块的排列顺序是地址高低之分。高地址的地址号大。栈区默认先使用高地址空间,再使用低地址空间。而数组在内存中连续存放,随着下标增长,地址由低到高。 原本分配给arr的0-9的空间,循环到10后,进入一个不属于arr的空间,随机数,之后11,12,如果i的空间和循环到12的那个空间正好一样,那就开启了死循环。
所以要合适地写程序,要不不知道会发生什么。不同编辑器不一样的位置会开始死循环。
但是这个代码在release幻境下就不会死循环,因为进行了优化。
printf("%p\n", arr);
printf("%p\n", &i);
观察地址,Debug下i的地址高于arr,但是Release下arr高于i。这就是进行了优化。
四、常见的敲打技巧
使用assert (断言)
尽量使用const
养成良好的编码风格
添加必要的注释
避免编码的陷阱
五、模拟实现strcpy这个函数
int main()
{
char arr1[] = "*************";
char arr2[] = "zyd";
strcpy(arr1, arr2);
printf("%s\n", arr2);
return 0;
}
strcpy(目的地,源头),把arr2的内容放到arr1里头去。现在改成自定义一个函数实现此功能。
void my_strcpy(char* dest, char* src)
{
while (*src != '\0')
{
*dest = *src;
src++;
dest++;
}
*dest = *src;
}
int main()
{
char arr1[] = "*************";
char arr2[] = "zyd";
my_strcpy(arr1, arr2);
printf("%s\n", arr2);
return 0;
}
这个代码,其实一般般,需要优化。
void my_strcpy(char* dest, char* src)
{
while (*dest++ = *src++)
{
;
}
}
当运行到*src = \0后,while的判断为0,所以停止执行。这样有所优化,但还不满足。
void my_strcpy(char* dest, char* src)
{
if (dest != NULL && src != NULL)
{
while (*dest++ = *src++)
{
;
}
}
}
如果传参时传入了NULL,那么对程序是不好的。这时候应该加入判断。但是这样操作后,不利于程序员发现问题,程序发现是NULL后,就不输出东西了,也一样执行。
#include <assert.h>
void my_strcpy(char* dest, char* src)
{
assert(dest != NULL);
assert(src != NULL);
while (*dest++ = *src++)
{
;
}
}
assert,错则报错,对则通过。 再继续优化
void my_strcpy(char* dest, const char* src)
{
assert(dest != NULL);
assert(src != NULL);
while (*dest++ = *src++)
{
;
}
}
加个const,*src就不能被修改,防止while的括号里写反顺序。
关于const
int main()
{
const int num = 10;
int* p = #
*p = 20;
printf("%d\n", num);
return 0;
}
虽然加上了const,但是num的值还是被改成20,这是一种违反规则的写法。如果要把num地址给p,编译器会报出警告不让这样做。
int main()
{
const int num = 10;
const int* p = #
*p = 20;
printf("%d\n", num);
return 0;
}
这样p也不能改了。如果是int num = 10,const int* p,此时不能通过p来改变指向的内容的数值,也就是说不能这样写*p = 20,但是可以用其它不是const修饰的指针来更改;而int* const p,说明p所指向的内容不能再更改,p只能指向这一个地址,而const int* p则可以更改指向的地址,但int* const p可以修改指向的内容的数值。现在是const int num,也就是num不能被改变,那么int* const p也就无法改变num,而const int* p,本来就不能修改*p。
回到代码中,src是源头,src不要被改变,所以const char* src,这样下面while的括号里写反后就会及时报错。
void my_strcpy(char* dest, const char* src)
{
assert(dest != NULL);
assert(src != NULL);
while (*dest++ = *src++)
{
;
}
}
int main()
{
char arr1[] = "*************";
char arr2[] = "zyd";
my_strcpy(arr1, arr2);
printf("%s\n", arr2);
return 0;
}
这样的代码之后,还可以再作改进。对于strcpy,这个函数是有返回值,将数据拷贝到目的地后,要返回这个目的地。所以要去掉void,加上返回值。不能直接写return dest。因为代码块运行到那里,dest已经不是之前的dest了,所以要事先保存。所以main函数里面也可以做修改。
char* my_strcpy(char* dest, const char* src)
{
char* ret = dest;
assert(dest != NULL);
assert(src != NULL);
while (*dest++ = *src++)
{
;
}
return ret;
}
int main()
{
char arr1[] = "*************";
char arr2[] = "zyd";
//my_strcpy(arr1, arr2);
printf("%s\n", my_strcpy(arr1, arr2));
return 0;
}
将一个函数的返回值作为另一个函数的参数,这也就是链式访问: printf("%s\n", my_strcpy(arr1, arr2))。
所以整个优化过程就是while括号改变(节省代码量),加上assert(保证传参成功,防止野指针),加上const(保证代码正常循环),改成char*类型(模拟strcpy加上返回值,做出链式访问)。
这里面也需要写注释
char* my_strcpy(char* dest, const char* src)
{
char* ret = dest;
assert(dest != NULL);
assert(src != NULL);
//把src指向的字符串拷贝到dest指向的空间,包含'\0'字符
while (*dest++ = *src++)
{
;
}
return ret;
}
int main()
{
char arr1[] = "*************";
char arr2[] = "zyd";
//my_strcpy(arr1, arr2);
printf("%s\n", my_strcpy(arr1, arr2));
return 0;
}
这样代码就完善了。
练习--strlen
int my_strlen(const char* str)防止字符串被改变
{
int count = 0;
assert(str != NULL);//保证指针有效性
while (*str++)
{
count++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
int len = my_strlen(arr);
printf("%d\n", len);
return 0;
}
六、编程常见错误
编译型错误
语法错误。直接看错误提示信息,解决问题。或者凭借经验修改。
连接性错误
比如头文件,传数据,使用未被定义的函数等等。无连接属性。看错误提示信息,主要在代码中找到错误信息中的标识符,然后定位问题所在,一般是标识符名不存在或者拼写错误。
运行时错误
借助调试,逐步定位问题。难。
调试需要自主动手做,才会有大进步,有成就感,促进知识积累。此篇尽量展现了调试过程,在vs code调试,只能尽量去理解,毕竟没有图,还是比较难受的。
结束。