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

C语言语法详解之指针(四)从入门到入土

1.0数组的本质

1.1数组名的理解

我们先来看这样一串代码

int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int *p = &arr[0]

我们这里使用了&arr[0]的方式取出了数组中首个元素的地址,但是实际上数组名本身就是地址,只不过这里使用了取地址的方式方便理解。那么我们不禁要思考一个问题,一个数组内部有那么多的元素,如果只写出一个数组名不标注序号,出现的地址是哪一个元素的呢,我们来做一个测试。

//测试环境:X86 #include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("&arr[0] = %p\n", &arr[0]); printf("arr = %p\n", arr); return 0; }

输出结果如下

这时候我们会发现单独一个数组名竟然和首元素地址的输出结果是一样的,那么显然,当我们单独打出一个数组名的时候得出的结果就只会与其首元素地址相同,但是这时候我们就会有疑惑了,使用sizeof的时候为什么显示的不是首元素的大小呢?

#include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("%d\n", sizeof(arr)); return 0; }

在这里面,输出的结果是40,和我们原先的猜想怎么不一样呢。但是其实这是属于极少数的个例,在C语言的编程中只有两种情况会发生不一样的结果,在其他情况下单独数组名的出现指向的都会是它首元素的地址。特殊情况如下所示
a. sizeof(数组名)在sizeof中存放数组名计算的是整个数组的大小而不是首元素的大小。
b. &(数组名)这里取出的是整个数组的地址,在实现数组加减的时候跨越的字节数量是不一样的,下面会用代码进行解释。

这两个例外只有在它们单独出现的时候才会生效,如果是处于叠加状态,比如说sizeof(arr+1)的情况下,那么arr数组名就仍然只会等于首元素的地址。为了更深入理解数组的性质,我们不妨再尝试一下这一串代码。

#include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("&arr[0] = %p\n", &arr[0]); printf("arr = %p\n", arr); printf("&arr = %p\n", &arr); return 0; }

尝试后我们不难发现,三个打印结果一模一样,那么arr到底与&arr有什么区别呢?

//测试环境:X86 #include <stdio.h> int main() { int arr[10] = { 1,2,3,4,5,6,7,8,9,10 }; printf("&arr[0] = %p\n", &arr[0]); printf("&arr[0]+1 = %p\n", &arr[0]+1); printf("arr = %p\n", arr); printf("arr+1 = %p\n", arr+1); printf("&arr = %p\n", &arr); printf("&arr+1 = %p\n", &arr+1); return 0; }

输出结果

&arr[0] = 0077F820 &arr[0]+1 = 0077F824 arr = 0077F820 arr+1 = 0077F824 &arr = 0077F820 &arr+1 = 0077F848

我们不难发现取出首元素地址与单个数组名的情况下都是仅仅向后跳过了4个字节,而取出&arr则是向后跳了整个数组也就是40个字节。证明了&arr确实是使用的取出整个数组的形式来存储与内存当中的,只是碍于表达形式只能打印出首元素的地址。

2.0使用指针访问数组

2.1arr[]实际上是*(arr+)的语法糖

我们不妨使用这么一个指针来访问数组

#include <stdio.h> int main() { int arr[10] = {0}; //输⼊ int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); //输⼊ int* p = arr; for(i = 0; i < sz; i++) { scanf("%d", p+i); //scanf("%d", arr+i);//也可以这样写 } //输出 for(i = 0; i < sz; i++) { printf("%d ", *(p+i)); } return 0; }

通过这一个代码我们可以很清楚的发现通过指针不断相加的存储,然后再进行解引用就能输出其中的内容,对于数组而言,在没有进行[]处理的时候使用解引用对应指针的方式也能够打印出其中的数值。但我们此时肯定不禁会有一个疑惑为什么在使用scanf进行输入的时候,并不需要解引用也能存储改变数组空间当中的内容呢,这是因为在scanf的输入之时就会自动的解引用指向对应的空间,所以在使用scanf的时候我们都会在变量之前加上&符号,而对于数组名就直接相当于首元素地址的数组而言,就不需要了,这也是数组与普通变量之间的直接区别。

这个代码搞明白后,我们再试一下,如果我们再分析一下,数组名arr是数组首元素的地址,可以赋值给p,其实数组名arr和p在这里是等价的。那我们可以使用arr[i]可以访问数组的元素,那p[i]是否也可以访问数组呢?
#include <stdio.h> int main() { int arr[10] = {0}; //输入 int i = 0; int sz = sizeof(arr)/sizeof(arr[0]); //输入 int* p = arr; for(i = 0; i < sz; i++) { scanf("%d", p+i); //scanf("%d", arr+i);//也可以这样写 } //输出 for(i = 0; i < sz; i++) { printf("%d ", p[i]); } return 0; }

我们运行这个代码,不难发现p[i]的结果与上面*(p+i)的结果是相同的,那么实际上这两个代码在运行的角度中就正是一样的,这样的情况正如标题所说称之为arr[]实际上是*(arr+)的语法糖,在指针的情况中如此,面对数组本体也能起到同样的效果

3.0一维数组传参的本质

3.1数组传参的实验

数组我们之前学过了,也讲过了。我们不妨来做一个实验来测试数组在函数当中是怎么传递的。到底是不是如同我们之前所说的那样传递的为首元素的大小。为了达成实验效果,我们将在函数中与函数外分别计算数组中的元素个数

#include <stdio.h> //测试环境是x86 void test(int arr[]) { int sz2 = sizeof(arr)/sizeof(arr[0]); printf("sz2 = %d\n", sz2); } int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; int sz1 = sizeof(arr)/sizeof(arr[0]); printf("sz1 = %d\n", sz1); test(arr); return 0; }

得出的结果是意料之中的

在函数内部并没有正确获得数组中的元素个数。在上个小节中我们学到了数组的本质实际上是指针,而单独一个数组名存储的实际上是数组首元素的地址,而在此处函数的传参中,传递上去的实际上就不过是一个地址而已,并没有将整个数组完整的传递上去。于此同时sizeof计算的大小就是一个地址的大小而不是整体数组的大小,正是因为函数的参数部分是本质是指针,所以在函数内部是没办法求的数组元素个数的。

void test(int arr[])//参数写成数组形式,本质上还是指针 { printf("%d\n", sizeof(arr)); } void test(int* arr)//参数写成指针形式 { printf("%d\n", sizeof(arr));//计算⼀个指针变量的⼤⼩ } int main() { int arr[10] = {1,2,3,4,5,6,7,8,9,10}; test(arr); return 0; }

总结:一维数组传参无论是写成数组形式还是指针形式都行,两者在代码中代表的含义是相同的,这就是数组的本质。

4.0二级指针与指针数组

4.1二级数组的本质

变量有各种各样的类型,从总的方面来说,指针变量也是变量,那么既然是变量就会有储存的空间,也就产生了指向这个空间的地址,那么储存这一个地址的指针就被称之为二级数组。这个概念对于数据中的链表是十分的重要的,它代表了一个串联的线,往后可以由此组成一张大网,方便我们快速寻找到应有的数据。

它的基础代码是这样的

#include <stdio.h> int main() { int a = 10; int *pa = &a; int** ppa = &pa; return 0; }

相比于一级指针来说,看起来好像不过只是多加了一个*,而在计算机的系统中,它却是这样表示

在实际上的意义中pa代表着一维指针指向的那个地点,而*pa则代表其指向的空间也就是a,那么*ppa实际上就是等于pa,而**ppa则代表着a,往后以此类推,哪怕是三维指针或者四维指针它们所代表的结果都是这样的了。

4.2指针数组

指针数组到底是数组还是指针呢?我相信大部分人在看见这个标题的时候都会产生这个疑惑。

但是正如同整形数组于字符数组是存放对应数据类型的数组,指针数组存放的就都是指针,这与后面我们会讲到的数组指针有很大的区别。

我们不妨先来看看这样一串代码,好让我们充分了解指针数组的初始化和创建方式

#include <stdio.h> int main() { int a = 10; int b = 20; int c = 30; int* arr[3] = {&a, &b, &c};//指针数组,存放的是三个int*类型的指针 int i = 0; for(i = 0; i < 3; i++) { printf("%d ", *(arr[i]));//通过指针解引用取出对应变量的值 } return 0; }

对于指针数组来说必须要填充其中的具体数字,当初始化为0时,实际上是默认在里面创建了NULL指针,当你不规定具体的元素个数时,这里的指针数组如果有具体指向就会很容易导致程序崩溃,所以在使用的时候最好还是提前规定一下

4.3指针数组模拟二维数组

我们都知道二维数组来表示编号的时候是以X Y来为具体的元素空间填充序号的,那么在使用指针数组的过程中我们就可以模拟这种效果,指针数组里面的序号变为实际上二维数组中的列号,内部蕴含着的数组成为行号。代码实现如下

#include <stdio.h> int main() { int arr1[] = {1,2,3,4,5}; int arr2[] = {2,3,4,5,6}; int arr3[] = {3,4,5,6,7}; //数组名是数组⾸元素的地址,类型是int*的,就可以存放在parr数组中 int* parr[3] = {arr1, arr2, arr3}; int i = 0; int j = 0; for(i = 0; i < 3; i++) { for(j = 0; j < 5; j++) { printf("%d ", parr[i][j]); } printf("\n"); } return 0; }

parr[i] 是访问 parr 数组的元素,parr[i] 找到的数组元素指向了整型一维数组,parr[i][j] 就是整型一维数组中的元素。

上述的代码模拟出二维数组的效果,实际上并非完全是二维数组,因为每一行并非是连续的。

从上面的代码和运行结果可以看出,指针数组模拟二维数组的核心思路是:用一个指针数组来存放多个一维数组的首元素地址,再通过双重下标访问的方式,把原本分散在内存中的多个一维数组,在逻辑上组织成一个类似二维数组的结构。其中 parr 是外层指针数组,parr[i] 取出第 i 个一维数组的首地址,parr[i][j] 则进一步访问该一维数组中的第 j 个元素。

这种写法在逻辑上非常直观,也便于理解指针与数组之间的紧密联系。但需要特别注意的是,它和真正的二维数组在内存布局上有着本质区别:真正的二维数组在内存中是连续存放的,所有元素按行紧密排列;而指针数组模拟出的"二维数组",每一行实际上是独立的一维数组,它们在内存中的地址并不连续,行与行之间可能存在空隙。因此,这种模拟方式更适合用于逻辑上的行列访问,如果对内存连续性有严格要求(例如需要整体拷贝或按连续内存块操作),仍然应当使用真正的二维数组。

总结:指针数组模拟二维数组,本质上是"用指针数组管理多个一维数组",它借助数组名即首元素地址的特性,把多个一维数组的首地址统一存放在一个指针数组中,再通过双重下标完成行列访问。这种方式代码简洁、逻辑清晰,是理解指针数组应用场景的经典示例;同时也要牢记它与真正二维数组在内存连续性上的差异,在实际开发中根据需求选择合适的数据组织方式。

http://www.cnnetsun.cn/news/4271329.html

相关文章:

  • 华为软件精英挑战赛复赛进阶:从算法优化到工程实践的全链路指南
  • WPF布局
  • 英文Thesis被Turnitin大面积判为AI生成:BunnyScholar长文降AI实测
  • MATLAB偏最小二乘回归(PLS)实战:从原理到代码解决高维共线性问题
  • C++继承与多态实战:从原理到支付系统设计
  • AI编程助手频繁跑偏?模型训练任务的边界设计与工具权限控制指南
  • Spring Authorization Server 1.4.0 使用及详细配置 搭配Spring Boot3.4.0 + Spring Security6.4.1
  • C++ STL核心组件解析:从容器、迭代器到泛型编程实战
  • 边缘推理框架升级的核查
  • 使用 authentik 搭建统一身份认证与 OIDC 单点登录实践
  • 海康工业相机SDK C#开发实战:从示例程序到项目工程化
  • Python SymPy求解方程组:从数学建模到工程实战
  • 软考系统架构设计师论文涉及知识点之Redis(5)
  • AI短剧工业化与网页端数据驱动:拆解短剧出海登顶路径
  • Windows系统文件WiaExtensionHost64.dll丢失找不到问题解决
  • C++ 逗号运算符详解
  • 如何评测LLM优化评估流程?HarnessOpt-Bench思路与实践
  • 2026AI论文工具终极排行榜✅实测无广!本科/硕博/期刊全场景排名
  • BFS算法实战:多源点扩散问题解析与Python实现
  • 强化学习中的奖励结构:如何重塑情景探索与神经记忆的交互
  • 蓝桥杯国赛Java选手五一冲刺:从算法复盘到实战模拟的备赛指南
  • 电力巡检绝缘子缺陷识别数据集:YOLOv5实战落地指南
  • Simulink数学建模:从微分方程到可视化仿真的工程实践
  • 从格式废墟到优雅存档:智谱清言导出Excel背后的技术破局
  • 一文读懂 GEO:定义、价值、服务商选型与效果评估全拆解
  • 本地部署开源大模型:用Ollama搭建AI写作辅助系统实战指南
  • 117、移动机器人与机械臂协同:移动操作中的导航与规划
  • MATLAB信号处理进阶实战:从旋变解码到雷达CFAR检测
  • C++ swap操作深度解析:从基础实现到拷贝交换设计模式
  • 蓝桥杯单片机国赛门禁系统实战:状态机设计与模块驱动详解