关于C/C++ 一些自己遇到的问题以及解惑

08/05 15:53
阅读数 11

在这里插入图片描述



       有些自己遇到的,有一些是通过群友的提问应发的,问题本身的价值可能并不高,但其背后的原因才是我们应该学习的,下面我们来看看。

1.数组越界造成的死循环

       有一位朋友在群里发了该代码,并说该代码导致了死循环???
在这里插入图片描述

       废话少说,上工具,我们来分析分析。
在这里插入图片描述

       Dev下的程序并无异常???????我们来看看vs2015的表现,虽然是正常输出内容,并没有造成死循环,但是弹出了一个异常~ 。这个异常是由于我们数组越界造成的,而数组越界又是一种未决行为,编译器不会做任何处理,但是vs2015还是义务的帮我提示了异常,所以Dev和vs该用哪一个编译器,心里有数了吧?
       回到问题本身,我询问了这位群友,在他的电脑上下确确实实是造成了死循环,用的是CodeBlocks,所以得出一个结论就是循环里发生数组越界在某些IDE编译运行,会导致死循环。再往下看,通过搜索,我了解到==导致死循环与编译器的内存分配有关,若内存递减分配会出现死循环,递增分配则不会,==并通过在不同IDE输出内存地址,确实验证这个结论成立.

在这里插入图片描述

       那么为什么会产生这样的效果呢,揭秘如下.

若是内存递减分配,对于数组和i的内存分配如下:
在这里插入图片描述
若是内存递增分配,对于数组和i的内存分配如下:
在这里插入图片描述
       现在可能就有人问了,为什么递减分配 i和iarray[2]挨着,而递增i就和iarray[0]挨着,其实这个不难理解,*(iarray+1)一定比*(iarray)的地址高不是吗,对于递减分配,必须倒着来分配,对于递减这种分配模式,iarray[3]的地址就是i的地址,iarray[3]=0便是i=0,这样一来便导致了i的值又重新被赋值为0,导致了死循环,然后,注意,没有完,之所以i会跟在数组后面,是因为字节对齐,对于32位来说是4字节,对于64位来说是8字节,当数组内容不足以字节对齐,i就会分配在其旁边,或者说是后面,当数组正好有8个元素,i就不会跟在数组后面,也就不会造成死循环,所以造成死循环一是编译器分配内存方式,二是字节对齐的问题.
       经测试,博主所使用的dev和vs2015,以及一些编译器会在数组和i的地址之间,用一小块内存,用来避免两者,从而一定程度上解决死循环问题,但当越界过大,还是会造成死循环.所以在使用对内存的操作上,应格外小心…




如何查看内存?如果是C,我们可以用%p来输出变量地址,若是C++,我们可以用static_cast<void *>(&a)来输出变量地址,大家若是使用vs,教大家一个小技巧,在调试模式中(F5)下依次单击调试,窗口,即使窗口,打开这个窗口后,我们可以用&变量名来获取地址
在这里插入图片描述


2.int main(int argc,char* argv[])里面的参数有什么作用?

       首先可以告诉大家的是对于单纯的C语言,main里面的参数对于我们学习C来说,并不重要,标准形式有两种int main(int argc,char* argv[])和int main(void),在实际的学习使用中,我们使用int main(void)这种形式就可以了,当然,你要是感觉酷一点可以用int main(int argc,char* argv[]),如果你还想知道int argc,char* argv[]这种参数的作用,那么请往下看。
       由于我们的main函数不被其他函数调用(注意:不是不可调用,是一般情况下不调用,如果你想挨骂的话…),所以就不能像其他函数一样,在程序运行中获取参数数据,那为什么还要有这个参数呢,实际上,这个参数是程序运行时,利用操作系统传进来的,argc代表着指针数组的元素个数,argv[0]是程序所在计算机的完整路径,例如: C:\Users\fdog\Desktop\hello.exe。而argv[0]之后的元素就是我们要利用操作系统传给的字符串类型的数据。
       如果大家还是体会不到这个参数的作用,我可以举几个例子:

       1.大多数人应该都写过XXXX管理系统,有管理,就有数据,有数据就需要我们保存,我们可以用一个文本来保存用户输入数据,但是这个文本应该保存在什么地方呢?总不能在代码中固定一个路径吧,大家计算机名字都不一样,这样肯定行不通,于是我们在代码中开始写到cout<<“请输入数据保存的路径”; 然后开始读取用户输入的路径,那么有没有进一步提升用户体验的写法?这时我们就可以用到main的参数了,利用argv[0]获取该程序的路径,并通过算法解析,即可得到用户把exe放在哪里,那么我们在exe所在的路径下保存数据文本即可,这样就会提示用户体验。

       2.当你编写的程序需要根据提供的数据执行不同从操作,但是每次执行所需要的数据又未知,这个时候我们就可以用到main的参数,我们可以写一个脚本程序,然后让程序读取脚本中提供的参数,这样就会事半功倍。

其实相当于是调用了exe,exe里面的函数利用参数工作,而exe也同样可以利用参数工作,那么如何输入参数呢,告诉大家几张方法:

1.直接在命令行输入 start 路径 参数1 参数2 参数3

2.我们将编译好的程序,快捷方式放到桌面,右键选择属性,在其目标的文本框 exe文件后面 加入参数
在这里插入图片描述
3.最后一个也就是直接在我们的IDE进行设置,例如我所使用的vs2015,右键项目->属性,在调试页面可以看到一个命令参数。
在这里插入图片描述
好了,main的参数就说到这里,悄悄告诉大家,其实main还有第三个参数:char *envp[],如果大家有兴趣,可自行研究研究。




3.程序代码区、文字常量区、静态区(全局区)、堆区、栈区

       为什么说这个,原因在图中:
在这里插入图片描述
       群里在讨论链表,一位名叫C语言信赖代考的网友讲了一句清除链表只需要释放头节点就行了,不用一个一个删,我看到了,于是好意提醒了一句,结果这位网友告诉我头节点后面连着所有节点,只需要释放头节点就行了,一看此现状,我就没再与他讲理了。刚奇葩的是这个哥们私聊我说有单子给他,这我敢给啊,再顺便讲一句,不要随随便便叫人代考啊。。。。我们进入正题。
       这位网友之所以会怎么说,应该是没有理解malloc/new,也就是malloc的内存申请在哪,就是栈区和堆区问题,但是因为程序代码区、文字常量区、静态区(全局区)、堆区、栈区这些东西常出现在一起,索性也就放在一起说了。
       我查找了大量的有关博文,大多数有关博文都有怎么一张图,如果说以前,我可能会同意,但是现在我对图中栈区的向下增长有一些疑惑,就拿我们刚开始数组死循环的内存分配来说,内存两种分配模式,递增,递减,所以我觉得这个图还有待考证。
在这里插入图片描述




  • 程序代码区:
    无疑问,存放程序代码的地方,不过这这里的代码被处理成二进制进行保存。
  • 文字常量区:
    存放常量(程序在运行的期间不能够被改变的量)。
  • 静态区(全局区):
    又分为
           未初始化的全局变量和静态变量。
           已初始化的全局变量和静态变量。
    静态变量和全局变量的存储区域是一起的,一旦静态区的内存被分配, 静态区的内存直到程序全部结束之后才会被释放。



  • 堆区:
    调用malloc()函数来主动申请的,需使用free()函数来释放内存,或者是C++中对应的new()函数,若申请了堆区内存,之后忘记释放内存,很容易造成内存泄漏。
  • 栈区(堆栈区):
    存放函数内的局部变量,形参和函数返回值。栈区之中的数据的作用范围过了之后,系统就会回收自动管理栈区的内存(分配内存 , 回收内存),不需要开发人员来手动管理。栈区就像是一家客栈,里面有很多房间,客人来了之后自动分配房间,房间里的客人可以变动,是一种动态的数据变动。

举个例子:
同时我们可以右键项目,链接器,系统,可以看到堆保留大小和堆栈保留大小。
在这里插入图片描述
然后再说一下malloc对应free,new对应delete,new,delete涉及构造函数和析构函数,不可混用,若是C++,应使用new。



4.函数指针 指针函数 指针数组 数组指针 傻傻分不清

int fun();                     函数
int * fun();                   指针(样式的)函数 本质是一个函数,只不过返回的类型是指针

int(*fun)();                   函数(样式的)指针 本质上是一个指针
fun =& fun_2               获取函数地址,fun_2 是一个函数名,
       
调用的话 使用(*fun)() 和fun() 效果是一样的


char * p[];              指针(样式的)数组 本质是数组 ,将指针进行集合,元素为指针

int (*p)[];              数组(样式的)指针 本质是指针

上面出现的括号都是必要的,不可省略,说其是一种格式也不为过,指针XX和XX指针分不清主次,可以像我一样在两者之间加上(样式的),应该会方便理解。


5.return continue break return 0 exit

break:跳出所在的当前整个循环,到外层代码继续执行,break不仅可以结束其所在的循环,还可结束其外层循环,但一次只能结束一种循环。

continue:跳出本次循环,从下一个迭代继续运行循环,内层循环执行完毕,外层代码继续运行,continue结束的是本次循环,将接着开始下一次循环。

其实这两个没什么说的,return 和 exit可能在书中不常见。

return:直接返回函数,所有该函数体内的代码(包括循环体)都不会再执行,同时结束其所在的循环和其外层循环。当自定义函数中无返回值时,可以使用该写法。相当于使用了break。

return 0; 当函数有返回值时,使用该写法。

exit(1); 程序/进程立即结束(正常退出)
exit(0); 程序/进程立即结束(异常退出)


6.最大值加1等于最小值?(一图看懂)

我们可以把变量的取值范围当作是汽车的里程表,一来为了好理解,二来确实是这样的,拿char来说:

在这里插入图片描述


7.精度问题

在这里插入图片描述
在这里插入图片描述
这位网友的问题很有意思,这个案例也是很好的图示了下面我要说的话,这是众多初学者的一个理解错误,每一本语言书都会告诉你单精度类型有效范围是7位,双精度类型有效范围15位,这就给大家造成一种错觉,认为只能存15位,其实是错了,它所指的15位指的是精度问题,就是图中的样子,它可以存储30位甚至更多,但是它的精度只有前15位,就是超过了15位,一起数字都化作了0。之所以可以保存到30多位,和浮点数的存储有关,浮点数是用科学记数法存储的,有关浮点数的定义,这个就涉及到计算机组成原理了,还是比较难的,大家有兴趣可以搜索IEEE754浮点数的标准,里面有关于浮点数的存储过程。


最后来一只可爱的猫猫,创作不易,求关注,求收藏!
在这里插入图片描述

展开阅读全文
c++
打赏
0
0 收藏
分享
加载中
更多评论
打赏
0 评论
0 收藏
0
分享
在线直播报名
返回顶部
顶部