实用调试技巧

实用调试技巧,第1张

调试(Debugging / Debug ): 又称为排错,是发现和减少计算机等电子仪器设备中程序错误的一个过程。


Ddbug 和 Relaease 的介绍

Debug版本是粗胚 ,而relese版本是精加工。


debug中加入了调式信息,其大小比release版本大,运行速度比release版本慢


调试的快捷键

 在想结束的位置按F9就可以建立断点 ,此时我们就可以让程序跑到一个地方后停止下来,除非我们进行 *** 作,否则程序不执行,即跑道断点就停止 , 再按一下F9可以取消断点

设置断点后按F5启动调试 -- 程序在断点处停止调试 -- 此时如果按F10(开始逐过程走),F11(开始逐语句走),F5调试继续,程序在哪停止就再从哪开始走,直到遇到下一个断点才停止

另外

1.调试的时候才有各种各样的调试窗口

2. 调用堆栈窗口的作用就是用像堆栈(先调用先进栈,后调用后进栈)一样的形式展示我们的函数调用逻辑


(如果输入的是字符串,我们应该将这个字符串存储在数组中!!!)

 在c语言中,二维数组中的元素是一维数组,二维数组的数组名是第一个元素,即一维数组的地址,而一维数组的地址与其首元素的地址相同

(*二维数组名)可以用来描述二维数组的第一个元素,即第一个一维数组

 且我们规定 二维数组的a[ i ] 即为对应一维数组元素的数组名b = a[ i ],多维数组中的一维中的元素的地址仍然是线性连续排列的。



scanf的括号里由两个内容组成,一个是类型,一个是地址。


类型是对输入数据的声明,地址用来存放输入数据的内存空间的地址。


关于scanf_s函数的整型参数 --

首先输入的字符串一定要用字符数组存储,存储格式是 (vs环境下)scanf_s("%s" , 字符数组名 ,能够输入到数组中的字符个数); 

而在非vs环境下的字符串输入格式是 : scanf("%s",字符数组名);

在非vs环境下的scanf字符串输入不需要带上 整型参数 --- 能够输入到字符数组的字符个数

另外scanf在录入字符串的时候不会将\0录入

空格字符对应的ascii码值是0


明天补充昨天那一道题的笔记 -- 就是scanf录入字符串到数组的一些相关问题

1.scanf在录入字符串到数组中的时候会将\0这个终止符也录进去

2.字符串在存进数组之后会被拆分为一个个字符存进去

3.puts ( ) 函数是将数组中的一个个字符按照从左到右的顺序拼成字符串输出,同时要注意如果数组中没有 终止符\0而直接使用puts的话,就会导致输出随机值


关于字符串和字符的一些知识补充

1.用单引号括起来的就是字符常量,且单引号中只能有一个元素,字符常量只占一个字节

2.用双引号括起来的就是字符串常量,双引号中可以有多个字符,也可以没有字符。


字符串常量最特殊的一点就是:

存储字符串常量的时候,除了存储双引号内的所有字符外,计算机还会在串的最后自动增加一个字节用于存放终止符 \0。


也就是说当我们建立一个字符串常量的时候,我们除了开辟双引号内所有字符所需的空间外,还会在最后一个字符的后面多开辟一个字节的空间用来存储终止符  \0 

所以说终止符\0是字符串中隐而不现的一个字符

另外:

字符常量的本质是一个个ascii码值

而字符串常量的本质就是一系列ascii码值的组合,这样一个组合被整合打包并在名为常量区的地方开辟内存空间存储它们,而要访问它们就需要一个地址,我们规定 "存储的字符" 这样的格式即代表存储字符串常量的地址

即 “abc” == 0x5548(常量区中的一个地址),有了这个地址我们就能够访问存储在其中的字符串常量 abc了


strlen函数再叙--

-- %u -- (unsigned)strlen( const char* str ) 

有unsigned保证我们出现的一定是正数 ,另外要有(unsigned)才能用%u。


strlen的作用是计算字符串的长度 ,单位是字节 

strlen库函数的参数是一个字符指针常量 --

1.可传实参有字符数组的数组名(作用:访问字符数组,并计算其内第一个字符到终止符\0的长度,即字符个数(\0不计入))

2.还有直接传字符串,字符串符号就是代表指向字符串的地址


回到调试技巧:

断点也分很多的类别

(鼠标右击断点就可以找到这个页面)

比如条件断点 -- 这个条件断点能够实现的功能就是 -- 只有在满足一定条件的时候程序才抵达断点,并于断点位置停止调试。


 

比如我们可以规定如图这个位置的断点

只有在循环进行了五次之后才能抵达,即 i == 5 的时候,这时设置一个条件断点就能够实现我们想要的功能了!


代码出错的类型有两种:

一种是语法错误,一种是运行时错误,而我们进行调试的目的就是解决运行时错误。


语法错误一旦出现程序都跑不起来,所以不用担心。


而运行时错误就是我们的程序跑的起来,但跑出的结果却是错误的,这就是运行时错误


F5是开始执行并调试,ctrl + F5是开始执行不调试

调试解决问题的逻辑是这样的:

1.脑海里首先要对程序每实现一个功能(每走一步)会有怎样的结果有一个预期

2.调试的时候发现某一步得出的结果不符合预期,欸,这时我们就找到了问题所在


数组的越界访问_HackerYip的博客-CSDN博客_数组访问越界

关于数组的越界访问:

1.数组是局部变量,所以数组是在栈区建立的

2.栈区的内存使用习惯是先使用高地址然后再使用地址

3.数组的越界访问是指访问了数组空间以外的内存,且这个越界是按照顺序越界访问,即比如a[10] 数组 -- 则当其越界访问 a[ 10 ]时,则a [ 10 ] 的地址紧随 a [ 9 ]  在a[ 9 ]的后面一位

4.尽管越界访问后的地址依然是按照顺序排列的,但是!值得注意的一点是,由于越界访问的地址中的数据没有被初始化,所有当我们越界访问时得到的就是一个随机值

栈区:从高地址到低地址用,且不同的编译器在栈区分配地址的时候,

另外这个i和数组之间空的内存空间的个数是由不同的编译器环境决定的,不同的编译器下可能空不同个数的内存空间

另外一般在出现了越界访问后计算机都会在程序执行完后报错的,但是为什么这个没有报错呢?

因为这个程序没有机会执行完,它由于上面那种巧合一直在执行死循环,这导致程序无法结束,进而导致计算机无法报错。


 最关键的一点是访问了同一个地址,拥有了同一个内容

循环,if等判断是对括号里的指进行布尔判断,若不等于0则进行,否则不进行

而上面这个图中的循环括号内则是一个表达式,表达式中的 *** 作符是赋值 *** 作符 “ = ” , *** 作数是其左右两个, *** 作完后输出结果,循环对括号内的这个输出结果做判断!

实在是妙啊,用最短的代码量就实现了功能!


常见的coding技巧

空指针哪里都没有指向,无法进行解引用,若强行解引用会直接报错 -- 它是一个“无内容”


assert(); --> 断言

 

 assert(你的预期) ;-- 断言的效果是:你把你对程序运行结果的预期放在括号里,如果程序运行的结果不等于你放在括号里的预期的话,程序直接终止,并输出上面这样的报错,否则程序继续正常运行。


这个断言会提示你,你的预期落空了(“falled”),程序运行的结果不等于你的预期,并且告诉你断言所在的具体位置

用assert()需要引用头文件

注意断言在做判断的时候要用逻辑判断符号 -- ==  ; != ; <= ....

断言的括号中也可以是表达式等等

只要是你的预期,且复合语法的话,都可以放进断言的括号中

为什么这里面要加const呢?

这是为了保证当我们进行下面那个交换时,如果交换反了,程序可以直接报错而不再进行下去

为什么能实现这个呢?这得从 = 赋值 *** 作符开始讲起

赋值 *** 作符有左右两个 *** 作数,对于这两个 *** 作数赋值 *** 作符有如下规定:
1.对于左值(左边的 *** 作数),规定其必须为变量而不能是常量,否则报错

2.对于右值,其既可以是变量,也可以是常量

有了上面这个思路我们就可以解释这个const从何而来

首先我们的目的是正常交换不受阻碍,错误交换报错

正常交换就是 *dest++ = *src++

错误交换就是 *src++ = *dest++

即src只能在右边

而什么东西只能在赋值 *** 作符的右边呀?没错就是常量,可是问题又出现了,str是指针变量呀,怎么就能变成常量呢? --- 这时候我们就能够使用 const来修饰这个变量让它成为常变量啦!!

综上所诉我们的功能就实现啦!

若我们将src这个指针变量修chang

(常变量不能够被修改,但它仍然是变量)

const修饰指针的作用就是 --  使得我们不能再通过解引用这个指针来访问和 *** 作指针指向的内存空间中的数据

 补充const的笔记

1.const修饰谁,谁就被赋予常量属性,无法被改变(变成只读)

2.const就近修饰 --- 即const只修饰最近的一个变量,且const会自动省略它后面的const(同一语句下)

3.const修饰常变量的原理 --- const会将自己修饰的变量变为只读状态,让我们无法对其进行 *** 作,使得它不能够成为一个 *** 作数。


4.const修饰的常变量是一个伪常量(const修饰了变量后,这个变量的内存空间仍然在可以修改内存中内容的栈区内),真正的常量会被存在内存的常量区中,此时这个常量不仅仅是只读量,同时我们也无法通过地址访问它的内存来修改它。


但是如果我们只是用const来修饰变量的话,我们只能实现一个功能,那就是让这个变量变为一个只读变量,而无法阻止我们通过指针访问它的内存空间,直接修改存储在它的内存空间中的内容

5.要想修改存储在一个变量中的数值我们有两种方式 --- 1.用赋值 *** 作符直接修改(必须为可 *** 作的 *** 作数,而不能是只读变量) 2. 通过指针直接访问这个变量的内存空间,然后通过解引用直接修改内存中的内容。


3.const在修饰指针的时候分为两种情况

{

int* p;

        1.const 修饰指针变量p 

格式: int* const p = 

nt* const p,da:这个时候这个指针变量就成为了只读变量,我们无法通过赋值 *** 作符来修改存储在它的内存空间中你的内容 -- 它指向的地址,但是我们可以访问存储在这个地址对应内存空间中的内容

(ps:对指针变量解引用的作用就是建立一个和指针指向的地址对应的内存空间中的数据相同类型的变量,同时copy一份这个数据并将其存储在这个变量中,并且将这个变量命名为 *p )

 

        2.const修饰指针变量的解引用:

格式: cosnt int* p = 

cosnt int* p  -- 这个时候 *p 就成为了一个只读变量,我们无法通过赋值 *** 作符来修改它的内存空间中存储的数据,但是它的本质仍然是一个变量,它的内存空间仍然在内存内容可修改的栈区中,所以我们可以通过 *p的地址直接访问它的内存,并修改其中的内容。


}

解引用只有一个解引用,可没有多级解引用啊

将一个函数的返回值最为另一个函数的参数,这就是函数的链式访问

数组在建立之后其各种各样的 *** 作都是建立在指针的基础上的,通过指针我们就能够高效的访问数组了。


写一个函数需要考虑的两点:1.参数要不要const 2. 函数里要不要assert

要想完整的访问一个数组,我们不仅需要它的首元素的地址(数组名),我们还需要知道它的元素个数,而数组在作为实参传给形参时,我们一般传递数组首元素的地址,如果数组元素个数是静态的,那我们通过下标引用 *** 作符可以直接用这个静态来实现我们想实现的功能,若为动态的,则在我们传参时,也要把这个个数作为参数传过去

数组元素的标志 a[ i ] 的本质就是一个地址,它的本质是通过下标引用 *** 作符实现的地址改变后解引用的过程

如下:  a的本质是首元素的地址  ---  a[ i ] <==> *( a+i ) -- a+i的本质就是数组中第i+1个元素的地址


空指针是 NULL -- 空指针谁也不指向,无法解引用。


赋值 *** 作符要求左值是变量,而判断 *** 作符无要求。


数组 *** 作的本质就是地址 *** 作


解引用的本质是创建变量的 *** 作

下标引用 *** 作符的本质是在 *** 作地址,且 *** 作的是首元素的地址

综上访问一个数组最重要的元素是它的首元素的地址,有了首元素的地址一切都好说,所以数组作为实参传给形参的时候,我们只传最重要的首元素的地址,这是最重要的,再用首元素地址结合下标引用 *** 作符我们就能很好的访问数组了,至于另一个 *** 作数则是我们根据实际需求和情况来确定的


__cdecl : 这个是函数调用约定 ,这个决定了函数在传参数的时候先传那个再传那个等等函数事项

链接型错误:1.调用函数时函数名写错 --- 计算机找不到对应函数;2.调用的函数根本不存在


c语言中是一个分号一个语句,而不是一个一行一个语句

c语言只规定了语法规则,而没有规定输入输出语句,而我们调用的库函数都是后来人们通过c语言的语法写出来的,构建库函数的意义就是帮助人们不去反复发明轮子,提高人们的工作效率。


库函数是独立于c语言的一部分 ,c语言是一门非常干净的语言,它只给定了编程时人们需要的语法,其他的功能实现都是靠人们自己去编写。


编译器是看不到注释的,编译的时候会自动忽略掉注释

整型除以整型只能得到整型,要想得到浮点数,必须将分子或分母中的一个或全部改为浮点数

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/langs/634937.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-16
下一篇 2022-04-16

发表评论

登录后才能评论

评论列表(0条)

保存