6

数日前、メモリ内の線形ベクトルの 2 次元ビューを提供するために、ポインターから型ポインター、型の配列への変換が広範囲に使用されているコードに出くわしました。わかりやすくするために、このような手法の簡単な例を以下に示します。

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

void print_matrix(const unsigned int nrows, const unsigned int ncols, double (*A)[ncols]) {  
  // Here I can access memory using A[ii][jj]
  // instead of A[ii*ncols + jj]
  for(int ii = 0; ii < nrows; ii++) {
    for(int jj = 0; jj < ncols; jj++)
      printf("%4.4g",A[ii][jj]);
    printf("\n");
  }
}

int main() {

  const unsigned int nrows = 10;
  const unsigned int ncols = 20;

  // Here I allocate a portion of memory to which I could access
  // using linear indexing, i.e. A[ii]
  double * A = NULL;
  A = malloc(sizeof(double)*nrows*ncols);

  for (int ii = 0; ii < ncols*nrows; ii++)
    A[ii] = ii;

  print_matrix(nrows,ncols,A);
  printf("\n");
  print_matrix(ncols,nrows,A);

  free(A);
  return 0;
}

type へポインターは type の配列へのポインターと互換性がないため、このキャストに関連するリスクがあるかどうか、またはこのキャストがどのプラットフォームでも意図したとおりに機能すると想定できるかどうかを尋ねたいと思います。

4

4 に答える 4

2

更新取り消し線部分は本当ですが、無関係です。

私がコメントに投稿したように、問題は本当に 2 次元配列で、サブ配列 (行) に内部パディングが含まれているかどうかです。標準では配列が連続していると定義されているため、各行内にパディングはありません。また、外側の配列はパディングを導入しません。実際、C 標準を調べてみると、配列のコンテキストでパディングについて言及されていないので、「連続」とは、多次元配列内のサブ配列の最後にパディングがないことを意味すると解釈します。sizeof(array) / sizeof(array[0])は配列内の要素数を返すことが保証されているため、そのようなパディングはあり得ません。

つまり、nrows行とncols列の多次元配列のレイアウトは、 の 1 次元配列のレイアウトと同じでなければなりませんnrows * ncols。したがって、互換性のない型エラーを回避するには、次のことができます

void *A = malloc(sizeof(double[nrows][ncols]));
// check for NULL

double *T = A;
for (size_t i=0; i<nrows*ncols; i++)
     T[i] = 0;

に渡しprint_arrayます。これにより、ポインターのエイリアシングの潜在的な落とし穴を回避できます。異なる型のポインターは、少なくとも 1 つの型void*が 、char*またはでない限り、同じ配列を指すことはできませんunsigned char*

于 2012-10-12T16:24:32.063 に答える
1

多次元配列のメモリ レイアウトは、要素の総数が同じ 1 次元配列と同じT arr[M][N]であることが保証されています。配列は連続しているため (6.2.5p20) 、配列内の要素数を返すことが保証されているため (6.5.3.4p7)、レイアウトは同じです。T arr[M * N]sizeof array / sizeof array[0]

ただし、型へのポインターを型の配列へのポインターにキャストすること、またはその逆が安全であるということにはなりません。まず、アラインメントが問題です。基本アラインメントを持つ型の配列は基本アラインメントも (6.2.8p2 により) 持つ必要がありますが、アラインメントが同じであるとは限りません。配列には基本型のオブジェクトが含まれているため、配列型のアラインメントは、少なくとも基本オブジェクト型のアラインメントと同じくらい厳密でなければなりませんが、より厳密にすることもできます (そのようなケースは今まで見たことがありません)。ただし、これは割り当てられたメモリには関係ありません。malloc基本アラインメント (7.22.3p1) に適切に割り当てられたポインターを返すことが保証されています。これは、自動または静的メモリへのポインターを配列ポインターに安全にキャストできないことを意味しますが、逆は許可されます。

int a[100];
void f() {
    int b[100];
    static int c[100];
    int *d = malloc(sizeof int[100]);
    int (*p)[10] = (int (*)[10]) a;  // possibly incorrectly aligned
    int (*q)[10] = (int (*)[10]) b;  // possibly incorrectly aligned
    int (*r)[10] = (int (*)[10]) c;  // possibly incorrectly aligned
    int (*s)[10] = (int (*)[10]) d;  // OK
}

int A[10][10];
void g() {
    int B[10][10];
    static int C[10][10];
    int (*D)[10] = (int (*)[10]) malloc(sizeof int[10][10]);
    int *p = (int *) A;  // OK
    int *q = (int *) B;  // OK
    int *r = (int *) C;  // OK
    int *s = (int *) D;  // OK
}

次に、キャスト規則 (6.3.2.3p7) がこの使用法をカバーしていないため、配列型と非配列型の間のキャストが実際に正しい場所へのポインターになることは保証されません。ただし、これが正しい場所へのポインター以外のものになる可能性はほとんどなく、キャスト ビアにchar *セマンティクスが保証されています。配列型へのポインターから基本型へのポインターに移行する場合は、ポインターを間接的に指定することをお勧めします。

void f(int (*p)[10]) {
    int *q = *p;                            // OK
    assert((int (*)[10]) q == p);           // not guaranteed
    assert((int (*)[10]) (char *) q == p);  // OK
}

配列添字のセマンティクスは何ですか? よく知られているように、[]演算は加算と間接参照のための構文糖衣にすぎないため、セマンティクスは+演算子のセマンティクスです。6.5.6p8 で説明されているように、ポインター オペランドは、結果が配列内または末尾を少し過ぎたところに収まる十分な大きさの配列のメンバーを指している必要があります。これは、両方向のキャストの問題です。配列型へのポインターにキャストする場合、その場所には多次元配列が存在しないため、追加は無効です。また、基本型へのポインターにキャストする場合、その位置にある配列はバインドされた内部配列のサイズのみを持ちます。

int a[100];
((int (*)[10]) a) + 3;    // invalid - no int[10][N] array

int b[10][10];
(*b) + 3;          // OK
(*b) + 23;         // invalid - out of bounds of int[10] array

ここから、理論だけでなく、一般的な実装に関する実際の問題が見え始めます。オプティマイザーは、未定義の動作が発生しないと仮定する権利があるため、基本オブジェクト ポインターを介して多次元配列にアクセスする場合、最初の内部配列内の要素以外の要素にエイリアスを設定しないと想定できます。

int a[10][10];
void f(int n) {
    for (int i = 0; i < n; ++i)
        (*a)[i] = 2 * a[2][3];
}

オプティマイザーは、へのアクセスがa[2][3]エイリアスではないと想定(*a)[i]し、ループの外に持ち上げることができます。

int a[10][10];
void f_optimised(int n) {
    int intermediate_result = 2 * a[2][3];
    for (int i = 0; i < n; ++i)
        (*a)[i] = intermediate_result;
}

fが で呼び出された場合、これはもちろん予期しない結果をもたらしますn = 50

最後に、これが割り当てられたメモリに当てはまるかどうかを尋ねる価値があります。7.22.3p1 は、malloc"によって返されるポインタは、基本的なアラインメント要件を持つ任意のタイプのオブジェクトへのポインタに割り当てられ、割り当てられた空間内のそのようなオブジェクトまたはそのようなオブジェクトの配列にアクセスするために使用される可能性があることを指定します"; 返されたポインターを別のオブジェクト型にさらにキャストすることについては何もないため、結論として、割り当てられたメモリの型は、返されたポインターがキャストされる最初のポインター型によって固定されます。にキャストした場合、さらに にキャストすることはできません。また、 にキャストした場合は、最初の要素にアクセスするためだけに使用できます。voiddouble *double (*)[n]double (*)[n]double *n

そのため、絶対に安全にしたい場合は、同じ基本型であっても、ポインターと配列型へのポインターの間でキャストしないでください。レイアウトが同じであるという事実は、ポインターmemcpyを介したその他のアクセスを除いては関係ありません。char

于 2012-10-12T22:17:48.243 に答える
1

C 標準では、オブジェクト (または不完全) 型へのポインターを別のオブジェクト (または不完全) 型へのポインターに変換できます。

ただし、いくつかの注意事項があります。

  • 結果のポインターが正しく配置されていない場合、動作は未定義です。この場合、標準はそれを保証しません。現実には、ありそうもないですけどね。

  • 標準は、結果のポインターの有効な使用法を 1 つだけ述べています。それは、元のポインター型に変換することです。その場合、標準は後者 (元のポインター型に変換された結果のポインター) が元のポインターと等しいことを保証します。結果のポインターを他の目的に使用することは、標準ではカバーされていません。

  • 標準では、このような変換を実行するときに明示的なキャストが必要print_matrixです。これは、投稿したコードの関数呼び出しにはありません。

したがって、標準の文字によると、コード サンプルでの使用法はその範囲外です。ただし、実際には、これはおそらくほとんどのプラットフォームで問題なく機能します (コンパイラが許可していると仮定します)。

于 2012-10-12T16:40:32.707 に答える
0

ここで私が最初に考えたのは、C は 2D 配列を作成するときに実際にその実装を使用するということです。つまり、メモリを線形に拡張します。

[11, 12, 13, 14, 15, 21, 22, 23, 24, 25....] // This is known as ROW-MAJOR form

コードでの割り当て方法

A = malloc(rows*columns);

そのため、A は double へのポインターであり、"inner-C" は実際に A[][] を double へのポインターに変換するため、これを行っても害はないと思います (注: ポインターへのポインターには当てはまりません! *)、だから違いはありません。

* A = malloc ( rows ); for_each_Ai ( Ai = malloc (columns) );

^ すべてのコードの疑似コードは明らかに

プラットフォームに依存しない部分に関しては、そのコードは問題ないはずです。ただし、他の卑劣なポインターも実行している場合は、エンディアンに注意してください

于 2012-10-12T14:52:39.203 に答える