1.内存和地址
1.1 内存
计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,那我们买电脑的时候,电脑上内存是8GB/16GB/32GB 等,其实管理方式也是把内存划分为一个个的内存单元,每个内存单元的大小取1字节。
每个内存单元都有一个地址,C语言中给地址起了一个新的名字:指针。
所以可以理解为:内存单元的编号 == 地址 == 指针
1.2 理解编址
首先,必须理解,计算机内是有很多的硬件单元,而硬件单元是要相互协同工作的。所谓的协同,至少相互之间要能够进行数据传递。但是硬件与硬件之间是相互独立的,那么如何通信呢?这就需要用“线”链接起来。而CPU和内存之间也是有大量的数据交互的,所以,两者必须也用线连起来。
本文先探讨一下“地址总线”,CPU访问内存中的某个字节空间,必须知道这个字节空间在内存的什么位置,而因为内存中字节很多,所以需要给内存进行编址。
计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
可以简单理解为,32位机器有32根地址总线,每根线只有两态,表示0,1【电脉冲有无】,那么一根线,就能表示2种含义,2根线就能表示4种含义,每一种含义都代表一个地址。
2.指针变量和地址
2.1 取地址操作符(&)和指针变量
理解了内存和地址的关系,我们再回到C语言,在C语言中创建变量其实就是向内存申请空间
2.2 解引用操作符(*)
2.3 指针变量的大小
32位机器假设有32根地址总线,由前文所解释,不难知道一个地址就是32个bit位,需要4个字节才能存储。
如果指针变量是用来存放地址的,那么指针变量的大小就得是4个字节的空间才可以。
同理,64位机器就需要8个字节。
3. 指针变量类型的意义
3.1 指针的解引用
对比下面2段代码,主要在调试时观察内存的变化。
代码一:
代码二:
通过调试我们可以看到,代码1会将n的4个字节全部改为0,但是代码2只是将n的第一个字节改为0。
结论:指针的类型决定了对指针解引用的时候有多大的权限(一次能操作几个字节)。
比如:char * 的指针解引用就只能访问一个字节,而 int * 的指针的解引用就能访问四个字节。
3.2 指针+-整数
char*类型的指针变量+1,可以跳过1个字节;int*类型的指针变量+1,可以跳过4个字节。这就是指针变量的类型差异带来的变化,指针+1,其实跳过1个指针指向的元素。指针可以+1,也可以-1。
结论:指针的类型决定了指针向前或者向后走一步有多大(距离)。
3.3 void* 指针
在指针类型中有一种特殊的类型是 void * 类型的,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,void*类型的指针不能直接进行指针的+-整数和解引用的运算。
在上面的代码中,将一个int类型的变量的地址赋值给一个char*类型的指针变量。编译器给出了一个警告,是因为类型不兼容,而使用void*类型就不会有这样的问题。
4.const修饰指针
4.1 const修饰普通变量
变量是可以修改的,如果把变量的地址交给一个指针变量,通过指针变量也可以修改这个变量。
但是如果我们希望一个变量加上一些限制,不能被修改,那么就需要const。
但是下述代码却能改变const修饰的值。
此处指针p拿到了m的地址,这样就打破了const的限制,这是不合理的,所以应该让p拿到n的地址也不能修改n,那接下来怎么做呢?
4.2 const 修饰指针变量
一般来讲const修饰指针变量,可以放在*的左边,也可以放在*的右边,意义是不一样的。
5. 指针运算
指针的基本运算有三种,分别是:
- 指针 +- 整数
- 指针-指针
- 指针的关系运算
5.1 指针+-整数
5.2 指针-指针
指针-指针:结果是两个指针之间元素的个数
5.3 指针的关系运算
6.野指针
概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)
6.1 野指针成因
1.指针未初始化
一个局部变量不初始化,它的值是随机的。此时解引用就会形成非法访问。(在VS中直接报错,dev里面给你一个随机值)。
2.指针越界访问
当p->arr[10]时,这时候就变成了野指针。
3.指针指向的空间释放
注意看上述想要返回n的地址,但是n是一个局部变量,当函数结束后,内存就被释放了。此时p就变成了野指针。
6.2 如何避免野指针
6.2.1 指针初始化
如果明确知道指针指向哪里就直接赋值地址,如果不知道指针应该指向哪里,可以给指针赋值NULL,NULL是C语言中定义的一个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写地址会报错。
6.2.2 小心指针越界
一个程序向内存申请了哪些空间,通过指针也就只能访问哪些空间,不能超出范围访问,超出了就是越界访问。
6.2.3 指针变量不再使用时,及时置NULL,指针使用之前检查有效性
当指针变量指向一块区域的时候,我们可以通过指针访问该区域,后期不再使用这个指针访问空间的时候,我们可以把该指针置为NULL。因为约定俗成的一个规则就是:只要是NULL指针就不去访问,同时使用指针之前要去判断指针是否为NULL。
我们可以把野指针想象成野狗,野狗放任不管是非常危险的,所以我们可以找一棵树把野狗拴起来,就相对安全了,给指针变量及时赋值为NULL,其实就类似把野狗拴起来,就是把野指针暂时管理起来。
不过野狗即使拴起来我们也要绕着走,不能去挑逗野狗,有点危险;对于指针也是,在使用之前,我们也要判断是否为NULL,看看是不是被拴起来的野狗,如果是,则不能直接使用,如果不是,我们再去使用。
6.2.4 避免返回局部变量的地址
上述例子已经说明
7. assert断言
<assert.h>头文件定义了宏assert(),用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行。这个宏常常被称为“断言”。
例: assert(p!=NULL);
上面代码在程序运行到这一行语句时,验证变量p是否等于NULL。如果确实不等于NULL,程序继续运行,否则就会终止运行,并且给出报错信息提示。
assert()宏接受一个表达式作为参数。如果该表达式为真(返回值为非0),assert()不会产生任何作用,程序继续运行。如果该表达式为假(返回值为0),assert()就会报错,在标准错误流stderr(就是屏幕上)中写入一条错误信息,显示没有通过的表达式,以及包含这个表达式的文件名和行号。
assert()的好处:它不仅能自动标识文件和出问题的行号,还有一种无需更改代码就能开启或关闭assert()的机制。如果已经确认程序没有问题,不需要再做断言,就在#include <assert.h>语句的前面,定义一个宏NDEBUG(no debug)。
然后,重新编译程序,编译器就会禁用文件中所有的assert()语句。如果程序又出现问题,可以移除这条#define NDEBUG 指令(或者把它注释掉)再次编译,这样就重新启用了assert()语句。
assert()的缺点是,因为引入了额外的检查,增加了程序的运行时间。
一般我们可以在debug中使用,在release版本中选择禁用assert就行,在vs这样的集成开发环境中,在release版本中,直接就是优化掉了。这样在debug版本中有利于我们排查问题,在release版本中不影响用户使用时程序的效率。
8.指针的使用和传址调用
8.1 strlen的模拟实现
库函数strlen的功能是求字符串的长度,统计的是字符串中\0之前的字符的个数。
8.2 传值调用和传址调用
例:写一个函数,来交换整型变量的值。
还是那个老问题,函数调用后,内存会被释放,但是当传地址,然后改变值后,就没事了。
作者自述:本文主要针对C语言的指针知识。内容中包含了很多总结内容。本文制作不易,求求动动你们发财的小手点个赞和关注,这是对我创造最大的动力。后续我也会跟进内容,尽量一周至少一次,保证内容的质量。如果有想知道的内容或者有建议的地方,欢迎后台私信或者在本文留言哦。感谢各位的支持捏Thanks♪(・ω・)ノ。