C语言 底层逻辑详细阐述指针(一)万字讲解 #指针是什么? #指针和指针类型 #指针的解引用 #野指针 #指针的运算 #指针和数组 #二级指针 #指针数组

文章目录

前言

序1:什么是内存?

序2:地址是怎么产生的?

一、指针是什么

1、指针变量的创建及其意义:

2、指针变量的大小

二、指针的解引用 

三、指针类型存在的意义

四、野指针

1、什么是野指针

2、野指针的成因

a、指针未初始化

b、指针越界访问

c、指针指向的空间释放

3、如何避免野指针的产生

a、指针要初始化

b、小心指针越界

c、指针指向空间释放及时置NULL

d、避免返回局部变量的地址

e、指针在使用前检查有限性

五、指针的运算

1、指针 加、减 整数

2、指针 - 指针 

3、指针的关系运算

六、指针和数组

八、二级指针

九、指针数组

总结


前言

全文12000+

抽丝剥茧地讲述指针,还不赶紧收藏起来!


序1:什么是内存?

在正式开始讲解指针之前,我们先来思考一下什么是内存。生活中,手机有内存、电脑也有内存……有了以上经验,内存似乎就是用来存放数据的一个空间

内存是电脑上重要的存储器,计算机中的CPU(中央处理器)在处理数据的时候,需要的数据是从内存中取得的。内存很大,有4GB\8GB\16GB等,所以如何高效地使用内存呢?计算机把内存划分为一个个小小的内存单元,其中每个内存单元的大小为1Byte[注1];由于数量之多,想要高效地访问到内存中地每个单元,于是乎就给每个内存单元进行了编号,而这些编号称为内存单元的地址

将上述语句平常化地理解就是:我们将内存当作一栋楼(宿舍楼),为了高效地利用这栋楼(宿舍楼)的空间,我们就要将这栋楼(宿舍楼)划分为一个一个房间(大小相同),而为了方便寝室的管理和快速找到一寝室,于是就给这些房间(宿舍)进行编号,于是宿舍就相当于内存中的一个个内存单元;

注1:为什么内存单元取 byte而不取 bit 呢?因为如果取 比特位,这是非常不合理的;若我创建一个变量 c : char c ;变量c 变占了1byte 即8bit的空间;若是一个内存单元为 1bit,那么光是想存放一个char 类型的数据就需要8个内存单元的空间,并且每个内存单元都有地址的话,十分浪费;而char 类型还是在内存空间中占得内存最小得类型;而从字节往上走,KB、MB、GB等都太大了;所以一个内存单元为1 byte 最合适。

序2:地址是怎么产生的?

那么你可能就会有疑问,每个内存单元的编号也就是地址,是怎么产生的呢?

地址产生的原理:依靠电脑硬件的电路产生地址中总线通电便会产生电信号,而电信号分为正脉冲和负脉冲;即地址线通电便会产生1或者0;地址信息会下达给内存,在内存中便可以找到该地址对应的数据,将数据通过地址总线传入CPU寄存器。

如果是32位电脑,就会有32条地址总线,通电时就会产生2^32 种二进制序列(产生32位二进制序列,而每一位有两种可能性,是0或者1);便可以用这2^32种二进制序列对内存单元进行编号,而一个内存单元的大小为 1Byte,那么32位的电脑内存便有2^32byte的大小,即4GB【注2】;

注2:计算机中的单位:

Bit (比特位): 一个比特位就是用来存放一个二进制位的0或者1,是计算机中的最小单位 

Byte(字节): 1 byte = 8 bit

KB (千字节Kilobytes) : 1kb = 1024 byte

MB (兆字节Megabytes) : 1 mb = 1024 kb

GB (吉字节Gigabyte) : 1 gb = 1024 mb

TB (太字节terabyte) : 1 tb = 1024 gb 

如果是64位的电脑,就会有64条地址总线,通电时就会产生2^64种二进制序列(产生64位的二进制系列,且每一位有两种可能性,是0或者1);便可以用这2^64种二进制序列对内存单元进行编号,而一个内存单元的大小为 1Byte,那么64位的电脑内存便有2^64byte的大小,也就是2^32GB;

显然,32为电脑上地址为32位的二进制序列;64位电脑上地址为64位的二进制序列;地址的本质是二进制序列,但是为了方便我们观察,呈现出来让我们看到的是十六进制的表现形式。

而变量在创建时就会根据其类型向内存申请空间,因为每个内存单元都有地址,所以变量也是有地址的;

注:内存单元的地址不需要再存放起来;这些地址是由硬件生成的,计算机是直接访问此编号对应的内存单元;除非你想要将其地址取出来放到一个指针变量中,此时才会将地址存放起来;

例如: int a = 4;

假设竖着的所有方块为内存,每一个方块为一个内存单元,由于变量a 的类型为Int 类型,int 类型在内存中所占的空间为4 byte;那么变量a 在创建的时候就会向内存申请4byte 的空间来存放变量a 的值,由于此处它初始化了,那么这 4byte 的空间中存放的数据便是4 ;变量a的地址取得是第一个内存单元的地址(低地址那一方的第一个内存单元)

一、指针是什么

从字面意思来看,指:意为指向,而针我们难免会想到时针,意为准确的意思;所以简单地从字面意思我们可以这样理解指针:准确指向一个东西;那么什么能准确地指向一个东西呢?如果想要准确地指向一个人,我们会想到说是身份证;而如若我们网购时想让包裹准确地送到(指向)我们家时,这时候就会用到地址;

概念讲述:

1、指针是内存中一个最小单元的编号,也就是地址。即内存单元的编号=地址=指针;

2、平时我们口语所说的指针为指针变量,指针变量只用来存放地址的一个变量

1、指针变量的创建及其意义:

当我们想创建一个变量时: int a = 4 ;--> “创建”就包含了这个变量的类型以及变量名 --> 有了类型才能向内存申请空间来存放变量中的数据

而若我们想把某一数据(举例将上面变量 a的地址存放起来)的地址存放到一个变量中时,同理也需要类型 + 变量名

存放地址的变量我们称之为指针变量,由于变量a 的类型是 int ,如果想要把变量a 地址存放起来以利于解引用时可以绕过a 访问到变量a ---> 为了能访问到变量a 存放在内存中的值,所以这里指针变量的类型为 int* ;

故而: int* p = &a ; -->  将变量a 的地址取出来放到指针变量 p 中

其中,int 说明p指向的对象的类型为 int 类型;* 说明 p 时指针变量 ; p 为指针变量 ;

既然 * 是用来说明此变量为指针变量的,所以在连续创建指针时,有一个需要注意的点:

int * p1, p2 , p3 ; 并不是创建了三个指针变量,实际上是 -->创建了一个指针加上两个整型变量 int* p1;   int p2 ;   int  p3;

若想要创建三个指针变量,应给这样写: int* p1,*p2 ,*p3 ;

2、指针变量的大小

指针变量的大小取决于地址的大小,而地址的大小取决于平台地址线的多少;

思考:还记得前文说地址是如何产生的吗?地址依靠电脑硬件的电路产生的,地址总线通电后会产生正脉冲和负脉冲,即1或者0;而电脑的地址线决定了电脑的位数,即32位平台下便有32条地址总线;64位平台下便有64条地址总线;

32位平台 --> 32条地址总线 --> 产生32位脉冲信号 --> 每一位存储的是1或者0 --> 二进制的每一位占1bit -->  32 bit 即 4byte 

64位平台 --> 64条地址总线 --> 产生64位脉冲信号 --> 每一位存储的是1或者0 --> 二进制的每一位占1 bit --> 64 bit 即 8 byte 

所以,在32位平台下,指针变量所占内存空间的大小为 4byte ;在64位平台下,指针变量所占的内存空间为 8 byte ; 

注:指针变量的大小只与平台有关,与其类型无关

二、指针的解引用 

思考:将地址存放到指针变量中有什么意义呢?

我们可以通过地址找到对象。但是如何通过地址找到对象呢? --> 对地址进行解引用操作,因为地址就是存放在指针变量中的,所以对指针变量进行解引用操作也是可以得到该对象;

例1:

代码如下:

#include<stdio.h>

int main()
{
	int a = 4;
	int* p = &a;
	*p = 6;
	printf("%d\n", a);

	return 0;
}

代码运行结果如下:

分析: int* p = &a; --> 取出变量a 的地址并存放到指针变量p中; *p = 6; --> 利用* 对存放在指针变量中的地址进行解引用操作找到了变量a ,并且对a 进行了赋值操作;故而 a 为6,即输出为6;

注:1、将地址存放到指针变量中的意义在于,有一天我可以通过对指针进行解引用的操作而找到它所指向的对象

2、地址是不能随意被改动的。因为编译器在运行起来的时候,地址已然被指派就不能随意更改

3、任何变量的创建均会在内存中开辟空间;

三、指针类型存在的意义

int* p = NULL; //当我们创建指针变量时不知到初始化为什么时,就可以初始化为NULL

指针变量 p的类型为 Int* 

我们先来看一个例子:

例2:

代码如下:

#include<stdio.h>

int main()
{
	char* p1 = NULL;
	short* p2 = NULL;
	int* p3 = NULL;
	long* p4 = NULL;

	printf("%zu\n", sizeof(p1));
	printf("%zu\n", sizeof(p2));
	printf("%zu\n", sizeof(p3));
	printf("%zu\n", sizeof(p4));
	return 0;
}

在x86环境下代码的运行结果如下:

分析:只要在x86环境下,不论指针为什么类型,指针变量在内存中所占的空间均为 4Byte;

在x64 环境下的运行结果:

分析:只要在x64环境下,不论指针为什么类型,指针变量在内存中所占的空间均为 8Byte;因为指针变量中存放的是地址,而地址的大小只与电脑的位数(硬件)有关。

看了以上例子,你可能就会有疑问了,指针的类型到底有什么作用?在此,我们先把指针变量的作用放出来:

1、指针类型决定了指针在进行解引用操作的时候会有几个字节的访问空间;

2、指针类型决定了指针在进行加法、减法(指针加减整数时),一次跳过多少个字节。

我们再看一个例子:

例3:

代码如下:

#include<stdio.h>

int main()
{
	int a = 0x11223344;
	char* p = (char*)&a;
	*p = 0;

	printf("%x\n", a);
	return 0;
}

代码运行结果如下:

注:进制仅仅只是数据的表现形式;变量a 的数据为十六进制数据:11223344; 变量a的数据在内存中存储的形式是二进制的补码,但是为了方便查看,表现给我们(eg.调试中的监视器上)看到的为十六进制的数据;而一个十六进制为表示为4个比特位,而8比特位为1字节,故而两个十六进制为占1字节。

这里变量p的类型为char* ,类型char 在内存中所占的空间为 1byte,故而 char* 类型的指针变量在解引用时访问空间的大小为 1byte;所以 *p = 0; 访问的是变量a存放在内存中4字节中的1字节并且将其赋值为0;由于硬件的问题,在vs编译器上显示为大端字节序(知道有这么个东西即可)即数据在内存中倒着排放;所以*p = 0; 将变量a在内存中的44赋值为0;故而输出为11223300;

注:占位符 %x 专门用来对应十六进制的数据;

 

此处调试--> 内存 --> &a  也可以发现数据在内存中是倒着存放的;

那么当指针变量 p 的类型为 Int* 时,*p = 0 ;会不会将变量a的存放在内存中4个字节的数据都更改为0呢?

例4:

代码如下:

#include<stdio.h>

int main()
{
	int a = 0x11223344;
	int * p = &a;
	*p = 0;

	printf("%x\n", a);
	return 0;
}

代码运行结果如下:

分析:将变量a存放数据的地址存放到指针变量p 中,因为指针变量p的类型为int* ,所以对p进行解引用操作就会访问4byte的空间,而 *p = 0; 也是将这四个字节的空间更改为0;

显然便可以证实指针变量的类型决定了当解引用该指针变量时会访问内存空间多少字节。

那么指针变量加、减一个整数时,它表达的意思是什么呢?

例5:

代码如下:

#include<stdio.h>

int main()
{
	int a = 0x11223344;
	int* p1 = &a;
	char* p2 = &a;

	printf("p1=%p\n", p1);
	printf("p2=%p\n", p2);
	return 0;
}

代码运行结果如下:

从以上例子中,我们可知,即使指针变量的类型不同,但存放的都是变量a的地址;

我们再看一个例子:

例6:

代码如下:

#include<stdio.h>

int main()
{
	int a = 0x11223344;
	int* p1 = &a;
	char* p2 = &a;

	printf("p1=%p\n", p1 );
	printf("p2=%p\n", p2 );
	printf("p1+1=%p\n", p1+1);
	printf("p2+1=%p\n", p2+1);
	return 0;
}

代码运行结果如下:

分析:指针变量p1 的类型为 int*,即指针变量 p1访问的内存空间的大小为 4byte,所以当 p1+1 时,指的是跳过此指针变量的大小即 4byte;而指针变量 p2 的类型为 char* ,即指针变量p2 访问内存空间的大小为 1byte,所以当 p2+1 时,指的是跳过此指针变量的大小即 1byte;

可以参考以下图解

分析:变量a 由于是 int 类型,在内存空间中所占4 byte;指针p1、p2 中存放了变量a第一个字节对的地址,由于p1和p2的类型不同,所以它们的访问权限不同它们的访问权限由其类型决定的。故而 p1+1 与 p2+1 指向的地址不同;(p1+1)的地址 在 p1 原地址的基础上增加了4 byte,而(p2+1)的地址在p2 的地址的基础上增加了1 byte;

注:内存被划分为一个个内存单元,每个内存单元都有编号,即地址;每个内存单元的大小为1 Byte

看到这里你可能又有疑问了,float 类型和 int 类型都在内存中占4 byte,那么可以将 float 与 int 混用吗?

我们先看一下一下代码:

例7-1:

代码如下:(当指针变量的类型为 float* 时)

#include<stdio.h>

int main()
{
	int a = 4;
	float* pf = &a;
	*pf = 100.0f;

	return 0;
}

调试 --> 内存 --> &a

例7-2:

代码如下:(当指针变量类型为 int* 时)

#include<stdio.h>

int main()
{
	int a = 4;
	int* pi = &a;
	*pi = 100.0f;

	return 0;
}

代码运行结果如下:

分析:整型与浮点数在内存中的存储是有差异的,故而在内存中体现不同

int* 与 float* 不能通用;一是因为int* 与float* 对内存的解读方式有所差异;二是因为站在指针变量角度来看:存放在指针变量 pf中的地址指向的是浮点型数据;而存放在指针变量 pi中的地址指向的是整型数据; 

综上,指针变量的类型是有意义的,它决定了指针在进行解引用时会有多少字节的访问空间;也决定了指针在进行加、减整数时,一次跳过多少个字节。同时即使在内存中占同样大小的类型也不能通用;

四、野指针

1、什么是野指针

顾名思义,野的指针就是野指针;

概念:野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的)

2、野指针的成因

a、指针未初始化

指针没有初始化就代表着没有明确地指向;若是一个局部变量不初始化,那么其中放的就是随机值--> 指针没有初始化,那么在指针放的也是随机的地址;但是这个随机的地址,不属于这个指针,故而没有使用该地址的权限;

b、指针越界访问

看一下此代码:

例8:

代码如下:

#include<stdio.h>

int main()
{
	int arr[10] = { 0 };
	int i = 0;
	int* p = &arr;
	for (i = 0; i < 12; i++)
	{
		*(p ++ ) = 1;
		printf("haha\n");
	}
	return 0;
}

分析:此代码中,数组arr只有10个元素,可是循环有12次,而在循环体中就会访问到数组以外的空间;当指针指向数组arr以外的空间时,此指针变量p就是野指针; 

c、指针指向的空间释放

例9:

代码如下:

int* test()
{
	int a = 10;
	return &a;
}

int main()
{
	int* p = test();
	*p = 4; //此时 p 已为野指针

	return 0;
}

分析:类型为int* 的指针变量 p 接收了 test() 函数的返回值;然而,变量a 是局部变量,作用于test() 函数内部;而局部变量进入其作用域才会创建,出了其作用域便会销毁(销毁即为将这个局部变量创建时向内存申请的空间还给操作系统);故而出了函数的作用域,变量a 的当初占用的内存空间已经不属于a的了,但是在main函数中,指针变量p中依然存放着局部变量a当初的地址,然而指针变量p还是有能力找到此地址对应的空间;然而p找到这块空间并不能去访问并使用(此空间已经不属于该程序的了) ,此时的p为野指针;

3、如何避免野指针的产生

a、指针要初始化

注:当不知道初始化什么时,可以初始化为NULL(空指针);NULL本质上就是0,但是空指也不能直接使用,初始化为空指针也仅仅只是保证了该指针变量不为野指针;

空指针不能直接使用,在使用之前需进行判断:

利用语句对该指针变量进行判断,确保它有了指向之后我才使用它:

但是用这个判断并不能用来避免野指针:

例10:

代码如下:

#include<stdio.h>

int* test()
{
	int a = 4;
	return &a;
}
int main()
{
	int* p = test();
		if (*p != NULL)
		{
			printf("%d\n", *p);
		}
	return 0;
}

代码运行结果如下:

思考:指针p指向的空间已然释放,可是为什么还可以打印出p中地址存放的数据呢?

首先if ( *p != NULL ) 仅仅只是想确认存放在指针p 中的地址是否有指向,并不能判断这个指针是不是野指针;其次是,出了作用域,局部变量a 便会被销毁(销毁即是将这个局部变量在创建时向内存申请的内存空间还给操作系统,但是这块空间仍然存在,只是不属于该程序了),此时变量a 与此空间就没有关系了,但是在main函数中,将这块空间的地址存放在了指针p中,指针p仍然可以顺着此地址找到对应的空间,此空间中还存放着之前存放的数据 4(此前提为:此空间未被其他数据覆盖;所以不代表此空空间一直存放着这一个数据).

关于数据覆盖,可以看一下一下例子:

例11-1:

代码如下:

#include<stdio.h>

int* test()
{
	int a = 4;
	return &a;
}
int main()
{
	int* p = test();
	printf("haha\n");

		if (*p != NULL)
		{
			printf("%d\n", *p);
		}
	return 0;
}

代码运行结果如下:

例11-2:

代码如下:

#include<stdio.h>

int* test()
{
	int a = 4;
	return &a;
}
int main()
{
	int* p = test();
	printf("haha\n");
	printf("hehehehe\n");

		if (*p != NULL)
		{
			printf("%d\n", *p);
		}
	return 0;
}

代码运行结果如下:

分析:函数栈帧:当调用test() 时,此栈帧中有变量a ,当函数调用结束之后,其函数栈帧的空间就空出来了;紧接着后面调用 printf() 函数,printf() 函数也会建立自己的函数栈帧,它把上一次test() 函数栈帧所占的空间给覆盖了;第一次是字符串 "haha" ,printf() 的返回值是成功打印数据分个数,在字符串后面还有一个 '\0',\n’, 但是printf() 不会打印 '\0' ,显然printf("haha\n");成功打印了5个元素;故而printf() 返回值为5;同理。第二个 printf() 成功打印了9个元素,故其返回值为9;

b、小心指针越界

c、指针指向空间释放及时置NULL

d、避免返回局部变量的地址

e、指针在使用前检查有限性

五、指针的运算

1、指针 加、减 整数

例12(利用地址来为数组元素赋值)

代码如下:

#include<stdio.h>

int main()
{
	int arr[10] = { 0 };
	int* p = arr;
	int i = 0;
	for (i - 0; i < 10; i++)
	{
		*p = 2;
		p++;
	}

	return 0;
}

代码调试结果如下:

分析:数组名为首元素地址,int* p = arr ;即将此数组首元素的地址存放到指针 p之中;*p = 2; 对指针 p 进行解引用操作:根据存放在p 中的地址找到这个地址的对象,并将此对象赋值为2;p++; 即让指针 p自增,数组元素的类型为Int 类型,而指针 p的类型为Int*, 所以p+1 就能跳过4byte 的内存空间,即跳过了一个整型的大小也就是说跳过了数组中的一个元素,而指向了下一个元素的地址;

2、指针 - 指针 

前提:这两个指针必须是指向同一空间才有意义

规则:|指针 - 指针| = 两指针间元素的个数

思考:我们从例12,或许可以感悟到存放有首元素地址的指针变量+1  (因为数组元素的类型为int 类型,而指针的类型为 int*)  便会跳过一个元素,从而指向下一个元素的地址;指针变量中存放的是地址,同理地址+1也可以实现跳过一个元素,以例12 中的数组为例,由于数组元素的类型为int 类型,故而各个数组元素的地址均为 Int* 类型。那么首元素地址+3便会跳过三个元素,指向数组中第四个元素的地址,那么第四个元素的地址- 首元素地址 = 3;这个3是什么意思呢?数组中第四个元素即为下标为3 的元素,而首元素就是下标为0 的元素,指向下标为0 的元素的地址是此元素中4byte 中的第一个字节的地址,指向下标为4 的元素的地址也是此元素中 4byte 中的第一个字节的地址, 所以 3 就代表着下标为4 的元素(不包含下标为4 的元素)到下标为0 的元素(包含下标为0 的元素),即两指针间元素的个数;

例13:

代码如下:

#include<stdio.h>

int main()
{
	int arr[10] = { 0 };
	printf("%d\n", &arr[5] - &arr[0]);

	return 0;
}

代码运行结果如下:

利用指针- 指针结果的绝对值代表着两指针间元素的个数,我们可以利用指针 - 指针模拟实现 strlen () 函数;

例14:

代码如下:

#include<stdio.h>

int my_strlen(char* str)
{
	char* start = str;//将元素的地址存放起来
	//在 '\0'之前的元素均为要算上个数的元素
	while (*str != '\0')
		str++;
	return (str - start); //随着数组元素下标的增长,元素的地址也变高;
//数组的存放是从低地址到高地址
}

int main()
{
	char ch[] = "abcdef";
	int ret = my_strlen(ch);//字符串传参的时候并不是传的其本身,
//而是字符串中首元素的地址
	printf("%d\n", ret);
	return 0;
}

代码运行结果如下:

3、指针的关系运算

思考:指针本质也是二进制的数组以代表着内存单元的编号,只不过给我们呈现的是十六进制的形式;进制仅仅只是数据的一种表现形式;既然地址也是数据,那是不是代表着地址之间也可以进行比较大小;

在例12中,我们利用数组元素的地址来进行赋值操作,同理,我们在控制循环时,其初始化、判断、调整都可以利用元素地址的形式;

例15:

代码如下:

#include<stdio.h>

int main()
{
	int arr[10] = { 1,2,3,4,5,6,6,7,8,9 };
	int* i = 0;
	int sz = sizeof(arr) / sizeof(arr[0]);
	for (i = arr; i < arr + sz ; i++)
	{
		printf("%d ", *i);
	}
	return 0;
}

代码运行结果如下:

例15中,指针 i ; i < arr + sz  ; 便用到了指针之间的关系运算,还可以写为 : i < &arr [ sz ]; 显然arr[ sz ]是数组 arr 范围之外的元素,在实际使用中也并未使用到该元素,故而不存在越界访问的问题;想要利用地址来访问元素,循环中如果会以数组外的地址作为判断的指标,就只能从低地址写向高地址;因为标准规定:允许指向数组元素的指针与指针数组的最后一个指针数组最后的那个内存位置的指针进行比较,但是不允许与指向第一个元素之前那个内存位置的指针进行比较;

如上图所示,在数组 arr范围以外的地址,在进行指针关系运算时只能用p2 指针,而不能用p1指针;

六、指针和数组

数组:一组相同类型元素的集合--> 在内存中体现为连续开辟的一块空间

指针:地址

指针变量:一个存放地址的变量

数组名就是首元素地址,我们可以通过其地址来访问数组中的元素;以上举过有关利用指针访问数组元素的例子,这里就不过多赘述了;

注:数组是数组,指针是指针需,二者要加以区别;

八、二级指针

概念:二级指针变量是用来存放一级指针的地址的

int a = 4;

int* pa = &a;

int** ppa = &pa;

pa 是一个一级指针变量,既然为变量那么也是需要向内存申请空间来存放其数据,所以pa 也有地址;将pa 的地址存放 在变量 ppa 之中,即指针变量 ppa为二级指针变量;

int* pa = &a ;--> int 代表存放在pa中地址的对象是int 类型; * 代表了变量pa 为指针变量;

同理,int** ppa = &pa ; -->  int* 代表了存放在ppa 中的对象是int* 类型,后面的* 代表了变量 ppa 为指针变量; 

调试结果如下:

九、指针数组

指针数组顾名思义就是存放指针的数组;主语为数组,而指针作为一个修饰词;

例16:(降数据的地址存放到数组中,然后再在数组中访问到该对象)

代码如下:

#include<stdio.h>
int main()
{
	int a = 10;
	int b = 20;
	int c = 30;
	int* arr[] = { &a,&b,&c };
	int sz = sizeof(arr) / sizeof(arr[0]);

	int i = 0;
	for (i = 0; i < sz; i++)
	{
		printf("%d ", *(arr[i]));
	}

	return 0;
}

代码运行结果如下:

看了以上代码,如果数组里面放的是数组的地址呢?在学习数组的时候我们知道二维数组可以看作是一个一维数组,只不过在这个一维数组中的元素也是一个数组;基于此,我们可以利用指针数组来模拟二维数组;

例17-1:(利用指针数组来模拟二维数组

代码如下:

#include<stdio.h>
int main()
{
	int arr1[4] = { 1,1,1,1 };
	int arr2[4] = { 2,2,2,2 };
	int arr3[4] = { 3,3,3,3 };
	int* parr[] = { arr1,arr2,arr3 };

	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 4; j++)
		{
			printf("%d ", *(*(parr + i) + j)); //将数组名当作地址
		}
		//打印完一行就换行
		printf("\n");
	}
	return 0;
}

例子17-2:

#include<stdio.h>
int main()
{
	int arr1[4] = { 1,1,1,1 };
	int arr2[4] = { 2,2,2,2 };
	int arr3[4] = { 3,3,3,3 };
	int* parr[] = { arr1,arr2,arr3 };
	int i = 0;
	int j = 0;
	for (i = 0; i < 3; i++)
	{
		for (j = 0; j < 4; j++)
		{
			printf("%d ",parr[i][j]); //利用数组下标进行访问
		}
		//打印完一行就换行
		printf("\n");
	}
	return 0;
}

两个例子的代码运行结果如下:

分析:数组 parr中元素的类型为 int* ,故数组parr的类型为 int* ; 数组即可从下标的视角来访问数组中的元素;若将数组名当作首元素的地址,也可以从访问地址的视角来访问数组中的元素;所以,有两种方法来访问数组中的元素;

一是,利用数组名为首元素地址的特点;*(*(parr + i) + j) ; -->  parr 为parr 数组的首元素 arr1 的地址,而arr1 代表着arr1 中首元素的地址;对(parr + i)解引用便可以找到数组parr 中的元素,而数组parr 中的元素又为数组的首元素地址,*(parr + i) + j 意为访问parr中的数组中的元素的地址,所以*(*(parr + i) + j) 便就访问到了数组 parr中存放的数组的元素;

二是,利用数组的下标进行访问,parr[ i ] 就是数组parr中的元素,因数组parr中的元素也是数组;例: parr[ 1 ] = arr ; 就可以将 parr [ i ] 也看作数组名,那么arr[ j ] 就可以写为 parr [ i ][ j ] ;


总结

1、内存是电脑上重要的存储器,计算机中的CPU(中央处理器)在处理数据的时候,需要的数据是从内存中取得的。

2、每个内存单元的编号也就是地址,是依靠电脑硬件的电路产生内存单元的地址不需要再存放起来,计算机是直接访问此编号对应的内存单元;

3、指针是内存中一个最小单元的编号。即内存单元的编号=地址=指针平时我们口语所说的指针为指针变量,指针变量只用来一个用来存放地址的变量

4、若想要创建三个指针变量,应给这样写: int* p1,*p2 ,*p3 ;

5、指针变量的大小取决于地址的大小,而地址的大小取决于平台地址线的多少;指针变量的大小只与平台有关,与其类型无关;32位平台--> 4byte ; 64位平台 --> 8byte;

6、地址是不能随意被改动的。因为编译器在运行起来的时候,地址已然被指派就不能随意更改。任何变量的创建均会在内存中开辟空间;

7、指针类型决定了指针在进行解引用操作的时候会有几个字节的访问空间;指针类型决定了指针在进行加法、减法(指针加减整数时),一次跳过多少个字节。

8、野指针就是指针指向的位置是不可知的(随机的、不正确的、没有明确限制的);

9、野指针的成因:a、指针未初始化;b、指针越界访问 ;c、指针指向的空间释放; 

10、避免野指针的产生:a、指针要初始化; b、小心指针越界  ; c 、指针指向的空间释放时要及时置为NULL; d、避免返回局部变量的地址 ; e 、指针在使用前检其有限性

11、指针 - 指针 :

前提:这两个指针必须是指向同一空间才有意义

规则:|指针 - 指针| = 两指针间元素的个数

12、二级指针变量是用来存放一级指针的地址的

13、指针数组顾名思义就是存放指针的数组;主语为数组,而指针作为一个修饰词;

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/802183.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【HarmonyOS】关于鸿蒙消息推送的心得体会 (一)

【HarmonyOS】关于鸿蒙消息推送的心得体会&#xff08;一&#xff09; 前言 这几天调研了鸿蒙消息推送的实现方式&#xff0c;形成了开发设计方案&#xff0c;颇有体会&#xff0c;与各位分享。 虽然没做之前觉得很简单的小功能&#xff0c;貌似只需要和华为服务器通信&…

玩转HarmonyOS NEXT之AppStorage应用全局UI状态存储

概述 AppStorage是应用全局的UI状态存储&#xff0c;是和应用的进程绑定的&#xff0c;由UI框架在应用程序启动时创建&#xff0c;为应用程序UI状态属性提供中央存储。 AppStorage是在应用启动的时候会被创建的单例。它的目的是为了提供应用状态数据的中心存储&#xff0c;这…

【HarmonyOS学习】Calendar Kit日历管理

简介 Calendar Kit提供日历与日程管理能力&#xff0c;包括日历的获取和日程的创建能力。 Calendar Kit为用户提供了一系列接口来获取日历账户&#xff0c;并使用特定的接口向日历账户中写入日程。 如果写入的日程带有提醒时间则系统会在时间到达时向用户发送提醒。 约束点…

Linux编程(通信协议---udp)

UDP&#xff08;用户数据报协议&#xff09;是一种无连接的网络协议&#xff0c;主要用于快速传输数据。以下是UDP协议的一些主要特点&#xff1a; 1. **无连接**&#xff1a;UDP是无连接的协议&#xff0c;这意味着在数据传输之前不需要建立连接。每个UDP数据包都是独立的&am…

remote: ERROR: commit b81ea84: missing Change-Id in message footer

首次拉取代码后,在本地已经编辑添加了代码并且想要提交到远端仓库 git add . git commit 当commit之后想要pull的时候报错了 git pull 执行到git pull 时出现这个问题,这是由于Change-Id没了,提示: ! [remote rejected] HEAD -> refs/for/master (commit b81ea84: mis…

git回退分支版本git reset --hard HEAD

git回退分支版本git reset --hard HEAD git reset --hard HEAD 上面命令清除本地所有修改&#xff0c;与下面相似&#xff1a; git reset --hard origin/master 等同于&#xff1a; git reset --hard HEAD~0 说明&#xff1a; HEAD 当前版本 HEAD^ 上一个版本 HEAD^^ 上上…

JVM---对象是否存活及被引用的状态

1.如何判断对象是否存活 1.1 引用计数算法 概念&#xff1a;在对象头部增加一个引用计数器,每当有一个地方引用它时&#xff0c;计数器值就加一&#xff1b;当引用失效时&#xff0c;计数器值就减一&#xff1b;任何时刻计数器为零的对象就是不可能再被使用的。 优点&#xff1…

LabVIEW学习-LabVIEW储存Excel表格

上述实现了将格式化的时间和正弦波的频率振幅相位以及正弦波数据输入到excel表格中。 下面介绍其中使用到的函数&#xff1a; 1. 所在位置&#xff0c;函数选板->定时->获取日期/时间(秒) 2. 将获取的时间进行格式化处理&#xff0c;输出格式化的日期/时间字符串。 函…

AI赋能基础设施巡检,技术革新助力水泥建筑缺陷检测分析,基于YOLOv8模型开发构建水泥建筑场景下裂缝缺陷分割检测识别系统

在现代化城市建设的宏伟蓝图中&#xff0c;公路、隧道、桥梁、大坝等水泥类基础设施如同城市的血脉&#xff0c;支撑着社会的正常运转与经济的蓬勃发展。然而&#xff0c;时间的侵蚀与自然的考验使得这些建筑不可避免地面临老化与损坏的问题&#xff0c;裂缝作为其中最为常见的…

AV1 编码标准环路滤波和后处理技术概述

AV1 环路滤波 去块滤波器 在视频编码的环路滤波管道中&#xff0c;去块滤波器&#xff08;deblocking filter&#xff09;用于减少量化引起的变换块边界处的块状伪影。 总结&#xff1a; 去块滤波器的应用&#xff1a; 对于亮度&#xff08;luma&#xff09;色度分量&#xff…

minIO集成springboot

问题 minIO与spring集成。 步骤 创建桶 创建key 找到创建账号页面&#xff0c;如下图&#xff1a; 点击创建&#xff0c;如下图&#xff1a; 设置如下权限&#xff1a; {"Version": "2012-10-17","Statement": [{"Effect": &q…

华为OD算法题汇总

60、计算网络信号 题目 网络信号经过传递会逐层衰减&#xff0c;且遇到阻隔物无法直接穿透&#xff0c;在此情况下需要计算某个位置的网络信号值。注意:网络信号可以绕过阻隔物 array[m][n]&#xff0c;二维数组代表网格地图 array[i][j]0&#xff0c;代表i行j列是空旷位置 a…

如何在所有docker命令前加上一个sudo

如果当前登录用户不是root不用&#xff0c;使用docker命令的时候&#xff0c;需要在前面加上一个sudo 提升权限。 但是每次都加&#xff0c;就感觉特别的麻烦&#xff0c;如何简化呢&#xff1f; 解决办法 打开你的shell配置文件&#xff08;例如&#xff0c;如果你使用bash&am…

Spring Cloud Eureka快读入门Demo

1.什么是Eureka&#xff1f; Eureka 由 Netflix 开发&#xff0c;是一种基于REST&#xff08;Representational State Transfer&#xff09;的服务&#xff0c;用于定位服务&#xff08;服务注册与发现&#xff09;&#xff0c;以实现中间层服务的负载均衡和故障转移&#xff…

C语言 | Leetcode C语言题解之第239题滑动窗口最大值

题目&#xff1a; 题解&#xff1a; int* maxSlidingWindow(int* nums, int numsSize, int k, int* returnSize) {int prefixMax[numsSize], suffixMax[numsSize];for (int i 0; i < numsSize; i) {if (i % k 0) {prefixMax[i] nums[i];} else {prefixMax[i] fmax(pref…

甄选范文“论软件维护方法及其应用”软考高级论文,系统架构设计师论文

论文真题 软件维护是指在软件交付使用后,直至软件被淘汰的整个时间范围内,为了改正错误或满足 新的需求而修改软件的活动。在软件系统运行过程中,软件需要维护的原因是多种多样的, 根据维护的原因不同,可以将软件维护分为改正性维护、适应性维护、完善性维护和预防性 维护…

Mindspore框架CycleGAN模型实现图像风格迁移|(三)损失函数计算

Mindspore框架&#xff1a;CycleGAN模型实现图像风格迁移算法 Mindspore框架CycleGAN模型实现图像风格迁移|&#xff08;一&#xff09;CycleGAN神经网络模型构建 Mindspore框架CycleGAN模型实现图像风格迁移|&#xff08;二&#xff09;实例数据集&#xff08;苹果2橘子&…

JAVA 异步编程(异步,线程,线程池)一

目录 1.概念 1.1 线程和进程的区别 1.2 线程的五种状态 1.3 单线程,多线程,线程池 1.4 异步与多线程的概念 2. 实现异步的方式 2.1 方式1 裸线程&#xff08;Thread&#xff09; 2.1 方式2 线程池&#xff08;Executor&#xff09; 2.1.1 源码分析 2.1.2 线程池创建…

新的“SCALE”软件允许为 AMD GPU 原生编译 CUDA 应用程序

虽然已经有各种努力&#xff0c;如HIPIFY来帮助将CUDA源代码转换为AMD GPU的可移植C代码&#xff0c;然后是之前AMD资助的ZLUDA&#xff0c;允许CUDA二进制文件通过CUDA库的直接替代品在AMD GPU上运行&#xff0c;但有一个新的竞争者&#xff1a;SCALE。SCALE现在作为GPGPU工具…

超算网络体系架构-资源层-平台层-服务层-应用层

目录 超算网络体系架构 我国超算基础设施 超算互联网相关标准研制方面 技术架构 资源层 基础资源 芯片多样 体系异构 高效存储 高速互连 资源池化 可隔离 可计量 互联网络 高带宽 低时延 高安全 平台层 算力接入 资源管理 算力调度 用户管理 交易管理 模…