数组表示法与指针表示法 C

Tro*_*cky 4 c arrays pointers

使用指针表示法比数组表示法有什么优势吗?我意识到在某些特殊情况下指针表示法可能更好,但在我看来数组表示法更清晰。我的教授告诉我们,他更喜欢指针表示法“因为它是 C”,但这不是他会标记的东西。我知道将字符串声明为字符数组与将指针声明为字符串之间存在差异 - 我只是在谈论一般循环数组。

Nom*_*mal 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;
};
Run Code Online (Sandbox Code Playgroud)

这个想法是,当您需要矩阵时,可以使用类型的局部变量来描述struct matrix它。所有引用的数据都存储在struct ownerC99 灵活数组成员中动态分配的结构中。当您不再需要该矩阵后,您必须显式“删除”它。这允许多个矩阵引用相同的数据:您甚至可以拥有单独的行、列或对角向量,对其中一个向量的任何更改都会立即反映在所有其他矩阵中(因为它们引用相同的数据值)。

当矩阵与数据关联时,无论是通过创建空矩阵,还是通过引用另一个矩阵引用的现有数据,所有者结构引用计数都会递增。每当删除矩阵时,所引用的所有者结构引用计数都会递减。当引用计数降至零时,所有者结构被释放。这意味着您只需要记住“删除”您使用的每个矩阵,所引用的数据将被正确管理并尽快发布(不需要),但永远不会太早。

这一切都假设一个单线程进程;多线程处理要复杂一些。

要访问矩阵struct matrix m、行r、列中的元素c,假设0 <= r < m.rows0 <= c < m.cols,您可以使用m.origin[r*m.rowstep + c*m.colstep].

如果你想转置一个矩阵,你只需交换m.rowsm.colsm.rowstepm.colstep。所有改变的是读取数据(存储在所有者结构中)的顺序。

(请注意,origin指向出现在矩阵中第 0 行、第 0 列的双精度值;并且rowstepcolstep可以为负数。这允许对原本沉闷的常规数据进行各种奇怪的“视图”,例如镜子和对角线等.)

如果我们没有 C99 灵活的数组成员——也就是说,我们只有指针,根本没有数组表示法——那么所有者结构data成员就必须是一个指针。这意味着硬件级别的额外重定向(稍微减慢数据访问速度)。我们要么需要单独分配指向的内存data,要么使用技巧来指向所有者结构本身后面的地址,但适当地对齐为双精度。

多维数组确实有其用途 - 基本上,当所有维度(或除一维之外的所有维度)的大小已知时 - 并且编译器能够处理索引是很好的,但这并不一定意味着它们是总是比使用指针的方法更容易。例如,在上面的矩阵结构情况下,我们总是可以定义一些辅助预处理器宏,例如

#define MATRIXELEM(m, r, c)  ((m).origin[(r)*(m).rowstep + (c)*(m).colstep])
Run Code Online (Sandbox Code Playgroud)

诚然,它有一个缺点,即它会计算第一个参数m3 次。(这意味着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);
Run Code Online (Sandbox Code Playgroud)

此类宏中的“额外”括号确保如果您使用计算(例如i + 4*j作为行),则索引计算是正确的((i + 4*j)*m.rowstep而不是i + 4*j*m.rowstep)。在预处理器宏中,这些括号根本不是真正的“额外”。除了确保计算正确之外,拥有“额外”括号还告诉其他程序员,宏编写者已经小心地避免了此类与算术相关的错误。(我认为将括号放在那里是一种“良好的形式”,即使在语法明确不需要它们的情况下,如果它向阅读代码的其他开发人员传达了这种“保证”。)

毕竟,这篇文章引出了我最重要的一点:我们人类程序员使用数组表示法比指针表示法更容易表达和理解某些事情,反之亦然。"Foo"[1]非常明显等于'o',而*("Foo"+1)则不那么明显。(话又说回来,两者都不是1["foo"],但你可以为此责怪 C 标准化人员。)

基于上面的例子,我认为这两种符号是互补的;它们确实有很大的重叠,特别是在简单的循环中——在这种情况下,只选择一个是可以的——但是能够利用这两种符号并选择一个不是基于一个人对一种符号的熟练程度,而是基于一个人对什么的看法最有道理。在我看来,可读性和可维护性对于任何 C 程序员来说都是一项重要技能。