1.内存和地址
1.1.内存
- 指针就是用来访问内存的
- 怎么访问内存呢? 1byte(字节) = 8bit,
- 把内存切成一个个的内存单元,一个单元是一个字节(带图理解)
- 每个内存单元就是一个地址,内存单元编号 ==地址 ==指针,指针就是地址
1.2.究竟该如何理解编址
- 控制总线:就是你要读取,还是写入(R/W),意思就是传输指令,要做什么事
- 地址总线:传输的是地址。去哪里读数据,或者去哪里写数据
-
数据总线:进行数据传递
-
访问的顺序:是控制总线先发信号,然后地址总线去拿数据或者写数据,接着就要进行数据传输。这样一套组合拳就完成啦!!!
-
硬件编址:32位机器有32根地址线总线,表示0或1(电脉冲有无)。假如是64位就有64根地址线
- 因为一根地址线有0或1两种意思所以有2^32个地址,就有2^32含义指针变量和地址指针变量和地址
2.指针变量和地址
2.1.取地址操作符(&)
- 取的是首地址,第一个字节地址
- 指针变量 -- 存放指针的变量 ,只要把东西放到指针变量里面他就是地址 p 的类型是int*
- p指针变量里面放着a的地址,a变量里面装着10这个元素
- 可以看上方调试图,可以肯定的是p里面就是存储着a的地址,指向的类型是int,所以在内存中0x3EFE54 --->0x3EFE58刚好是4个字节
2.2.指针变量和解引⽤操作符(*)
- 通过 *(解引用)p里面的地址,找到里面指向的对象
- 确切的说是拿到这块空间,这块空间里装着元素
- 拿到了这块地址,可以间接访问里面的元素对他进行修改
- a = 0是自己亲自动手 ,p 就是打手,通过p来帮老板干活(间接访问)
2.3.指针变量的大小
- 32位(x86)有32根地址线,0或1。要存储这样的地址:32bit位的空间 ==4个字节
- 64位(x64)有64根地址线,需要64bit位空间 ==8个字节
- 指针变量的大小和类型无关,相同平台下,大小都是相同的
3. 指针变量类型的意义
3.1指针的解引用
- 对下列代码进行解释,0x11223344是16进制的数存储到int类型里面,地址也是以16进制存储。
- 为了方便观察,调试的时候是以16进制呈现出来给你看的
- 指针类型决定了,指针解引用的操作符可以访问几个字节,决定了指针的权限
- 下面栗子解释,因为pi指针的类型是char*,访问的权限一个字节,所以对他进行修改时只有一个字节
3.2指针 + -整数
-
指针的类型决定了指针向前或者向后走一步有多大(距离)
- 一次能操作几个字节。
3.3void*指针
- 无具体类型的指针(或者叫泛型指针),这里不就具体讲了,后面再介绍
- 可以用来接收任何类型的指针(作用)
-
一般 void* 类型的指针是使用在函数参数的部分
-
弊端
-
不能进行指针+-操作
-
不能用 * 解引用操作符
-
4. const修饰指针
4.1const修饰变量
- a 是不是常量呢? 虽然a是不能被修改,但是本质上还是变量,下面栗子就很好的说明了
4.2const修饰指针变量
- 虽然 int * const pa,const在 * 号的右边,这样就不能修改指针本身。但是是可以修改指向地址内的内容
- 虽然const int *pb 或者 int const *pb,前面两个一样。因为都是在 * 号的左边,不能可以修改指针本身,可以修改指针指向的内容
5.指针运算,有三种
5.1指针+-整数
- *(p + i) == arr[ i ],先加,再解引用。p拿到的是数组的首元素地址
- 可以通过循环指针每次 + i,就能访问到对应空间拿到元素
5.2指针-指针
- 直接上栗子
- 指针-指针的绝对值,是两个元素之间的个数
- 计算的前提条件一定是指向同一块空间
- 首先要知道strlen函数,是从第一个元素开始向后访问直到‘ \0 ’之前的字符长度
- 当通过函数调用时,先把a首元数存储起来,while里的表达式刚好满足strlen函数的特性,
- a传过来是个地址
- *a访问到这块空间只要不等于‘\0’,a++通过不断自增,最终会指向 \0的地址
- 返回值时,用临时指针变量start存储下来的首地址。a的地址 - start的地址,a - start就是这个字符串的长度了,画个图给你理解一下吧
5.3指针的关系运算
- 指针和指针比较大小,地址和地址比较大小
- zarr + sz 刚好是10后面一个地址 ,因为是小于号嘛
6. 野指针
6.1野指针成因
- 局部变量指针未初始化,默认为随机值
- 指针指向的空间释放
- 当指针指向的范围超出数组arr的范围时,p就是野指针
- 这个可以去vs里自己试试哈
-
int main() { int *p;//第一种局部变量指针未初始化,默认为随机值 *p = 20; //第二种,数组越界访问 int arr[10] = {0}; int *p = &arr[0]; int i = 0; for(i=0; i<=11; i++) { //当指针指向的范围超出数组arr的范围时,p就是野指针 *(p++) = i; } return 0; } int* test() { int n = 100; return &n;//当返回地址时,地址已经销毁还给内存了,拿到的是个野指针 } //第三种情况 int main() { int*p = test();//接收到的就是野指针了 printf("%d\n", *p); return 0; }
6.2如何规避野指针
- 6.1野指针成因
- 知道指针应该指向哪里,就给一个明确的地址
- 不知道指向哪里就初始化为NULL(空指针)
- NULL 是C语言中定义的⼀个标识符常量,值是0,0也是地址,这个地址是无法使用的,读写该地址 会报错
- 不要返回局部变量地址。需要检测地址的有效型,下面assert断言
7.assert断言
assert.h 头文件定义了宏 assert() ,用于在运行时确保程序符合指定条件,如果不符合,就报错终⽌运行。这个宏常常被称为“断言”
1.基本用法,就拿下面栗子说,当输入大于10会报错有弹窗,控制台还指明了第几行,例子中是130行
7.1assert的好处
- assert 出现错误的时候,直接报错,指明什么文件,哪一行
- 想vs这种集成环境中,release版本中,会被直接优化掉。
- 想关掉assert断言,在#include <assert.h>的前面用#define NDEBUG,当然也可以注释掉
7.2assert的缺点:引入了额外的检查,增加了程序的运行时间
7.3额外补充:
- strlen函数的返回值是size_t(无符号整型);
- 想一想处理这一块功能需不要assert断言,适当的使用,提高代码韧性,变得强壮些
8. 指针的使用和传址调用
- 当实参传递给形参的时候,形参是实参的一份临时拷贝
- 对形参的修改不会影响实参的
- 下方栗子
- 当你用传值调用(swap2)的时候是完成不了的交换两数的
- 传址调用应用场景:传址调用可以与主调函数建立联系,可以在函数内部修改主调函数中变量的值
- 传值调用:如果只需要主调函数中的变量值,就可以用