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

news2024/11/15 12:32:29

文章目录

前言

序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、指针数组顾名思义就是存放指针的数组;主语为数组,而指针作为一个修饰词;

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Unity客户端接入原生Google支付

Unity客户端接入原生Google支付 1. Google后台配置2. 开始接入Java部分C#部分Lua部分 3. 导出工程打包测试参考踩坑注意 1. Google后台配置 找到内部测试&#xff08;这个测试轨道过审最快&#xff09;&#xff0c;打包上传&#xff0c;这个包不需要接入支付&#xff0c;如果已…

机器人开源调度系统OpenTcs6-架构运行分析

系统启动 启动 Kernel&#xff1a;加载核心应用&#xff0c;初始化系统配置和状态。 启动 Plant Overview&#xff1a;加载图形用户界面&#xff0c;初始化模型和用户界面。 模型导入和配置 在 Plant Overview 中导入或创建工厂布局模型。 配置路径、位置和车辆信息。 车辆连…

用DrissionPage过某里滑块分析

最近我又在找工作了&#xff0c;悲哀啊~&#xff0c;面试官给了一道题&#xff0c;要求如下&#xff1a; 爬虫机试&#xff1a;https://detail.1688.com/offer/643272204627.html 过该链接的滑动验证码&#xff0c;拿到正确的商品信息页html&#xff0c;提取出商品维度的信息&a…

排序一次讲清(从冒泡到基数)

文章目录 冒泡原理代码pythonc 选择原理代码pythonc 插入原理代码pythonc 希尔原理代码pythonc 快排原理代码pythonc 归并原理代码pythonc 堆原理代码pythonc 计数原理代码pythonc 桶原理代码pythonc 基数原理代码pythonc 【待更新】 冒泡 原理 如果我们想要让数组从左至右从…

海豚调度器(DolphinScheduler)集群搭建详细笔记

海豚调度器集群搭建笔记 1.DolphinScheduler Cluster部署1.1 集群部署规划1.2 集群准备工作1.3 初始化数据库1.4 修改安装环境配置1.5 安装DolphinScheduler1.6 启停命令1.7 登录 DolphinScheduler UI 1.DolphinScheduler Cluster部署 分布式去中心化易扩展的工作流任务调度系…

【最强八股文 -- 计算机网络】TCP 四次挥手的过程及原因

第一次挥手&#xff1a;FIN 报文第二次挥手&#xff1a;ACK 报文第三次挥手&#xff1a;FIN 报文第四次挥手&#xff1a;ACK 报文 为什么需要四次挥手&#xff1f; 为什么需要 TIME_WAIT 状态&#xff1f; TIME_WAIT 的时间为什么是 2MSL&#xff1f;

springboot服务如何执行sql脚本文件

当sql脚本文件包含不同数据库实例sql时&#xff0c;遍历读取sql文件再插入时&#xff0c;由于是不同的数据库实例这种方式就不行了&#xff0c;这时就需要程序直接执行sql脚本。 springboot执行sql脚本 /*** 执行sql脚本* throws SQLException*/ private void executeSqlScri…

go-zero框架入门

go-zero框架环境的安装 goctl 若想用go-zero框架&#xff0c;还需要一些前置条件&#xff1a; 安装goctl go install github.com/zeromicro/go-zero/tools/goctllatest可以使用 goctl 命令查看是否安装成功 成功后安装protoc goctl env check --install --verbose --force…

重生奇迹MU 三代翅膀行情

在重生奇迹MU游戏中&#xff0c;达到400级以上的玩家都知道&#xff0c;重生奇迹大陆拍卖行里最值钱的物品是翅膀。翅膀可以分为一代、二代和三代翅膀&#xff0c;而其中价格最高的则是三代翅膀。有时候&#xff0c;三代翅膀的售价非常之高&#xff0c;甚至有市无价。这是因为三…

[论文笔记] CT数据配比方法论——1、Motivation

我正在写这方面的论文,感兴趣的可以和我一起讨论!!!!!! Motivation 1、探测原有模型的配比: 配比 与 ppl, loss, bpw, benchmark等指标 之间的关系。 2、效果稳定的配比:配比 与 模型效果 之间的规律。 Experiments 1、主语言(什么语言作为主语言,几种主语言?…

PyTorch 深度学习实践-逻辑斯蒂回归

视频指路 参考博客笔记 参考笔记二 用来分类的模型 说明&#xff1a;1、 逻辑斯蒂回归和线性模型的明显区别是在线性模型的后面&#xff0c;添加了激活函数(非线性变换) ​ 2、分布的差异&#xff1a;KL散度&#xff0c;cross-entropy交叉熵 现在损失函数衡量不是距离而是分布…

Jmeter性能测试(九)

一、Jmeter性能测试需要特别注意的地方 1、参数化 2、请求参数 3、BeanShell 预处理程序更新jmeter请求参数 4、接口中不可重复的随机数处理 5、线程组设置 6、总结 二、参数化 1、参数化配置,多个参数用英文逗号隔开 2、wallet参数化文件,不要写表头,多个参数用英文逗号…

【YOLOv8改进[Conv]】KAN系列 |使用KACNConv改进C2f + 含全部代码和详细修改方式 + 手撕结构图

本文将进行在YOLOv8中使用KACNConv改进C2f 的实践,助力YOLOv8目标检测效果,文中含全部代码、详细修改方式以及手撕结构图。助您轻松理解改进的方法。训练速度会慢一些,要有心理准备哈! 改进前和改进后的参数对比: 目录

《基于 Kafka + Quartz 实现时限质控方案》

&#x1f4e2; 大家好&#xff0c;我是 【战神刘玉栋】&#xff0c;有10多年的研发经验&#xff0c;致力于前后端技术栈的知识沉淀和传播。 &#x1f497; &#x1f33b; CSDN入驻不久&#xff0c;希望大家多多支持&#xff0c;后续会继续提升文章质量&#xff0c;绝不滥竽充数…

玳数科技集成 Flink CDC 3.0 的实践

摘要&#xff1a;本文投稿自玳数科技工程师杨槐老师&#xff0c;介绍了 Flink CDC 3.0 与 ChunJun 框架在玳数科技的集成实践。主要分为以下六个内容&#xff1a; 背景技术选型架构设计挑战与解决方案上线效果未来规划 1. 背景 玳数科技对内外部用户提供了一站式的数据开发治理…

【BUG】已解决:error: subprocess-exited-with-error

已解决&#xff1a;error: subprocess-exited-with-error 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0c;就职于医疗科技公司&#xff0c;热衷分享知识&#xff0c;武汉城市开发者社区主…

【Vue】深入解析 Vue 生命周期:从创建到销毁的完整流程

文章目录 一、Vue 生命周期概述二、创建阶段1. beforeCreate 钩子2. created 钩子 三、挂载阶段1. beforeMount 钩子2. mounted 钩子 四、更新阶段1. beforeUpdate 钩子2. updated 钩子 五、销毁阶段1. beforeDestroy 钩子2. destroyed 钩子 六、Vue 3 的生命周期钩子变化七、生…

PHP pwn 学习 (2)

文章目录 A. 逆向分析A.1 基本数据获取A.2 函数逆向zif_addHackerzif_removeHackerzif_displayHackerzif_editHacker A.3 PHP 内存分配 A.4 漏洞挖掘B. 漏洞利用B.1 PHP调试B.2 exp 上一篇blog中&#xff0c;我们学习了一些PHP extension for C的基本内容&#xff0c;下面结合一…

软件著作权申请教程(超详细)(2024新版)软著申请

目录 一、注册账号与实名登记 二、材料准备 三、申请步骤 1.办理身份 2.软件申请信息 3.软件开发信息 4.软件功能与特点 5.填报完成 一、注册账号与实名登记 首先我们需要在官网里面注册一个账号&#xff0c;并且完成实名认证&#xff0c;一般是注册【个人】的身份。中…

STM32智能城市交通管理系统教程

目录 引言环境准备智能城市交通管理系统基础代码实现&#xff1a;实现智能城市交通管理系统 4.1 数据采集模块 4.2 数据处理与控制模块 4.3 通信与网络系统实现 4.4 用户界面与数据可视化应用场景&#xff1a;城市交通管理与优化问题解决方案与优化收尾与总结 1. 引言 智能城…