数组表示法与指针表示法C语言中的区别

4

使用指针符号与数组符号相比有什么优势吗?我知道可能有一些特殊情况下指针符号更好,但在我看来,数组符号更加清晰明了。我的教授告诉我们他更喜欢使用指针符号“因为它是C语言”,但这不是他用来评分的标准。我知道用字符数组声明字符串与用指针声明字符串之间存在差异——我只是在谈论一般情况下遍历数组。


可能是Char array vs Char Pointer in C的重复问题。 - DYZ
只需花点时间阅读有关指针的内容。起初,你可能看不到任何区别,只会更加困惑,但随后你会对它们的功能感到惊讶。我能想到最简单的例子是:你可以重新分配指针所指向的内存(从而改变大小)。另一方面,一旦创建了一个简单数组,它的大小就无法修改。 - Fureeish
@DYZ 这是关于字符数组和字符指针的问题。我知道它们之间有区别,我只想知道在像循环遍历整型数组这样的情况下使用指针表示法是否有任何实际原因。 - Troy Nechanicky
2
不用担心,使用你认为可以使代码更清晰的任何一种符号。这些符号几乎是等效的。使用指针符号,你可以少用一些变量。它可以编译成比索引符号稍微慢或快一点的代码。这在不同的处理器上会有所波动,但差异通常非常非常小。 - Petr Skocik
@Fureeish 在我的课堂上,我们被告知首先使用数组表示法创建数组,然后使用指针来操纵它们。但我认为你对我来说最相关的观点是,就目前而言,数组表示法是可以的(初学者C语言课程)。 - Troy Nechanicky
显示剩余3条评论
2个回答

5
如果您编写一个简单的循环,使用数组和指针形式通常会编译为相同的机器代码。
特别是在非常数循环退出条件方面存在差异,但只有当您尝试为特定的编译器和架构优化循环时才会有影响。
那么,我们来考虑一个依赖于两者的真实世界示例,怎么样?
这些类型实现了一个动态确定大小的双精度浮点矩阵,并带有单独引用计数的数据存储。
struct owner {
    long          refcount;
    size_t        size;
    double        data[];    /* C99 flexible array member */
};

struct matrix {
    long          rows;
    long          cols;
    long          rowstep;
    long          colstep;
    double       *origin;
    struct owner *owner;
};

这个想法是当你需要一个矩阵时,你使用一个类型为struct matrix的本地变量来描述它。所有引用的数据都存储在动态分配的struct owner结构中,以C99灵活数组成员的形式存在。当你不再需要该矩阵时,必须明确地“删除”它。这允许多个矩阵引用相同的数据:你甚至可以有单独的行、列或对角线向量,对其中任何一个进行更改会立即反映在所有其他向量上(因为它们引用相同的数据值)。
当一个矩阵与数据相关联时,无论是通过创建一个空矩阵还是通过引用另一个矩阵引用的现有数据,所有者结构的引用计数都会增加。每当一个矩阵被删除时,所引用的所有者结构的引用计数都会减少。当引用计数降为零时,所有者结构将被释放。这意味着你只需要记住“删除”你使用过的每个矩阵,所引用的数据将被正确管理和尽快释放(不需要),但永远不会太早。
这一切都假设单线程进程;多线程处理要复杂得多。
要访问矩阵struct matrix m中的元素,行r、列c,假设0 <= r < m.rows0 <= c < m.cols,你可以使用m.origin[r*m.rowstep + c*m.colstep]
如果你想转置一个矩阵,只需交换m.rowsm.cols以及m.rowstepm.colstep。所有改变的只是读取数据(存储在所有者结构中)的顺序。
(注意,origin指向出现在矩阵中行0、列0处的双精度数;rowstepcolstep可以是负数。这允许各种奇怪的“视图”到否则单调的常规数据,如镜像和对角线等。)
如果我们没有C99灵活数组成员——比如说,我们只有指针,没有任何数组表示法——那么所有者结构data成员将必须是一个指针。这将意味着在硬件级别上需要额外的重定向(稍微减慢数据访问速度)。我们要么需要单独分配由data指向的内存,要么使用技巧指向紧随所有者结构本身之后的地址,但适当地对齐为双精度数。
多维数组确实有它们的用处——基本上是当所有维度(或除一个维度之外的所有维度)的大小都已知时——编译器负责索引很好,但这并不意味着它们总是比使用指针的方法更容易。例如,在上述矩阵结构案例中,我们总是可以定义一些帮助器预处理器宏,如:
#define MATRIXELEM(m, r, c)  ((m).origin[(r)*(m).rowstep + (c)*(m).colstep])

这种方法的缺点是它评估第一个参数m三次。(这意味着MATRIXELEM(m++,0,0)实际上会尝试将m增加三次。)在这个特定的情况下,m通常是struct matrix类型的局部变量,这应该会减少意外发生的可能性。例如,可以这样写:

struct matrix m1, m2;

/* Stuff that initializes m1 and m2, and makes sure they point
   to valid matrix data */

MATRIXELEM(m1, 0, 0) = MATRIXELEM(m2, 0, 0);

这些宏中的“额外”括号确保如果您使用计算,例如i + 4*j作为行时,索引计算是正确的((i + 4*j)*m.rowstep而不是 i + 4*j*m.rowstep)。在预处理器宏中,这些括号并不是真正的“额外”。除了确保正确的计算之外,使用“额外”括号也告诉其他程序员宏编写者已经小心避免了这样的算术相关错误。(我认为在这种情况下放置括号即使在语法不明确的情况下,也会向阅读代码的其他开发人员传达这种“保证”,因此应该被视为“好习惯”)。
总之,在所有这些文本之后,我想说的最重要的一点是:有些东西用数组表示法比指针表示法更容易理解和表达。例如,"Foo"[1]很明显等于'o',而*("Foo"+1)就没有那么直观了。(然而,1["foo"]也不太显然,但您可以责怪C标准化人员的这个问题)
基于上述示例,我认为这两种表示法是互补的;它们有很大的重叠部分,特别是在简单循环中,可以选择其中之一。但能够利用这两种表示法并根据阅读性和可维护性来选择一种而不是根据自己的熟练程度,这对于任何C程序员来说都是一种重要的技能,这是我非常谦虚的意见。

2
实际上,如果你在C语言中向函数传递一个数组参数,你实际上是传递了指向其开头的指针。这并不真正地传递了一个数组,首先,因为传递一个数组将包括传递其实际长度,其次,因为传递一个数组(作为值)将意味着它的复制。换句话说,你真正传递的是指向数组开头的迭代器(就像C++中的std::vector::begin()),但你假装你传递的是数组本身。这实际上非常令人困惑。因此,使用指针可以更清晰地表示实际发生的事情,绝对应该被优先选择。
使用数组符号可能也有一些优点,但我认为它们不能抵消上述缺点。首先,使用数组符号强调了单个值的指针和连续块的指针之间的区别。然后,你可以为自己的参考指定传递的数组的预期大小。但是该大小实际上并没有传递给表达式或函数或以某种方式进行检查,这一事实非常令人困惑。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接