指针?地址而已

指针系列目录

第一章 指针初阶——什么是指针?如何使用?【本文】
第二章 指针进阶 01——数组和指针【链接: link
第三章 指针进阶 02——不同类型的指针【待更新】
第四章 指针进阶 03——存放不同指针的数组【待更新】



前言

计算机上的 CPU (中央处理单元) 处理数据时,需要先从内存中读出数据然后再进行处理,最后将处理后的数据放回内存中。为了对内存中的空间进行高效管理,将内存划分为字节 (Byte) 大小的内存单元,并对这些 1 字节大小的空间进行编号,这样 CPU 就可以使用这些编号来对内存进行高效的访问和管理。这个地址,我们给它取个新名字,叫做指针,用来指向对象的地址空间。(本系列所有代码均在 VS Studio 2022 环境中测试运行)


第一章 什么是指针?

这里先讲指针变量,指针变量的本质是一个变量,其中存储了某对象的地址,因此,指针变量是存放了对象地址的变量。但是习惯上,我们把指针变量简短说成了指针,这就和前边“指针就是地址,地址就是指针”的说法产生了冲突。一个是指针(变量),一个是指针(地址),怎么分辩所说的是指针变量还是指针(地址)呢?关于这点就只能放到实际的代码和语境中去分析,因为现在大家所说的指针二字同时表示了指针变量和指针两个概念。


第二章 相关操作符 " & " 和 " * "

2.1 取地址操作符 " & "

C 语言中变量的创建其实是向内存申请空间的过程。如示例代码 2-1 所示,创建 int 型的 a 时,申请了四个字节的空间。

//示例代码 2-1
#include <stdio.h>		//64位环境运行
int main()				//在内存中的存储方式  一共占四个字节
{						//0x0000009DC3CFFAC4  07  
	int a = 7;			//0x0000009DC3CFFAC5  00  
	return 0;			//0x0000009DC3CFFAC6  00  
}						//0x0000009DC3CFFAC7  00 

数据既然有地址,如何得到这个地址呢?这里就要用到取地址操作符 “ & ”,如示例代码 2-2 所示。“ &a ” 这个整体表示 a 的地址,我们可以使用指针变量来存放这个地址。“ & ” 与其说是得到地址,不如说是生成了指向其数据的指针更自然,这里可以结合整型变量的赋值来加深理解。有整型变量 c 和 d, " c = d; " 语句表示将变量 d 的值赋给 c;那么再把 " int *pa = &a; " 语句拿过来," &a " 这个整体本来就表示一个指向整型数据 a 的指针 (int*),将其赋值给同类型的指针变量 int *pa 后,pa 也指向了变量 a 内存储的数据 7 。

//示例代码 2-2
#include <stdio.h>
int main()
{
	int a = 7;
	int *pa = &a;			//pa 就是指针变量,用来存放 a 的地址
	printf("%p\n", &a);		//格式转换符 %p ,表示以地址形式打印
							//输出结果: 0x0000009DC3CFFAC4
	return 0;
}

指针变量如何理解呢?如示例代码 2-3。变量名左侧有 “ * ” 号,表示该变量是指针变量,接下来看“ * ” 左侧的数据类型,如果是 int ,说明该指针变量指向一个 int 型的数据。

//示例代码 2-3 
指向的数据类型 * 指针变量名 		//【指向 数据类型 的 指针(变量)】
char *pa;  				//pa左侧有一个*,pa是指针变量(一级指针)   					【pa本身的类型是char*,一级指针】
						//去掉表示pa是指针的*和变量名pa,剩下是其指向的数据类型char,表示pa是指向char的指针		
							  
char* *pa; 				//pa左侧有两个*,pa是二级指针。和pa最近的*表示pa是指针		【pa本身的类型是char**,二级指针】
						//去掉表示pa是指针的*和变量名pa,剩下的是其指向的数据类型char*,表示pa是指向一级指针char*的指针
						
int(*pa)[5]; 			//pa左侧有一个*,pa是指针变量。去掉表示pa是指针的*和变量名pa,剩下是其指向的数据类型int [5],表示pa是指向有5个整型元素的数组的指针
						//写简洁点,pa是指向整型数组的指针,数组指针				【pa本身的数据类型是 int(*)[5],数组指针】
						
void (*pa)(int, char);  //pa左侧有一个*,pa是指针变量。去掉表示pa是指针的*和变量名pa,剩下是其指向的数据类型void (int, char),表示pa是指向形参为int和char,返回值为void的函数指针
						//写简洁点,pa是指向函数的指针,函数指针					【pa本身的数据类型是 void(*)(int, char),函数指针】
						
int (*(*pa)(int, void(*)(char)))(int); //pa左侧有一个*,pa是指针变量。去掉表示pa是指针的*和变量名pa,剩下是其指向的数据类型int (* (int, void(*)(char)))(int)
									   //int (* (int, void(*)(char)))(int) 这个数据类型是一个函数,该函数有两个参数 int 和 void(*)(char),返回值为 int(*)(int)
									   //其中一个参数 void(*)(char) ,是一个参数为char,返回值为 void 的函数指针。返回值 int(*)(int),是一个参数为int, 返回值为 int 的函数指针
									   //pa 是一个函数指针。参数1:int ;参数2:参数为 char,返回值为 void 的函数指针。返回值:参数为 int , 返回值为 int 的函数指针
									   //pa 是一个参数为int 和 函数指针,返回值是(参数为int,返回值为int的)函数指针。
									   //改写形式:int(*)(int) pa(int, void(*)(char)) 注意该形式只是为了以函数形式来理解,但其本身的书写形式是错误的,在语法上是不被认可的
									   //省流:pa 是函数指针						【pa本身的数据类型是 int ( (*)(int, void(*)(char)) ) (int),函数指针】

2.2 解引用操作符 " * "

当我们知道小明家的地址时,就可以走到这个地址处找到小明这个人。对于指针(变量)来说,它本身存储的数据就是一个地址,那么使用解引用操作符“ * ”就可以直接访问这个地址处存储的数据。

//示例代码 2-4
#include <stdio.h>
int main()
{
	int a = 5;
	int* pa = &a;
	printf("%d\n", a);	//输出:5
	printf("%d\n", *pa);//输出:5
	return 0;
}

如示例代码 2-4 所示,打印 printf() 时使用参数 a 和 *pa 的结果是一致的,说明 *pa 确实可以找到 a 中存储的数据 5 。也可以说,*pa 是变量 a 的另一个名字,它们表示同一个地址空间,那么它们表示的数据内容肯定也是一样的。指针变量起到指向数据存储空间的作用,一旦遇到解引用操作符“*”就可以访问其指向空间内的数据。如图 2-5 ,注意理解指针的指向作用。从下一节“指针变量的性质”开始,请读者自行区分指针变量和指针的概念,文中将不再进行标示。

在这里插入图片描述
图 2-5 指针的指向作用示意图


第三章 指针变量的性质

3.1 指针大小和 sizeof()

首先来了解下地址总线。一台 32 位的机器,假设有 32 根地址线,每根地址线的电信号转换成数字信号后是 1 或者 0 状态,将 32 根地址线产生的 2 进制序列当做一个地址,那么一个地址就是 32 个bit位,即 4 个字节。前边提到指针变量是用来存放地址的,那么指针变量的大小就得是 4 个字节的空间才可以存储这个地址。同理 64 位机器,假设有 64 根地址线,一个地址就是 64 个二进制位组成的二进制序列,存储起来就需要 8 个字节的空间,指针变量的大小就是 8 个字节。由此可以看出,指针的大小是由其当前所在的操作系统或解决方案平台的位数决定(再准确点,是由当前CPU运行模式的寻址位数决定),与指针的类型无关。
示例代码 3-1 展示了部分类型的指针具体大小。sizeof 关键字是一个运算符,它以字节为单位,给出变量或类型(包括聚合类)的存储空间大小,返回类型为 size_t (无符号整数类型)。使用 sizeof 运算符可以便捷求出当前环境中的指针大小。

//示例代码 3-1 
#include <stdio.h>
//代码在VS Studio 64位环境中运行
int main()
{
	printf("%zd\n", sizeof(char *));	 //8
	printf("%zd\n", sizeof(short *));	 //8
	printf("%zd\n", sizeof(int *));		 //8
	printf("%zd\n", sizeof(float *));	 //8
	printf("%zd\n", sizeof(double *));	 //8
	printf("%zd\n", sizeof(long *));	 //8
	printf("%zd\n", sizeof(long long *));//8
	return 0;
}

3.2 指针类型和 void* 指针

根据需要指向的对象(变量)的数据类型,我们需要使用不同类型的指针。比如指向整型数据的对象,就要使用整型指针,能不能用字符型指针呢?反正指针都是来存放地址的,而且同一运行环境中,指针的大小都是相同的。先说结论,使用[和指向对象的数据类型]不一致的指针,可以正确指向这个数据的地址空间,但在进行数据访问时会出现错误。来看示例代码 3-2 。

//示例代码 3-2
#include <stdio.h>
//代码在VS Studio 32位环境中运行
int main()
{
	int a = 0x00000107;   //转换为十进制,这里是数学运算,"^"是乘方: 1*(16^2)+0*(16^1)+7*(16^0)=263
	int*  p_int  = &a;
	char* p_char = &a;										//运行结果:
	printf("a的地址:%p\t*p_int  = %d\n", p_int,	*p_int);	//a的地址:00CFFE30       *p_int  = 263
	printf("a的地址:%p\t*p_char = %d\n", p_char,*p_char);	//a的地址:00CFFE30       *p_char = 7
	return 0;
}

示例代码 3-2 在VS Studio 32 位平台中运行,此时指针大小为 4 字节,以 %p 形式打印地址时,会显示 32 位二进制的数字,即 8 位的十六进制数字。VS Studio 中整型数据为 4 字节,32 个比特位,可以使用 8 位的十六进制数字表示, 0x00000107 即是这种表示方式,前缀 0x 表示该数字是十六进制。代码中,变量 a 的数据类型为 int ,当使用指针指向变量 a 的存储空间时,应当使用 int* 型的指针。这里创建了两个类型的指针 int* p_int 和 char* p_char,二者在以地址形式打印时,给出了相同的结果,说明这两种类型的指针,都可以正确指向变量 a 的存储空间,但当打印所指向空间内的数据时,只有 p_int 指针的结果是我们所期望的。

这里就要分析下为何 char* 类型的 p_char 指针无法解引用得到 263 这个值呢?原来创建 p_char 时的指针类型 char* ,决定了 p_char 本来是要指向 char 类型的数据对象,而 char 是一个字节的大小,因此解引用 *p_char 时也只能访问一个字节大小的空间,也就是只能得到一个字节空间里的数据。当使用 char* 指针指向 int 变量时,对 char* 指针解引用,只能访问 int 型 4 字节空间的四分之一,一个字节,因此当然无法得到原数据 263。这里访问 int 的一个字节,先从低位数据开始访问(如十进制整数 123 ,3 是个位,它就是数据低位)。指针是指向数据的起始地址,当开始访问数据空间时,是从数据低位所在的空间到数据高位所在的空间依次访问。但数据低位是在地址高位,还是地址低位呢?这里又牵扯到大小端字节序存储的问题,这个取决于编译器的实现方式,这里给出 VS Studio 小端字节序存储数据的概念图 3-3,仅供参考,当作一个小拓展。这里可以得出初步结论,指针的类型决定了指针解引用时可以访问的空间大小,因此在实际使用指针时,记得保持指针和其指向对象的数据类型保持一致。
在这里插入图片描述图 3-3 VS Studio 小端字节序存储示意图

指针类型说到这里,不得不提到一个无类型的指针 void* 。首先,void* 也是指针类型的一种,特殊之处就在于其没有具体类型,既然没有具体类型,那就可以用来接收不同类型的指针,也就是用来接收不同类型数据的地址,实现泛型编程的效果。但 void* 类型的指针无法直接进行解引用操作和指针运算,正如前边提到的:指针的类型决定了指针解引用时可以访问的空间大小,void* 没有具体的指针类型,就无法对指针即将访问的空间大小进行限定,也就是指针自己都不知道自己要访问多大空间的数据,这波属于是直接把指针的 CPU 干烧了狗头。虽然 void* 没有类型,但需要访问空间时咱们可以自行赋予合适的类型呀!如示例代码 3-4 所示。

//示例代码 3-4
//动态开辟指定大小的内存空间
#include <stdio.h>
#include <malloc.h>
#include <assert.h>
#include <string.h>
int main()
{
	char* p = NULL;
	p = (char*)malloc(sizeof("hello world!"));	//根据需要自行选择需要的指针类型
	assert(p);									//确认空间开辟成功?
	char* pc = p;								//空间开辟成功,开始使用
	strcpy(pc, "hello world!");
	printf("%s\n", pc);
	free(p);									//谁申请空间,使用结束就由谁释放空间
	p = NULL;									//使用结束,置空
	pc = NULL;									//使用结束,置空
	return 0;
}

这里简单介绍下示例中使用的 malloc() 。malloc() 用于在堆区动态开辟一块内存空间,该空间以字节为单位,并返回指向开辟空间起始地址的指针,而且新分配的空间内容不会初始化,因此会保留不确定的值,使用时记得初始化内容。当开辟空间失败时,会返回空指针。值得一提的是,该函数的返回指针类型可以根据自己的设计需要自行转换,示例中使用了 char* 的指针类型转换。
函数原型: void* malloc(size_t size);
size_t 是无符号整数类型,以字节为单位。

3.2.1 空指针 NULL

NULL 是 C 语言定义的一个标识符常量,在 ASCII 编码表中表示空,二进制等数字进制表示的值均为 0 ,0 也是一个地址,但这个地址是无法使用的。当创建一个指针变量时,如果此刻没有明确的地址赋给该指针,可以先置空。后续有了明确的对象后,再将该指针指向对象。当需要使用指针时,先检查指针的有效性,再使用该指针。注意!!!置空的指针(空指针)无法直接解引用。 见示例代码 3-5。

//示例代码 3-5
//仅作为 NULL 的使用参考,示例中的数组指针不适合在一维数组中使用
//01
#include <stdio.h>
int main()
{
	int arr[] = { 1, 2, 3, 4, 5 };
	int (*parr1)[5] = &arr;			// 创建数组指针 parr1 时,将 parr1 指向对象(数组arr)
	void* parr2 = &arr;				//可以使用无类型指针接收数组 arr 的地址,但解引用等操作时,需要注意指针的类型
	return 0;
}
//02
#include <stdio.h>					//NULL  == ((void*)0)
int main()
{
	int(*parr1)[5] = NULL;			//创建数组指针 parr1 时无指向的对象,暂时置空
	int arr1[] = { 1, 2, 3, 4, 5 };
	parr1 = &arr1;					//有了数组 arr1 后,将数组指针 parr1 指向数组 arr1
	//其它操作......
	parr1 = NULL;					//不需要 parr1 指向数组 arr1 ,parr1 暂时置空
	int arr2[] = { 3, 4, 5, 6, 7 };
	if (parr1 == NULL)				//此时需要重新指向新空间数组 arr2。先检查 parr1 是否指向有效的地址(非空),防止需要访问的地址被覆盖
		parr1 = arr2;				//parr1 重新指向新地址 arr2

	return 0;
}

3.2.2 assert 断言

关于检查指针的有效性 (指针是否为空) ,示例代码 3-5 02 中的条件判断语句 if()… 属于比较温柔的判断方式,另一种暴力方式是使用 assert 断言。
assert() 是一个宏,头文件为 assert.h ,接收一个表达式作为参数。如果此宏的函数形式的参数表达式等于零(即表达式为 false),则会将消息写入标准错误流设备并被调用,从而终止程序执行;如果参数表达式不等于零(即表达式为 true),则不会产生任何作用,程序继续运行。如果在包含时已定义具有该名称的宏,则禁用此宏。这允许编码人员在调试程序时根据需要在源代码中包含任意数量的调用。因此,此宏旨在捕获编程错误,而不是用户或运行时错误,因为它通常在程序退出其调试阶段后被禁用。如示例代码 3-6 所示。

//示例代码 3-6
//#define NDEBUG	//启用后,程序中的所有 assert() 将被禁用,不再发挥断言的作用
#include <stdio.h>
#include <assert.h>
int main()
{
	int a = 7;
	int* pa = &a;
	assert(pa);		//指针变量 pa 不为空,逻辑“真”,assert() 无作用。等价写法 assert(pa != NULL)
	pa = NULL;
	assert(pa);		//指针变量 pa 为空,逻辑“假”,assert() 报错并终止程序运行。
					//如报错信息【 Assertion failed: pa, file E:\C_code\learning_c\2024_C_code\24_test_03_13\24_test_03_13\test.c, line 27】
					//这里的报错信息给出了断言失败的对象、源文件的名称以及发生断言的行号
	return 0;
}

一般我们可以在 Debug 中使用 assert,在 Release 版本中选择禁用。在VS这样的集成开发环境中,在 Release 版本下,直接就是优化掉了。这样在 Debug 版本写有利于程序员排查问题,在 Release 版本不影响用户使用时程序的效率。最后注意禁用 assert 时定义的宏 NDEBUG 需要放在包含的头文件 assert.h 之前。

3.3 指针运算

3.3.1 指针和整数

指针和整数间的加减,表示指针向前或向后走的步宽。指针加整数,表示向前走,反之向后走。步宽和指针的类型紧密联系,指针加减 1 ,即是向前或向后越过一个指针指向的数据类型的宽度。如果指针类型为 int* ,则一次可以越过一个 int 类型宽度,即 4 个字节的空间。

//示例代码 3-7
#include <stdio.h>
int main()
{
	int arr[] = { 1, 2, 3, 4, 5 };
	int* p = arr;
	
	printf("%d\n", *p);				//1 *p == *(p + 0)
	printf("%d\n", *(p + 2));		//3	解引用的三种写法 *(p + 2) == p[2] == 2[p] 等价
	printf("%d\n", p[2]);			//3	由以上三种写法也可以看出,数组元素使用下标访问的本质还是指针运算
	printf("%d\n", 2[p]);			//3	该种写法不方便直接理解,但语法正确,可以编译

	p = arr + 3;					//将指针 p 重新赋值
	printf("%d\n", p[-1]);			//3

	return 0;
}

如示例代码 3-7 所示。数组名表示指向数组首元素地址的指针,也可以理解为数组名表示数组首元素的地址,读者自行选择理解方式。既然 p 和数组名 arr 都是 int* 类型的指针,那么可以直接使用赋值符号,此时指针 p 也是指向数组 arr 首元素地址的指针,解引用后以 %d 形式打印,也就得到了数组的首元素 1 。p + 2 就是从 p 原来指向的地址再向后越过 2 个 int 数据类型的大小,指向数组的第 2 个元素 3 (首元素是第 0 个元素)。指针 p 重新赋值后, p 指向的地址和 arr+3 指向的地址相同,p[-1] 即 *(p - 1) ,也就是 (arr+3)-1 的地址所指向的元素 3 ,如指针和整数运算示意图 3-8 所示。数组的存储空间是连续的,且随着数组元素的下标的增长,数组元素的地址逐渐变大,这也是本次示例中对指针进行加减整数运算的基础所在,这里指针的地址变化是可预测的。

指针
图 3-8 指针和整数运算示意图

3.3. 2 指针和指针

指针和指针之间既可以进行运算,也可以进行大小比较。指针进行比较时,如示例代码 3-9 。指针之间进行运算时的限制条件较多,因为随意两个指针间的差值是毫无意义的,但当两个指针同时指向同一个数组空间时,指针和指针间的差值的绝对值是两个指针之间的元素个数,如示例代码 3-10 模拟实现 strlen() 的功能,使用指针间的差值作为字符串长度的值。

//示例代码 3-9
#include <stdio.h>
int main()
{
	int arr[] = { 1, 2, 3, 4, 5 };
	int* p1 = arr;
	int* p2 = arr + 1;
	if (p1 < p2)	
		printf("p1 < p2\n");	//p1 < p2
	return 0;
}

//示例代码 3-10
#include <stdio.h>
#include <assert.h>
size_t my_strlen(const char* str)
{
	assert(str);
	const char* beg = str;
	//01
	while (*str)
		str++;
	return str - beg;
	//02
	/*while (*str++)
		;
	return str - beg - 1;*/
}
int main()
{
	char arr[] = "abc";
	printf("%zd\n", my_strlen(arr));
	return 0;
}

3.4 const 类修饰符和指针

const 修饰的变量具有常量的性质,变得无法被修改,但该变量的本质还是变量(另见const类修饰符 文章链接: link)。放在指针中,被 const 修饰的指针也会变得无法被修改,但不同之处在于,const 修饰指针时,根据其放置在 * 的不同位置,又引出指针常量和常量指针两个概念。

//示例代码 3-11
#include <stdio.h>
int main()
{
	int a = 3;
	int b = 5;
	int* const p1 = &a;		//指针常量
	int const * p2 = &a;	//常量指针
	const int * p3 = &a;	//常量指针
	p1 = &b;				//error 表达式必须是可修改的左值
	*p1 = 7;				//OK!
	*p2 = 7;				//error 表达式必须是可修改的左值
	p2 = &b;				//OK!
	return 0;
}

如示例代码 3-11 所示,其中 int const * p2 和 const int * p3 两种写法等价,此时的 const 都位于 * 的左边。

3.4.1 指针常量

当 cosnt 位于 * 右侧,只修饰变量名 p1 时,指针 p1 指向的空间,即 p1 指向变量 a 的行为本身无法被改变,p1 只能指向变量 a ,但 p1 指向的对象 a 的值可以被修改。

3.4.2 常量指针

当 cosnt 位于 * 左侧,同时修饰指针的标志性符号 * 和变量名 p2 时,指针 p2 指向的变量 a 的值无法被修改,但 p2 指向对象 a 的行为本身可以被修改,即 p2 可以指向其它内存空间, p2 可以指向其它对象。(当 const 修饰 *p 时,*p 可以理解为变量 a 的别名,变量 a 被 const 修饰,因此 *p 指向的对象内容无法被修改。)

3.5 野指针

野指针的指针指向的位置是不可知的,也就是随机的、不正确的且没有明确限制的。在实际使用中,只有指向已分配空间且位置明确的指针才是我们需要使用的,由此可以推断出野指针的三种成因:
指针未初始化。此时创建了指针变量,但暂时还没有用指针指向的对象,因此也就是未初始化,此时可以先使用 NULL 置空,后续再指向明确的对象;当使用完指针,后续不需要再访问某个空间时,可以把指针再次置空,后续根据需要重新指向新的地址空间。详见前述空指针 NULL 。
指针越界访问。如示例代码 3-12 ,因此在实际使用时,要注意指针只能在程序已经申请的空间范围内进行进行访问,不能访问未经分配的内存空间。

//示例代码 3-12
#include <stdio.h>
int main()
{
	char arr[] = "ab";
	char* p = arr;
	printf("%c ", p[0]);	//a
	printf("%c ", p[1]);	//b
	printf("%c ", p[2]);	//error   此时 p[2] 访问的地址超出了数组 arr 的范围
	return 0;
}

指针指向的空间已释放。如示例代码 3-13,好比小明去旅馆开了一间房,第二天早上就退了宿,但第三天小明没付钱,直接找到这个旅馆当时住过的那个房间,想要进去白睡一晚,这肯定是不合法的。由示例代码可以得出,设计函数时要尽量避免返回局部变量的地址。

//示例代码 3-13
#include <stdio.h>
int* test(void)
{
	int a = 27;				//局部变量 当函数调用结束后,该临时变量 a 会被销毁,空间被收回,原来存储 27 这个数据的空间,不一定还是存储 27 
	return &a;
}
int main()					//能运行,但不代表没有错误
{
	printf("%d\n", *test()); //能正常打印出 27 。有警告【返回局部变量或临时变量的地址: a】
	return 0;
}

3.6 传值和传址

既然 C 语言中定义了指针类型,那么它有什么作用呢?先来看示例代码 3-14 ,主函数中定义了一个函数 change1() 用来改变 a 的值,但最后的结果,并未将 a 的值改为 7 ,但 change2() 却可以将主函数中 a 的值修改为 7 。

//示例代码 3-14
#include <stdio.h>
void change1(int a)
{
	a = 7;
}
void change2(int* pa)
{
	*pa = 7;
}
int main()
{
	int a = 3;
	change1(a);
	printf("%d\n", a);	//运行结果【3】
	change2(&a);
	printf("%d\n", a);	//运行结果【7】
	return 0;
}

3.6.1 传值

分析 change1() 前要先有一个概念:形参是实参的一份临时拷贝。在创建函数的栈帧空间时,编译器先申请一块空间给 main() 使用,并在 main() 的运行空间中创建一个变量 a ;在函数调用 change1() 时,重新申请一块空间给 change1() 使用,在这个空间里创建了一个新的变量 a 。chang1() 中的 a 只是将 main() 中 a 的值拷贝了过来,二者仅仅只有一个关系:它们的初值相等,都是 3 。chang1() 中对 a 重新赋值为 7 后,只是 chang1() 运行空间中的临时变量 a 变为了 7 ,但 chang1() 调用结束后,所有在 chang1() 的运行空间中创建的临时变量都会被销毁,change1() 使用的运行空间也被回收。因此,函数调用时,如果直接传递实参,无法通过被调用的函数对实参进行修改,因此 change1() 无法达到预期结果。

3.6.2 传址

chang2() 的调用过程和 change1() 类似 ,不同之处在于 change2() 函数中传递的参数是 main() 中创建的临时变量 a 的地址,鉴于传递给 chang2() 的实参 a 的数据类型,change2() 的形参需要 int* 类型的指针来接收 a 的地址。形参 pa 是指向 main() 运行空间中创建的变量 a 的指针,在 chang2() 中对 pa 解引用并赋新值 7 ,相当于直接对 main() 中的临时变量 a 进行修改,因为这个变化是直接在 main() 中存储 a 值的空间内发生的。因此 change2() 可以通过函数调用的方式改变 main() 中的变量 a 的值。示例代码的运行空间示意图如 3-15 所示。之后只要涉及到修改实参值的函数设计,都应使用指针当作形参,以便对原地址空间处的数据进行修改。

在这里插入图片描述
图 3-15 函数运行空间的简略示意图


总结

以上便是指针初阶要讲的内容,后续内容笔者会尽快更新。感谢阅读 ^ - ^

  • 17
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值