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

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

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

相关文章

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

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

学习分布式事务遇到的小bug

一、介绍Seata 在处理分布式事务时我用到是Seata&#xff0c;Seata的事务管理中有三个重要的角色&#xff1a; TC (Transaction Coordinator) - 事务协调者&#xff1a;维护全局和分支事务的状态&#xff0c;协调全局事务提交或回滚。 TM (Transaction Manager) - 事务管理器…

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;下面结合一…

Open3D Ransac算法拟合点云球面

目录 一、概述 二、代码实现 2.1关键函数 2.2完整代码 三、实现效果 3.1原始点云 3.2拟合后点云 前期试读&#xff0c;后续会将博客加入该专栏&#xff0c;欢迎订阅 Open3D点云算法与点云深度学习案例汇总&#xff08;长期更新&#xff09;-CSDN博客 一、概述 RANSAC&a…

【Chatgpt大语言模型医学领域中如何应用】

随着人工智能技术 AI 的不断发展和应用&#xff0c;ChatGPT 作为一种强大的自然语言处理技术&#xff0c;无论是 自然语言处理、对话系统、机器翻译、内容生成、图像生成&#xff0c;还是语音识别、计算机视觉等方面&#xff0c;ChatGPT 都有着广泛的应用前景。特别在临床医学领…

pycharm如何debug for循环里面的错误值

一般debug时&#xff0c;在for循环里面的话&#xff0c;需要自己一步一步点。如果循环几百次那种就比较麻烦。此时可以采用try except的方式来解决 例子如下 #ptyhon debug for循环的代码 num[1,2,3,s,4] ans0 for i in num:try:ansiexcept:print(错误) print(ans) 结果如下&a…

HTML+echarts.js实现的炫酷金色风格可视化组件

模板下载地址&#xff1a; 炫酷金色风格可视化组件 (bootstrapmb.com)https://www.bootstrapmb.com/item/14888 一款炫酷金色风格可视化组件&#xff0c;统计图表使用Echarts.js&#xff0c;整体风格采用金黄色看起来很大气&#xff0c;设计是通用型的&#xff0c;可以用作任…

自动驾驶系列—智能巡航辅助功能中的横向避让功能介绍

文章目录 1. 背景介绍2. 功能定义3. 功能原理4. 传感器架构5. 实际应用案例5.1 典型场景1&#xff1a;前方车辆压线5.2 典型场景2&#xff1a;相邻车道有大型车辆5.3 典型场景3&#xff1a;它车近距离cut in 6. 总结与展望 1. 背景介绍 随着汽车技术的发展&#xff0c;智能巡航…

springboot开发实用篇

一、Mongodb &#xff08;1&#xff09;简介 MongoDB是一个开源、高性能、无模式的文档型数据库。NoSQL数据库产品中的一种&#xff0c;是最像关系型数据库的非关系型数据库。 数据库&#xff1a;永久性存储&#xff0c;修改频率极低 Mongodb&#xff1a;永久性存储与临时存…

Cxx Primer-Chap4

表达式可以没有操作符&#xff0c;但一定有操作数&#xff1a;理解表达式中含有多个操作符时涉及操作符的优先级、关联性以及操作数的计算顺序&#xff1a;如果操作符需要的操作数类型不同&#xff0c;则会发生一些默认的类型转换&#xff1a;什么叫Overloaded Operators&#…

跨平台应用进程cpu与内存监控的搭建说明

1. 前言: 随着科技的进步,互联网发展,能网上办理的就网上办理,按装一个app客户端,连接后台服务,只要是有网络就OK.便捷,快速,省事.但随之而来的是pc端上安装的应用越来越多,系统资源越来越不够用.这也一定程度上对应用程序有一定的要求,除了实现其功能外,性能也是需要关注的. …

python如何输入矩阵

使用numpy创建矩阵有2种方法&#xff0c;一种是使用numpy库的matrix直接创建&#xff0c;另一种则是使用array来创建。 首先导入numpy&#xff1a; &#xff08;1&#xff09;import numpy &#xff08;2&#xff09;from numpy import * &#xff08;3&#xff09;import …

【JVM】JVM调优练习-随笔

JVM实战笔记-随笔 前言字节码如何查看字节码文件jclasslibJavapArthasArthurs监控面板Arthus查看字节码信息 内存调优内存溢出的常见场景解决内存溢出发现问题Top命令VisualVMArthas使用案例 Prometheus Grafana案例 堆内存情况对比内存泄漏的原因:代码中的内存泄漏并发请求问…

MenuToolButton自绘控件,带下拉框的QToolButton,附源码

MenuToolButton自绘控件&#xff0c;带下拉框的QToolButton 效果 下拉样式可自定义 跟随QToolButton的Qt::ToolButtonStyle属性改变图标文字样式 使用示例 正常UI文件创建QToolButton然后提升&#xff0c;或者直接代码创建都可以。 // 创建一个 QList 对象来存储 QPixm…

TDC 5.0:多集群统一纳管,构建一体化大数据云平台

近期&#xff0c;星环科技数据云平台Transwarp Data Cloud&#xff08;简称TDC&#xff09;5.0版本正式发布&#xff0c;TDC5.0架构屏蔽底层多个TDH集群的差异&#xff0c;采用统一操作模式&#xff0c;新增一个多集群抽象与管理层&#xff0c;能够实现多集群网络互通、跨集群资…

QT纯代码实现滑动开关控件

开关按钮大家应该很熟悉&#xff0c;在设置里面经常遇到&#xff0c;切换时候的滑动效果比较帅气。通常说的开关按钮&#xff0c;有两个状态&#xff1a;on、off。大部分的开关按钮控件&#xff0c;基本上有两大类&#xff0c;第一类是纯代码绘制&#xff0c;这种对代码的掌控度…

dhtmlx-gantt甘特图数据展示

官网文档&#xff1a;甘特图文档 实现效果&#xff1a; 首先需要下载 dhtmlx-gantt组件 npm i dhtmlx-gantt //我项目中使用的是"dhtmlx-gantt": "^8.0.6" 这个版本&#xff0c;不同的版本api或是文档中存在的方法稍有差异 界面引用 <template>&l…

目标检测算法与应用算法 DS集成 接口相关_v0.1

目录 文章目录 目录0. 目标GPS信息、速度、加速度、航向角信息的输出1. 目标检测算法接口1.1 模型相关1.2 检测结果相关 2. 应用算法接口2.1 bool cross_line; //跨线&#xff08;变道压线检测&#xff09;2.2 bool break_in; //闯入&#xff08;目标闯入&#xff09;2.3 bool …

Linux HOOK机制与Netfilter HOOK

一. 什么是HOOK&#xff08;钩子&#xff09; 在计算机中&#xff0c;基本所有的软件程序都可以通过hook方式进行行为拦截&#xff0c;hook方式就是改变原始的执行流。 二. Linux常见的HOOK方式 1、修改函数指针。 2、用户态动态库拦截。 ①利用环境变量LD_PRELOAD和预装载机…

STM32自己从零开始实操:PCB全过程

一、PCB总体分布 以下只能让大家看到各个模块大致分布在板子的哪一块&#xff0c;只能说每个人画都有自己的理由&#xff1a; 电源&#xff1a;从外部接入电源&#xff0c;5V接到中间&#xff0c;向上变成4V供给无线&#xff0c;向下变成3V供给下面的接口&#xff08;也刻意放…