VLA的语法作为函数参数

时间:2015-08-11 03:30:03

标签: c arrays

我写了最简单的矩阵乘法代码来完成我对C99 VLA的理解。让我感到困惑的是当我在函数定义中在参数列表中声明指向VLA的指针时。

例如,在fill_matrix_randomly中,声明为m的参数double (*m)[n_cols]编译得很好。 double (*m)[*]是编译错误,因为[*]只能出现在声明中。 double (*m)[]也是一个错误,因为我无法访问不完整类型的数组。直到现在都没什么奇怪的,但是。 double (*m)[n_rows]编译得很好甚至可以正常运行? double (*m)[1]double (*m)[2]也有效,我在这里真的很困惑。帮助我减少困惑。

#include <stdio.h>
#include <stdlib.h>

static void fill_matrix_randomly(int, int, double (*)[*]);
static void print_matrix(int, int, double (*)[*]);
static void multiply_matrices(int, int, int, double (*restrict)[*],
double (*restrict)[*], double (*restrict)[*]);

int main(void) {
    const int a = 1, b = 3, c = 5;
    double m[a][c], m2[a][b], m3[b][c];
    fill_matrix_randomly(a, b, m2);
    fill_matrix_randomly(b, c, m3);
    multiply_matrices(a, b, c, m, m2, m3);
    print_matrix(a, b, m2);
    print_matrix(b, c, m3);
    print_matrix(a, c, m);
}

static void fill_matrix_randomly
(int n_rows, int n_cols, double (*m)[n_cols]) {
    for (int i = 0; i < n_rows; ++i) {
        for (int j = 0; j < n_cols; ++j) {
            m[i][j] = (double)rand() / RAND_MAX + 1;
        }
    }
}

static void print_matrix(int n_rows, int n_cols, double (*m)[n_cols]) {
    for (int i = 0; i < n_rows; ++i) {
        printf("[ ");
        for (int j = 0; j < n_cols; ++j) {
            printf("%.3f", m[i][j]);
            if (j != n_cols - 1) {
                printf(", ");
            } else {
                printf(" ]\n");
            }
        }
    }
    putchar('\n');
}

static void multiply_matrices
(int n, int m, int p, double (*restrict r)[p],
double (*restrict a)[m], double (*restrict b)[p]) {
    for (int i = 0; i < n; ++i) {
        for (int j = 0; j < p; ++j) {
            double sum = 0;
            for (int k = 0; k < m; ++k) {
                sum += a[i][k] * b[k][j];
            }
            r[i][j] = sum;
        }
    }
}

3 个答案:

答案 0 :(得分:3)

  

double(* m)[n_rows]编译得很好,甚至可以正常运行

如果使用类型double (*)[n_rows]声明了函数参数,但是传递了double (*)[n_columns]类型的参数,并且n_rowsn_columns不同,则行为未定义。

同样适用于double (*m)[1]double (*m)[2]变体。

参数传递要求参数类型与参数类型兼容。在指向数组的情况下,指针必须指向兼容的数组类型。在您的情况下,以下适用

  

6.7.5.2数组声明符

     

6 对于要兼容的两种数组类型,两者都应具有兼容的元素类型,并且如果两个数组说明符都存在,则   整数常量表达式,然后两个大小说明符都应该有   相同的常数值。如果在上下文中使用这两种数组类型   这要求它们兼容,如果是,则是未定义的行为   两个大小说明符评估为不相等的值。

显然,没有人可以合理地期望在编译时捕获这样的违规,因为编译器通常无法在编译时预测和实施运行时关系(VLA大小)。

在提交此违规(其本身足以触发UB)之后,您将通过对fill_matrix_randomly内的数组执行越界访问来继续提交另一个。

至于运行它...你知道double (*m)[n_rows]“运行良好的代码”的想法对我来说并不清楚。一个快速的实验表明,如果你幸运的话,那样对编译器说谎会导致数据填充不正确

http://coliru.stacked-crooked.com/a/6032864f2baa2eae

如果你没那么幸运就会崩溃

http://coliru.stacked-crooked.com/a/7ba1002e3150bd1c

答案 1 :(得分:1)

定义:

static void print_matrix(int n_rows, int n_cols, double (*m)[n_cols]) {
    …
}

表示m是指向数组的指针,其中数组的每个元素都有n_cols列。这完全是犹太人。

具有恒定行大小(double (*m)[1]double (*m)[2])的选项以及具有可变行大小的选项。

您可能还需要注意,代码中的这个次要变体也会编译并运行(因为没有随机数生成器的播种)会产生相同的答案:

#include <stdio.h>
#include <stdlib.h>

static void fill_matrix_randomly(int, int, double[*][*]);
static void print_matrix(int, int, double[*][*]);
static void multiply_matrices(int, int, int, double[*][*],
                              double[*][*], double[*][*]);

int main(void)
{
    const int a = 1, b = 3, c = 5;
    double m[a][c], m2[a][b], m3[b][c];
    fill_matrix_randomly(a, b, m2);
    fill_matrix_randomly(b, c, m3);
    multiply_matrices(a, b, c, m, m2, m3);
    print_matrix(a, b, m2);
    print_matrix(b, c, m3);
    print_matrix(a, c, m);
}

static void fill_matrix_randomly(int n_rows, int n_cols, double m[n_rows][n_cols])
{
    for (int i = 0; i < n_rows; ++i)
    {
        for (int j = 0; j < n_cols; ++j)
            m[i][j] = (double)rand() / RAND_MAX + 1;
    }
}

static void print_matrix(int n_rows, int n_cols, double m[n_rows][n_cols])
{
    for (int i = 0; i < n_rows; ++i)
    {
        printf("[ ");
        for (int j = 0; j < n_cols; ++j)
        {
            printf("%.3f", m[i][j]);
            if (j != n_cols - 1)
                printf(", ");
            else
                printf(" ]\n");
        }
    }
    putchar('\n');
}

static void multiply_matrices(int n, int m, int p, double r[n][p],
                              double a[n][m], double b[m][p])
{
    for (int i = 0; i < n; ++i)
    {
        for (int j = 0; j < p; ++j)
        {
            double sum = 0;
            for (int k = 0; k < m; ++k)
                sum += a[i][k] * b[k][j];
            r[i][j] = sum;
        }
    }
}

答案 2 :(得分:1)

您需要指定除第一个维度以外的所有尺寸。这是因为多维数组存储在连续的内存位置,因此需要维度来计算起始地址的实际偏移量,以找到第一维中的每个元素。

您的二维数组包含行和列(从高级别的意义上说)。它可能在内存中看起来像这样:

Row   0            | 1            | 2 ...
    +----+----+----|----+----+----|----+----+
    |    |    |    |    |    |    |    |    | ...
    +----+----+----|----+----+----|----+----+
Col   0    1    2  | 0    1    2  | 0    1

array[i][j]形式的数组访问等同于指针array的添加和取消引用:*(*(array + i) + j)

array + i用于将您偏移到行号i(并取消引用以提供保存在那里的数组),j用于向您提供更多j该行中的{1}}元素。很明显,为了获得正确的行偏移量,编译器必须知道每行的大小。

在上面的示例中,例如,编译器需要知道跳过3个内存位置/数据单元以从一行到另一行前进。

更完整的公式如下(请注意编译器会自动执行此扩展):

*((array + (i * #cols/row * sizeof(array elem))
         + (j * sizeof(array elem)))

很明显,提前需要知道的是sizeof(array elem)#cols/row

sizeof(array elem)是已知的,因为您指定了存储在多维数组中的基类型(在您的情况下为double)。但是#cols/row必须由程序员指定,以便编译器正确计算偏移量。

在所有工作代码示例中,您将为每行中包含的列赋予具体值。但是,在所有这些中,您将获得意外行为,因为特定大小的数组被视为不同大小的数组。通过指定不正确或不匹配的大小,您将调用未定义的行为,因为参数和提供的参数基本上存在类型不匹配。您使用x列声明了2D数组,但是将其作为包含y列的数组传递。

声明第二个维度为3的数组并将其传递给接受具有第二维2的数组的函数,如下所示,其中相同的内存以两种不同的方式解释:

Row   0            | 1            | 2 ...
    +----+----+----|----+----+----|----+----+
    |    |    |    |    |    |    |    |    | ...
    +----+----+----|----+----+----|----+----+
Col   0    1    2  | 0    1    2  | 0    1

Row   0       | 1       | 2       | 3 ...
    +----+----|----+----|----+----|----+----+
    |    |    |    |    |    |    |    |    | ...
    +----+----|----+----|----+----|----+----+
Col   0    1  | 0    1  | 0    1  | 0    1

您可以看到访问array[1][0]会给您两个完全不同的结果,第一个是预期结果,第二个是由于类型不匹配而呈现的结果。