前言:该篇我将详细讲解指针当中的一些基本概念,有内存和地址的部分硬件知识,有专门服务于指针的操作符&和*,有指针大小固定不变的原因,还有专属于指针的运算规则。
目录
1. 内存和地址
1.1 内存地址的概念(指针的概念)
1.2 理解编址【硬件知识的补充】
2. 指针相关的操作符
2.1 取地址操作符(&)
2.2 解引用操作符(*)
2.2.1 指针变量创建和拆分理解(操作符 * 在不同情况下的意义)
2.2.2 小知识:下标引用操作符[ ]的本质
3. 指针变量的大小(通用性质)
4.指针的访问范围
4.1 证明1:指针的解引用
4.2 证明2:指针+-整数的地址跳过
5. 指针的运算(常用于数组)
5.1 指针+-整数
5.2 指针++、--
5.3 指针 - 指针
5.4 指针的关系运算(地址的高低比较)
6. 传值调用和传址调用
1. 内存和地址
1.1 内存地址的概念(指针的概念)
引入一个生活中的案例:加入你所在的学生宿舍楼有100个房间,你的朋友来你宿舍玩,假如房间没有编号,就得挨个房⼦去找,这样效率很低;但是我们如果根据楼层和楼层的房间的情况,给每个房间编上号( 如102, 306, 404等),你的朋友对照房间号就可以快速找到房间。
⽣活中,每个房间有了房间号,就能提⾼效率,能快速的找到房间。如果把上⾯的例⼦对照到计算机中,⼜是怎么样呢?
内存单元:其实计算机中也是把内存划分为⼀个个的内存单元,每个内存单元的大小取1个字节。
补充:8比特(bit)=1字节(byte),1024比特 = 1KB,1024KB=1MB ……电脑上的运行内存一般是8GB/16GB/32GB
在这个生活案例中,一个学生宿舍等于一个内存单元,这个宿舍可以住8个“比特人”。而门牌号对应的就是内存单元的编号,在生活中也被称为地址,C语⾔中给地址起了个新的名字叫:指针。
所以我们可以理解为: 内存单元的编号 == 地址 == 指针
1.2 理解编址【硬件知识的补充】
因为内存中字节很多,所以需要给内存进⾏编址(就如同宿舍很 多,需要给宿舍编号⼀样)。计算机中的编址,并不是把每个字节的地址记录下来,⽽是通过硬件设计完成的。
但是硬件与硬件之间是互相独⽴的,那么如何通信呢?答案很简单,⽤"线"连起来。 ⽽CPU和内存之间也是有⼤量的数据交互的,所以两者必须也⽤线连起来。
其中影响最大的一组线被称作地址总线,32位机器有32根地址总线, 每根线只有两态,表⽰0,1【电脉冲有⽆】,那么⼀根线,就能表⽰2种含义,2根线就能表⽰4种含 义,依次类推。32根地址线,就能表⽰2^32种含义(组合),每⼀种含义(组合)都代表⼀个地址。
地址信息被下达给内存,在内存上,就可以找到该地址对应的数据,将数据在通过数据总线传⼊ CPU内寄存器。
2. 指针相关的操作符
2.1 取地址操作符(&)
取地址操作符(&):用于获取变量的内存地址,可以应用于任何数据类型的变量,包括基本数据类型( 如:整型、浮点型、字符型 )和复合数据类型( 如:结构体、数组、联合体)。
基本语法:
1. &变量名 //比如:int *p = &a
%p打印格式:
%p
是专门用来打印地址的(即:打印指针变量),以16进制输出地址。
int main()
{int a = 0;printf("a的地址是:%p", &a);return 0;
}
可以看到,地址是以16进制打印的,而且数字前面不会有“0X”。
2.2 解引用操作符(*)
解引用操作符(*):指针变量是专门⽤来存放地址的变量,对指针使用解引用操作符(*),其实是对地址进行解引用操作,从而间接访问该地址存放的信息。
间接访问包括可读取、可修改:
int main()
{int a = 0;int* pa = &a;//间接访问————读取printf("读取到a的值:%d\n", *pa);//间接访问————修改*pa = 10;printf("修改后a的值:%d\n", a);return 0;
}
2.2.1 指针变量创建和拆分理解(操作符 * 在不同情况下的意义)
指针变量创建的基础语法:
1. 数据类型 *指针变量名;
符号* 不是解引用的意思吗,为什么这里也有符号*,要怎么理解这里的符号*?
符号*的多重作用:其实符号* 的作用除了进行解引用操作,还有声明指针变量的作用。
(1)在指针变量的创建或初始化阶段,符号*的作用是声明指针变量;
(2)出去第1种情况,其他情况都是解引用操作。
假如现在有int a=0以及int *p=&a这两条语句:
int *p的拆分解读:
int* p说明变量p是个指针,相当于函数的声明。操作*p相当于函数的调用。函数调用前必须先声明,指针也一样,像这里变量a不是指针变量却使用操作*a是会报错的。
补充:其实在变量创建时,符号* 紧贴“数据类型” 和 紧贴“指针变量名” 的效果是一样的。(如:int* p和int *p)
需要注意的是:1个符号* 只能声明它身后最近的1个变量名是指针,不能1个符号* 声明多个变量名。
比如:
这里的变量pa、pb、pc,只有pa是被符号*声明成指针变量,而pb、pc都是普通整型变量。
如果想让pa、pb、pc都是指针,要这样写:“ int *pa,*pb,*pc; ”。
2.2.2 小知识:下标引用操作符[ ]的本质
其实对于数组arr来说,我们对其进行下标引用,其实在计算机中会自动转换成解引用,再对数据进行运算等操作。( arr[数字] == *(arr + 数字) )
比如:语句arr[3] = 0。计算机会先把“arr[3]”转换成“*(arr + 3)”,再对arr+3那里的地址进行间接访问。
3. 指针变量的大小(通用性质)
(1)前⾯的内容我们了解到,32位机器有32根地址总线,每根地址线出来的电信号转换成数字信号后 是1或者0,那我们把32根地址线产⽣的2进制序列当做⼀个地址,那么⼀个地址就是32个bit位,需要4个字节才能存储。如果指针变量是⽤来存放地址的,那么指针变的⼤⼩就得是4个字节的空间才可以。
(2)同理在64位机器中,有64根地址线,⼀个地址就是64个⼆进制位组成的⼆进制序列,存储起来就需要 8个字节的空间,指针变量的⼤⼩就是8个字节。
我们用代码测试一下:
int main()
{printf("%zd\n", sizeof(void*));printf("%zd\n", sizeof(char*));printf("%zd\n", sizeof(short*));printf("%zd\n", sizeof(int*));printf("%zd\n", sizeof(double*));return 0;
}
结论:
• 32位平台下地址是32个bit位,指针变量⼤⼩是4个字节
• 64位平台下地址是64个bit位,指针变量⼤⼩是8个字节
• 注意指针变量的大小和类型是⽆关的,只要指针类型的变量,在相同的平台下,大小都是相同的。
4.指针的访问范围
首结论:指针的类型决定了,指针解引⽤的时候有多大的权限(⼀次能间接访问几个字节)。
4.1 证明1:指针的解引用
我们用两段不同的代码比较验证:(VS的内存窗口:存放的数值以小端字节序排列,这里看不懂为什么显示“44332211”是无所谓的)
当我们再次按下F10,n地址中的数据“11223344”会被修改成什么?
按下F10后,代码1的4个字节都被修改成0,n的值从“11223344”变成了“00000000”;代码2的最低位的1个字节被修改成0,n的值从“11223344”变成了“11223300”。所以打印的最终结果也不一样:
4.2 证明2:指针+-整数的地址跳过
以下面的代码举例:
int main()
{int n = 10;char* Pchar = (char*)&n;int* Pint = &n;printf("变量n的地址:%p\n\n", &n);//检查Pcharprintf("Pchar存的地址值:%p\n", Pchar);printf("Pchar+1后的地址值:%p\n\n", Pchar + 1);//检查Pintprintf("Pint存的地址值:%p\n", Pint);printf("Pint+1后的地址值:%p\n", Pint + 1);return 0;
}
我创建了1个int型的变量n,然后我用 字符指针Pchar 和 整型指针Pint 都指向变量n(都存入n的地址)。现在我对Pchar和Pint都分别加一,看看它们存的地址值是否都是简单地加1?
一个内存单元是一个字节,而字符指针Pchar的访问权限是1个字节,整型指针Pint的访问权限是4个字节。char* 类型的指针变量+1跳过1个字节, int* 类型的指针变量+1跳过了4个字节。 这就是指针变量的类型差异带来的变化。
次结论:指针的类型决定了指针向前或者向后⾛⼀步有多大(的距离)。
5. 指针的运算(常用于数组)
5.1 指针+-整数
因为数组在内存中是连续存放的,只要知道第⼀个元素的地址,顺藤摸⽠就能找到后⾯的所有元素。比如像下面这样操作:
int main()
{int arr[10] = {1,2,3,4,5,6,7,8,9,10};int *p = &arr[0]; //获取首元素的地址int sz = sizeof(arr)/sizeof(arr[0]);for(int i=0; i<sz; i++)printf("%d ", *(p+i)); //p+i这⾥就是指针+整数return 0;
}
补充:前面说了,计算机会把arr[i]自动转换成*(arr + i),所以把这里的 *(p+i) 换成 p[i] 结果也是一样的。
5.2 指针++、--
指针的加加减减规则与普通变量是类似的。(关于普通变量的++--,详细请看数学计算类操作符 和 算术类型转换中的++--部分)
不同的是:指针++--之后的地址值变化与指针的类型有关。
取地址&、解引用* 与 指针++-- 的优先级:
- 取地址运算符
&
的优先级最高,它用于获取变量的内存地址。- 解引用运算符
*
的优先级次之,它用于访问指针所指向的内存地址的值。- 指针加加减减操作符(
++
和--
)的优先级最低,它们用于改变指针的值。
比如*p++:先对指针p进行解引用,访问到p所指向的内存的数据,再对该数据进行++操作。
5.3 指针 - 指针
指针 - 指针的意义在于,表示两个指针之间相隔的元素数量,而不是简单的内存地址之差。
比如:
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int *p1 = &arr[0]; int* p2 = &arr[4];printf("p2 - p1 = %d\n", p2 - p1); //p2与p1隔了4个元素return 0;
}
p1与p2之间隔了4个元素,而p1到p2共5个元素。
指针 - 指针的真实计算方式:
结果 == 内存地址之差 / 被减指针的类型权限
以下面的代码为例:(pi是整型指针,pc是字符型指针,psh是短整型指针)
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int *pi1 = &arr[0]; int* pi2 = &arr[4];printf("pi2 - pi1 = %d\n", pi2 - pi1); //除以intprintf("pi1 - pi2 = %d\n\n", pi1 - pi2);//除以intchar *pc = (char*)&arr[7] ;printf("pi2 - pc = %d\n", pi2 - pc);//除以intprintf("pc - pi2 = %d\n\n", pc - pi2);//除以charshort* psh = (short*)&arr[0];printf("pi2 - psh = %d\n", pi2 - psh);//除以intprintf("psh - pi2 = %d\n\n", psh - pi2);//除以shortprintf("pc - psh = %d\n", pc - psh);//除以charprintf("psh - pc = %d\n", psh - pc);//除以shortreturn 0;
}
结果如下:
补充:不存在 指针+指针、指针*指针、指针/指针、指针%指针 以及 指针的连减,上述这些操作都没有意义,编译器会报错的。
5.4 指针的关系运算(地址的高低比较)
指针的关系运算符包括<、<=、>、>=,这些运算符比较两个指针的大小,但这种比较仅在它们都指向同一个数组中的元素时才有意义。
比如:
int main()
{int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int* p = &arr[0];int i = 0;int sz = sizeof(arr)/sizeof(arr[0]);while (p < arr + sz) //指针的⼤⼩⽐较{printf("%d ", *p);p++;}return 0;
}
这里的条件“p < arr + sz”,就是为了防止数组的越界访问。
6. 传值调用和传址调用
学习指针的⽬的是使⽤指针解决问题,那什么问题,⾮指针不可呢?
例如:写⼀个函数,交换两个整型变量的值。
⼀番思考后,我们可能写出这样的代码:
代码1:传值调用
void Swap1(int x, int y)
{int t = x;x = y;y = t;
}int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交换前:a=%d b=%d\n", a, b);Swap1(a, b);printf("交换后:a=%d b=%d\n", a, b);return 0;
}
当我们运⾏代码,结果如下:
其实通过监视窗口(自己调试一下),我们可以发现:main函数内部创建了a和b,在Swap1函数内部创建了形参x和y接收a和b的值,x和y确实接收到了a和b的值,不过x的地址和a的地址不 ⼀样,y的地址和b的地址不⼀样,相当于x和y是独⽴的空间。那么在Swap1函数内部交换x和y的值, ⾃然不会影响a和b。
Swap1函数在使⽤ 的时候,是把变量本⾝直接传递给了函数,这种调⽤函数的⽅式我们之前在函数的时候就知道了,这 种叫传值调用。
传址调用:可以让函数和主调函数之间建⽴真正的联系,在函数内部可以修改主调函数中的变量;所 以未来函数中只是需要主调函数中的变量值来实现计算,就可以采⽤传值调⽤。如果函数内部要修改 主调函数中的变量的值,就需要传址调⽤。
所以我们写出新的版本:
void Swap2(int* px, int* py)
{int tmp = 0;tmp = *px;*px = *py;*py = tmp;
}int main()
{int a = 0;int b = 0;scanf("%d %d", &a, &b);printf("交换前:a=%d b=%d\n", a, b);Swap2(&a, &b);printf("交换后:a=%d b=%d\n", a, b);return 0;
}
结果如下:
我们可以看到实现成Swap2的⽅式,顺利完成了任务,这⾥调⽤Swap2函数的时候是将变量的地址传 递给了函数,这种函数调⽤⽅式叫:传址调⽤。
本期分享完毕,谢谢大家的支持Thanks♪(・ω・)ノ