Visual Studio调试&Code技巧

一、Debug&Release版本

Debug版本:调试版本,不对代码进行任何优化,保存着调试信息,用于程序员写入、检查自己代码。 

Release版本:发布版本,对带代码的运行速度和大小进行了大幅度的优化,不保留调试信息(无法进行调试),用于测试人员对程序员代码进行测试,通常是公司向用户发布的版本。

接下来给大家演示一下两个版本在代码大小上的区别:

Debug版本:演示程序大小为40KB;

Release版本:仅有10KB。

注:x86的环境下Debug和Release文件夹都在VS创建的文件里,而x64环境下会在VS所创文件夹中单独开一个x64的文件夹,其中放Debug、Release文件夹:

二、常用的VS调试执行快捷键及作用

        一些便于写代码的快捷键在此处就不提了,以后有机会再做一个汇总,今天主要讲讲有关于调试的一些快捷键:

下面简单说明一下断点和条件断点的创建:鼠标点击某行,F9便能创建断点。

右键断点,点击条件:

这个循环很大,我们假设错误出现在后一半循环中,只需要调试后半段,我们赋予条件a == 5000时开始断点的调试:

这样一个条件断点就创建完成了,我们在后续调试过程中便可以直接跳到a==5000处开始调试。

三、调试窗口

我们在没有进行调试时,在VS上方的调试--->窗口中是这样的:

在我们F10或者F11进行调试时,在这一过程中,窗口中就有不一样的选项了:

我们最常用的监视窗口,有4个并行窗口(监视1/2/3/4/),可以在其中输入想要看到变化的多个变量然后去调试发现它们的变化。

在监视窗口中,如果想要监视二维数组的变化,我们可以输入数组名+ ,+数字的方式监视二维数组的每行每列。

调用堆栈:主要是用来理清楚函数之间的调用逻辑,当多个函数之间进行复杂的调用时,如果无法简单的区分理清它们之间的逻辑的话,我们可以通过调用堆栈来一步一步梳理:

点击调用堆栈后我们点击显示外部代码,然后一步步调试:

当我们调试到函数A2中时,调用堆栈中便按顺序出现了main、A1、A2三个函数。

在打印完hehe退出A2函数后,相应的调用堆栈中A2函数也会消失。

同理调用完A1后A1在调用堆栈中消失,当回到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\n");
	}
	return 0;
}

乍一看,这个代码访问数组越界,应该会报错,但是实际打印会死循环输出hehe:

为什么会出现死循环呢?似乎跟我们的理解不太一样啊,这时候我们通过调试来一步步观察数组内部甚至是越界后发生的变化:

当我们刚刚调试到for循环时,我们发现 i 和arr数组已经被我们初始化完成了:

那么我们紧接着进行循环内的调试:当还没有越界访问时,我们发现一切正常:

接下来我们监视一下arr[10]、arr[11]、arr[12],看看到底发生了什么导致了死循环:

当我们把他仨写入监视中时,其实就已经初现端倪了,arr[10]、arr[11]都是随机值,怎么你arr[12]跟i一样呢?

我们走调试看看:

调试完 i=11的节点,我们发现,越界访问的arr[10]与arr[11]居然被改成0了!但是于此同时,不难发现arr[12]与 i 的值始终绑定相同。

这是否意味着什么呢?

我们接着调试:i 取12的时候arr[12]也变为了12:

当我们在循环内赋值0给arr[12]时,惊奇的是 i 也变为0了:

那么就可以解释死循环了:因为从此刻开始,i<=12的条件依然成立,这意味着循环一直在持续着,编译器没有空余时间来给我们报错越界访问。

那么 i 和arr[12]有什么关系呢?我们通过地址来判断一下:我们来监视一下 i 的地址:

相同的地址,所以 i 其实就存放在arr[12]的地址上:所以 i 的变化与arr[12]的变化是完全同步的。

那么为什么 i 的地址在数组arr[10]之后呢?

其实这也是有迹可循的:栈区中内存使用的习惯是由高地址向低地址进行使用去开辟空间,这个函数中优先定义 i 然后定义arr[10]这个数组,所以会使arr[10]的地址在偏低位置而 i 的存放地址在较高位置,而在VS、Debug、x86的严格环境下,碰巧 i 的地址就在arr[9]后面两个整型大小处:

那么我们就算是通过调试弄清楚了上述代码的死循环的真正原因,这也是调试作为VS一项非常重要的功能所给我们带来的无与伦比的自主学习便利性:我们可以通过调试去弄懂我们原本所不理解的代码,也可以通过调试去发现自己代码的问题。

五、写优秀代码的习惯与技巧

        好的代码一般有以下特点:

1.运行完整不报错;2.bug较少;3.可读性强;4.代码执行效率高;5.注释完整;6.可维护性强。

那么我们如何尽量写出较好的代码呢?

以下介绍一下写出良好代码的一些技巧与习惯:

        1.assert()

        assert()称为断言,是一种宏,不是函数。assert后括号中的判断若为假(0),则会直接弹出报错。

如写出一个函数来完成strcpy函数的功能的例子:主函数如下,我们完成my_strcpy函数:

int main()
{
	char arr1[20] = "aaaaaaaaaaa";
	char arr2[] = "hello world";
	my_strcpy(arr1, arr2);
	printf("%s\n", arr1);
	return 0;
}

较低级的写法如下,首先预防传入空指针,因此使用if循环判断是否为空地址,再进行传值:

//strcpy函数的深度理解,自行创建函数my_strcpy完成strcpy的功能
void my_strcpy(char* dest, char* sour)
{
	//效率低,代码搓
	if (dest == NULL || sour == NULL)
	{
		return ;
	}//预防空指针,一旦出现直接返回,不对地址进行下述解引用
	while (*sour)
	{
		*dest = *sour;
		dest++;
		sour++;
	}
	*dest = *sour;//'/0'也得赋过去
}

这样的代码在执行上是没有任何问题的:也成功实现了strcpy函数的功能:

但是有更高效的写法:if循环我们完全可以使用断言进行替换:assert可以完美的替代if循环,并且更牛的是,assert在release下可以被直接优化、而if在release下得一直执行,这意味着代码的执行效率和大小有了提升,同时下面的传值的循环也可以进一步优化,不需要分两步传前面的字符串和末尾的 /0 :

#include<assert.h>
void my_strcpy(char* dest, char* sour)
{
	//预防空指针的最优解:使用assert断言(需要头文件assert.h)
	//断言assert中条件为0则会直接报错
	assert(dest != NULL);
	assert(sour != NULL);
	while (*dest++ = *sour++)
	{
		;
	}
}

这里我们不希望sour指向的内容发生改变,sour只是用来添值的,最好在定义类型时加上const:

#include<assert.h>
void my_strcpy(char* dest, const char* sour)
{
	//预防空指针的最优解:使用assert断言(需要头文件assert.h)
	//断言assert中条件为0则会直接报错
	assert(dest != NULL);
	assert(sour != NULL);
	while (*dest++ = *sour++)
	{
		;
	}
}

上图中while循环,当sour指向/0时,/0对应ASCII码值为0,因此正好赋值完 /0 后while循环终止。

我们查询资料可以知道:原本在编译器中的strcpy函数,返回值类型其实是目标数组首元素地址而不是返回空,因此我们可以进一步改进:将my_strcpy函数设置成返回char*类型的指针:

#include<assert.h>
//strcpy的返回值其实是目标数组首地址,因此我们在创建my_strcpy函数时也可以返回目标数组的首地址
char* my_strcpy(char* dest, const char* sour)
{
	//预防空指针的最优解:使用assert断言(需要头文件assert.h)
	//断言assert中条件为0则会直接报错
	assert(dest != NULL);
	assert(sour != NULL);
	char* ret = dest;
	while (*dest++ = *sour++)
	{
		;
	}
	return ret;
}

答案是完全一样的。

那么下面演示一下assert的报错:假如我们在my_strcpy中传入了空指针NULL:那么就会有assert报错提示:

        2.const

        const放于类型前/后与类型一起用来修饰变量,作用是锁定变量的值,给变量上锁,无法再轻易改变变量的值。

const可以用来修饰普通变量,也可以用来修饰指针变量,下面我们详细介绍一下两者:

①const修饰普通变量

        修饰普通变量其实就如上图,但是有很严重的问题存在:比如说,我们想对a=0上锁不让其改变,这时候我们还是有办法改变a的值的,那就是通过指针解引用修改访问的内容:

我们发现a的值被修改为了20,并且能够正常调试执行,说明const修饰普通变量是存在局限性的。

门上了锁,我们通过窗户进了屋。

②const修饰指针变量

        const在修饰指针变量时又分为两种情况:const int* pa 与 int const* pa等效,但是前两者与int* const pa不同

(I) 对于const int* pa 与 int const* pa而言:const修饰的是 *pa,即const控制的是指针pa所访问的内容。

但是可以更改pa这个指针的指向即使指向更改了,也无法解引用

(II) 对于int* const pa而言:const修饰的是pa,即const控制的是指针pa(地址)。

const位于 * 之后时,修饰的是pa,锁住了指针pa的指向,我们无法对pa的后续指向地址进行更改,但是可以解引用对pa访问的内容进行修改。

因此我们便能区分清楚const的几种不同用法。

接下来我们再举一个例子,模拟实现strlen函数:

主函数:strlen返回size_t类型,这里就不使用整型int了。

int main()
{
	char arr[] = "abcdef";
	size_t len = my_strlen(arr);
	printf("%zd\n", len);
	return 0;
}

接下来是函数my_strlen部分:

我们不希望改变数组arr的内容,因此在函数调用首地址时对其进行const修饰,可以锁住pa的访问内容,同时为了预防空指针的解引用,我们在函数开头加上assert的报错限制。

然后就是计算字符串的长度了,使用一个while循环,每次使count自增直到访问 /0时跳出循环,返回size_t类型的count值。

#include<assert.h>
size_t my_strlen(const char* pa)
{
	assert(pa != NULL);
	//或者直接assert(pa);
	size_t count = 0;
	while (*pa++)
	{
		count++;
	}
	return count;
}

我们看看输出情况:

我也把第一种方法放出来,是我利用sizeof直接求字符串的长度:

int my_strlen(const char* pa,int s)
{
	return s - 1;
}
int main()
{
	char arr[] = "abcdef";
	int sz = 0;
	sz = sizeof(arr);
	size_t len = my_strlen(arr,sz);
	printf("%zd\n", len);
	return 0;
}

        3.养成良好的编码风格

        我们要慢慢写出可读性高的代码,代码的排版、变量名等等,应该让他人读起来比较容易轻松,而不是随意的变量命名,或者函数声明前不加类型等。

        4.养成写代码注释的习惯

        必要的注释可以很好的增强我们代码的可读性,不仅在他人读代码时能够更有帮助,更是在自己对代码的回顾与复习时能够起到非常好的效果。

        5.避免编码陷阱

六、编程常见的错误

        编程常见的错误一般分为三类:编译错误(常为语法错误)、链接错误、运行错误(最难解决)。

        1.编译错误

        编译错误一般为语法错误,我们在代码执行时(Ctrl F5)报错,这个时候就发生了编译错误,会弹出错误信息,一般双击错误信息就能跳转到对应错误的行处,甚至于编译器会温馨地提示你哪里不对。

        2.链接错误

        链接错误一般是某个符号无法被调用,此时就会发生链接错误:

链接错误一般有三种情况:一是符号对应的头文件没有被定义,如上图;二是符号名写错如下图;

三是压根就没有这个符号,而我们在代码中使用了,那么也会出现链接错误。

        3.运行错误

         运行错误一般是最难缠的错误,顾名思义,就是在最终执行的时候,我们发现了其中有执行的情况与需求不对的时候,这个时候我们一般通过上面讲到的调试,逐语句或者断点调试,最后找出问题然后解决。

本次全部内容就在这里啦,主要是简单回顾了一下VS中Debug和Release两种版本,还有调试的相关内容以及我们编程时常见的代码技巧和错误类型,顺带着完成了两个函数strcpy和strlen的模拟实现,我们下期再见!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值