Разница между ** переменной и переменной [ ] [ ]?
Я не понимаю, почему я должен получить содержимое двумерного массива в 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.