在C中安全传递数组

pinkon5k  于 2023-08-03  发布在  其他
关注(0)|答案(2)|浏览(78)

在C中,传递数组不会给出关于所传递数组的长度的信息,因为它们会衰减为原始指针。这将指针信息的传递留给了程序员。我在下面演示了几种方法,并在评论中讨论了一些优点和缺点。

// only works if array has not already decayed,
// passing a raw pointer produces incorrect results 
#define foo(arr) _foo(arr, sizeof arr / sizeof *arr)
// most straightforward option, but can be unsafe
void _foo(int *arr, size_t n)
{
    for (size_t i=0; i < n; i++) {
        // code
    }
}

// very dangerous if array not prepared properly
// simple usage and implementation, but requires sentinel value
void bar(int *arr /* arr must end in -1 */ ) 
{
    for (size_t i=0; arr[i] != -1; i++) {
        // code
    }
}

/* doesn't provide type safety, pointless
// simplifies usage, still hacky in implementation
#define baz(arr) _baz(sizeof arr / sizeof *arr, &arr)
// safest option, but complex usage and hacky implementation
void _baz(size_t n, int (*pa)[n])
{
    int *arr = *pa;
    for (size_t i=0; i < n; i++) {
        // code
    }
}
*/

字符串
还有什么方法我没有考虑到的吗?还有我错过的优点和缺点吗?总的来说,你认为哪种方法最适合一般使用?你用哪种方法?
最常见的方法似乎是第一个没有宏的选项。对宏使用第三个选项被认为是不好的做法吗?对我来说,它似乎是最强大的。
我看到了this问题,但它没有提到第三种方法,考虑到它是在近12年前提出的,如果能获得新的见解,我不会感到惊讶。

编辑:进一步检查后,似乎选项3只在函数接受固定大小数组时提供指针类型安全,我错误地认为this answer的方法将扩展到可变长度数组,并忽略了测试它。

我不确定chux's answer中提到的C23中的更改是否会修复此方法,或者是否可以简化为baz(size_t n, int arr[n])。通过阅读它,linked paper中没有任何东西似乎表明int arr[n]将不再衰变为int *arr,但我可能错了。

7eumitmz

7eumitmz1#

还有什么方法我没有考虑到的吗?
Let's make a deal并考虑门#4:使用大小和指针:(size_t n, int a[n])
C23正在进行更改:Variably-Modified Types允许“可能在编译时进行更强的分析”。
这与现代编译器一起工作,以形成更好的代码并检测弱点。

void foo_vla(size_t n, int arr[n]);

字符串
在C23中可用,即使不支持VLA作为对象。
提供C11/C17版本,支持VLA。
在C99中可用,因为它始终支持VLA。
在C89中可以作为void foo_val(size_t n, int arr[/*n*/]);使用,但是我们降低了可分析性。
这就像good answer到OP没有考虑的旧question,甚至与C23更相关。

lo8azlld

lo8azlld2#

你在一些假设上是错误的:

// only works if array has not already decayed,
// passing a raw pointer produces incorrect results

字符串
数组在运行时不衰减为指针;数组衰减由在编译时通过将数组声明转换为指针声明而进行的静态转换组成。所以你声明了一个指针,在运行时没有转换,但是计算第一个数组元素的地址-这是编译器解释数组名称的方式-并将其作为所需的指针传递。
最初,指针是通过在末尾追加[]来声明的,后来引入了用于指针声明的*。(今天,如果你以这种方式声明一个指针--除了在参数声明中,因为它衰减为一个指针--像这样的声明

/* global file scope */
int arr[];


是一个不完整的类型声明,它在编译时通过仅为数组分配一个元素来完成,并发出警告。(我不知道这是标准要求还是编译器扩展):

pru.c: At top level:
pru.c:5:5: warning: array ‘ptr’ assumed to have one element
    5 | int ptr[];
      |     ^~~


参数声明如下:

void _foo(int *arr, size_t n)


从来不声明一个数组,而是一个指针。即使你这样做:

void _foo(int arr[], size_t n)


或者是

void _foo(int arr[7], size_t n)


这是一个不完整的数组声明 *,它确实衰减 * 为指针声明int *arr(实际上,在K&R的C原始草案中,指针是使用[]声明的,后来包含了*表示法,现在上面指出的不同含义被发布了)
参数声明(这是我们唯一可以讨论decayment的情况)是当你做一个完整的数组声明(完整或不完整)时,如下所示:

void _foo(int arr[7], size_t n)


或者是

void _foo(int arr[], size_t n)


在这种情况下,数组声明被称为 decay 到指针声明,相当于:

void _foo(int *arr, size_t n)


在任何情况下,数组衰减到指针都是因为数组不是一个可用的对象,编译器没有允许你计算数组大小的规定,所以如果你这样做了,例如。

#include <stdio.h>

#define P(_lbl) printf("%s: size of %s == %zd\n", __func__, _lbl, sizeof arr)
void foo1(int arr[7])
{
   P("arr[7]");
}
void foo2(int arr[])
{
   P("arr[]");
}
void foo3(int *arr)
{
   P("*arr");
}
int main()
{
    int arr[3] = {0, 0, 0};
    P("arr[3]");
    foo1(arr);
    foo2(arr);
    foo3(arr);
}


将导致(显示阵列情况下的衰减):

$ a.out
main: size of arr[3] == 12  -- in main, no array decayment happens
foo1: size of arr[7] == 8   -- in foo1, array decays into a pointer.
foo2: size of arr[] == 8    -- in foo2, array again decays into a pointer.
foo3: size of *arr == 8     -- in foo3, no decayment happens, but a pointer is declared.
$ _


将数组衰减为指针是一个完全受支持的特性,它不会在运行时发生(编写代码是为了使用指针,并且数组大小在函数体中永远不知道)您已经看到,在main()中,数组被声明为局部变量,但它不会衰减为指针,因为它也不会在全局变量中衰减。
在其他语言中允许你把数组作为参数传递,并且你可以知道从函数外部传递到内部的数组大小,数组大小(这是以字节为单位的总大小,或元素的数量)必须作为参数传递,hidden,这将允许数组边界检查,或者它们作为对象的引用传递(在OOP中),允许您在对象示例表示中隐藏传递它。这里没有魔法。在C中,只有实际声明的参数才被传递,当传递一个数组时,编译器将所有的数组访问转换为指针访问(并且仅在第一个数组级别,这里没有递归性),所以复杂的声明如下:

int foo(int(*arr[7][3])(void));


将衰减为:

int foo(int(*(*arr)[3])(void));


这是一个指向三个指针的数组的指针,指向不带参数并返回整数的函数。

相关问题