1. 内存和地址
1.1 内存
我们知道,CUP(中央处理器)在计算的时候是从内存中提取需要的数据,计算完成后再将结果放回内存当中,那这些内存空间是如何高效管理的呢?
一个bit(比特位)可以存放一个二进制位,一个byte(字节)可以存放8个bit
每个内存单元可以类比成一个8人制宿舍,每个比特位相当于一个铺位
每个内存单元都有一个编号(这个编号就相当于宿舍门牌号),有了这个编号,CPU就可以快速找到一块内存空间
生活中我们把门牌号叫做地址,计算机中将内存单元的编号也称为地址。C语言中给地址起了一个新名字:指针
内存单元的编号 == 地址 == 指针
1.2 如何理解编址
CPU通过控制总线下达命令要向内存中读入(写出)一个数据,然后通过地址总线获取要操作的地址位置,最后通过数据总线从内存中获取(使内存得到)一个数据
在硬件中地址线的开关代表0和1,也就是说1根地址线有两种含义,2根地址线有4中含义,以此类推,32跟地址线就有2^32中含义,或者说可以控制这么多的内存单元
32位机器有32跟地址线(也就是说最多可以控制4G的内存),64位机器有64根地址线
2. 指针变量和地址
2.1 取地址操作符 &
这段代码中我用16进制数填满了变量a的四个字节,接下来我们观察a变量的内存地址
可以看出每个内存编号(地址)中包含了两个16进制数,一个16进制位相当于4个2进制位,两个16进制位相当于8个二进制位,也就是一个字节,这说明了每个内存块中存放了一个字节的内容。接下来观察打印出来的 a变量 的地址。
这个地址就是存放变量a数据的第一块内存块的编号,也就是说我们不同时需要用4个内存编号来表示a的地址,只去要知道存放变量a数据的第一块地址即可
2.2 指针变量和解引用操作符 *
2.2.1 指针变量
上面这段代码展示了如何创建一个指针变量,其目的是存放另一个变量的地址,之后可能需要通过这个地址来找到这个变量。
2.2.2 解引用操作
这段代码展示了解引用操作,就是说如何去使用这个指针变量
2.3 指针变量的大小
指针变量是专门用来存放指针的,指针变量的大小就取决于存放一个地址需要多大空间
32位机器:有32根地址线(32个比特位),需要4个字节空间
64位机器:有64跟地址线(64个比特位),需要8个字节空间
3. 指针变量类型的意义
3.1 指针的解引用
指针类型决定了,对指针解引用时的访问权限(一次能操作几个字节),也就是说,如果用char*类型指针中存放int型变量地址,那么在解引用修改的时候并不能完全修改int型变量的所有4个字节,只能访问修改一个字节。
很明显a没有被完全修改成0
3.2 指针加减整数
指针类型决定了指针向前或向后走一步要跨过多少字节
通过这段代码可以看出int*类型指针和char*类型指针在加减1后跳过的字节数是不一样的,int*型跳过了4给字节。而char*型只跳过了一个字节
3.3 void* 指针
在指针类型中有一种特殊的类型是 void* 指针,可以理解为无具体类型的指针(泛指指针),这种类型指针可以接收任意类型地址。但是它也有局限性,不能直接进行指针加减和解引用操作
void* 指针只能用来接收存放地址,但不能对地址有任何操作
一般 void* 指针用在函数参数的部分,来接收不同类型数据的地址,这样的设计可以实现泛型变成的效果。使得一个函数来处理多种类型的数据(在深入理解指针4中详细讲解)
4. const(常属性) 修饰指针
const int a=10; 这个语句定义a之后,a就不能被修改了,但是本质上a还是变量,const只是在语法上进行了限制,所以我们习惯称这种变量叫常变量
当然我们可以绕过变量a来访问其内存空间,来达到修改const int a 的值
下面进入正题,const 对于指针的修饰
const 修饰指针变量的时候可以放在 * 的左边或右边
放在左边,限制 *p ,意思是不能通过指针变量p修改p指向的空间的内容,但是p变量是不受限制的,还可以转存别的变量地址
放在右边,限制 p ,意思是不能修改变量p中的内容,就是不能更改指针变量指向的空间地址,但是*p不受限制可以修改
当然,还可以两边都加const,这样就什么都不能改了
5. 指针运算
5.1 指针加减整数
因为数组在内存中是连续存放的,只要知道第一个元素的地址,之后这个地址加上下标的值就能找到别的元素
这段代码通过指针加运算完成数组各个元素的依次访问并打印
5.2 指针减指针
指针减指针能计算出两个地址中间有多少个元素
指针减指针的运算条件是在同一块有关联的内存区间中
指针加指针没有意义
下面写一段代码来模拟strlen函数的实现
这段代码中用int* start变量记录了字符串的第一个字符地址,之后通过两个指针相减得到字符串长度。
5.3 指针的关系运算
指针是可以进行大小比较的
这段代码通过对比当前打印元素的地址与最后一位要打印的元素地址比较,来判断打印是否完成,以此打印出了整个数组
6. 野指针
野指针就是指针指向的位置是不可知的
6.1 野指针成因
1.指针未初始化
2.指针越界访问
3.指针指向的空间释放
6.2 如何规避野指针
1.指针初始化,如果不知道给指针初始化谁的地址,直接给NULL,读写该地址都会报错
2.小心指针越界
3.当指针变量不再使用时,即使置NULL,在使用指针前检查有效性(判断指针是否是NULL)
4.避免返回局部变量的地址
7. assert 断言
assert.h 头文件中定义了宏 assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报错终止运行,这个宏常量常被称为“断言”
#include<assert.h>
assert( p != NULL ); 确保p指针不是空指针,否则就报错
assert()中的表达式为假将会报错,并显示错误的位置,从文件位置一直到代码位置
assert()的使用对程序员是非常友好的,它不仅能自动标出文件和出问题的行号,还有一种无需更改代码就能完成关闭assert()的机制,如果已经确认程序没有问题,不需要再做出断言,在 #include<assert.h>前面定义一个宏NDEBUG
assert断言因为引入了额外的检查量,增加了程序的运行时间
一般断言我们会在debug中使用,在release版本中选择禁用assert就行,当然,在VS的release版本中会自动优化掉断言。这样在debug版本中有利于程序员排查问题,但是在release版本中不会影响用户使用效率