2

2D配列の内容をではなくで受信する必要がある理由がわかりb[][3]ません **b。また、2D配列の値で呼び出すにはどうすればよいですか?また、2D配列のアドレスは次のarr内容にarr等しい*arrです&arr[0][0]。すべてのアドレスは同じです。私はそれをはっきりと視覚化することができません。多次元配列が実際にどのように格納されているかを誰かに説明してもらえますか。「絵の役に立つリンクは歓迎されるでしょう」。

#include "hfile.h" // contains all needed H files

void caller(int b[][3])  // why can't we write **b?
{
    int k=100;
    printf("\n****Caller:****\n");

    for(int i=0;i<3;i++)
    {
        for(int j=0;j<3;j++)
        {
            b[i][j]=k++;
            printf("\t %d",b[i][j]);
        }
        printf("\n");
    }
}

int main()
{
    int arr[3][3]={1,2,3,4,5,6,7,8,9}; // original containts

    caller(arr);              // Called caller function passing containts of "arr"

    printf("\n****Orignal****\n");
    for(int i=0;i<3;i++)
    {
        for(int j=0;j<3;j++)
            printf("\t %d",arr[i][j]);           

        printf("\n");
    }
    return 0;
}
4

3 に答える 3

5

多次元配列を宣言する場合:

int b[M][N];

ストレージは隣接しています。したがって、()などの要素にアクセスするx = b[i][j];と、コンパイラはこれと同等のコードを生成します。

int *c = (int *)b;  // Treat as a 1D array
int  k = (i*N + j); // Offset into 1D array
x = c[k];

ポインタからポインタを介して要素にアクセスすると、コンパイラはディメンションを認識せず、次のようなコードを生成します。

int *t = b[i];  // Follow first pointer (produces another pointer)
x = t[j];       // Follow second pointer

つまり、単にポインタをたどります。

これらは完全に互換性がないため、コンパイラーは、ポインターからポインターを取得する関数に真の2D配列を渡すことを防ぎます。

于 2012-02-12T11:16:47.050 に答える
1

アスキーアートルール!

2D配列を図で見てみましょう。short配列が2バイト整数であり、アドレスも便利な2バイトであると仮定しましょう。必要に応じて、これはZilog Z80チップにすることもできますが、これは数を少なくするためだけのものです。

short A[3][3];

+---------+---------+---------+
| A[0][0] | A[0][1] | A[0][2] |
+---------+---------+---------+
| A[1][0] | A[1][1] | A[1][2] |
+---------+---------+---------+
| A[2][0] | A[2][1] | A[2][2] |
+---------+---------+---------+

アドレスを想定しましょう:A = 0x4000。したがってshort *、配列の要素のアドレスは次のとおりです。

&A[0][0] = 0x4000;
&A[0][1] = 0x4002;
&A[0][2] = 0x4004;
&A[1][0] = 0x4006;
&A[1][1] = 0x4008;
&A[1][2] = 0x400A;
&A[2][0] = 0x400C;
&A[2][1] = 0x400E;
&A[2][2] = 0x4010;

ここで、次のように記述できることにも注意してください。

&A[0]    = 0x4000;
&A[1]    = 0x4006;
&A[2]    = 0x400C;

これらのポインタのタイプは、'のarray[3]へのポインタshort'、またはshort (*A)[3]

次のように書くこともできます。

&A       = 0x4000;

このタイプは、'のarray[3] [3]へのポインタshort、またはshort (*A)[3][3]

このコードが示すように、主な違いの1つはオブジェクトのサイズにあります。

#include <stdio.h>
#include <inttypes.h>

static void print_address(const char *tag, uintptr_t address, size_t size);

int main(void)
{
    char  buffer[32];
    short A[3][3] = { { 0, 1, 2 }, { 3, 4, 5 }, { 6, 7, 8 } };
    int i, j;

    print_address("A",  (uintptr_t)A,  sizeof(A));
    print_address("&A", (uintptr_t)&A, sizeof(*(&A)));

    for (i = 0; i < 3; i++)
    {
        for (j = 0; j < 3; j++)
        {
            sprintf(buffer, "&A[%d][%d]", i, j);
            print_address(buffer, (uintptr_t)&A[i][j], sizeof(*(&A[i][j])));
        }
    }

    for (i = 0; i < 3; i++)
    {
        sprintf(buffer, "&A[%d]", i);
        print_address(buffer, (uintptr_t)&A[i], sizeof(*(&A[i])));
    }

    putchar('\n');
    for (i = 0; i < 3; i++)
    {
        for (j = 0; j < 3; j++)
        {
            printf("  A[%d][%d] = %d", i, j, A[i][j]);
        }
        putchar('\n');
    }

    return 0;
}

static void print_address(const char *tag, uintptr_t address, size_t size)
{
    printf("%-8s  = 0x%.4" PRIXPTR " (size %zu)\n", tag, address & 0xFFFF, size);
}

このプログラムは、関数のマスキング操作で16ビットアドレスを偽造しprint_address()ます。

MacOS X 10.7.2(GCC'i686-apple-darwin11-llvm-gcc-4.2(GCC)4.2.1(Apple Inc.ビルド5658に基づく)(LLVMビルド2335.15.00)で64ビットモードでコンパイルされた場合の出力)')、 だった:

A         = 0xD5C0 (size 18)
&A        = 0xD5C0 (size 18)
&A[0][0]  = 0xD5C0 (size 2)
&A[0][1]  = 0xD5C2 (size 2)
&A[0][2]  = 0xD5C4 (size 2)
&A[1][0]  = 0xD5C6 (size 2)
&A[1][1]  = 0xD5C8 (size 2)
&A[1][2]  = 0xD5CA (size 2)
&A[2][0]  = 0xD5CC (size 2)
&A[2][1]  = 0xD5CE (size 2)
&A[2][2]  = 0xD5D0 (size 2)
&A[0]     = 0xD5C0 (size 6)
&A[1]     = 0xD5C6 (size 6)
&A[2]     = 0xD5CC (size 6)

  A[0][0] = 0  A[0][1] = 1  A[0][2] = 2
  A[1][0] = 3  A[1][1] = 4  A[1][2] = 5
  A[2][0] = 6  A[2][1] = 7  A[2][2] = 8

32ビットモードでマスキング操作なしでバリアントをコンパイルし、次の出力を取得しました。

A         = 0xC00E06D0 (size 18)
&A        = 0xC00E06D0 (size 18)
&A[0][0]  = 0xC00E06D0 (size 2)
&A[0][1]  = 0xC00E06D2 (size 2)
&A[0][2]  = 0xC00E06D4 (size 2)
&A[1][0]  = 0xC00E06D6 (size 2)
&A[1][1]  = 0xC00E06D8 (size 2)
&A[1][2]  = 0xC00E06DA (size 2)
&A[2][0]  = 0xC00E06DC (size 2)
&A[2][1]  = 0xC00E06DE (size 2)
&A[2][2]  = 0xC00E06E0 (size 2)
&A[0]     = 0xC00E06D0 (size 6)
&A[1]     = 0xC00E06D6 (size 6)
&A[2]     = 0xC00E06DC (size 6)

  A[0][0] = 0  A[0][1] = 1  A[0][2] = 2
  A[1][0] = 3  A[1][1] = 4  A[1][2] = 5
  A[2][0] = 6  A[2][1] = 7  A[2][2] = 8

また、64ビットモードでは、バリアントからの出力は次のとおりです。

A         = 0x7FFF65BB15C0 (size 18)
&A        = 0x7FFF65BB15C0 (size 18)
&A[0][0]  = 0x7FFF65BB15C0 (size 2)
&A[0][1]  = 0x7FFF65BB15C2 (size 2)
&A[0][2]  = 0x7FFF65BB15C4 (size 2)
&A[1][0]  = 0x7FFF65BB15C6 (size 2)
&A[1][1]  = 0x7FFF65BB15C8 (size 2)
&A[1][2]  = 0x7FFF65BB15CA (size 2)
&A[2][0]  = 0x7FFF65BB15CC (size 2)
&A[2][1]  = 0x7FFF65BB15CE (size 2)
&A[2][2]  = 0x7FFF65BB15D0 (size 2)
&A[0]     = 0x7FFF65BB15C0 (size 6)
&A[1]     = 0x7FFF65BB15C6 (size 6)
&A[2]     = 0x7FFF65BB15CC (size 6)

  A[0][0] = 0  A[0][1] = 1  A[0][2] = 2
  A[1][0] = 3  A[1][1] = 4  A[1][2] = 5
  A[2][0] = 6  A[2][1] = 7  A[2][2] = 8

32ビットと64ビットのアドレスバージョンには多くのノイズがあるため、「疑似16ビット」アドレスバージョンを維持できます。

のアドレスがとA[0][0]のアドレスと同じであるが、ポイントされているオブジェクトのサイズが異なることに 注意してください。単一の(短い)整数を指します。3つの(短い)整数の配列を指します。3x3(短い)整数の配列を指します。A[0]A&A[0][0]&A[0]&A

次に、どのように機能するかを確認する必要がありますshort **。動作はまったく異なります。これは、前の例に関連しているが異なるテストコードです。

#include <stdio.h>
#include <inttypes.h>

static void print_address(const char *tag, uintptr_t address, size_t size);

int main(void)
{
    char  buffer[32];
    short t[3] = { 99, 98, 97 };
    short u[3] = { 88, 87, 86 };
    short v[3] = { 77, 76, 75 };
    short w[3] = { 66, 65, 64 };
    short x[3] = { 55, 54, 53 };
    short y[3] = { 44, 43, 42 };
    short z[3] = { 33, 32, 31 };
    short *a[3] = { t, v, y };
    short **p = a;
    int i, j;

    print_address("t",  (uintptr_t)t,  sizeof(t));
    print_address("u",  (uintptr_t)u,  sizeof(u));
    print_address("v",  (uintptr_t)v,  sizeof(v));
    print_address("w",  (uintptr_t)w,  sizeof(w));
    print_address("x",  (uintptr_t)x,  sizeof(x));
    print_address("y",  (uintptr_t)y,  sizeof(y));
    print_address("z",  (uintptr_t)z,  sizeof(z));

    print_address("a",  (uintptr_t)a,  sizeof(a));
    print_address("&a", (uintptr_t)&a, sizeof(*(&a)));

    for (i = 0; i < 3; i++)
    {
        for (j = 0; j < 3; j++)
        {
            sprintf(buffer, "&a[%d][%d]", i, j);
            print_address(buffer, (uintptr_t)&a[i][j], sizeof(*(&a[i][j])));
        }
    }

    for (i = 0; i < 3; i++)
    {
        sprintf(buffer, "&a[%d]", i);
        print_address(buffer, (uintptr_t)&a[i], sizeof(*(&a[i])));
    }

    putchar('\n');
    for (i = 0; i < 3; i++)
    {
        for (j = 0; j < 3; j++)
        {
            printf("  a[%d][%d] = %d", i, j, a[i][j]);
        }
        putchar('\n');
    }

    putchar('\n');
    print_address("p",  (uintptr_t)p,  sizeof(*(p)));
    print_address("&p", (uintptr_t)&p, sizeof(*(&p)));

    for (i = 0; i < 3; i++)
    {
        for (j = 0; j < 3; j++)
        {
            sprintf(buffer, "&p[%d][%d]", i, j);
            print_address(buffer, (uintptr_t)&p[i][j], sizeof(*(&p[i][j])));
        }
    }

    for (i = 0; i < 3; i++)
    {
        sprintf(buffer, "&p[%d]", i);
        print_address(buffer, (uintptr_t)&p[i], sizeof(*(&p[i])));
    }

    putchar('\n');
    for (i = 0; i < 3; i++)
    {
        for (j = 0; j < 3; j++)
        {
            printf("  p[%d][%d] = %d", i, j, p[i][j]);
        }
        putchar('\n');
    }

    return 0;
}

static void print_address(const char *tag, uintptr_t address, size_t size)
{
    printf("%-8s  = 0x%.4" PRIXPTR " (size %zu)\n", tag, address & 0xFFFF, size);
}

これは2つの半分のプログラムです。半分は配列を分析しaます; もう1つは、ダブルポインタを分析しますp。これを理解するのに役立つASCIIアートを次に示します。

+------+------+------+                      +------+------+------+
|  99  |  98  |  97  |    t = 0x1000        |  88  |  87  |  86  |    u = 0x1100
+------+------+------+                      +------+------+------+

+------+------+------+                      +------+------+------+
|  77  |  76  |  75  |    v = 0x1200        |  66  |  65  |  64  |    w = 0x1300
+------+------+------+                      +------+------+------+

+------+------+------+                      +------+------+------+
|  55  |  54  |  53  |    x = 0x1400        |  44  |  43  |  42  |    y = 0x1500
+------+------+------+                      +------+------+------+

+------+------+------+
|  33  |  32  |  31  |    z = 0x1600
+------+------+------+

+--------+--------+--------+
| 0x1000 | 0x1200 | 0x1500 |    a = 0x2000
+--------+--------+--------+

+--------+
| 0x2000 |                      p = 0x3000
+--------+

配列は「任意の」場所に配置されていることに注意してくださいtz図では隣接していません。一部の配列は、たとえば別のファイルからのグローバル変数である可能性があり、他の配列は同じファイル内で関数の外部にある静的変数である可能性があり、他の配列は静的であるが関数に対してローカルである可能性があります。これらのローカル自動変数。pアドレスを含む変数がどのようになっているのかがわかります。アドレスは配列のアドレスですa。次に、アレイaには3つのアドレス、他の3つのアレイのアドレスが含まれます。

これは、人為的に分割されたプログラムの64ビットコンパイルからの出力です。16進アドレスの最後の4桁を除くすべてをマスクすることにより、16ビットアドレスをシミュレートします。

t         = 0x75DA (size 6)
u         = 0x75D4 (size 6)
v         = 0x75CE (size 6)
w         = 0x75C8 (size 6)
x         = 0x75C2 (size 6)
y         = 0x75BC (size 6)
z         = 0x75B6 (size 6)

これにより、未使用の変数に関する警告が防止され、3つの整数の7つの配列のアドレスも識別されます。

a         = 0x7598 (size 24)
&a        = 0x7598 (size 24)
&a[0][0]  = 0x75DA (size 2)
&a[0][1]  = 0x75DC (size 2)
&a[0][2]  = 0x75DE (size 2)
&a[1][0]  = 0x75CE (size 2)
&a[1][1]  = 0x75D0 (size 2)
&a[1][2]  = 0x75D2 (size 2)
&a[2][0]  = 0x75BC (size 2)
&a[2][1]  = 0x75BE (size 2)
&a[2][2]  = 0x75C0 (size 2)
&a[0]     = 0x7598 (size 8)
&a[1]     = 0x75A0 (size 8)
&a[2]     = 0x75A8 (size 8)

  a[0][0] = 99  a[0][1] = 98  a[0][2] = 97
  a[1][0] = 77  a[1][1] = 76  a[1][2] = 75
  a[2][0] = 44  a[2][1] = 43  a[2][2] = 42

重要な違いに注意してください。のサイズはa、3(64ビット)ポインターの配列であるため、18ではなく24バイトになりました。&a[n]それぞれがポインタであるため、のサイズは8バイトです。配列の場所にデータをロードする方法もまったく異なります。Cのソースコードは同じように見えるため、アセンブラを調べて確認する必要があります。

2D配列コードでは、のロード操作は次のようにA[i][j]計算します。

  • のバイトアドレスA
  • (3 * i + j) * sizeof(short)そのバイトアドレスに追加します
  • そのアドレスから2バイト整数をフェッチします。

ポインタコードの配列では、のロード操作は次のようにA[i][j]計算します。

  • のバイトアドレスa
  • i * sizeof(short *)そのバイトアドレスに追加します
  • その計算値からバイトアドレスをフェッチし、それを呼び出しますb
  • j * sizeof(short)に追加b
  • アドレスから2バイト整数をフェッチしますb

の出力pは多少異なります。特に、のアドレスは。のアドレスとは異なることに注意 pください。ただし、それを過ぎると、動作は基本的に同じになります。 p

p         = 0x7598 (size 8)
&p        = 0x7590 (size 8)
&p[0][0]  = 0x75DA (size 2)
&p[0][1]  = 0x75DC (size 2)
&p[0][2]  = 0x75DE (size 2)
&p[1][0]  = 0x75CE (size 2)
&p[1][1]  = 0x75D0 (size 2)
&p[1][2]  = 0x75D2 (size 2)
&p[2][0]  = 0x75BC (size 2)
&p[2][1]  = 0x75BE (size 2)
&p[2][2]  = 0x75C0 (size 2)
&p[0]     = 0x7598 (size 8)
&p[1]     = 0x75A0 (size 8)
&p[2]     = 0x75A8 (size 8)

  p[0][0] = 99  p[0][1] = 98  p[0][2] = 97
  p[1][0] = 77  p[1][1] = 76  p[1][2] = 75
  p[2][0] = 44  p[2][1] = 43  p[2][2] = 42

これらはすべて、単一の(メイン)機能に含まれていました。さまざまなポインターを関数に渡し、それらのポインターの背後にある配列にアクセスするときは、独自の並列実験を行う必要があります。

于 2012-02-12T13:08:28.000 に答える
0
void caller(int b[][3])  // why can't we write **b ?

を書くことはできますが、タイプと互換性がないと定義されているため、この関数にint **b渡すことはできません。arrarrint arr[3][3]int **

arrに変換できますが、に変換することはできint (*)[3]ませんint **。だからあなたはこれを書くことができます:

void caller(int (*b)[3])  //ok

実際int[3][3]には配列1の配列をint**定義し、ポインタへのポインタを定義します。(は)へのポインタに変換できるのと同じように、int[3][3]3の配列int(は)へのポインタに変換できます。int (*)[3]int[3]intint*

1.より正確には、3つのarray-of-3-intの配列を定義します。

于 2012-02-12T11:17:57.707 に答える