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

C程序中的数组与指针共生关系

C程序中的数组与指针共生关系

在C语言的编程世界中,处理大量同类型的数据是程序员不可避免的任务。无论是统计日降雨量、管理库存,还是记录客户交易,我们都需要一种高效的方式来组织这些相关数据。数组,作为相同数据类型元素的集合,正是为此而生。然而,要真正掌握C语言的精髓,就必须深入理解数组背后更本质的概念——指针。这篇文章将深度剖析数组与指针之间密不可分、甚至可以说是一体两面的共生关系,揭示C语言高效和强大的根源。

数组:数据的有序集合

创建数组时,我们必须明确告知编译器两件事:元素的类型和数量。编译器会据此在内存中开辟一块连续的空间。访问数组中的元素,我们通过下标(或称索引)来实现,这个编号从0开始。

初始化与声明

初始化数组最直接的方式是提供一个值的列表,用花括号 {} 包围。

/* day_mon1.c -- 打印每个月的天数 */
#include <stdio.h>
#define MONTHS 12int main(void)
{// 使用花括号和逗号分隔的列表来初始化数组const int days[MONTHS] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };int index;for (index = 0; index < MONTHS; index++)printf("Month %2d has %2d days.\n", index + 1, days[index]);return 0;
}

代码描述:此程序定义了一个包含12个整数的常量数组 days,用于存储每个月的天数。const 关键字确保这个数组在程序运行期间不会被意外修改,这是一种良好的编程实践,用于保护关键数据。for 循环遍历数组,通过下标 days[index] 访问每个元素并打印出来。

C语言在初始化方面提供了相当的灵活性。如果初始化列表中的项数少于数组的元素个数,编译器会自动将剩余的元素初始化为0。这是一个非常重要的特性,可以避免未初始化数组中存在的“垃圾值”。

/* some_data.c -- 部分初始化数组 */
#include <stdio.h>
#define SIZE 4
int main(void)
{int some_data[SIZE] = { 1492, 1066 }; // 只提供了2个初始值int i;printf("%2s%14s\n", "i", "some_data[i]");for (i = 0; i < SIZE; i++)printf("%2d%14d\n", i, some_data[i]);return 0;
}

代码描述some_data 数组的大小为4,但只初始化了前两个元素。运行此程序,你会发现 some_data[2]some_data[3] 的值都是0,这是编译器自动完成的。

C99标准引入了“指定初始化器”,允许我们初始化指定的数组元素,而不必按顺序进行。

// designate.c -- 使用指定初始化器
#include <stdio.h>
#define MONTHS 12
int main(void)
{int days[MONTHS] = { 31, 28, [4] = 31, 30, 31, [1] = 29 };int i;for (i = 0; i < MONTHS; i++)printf("%d %d\n", i + 1, days[i]);return 0;
}

代码描述:这段代码展示了指定初始化器的两个重要特性:

  1. 顺序填充[4] = 31 初始化了第5个元素(下标为4)后,紧随其后的 3031 会被用来初始化后续的元素,即 days[5]days[6]
  2. 最终赋值有效days[1](下标为1的元素)最初被初始化为 28,但随后又被 [1] = 29 重新赋值,因此最终它的值是 29。未被显式初始化的元素,如 days[2]days[3],会被自动初始化为0。

指针:访问数据的另一种途径

指针提供了一种以符号形式使用内存地址的方法。它与数组的关系,是C语言强大与高效的根源之一。最核心的概念是:数组名是该数组首元素的地址

这意味着,如果 flizny 是一个数组,那么 flizny&flizny[0] 在值上是完全等价的。这个地址是一个常量,不能被修改。但我们可以将它赋值给一个指针变量,然后通过操作指针来访问数组。

// pnt_add.c -- 指针地址
#include <stdio.h>
#define SIZE 4
int main(void)
{short dates[SIZE];double bills[SIZE];short * pti;double * ptf;pti = dates; // 把数组地址赋给指针ptf = bills;printf("%23s %15s\n", "short", "double");for (int index = 0; index < SIZE; index++)printf("pointers + %d: %10p %10p\n", index, pti + index, ptf + index);return 0;
}

代码描述:该程序声明了一个 short 型指针 pti 和一个 double 型指针 ptf,并分别将 datesbills 数组的地址赋给它们。循环打印 指针 + 整数 的结果。你会发现 pti 每加1,其地址值增加2(sizeof(short));而 ptf 每加1,地址值增加8(sizeof(double))。这揭示了指针运算的本质:在C中,指针加1,指的是增加一个存储单元。对数组而言,这意味着地址会移动到下一个元素的地址,而不是下一个字节的地址

这种关系使得数组表示法和指针表示法可以互换。C语言标准明确定义 ar[n] 的意思就是 *(ar + n)

/* day_mon3.c -- 使用指针表示法 */
#include <stdio.h>
#define MONTHS 12
int main(void)
{int days[MONTHS] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };int index;for (index = 0; index < MONTHS; index++)// *(days + index) 与 days[index] 相同printf("Month %2d has %d days.\n", index + 1, *(days + index));return 0;
}

代码描述:这个程序使用 *(days + index) 来获取数组元素的值,其中 days + index 计算出第 index 个元素的地址,* 运算符则解引用该地址以获取存储的值。其功能和输出与使用 days[index] 的版本完全一致,有力地证明了二者的等效性。

协同工作:函数中的数组与指针

将数组传递给函数时,这种指针与数组的等价性变得至关重要。你无法将整个数组作为参数传递给函数,因为这涉及到庞大的数据拷贝,效率极低。实际上传递的,仅仅是数组的地址——一个指向其首元素的指针。

因此,一个接收数组的函数,其形参实际上是一个指针。int *arint ar[] 这两种写法在作为函数形参时是完全等价的。

// sum_arr1.c -- 数组元素之和
#include <stdio.h>
#define SIZE 10
int sum(int ar[], int n);int main(void)
{int marbles[SIZE] = { 20, 10, 5, 39, 4, 16, 19, 26, 31, 20 };printf("The size of marbles is %zd bytes.\n", sizeof marbles);sum(marbles, SIZE);return 0;
}int sum(int ar[], int n)
{printf("The size of ar is %zd bytes.\n", sizeof ar);// ... sum calculation ...
}

代码描述:此代码的关键在于 sizeof 运算符的结果。在 main 函数中,sizeof marbles 会返回 10 * sizeof(int),即40字节(假设 int 为4字节)。然而,在 sum 函数内部,sizeof ar 返回的是指针的大小(在64位系统上通常是8字节),而不是整个数组的大小。这清晰地证明了传递给函数的是地址,而非数组实体。

为了保护传入函数的数据不被意外修改,我们可以使用 const 关键字。

// arf.c -- 处理数组的函数
void show_array(const double ar[], int n);
void mult_array(double ar[], int n, double mult);

代码描述:在 show_array 函数中,ar 被声明为 const,这意味着函数内部任何试图修改 ar 指向的数据(如 ar[i] = 0;)的操作都会导致编译错误。而 mult_array 函数需要修改数组内容,因此其参数 ar 没有 const 限定。

深入多维数组

对于二维数组,例如 int zippo[4][2],我们可以将其理解为“数组的数组”。这里的指针关系变得更加微妙:zippo 是指向“包含2个int的数组”的指针,而 zippo[0] 是指向 int 的指针。

// zippo1.c -- zippo的相关信息
#include <stdio.h>
int main(void)
{int zippo[4][2] = { { 2, 4 }, { 6, 8 }, { 1, 3 }, { 5, 7 } };printf("      zippo = %p,   zippo + 1 = %p\n", zippo, zippo + 1);printf("   zippo[0] = %p, zippo[0] + 1 = %p\n", zippo[0], zippo[0] + 1);printf("     *zippo = %p,   *zippo + 1 = %p\n", *zippo, *zippo + 1);printf("      **zippo = %d\n", **zippo);printf("    zippo[2][1] = %d\n", zippo[2][1]);printf("*(*(zippo+2)+1) = %d\n", *(*(zippo + 2) + 1));return 0;
}

代码描述:这段代码是理解多维数组与指针关系的关键。

  • zippozippo[0] 的地址值相同,但 zippo + 1 的地址偏移了8字节(一行的大小),而 zippo[0] + 1 只偏移了4字节(一个元素的大小)。
  • *zippo 解引用了行指针,其结果是第一行的地址,所以 *zippozippo[0] 的值相同。
  • **zippo 是对行指针解引用再对元素指针解引用,最终得到第一个元素 zippo[0][0] 的值。
  • *(*(zippo+2)+1) 通过指针运算精确地定位到了 zippo[2][1]

要声明一个能指向 zippo 这种二维数组的指针,必须使用 int (* pz)[2]; 语法。在将二维数组传递给函数时,这个概念尤为重要,函数的形参必须能匹配传入的实参类型,且必须指明除第一维之外的所有维度的大小。

// array2d.c -- 处理二维数组的函数
#define ROWS 3
#define COLS 4
void sum_cols(int ar[][COLS], int rows); // COLS是必须的int main(void) {int junk[ROWS][COLS] = { ... };sum_cols(junk, ROWS);
}

代码描述sum_cols 函数的声明 int ar[][COLS] 告诉编译器 ar 是一个指针,它指向一个包含 COLS(即4)个 int 元素的数组。编译器需要 COLS 这个信息来正确计算 ar[r][c] 的内存地址。

C99/C11 的现代特性

变长数组 (VLA)

C99标准引入了变长数组,允许使用变量来定义数组的维度,极大地增强了代码的通用性。

// vararr2d.c -- 使用变长数组的函数
#include <stdio.h>
int sum2d(int rows, int cols, int ar[rows][cols]); // VLA作为函数形参int main(void)
{int junk[3][4] = { ... };int varr[3][10]; // VLA声明// ...sum2d(3, 4, junk);sum2d(3, 10, varr);return 0;
}

代码描述:通过VLA,sum2d 函数现在可以处理任意行列的二维 int 数组。注意,在函数原型和定义中,维度变量 rowscols 必须在数组 ar 之前声明。

复合字面量

C99的另一个创新是复合字面量,它允许创建匿名的、临时的数组常量,常用于函数调用。

// flc.c -- 有趣的常量
#include <stdio.h>
int sum(const int ar[], int n);
int sum2d(const int ar[][4], int rows);int main(void)
{int total1, total2, total3;int *pt1 = (int[2]){10, 20}; // 创建匿名数组并存储地址total1 = sum(pt1, 2);total2 = sum2d((int[2][4]){{1,2,3,-9},{4,5,6,-8}}, 2); // 直接传递匿名二维数组total3 = sum((int[]){4,4,4,5,5,5}, 6); // 省略大小的匿名数组// ...return 0;
}

代码描述:该代码展示了复合字面量的三种用法。pt1 指向一个在代码中直接定义的匿名数组。sum2dsum 的调用则直接将复合字面量作为实参传递,避免了预先声明变量的需要,使代码更加紧凑。


附录:精选代码解读

1. ptr_ops.c — 指针操作的权威展示

这个程序是理解指针所有基本操作的绝佳范例。让我们逐一分析。

// ptr_ops.c -- 指针操作
#include <stdio.h>
int main(void)
{int urn[5] = {100, 200, 300, 400, 500};int * ptr1, *ptr2, *ptr3;ptr1 = urn;      // 操作1: 赋值ptr2 = &urn[2];  // 也是赋值// 打印初始状态printf("ptr1 = %p, *ptr1 =%d, &ptr1 = %p\n", ptr1, *ptr1, &ptr1);ptr3 = ptr1 + 4; // 操作2: 指针加法printf("ptr1 + 4 = %p, *(ptr1 + 4) = %d\n", ptr1 + 4, *(ptr1 + 4));ptr1++;          // 操作3: 递增指针printf("ptr1 after ptr1++: %p, *ptr1 = %d\n", ptr1, *ptr1);ptr2--;          // 操作4: 递减指针printf("ptr2 after ptr2--: %p, *ptr2 = %d\n", ptr2, *ptr2);// 恢复ptr1和ptr2--ptr1;++ptr2;printf("ptr2 - ptr1 = %td\n", ptr2 - ptr1); // 操作5: 指针求差printf("ptr3 - 2 = %p\n", ptr3 - 2);        // 操作6: 指针减整数return 0;
}

深度解读:

  • 赋值: ptr1 = urn; 将数组 urn 的首地址赋给 ptr1ptr2 = &urn[2]; 将第3个元素的地址赋给 ptr2
  • 解引用与取址: *ptr1 得到 ptr1 指向地址的值(100)。&ptr1 得到指针变量 ptr1 自身的内存地址,它与 urn 的地址是完全不同的。
  • 指针加法: ptr1 + 4 计算的是 ptr1 的地址加上 4 * sizeof(int)。结果是指向 urn[4] 的地址。*(ptr1 + 4) 自然就是 urn[4] 的值(500)。
  • 递增/递减: ptr1++ 是一个副作用操作,它修改了 ptr1 自身的值,使其指向下一个元素 urn[1]ptr2-- 同理,使其从指向 urn[2] 变为指向 urn[1]
  • 指针求差: ptr2 - ptr1 计算的是两个地址之间相隔多少个元素。在恢复后,ptr1 指向 urn[0]ptr2 指向 urn[2],它们之间相隔2个 int 元素,所以结果是2,而不是地址的字节差。
  • 指针减整数: ptr3 - 2 与指针加法类似,计算结果是一个新的地址,即 ptr3 的地址减去 2 * sizeof(int)。因为 ptr3 指向 urn[4],所以结果是 urn[2] 的地址。
2. zippo 数组 — *(*(zippo+2)+1) 的逐步解析

这个表达式是理解多维数组指针表示法的试金石。让我们以 int zippo[4][2] 为例,一步步拆解它如何等价于 zippo[2][1]

  1. zippo:

    • 含义: 二维数组的名称。
    • 类型: int (*)[2],即一个指向“包含2个int的数组”的指针。
    • : 整个数组的起始地址,数值上等于 &zippo[0]
  2. zippo + 2:

    • 含义: 对指向行的指针进行算术运算。
    • 计算: zippo 的地址值加上 2 * sizeof(int[2])sizeof(int[2]) 是8字节(假设int为4字节),所以地址增加了16字节。
    • 结果: &zippo[2],即第3行的起始地址。
  3. *(zippo + 2):

    • 含义: 解引用行指针。
    • 计算: 获取 &zippo[2] 地址处存储的值。由于 zippo + 2 是一个指向数组的指针,解引用它得到的是那个数组本身。在C中,数组名即其首元素地址。
    • 结果: zippo[2],它是一个一维数组。其值等于 &zippo[2][0],即第3行第1个元素的地址。
    • 类型: int *,即一个指向 int 的指针。
  4. *(zippo + 2) + 1:

    • 含义: 对指向元素的指针进行算术运算。
    • 计算: &zippo[2][0] 的地址值加上 1 * sizeof(int),即4字节。
    • 结果: &zippo[2][1],即第3行第2个元素的地址。
  5. *(*(zippo + 2) + 1):

    • 含义: 解引用最终的元素指针。
    • 计算: 获取 &zippo[2][1] 地址处存储的值。
    • 结果: zippo[2][1] 的值。
http://www.dtcms.com/a/503416.html

相关文章:

  • 网站的建立步骤网站ip地址大全
  • 每日Reddit AI信息汇总 10.19
  • k8s(九)安全机制
  • 中国建设银行陕西分行官方网站微网站建设使用程序
  • 福州网站优化手机网站设计案例
  • Orleans 序列化、Actor Placement 和 Actor 调用详细分析
  • Java Object类及包装类
  • 《算法通关指南---C++编程篇(4)》
  • VScode 入门(设置篇)
  • 【第十八周】机器学习笔记07
  • 机械行业做网站wordpress 唯艾迪
  • TVM | 基本概念
  • 建设网站免费模板下载中国旅游网站模板
  • UVa 1471 Defense Lines
  • 【题解】洛谷 P11673 [USACO25JAN] Median Heap G [树形 dp]
  • 气球游戏(DP,分治)
  • MySQL同步连接池与TrinityCore的对比学习(六)
  • UserWarning: No file found at “C:\Faces\image_0032.jpg“AssertionError
  • 网站生成器下载wordpress 添加微博关注
  • 【个人成长笔记】Qt Creator快捷键终极指南:从入门到精通
  • 【开题答辩过程】以《校园可共享物品租赁系统的设计与实现》为例,不会开题答辩的可以进来看看
  • 北京高端网站定制公司猎头公司工作怎么样
  • StarRocks-基本介绍(一)基本概念、特点、适用场景
  • Java零基础入门:从封装到构造方法 --- OOP(上)
  • JAVA算法练习题day43
  • 如何学习Lodash源码?
  • 建个自己的网站难吗宁波 seo整体优化
  • uni-app详解
  • AI学习:SPIN -win-安装SPIN-工具过程 SPIN win 电脑安装=accoda 环境-第五篇:代码修复]
  • 【Linux】Linux:sudo 白名单配置与 GCC/G++ 编译器使用指南