伊吖学C笔记(7、地址、指针、指针数组)

发布于:2025-07-06 ⋅ 阅读:(19) ⋅ 点赞:(0)

一、内存、地址

书上说:指针是C语言的精华。在C语言中,数据类型除了整型int、实型float/double、字符型char等之外,还有一种特殊的指针类型(简称指针),变量的内存地址称为变量的指针。就好比汉宜公路的荆州段、318国道的湖北段,类似一个路标。

1、内存

何为内存?都说CPU是计算机的大脑,没错,那内存就是大脑工作的辅助场所。计算机运行时,数据会临时存放在内存中,断电后消失。我们可以把CPU比作工厂里的加工机器,那内存就是工作车间。机器运转中产生各种原材料、中间件、半成品等都临时堆放在车间里,随用随取。如果车间太小,就会借用仓库来周转(仓库相当于计算机里的硬盘,数据不会因为停电而消失,但运行速度比内存慢很多)。仓库的原材料需要先拉入车间(载入内存),才能加工,这样效率就慢很多,所以内存越大越好(当然也越贵)。【为了进一步提高效率,CPU还自带缓存(相当于机器内部的储物格),缓存速度快于内存。这个暂不说。】

2、内存地址

内存太大,乱堆乱放可不好,如何有序管理?答案是:内存会以字节为单位(字节是最小的存储单位),划分为多个连续的存储单元,并且为每个字节默认设置一个对应的编号,这个编号就是内存地址。就好象车间里,机器两边摆满储物格,这些储物格大小一致,每格一编号,比如左边第3排第5格,编号010305。又好象中药铺里的存放中药的小格子编号一样。

程序在运行过程中,总是会涉及到这样的那样的变量(比如:int i=0,a=2;char b='a',c;float x,y;),因为变量可以在运行过程中动态传递数据,使问题的解决变得简单。这些变量类型不一,长短不一,程序初始化时会在内存中划出一块地盘,将这些变量规划排列以及赋值,以方便在程序运行过程中快速准确提取调用或更改。在这张初始表中,不再是i=0、a=2、b='a',而是:地址①的值=0、地址②=2、地址③='a'(这个等于实际上是地址所指内存的值),变量i、a、b变成了三个地址,自身消失。所以变量名取长取短不影响编译及执行效率。程序在初始化时,将变量名转化为内存地址,为指针的运用创造了条件。

3、内存地址的本质

内存地址是计算机系统中用于标识内存中特定存储位置的唯一编号,它属于硬件层面的信号机制。

‌地址生成机制‌:内存地址由CPU通过程序计数器(PC)、基址寄存器等生成,并通过地址总线传输到内存控制器。CPU将虚拟地址发送给MMU(内存管理单元),由MMU转换为物理地址后访问实际内存。[程序虚拟地址] → [MMU转换] → [物理地址] → [内存控制器解码] → [实际存储单元]

‌硬件实现‌:地址总线是单向的,专门用于传输地址信号,其宽度决定了CPU的寻址能力(如32位地址总线可寻址4GB空间)。

若将CPU的地址生成单元、内存控制器等视为"上级电路",则内存地址确实由这些上级电路产生和控制,它总是优先于内存操作。

4、对变量地址的操作理解

变量名前面加“&”,就代表地址,&a就是地址。操作一下:

通常我们声明一个变量,如果不赋值,会出现莫名其妙的数字(上图第一句),这是程序初始化内存的情况。可以通过%x或%p转变为十六进制查看(第2、3句),两者的区别就%p为地址专用格式,前面的0不能省,而%x是取的值(第4、5句)。如果要查看变量地址,需要用到“%p”和“&变量名”输出格式(第4-7句)。

从上图可以看到,当变量赋值后,内存里存放的数据改变了,变量a的值从2130567168变成了1,用十六进制表示从7efde000变成了1。而变量a的地址始终是0028FF44。变量b的地址0028FF40,变量c的地址0028FF3C。从三个地址编号可以发现,a放在高位(地址数大),b、c依次往前(地址数变小),即变量地址顺序:从后向前排列。还可以发现,地址数之差为4,即int变量长度为4。变量赋值后其地址不会改变。

 我们再定义几个变量,可以肯定:变量排序从后向前;char变量占1个字节,int、float变量占4个字节,double变量占8个字节。

变量代表内存中具有特定属性的一个存储单元。它是一个地址和一个值的统称。

二、指针的定义、引用

1、指针定义

变量前面加个“*”,就成了指针。指向谁?用p=&a,表示指向变量a的地址。如下:

指针本身也是变量,也会分配内存地址,上图指针p、q分别拥有自己的地址0028FF3C、0028FF34。当指针指向变量名后,它的地址对应的内存所存的内容为:变量名的地址。图中指针p的内容为0028FF44,为变量a的地址;指针q保存变量d的地址。

各种类型的指针长度,为4个字节:

当声明了字符变量(一个字节)后,再声明指针,指针地址不会紧挨着,会留有空余(4的倍数):

变量b与指针q之间有7个字节,留有3字节的空地址:

指针q的地址一直都是0028FF38。字符变量的定义充分利用内存。

为避免出现野指针(指向不明),可用*p=NULL;(暂定为空指针)。

2、指针引用

如果声明了指针p=&a,则:*p=a表示a的内容,而p则表示自己的内容,存的地址。

加“*”的指针与普通变量同等运算;不加*的指针只能与不加*的其它指针运算,或直接加减数字,int类型,一个1代表4个字节。如下:

3、数组与指针

数组名代表首地址,可视着第一个指针。数组地址:数组名+数。a+2表示a[2]的地址,a表示a[0]的地址,这与指针异曲同工啊。如下:

地址、指针前面加“*”,表示该地址对应的取值。

数组地址排列规律。如下:

a和b相距8字节。

a和b相距16字节,大于8字节不足16字节,以16计算。

 

a和b相距32字节。以16为倍数进行预留。

a和b相距48字节。还是16步进。

定义int数组时,以4的倍数进行地址预留排列,b[3]按b[4]预留,b[5]按b[8]预留。

再看看字符型数组:

看起来正常,改变一个长度,如下:

这个数组b到a预留是30字节,跳了很多,规律不详。

4、字符指针

可以直接定义,如下:

指针处理字符串,似乎是个不错的选择。但这样定义,似乎不能修改单个值,举例如下:

从地址排列上看,0040跑到远端去,离0028太远,不好修改。我们用数组过渡一下:

p++使用后,指针首地址的重新定位,加深理解。

字符指针作为函数参数,完成字符串的复制:

上面把数组名当作指针,传给函数,函数操作完后,直接调用数组即可。

5、指针的跳变

int指针4个字节一跳变,即执行p++;地址增加4字节;而char指针则是一字节一跳变。

由此我们得到变量、地址、内存值的排列如下:

6、指针与函数参数

C定义的函数居然不能返回多个值,如下:

通过传递指针参数,就可以解决多个值同时变化。

指针函数调用改变数组的值,举例如下:

上面这样的函数调用没有任何意义(没有发生),换成指针才行:

三、指针进阶

1、二级指针

从前面我们知道,指针本身也分配地址,所对应内存存放变量的地址,此为一级指针,如下:

一级指针‌(Type*):存储普通变量的地址(如int* p指向int变量)

‌二级指针‌(Type‌**):存储一级指针的地址(如int**pp; pp指向int* p)

二级指针取值,一颗星取一级指针的值(地址);两颗星才能取到变量的值,如下:

定义二级指针当作一级指针使用:

二级指针的主要应用场景:‌动态内存管理‌malloc();‌函数内修改外部指针‌;‌复杂数据结构操作‌(树形结构的节点操作、哈希表实现、多级间接访问需求场景);‌参数传递优化‌(避免大数组拷贝、多字符串处理)。

2、指针数组

指针数组是一种特殊的数据结构,它是一个数组,其中每个元素都是一个指针变量。在C/C++中,指针数组的定义形式为:类型 *数组名[大小]。运算符优先级决定了[]的优先级高于*,因此先构成数组特性。

指针数组的特点

‌①本质是数组‌:指针数组首先是一个数组,其次它的元素都是指针类型。

‌②元素存储指针‌:每个数组元素存储的是一个内存地址,指向特定类型的数据。

‌③内存占用‌:在32位系统中,每个指针元素占4个字节;在64位系统中占8个字节。

‌④灵活性‌:可以独立管理每个指针指向的内存区域。

指针数组的应用场景

‌①字符串数组处理‌:存储多个字符串的首地址,常用于命令行参数处理。

‌②动态内存管理‌:管理多个动态分配的内存块地址。

‌③函数指针数组‌:存储多个函数的入口地址,实现回调机制。

‌④多级数据结构‌:构建树、图等复杂数据结构的节点关系。

先看看整形变量指针数组,如下:

由4个指针构成的数组,数组的值是变量地址。

但要注意指针数组本身还分配地址,长度为4个单位:

我们把其中一个变量换成数组:

并不能全部输出,数组只输出首个。查看地址发生了跳跃:

可以看出,数组地址预留了6个数据位(本身3个),6*4=24字节,要想全部输出,可以加上数组内部指针的变化:

再来看看字符型指针数组:

把单个字符换成数组或指针:

直接定义在一起,看看地址的变化:

记录的内存地址发生了转移,跳到了0040高端。它的排列是不留缝隙,一个挨一个,每个字符串后加个“\0”结束,很连续。

一般为方便操作,用二级指针访问。

与一级指针访问数组类似,只是多了一级。注意访问单个值用**pp;访问一串用*pp(与%s配合使用)。可以发现,指数数组使多个字符串的操作变得简单。比如:

到此,考题设计又多了一个新方法,用字符型指针数组。

3、内存分配malloc()

格式:(类型 *)malloc(字节数);

说明:分配指定字节数的内存空间,转换为指定类型,返回指针。举例:

虽然定义了100个长度,实际上只用了36个。如果定义少了,也是可以的,它是自动加长的,正所谓动态,如下:

来段int型代码:

可以看出,动态分配自由扩充数组长度,且不能通过sizeof获取长度。分配长度也只是象征性的。

内存释放free()

格式:free(p);养成好习惯,及时释放内存空间。

4、二级指针操作内存

二级指针操作内存,用于多个字符串,比如:

此二级指针相当于一个二维数组。看一个int例子:

是不是象标准的二维数组?二级指针与数组的混合玩法。

二级指针使用注意事项

‌①内存管理‌:多级指针释放应‌从内到外‌:先释放内容指针,再释放容器指针;动态分配后必须检查分配是否成功。

‌②空指针检查‌:‌‌解引用前必须验证指针有效性;函数参数应检查二级指针是否为NULL。

‌③ ‌类型安全:确保各级指针类型匹配;避免不同类型指针间的强制转换。

‌④内存边界‌:指针运算时防止越界访问;数组操作时确保索引有效。

‌⑤初始化问题:未初始化的二级指针是野指针;建议初始化为NULL便于检查。

5、二级指针作为函数参数

我们先看指针赋值:

赋值用数组形式arr[i]=i*10;也是可以的。

当我们把它放进函数里,就不行了:

为什么不行?因为这时用到了二级指针:**arr代表二级指针,所以*arr或*(arr+1)代表地址,不是值。如果不用二级指针也是可以的,如下:

我们再回到二级指针的函数,这里需要用到数组指针。如下:

6、数组指针

数组指针是指向数组的指针,它与普通指针有以下关键区别:

①‌类型定义‌:数组指针的类型包含数组长度信息,如int (*ptr)[5]表示指向包含5个int元素的数组的指针;

②‌指针运算‌:数组指针加减1会移动整个数组大小的字节数,而非单个元素大小;

③‌解引用方式‌:解引用数组指针会得到整个数组,而非单个元素。

数组指针与指针数组的区别,这是两个容易混淆但完全不同的概念:

‌数组指针‌:int (*ptr)[5] -指向数组长度为5的指针

‌指针数组‌:int *arr[5] -包含5个指针的数组

操作一下:

看得出,这个长度[4]在一维输出里并不重要,超出也是可以的。从输出看,(*ptr)相当于数组,也不奇怪。再看它的变化:

可以看出,数组指针相当于一个二级指针,(*ptr)代表取值,取[i]的值;*ptr代表第0个地址,*(*ptr+i)代表取值。

数组指针更多用于二维数组(方阵),举个例子:

它有多种方式引用,实际上,a+i == p+i,a[i] == p[i] == *(a+i) == *(p+i),a[i][j] == p[i][j] == *(a[i]+j) == *(p[i]+j) == *(*(a+i)+j) == *(*(p+i)+j)。

可以看出,i变化1,指针跳了4格,就是(*p)[4]定义的长度,意思是每行里有4个列输出,二维数组里的第二维,即j的变化。

看看一维数组的二维输出,先输出第一行:

再输出第3行:

这时候的定义长度[3]发挥了作用,它定位到第7个,前面空了6(2个3)。可以得到一维数组的矩阵输出:

再看看函数调用:

出现了警告,我们把函数也变成指针形式:

int (*allocate_2d_array(int rows,int cols))[]{...}

警告消除了。我们再添加一个打印函数:

可以看出,在打印函数里可以改变输出时的列数,而不在乎原矩阵的形状。本例实现了3*4转4*3的矩阵变化。

7、指针函数与函数指针

指针函数(Pointer function)是指返回值为指针类型的函数,本质上是一个函数,但其返回值是一个地址值。定义格式为:类型名 *函数名(函数参数列表)。看个例子:

函数指针是指向函数的指针变量,它存储了函数在内存中的入口地址。与普通指针不同,函数指针指向的是可执行代码而非数据。

格式为:返回值类型 (*指针变量名)(参数类型列表);例子:

与普通指针的区别:

‌指向内容‌:函数指针指向代码段而非数据段。

‌运算限制‌:不能进行指针算术运算。

‌调用方式‌:通过指针可以直接调用函数。

‌大小‌:在32位系统中通常占4字节,64位系统中占8字节。

8、双指针

双指针是一种广泛应用于数组、链表等数据结构中的高效算法技巧,通过维护两个指针来优化遍历过程。类型:

对撞指针(相向双指针)‌:两个指针分别从序列两端向中间移动,常用于有序数组的查找问题

快慢指针(同向双指针)‌:两个指针以不同速度向同一方向移动,常用于链表环检测等问题

背向指针‌:两个指针从中间向两端移动,常用于查找回文子串等问题

分离双指针‌:两个指针分别属于不同的数组或链表

典型应用场景

①数组操作

移动零‌:将数组中的零元素移动到末尾,保持非零元素相对顺序

两数之和‌:在有序数组中找出两个数使它们的和等于目标值

三数之和‌:找出数组中三个数使它们的和等于目标值

移除元素‌:原地删除数组中特定值的元素

②链表操作

检测环‌:判断链表中是否存在环

寻找中间节点‌:找到链表的中间节点

删除倒数第n个节点‌:删除链表中倒数第n个节点

③字符串处理

反转字符串‌:原地反转字符串

判断回文串‌:判断字符串是否为回文

反转元音字母‌:反转字符串中的元音字母

举个例子,给定一个字符串,前后反转,如下:

再来个例子,删除数组中的指定值,如下:

慢指针指到有效位,原数组依然长为15,有效位以后的数还是老样子:


网站公告

今日签到

点亮在社区的每一天
去签到