VS实用调试技巧
BUG是什么
bug本意只“昆虫”或“虫子”,一般指在电脑系统或程序中,隐藏着未被发现的缺陷或问题,简称为程序漏洞。
“Bug”是由格蕾丝·赫柏(美国海军的一位电脑专家)在1947年9月9日发现,这个故事很简单,就是一只飞蛾(生性喜光)飞进了巨大计算机内部,在一组继电器的触点上导致了机器故障。
调试
当我们发现程序中存在的问题时,下一步就是找到并解决问题。这个寻找问题的过程称为调试,英文“debug”(消灭bug)。
当我们调试一个程序,首先是承认出现了问题,然后通过各种手段去定位问题位置(方式包括:逐过程调试、隔离和屏蔽代码等),找到问题后确定错误产生的原因,再修复代码,重新测试。
debug和release
Debug通常称为调试版本,它包含调试信息,并且不做任何优化,便于程序员调试程序;
程序员写代码时,需要经常性的调试代码,就将这里设置为dubug,这样编译产生的是debug版本的可执行程序,其中包含调试信息,是可以直接调试的。
Release称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。当程序员写完代码,测试再对程序进行测试,直到程序的质量符合交付给用户使用的标准,这个时候就会设置为release,编译产生的就是release版本的可执行程序,这个版本是用户使用的,无需包含调试信息等。
事实上,release版本明显比debug版本小。
VS调试快捷键
最常用:
F9:创建断点和取消断点
断点的作用是可以在程序的任意位置设置断点,打上断点就可以使得程序执行到想要的位置暂停执行,接下来我们就可以使用F10、F11这些快捷键,观察代码的执行细节。
条件断点:满足一定条件才可触发断点。
F5:启动调试,经常用来直接跳到下一个断点处,一般与F9配合使用。
F10:逐过程,通常用来处理一个过程,一个过程可以是一次函数调用,或者是一条语句。
F11:逐语句,就是每次都执行一条语句,但是这个快捷键可以使我们的执行逻辑进入函数内部。【在函数调用的地方,想进入函数观察细节,必须使用F11,如果使用F10,直接完成函数调用】
CTRL+F5:开始执行不调试,如果想让程序直接运行起来而不调试就可以直接使用。
更多快捷键:VS中常用的快捷键_vs快捷键-CSDN博客
监视和内存观察
在调试过程中,如果要观察代码执行过程中上下文环境中的变量值,有如下方法:
【观察的前提条件一定是开始调试后观察。】
1.监视
在开始监视后,在菜单栏中【调试】->【窗口】 ->【监视】,打开任意一个监视窗口,输入想要观察的对象。
2.内存
如果监视窗口看的不够仔细,也可以观察变量在内存中的存储情况,选择在菜单栏中【调试】->【窗口】 ->【内存】,打开内存窗口,在内存窗口中观察数据,要在地址栏输入:arr、&num、&c这类地址,就能观察到该地址处的数据。
除此之外,调试窗口还有:自动窗口,局部变量,反汇编、寄存器等窗口,自行验证使用一下。
编程常见错误归类
1.编译型错误
一般是语法错误
2.链接型错误
-
标识符名不存在
-
拼写错误
-
头文件没包含
-
引用的库不存在
3.运行时错误
函数递归
递归的含义
递归是一种解决问题的方法,在C语言中,递归就是函数调用自身。
//instance #include<stdio.h> int main() { printf("hehe\n"); main(); return 0; }//这是一个简单的递归,但是会陷入死循环,导致栈溢出
1.1递归的思想
把一个大型复杂问题层层转化为一个与原问题相似且规模更小的子问题来求解,直至子问题不可再分,递归结束。
1.2递归的限制条件
-
递归存在限制条件,满足此条件递归终止;
-
每次递归后都会接近限制条件。
递归举例
2.1 n的阶乘
#include<stdio.h> int Fact(int n) { if(n==0) return 1; else return n*Fact(n-1); } int main() { int n = 0; scanf("%d",&n); int ret = Fact(n); printf("%d\n",ret); return 0; }
2.2 打印整数各位数组
#include<stdio.h> void Print(int n) { if(n>9) Print(n/10); printf("%d ",n%10); } int main() { int m; scanf("%d",&m); Print(m); return 0; }
递归与迭代
每次使用递归,都需要为本次函数调用在内存的栈区申请一块内存空间来保存函数调用期间的各种局部变量的值,这块空间被称为运行时堆栈或者函数栈帧,函数不返回,这些空间就一直占用,如果递归层次太深,就会浪费太多的帧栈空间,可能引起栈溢出的问题。
不想使用递归时,可以使用迭代的方式(通常采用循环方式)
//例如n的阶乘 int Fact(int n) { int i = 0; int ret = 1; for(i=1;i<=n;i++) { ret *=1; } return ret; }
事实上,迭代实现往往比递归实现效率更高。
求第n个斐波那契数
//递归方法 #include<stdio.h> int Fib(int n) { if(n<=2) return 1; else return Fib(n-1)+Fib(n-2); } int main() { int n = 0; scanf("%d",&n); int ret = Fib(n); printf("%d\n",ret); return 0; }//当输入一个较大的数,例如50,这个程序需要很长时间才能算出结果,这是非常抵效的,原因在于递归过程中不断展开,层次越深,计算冗余也越多。
//简单实验一下,观察计算次数 #include<stdio.h> int count = 0; int Fib(int n) { if(n==3) count++; if(n<=2) return 1; else return Fib(n-1)+Fib(n-2); } int main() { int n = 0; scanf("%d",&n); int ret = Fib(n); printf("%d\n",ret); printf("\ncount = %d\n",count); return 0; }
//迭代方式 int Fib(int n) { int a = 1; int b = 1; int c = 1; while(n>2) { c = a+b; a = b; b = c; n--; } return c; }
递归拓展学习的两个问题
-
青蛙跳台阶问题;
-
汉诺塔问题。
操作符详解
操作符的分类
-
算术操作符:+、-、*、/、%
-
位移操作符:<< >>
-
位操作符:&、|、^
-
赋值操作符:=、+=、-=、*=、/=、%=、<<=、>>=、&=、|=、^=
-
单目操作符:!、++、--、&、*、-、+、~、sizeof、(类型)
-
关系操作符:>、>=、<、<=、==、!=
-
逻辑操作符:&&、||
-
条件操纵符:?、:
-
逗号表达式:,
-
下标引用[]:[]
-
函数调用():()
-
结构成员访问:. 、->
原码、反码、补码
整数二进制表示方法有三种,即原码、反码、补码。
有符号整数的三种表示方法均有符号位和数值位两部分,2进制序列中,最高位的1位是被当作符号位,剩余的都是数值位。【符号位用0表示正,用1表示负】
【正整数的原码、反码、补码都相同】
负整数的三种表示方式各不相同:
-
原码:将数值按照正负数的形式翻译成二进制得到的就是原码;
-
反码:原码符号位不变,其他位依次取反(1变成0,0变成1);
-
补码:反码+1;
【对于整型来说,数据存放在内存中采用补码形式】
原因是:使用补码可以将符号位和数值域统一处理;同时,加法和减法也可以统一处理,此外,补码与原码互相转换,其运算过程相同,不需要额外的硬件电路。
位移操作符
-
<< 左移操作符
-
>>右移操作符
【位移操作符的操作数只能是整数】
4.1左移操作符
位移规则:左边抛弃,右边补0
#include<stdio.h> int main() { int num = 10; int n = num<<1; printf("n = %d\n",n);//输出20 printf("num = %d\n",num);//输出10 return 0; } //00000000000000000000000000001010 num的2进制表示 //00000000000000000000000000010100 n的2进制表示
4.2右移操作符
位移规则:(采取哪种取决于编译器)
-
1.逻辑右移:左边用0填充,右边丢弃;
-
2.算术右移:左边用该值原符号位填充,右边丢弃。
#include<stdio.h> int main() { int num = 10; int n = num>>1; printf("n = %d\n",n);//输出5 printf("num = %d\n",num);//输出10 return 0; }
【!对于位移运算符,不可移动负数位,这个标准是无定义的】
位操作符:&、|、^、~
【位操作符的操作数必须是整数】
-
按位与的计算规则:两个二进制操作数对应位都为 1 ,结果位才为 1 ,其余情况为 0 ;
-
按位或的计算规则:两个二进制操作数对应位只要有一个为 1 ,结果 位 就为 1 ,其余情况为 0 ;
-
按位异或的计算规则:两个二进制操作数对应 位 相同为 0 ,不同为 1。
#include<stdio.h> int main() { int num1 = -1; int num2 = 3; printf("%d\n",num1 & num2);//按位与,输出3 printf("%d\n",num1 | num2);//按位或,输出-1 printf("%d\n",num1 ^ num2);//按位异或,输出-4 printf("%d\n",~0);//取反,输出-1 return 0; }
//巧妙应用:不能创建临时变量,实现两个整数的交换 /*普通方式 int main() { int a = 10; int b = 20; a = a + b; b = a - b; a = a - b; printf("a = %d b = 5d",a,b); return 0; } 这种方式是存在局限性的,当a,b数字过大,以至于a,b之和可能超出int定义的范围 */ #include<stdio.h> int main() { int a = 10; int b = 20; a = a^b; b = a^b; a = a^b; printf("a = %d b = 5d",a,b); return 0; }//很好理解,自身进行异或运算,结果必为0,然而一个数与0异或必为自身,于是b=a^b^b=a;a=a^a^b=b.
两个练习:
//1.实现:求一个整数存储在内存中的二进制中1的个数 //方式1 #include<stdio.h> int main() { int num = 10; int count = 0; while(num) { if(num%2 == 1) count++; num = num / 2; } printf("二进制中1的个数为%d\n",count); return 0; } //方式2 #include<stdio.h> int main() { int num = -1; int i = 0; int count = 0; for(i=0;i<32;i++) { if(num & (1<<i))//条件语句,判断num与(1<<i)的按位与结果是否为真。(1<<i)表示将1左移i位,用于检查num的二进制表示中每一位是否为1。 count++; } printf("二进制中1的个数为%d\n",count); return 0; } //方式3 #include<stdio.h> int main() { int num = -1; int i = 0; int count = 0; while(num) { count++; num = num&(num-1); } printf("二进制中1的个数为%d\n",count); return 0; }
//2.二进制位置制0或置1 /*13的2进制为01101(后5位),将第五位修改为1,再改回0*/ #include<stdio.h> int main() { int a = 13; a = a | (1<<4); printf("a = %d\n",a); a = a & ~(1<<4); printf("a = %d\n",a); return 0; }
单目操作符
!、++、--、&、*、+、-、~、sizeof()、(类型)
逗号表达式
exp1,exp2,exp3,…,expN
逗号表达式,从左至右依次执行,整个表达式的结构是最后一个表达式的结果。
//代码1 int a = 1; int b = 2; int c = (a>b,a=b+10,a,b=a+1); //代码2 if(a = b + 1,c = a / 2,d > 0); //代码3 a = get_val(); count_val(a); while(a>0) { //业务处理 a = get_val(); count_val(a); } //使用逗号表达式改写为 while(a = get_val(),count_val(a),a>0) { //业务处理 }
下标访问 []、函数调用()
8.1下标引用操作符
操作数:一个数组名+一个索引值(下标)
int arr[10]; arr[9] = 10; //[]的两个操作数是arr和9.
8.2函数调用操作符
接受一个或者多个操作数:第一个操作数是函数名,剩余的操作数就是传递给函数的参数。
#include<stdio.h> void test1() { printf("hehe\n"); } void test2(const char *str) { printf("%s\n",str) } int main() { test1(); test2("hello bit."); return 0; }
结构成员访问操作符
1.结构体
C语言提供了内置类型,但是使用单一的内置类型无法满足对于复杂对象的属性描述(例如描述一个学生的基本信息),结构体让程序员可以自己创造适合的类型。
【结构是一些值的集合,这些值称为成员变量,结构的每个成员可以是不同类型的变量,如:标量、数值、指针,甚至是 其他结构体。】
结构的声明
struct tag { member-list; }variable-list; //描述一个学生 struct Stu { char name[20]; int age; char sex[5]; char id[20]; };
结构体变量的定义和初始化
//变量的定义 struct Point { int x; int y; }p1;//声明类型的同时定义变量1 struct Point p2;//定义结构体变量p2 struct Point p3 = {10,20}//初始化 strcut Stu//类型声明 { char name[20]; int age; }; struct Stu s1 = {"tuling", 20};//初始化 struct Stu s2 = {.age=20,.name="fengnuoyiman"};//指定顺序初始化 struct Node { int data; struct Piont; strcut Node* next; }n1 = {10,{4,5},NULL};//结构体嵌套初始化 struct Node n2 = {20,{5,6},NULL};
2.结构成员访问操作符
结构体成员的直接访问是通过点操作符访问的,点操作符接受两个操作数。
#include<stdio.h> struct Point { int x; int y; }p={1,2}; int main() { printf("x: %d y: %d\n",p.x,p.y); return 0; }
【使用方式:结构体变量.成员名】
结构体成员的间接访问
当可处理的不是结构体变量而是一个指向结构体的指针,使用->
#include<stdio.h> struct Point { int x; int y; }; int main() { struct Point p = {3,4}; struct Point *ptr = &p; ptr->x = 10; ptr->t = 20; printf("x = %d y = %d\n",ptr->x,ptr->y); return 0; }
【使用方式:结构体指针->成员名】
举例应用:
#include<stdio.h> #include<string.h> struct Stu { char name[15]; int age; }; void print_stu(struct Stu s) { printf("%s %d\n",s.name,s.age); } void set_stu(struct Stu* ps) { strcpy(ps->name,"葫芦娃");//简单理解为把后面的值赋到前面 ps->age = 28; } int main() { struct Stu s = {"小黑子",20}; void print_stu(s); void set_stu(&s); void print_stu(s); return 0; }
操作符的属性:优先级、结合性
C语言的操作符有两个重要属性:优先级、结合性,这两个属性决定了表达式求值的计算顺序。
优先级:一个表达式包含多个运算符,运算的先后顺序由优先级决定;
结合性:当多个运算符的优先级相同,通过结合性决定先计算哪个。
大部分运算符是左结合(从左到右执行),少数运算符是右结合(从右到左执行)。
运算符先后顺序参考:C 运算符优先级 - cppreference.com
表达式求值
整型提升
C语言中的整型算术运算总是至少以缺省(就是“默认”的意思)整型类型的精度来进行的。为了获得这个精度,表达式的字符和短整型操作数在使用之前被转换称为整型提升。
整型提升的意义:
表达式的整型运算要在CPU的相应运算器件内执行,CPU内整型运算器(ALU)的操作数长度一般就是int的字节长度,同时也是CPU的通用寄存器的长度。因此,即使两个char类型的相加,在CPU执行实际上也要先转换为CPU内整型操作数的标准长度。通用CPU是难以直接实现两个8比特字节直接相加运算。所有,表达式中各种长度可能小于int长度的整型值,都必须先转换为int或unsigned int,然后才能送入CPU去执行运算。
char a,b,c; a = b + c;
b,c的值被提升为普通整型,然后再执行加法运算,运算完成之后,将结果截断,再存储与a中。
整体提升的方法:
-
有符号整数提升是按照变量的数据类型的符号位来提升的;
-
无符号整数提升,高位补0.
负数的整型提升 char c1 = -1; 变量c1的补码:11111111 因为char为有符号的char 高位补充符号位,提升后结果为:11111111111111111111111111111111 正数的整型提升 char c2 = 1; 00000001 ---> 00000000000000000000000000000001
算术转换
如果某个操作符的各个操作数属于不同的类型,那么除非其中一个操作数的转换为另一个操作数的类型,否则操作无法进行。
寻常算术转换
long double double float unsigned long int long int unsigned int int
从下往上转换
问题分析表达式
即使有了操作符的优先级和结合性,写出的表达式依然有可能不能通过操作符的属性确定唯一的计算路径,特别复杂的表达式存在潜在风险,所以不要写特别复杂的表达式。