1.地址和地址上存储的值
- 地址:在计算机内存中,每个字节都有一个唯一的地址,用来标识它在内存中的位置。这个地址通常以十六进制表示,并且在程序运行时是固定的。指针就是用来存储和操作这些地址的变量,它们存储的是内存中某个对象或变量的地址。
- 值:内存地址中存储的实际数据,即在特定地址处存储的内容。这个值可以是任何数据类型,比如整数、字符、浮点数等。
所以当我们说“解引用一个指针”时,我们实际上是在获取该指针所指向地址处存储的值。
通俗的讲,内存中的每个字节是实际存在的,我们用地址标识这些字节。而地址本身没有意义,只有解引用地址才能得到该地址对应的字节数据。
并不是说一个地址只标识一个字节,int *P,char *Q,
对P保存的地址解引用*P
可以得到一个8字节整型,对Q保存的地址解引用*Q
可以得到一个4字节字符。因此,我们可以通过强制转换指针的类型,修改它所标识字节的范围!
刚才我们使用了变量P和Q的值也就是他们保存的地址,当然也可以变量P进行修改:P = Q
,这时我们修改了内存里一个8字节的数据,但是我们没有使用地址,这是怎么做到的呢?
当我们执行 int *P;
这样的语句时,系统会为变量 P
分配一块内存,然后将其地址与标识符 P
关联起来。所以在实际使用时,我们不需要显式地使用地址,编译器会自动帮你完成地址的管理。
在 P = Q;
这样的语句中,编译器会知道 P
的地址,并将 Q
的值存储到该地址中。这个过程是由编译器隐式地完成的。你不需要手动获取 P
和Q
的地址,因为编译器已经知道P和Q的地址,可以直接将值存储到那个地址中。
也就是说我们虽然没有显式地解引用地址,然后对实际的字节进行修改,实际上标识符P已经和若干字节的地址关联起来了,所以我们在使用变量P和Q时,实际上就是通过解引用地址来操作实际的内存字节
只不过指针为我们提供了更灵活更强大的操作内存的方式,感谢指针!
2.二级指针进阶用法:深入理解地址和内存的关系
二级指针也是指针,保存的也是地址,不论几级指针,只要是指针,就保存地址。
而二级指针的类型是确定的,是一个指针,一级指针的类型不确定
我们之前谈到了,改变指针类型,可以修改它标识内存中字节的范围,因此也就修改了这个指针解引用的结果
而二级指针解引用的结果是内存中的8个字节,一级指针解引用的结果由它的类型确定,比如int是4字节,char是1字节,等等
说了这么多,将一级指针转换成二级指针有什么用呢?
举一个例子
typedef struct task_s { void *next; // 指向下一个task int a; }task_t;
这是一个结构体,大小是8+4=12字节
下面我们将一级指针转换成二级指针:
01: task_t *task = (task_t *)malloc(sizeof(task_t)); // 创建一个task_t对象 02: task_t *p = (task_t *)malloc(sizeof(task_t)); // 创建另一个task_t对象 03: 04: task->next = p; // 等价于(*task).next = p; 05: *(task **)task = p; //将一级指针转换成二级指针,并解引用, 结果是next指针
注意,04行和05行是完全等价的,你看出来二级指针的用法了吗?
一级指针task和二级指针task指向的地址是同一个task_t对象的首地址
而它们的区别在于解引用的结果:
1.一级指针解引用的结果是12个字节也就是整个task_t对象,操作其中的next成员:task->next或(*task).next
2.二级指针解引用的结果不再是整个对象,而是task_t对象的前8个字节,刚好是next在内存中的位置,所以*(task **)task等价于next
为什么会有这样的区别?
答案在前文中提到过两次:改变指针类型,可以修改它标识内存中字节的范围,因此也就修改了这个指针解引用的结果
将一级指针转换成二级指针,指针所标识的内存中的字节的范围从12字节变为8字节,解引用的结果也因此改变