调试是一个程序员所要必备的技能,我们再遇到程序编译器无法发现的问题时要能够通过调试一步一步的来找到问题所在。
bug原意指虫子,有一天小飞蛾意外飞进了正在工作的计算机电路里导致了计算机工作发生故障,工作人员对当时的计算机进行了细致的检查后最终发现了这只被夹扁的飞蛾,之后计算机便恢复了正常工作状态。这只飞蛾顺手被夹在了格蕾丝-霍普的工作笔记里并备注为bug
,bug
便诞生了。
有了bug
就必须要找出这个bug
,这个操作过程叫做调试debug/debugging
。
调试就像推理,从迷雾中寻找真相。
调试是发现和减少计算机程序或电子仪器设备中程序错误的一个过程。
Debug称为调试版本,包含调试信息,不做任何优化,以便于程序员进行调试。
Release称为发布版本,不包含调试信息,进行了各种优化,程序在代码大小和运行速度上都是最优的,以便于用户使用。 相比调试版本,发布版本重点优化了体积大小与性能效率两方面。
不是所有程序都能进行调试,包含调试信息的程序才能进行调试。
对于同一个程序分别在Debug和Release版本下的一些差异
#include <stdio.h>
int main() {
printf("Hello World!\n");
return 0;
}
#include <stdio.h>
int main() {
int arr[10] = { 0 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
for (i = 0; i < sz; i++) {
arr[i] = i;
}
return 0;
}
Debug版本下:可以进行调试
Release版本下:不可进行调试,程序直接执行完毕
注意变量i与数组arr建立先后关系的差异导致程序运行的不同。
#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;
}
Debug版本下:陷入死循环 Release版本下:
#include <stdio.h>
int main() {
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
for(i=0; i<=12; i++){
arr[i] = 0;
printf("hello!\n");
}
return 0;
}
Debug版本下:
程序崩溃, Release版本下:
选择Debug模式才能进行调试。
F5
**启动调试,遇到断点时停下,如果没有断点就直接完整执行程序。 ** 如果有多个断点,按下F5可以使程序从当前断点直接运行到下一个逻辑上的断点。(注意逻辑断点与实际断点可能并不一定完全等价,例如断点设置在一个循环的内部时,逻辑断点是下一次循环的断点,但可能实际的断点位置不变)。
ctrl + F5
开始执行但不调试。直接运行程序,如果程序没有编译链接过,该操作还会进行新程序的编译与链接。
F9
在某一行设置断点或者取消某一行已有的断点。 可以在程序的任意位置设置断点,但在空语句处的断点没有意义。 断点可以使程序在我们预期停止的地方停下来.
F10
逐过程调试,程序停在main函数入口处,可以通过多次按F10来使程序在可观察的状态运行。通常用来处理一个过程,一个过程可以是一次函数调用、一条语句等。
F11
逐语句调试,每次都只执行一条语句,使用F11可以进入到用户自定义函数的内部,比F10更加细致(因为F10并不能进入用户自定义函数内部)。
自动窗口
不需要手动输入,随着调试的进行程序中变量、数组等信息会自动显示相关信息,注意自动窗口显示的是调试附近的相关信息,距离较远的已经调试过得或未调试的都不会再显示,也就是说信息显示是不固定的,观察起来并不方便。
监视
需要手动输入想知道的信息,只有手动删除输入的信息时才会删除。信息的显示是固定的,方便观察。
内存
查看程序中各数据在内存中的信息。
调用堆栈
调用堆栈,主要是程序有多个函数并且存在嵌套调用时可以观察到函数的调用关系和当前调用所处的位置。
反汇编
查看程序的汇编代码,更加底层。
寄存器
寄存器是CPU内部用来存放数据的一些小型储存区域,用来暂时存放参与运算的数据和运算结果,有着非常高的读写速度。可以观察到当前运行情况下寄存器的使用信息。
便于调试 运行良好 效率高 可读性高 可维护性高 注释清晰 文档齐全
写代码时的一些技巧:
先思考再动手 使用assert(断言) 使用const 有良好的代码风格 注意写上应有的注释
const修饰可以减小权限,使程序更加安全。 对于普通变量const修饰后普通变量不能直接被修改,否则程序出错。但是可以通过对变量的地址解引用修改变量的值。
#include <stdio.h>
int main(){
int a = 10;
const int b = 10;
int const c = 10;
a = 5;//true
b = 5;//error,不能修改
c = 5;//error
int *pb = &b;
int *pc = &c;
*pb = 5;//true,可以修改
*pc = 5;//true
return 0;
}
对于指针变量有两种情况:
const在
*
左边,此时const修饰的是指针所指向的对象,而不是指针本身。不能通过指针解引用的方式改变指针所指向的对象,但可以不通过指针而直接修改那个对象。const在
*
右边,此时const修饰的是指针本身。指针获得一个变量的地址后不能在被另一个地址赋值。
#include <stdio.h>
int main(){
int a = 10;
int b = 10;
int c = 10;
int d = 10;
//无限制的指针变量p1
int *p1 = &a;
//指针指向对象受到限制的指针p2
const int *p2 = &b;//int const *p2 = &a;
a = 5;//true
//指针本身受到限制的指针p3
int* const p3 = &c;
p3 = &a;//error
//指针本身和指针指向对象都受到限制的指针p4
const int* const p4 = &d;
*p4 = 5;//error
d = 5;//true
p4 = &a;//error
return 0;
}
#include <stdio.h>
#include <assert.h>
//使用const修饰指针str所指向的对象,使其不能被更改,否则程序出错
int my_strlen(const char *str){
//传入的指针可能是空指针NULL,此时str就是野指针,没有指向对象,会导致程序出现错误
assert(str != NULL);
int cnt = 0;
while(*str != NULL){
str++;
cnt++;
}
return cnt;
}
int main(){
char str[] = "abcdef";
int len = my_strlen();
printf("%d\n", len);
return 0;
}
从一个代码文件(源文件)经过编译、链接过程到得到可执行程序
在编译期间出现的错误,编译器一般会给出对应错误的相关位置代码行,是语法方面的错误,相对简单。
在链接期间出现的错误,链接器把包括源文件在内的多个文件(如头文件)链接在一起形成一个可执行文件。不是语法错误,一般是代码中出现了未定义的函数等外部符号,链接错误一般不给出错误出现的代码行,但会标识除未定义的符号,可以使用查找功能进行排查。
#include <stdio.h>
void print(){
printf(" world\n")
}
int main(){
printf("hello");
test();//该函数未定义;
Print();//该函数虽然定义了,但定义的函数名与引用的函数名不匹配
return 0;
}
逻辑错误等,需要进行调试找出错误所在,最不好找!。
调试技能是程序员所要必备的技能,随着项目代码量的增加,调试寻找问题也就显得更加重要。不同编译器调试功能可能会有不同,但调试的方法是相同的。
END