数组指针的常量正确性?

18

有人提出了一个观点,认为在现代C语言中,我们应该总是通过数组指针将数组传递给函数,因为数组指针具有强类型化。例如:

void func (size_t n, int (*arr)[n]);
...

int array [3];
func(3, &array);

这听起来可能是一个很好的想法,可以防止各种类型相关和数组越界错误。但是我想到了一个问题,就是我不知道如何将const正确应用于此。
如果我执行void func (size_t n, const int (*arr)[n]),那么它就是const正确的。但是我不能再传递数组,因为指针类型不兼容。int (*)[3]const int (*)[3]。限定符属于指向的数据而不是指针本身。
调用者中的显式转换会破坏增加类型安全性的整个想法。
我如何将const正确应用于作为参数传递的数组指针?这是否有可能?
编辑
只是作为信息,有人说像这样通过指针传递数组的想法可能源自MISRA C++:2008 5-2-12。例如,请参见PRQA的高完整性C++标准

1
我猜重点是要考虑使用const来正确设计所有内容? - George Stocker
正确的指针传递应该比您传递的数组少一维。例如,对于int ia[3],应该使用int *arr来传递数组本身。或者,为了对称性,可以像传递数组一样声明参数:int arr[],并使用形式参数的隐式转换。 - too honest for this site
1
@Olaf,如果这样做,数组会退化,整个类型安全性的论点也就失去了。相对于int *param,参数int param[n]并没有增加类型安全性的保障。 - Lundin
@Lundin:好的,那么您接受为每个数组访问添加*。明白了。 - too honest for this site
1
这就是为什么我个人认为MISRA是一本好书,但主要是因为它的理性。把样式指南当作宗教来看待总是不好的。如果涉及到合规性与质量之间的选择,我总是更喜欢后者。(我非常清楚我们并不总是有选择的余地) - too honest for this site
显示剩余5条评论
3个回答

5
除了使用 cast 外,没有其他方法可以这样传递数组。这是将数组以此方式传递的想法的一个重大缺陷。 这里有一个类似的线程,其中比较了 C 规则和 C++ 规则。我们可以从这个比较中得出结论,C 规则设计得不太好,因为您的用例是有效的,但 C 不允许隐式转换。另一个例子是将 T ** 转换为 T const * const *;这是安全的,但 C 不允许这样做。
请注意,由于 n 不是常数表达式,因此 int n, int (*arr)[n]int n, int *arr 相比并没有增加任何类型安全性。您仍然知道长度(n),访问越界仍然是未定义行为,传递实际上不是长度为 n 的数组也是未定义行为。
当编译器必须报告您是否传递了错误长度的数组指针时,此技术在传递非 VLA 数组的情况下更有价值。

1
VLA 部分并不是真正需要的,它只是我所举的一个例子。同样的问题也存在于普通静态大小的数组中。 - Lundin
@Lundin 是的。有一次我使用了这样的设置 typedef char KEY_BUFFER[8]; typedef char const CKEY_BUFFER[8]; ,如果调用一个接受常量缓冲区的函数,就要像这样编写代码:KEY_BUFFER buf; ... check_buf( (CKEY_BUFFER *)&buf ); ,然而看到最终结果,必须使用强制转换意味着类型安全性的收益很小,可读性的代价很大。我最终没有使用const版本,并依赖于文档。如果从头开始编写,我可能不会再尝试那种设置。 - M.M
@M.M 我有一个使用宏 #define const_cast(n, arr) _Generic(arr, int(*)[n] : (const int(*)[n])arr ) 的想法。然后强制调用者使用该宏,类似于您的示例,但据我所知,这应该是类型安全的。不过,它仍会给调用者代码添加混乱。 - Lundin
给定参数声明 int n, int (*arr)[n],传递比 n 更小的数组不是未定义行为 - 您需要使用 int n, int (*arr)[static n]。没有 staticn 将被完全忽略,并且传递较小的数组是良好定义的(在我看来不太好,但我们就是这样)。当然,MISRA 禁止使用 static(因为 MISRA 作者不理解它的作用,并且更喜欢不太安全的代码,R17.6)。 - Alex Celeste
2
@Leushenko C11 6.7.6.2/6 表示,如果与 int (*arr)[n] 相对应的参数不是类型为 int[n] 的数组的精确地址,则它是未定义行为。在 int (*arr)[static n] 中,static 没有影响。您将此情况与参数声明 int v[n]int v[static n] 混淆了(对于这种情况,您的分析是正确的)。 - M.M

5
C标准规定(§6.7.3/9):如果数组类型的说明符中包含任何类型限定符,则元素类型会被限定,而不是数组类型。因此,在 const int (*arr)[n] 的情况下,const 应用于数组的元素而不是数组 arr 本身。arr 是指向数组[n]的 const int 指针类型,而您正在传递一个 int 数组[n] 的指针类型参数。这两种类型不兼容。
如何将 const 正确应用于作为参数传递的数组指针?这是否可能?
这在标准 C 中是不可能的,除非使用显式转换。但是,GCC 允许这种扩展

In GNU C, pointers to arrays with qualifiers work similar to pointers to other qualified types. For example, a value of type int (*)[5] can be used to initialize a variable of type const int (*)[5]. These types are incompatible in ISO C because the const qualifier is formally attached to the element type of the array and not the array itself.

 extern void
 transpose (int N, int M, double out[M][N], const double in[N][M]);
 double x[3][2];
 double y[2][3];
 ...
 transpose(3, 2, y, x); 

进一步阅读:C和C++中带有const限定符的数组指针


嗯,那就是我在问题中解释的内容?“但是我不能再传递数组了,因为指针类型不兼容。”这就是问题所在,而不是解决方案。或者你是说没有解决方案吗?我只对标准C感兴趣。 - Lundin
@Lundin; 很抱歉目前还没有解决方案。但是,您可以使用GCC扩展。 - haccks
gcc vs. C - 1:0。@Lundin:说到const正确性,C语言有很多陷阱。如果我没记错,曾经有一个关于混合const / not const的嵌套指针的问题是无法正确检测的。你的情况主要是由于有损语法,因此gcc扩展很可能不会成为标准。实际上,你需要将const应用于int [3](这也是一些衍生语言使用的原因之一)。 - too honest for this site

4

OP描述了一个函数func(),其签名如下。

void func(size_t n, const int (*arr)[n])

OP想要调用它通过传递不同的数组

#define SZ(a) (sizeof(a)/sizeof(a[0]))

int array1[3];
func(SZ(array1), &array1);  // problem

const int array2[3] = {1, 2, 3};
func(SZ(array2), &array2);

如何将const正确应用于作为参数传递的数组指针?使用C11,使用_Generic进行必要的转换。只有当输入是可接受的非const类型时,才会发生转换,从而保持类型安全性。这就是“如何”做到这一点。OP可能认为它很“臃肿”,因为它类似于this。这种方法简化了宏/函数调用,只需要一个参数。
void func(size_t n, const int (*arr)[n]) {
  printf("sz:%zu (*arr)[0]:%d\n", n, (*arr)[0]);
}

#define funcCC(x) func(sizeof(*x)/sizeof((*x)[0]), \
  _Generic(x, \
  const int(*)[sizeof(*x)/sizeof((*x)[0])] : x, \
        int(*)[sizeof(*x)/sizeof((*x)[0])] : (const int(*)[sizeof(*x)/sizeof((*x)[0])])x \
  ))

int main(void) {
  #define SZ(a) (sizeof(a)/sizeof(a[0]))

  int array1[3];
  array1[0] = 42;
  // func(SZ(array1), &array1);

  const int array2[4] = {1, 2, 3, 4};
  func(SZ(array2), &array2);

  // Notice only 1 parameter to the macro/function call
  funcCC(&array1);  
  funcCC(&array2);

  return 0;
}

输出

sz:4 (*arr)[0]:1
sz:3 (*arr)[0]:42
sz:4 (*arr)[0]:1

或者代码可以使用

#define funcCC2(x) func(sizeof(x)/sizeof((x)[0]), \
    _Generic(&x, \
    const int(*)[sizeof(x)/sizeof((x)[0])] : &x, \
          int(*)[sizeof(x)/sizeof((x)[0])] : (const int(*)[sizeof(x)/sizeof((x)[0])])&x \
    ))

funcCC2(array1);
funcCC2(array2);

这大致上是我想出来的同样的技巧,见注释。不确定使用这样的宏是否是一个好主意...你会增加很多复杂性,可能会弊大于利。你必须无论如何记录宏并说“为了调用func,请调用funcCC”。所以你最好记录缺少const正确性的原始函数,并说“嘿,我保证不会触及传递的内容”。 - Lundin
@Lundin 在我完成的许多编程任务中,我不认为这是“非常复杂的”。要更好地评估(过度)复杂性,最好看到更多整体图片和竞争选择。这确实保持了类型安全,这是一个明确的目标。复杂性不是一个发布的目标,但这确实简化了调用函数。肯定比 func(3, &array); 好。无论如何 - 有趣的问题 - 探索 C 的新领域。 - chux - Reinstate Monica
@Lundin 顺便说一句:这样一个评价很高且有趣的问题应该在接受答案之前多给一些时间。我相信如果再多等一会儿,可能会出现其他有思想的答案。 - chux - Reinstate Monica

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