Bootstrap

指针之旅(1)—— 指针基础概念知识(详细解析)

前言:该篇我将详细讲解指针当中的一些基本概念,有内存和地址的部分硬件知识,有专门服务于指针的操作符&和*,有指针大小固定不变的原因,还有专属于指针的运算规则。

目录

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);
	//检查Pchar
	printf("Pchar存的地址值:%p\n", Pchar);
	printf("Pchar+1后的地址值:%p\n\n", Pchar + 1);
	//检查Pint
	printf("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 指针++、--

指针的加加减减规则与普通变量是类似的。(关于普通变量的++--,详细请看数学计算类操作符 和 算术类型转换中的++--部分)

不同的是:指针++--之后的地址值变化与指针的类型有关。

取地址&、解引用* 与 指针++-- 的优先级:

  1. 取地址运算符&的优先级最高,它用于获取变量的内存地址。
  2. 解引用运算符*的优先级次之,它用于访问指针所指向的内存地址的值。
  3. 指针加加减减操作符(++--)的优先级最低,它们用于改变指针的值。

比如*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); //除以int
	printf("pi1 - pi2 = %d\n\n", pi1 - pi2);//除以int

	char *pc = (char*)&arr[7] ;
	printf("pi2 - pc = %d\n", pi2 - pc);//除以int
	printf("pc - pi2 = %d\n\n", pc - pi2);//除以char

	short* psh = (short*)&arr[0];
	printf("pi2 - psh = %d\n", pi2 - psh);//除以int
	printf("psh - pi2 = %d\n\n", psh - pi2);//除以short

	printf("pc - psh = %d\n", pc - psh);//除以char
	printf("psh - pc = %d\n", psh - pc);//除以short
	return 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♪(・ω・)ノ

;