Разница между ** переменной и переменной [ ] [ ]?

Я не понимаю, почему я должен получить содержимое двумерного массива в b[][3] и не в **b? Кроме того, как мы можем сделать вызов по значению для 2D-массивов? Также адрес двумерного массива arr равно содержанию arr равно *arr равно &arr[0][0]; все адреса одинаковы. Я не могу ясно представить это; Может кто-нибудь объяснить мне, как на самом деле хранится многомерный массив. Msgstr "Полезные ссылки с картинками приветствуются".

#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;
}

3 ответа

Решение

ASCII Art Rules!

Давайте посмотрим на 2D массив наглядно. Давайте предположим, что массив имеет 2 байта short целые числа, и чтобы адреса были удобными и 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;

Типы этих указателей 'указатель на массив [3] из short ', или же short (*A)[3],

Вы также можете написать:

&A       = 0x4000;

Тип этого 'указатель на массив [3][3] из short ', или же short (*A)[3][3],

Одно из ключевых отличий заключается в размерах объекта, поскольку этот код демонстрирует:

#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() функция.

Вывод при компиляции в 64-битном режиме на MacOS X 10.7.2 (GCC 'i686-apple-darwin11-llvm-gcc-4.2 (GCC) 4.2.1 (на основе Apple Inc., сборка 5658) (сборка LLVM, 2335.15.00))'), было:

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] совпадает с адресом A[0] а также A, но размеры объекта, на который указывают, разные. &A[0][0] указывает на одно (короткое) целое число; &A[0] указывает на массив из 3 (коротких) целых чисел; &A указывает на массив из 3x3 (коротких) целых чисел.

Теперь нам нужно посмотреть, как 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);
}

Это программа в две половины. Одна половина рассекает массив a; другой рассекает двойной указатель 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
+--------+

Обратите внимание, что массивы t.. z расположены в "произвольных" местах - не смежных на диаграмме. Возможно, что некоторые массивы будут глобальными переменными, например, из другого файла, а другие будут статическими переменными в том же файле, но вне функции, а другие будут статическими, но локальными для функции, а также эти локальные автоматические переменные. Вы можете увидеть, как p переменная, которая содержит адрес; адрес является адресом массива a, В свою очередь, массив a содержит 3 адреса, адреса 3 других массивов.

Это результат 64-битной компиляции программы, искусственно разделенной. Он имитирует 16-битные адреса, маскируя все, кроме последних 4 цифр шестнадцатеричного адреса.

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)

Это предотвращает предупреждения о неиспользуемых переменных, а также идентифицирует адреса 7 массивов из 3 целых чисел.

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 теперь 24 байта, а не 18, потому что это массив из 3 (64-битных) указателей. Размер &a[n] 8 байтов, потому что каждый является указателем. Способ загрузки данных в расположение массива также совершенно другой - вам придется взглянуть на ассемблер, чтобы увидеть это, потому что исходный код на Си выглядит одинаково.

В коде двумерного массива операция загрузки для 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

Все это было в единой (основной) функции. Вам нужно будет проводить свои собственные параллельные эксперименты, передавая различные указатели на функции и получая доступ к массивам за этими указателями.

Если вы объявляете многомерный массив:

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-массив в функцию, принимающую указатель на указатель.

void caller(int b[][3])  // why can't we write **b ?

Ты можешь написать int **b, но тогда вы не можете пройти arr к этой функции, потому что arr определяется как int 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] можно преобразовать в указатель на int (который int*).

1. Точнее, он определяет массив из 3 array-of-3-int.

Другие вопросы по тегам