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.rhkb.cn/news/379599.html

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

相关文章

js vue axios post 数组请求参数获取转换, 后端go参数解析(gin框架)全流程示例

今天介绍的是前后端分离系统中的请求参数 数组参数的生成&#xff0c;api请求发送&#xff0c;到后端请求参数接收的全过程示例。 为何会有这个文章&#xff1a;后端同一个API接口同时处理单条或者多条数据&#xff0c;这样就要求我们在前端发送请求参数的时候需要统一将请…

纯前端小游戏,4096小游戏,有音效,Html5,可学习使用

// 游戏开始运行create: function(){this.fieldArray [];this.fieldGroup this.add.group();this.score 0;//4096 增加得分this.bestScore localStorage.getItem(gameOptions.localStorageName) null ? 0 : localStorage.getItem(gameOptions.localStorageName);for(var …

昇思25天学习打卡营第9天|生成式

昇思25天学习打卡营第9天 文章目录 昇思25天学习打卡营第9天CycleGAN图像风格迁移互换模型介绍模型简介模型结构 数据集数据集下载数据集加载可视化 构建生成器构建判别器优化器和损失函数前向计算计算梯度和反向传播模型训练模型推理参考打卡记录 CycleGAN图像风格迁移互换 本…

【PostgreSQL】PostgreSQL 教程

博主介绍&#xff1a;✌全网粉丝20W&#xff0c;CSDN博客专家、Java领域优质创作者&#xff0c;掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java技术领域✌ 技术范围&#xff1a;SpringBoot、SpringCloud、Vue、SSM、HTML、Nodejs、Python、MySQL、PostgreSQL、大数据、物…

C++【OpenCV】图片亮度色度归一化

#include <opencv2/highgui.hpp> #include <opencv2/imgproc.hpp> #include <iostream>using namespace cv; using namespace std;int main() {Mat image imread("SrcMF.jpg");// 灰度、Gamma归一化亮度cv::Mat m_gray;cv::cvtColor(image, m_gra…

Richteck立锜科技电源管理芯片简介及器件选择指南

一、电源管理简介 电源管理组件的选择和应用本身的电源输入和输出条件是高度关联的。 输入电源是交流或直流&#xff1f;需求的输出电压比输入电压高或是低&#xff1f;负载电流多大&#xff1f;系统是否对噪讯非常敏感&#xff1f;也许系统需要的是恒流而不是稳压 (例如 LED…

Docker-compose单机容器集群编排

传统的容器管理&#xff1a;Dockerfile文件 -> 手动执行 docker build 一个个镜像的构建 -> 手动执行 docker run 一个个容器的创建和启动 容器编排管理&#xff1a;Dockerfile文件 -> 在docker-compose.yml配置模板文件里定义容器启动参数和依赖关系 -> 执行dock…

vue echarts 柱状图表,点击柱子,路由代参数(X轴坐标)跳转

一 myChart.on(click, (params) > {if (params.componentType series && params.dataIndex ! undefined) {const months this.month_htqd[params.dataIndex]; // 获取点击柱状图的 X 轴坐标值alert(点击了柱状图&#xff0c;值为: ${months});// 根据点击的柱状图…

基于PHP+MYSQL开发制作的趣味测试网站源码

基于PHPMYSQL开发制作的趣味测试网站源码。可在后台提前设置好缘分&#xff0c; 自己手动在数据库里修改数据&#xff0c;数据库里有就会优先查询数据库的信息&#xff0c; 没设置的话第一次查询缘分都是非常好的 95-99&#xff0c;第二次查就比较差 &#xff0c; 所以如果要…

深度解析:如何优雅地删除GitHub仓库中的特定commit历史

&#x1f49d;&#x1f49d;&#x1f49d;欢迎莅临我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

第二证券:电影暑期档持续升温 农机自动驾驶驶入快车道

农机自动驾驶打开驶入快车道 得益于农机补贴、土地流通、高标准农田制造等方针引导&#xff0c;叠加技术突围和用户降本增效的内生需求&#xff0c;我国正处于农业2.0向农业3.0的过渡阶段。其间农机自动驾驶系统是结束农业3.0&#xff08;即自动化&#xff09;的要害并迎来快速…

互联网行业的产品方向(二)

数字与策略产品 大数据时代&#xff0c;数据的价值越来越重要。大多数公司开始对内外全部数据进行管理与挖掘&#xff0c;将业务数据化&#xff0c;数据资产化&#xff0c;资产业务化&#xff0c;将数据产品赋能业务&#xff0c;通过数据驱动公司业务发展&#xff0c;支撑公司战…

QT5:简单显示百度页面

目录 前言 一、环境 二、实现过程 1.引入模块 2.环境构建 三、代码示例 总结 参考博客 前言 使用qt5 QT WebEngine 模块实现在Designer 上展示百度页面。 一、环境 qt版本&#xff1a;5.12.7 windows 11 下的 Qt Designer &#xff08;已搭建&#xff09; 编译器&a…

地图项目涉及知识点总结

序&#xff1a;最近做了一个在地图上标记点的项目&#xff0c;用户要求是在地图上显示百万量级的标记点&#xff0c;并且地图仍要可用&#xff08;能拖拽&#xff0c;能缩放&#xff09;。调研了不少方法和方案&#xff0c;最终实现了相对流畅的地图系统&#xff0c;加载耗时用…

STM32全栈嵌入式人脸识别考勤系统:融合OpenCV、Qt和SQLite的解决方案

1. 项目概述 本项目旨在设计并实现一个基于STM32的全栈人脸识别考勤系统。该系统结合了嵌入式开发、计算机视觉和数据库技术&#xff0c;实现了自动人脸检测、识别和考勤记录功能。 主要特点: 使用STM32F4系列微控制器作为主控制器采用OpenCV进行人脸检测和识别Qt开发跨平台…

数据结构 day3

目录 思维导图&#xff1a; 学习内容&#xff1a; 1. 顺序表 1.1 概念 1.2 有关顺序表的操作 1.2.1 创建顺序表 1.2.2 顺序表判空和判断满 1.2.3 向顺序表中添加元素 1.2.4 遍历顺序表 1.2.5 顺序表按位置进行插入元素 1.2.6 顺序表任意位置删除元素 1.2.7 按值进…

Xcode 16 beta3 真机调试找不到 Apple Watch 的尝试解决

很多小伙伴们想用 Xcode 在 Apple Watch 真机上调试运行 App 时却发现&#xff1a;在 Xcode 设备管理器中压根找不到对应的 Apple Watch 设备。 大家是否已将 Apple Watch 和 Mac 都重启一万多遍了&#xff0c;还是束手无策。 Apple Watch not showing in XCodeApple Watch wo…

PHP手边酒店多商户版平台小程序系统源码

&#x1f3e8;【旅行新宠】手边酒店多商户版小程序&#xff0c;一键解锁住宿新体验&#xff01;&#x1f6cc; &#x1f308;【开篇&#xff1a;旅行新伴侣&#xff0c;尽在掌握】&#x1f308; 还在为旅行中的住宿选择而纠结吗&#xff1f;是时候告别繁琐的搜索和比价过程&a…

电脑屏幕录制怎么弄?分享3个简单的电脑录屏方法

在信息爆炸的时代&#xff0c;屏幕上的每一个画面都可能成为我们生活中不可或缺的记忆。作为一名年轻男性&#xff0c;我对于录屏软件的需求可以说是既挑剔又实际。今天&#xff0c;我就为大家分享一下我近期体验的三款录屏软件&#xff1a;福昕录屏大师、转转大师录屏大师和OB…

TikTok账号矩阵运营怎么做?

这几年&#xff0c;聊到出海避不过海外抖音&#xff0c;也就是TikTok&#xff0c;聊到TikTok电商直播就离不开账号矩阵&#xff1b; 在TikTok上&#xff0c;矩阵养号已经成为了出海电商人的流行策略&#xff0c;归根结底还是因为矩阵养号可以用最小的力&#xff0c;获得更大的…