16-程序的调试
vs中的debug和release
debug通常称为调试版本,它包含调试信息,并且不做任何优化,便于程序员调试程序
release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户更好地使用
F5快捷键:启动调试,经常用来直接跳到下一个断点处。从开始处运行程序,直到遇到第一个断点才会停止
F9快捷键:创建断点和取消断点。(首先把鼠标光标放到需要打断点的那行上)断点是指在代码的某一行打上一个标记,当程序运行到这一行时,会自动暂停(中断),此时程序调试模式。设置断点后按下F5,程序会直接跳到断点处,断点前面的代码会自动运行好(如果有sanf语句,要程序员输入完成后才会继续运行断点前的代码)
F10快捷键:逐过程执行代码,执行当前行,如果当前行包含函数调用,不会进入函数内部,而是直接将整个函数执行完毕
F11快捷键:为逐语句快捷键。执行当前行,如果该行包含函数调用,会进入函数内部进行单步调试
Ctrl+F10快捷键:运行到光标处。让调试器从当前位置直接运行到你光标所在的那一行
shift+F5快捷键:停止调试。直接终止当前的调试会话
Ctrl+F5:开始执行而不调试
#include<stdio.h>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("hello\n");}return 0;
}在X86环境下:
运行会出现死循环,无限次输出hello
在X64环境下:
运行会输出13次hello,然后报错
死循环的原因:在栈区(i,arr都是局部变量),数组地址随着i的增加从低位到高位,到了i=10时,到i=12时,arr[12]会和i(值为12)的地址重合,i被赋值成0,从而i会永远小于等于12,所以跳不出循环
#include<stdio.h>
#include<string.h>int main()
{char arr1[15] = "pythonjavac++c";char arr2[] = "hello world";strcpy(arr1,arr2); //strcpy()是头文件<string.h>库中的函数,用于拷贝字符串,第一个参数是要拷贝到的字符串,第二个参数是要拷贝的(源字符串)printf("%s\n", arr1); //输出hello worldreturn 0;
}
#include<stdio.h>void my_strcpy(char* destination, char* source)
{while (*source != '\0'){*destination = *source;destination++;source++;}*destination = *source; //使用<string.h>头文件中的库函数strcpy时会把字符串结束符空字符'\0'也拷贝过去,所以在手动实现这个功能时也要把'\0'给拷贝过去
}int main()
{char arr1[] = "hello world";char arr2[] = "cprogram";my_strcpy(arr1,arr2); //数组名是数组首元素的地址(两种情况除外:使用sizeof是整数数组的大小,&数组名取得是整个数组的地址,虽然和首元素地址相同)printf("%s\n", arr1);return 0;
}
#include<stdio.h>
#include<assert.h>void my_strcpy(char* destination, char* source)
{assert(source != NULL); //设置断言:指针不能为空,为空不能解引用assert(destination != NULL);while (*destination++ = *source++) //空字符'\0'的ASCII码值为0,当source解引用为空字符'\0'被赋值给对应的解引用的destination时,while循环遇到假(0)循环结束 因为此时destination已经被赋值为空字符,所以后面就不用解引用赋值{; //空语句,占位}}int main()
{char arr1[] = "hello world";char arr2[] = "cprogram";my_strcpy(arr1,arr2); //数组名是数组首元素的地址(两种情况除外:使用sizeof是整数数组的大小,&数组名取得是整个数组的地址,虽然和首元素地址相同)printf("%s\n", arr1);return 0;
}尽管优化了,但是还不够,比如万一有人喝醉了写酒把目标字符数组和源字符数组的位置写反了,就得不到正确的结果,需要在函数定义时形参中的源字符数组前面加const关键字修饰,变成常量,不可修改,所以就算位置写反了,对常量进行修改,编译器就会立马报错
void my_strcpy(char* destination,const char* source)
{assert(source != NULL); //设置断言:指针不能为空,为空不能解引用assert(destination != NULL);while (*destination++ = *source++) //空字符'\0'的ASCII码值为0,当source解引用为空字符'\0'被赋值给对应的解引用的destination时,while循环遇到假(0)循环结束 因为此时destination已经被赋值为空字符,所以后面就不用解引用赋值{; //空语句,占位}}
#include<stdio.h>int main()
{const int a = 10;int* p = &a;*p = 20;printf("a=%d\n", a);return 0;
}上面的代码说明了,一个变量通过const关键字声明是一个常量不能被修改,但还是可以通过指针来修改这个常量变量;
可以在定义指针时用const修饰,即const int* p = &a; 这样指针变成指向常量的指针,不能修改指针指向的值了,否则会报错。这样a就不会被指针修改了
const int* p; 和 int* const p; 是不同的!!!
- const int* p; 说明指针变量p指向的是const int类型的变量,指针变量的类型是const int*,就是说指针变量指向的值不能修改(即不能通过解引用修改),但指针本身可以改,可以通过指针变量赋值(即取地址)来修改指针本身,不过改指针变量就不指向原先那个常量const int了
- int* const p;说明p的类型是int* const,是不可被修改的(不可通过取地址再修改指针变量本身,所以在定义时就应该要初始化),但是可以通过解引用修改指向的那个值
- const int* const p;指向的变量和指针本身都不可被修改
- 总结,const离谁更近(指针名还是指针指向的类型名),就修饰谁为不可变的常量
#include<stdio.h>
#include<assert.h>char* my_strcpy(char* destination, const char* source)
{char* result = destination; //result存储着destination指向的字符数组 result不是二级指针而是一个一级指针,类型是char*assert(destination != NULL);assert(source != NULL);while (*destination++ = *source++) //后置递增运算符++的运算优先级高于解引用*符号 *a++ 等价于*(a++) 其实如果不清楚的话使用括号确保优先级更好{;}//return destination;//返回destination是错误的,得不到先要的结果,会输出空;因为while循环结束是destination指向的是空字符'\0',如果把它返回,由于返回的是数组首元素的地址,但此时destination首元素是'\0',后面没有元素了return result;
}int main()
{char arr1[50] = "hello python";char arr2[50] = "hypothecial假设的、假定的";printf("%s\n", my_strcpy(arr1, arr2));return 0;
}这段代码让my_strcpy有返回值,返回值的类型是字符数组指针。因为这实现了和strcpy完全一样的功能,还可以链式调用(返回值作为其他函数的参数)
如何写出优秀的代码:
- 代码运行正常
- bug很少
- 效率高
- 可读性高
- 可维护性高
- 注释清晰
- 文档齐全
常见的coding技巧:
- 使用assert
- 尽量使用const
- 养成良好的编码风格
- 添加必要的注释
- 避免编码的陷阱
一个比较好的例子就是上面模拟实现strcpy函数的过程
模拟实现头文件<string.h>的库函数strlen
strlen用于计算字符串的长度,返回值是一个类型为size_t的无符号整数
sizeof是内置的操作符,用于计算对象或者类型在内存中占用的字节数,返回值也是一个size_t类型的无符号整数,但是对象是字符串时,和strlen函数相比,sizeof操作符会计算字符串的结尾符号(空字符'\0'),而strlen函数返回的结果不会包含这个空字符
#include<stdio.h>
#include<string.h>int main()
{printf("%zu\n", strlen("hello world"));//11printf("%zu\n", sizeof("hello world"));//12return 0;
}哇去,又学到一个快捷键!按住shift键不松,再按右箭头会连续地选中这一行的文字,按一次向后选一个文字!
ctrl+d:向下复制一行
再网上找了一下:
shift加箭头键:
- shift加左右箭头:从当前光标位置开始,向左或者向右选择一个字符
- shift加上下箭头:从当前光标位置开始,向上或者向下选择一整行
- 在文件资源管理器(文件夹)或者桌面上,shift加上下箭头用于连续选中多个文件或文件夹(以前我只直到shift加鼠标选中才能连续选中多个)
ctrl加箭头:(核心作用:快速移动或跳跃)
- 在文本编辑器、输入框、word文档等中,ctrl加左右箭头会向左或向右跳跃一个“词”(通常是遇到空格、标点符号或者中英文分界时就视为一个词的结束),这比一个一个移动字符快得多
- ctrl加上下箭头,行为因软件而异。在word中,通常是光标向上或者向下移动一个段落
强力组合技:ctrl+shift+左箭头或右箭头---快速选择一个完整的词
#include<stdio.h>
#include<assert.h>size_t my_strlen(const char* input_str)
{assert(input_str);size_t count = 0;while (*input_str != '\0'){count++;input_str++;}return count;
}
int main()
{char test_str[50] = "hello world";printf("%zu\n", my_strlen(test_str));//11return 0;
}
#include<stdio.h>
#include<assert.h>size_t my_strlen(const char* input_str)
{assert(input_str);size_t count = 0;while (*input_str != '\0'){count++;input_str++;}return count;
}
int main()
{char input_str[50];scanf("%s", input_str);printf("%zu\n", my_strlen(input_str)); //如果输入hello world 程序指挥输出5不会输出11return 0;
}正如上面代码的注释所说的一样,由于scanf的特性,遇到空格、制表符、换行符会停止读取输入的内容
所以当输入”hello world“时,scanf函数读取到hello后面的空格时,就会立即停止读取,并”hello“后面加'\0',表字符串的结束。所以数组input_str存储的是”hello\0“,而”world“部分还停留在缓冲区中,没有被读取
如果要完整地读取包含空格的输入,有一下几种方法:
第一种方法:fgets函数subtitute scanf函数
#include<stdio.h>
#include<assert.h>
#include<string.h>size_t my_strlen(const char* str)
{assert(str != NULL);size_t count = 0;while (*str != '\0'){count++;str++;}return count;
}int main()
{char buffer[100];fgets(buffer, sizeof(buffer), stdin);size_t len = my_strlen(buffer);printf("手动实现strlen函数:%zu\n", len-1);printf("调用库函数strlen:%zu\n", strlen(buffer));return 0;
}运行:
hello
手动实现strlen函数:6
调用库函数strlen:6可是明明hello字符长度是5,为什么都输出了6
这是因为fgets函数会包含换行符,长度会比用scanf多1
#include<stdio.h>
#include<string.h>int main()
{char str[50];scanf("%49s", str); //数组名本身就表示地址(首元素的地址),所以不用加取地址符号& 49s是为了防止缓冲区溢出,限制scanf最多只能读取49个字符,为'\0'腾出空间printf("%zu\n", strlen(str));printf("%zu\n", sizeof(str));return 0;
}运行:
hello
5
50手动查找并替换换行符:
#include<stdio.h>
#include<assert.h>
#include<string.h>size_t my_strlen(const char* str)
{assert(str != NULL);size_t count = 0;while (*str != '\0'){count++;str++;}return count;
}int main()
{char buffer[100];fgets(buffer, sizeof(buffer), stdin);size_t len = my_strlen(buffer);//手动查找并替换换行符if (len > 0 && buffer[len - 1] == '\n'){buffer[len - 1] = '\0';}printf("手动实现strlen函数:%zu\n", len); //应该len-1printf("手动实现strlen函数并且直接输出,不用变量存储:%zu\n", my_strlen(buffer));printf("调用库函数strlen:%zu\n", strlen(buffer));return 0;
}运行:
hello
手动实现strlen函数:6
手动实现strlen函数并且直接输出,不用变量存储:5
调用库函数strlen:5
hello world
手动实现strlen函数:12
手动实现strlen函数并且直接输出,不用变量存储:11
调用库函数strlen:11编程的常见错误:
1.编译错误(出现在编译期间)
语法错误
2.链接错误(出现在链接期间)
使用了,但是没有定义或者写错了名字,总之,编译器没有找到这个符号
3.运行错误
结果可以正常出来,但是不是我们想要的
调试解决
