当前位置: 首页 > news >正文

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

相关文章:

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

文章目录 前言 序1&#xff1a;什么是内存&#xff1f; 序2&#xff1a;地址是怎么产生的&#xff1f; 一、指针是什么 1、指针变量的创建及其意义&#xff1a; 2、指针变量的大小 二、指针的解引用 三、指针类型存在的意义 四、野指针 1、什么是野指针 2、野指针的成因 a、指…...

【人工智能大模型】文心一言介绍以及基本使用指令

目录 一、产品背景与技术基础 二、主要功能与特点 基本用法 指令的使用 注意事项 文心一言&#xff08;ERNIE Bot&#xff09;是百度基于其文心大模型技术推出的生成式AI产品。以下是对文心一言的详细介绍&#xff1a; 一、产品背景与技术基础 技术背景&#xff1a;百度…...

AI绘画入门实践|Midjourney 的模型版本

模型分类 Midjourney 的模型主要分为2大类&#xff1a; 默认模型&#xff1a;目前包括&#xff1a;V1, V2, V3, V4, V5.0, V5.1, V5.2, V6 NIJI模型&#xff1a;目前包括&#xff1a;NIJI V4, NIJI V5, NIJI V6 模型切换 你在服务器输入框中输入 /settings&#xff1a; 回车后…...

Web3时代的教育技术革新:智能合约在学习管理中的应用

随着区块链技术的发展和普及&#xff0c;Web3时代正在为教育技术带来前所未有的革新和机遇。智能合约作为区块链技术的核心应用之一&#xff0c;不仅在金融和供应链管理等领域展示了其巨大的潜力&#xff0c;也在教育领域中逐渐探索和应用。本文将探讨智能合约在学习管理中的具…...

云计算实训室的核心功能有哪些?

在当今数字化转型浪潮中&#xff0c;云计算技术作为推动行业变革的关键力量&#xff0c;其重要性不言而喻。唯众&#xff0c;作为教育实训解决方案的领先者&#xff0c;深刻洞察到市场对云计算技能人才的迫切需求&#xff0c;精心打造了云计算实训室。这一实训平台不仅集成了先…...

芯科科技第五届物联网开发者大会走进世界各地,巡回开启注册

中国&#xff0c;北京 – 2024年7月18日 – 致力于以安全、智能无线连接技术&#xff0c;建立更互联世界的全球领导厂商Silicon Labs&#xff08;亦称“芯科科技”&#xff0c;NASDAQ&#xff1a;SLAB&#xff09;今日宣布&#xff0c;其2024年Works With开发者大会现正开放注册…...

Python创建Excel表和读取Excel表的基础操作

下载openpyxl第三方库 winr打开命令行输入cmd 这个如果不行可以试试其他方法&#xff0c;在运行Python代码的软件里也有直接下载的地方&#xff0c;可以上网搜索 创建Excel表 示例代码&#xff1a;最后要记得保存&#xff0c;可以加一句提示语句。 import openpyxl lst[100,…...

JVM(day2)经典垃圾收集器

经典垃圾收集器 Serial收集 使用一个处理器或一条收集线程去完成垃圾收集工作&#xff0c;更重要的是强调在它进行垃圾收集时&#xff0c;必须暂停其他所有工作线程&#xff0c;直到它收集结束。 ParNew收集器 ParNew 收集器除了支持多线程并行收集之外&#xff0c;其他与 …...

华为od机试真题 — 分披萨(Python)

题目描述 “吃货”和“馋嘴”两人到披萨店点了一份铁盘(圆形)披萨&#xff0c;并嘱咐店员将披萨按放射状切成大小相同的偶数个小块。 但是粗心服务员将披萨切成了每块大小都完全不同奇数块&#xff0c;且肉眼能分辨出大小。 由于两人都想吃到最多的披萨&#xff0c;他们商量…...

ubuntu22.04 安装boost

下载boost压缩包&#xff0c;我这里上传了一份1_81_0版本tar -xzvf boost_1_81_0.tar.gzcd boost_1_81_0/sudo apt install build-essential g autotools-dev libicu-dev libbz2-dev -ysudo ./bootstrap.sh --prefix/usr/./b2sudo ./b2 install 上述7步完成后&#xff0c;相关…...

基于JAVA+SpringBoot+uniapp的心理小程序(小程序版本)

✌全网粉丝20W,csdn特邀作者、博客专家、CSDN新星计划导师、java领域优质创作者,博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ 技术范围&#xff1a;SpringBoot、Vue、SSM、HLMT、Jsp、SpringCloud、Layui、Echarts图表、Nodejs、爬…...

C语言 ——— 输入两个正整数,求出最小公倍数

目录 何为最小公倍数 题目要求 代码实现 方法一&#xff1a;暴力求解法&#xff08;不推荐&#xff09; 方法二&#xff1a;递乘试摸法&#xff08;推荐&#xff09; 何为最小公倍数 最小公倍数是指两个或者多个正整数&#xff08;除了0以外&#xff09;的最小的公共倍数…...

Langchain 对pdf,word,txt等不同文件的加载解析

项目中遇到各种数据资源想要加载近langchain构建本地知识ai系统&#xff0c;怎么加载对应的文件格式呢&#xff0c;一起研究下 引入Langchain from langchain.document_loaders import UnstructuredWordDocumentLoader,PyPDFium2Loader,DirectoryLoader,PyPDFLoader,TextLoad…...

BL201分布式I/O耦合器连接Profinet网络

钡铼技术的BL201分布式I/O耦合器是一个用于Profinet网络的设备&#xff0c;用于连接远程输入/输出&#xff08;I/O&#xff09;设备到控制系统&#xff0c;如可编程逻辑控制器&#xff08;PLC&#xff09;&#xff0c;能够实现分布式的I/O连接和通信。 它支持标准Profinet IO …...

Pycharm 报错 Environment location directory is not empty 解

删除项目中ven文件夹&#xff08;已存在的&#xff09;&#xff0c;然后再添加新的ven虚拟环境就可以了...

【Android】Intent基础用法及作用

文章目录 使用Intent在活动中穿梭组成显式Intent隐式Intent显式与隐式区别作用 活动间传递数据向下一个活动传递数据返回数据给上一个活动 使用Intent在活动中穿梭 Intent&#xff08;意图&#xff09;是一种重要的消息传递对象&#xff0c;用于在不同组件&#xff08;如活动&…...

Web开发:ASP.NET CORE的后端小结(基础)

1.后端重定向到指定路由 public IActionResult Index(){return RedirectToAction("Index", "Main");//重定向>Main/Index} 【备注】如果在MainController的Index方法中return View();本质是 return View("Index")&#xff0c;返回和方法同名的…...

侧开知识点合集2

一、try .... catch.. AccessViolationException异常触发后&#xff0c;下列程序的输出结果为 static void Main(string[] args) { try { throw new AccessViolationException(); Console.WriteLine("error1"); } catch (Exception e) { Console.WriteLi…...

ARM/Linux嵌入式面经(十六):蔚来嵌入式一二三面面经

文章目录 static作用,局部static和全局static区别TCP三次握手Linux虚拟内存指针引用区别C++内存分区new/delete和malloc/free区别职业规划为什么选择蔚来介绍一下项目然后问我有没有内核级别开发经验,我说没有什么情况进入内核态一、主动式二、被动式三、其他方式注意事项示例…...

Apache BookKeeper 一致性协议解析

导语 Apache Pulsar 是一个多租户、高性能的服务间消息传输解决方案&#xff0c;支持多租户、低延时、读写分离、跨地域复制&#xff08;GEO replication&#xff09;、快速扩容、灵活容错等特性。Pulsar 存储层依托于 BookKeeper 组件&#xff0c;所以本文简单探讨一下 BookK…...

Solana的账户模型

Solana的账户模型与其他区块链平台&#xff08;如以太坊&#xff09;有所不同&#xff0c;其设计旨在提高性能和扩展性。以下是Solana账户模型的主要特点和工作原理&#xff1a; Solana账户模型概述 账户类型&#xff1a; 普通账户&#xff08;User Accounts&#xff09;&…...

iPython与Matplotlib:数据可视化的秘籍

iPython与Matplotlib&#xff1a;数据可视化的秘籍 前言 欢迎来到"iPython与Matplotlib&#xff1a;数据可视化的秘籍"教程&#xff01;无论你是数据可视化新手还是希望提升技能的专业人士&#xff0c;这里都是你开始的地方。让我们开始这段数据可视化之旅吧&#…...

做一只勤劳的小蜜蜂

机缘 成为创作者的初心&#xff0c;对我而言&#xff0c;是一个融合了个人兴趣、职业成长以及对知识传播热爱的复杂而纯粹的情感交织。回顾这段旅程的起点&#xff0c;几个核心驱动力始终引领着我前行&#xff1a; 1、记录与反思&#xff1a;在职业生涯的早期&#xff0c;我遇…...

如何处理 PostgreSQL 中死锁的情况?

&#x1f345;关注博主&#x1f397;️ 带你畅游技术世界&#xff0c;不错过每一次成长机会&#xff01;&#x1f4da;领书&#xff1a;PostgreSQL 入门到精通.pdf 文章目录 如何处理 PostgreSQL 中死锁的情况&#xff1f;一、认识死锁二、死锁的症状三、死锁的检测四、预防死锁…...

新版本 idea 创建不了 spring boot 2 【没有jkd8选项】

创建新项目 将地址换成如下 https://start.aliyun.com/...

linux系统和windows系统如何同步时间,服务器时间变动怎么同步

一、Linux系统时间同步 1. 使用NTP&#xff08;网络时间协议&#xff09; NTP是最常用的Linux系统时间同步方式。NTP通过连接到外部时间服务器&#xff08;如原子钟或GPS接收器&#xff09;来获取高精度的时间信息&#xff0c;并校准本地系统时间。 步骤&#xff1a; 安装N…...

Mac M1安装配置Hadoop+Flink SQL环境

Flink 1.18.1 Hadoop 3.4.0 一、准备工作 系统&#xff1a;Mac M1 (MacOS Sonoma 14.3.1) JDK&#xff1a;jdk1.8.0_381 &#xff08;注意&#xff1a;尽量一定要用JDK8&#xff0c;少用高版本&#xff09; Scala&#xff1a;2.12 JDK安装在本机的/opt/jdk1.8.0_381.jdk/C…...

【所谓生活】马太效应

简介 马太效应又称马太定律或两级分化现象。该效应描述的是在社会生活中&#xff0c;强者因为优势而获得更多机会&#xff0c;而弱者因劣势而失去机会&#xff0c;最终导致强者愈强、弱者愈弱的现象。这一概念最早由美国社会学家罗伯特莫顿于1968年提出&#xff0c;其名字来源…...

品牌进行电商数据采集的流程

品牌在进行数据分析与渠道管控时&#xff0c;均离不开电商数据的有力支撑&#xff0c;故而数据采集的质量举足轻重。电商数据采集首先要确保准确率&#xff0c;其次要保障覆盖率&#xff0c;即页面上呈现的商品信息必须采集完整&#xff0c;否则难以得出精确的数据分析成果&…...

面试问题:React基本概念,和所遇到的CPU和IO问题

在官方文档里面可以看见React基本设计概念&#xff0c;React是用 JavaScrip构建快速响应的大型Web应用程序的首选方式&#xff0c;但是快速响应用一定的是依赖&#xff0c;CPU的性能和IO的约束。 首先CPU性能原因&#xff1a;大部分浏览器的刷新频率为60HZ&#xff0c;及16.6ms…...

phpwind做的网站/广告投放

手机CPU天梯图3月版在上一期的“手机CPU怎么看好坏 手机CPU天梯图2019年2月最新版”中&#xff0c;我们为大家聊了如何去判断一看处理器性能&#xff0c;大致最核心的主要是架构、主频、核心、基带、功耗等方面&#xff0c;今天就不再介绍一些参数部分了。以下是手机CPU天梯图2…...

大学生做家教比较好的网站/最全bt搜索引擎入口

一、定义 桥接模式&#xff08;Bridge Pattern&#xff09;&#xff0c;将抽象部分与它的实现部分分离&#xff0c;使的抽象和实现都可以独立地变化。 主要解决&#xff1a;在多维可能会变化的情况下&#xff0c;用继承会造成类爆炸问题&#xff0c;扩展起来不灵活。 何时使用&…...

网站代建设费用吗/日本域名注册

1 //遍历服务器指定文件夹下的所有文件2 string path "uploads/Image/";3 string serverPath Server.MapPath(path);4 5 //创建临时文件夹6 string tempName DateTime.Now.ToString("yyyyMMddH…...

一个专门做字画的网站/南宁seo推广优化

一. 常用命令 1. 编辑相关 ①. awk NF&#xff1a;字段总数NR&#xff1a;第几行数据FS&#xff1a;分隔字符 ②. sed -n-i 直接修改4a&#xff1a;在第四行后添加4i&#xff1a;在第四行前插入1,5c sting&#xff1a;用sting替换1到5行的内容s/要被替换的字符串/新的字符串…...

北京出名做网站的公司/东莞网站制作

1.概率图模型简介&#xff1a; 概率图模型是图灵奖获得者Pearl开发出来的用图来表示变量概率依赖关系的理论。概率图模型理论分为概率图模型表示理论&#xff0c;概率图模型推理理论和概率图模型学习理论。 概率图理论共分为三个部分&#xff0c;分别为概率图模型表示理论&…...

iis7安装wordpress/视频号怎么付费推广

软件工程-软件维护/系统维护 系统可维护性 评价指标 可理解性可测试性可修改性 系统维护类型 改正性维护适应性维护预防性维护完善性维护 tip&#xff1a;完善性维护占半壁江山 系统文档 开发文档&#xff1a;技术员编写&#xff0c;与开发相关产品文档&#xff1a;与用…...