目录
- 引言
- 一、内存和地址
-
- 内存
- 如何理解编址
- 二、指针变量和地址
-
- 取地址操作符(&)
- 指针变量和解引用操作符(*)
-
- 指针变量
- 拆解指针类型
- 解引用操作符
- 指针变量的大小
- 三、指针类型的意义
-
- 指针的解引用
- 指针+ -整数
- void*指针
- 四、指针运算
-
- 指针 + – 整数
- 指针 – 指针
- 指针的关系运算
- 总结
引言
距离上次写博客已经好久了,中间遇到许多挫折,几度想要放弃,最后想通了,与其每天生活在焦虑中,不如真正的静下心来学习知识,不求做到完美,但求每天都有进步,敬不完美的明天。
一、内存和地址
内存
说到内存,大家第一时间想到的会是什么呢?想必有很多人会认为在电脑中500GB、1TB这样的会是内存,要是这样说就比较外行了。 在搞清楚内存是什么之前,我们先看看电脑中有什么。 先简单介绍一下,电脑中有CPU、内存、硬盘、还有一些外部设备等等。像我们刚才提到的500G、1T这种都是硬盘,而4GB、8GB、16GB、32GB这种才是内存。 程序运行起来后,程序要加载到内存中,数据的存储也是在内存中进行。 我们知道计算机上CPU(中央处理器)在处理数据的时候,需要的数据是在内存中读取的,处理后的数据也会放回内存中,我们买电脑时,电脑上内存是8GB/16GB/32GB等,那这些内存空间 如何高效管理 ? 把内存划分为一个个的内存单元,每个内存单元的大小取1个字节。其中一个比特位可以存储一个2进制的位1或者0(具体如下图)。
每个内存单元,相当于一个学生宿舍,一个字节空间里面能放 8 个比特位,就好比同学们住的八人间,每个人是一个比特位。 每个内存单元也都有一个编号(这个编号就相当于宿舍房间的门牌号),有了这个内存单元的编号,CPU 就可以快速找到一个内存空间。 生活中我们把门牌号也叫地址,在计算机中我们把内存单元的编号也称为地址。C 语言中给地址起了新的名字叫:指针。 所以我们可以理解为: 内存单元的编号 = 地址 = 指针 
如何理解编址
计算机内有很多的硬件单元,而硬件单元是要互相协同工作的。所谓协同,就是至少相互之间要能够进行数据传递。 但硬件与硬件之间是互相独立的,想要相互通讯就要用 “ 线 ” 将他们连起来。而CPU和内存之间也是有大量的数据交互的,所以,两者必须也用线连起来。 我们现在只需要理解一组线———地址总线。
CPU想要访问内存中的某个字节空间,就必须知道这个字节空间在内存的什么位置,所以需要给内存进行编址(就像想要找到一个宿舍,需要给宿舍编号一样)。 计算机中的编址,并不是把每个字节的地址记录下来,而是通过硬件设计完成的。
我们可以简单理解,32位机器有32根地址总线,每根线只有两态,表示0,1(电脉冲有无),那么一根线,就能表示2种含义,2根线就能表示4种含义,依次类推。32根地址线,就能表示2^32种含义,每一种含义都代表一个地址。
地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传入CPU内寄存器。 
二、指针变量和地址
取地址操作符(&)
我们要先理解,变量创建的本质是什么? 变量创建的本质是在内存中申请一块空间,用来存放数据。 举个例子:
int main()
{
int a = 2;
return 0;
}
向内存申请4个字节的空间,用来存储2;空间的名字叫a,这个a名字不是给编译器、计算机看的,而是给程序员自己看的。因为计算机知道地址是多少,但程序员很难记住地址。
那我们如何得到 a 的地址呢?这里就得学习一个操作符 (&)——取地址操作符
1 #include <stdio.h>
2 int main()
3 {
4 int a = 10;
5 &a; //取出a的地址
6 printf("%p\\n", &a);
7 return 0;
8 }
我们得到结果,并将其绘图,直观得到: 
按照我画图的例子,会打印处理:006FFD70;因此 &a 取出的是 a 所占 4 个字节中地址较小的字节的地址。虽然整型变量占用 4 个字节,但我们只要知道了第一个字节地址,就能顺藤摸瓜地访问到 4 个字节的数据。
指针变量和解引用操作符(*)
指针变量
我们通过取地址操作符 (&) 拿到一个地址,如:0x006FFD70,这个数值有时候也是需要存储起来,方便后期再使用,我们要把这样的地址存放在指针变量中。 举个例子:
1 #include <stdio.h>
2 int main()
3 {
4 int a = 10;
5 int * pa = &a; //取出a的地址并存储到指针变量pa中
6
7 return 0;
8 }
p被称为指针变量,理解为:存放指针的变量。 指针变量也是一种变量,这种变量就是用来存放地址的,存放在指针变量中的值都会理解为地址。
拆解指针类型
p的类型是 int * ,我们能够通过 p(地址)找到 a(数据)。这里的 int *应该分开理解。
- int说明p指向的数据(a)的类型是int
- *说明p是指针变量
同理,字符数据的指针变量就是char *
解引用操作符
“ * ” – 解引用操作符(也叫间接访问操作符) 举个例子:
#include <stdio.h>
int main()
{
int a = 0;
int *p = &a;
*p = 10;
printf("%d",a);
return 0;
}
运行结果如下:
由此我们知道,解引用操作符的作用是知道地址后,通过地址来找到地址所指向的对象。 那么为什么不直接修改 a 的值呢? 我们要拥有大哥思维,即所有的事不要亲历亲为,要让手下的“小弟”去干活。 这里就是把a的修改交给了pa来操作,这样对a的修改,就多了一种的途径,慢慢积累就会发现这样做的好处了。
指针变量的大小
pa 是指针变量,指针变量也是变量,也是需要向内存申请空间的。 那么pa 这个指针变量的大小是多少?向内存申请了多大空间? 其实,地址的存储需要多大空间,指针变量的大小就是多大。
我们分析一下:
32位的机器上,地址是通过32根地址线传递的 那么一个地址就是32个0/1的二进制,需要32个bit位存储 = 4个字节 指针变量的大小也就是4个字节 64位的机器上,地址是通过64根地址线传递的 那么一个地址就是64个0/1的二进制,需要64个bit位存储 = 8个字节 指针变量的大小也就是8个字节
总结一下:
- 32位平台下地址是32个bit位,指针变量大小是4个字节
- 64位平台下地址是64个bit位,指针变量大小是8个字节
- 注意指针变量的大小和类型是无关的,只要是指针类型的变量,在相同的平台下,大小都是相同的。
三、指针类型的意义
指针变量的大小和类型无关,指针类型是有特殊意义的。
指针的解引用
#include <stdio.h>
int main()
{
int a = 0x11223344;
int *pa = &a;
//char *pa = &a;
*pa = 0;
return 0;
}
我们进行调试:
我们取出a的地址,然后分别将它存在int*和char*类型的指针变量pa中,然后进行解引用操作,将pa的值改为0,分别得到以下结果:
那么我们就得到,当我们是整数类型指针时,我们的视野是4个字节;当我们是字符类型指针时,我们的视野是1个字节。简单来说它的意义就是视野范围(操作范围)不同,整型类型指针认为它是指向的是整型,所以它能访问4个字节;字符类型指针认为它是指向的是字符,所以它只能访问1个字节。
因此,我们得出结论: 指针的类型决定了,对指针解引用的时候有多大的权限(一次能操作几个字节)。 比如:char* 的指针解引用就只能访问一个字节,而 int* 的指针的解引用就能访问四个字节。
指针+ -整数
#include <stdio.h>
int main()
{
int a = 10;
int* pa = &a;
char* pc = &a; // int*
printf("pa = %p\\n", pa);
printf("pa+1 = %p\\n", pa + 1);
printf("pc = %p\\n", pc);
printf("pc+1 = %p\\n", pc + 1);
return 0;
}
运行结果如下:
由此我们可以看出,int*类型的指针变量加1,跳过了4个字节;char*类型的指针变量加1,跳过了1个字节。
因此,我们得出结论: 指针的类型决定了指针向前或者向后走一步有多大(距离)。
void*指针
在指针类型中有一种特殊的类型是 void * 类型,可以理解为无具体类型的指针(或者叫泛型指针),这种类型的指针可以用来接受任意类型地址。但是也有局限性,void* 类型的指针不能直接进行指针的+ -整数和解引用的运算。
当我们有一个指针变量,但是不知道要接收什么类型的指针时,直接创建一个void*类型的指针变量就行。
但是物极必反,当我们对void*类型的指针变量进行解引用操作或进行+ -运算时,会报错。 
四、指针运算
指针运算有三种,分别是:
- 指针 + – 整数运算
- 指针 – 指针
- 指针的关系运算
指针 + – 整数
我们先以数组为例,因为数组在内存中是连续存放的,只要知道第一个元素的地址,顺藤摸瓜就能找到后面的元素。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//拿到第一个元素的地址
int* p = &arr[0];
for (i = 0; i < sz; i++)
{
printf("%d ", *p);
p++;//p = p+1;
}
return 0;
}
运行结果如下:

同样的问题,我们再换一个思路:
因为 p+i 就是数组中下标为 i 元素的地址,所以 *(p+i) 就是下标为 i 的这个元素。
#include <stdio.h>
int main()
{
int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int i = 0;
int sz = sizeof(arr) / sizeof(arr[0]);
//拿到第一个元素的地址
int* p = &arr[0];
for (i = 0; i < sz; i++)
{
printf("%d ", *(p+i));
}
return 0;
}
运行结果如下:
我们再举一个例子,当我们想逐字符打印一串字符串时,
#include <stdio.h>
int main()
{
char arr[] = "hello world";
char* pa = &arr[0];//&arr[0]也可以写成arr
while(*pa)//*pa也可以写成*pa != 0
{
printf("%c",*pa);
pa++;
}
return 0;
}
运行结果如下:

指针 – 指针
指针 – 指针的前提是两个指针指向了同一块空间,否则不能相减。 | 指针 – 指针 |:得到的是两个指针之间的元素个数。 举个例子:
#include <stdio.h>
int main()
{
int arr[10] = { 0 };
printf("%lld\\n", &arr[9] – &arr[0]);
printf("%lld\\n", &arr[0] – &arr[9]);
return 0;
}
运行结果如下:

数组随着下标的增长,地址是由低到高变化的
用数学中的移项,并类比日期与天数的问题: &arr[0] + 9 == &arr[9] —> 日期 – 日期 = 天数 &arr[0] – &arr[9] == -9 —> 日期 + 天数 = 日期 9 == &arr[9] – &arr[0] —> 日期 – 天数 = 日期
再举个例子:问题:想一个函数,求字符串个数。
①用指针 + – 整数来写
#include <stdio.h>
size_t my_strlen(char* p)
{
size_t count=0;//计数器
while (*p)
{
count++;
p++;
}
return count;
}
int main()
{
char arr[] = "abcdef";
//数组名其实也是数组首元素的地址
//arr == &arr[0]
char* p = arr;
printf("%zu", my_strlen(arr));
/* size_t n = my_strlen(p);
printf("%zu", n);*/
return 0;
}
运行结果如下: 
②如果我们想用指针 – 指针来写
#include <stdio.h>
size_t my_strlen(char* p)
{
char* start = p;
while (*p)//使用p来找到字符串中的\\0
{
p++;
}
return p – start;
}
int main()
{
char arr[] = "abcdef";
//数组名其实也是数组首元素的地址
//arr == &arr[0]
char* p = arr;
printf("%zu", my_strlen(arr));
/* size_t n = my_strlen(p);
printf("%zu", n);*/
return 0;
}
运行结果如下:

指针的关系运算
指针也是可以比较大小的。 我们之前想要打印一个数组的内容时需要用到数组下标,for循环打印,现在我们可以用指针的关系运算来打印。
#include <stdio.h>
int main()
{
int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = &arr;
int sz = sizeof(arr) / sizeof(arr[0]);
while (p < arr + sz)
{
printf("%d ", *p);
p++;
}
return 0;
}
运行结果如下:

总结
再学一遍指针,现在真正将它学透了,收获颇丰,继续坚持,一切皆有可能。 最后送大家一句话:
世界上只有一种真正的英雄主义,那就是在认清生活的真相后依然热爱生活。——罗曼·罗兰

