All streams
Search
Write a publication
Pull to refresh
2
0

User

Send message

При чем тут sizeof?

Если то – указатель, и это – указатель, да ещё и на один и тот же тип, то у них размеры должны быть одинаковые, я из этого исходил.

Как одинаковость или разность значений, возвращаемых sizeof, определяет тип переменной?

Не в эту сторону, а в обратную.

Не типы одинаковые, потому что sizeof одинаковый, а – раз одинаковые типы, то и sizeof у них должен быть одинаковый.

Вы вообще в курсе, что такое типы?

Конечно, это же основополагающее начало языка C.

Справочник, кстати, был отличный, перевод канонического стандарта, кажется, K&R, но я не уверен. Чай, не википедия какая. И про типы там всё было хорошо написано.

Возможно, и отличный, правда, из ваших слов следует, что, похоже, местами, неправильный.

Все ваши примеры отлично работают в пределах одного исходинка.

Согласен, в пределах одного работают.

Попробуйте передать ссылку на ваш "массив массивов" во внешнюю процедуру, определенную в прилинкованной библиотеке и посмотрите, как там будет осуществляться доступ.

Обязательно попробую, прямо сейчас и попробую, только функцию размещу вместо библиотеки в отдельно компилирующемся файле.

Ведь это не меняет сути?

Hint: подумайте о том, как та процедура узнает о размерности массива, по одному указателю.

Я прямо расскажу об этом этой самой процедуре путём описания соответствующего типа её параметра.

Ну и про типы почитайте что-нибудь, очень полезно, рекомендую.

Лучше я тогда сразу и продемонстрирую своё понимание типов, чтобы у вас не оставалось сомнений, что я про типы не только читал, но и как следует в них разобрался.

Функция, в числе прочего, будет принимать константный указатель на "4-мерный массив".

На самом деле, это будет константный указатель на массив массивов массивов массивов int'ов.

Две функции: одна читает, другая пишет.

Файл main.c:

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

#include "array.h"

void fun(int const value) {
	int array[10][10][10][10] = {0};

	printf("value: %i\n", value);
	puts("");

	printf("array[1][2][3][4] before: %i\n", get_array_element(1, 2, 3, 4, &array));
	array[1][2][3][4] = value;
	printf("array[1][2][3][4] after : %i\n", get_array_element(1, 2, 3, 4, &array));
	puts("");

	printf("array[4][3][2][1] before: %i\n", array[4][3][2][1]);
	set_array_element(4, 3, 2, 1, &array, value);
	printf("array[4][3][2][1] after : %i\n", array[4][3][2][1]);
}

int main(void) {
	fun(777);
	return EXIT_SUCCESS;
}

В функции fun заводится "4-мерный" массив, по 10 элементов в каждом "измерении", и инициализируется 0-ми.

Далее, сначала распечатывается значение из массива по индексам 1, 2, 3 и 4 с использованием функции get_array_element, скомпилированной в другом файле, затем это же место модифицируется путём прямого обращения к массиву, затем опять читается функцией get_array_element, чтобы убедиться, что функция "видит" массив, и видит его правильно.

После этого распечатывается значение из массива по индексам 4, 3, 2 и 1 с помощью прямого обращения к массиву, затем это же место модифицируется функцией set_array_element, и опять распечатывается с помощью прямого обращения к массиву, чтобы увидеть, что функция записала значение в массив, и записала его туда, куда нужно.

Файл array.c:

#include "array.h"

int get_array_element(
	size_t const x0,
	size_t const x1,
	size_t const x2,
	size_t const x3,
	int (*const array)[10][10][10][10]
)
{
	return (*array)[x0][x1][x2][x3];
}

void set_array_element(
	size_t const x0,
	size_t const x1,
	size_t const x2,
	size_t const x3,
	int (*const array)[10][10][10][10],
	int const value)
{
	(*array)[x0][x1][x2][x3] = value;
}

Реализации функций get_array_element и set_array_element.

В числе прочего, функции принимают указатель на массив.

Видите, как я передал информацию о размерностях массива по одному указателю?

Файл array.h:

#ifndef ARRAY_H__
#define ARRAY_H__

#include <stddef.h>

int get_array_element(size_t x0, size_t x1, size_t x2, size_t x3, int (*array)[10][10][10][10]);
void set_array_element(size_t x0, size_t x1, size_t x2, size_t x3, int (*array)[10][10][10][10], int value);

#endif // ARRAY_H__

Здесь – декларации функций get_array_element и set_array_element, чтобы из main.c их можно было правильно вызвать.

Поскольку godbolt теперь поддерживает cmake с возможностью компиляции множества файлов, предоставляя примитивную IDE, то – вот ссылка на микро-проект с этим кодом.

Теперь самое интересное: в ассемблерном коде для функции main видно, что функция fun вызывается, а не inline'ится:

main:
 sub    rsp,0x8
 mov    edi,0x309
 call   401170 <fun>
 xor    eax,eax
 add    rsp,0x8
 ret  

В ассемблерном коде функции fun также видно, что функции get_array_element и set_array_element вызываются, а не inline'ятся (компилятору здесь некуда деться, потому что компиляция – раздельная):

fun:
 push   rbx
 mov    edx,0x9c40
 mov    ebx,edi
 xor    esi,esi
 sub    rsp,0x9c40
 mov    rdi,rsp
 call   401050 <memset@plt>
 mov    esi,ebx
 mov    edi,0x402004
 xor    eax,eax
 call   401040 <printf@plt>
 mov    edi,0x402086
 call   401030 <puts@plt>
 mov    r8,rsp
 mov    ecx,0x4
 mov    edx,0x3
 mov    esi,0x2
 mov    edi,0x1
 call   401260 <get_array_element>
 mov    edi,0x40200f
 mov    esi,eax
 xor    eax,eax
 call   401040 <printf@plt>
 mov    r8,rsp
 mov    ecx,0x4
 mov    edx,0x3
 mov    esi,0x2
 mov    edi,0x1
 mov    DWORD PTR [rsp+0x1348],ebx
 call   401260 <get_array_element>
 mov    edi,0x40202d
 mov    esi,eax
 xor    eax,eax
 call   401040 <printf@plt>
 mov    edi,0x402086
 call   401030 <puts@plt>
 mov    esi,DWORD PTR [rsp+0x4384]
 mov    edi,0x40204b
 xor    eax,eax
 call   401040 <printf@plt>
 mov    r9d,ebx
 mov    r8,rsp
 mov    ecx,0x1
 mov    edx,0x2
 mov    esi,0x3
 mov    edi,0x4
 call   401280 <set_array_element>
 mov    esi,DWORD PTR [rsp+0x4384]
 mov    edi,0x402069
 xor    eax,eax
 call   401040 <printf@plt>
 add    rsp,0x9c40
 pop    rbx
 ret   

Долгожданный ассемблерный код функции get_array_element:

get_array_element:
 imul   rdi,rdi,0x3e8
 lea    rax,[rdx+rdx*4]
 imul   rsi,rsi,0x64
 lea    rax,[rdi+rax*2]
 add    rax,rsi
 add    rax,rcx
 mov    eax,DWORD PTR [r8+rax*4]
 ret 

Видите, сколько там обращений к памяти?
Я вижу только одно, в предпоследней инструкции.

Долгожданный ассемблерный код функции set_array_element:

set_array_element:
 imul   rdi,rdi,0x3e8
 lea    rax,[rdx+rdx*4]
 imul   rsi,rsi,0x64
 lea    rax,[rdi+rax*2]
 add    rax,rsi
 add    rax,rcx
 mov    DWORD PTR [r8+rax*4],r9d
 ret 

Видите, сколько в этой функции обращений к памяти?
Я опять вижу только одно, в предпоследней инструкции.

А, да, результат исполнения программы:

value: 777

array[1][2][3][4] before: 0
array[1][2][3][4] after : 777

array[4][3][2][1] before: 0
array[4][3][2][1] after : 777

Я нигде не ошибся в рассуждениях и в предоставленном коде?

Соглашусь с вами в том, что вы убедительно показали, что данный пример ничего не доказывает по сути обсуждения.

Это потому, что вы не связываете адресную арифметику с размером элемента.

Что касается адресной арифметики, то ей наплевать и на размер элемента тоже. Ей важны собственно адреса элементов.

Размер элемента является фундаментальнейшей и определяющей сущностью для адресной арифметики, для неё нет ничего важнее.

struct S {
	int i;
	char c;
};

struct S *
fun(struct S *p) {
	return p + 1;
}

Если посмотреть, во что транслируется fun, то мы увидим следующее (ссылка на godbolt):

fun:
        lea     rax, [rdi+8]
        ret

Откуда компилятор знает, что надо на 8 "шагнуть"?
Он совсем не знает, откуда вообще взялся этот указатель: на элемент массива он изначально указывает, на поле структуры или вообще, на отдельную переменную.

Более того, могут быть различные вызовы этой, одной и той же, функции с указателями на различные, в смысле предыдущего абзаца, объекты, а обработка-то должна быть какой-то одной: указатель не несёт в себе информации о том, в составе чего находится объект.

Поэтому на что адресной арифметике точно плевать, так это – на происхождение указателя.
Но ей точно не плевать на размер типа данных, на который указывает указатель.
Именно потому, что размер struct S составляет 8 байт, и только поэтому, численное значение указателя увеличивается на 8.
В состав чего входит объект, а также то, как он устроен внутри, имеют для адресной арифметики строго нулевое значение.
Для неё важен исключительно размер элемента и только он.

Вот пример (ссылка на godbolt):

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

struct S;

void fun0(struct S *p) {
	printf("p: %p\n", (void *)p);
#if 0
	printf("sizeof *p: %zu\n", sizeof *p);
	p++;
#endif
	printf("p: %p\n", (void *)p);
}

struct S {
	int i;
	char c;
};

void fun1(struct S *p) {
	printf("p: %p\n", (void *)p);
	printf("sizeof *p: %zu\n", sizeof *p);
	p++;
	printf("p: %p\n", (void *)p);
}

int main(void) {
	struct S s;

	fun1(&s);
        return EXIT_SUCCESS;
}

Если раскомментировать закомментированный код, то, несмотря на то, что функции fun0 и fun1 станут идентичными, сразу же возникнет ошибка:

<source>: In function 'fun0':
<source>:9:43: error: invalid application of 'sizeof' to incomplete type 'struct S'
    9 |         printf("sizeof *p: %zu\n", sizeof *p);
      |                                           ^
<source>:10:10: error: increment of pointer to an incomplete type 'struct S'
   10 |         p++;
      |          ^~

Во время определения функции fun0 структура struct S ещё не определена, она только объявлена, а это значит, кроме прочего, что неизвестен её размер.

Именно потому, что адресная арифметика не существует без размера элемента, ругань относится не только к операции sizeof, но и к операции инкремента указателя.

Но как только структура определена, и становится известен её размер, у компилятора, при компиляции функции fun1, претензии пропадают не только к операции sizeof, но и к инкременту указателя.

А что изменилось-то?
Стал известен размер struct S.
И этого оказалось совершенно достаточно.

То есть:

  1. Для работы адресной арифметики строго необходим размер типа, на который указывает указатель.

  2. Кроме размера типа, адресной арифметике больше ничего не нужно, будь то происхождение объекта или его внутренняя структура, – это никак не влияет на адресную арифметику.

Программы не эквивалентны. Вы в Си перешли для использования в массиве от типа char к int, а в Фортране оставили integer1. Поменяйте integer1 на integer, и получите длинный конвейер (для моего процессора конвейер имеет длину 16 байт).

Я Fortran'а не знаю, особенно современного, поэтому не догадался, что значит эта 1.
В программе на C я этот тип за' typedef'ил, и, получается, совсем не зря.

C-щный код с char'ом (ссылка на godbolt):

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

#define ELEMS(a) (sizeof (a) / sizeof *(a))

typedef char type;

void fun(size_t M, size_t N) {
	type (*a)[M][N] = malloc(sizeof *a);

	if (a) {
		type (*b)[M][N] = malloc(sizeof *b);

		if (b) {
			type (*c)[N][M] = malloc(sizeof *c);

			if (c) {
				for (size_t i = 0; i < ELEMS(*b); ++i) {
					for (size_t j = 0; j < ELEMS((*b)[i]); ++j) {
						(*b)[i][j] = rand();
					}
				}

				for (size_t i = 0; i < ELEMS(*c); ++i) {
					for (size_t j = 0; j < ELEMS((*c)[i]); ++j) {
						(*c)[i][j] = 3;
					}
				}

				for (size_t i = 0; i < ELEMS(*a); ++i) {
					for (size_t j = 0; j < ELEMS((*a)[i]); ++j) {
						(*a)[i][j] = (*b)[i][j] + (*c)[j][i];
					}
				}

				printf("a[0][0]: %i\n", (*a)[0][0]);
				free(c);
			}

			free(b);
		}

		free(a);
	}
}

int main(void) {
	//srand(time(NULL));
	srand(0);

        size_t M = 100. * rand() / RAND_MAX;
        size_t N = 100. * rand() / RAND_MAX;

	printf("M: %zu, N: %zu\n", M, N);

	fun(M, N);

        return EXIT_SUCCESS;
}

gcc:

.L14:
        movzx   eax, BYTE PTR [r10+rdi]
        movzx   r13d, BYTE PTR [r10+rbx*2]
        vmovd   xmm0, DWORD PTR [rcx+r11]
        sal     eax, 8
        or      eax, r13d
        movzx   r13d, BYTE PTR [r10+rbx]
        sal     eax, 8
        or      eax, r13d
        movzx   r13d, BYTE PTR [r10]
        add     r10, r9
        sal     eax, 8
        or      eax, r13d
        vmovd   xmm1, eax
        vpaddb  xmm0, xmm1, xmm0
        vmovd   DWORD PTR [rdx+r11], xmm0
        add     r11, 4
        cmp     r11, r8
        jne     .L14

Похоже, код идентичен с Fortran'овским, с точностью до используемых регистров.

clang:

Первичный цикл у clang'а очень большой
.LBB0_20:                               #   Parent Loop BB0_13 Depth=1
        vmovdqu ymm0, ymmword ptr [rsi]
        vmovdqu ymm1, ymmword ptr [rsi + 32]
        vmovdqu ymm2, ymmword ptr [rsi + 64]
        vmovdqu ymm3, ymmword ptr [rsi + 96]
        lea     rdx, [rsi + rbx]
        vpaddb  ymm0, ymm0, ymmword ptr [rcx + r10 - 992]
        vpaddb  ymm1, ymm1, ymmword ptr [rcx + r10 - 960]
        vpaddb  ymm2, ymm2, ymmword ptr [rcx + r10 - 928]
        vpaddb  ymm3, ymm3, ymmword ptr [rcx + r10 - 896]
        lea     rbp, [rdx + rbx]
        vmovdqu ymmword ptr [rdi + r10 - 992], ymm0
        vmovdqu ymmword ptr [rdi + r10 - 960], ymm1
        vmovdqu ymmword ptr [rdi + r10 - 928], ymm2
        vmovdqu ymm0, ymmword ptr [rsi + rbx]
        vmovdqu ymm1, ymmword ptr [rsi + rbx + 32]
        vmovdqu ymm2, ymmword ptr [rsi + rbx + 64]
        vmovdqu ymmword ptr [rdi + r10 - 896], ymm3
        vmovdqu ymm3, ymmword ptr [rsi + rbx + 96]
        add     rsi, rax
        vpaddb  ymm0, ymm0, ymmword ptr [rcx + r10 - 864]
        vpaddb  ymm1, ymm1, ymmword ptr [rcx + r10 - 832]
        vpaddb  ymm2, ymm2, ymmword ptr [rcx + r10 - 800]
        vpaddb  ymm3, ymm3, ymmword ptr [rcx + r10 - 768]
        vmovdqu ymmword ptr [rdi + r10 - 864], ymm0
        vmovdqu ymmword ptr [rdi + r10 - 832], ymm1
        vmovdqu ymmword ptr [rdi + r10 - 800], ymm2
        vmovdqu ymm0, ymmword ptr [rbx + rdx]
        vmovdqu ymm1, ymmword ptr [rbx + rdx + 32]
        vmovdqu ymm2, ymmword ptr [rbx + rdx + 64]
        vmovdqu ymmword ptr [rdi + r10 - 768], ymm3
        vmovdqu ymm3, ymmword ptr [rbx + rdx + 96]
        lea     rdx, [rbp + rbx]
        vpaddb  ymm0, ymm0, ymmword ptr [rcx + r10 - 736]
        vpaddb  ymm1, ymm1, ymmword ptr [rcx + r10 - 704]
        vpaddb  ymm2, ymm2, ymmword ptr [rcx + r10 - 672]
        vpaddb  ymm3, ymm3, ymmword ptr [rcx + r10 - 640]
        vmovdqu ymmword ptr [rdi + r10 - 736], ymm0
        vmovdqu ymmword ptr [rdi + r10 - 704], ymm1
        vmovdqu ymmword ptr [rdi + r10 - 672], ymm2
        vmovdqu ymm0, ymmword ptr [rbx + rbp]
        vmovdqu ymm1, ymmword ptr [rbx + rbp + 32]
        vmovdqu ymm2, ymmword ptr [rbx + rbp + 64]
        vmovdqu ymmword ptr [rdi + r10 - 640], ymm3
        vmovdqu ymm3, ymmword ptr [rbx + rbp + 96]
        lea     rbp, [rdx + rbx]
        vpaddb  ymm0, ymm0, ymmword ptr [rcx + r10 - 608]
        vpaddb  ymm1, ymm1, ymmword ptr [rcx + r10 - 576]
        vpaddb  ymm2, ymm2, ymmword ptr [rcx + r10 - 544]
        vpaddb  ymm3, ymm3, ymmword ptr [rcx + r10 - 512]
        vmovdqu ymmword ptr [rdi + r10 - 608], ymm0
        vmovdqu ymmword ptr [rdi + r10 - 576], ymm1
        vmovdqu ymmword ptr [rdi + r10 - 544], ymm2
        vmovdqu ymm0, ymmword ptr [rbx + rdx]
        vmovdqu ymm1, ymmword ptr [rbx + rdx + 32]
        vmovdqu ymm2, ymmword ptr [rbx + rdx + 64]
        vmovdqu ymmword ptr [rdi + r10 - 512], ymm3
        vmovdqu ymm3, ymmword ptr [rbx + rdx + 96]
        lea     rdx, [rbp + rbx]
        vpaddb  ymm0, ymm0, ymmword ptr [rcx + r10 - 480]
        vpaddb  ymm1, ymm1, ymmword ptr [rcx + r10 - 448]
        vpaddb  ymm2, ymm2, ymmword ptr [rcx + r10 - 416]
        vpaddb  ymm3, ymm3, ymmword ptr [rcx + r10 - 384]
        vmovdqu ymmword ptr [rdi + r10 - 480], ymm0
        vmovdqu ymmword ptr [rdi + r10 - 448], ymm1
        vmovdqu ymmword ptr [rdi + r10 - 416], ymm2
        vmovdqu ymm0, ymmword ptr [rbx + rbp]
        vmovdqu ymm1, ymmword ptr [rbx + rbp + 32]
        vmovdqu ymm2, ymmword ptr [rbx + rbp + 64]
        vmovdqu ymmword ptr [rdi + r10 - 384], ymm3
        vmovdqu ymm3, ymmword ptr [rbx + rbp + 96]
        lea     rbp, [rdx + rbx]
        vpaddb  ymm0, ymm0, ymmword ptr [rcx + r10 - 352]
        vpaddb  ymm1, ymm1, ymmword ptr [rcx + r10 - 320]
        vpaddb  ymm2, ymm2, ymmword ptr [rcx + r10 - 288]
        vpaddb  ymm3, ymm3, ymmword ptr [rcx + r10 - 256]
        vmovdqu ymmword ptr [rdi + r10 - 352], ymm0
        vmovdqu ymmword ptr [rdi + r10 - 320], ymm1
        vmovdqu ymmword ptr [rdi + r10 - 288], ymm2
        vmovdqu ymmword ptr [rdi + r10 - 256], ymm3
        vmovdqu ymm0, ymmword ptr [rbx + rdx]
        vmovdqu ymm1, ymmword ptr [rbx + rdx + 32]
        vmovdqu ymm2, ymmword ptr [rbx + rdx + 64]
        vmovdqu ymm3, ymmword ptr [rbx + rdx + 96]
        vpaddb  ymm0, ymm0, ymmword ptr [rcx + r10 - 224]
        vpaddb  ymm1, ymm1, ymmword ptr [rcx + r10 - 192]
        vpaddb  ymm2, ymm2, ymmword ptr [rcx + r10 - 160]
        vpaddb  ymm3, ymm3, ymmword ptr [rcx + r10 - 128]
        vmovdqu ymmword ptr [rdi + r10 - 224], ymm0
        vmovdqu ymmword ptr [rdi + r10 - 192], ymm1
        vmovdqu ymmword ptr [rdi + r10 - 160], ymm2
        vmovdqu ymmword ptr [rdi + r10 - 128], ymm3
        vmovdqu ymm0, ymmword ptr [rbx + rbp]
        vmovdqu ymm1, ymmword ptr [rbx + rbp + 32]
        vmovdqu ymm2, ymmword ptr [rbx + rbp + 64]
        vmovdqu ymm3, ymmword ptr [rbx + rbp + 96]
        vpaddb  ymm0, ymm0, ymmword ptr [rcx + r10 - 96]
        vpaddb  ymm1, ymm1, ymmword ptr [rcx + r10 - 64]
        vpaddb  ymm2, ymm2, ymmword ptr [rcx + r10 - 32]
        vpaddb  ymm3, ymm3, ymmword ptr [rcx + r10]
        vmovdqu ymmword ptr [rdi + r10 - 96], ymm0
        vmovdqu ymmword ptr [rdi + r10 - 64], ymm1
        vmovdqu ymmword ptr [rdi + r10 - 32], ymm2
        vmovdqu ymmword ptr [rdi + r10], ymm3
        add     r10, 1024
        add     r15, -8
        jne     .LBB0_20

Шаг равен 1024, не постеснялись они так развернуть.

Вторичный цикл сильно меньше:

.LBB0_23:                               #   Parent Loop BB0_13 Depth=1
        vmovdqu ymm0, ymmword ptr [r10]
        vmovdqu ymm1, ymmword ptr [r10 + 32]
        vmovdqu ymm2, ymmword ptr [r10 + 64]
        vmovdqu ymm3, ymmword ptr [r10 + 96]
        add     r10, rbx
        vpaddb  ymm0, ymm0, ymmword ptr [rsi + rbp - 96]
        vpaddb  ymm1, ymm1, ymmword ptr [rsi + rbp - 64]
        vpaddb  ymm2, ymm2, ymmword ptr [rsi + rbp - 32]
        vpaddb  ymm3, ymm3, ymmword ptr [rsi + rbp]
        vmovdqu ymmword ptr [rdx + rbp - 96], ymm0
        vmovdqu ymmword ptr [rdx + rbp - 64], ymm1
        vmovdqu ymmword ptr [rdx + rbp - 32], ymm2
        vmovdqu ymmword ptr [rdx + rbp], ymm3
        sub     rbp, -128
        cmp     rax, rbp
        jne     .LBB0_23

Шаг, насколько я понимаю, 128.

Есть и третичный, совсем маленький:

.LBB0_27:                               #   Parent Loop BB0_13 Depth=1
        vmovdqu xmm0, xmmword ptr [rsi]
        add     rsi, rax
        vpaddb  xmm0, xmm0, xmmword ptr [r9 + rdx]
        vmovdqu xmmword ptr [r14 + rdx], xmm0
        add     rdx, 16
        cmp     r8, rdx
        jne     .LBB0_27

Шаг – 16.

Не знаю, насколько это эффективно для относительно небольших массивов, но clang здесь отличлся.

icc (если я правильно нашёл место):

..B1.28:                        # Preds ..B1.28 ..B1.27
        mov       bl, BYTE PTR [rdi+rax*2]                      #34.22
        add       bl, BYTE PTR [r15+r11*2]                      #34.35
        mov       BYTE PTR [rsi+rax*2], bl                      #34.9
        mov       bl, BYTE PTR [1+rdi+rax*2]                    #34.22
        add       bl, BYTE PTR [rdx+r11*2]                      #34.35
        add       r11, r12                                      #33.6
        mov       BYTE PTR [1+rsi+rax*2], bl                    #34.9
        inc       rax                                           #33.6
        cmp       rax, r8                                       #33.6
        jb        ..B1.28       # Prob 63%                      #33.6

Похоже, icc не справился.

Итак, если взять GNU, то результат одинаковый, что для Fortran'а, что для C.

Опять получается так, что у Fortran'а нет преимуществ перед C в смысле быстродействия.

в 90-х у меня был бумажный справочник стандарта C и в нем было написано, что это должен быть массив ссылок.

Каждые 5 лет в IT сменяется эпоха.
За 30 лет сменилось 6 эпох.
Также тот справочник мог быть низкого качества.

С одномерным массивом всё понятно:

char x[10];

Тип переменной x тут -- указатель char*, с этим не будете спорить?

Ещё как буду (ссылка на godbolt):

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

int main(void) {
	char x[10];
	char *p;

	printf("sizeof x: %zu\n", sizeof x);
	printf("sizeof p: %zu\n", sizeof p);

        return EXIT_SUCCESS;
}

Результат:

sizeof x: 10
sizeof p: 8

Заметьте, все компиляторы единодушны.
Размеры указателя и массива x отличаются.

Это потому, что массив есть "смежно выделенное непустое множество объектов с определенным типом объекта-члена, называемым типом элемента", а никакой не указатель.

10 смежных char' ов – вот вам и sizeof, равный 10.

Пойдем далее, какой тип у x в этом коде:

char x[10][10];

Возьму на себя смелость утверждать, что это будет char**, то есть указатель на массив указателей.

Если применить определение из стандарта, то никакой это не указатель на указатель.

Лучше взять char x[10][20];, чтобы проще отличать было.

x – это 10 смежно выделенных элементов, каждый из которых есть char y[20];.
А y – (условный, конечно, y) – это 20 смежно выделенных элементов, каждый из которых есть char.

То, что существует неявное преобразование от массива к указателю на его первый элемент, не делает массив указателем.

Фактически, код:

char c=x[3][3];

эквивалентен коду:

char c=*(*(x+3)+3);

Да, эквивалентно, и поэтому можно написать так:

char c = 3[3[x]];

И ни один компилятор даже не пикнет (я не шучу).

Но это тоже никоим образом не превращает массив в указатель на его первый элемент.

То, что компилятор делает оптимизацию, превращая массив в фортрановский (да, массивы такого типа так называются), нарушает стандарты.

Вот странно.

Вроде и место в стандарте показал, где массив определяется, а до сих пор слышу, про какое-то нарушение.

Если я в другом месте (а лучше всего, в другом объектном файле или библиотеке) попытаюсь дважды разыменовать указатель char **x, то получу в лучшем случае SIGSEGV, в худшем -- рандомное значение.

Смотрите, а внаглую 10-кратно разыменовал 10-мерный массив (ссылка godbolt):

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

int main(void) {
	char x[1][2][3][4][5][6][7][8][9][10] = {{{{{{{{{{'!'}}}}}}}}}};

	printf("sizeof x: %zu\n", sizeof x);
	printf("**********x: %c\n", **********x);
        return EXIT_SUCCESS;
}

и ничего не упало ни в каком из компиляторов.
Никаких, понимаете ли, SIGSEGV:

sizeof x: 3628800
**********x: !

Оптимизацию специально отключил, чтобы в ассемблерном коде видно было, что в массив кладётся 33 ('!') при инициализации и потом читается при разыменовании, а также распечатал размер массива, чтобы было видно, что он и правда, 10-мерный.

Да, интересный способ вычислить факториал 10...

Но функция, получившая этот указатель, будет обязана разыменовывать указатели как положено, делая по нескольку лишних запросов в память, с этим ничего не поделаешь.

Массив – не указатель.

Видите, я следую стандарту, и у меня ничего не падает.
И никаких лишних обращений к памяти у меня нет.

Неужели вы продолжите утверждать, что массив массивов - это массив указателей?

Преимущества в плане качества кода у компиляторов Фортрана перед C нет, как, впрочем, нет и особого отставания.

Вот бы ещё и в статье бы было так сказано...

За счёт этого программа, написанная неопытным программистом на Фортране будет гораздо производительнее программы, написанной неопытным программистом на C++. Какой-нибудь физик перепишет алгоритм на Фортран почти 1-в-1 из статьи и получит результат за приемлемое время, не имея при этом вообще никакого представления о том, как матрицы размещены в памяти и какие инструкции выполняет процессор при их умножении.

Вот в это уже вполне верится.

К тому же, C, и особенно C++, – не для использования новичками.

Именно, в стандарте С написано, что многомерный массив -- это массив ссылок на одномерные массивы.

А почему я не могу обнаружить этого в стандарте?

Вот, где определяется, что такое массив (ссылка на draft C11):

6.2.5 Types

...

20 Any number of derived types can be constructed from the object and function types, as follows:
— An array type describes a contiguously allocated nonempty set of objects with a particular member object type, called the element type. The element type shall be complete whenever the array type is specified. Array types are characterized by their element type and by the number of elements in the array. An array type is said to be derived from its element type, and if its element type is T, the array type is sometimes called ‘‘array of T’’. The construction of an array type from an element type is called ‘‘array type derivation’’.

Всё.

Где здесь сказано, что массивы бывают многомерные, и, тем более, что тогда они должны быть массивами ссылок?

В C нет многомерных массивов.
Зато там есть массивы массивов.
Это можно продолжать рекурсивно.

У меня в изначальном примере не 5-мерный массив, а "5-мерный".
Это значит, что, на самом деле, там массив массивов массивов массивов массивов int'ов.

Перегрузка операторов в Фортране есть, ООП есть, по сравнению с C++ только шаблонов нет. Но можно писать код, общий относительно вариантов типа (real(4), real(8), real(16)). Небольшое расширение вполне себе реализуется.

Получается, это немного лучше, чем C-шный _Generic, но явно слабее чем C++. Но если этого хватает, то – и ладно.

В статье утверждалось, что Fortran значительно быстрее C/C++, но увидеть этого мне пока не удалось.
По эффективности я не вижу преимуществ Fortran'а перед C.

А теперь вернёмся к тому, о чём я писал с самого начала – к обработке двухмерного массива. Подставим в сложение c[n][m] вместо c[m][n]. И то же самое в фортране – вместо простого сложения массивов напишем цикл (наружный цикл по j, так как в фортране массивы хранятся по столбцам)

Да, давайте вернёмся (ссылка на godbolt):

program main

integer*1, allocatable :: a (:,:), b (:,:), c (:,:)
integer ::  m, n, mm, nn, i, j
real :: u  
   
call random_init (.true., .true.)

call random_number (u)
m = floor (u * 102)

call random_number (u)
n = floor (u * 205)

allocate (a (m,n), b (m, n), c (n, m))
 
do j=1, N
  do i=1, M
    call random_number (u)
    b (i, j) = floor (u * 100)
  end do
end do

nn = 3
c = nn

do j=1, N
  do i=1, M
    a (i,j) = b (i,j) + c (j,i)
  end do
end do

print *, m, n
print *, a
end program main

Программа слегка подрихтована, чтобы M и N всегда были одинаковыми и равными таковым в последующем коде на C, но чтобы выглядели как настоящие достаточно случайные.

gfortran:

.L17:
        movzx   eax, BYTE PTR [rdx+r11]
        movzx   r8d, BYTE PTR [rdx+rsi*2]
        vmovd   xmm0, DWORD PTR [r12+rcx]
        sal     eax, 8
        or      eax, r8d
        movzx   r8d, BYTE PTR [rdx+rsi]
        sal     eax, 8
        or      eax, r8d
        movzx   r8d, BYTE PTR [rdx]
        add     rdx, rbp
        sal     eax, 8
        or      eax, r8d
        vmovd   xmm4, eax
        vpaddb  xmm0, xmm4, xmm0
        vmovd   DWORD PTR [r10+rcx], xmm0
        add     rcx, 4
        cmp     rbx, rcx
        jne     .L17

Уже известная тройка инструкций и затем add rcx, 4.

В сообщении компилятора написано:

app/example.f90:28:7: optimized: loop vectorized using 4 byte vectors

И в этом отрывке, если проследить, то видно, что обращения, действительно, 4-х байтовые (DWORD PTR).

Это – не векторизация, компилятора только вид сделал, что векторизовал.

У flang-trunk код идентичен.

У ifort – простыня:

Простыню под спойлер убрал
..B1.52:                        # Preds ..B1.52 ..B1.51
        mov       r14, rsi                                      #29.25
        lea       r8, QWORD PTR [rsi+rsi*2]                     #29.23
        imul      r14, r10                                      #29.25
        lea       r13, QWORD PTR [rax+r14]                      #29.23
        movzx     r15d, BYTE PTR [r13]                          #29.23
        lea       r12, QWORD PTR [rsi+rsi*4]                    #29.23
        lea       rdi, QWORD PTR [rsi*8]                        #29.23
        sub       rdi, rsi                                      #29.23
        vmovd     xmm0, r15d                                    #29.23
        vpinsrb   xmm1, xmm0, BYTE PTR [rsi+r13], 1             #29.23
        vpinsrb   xmm2, xmm1, BYTE PTR [r13+rsi*2], 2           #29.23
        vpinsrb   xmm3, xmm2, BYTE PTR [r8+r13], 3              #29.23
        vpinsrb   xmm4, xmm3, BYTE PTR [r13+rsi*4], 4           #29.23
        vpinsrb   xmm5, xmm4, BYTE PTR [r12+r13], 5             #29.23
        vpinsrb   xmm6, xmm5, BYTE PTR [r13+r8*2], 6            #29.23
        vpinsrb   xmm7, xmm6, BYTE PTR [rdi+r13], 7             #29.23
        lea       rdi, QWORD PTR [rsi+rsi*8]                    #29.23
        vpinsrb   xmm8, xmm7, BYTE PTR [r13+rsi*8], 8           #29.23
        lea       rbx, QWORD PTR [rdi+rsi*2]                    #29.23
        vpinsrb   xmm9, xmm8, BYTE PTR [rdi+r13], 9             #29.23
        lea       r14, QWORD PTR [rdi+rsi*4]                    #29.23
        vpinsrb   xmm10, xmm9, BYTE PTR [r13+r12*2], 10         #29.23
        vpinsrb   xmm11, xmm10, BYTE PTR [rbx+r13], 11          #29.23
        mov       rbx, rsi                                      #29.23
        vpinsrb   xmm12, xmm11, BYTE PTR [r13+r8*4], 12         #29.23
        shl       rbx, 4                                        #29.23
        vpinsrb   xmm13, xmm12, BYTE PTR [r14+r13], 13          #29.23
        mov       r14, rbx                                      #29.23
        sub       r14, rsi                                      #29.23
        mov       r15, r14                                      #29.23
        sub       r15, rsi                                      #29.23
        vpinsrb   xmm14, xmm13, BYTE PTR [r15+r13], 14          #29.23
        movzx     r15d, BYTE PTR [rbx+r13]                      #29.23
        add       rbx, rsi                                      #29.23
        vpinsrb   xmm15, xmm14, BYTE PTR [r14+r13], 15          #29.23
        vmovd     xmm16, r15d                                   #29.23
        vpinsrb   xmm17, xmm16, BYTE PTR [rbx+r13], 17          #29.23
        imul      rbx, rsi, 19                                  #29.23
        vpinsrb   xmm18, xmm17, BYTE PTR [r13+rdi*2], 18        #29.23
        imul      rdi, rsi, 23                                  #29.23
        vpinsrb   xmm19, xmm18, BYTE PTR [rbx+r13], 19          #29.23
        imul      rbx, rsi, 22                                  #29.23
        vpinsrb   xmm20, xmm19, BYTE PTR [r13+r12*4], 20        #29.23
        imul      r12, rsi, 21                                  #29.23
        vpinsrb   xmm21, xmm20, BYTE PTR [r12+r13], 21          #29.23
        lea       r12, QWORD PTR [rsi*4]                        #29.23
        vpinsrb   xmm22, xmm21, BYTE PTR [rbx+r13], 22          #29.23
        mov       r15, rsi                                      #29.23
        imul      rbx, rsi, 26                                  #29.23
        vpinsrb   xmm23, xmm22, BYTE PTR [rdi+r13], 23          #29.23
        imul      rdi, rsi, 27                                  #29.23
        vpinsrb   xmm24, xmm23, BYTE PTR [r13+r8*8], 24         #29.23
        imul      r8, rsi, 25                                   #29.23
        vpinsrb   xmm25, xmm24, BYTE PTR [r8+r13], 25           #29.23
        neg       r12                                           #29.23
        imul      r8, rsi, 29                                   #29.23
        vpinsrb   xmm26, xmm25, BYTE PTR [rbx+r13], 26          #29.23
        vpinsrb   xmm27, xmm26, BYTE PTR [rdi+r13], 27          #29.23
        shl       r15, 5                                        #29.23
        add       r12, r15                                      #29.23
        sub       r15, rsi                                      #29.23
        mov       r14, r15                                      #29.23
        sub       r14, rsi                                      #29.23
        vpinsrb   xmm28, xmm27, BYTE PTR [r12+r13], 28          #29.23
        vpinsrb   xmm29, xmm28, BYTE PTR [r8+r13], 29           #29.23
        vpinsrb   xmm30, xmm29, BYTE PTR [r14+r13], 30          #29.23
        vpinsrb   xmm31, xmm30, BYTE PTR [r15+r13], 31          #29.23
        vinserti32x4 ymm0, ymm15, xmm31, 1                      #29.23
        vpaddb    ymm1, ymm0, YMMWORD PTR [rdx+r10]             #29.23
        vmovdqu   YMMWORD PTR [r10+rcx], ymm1                   #29.5
        add       r10, 32                                       #28.3
        cmp       r10, r9                                       #28.3
        jb        ..B1.52       # Prob 82%                      #28.3

Зато здесь шаг, как видно из инструкции addr 10, 32 , – 32, и обращения – 32-байтовые (YMMWORD PTR).

Да, это уже – векторизация, причём, довольно мощная.

Ассемблерный код в фортране не сильно изменился, там такая же команда paddb / vpaddb в чуть более сложном окружении. А сишный кодогенератор пошёл вразнос (я уже писал, что не могу проинтерпретировать увиденное).

Как видно, gfortran не справился, 4-байтный "вектор" – это не векторизация.

Теперь посмотрим C-шный код, максимально близкий к рассматриваемому Frotran'овскому (ссылка на godbolt):

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

#define ELEMS(a) (sizeof (a) / sizeof *(a))

typedef int type;

void fun(size_t M, size_t N) {
	type (*a)[M][N] = malloc(sizeof *a);

	if (a) {
		type (*b)[M][N] = malloc(sizeof *b);

		if (b) {
			type (*c)[N][M] = malloc(sizeof *c);

			if (c) {
				for (size_t i = 0; i < ELEMS(*b); ++i) {
					for (size_t j = 0; j < ELEMS((*b)[i]); ++j) {
						(*b)[i][j] = rand();
					}
				}

				for (size_t i = 0; i < ELEMS(*c); ++i) {
					for (size_t j = 0; j < ELEMS((*c)[i]); ++j) {
						(*c)[i][j] = 3;
					}
				}

				for (size_t i = 0; i < ELEMS(*a); ++i) {
					for (size_t j = 0; j < ELEMS((*a)[i]); ++j) {
						(*a)[i][j] = (*b)[i][j] + (*c)[j][i];
					}
				}

				printf("a[0][0]: %i\n", (*a)[0][0]);
				free(c);
			}

			free(b);
		}

		free(a);
	}
}

int main(void) {
	//srand(time(NULL));
	srand(0);

        size_t M = 100. * rand() / RAND_MAX;
        size_t N = 100. * rand() / RAND_MAX;

	printf("M: %zu, N: %zu\n", M, N);

	fun(M, N);

        return EXIT_SUCCESS;
}

gcc:

.L22:
        vmovd   xmm2, DWORD PTR [rax+rbx*2]
        vmovd   xmm3, DWORD PTR [rax]
        vpinsrd xmm0, xmm2, DWORD PTR [rax+rcx], 1
        vpinsrd xmm1, xmm3, DWORD PTR [rax+rbx], 1
        add     rax, rdi
        vpunpcklqdq     xmm0, xmm1, xmm0
        vpaddd  xmm0, xmm0, XMMWORD PTR [r11+r8]
        vmovdqu XMMWORD PTR [r10+r8], xmm0
        add     r8, 16
        cmp     r8, rsi
        jne     .L22

gcc справился, шаг равен 16, что также подтверждается 16-байтовыми обращениями (XMMWORD PTR).

clang:

.LBB0_36:                               #   Parent Loop BB0_31 Depth=1
        vmovdqu ymm0, ymmword ptr [rcx - 96]
        vmovdqu ymm1, ymmword ptr [rcx - 64]
        vmovdqu ymm2, ymmword ptr [rcx - 32]
        vmovdqu ymm3, ymmword ptr [rcx]
        vpaddd  ymm0, ymm0, ymmword ptr [rsi + 4*rdx - 224]
        vpaddd  ymm1, ymm1, ymmword ptr [rsi + 4*rdx - 192]
        vpaddd  ymm2, ymm2, ymmword ptr [rsi + 4*rdx - 160]
        vpaddd  ymm3, ymm3, ymmword ptr [rsi + 4*rdx - 128]
        vmovdqu ymmword ptr [r8 + 4*rdx - 224], ymm0
        vmovdqu ymmword ptr [r8 + 4*rdx - 192], ymm1
        vmovdqu ymmword ptr [r8 + 4*rdx - 160], ymm2
        vmovdqu ymmword ptr [r8 + 4*rdx - 128], ymm3
        vmovdqu ymm0, ymmword ptr [rcx + r9 - 96]
        vmovdqu ymm1, ymmword ptr [rcx + r9 - 64]
        vmovdqu ymm2, ymmword ptr [rcx + r9 - 32]
        vmovdqu ymm3, ymmword ptr [rcx + r9]
        vpaddd  ymm0, ymm0, ymmword ptr [rsi + 4*rdx - 96]
        vpaddd  ymm1, ymm1, ymmword ptr [rsi + 4*rdx - 64]
        vpaddd  ymm2, ymm2, ymmword ptr [rsi + 4*rdx - 32]
        vpaddd  ymm3, ymm3, ymmword ptr [rsi + 4*rdx]
        vmovdqu ymmword ptr [r8 + 4*rdx - 96], ymm0
        vmovdqu ymmword ptr [r8 + 4*rdx - 64], ymm1
        vmovdqu ymmword ptr [r8 + 4*rdx - 32], ymm2
        vmovdqu ymmword ptr [r8 + 4*rdx], ymm3
        add     rdx, 64

clang справился лучше всех: шаг 64 байта.

icc:

..B1.58:                        # Preds ..B1.58 ..B1.57
        mov       rcx, r13                                      #34.35
        imul      rcx, rdi                                      #34.35
        lea       rcx, QWORD PTR [r14+rcx*4]                    #34.35
        vmovd     xmm0, DWORD PTR [rcx+r9]                      #34.35
        vmovd     xmm1, DWORD PTR [rcx]                         #34.35
        vpinsrd   xmm17, xmm0, DWORD PTR [rdx+rcx], 1           #34.35
        vpinsrd   xmm16, xmm1, DWORD PTR [rcx+r13*8], 1         #34.35
        add       rcx, rbx                                      #34.35
        vpunpckldq xmm6, xmm16, xmm17                           #34.35
        vmovd     xmm2, DWORD PTR [rcx+r9]                      #34.35
        vmovd     xmm3, DWORD PTR [rcx]                         #34.35
        vpinsrd   xmm5, xmm2, DWORD PTR [rdx+rcx], 1            #34.35
        vpinsrd   xmm4, xmm3, DWORD PTR [rcx+r13*8], 1          #34.35
        vpunpckldq xmm7, xmm4, xmm5                             #34.35
        vinserti128 ymm8, ymm6, xmm7, 1                         #34.35
        vpaddd    ymm9, ymm8, YMMWORD PTR [r12+rdi*4]           #34.35
        vmovdqu   YMMWORD PTR [rsi+rdi*4], ymm9                 #34.9
        add       rdi, 8                                        #33.6
        cmp       rdi, rax                                      #33.6
        jb        ..B1.58       # Prob 82%                      #33.6

icc – хуже всех, но 8 – это, всё-таки, не 4.

Итак, что мы видим?

C ни в какой разнос не пошёл.
C-шный код векторизуется лучше, чем Fortran'овский.
C-шный код ещё и транслируется в более понятный ассемблер.

Выходит, нет у Fortran'а преимуществ перед C в смысле векторизуемости.

Отличный пример! Тут компилятор явно нарушает стандарт, используя массивы в "фортрановском стиле", например, интеловский компилятор так делает.

И какой пункт стандарта он нарушает?

Все компиляторы так делают.
Добавил MSVC (ссылка на godbolt).

clang:

fun:                                    # @fun
        imul    rax, rdi, 12096
        imul    rsi, rsi, 2016
        lea     rdi, [rdx + 8*rdx]
        lea     rcx, [rcx + 8*rcx]
        shl     rdi, 5
        add     rax, r9
        add     rsi, rax
        add     rdi, rsi
        lea     rax, [rdi + 4*rcx]
        mov     eax, dword ptr [rax + 4*r8]
        ret

Intel:

fun:
        lea       rax, QWORD PTR [rdx+rdx*8]                    #6.11
        imul      rdx, rdi, 12096                               #6.11
        shl       rcx, 2                                        #6.11
        mov       r10, rsi                                      #6.11
        shl       r10, 5                                        #6.11
        lea       r8, QWORD PTR [r9+r8*4]                       #6.11
        shl       rsi, 11                                       #6.11
        shl       rax, 5                                        #6.11
        sub       rsi, r10                                      #6.11
        add       rdx, rsi                                      #6.11
        lea       r11, QWORD PTR [rcx+rcx*8]                    #6.11
        add       rax, r11                                      #6.11
        add       rdx, rax                                      #6.11
        mov       eax, DWORD PTR [r8+rdx]                       #6.11
        ret

MSVC:

fun     PROC
        lea     rax, QWORD PTR [rcx+rcx*2]
        lea     rcx, QWORD PTR [rdx+rax*2]
        imul    rax, rcx, 7
        mov     rcx, QWORD PTR m$[rsp]
        add     rax, r8
        lea     r8, QWORD PTR [r9+rax*8]
        mov     rax, QWORD PTR a$[rsp]
        lea     rdx, QWORD PTR [rcx+r8*8]
        add     r8, rdx
        mov     eax, DWORD PTR [rax+r8*4]
        ret     0
fun     ENDP

Смотрите, — все компиляторы так делают, у всех одно-единственное обращение к памяти.

Может, всё-таки, дело не в нарушении стандарта?

Вообще-то, сообщество настолько огромно, и у него такое количество глубоко разбирающихся в вопросе людей, что подобное "нарушение" было бы вскрыто в короткие сроки и давно исправлено.

В C нет многомерных массивов, зато есть массивы массивов.

;

Так "дырявые" же структуры, а не массив.
А массив состоит из структур, а не из элементов структур.

Вот пример (ссылка на godbolt):

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

struct S {
	int i;
	char c;
};

int main(void) {
	struct S s = {0};

	printf("sizeof s: %zu\n", sizeof s);
	printf("sizeof s.i: %zu\n", sizeof s.i);
	printf("sizeof s.c: %zu\n", sizeof s.c);

	struct S a[3] = {0};

	puts("");
	printf("sizeof a: %zu\n", sizeof a);
	printf("sizeof a[0]: %zu\n", sizeof a[0]);

        return EXIT_SUCCESS;
}

Результат:

sizeof s: 8
sizeof s.i: 4
sizeof s.c: 1

sizeof a: 24
sizeof a[0]: 8

Видно, что сумма размеров полей структуры на 3 байта меньше размера структуры.
В структуре – дыра, причём, с краю, а не в середине, но она принадлежит структуре, входит в неё, является её неотъемлемой частью.
Именно поэтому размер структуры равен 8.

Массив структур в качестве элементов имеет объекты типа struct S размером 8 байт.
Количество элементов – 3.
Размер массива – 24.

Между элементами массива, каждый из которых имеет размер 8 байт, нет никаких дыр.
Они есть внутри каждого элемента, но это – внутреннее свойство типа элемента, а не свойство самого массива.

Адресная арифметика "щёлкает" на размер элемента и абстрагируется от его внутренних свойств.
Поэтому ей все равно, есть дыры в каждом из элементов, нет, сколько их и какого они размера.

Единственное, что её "заботит" – размер элемента.

Это ваша вольная интерпретация стандарта.

Точно моя?

Обратите внимание, что contiguously allocated относится к objects, а element type – к member object. Отсюда никак не следует, что тип элемента, т.е. тип объекта-члена – это то же самое, что непрерывно размещённые в памяти объекты. Буквально: размещённый набор объектов с определёнными типами объектов-членов.

Здесь имеется ввиду, что тип у всех объектов должен быть одинаковый.
И объекты не просто размещённые, а смежно или непрерывно размещённые.

Переводчик №1 (ссылка): "Тип массива описывает смежно выделенный непустой набор объектов с определенным типом объекта-члена, называемым типом элемента".

Переводчик №2 (ссылка): "Тип массива описывает непрерывно выделенный непустой набор объектов с определенным типом объекта-члена, называемым типом элемента".

"Смежно/непрерывно выделенный непустой набор объектов".

В отличие от структур, о которых можно прочесть в следующем предложении стандарта:

— A structure type describes a sequentially allocated nonempty set of member objects (and, in certain ircumstances, an incomplete array), each of which has an optionally specified name and possibly distinct type.

Переводчик №1 (ссылка): "Тип структуры описывает последовательно выделенный непустой набор объектов-членов (и, при определенных обстоятельствах, неполный массив), каждый из которых имеет опционально заданное имя и, возможно, отдельный тип".

Переводчик №2 (ссылка): "Тип структуры описывает последовательно размещенный непустой набор объектов-членов (и, в некоторых случаях, неполный массив), каждый из которых имеет необязательно указанное имя и, возможно, отдельный тип".

Для массивов размещение смежное/непрерывное, а для структур – лишь последовательное (упорядоченное по порядку объявления полей) без требования непрерывности размещения.

И тип элементов – для массивов один и тот же, для структур – нет.

Да и по сути, машин с адресацией памяти словами – вагон и маленькая тележка.

Вы уже приводили пример PDP-11, и выяснилось, что никакой проблемы нет, ибо наличествует и адресация байтами, и в этом случае доступны и нечётные адреса.

В том, что на практике бывают массивы с дырками между элементами, я совершенно уверен, хотя лично с языком Си на таких машинах не работал.

Чья-либо уверенность в данном случае ничего не проясняет.
Тем более, если человек не работал с языком C.

Более интересный вопрос другой: бывают ли в Си многомерные массивы с дополнительными дырками между строками?

Нет, не бывают.

В принципе, там и многомерных не бывает, бывают массивы массивов.

Видимо, процессоры разные у меня и на сайте, вот и инструкции другие.

Процессор там такой (ссылка на godbolt):
Intel(R) Xeon(R) Platinum 8375C CPU @ 2.90GHz
или такой:
AMD EPYC 7R32

Но, в любом случае, чудес не бывает, одной инструкцией paddb обойтись не получится.

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

"5-мерный" массив (передаётся указатель на массив):

int fun(size_t i, size_t j, size_t k, size_t l, size_t m, int (*a)[5][6][7][8][9]) {
	return (*a)[i][j][k][l][m];
}

Результирующий ассемблерный код (ссылка на godbolt):

fun:
        imul    rdi, rdi, 3024
        lea     rax, [rcx+rcx*8]
        lea     rdx, [rdx+rdx*8]
        imul    rsi, rsi, 504
        add     rax, rdi
        add     rax, rsi
        lea     rax, [rax+rdx*8]
        add     rax, r8
        mov     eax, DWORD PTR [r9+rax*4]
        ret

Сколько обращений к памяти?
Я вижу одно, в предпоследней инструкции.

На эти грабли, как правило, наступает каждый аспирант, загоревшийся идеей переписать код с фортрана на Си и потом недоумевающий, почему его версия работает в несколько раз медленнее)

Наверное, у того аспиранта массивы неправильные.

Грабли как правило заключаются в отсутствии владении языком, неэффективным его использованием, а не недостатком языка.

Посмотрел, кстати – gfortran это сложение массивов транслирует в ОДНУ векторную команду paddb.

Нет, он транслирует сложение массивов ровно в те же векторные инструкции (в количестве 3-х), в которые транслируется и та программа на C, которую я приводил:

.L18:
        vmovdqu ymm4, YMMWORD PTR [r12+rax]
        vpaddb  ymm0, ymm4, YMMWORD PTR [r14+rax]
        vmovdqu YMMWORD PTR [r9+rax], ymm0
        add     rax, 32
        cmp     rax, r10
        jne     .L18

Ссылка на godbolt с вашим кодом, я оттуда достал этот фрагмент из ассемблерного листинга для компилятора gfortran (добавил опцию -march=native, чтобы более эффективные векторные инструкции использовались).

Однако, и flang, и ifort (в последнем надо как следует поискать нужное место), выдают для сложения матриц ровно такой же код с точностью до конкретных используемых регистров.

Видите, выясняется, что Fortran не имеет преимуществ в производительности перед C.

Вы не забывайте, что всеми этими макросами, sizeof'ами, циклами, преобразованиями пойнтеров туда и обратно вы расписываете аналог следующего фортрановского кода

И что?
Язык C – общего назначения, в него не тащат, что не попадя.

Если перейти на C++, Fortran проиграет, ибо C++ позволяет создать себе инструмент, причём такой, какой надо, и пользоваться им.

В Fortran'е же, насколько я понимаю, есть только то, что есть.

Из какого места стандарта вы делаете такой вывод?

Ссылка на draft С11.

6.2.5 Types
...
20 Any number of derived types can be constructed from the object and function types, as follows:
— An array type describes a contiguously allocated nonempty set of objects with a particular member object type, called the element type.
...

На всякий случай, ссылка на перевод слова contiguous.

Чтобы была работоспособной адресная арифметика, достаточно того, чтобы инкремент указателя на элемент массива давал следующий элемент.

Тогда все массивы, а также области памяти, интерпретируемые как массивы, будут "дырявыми".

Но в C они не могут быть "дырявыми".

Как вы вообще представляете себе char [] на машине с невозможностью обращения по нечётному адресу (как, например, некоторые модели PDP, хорошо известные разработчикам Си)?

Невозможность обратиться по нечетному адресу характерна лишь для регистров R6 (PC) и R7 (SP).
Для остальных регистров проблемы обратиться к байту, в том числе, по нечётному адресу, нет.

Очень просто представляю: байтовые инструкции.

В близнеце Си Паскале – так там специально даже есть синтаксис packed array.

Pascal – очень условный "близнец" C.

Хорошо, что Вы тратите своё время и пытаетесь доказательно разобраться в вопросе

А как ещё иначе?
Авторитетов же не существует.

К тому же, чтобы навыки не "загнивали", их необходимо тренировать, а на это так и так уходит время.

но плохо, что при этом игнорируете часть смысла сказанного.

Это, видимо, потому, что я возражал по конкретному пункту о необходимости явно прописывать адресную арифметику, а потом по конкретному другому пункту касательно "устройства" массива массивов.

Когда вы в своей функции add обходите массив по строкам, то вы никак не используете его двухмерность. Фактически это проход по одномерному массиву, расписанный в два индекса, и компилятор понимает, что происходит обращение к последовательным ячейкам памяти.

Ну, то есть, не требуется прописывать адресную арифметику, и компилятор может соптимизировать, поскольку "понимает", с чем работает.

Если вы заметите, я изначально писал про перемножение матриц, потому что там таким трюком не обойтись.

Во-первых, это никакой не трюк, а штатная возможность, абсолютно переносимая и обязанная одинаковым образом быть поддержанной всеми компиляторами, претендующими на соблюдение стандарта.
А, во-вторых, основной фоновый контекст у нас здесь – преимущество (или отсутствие такового) Fortran'а перед C в рассматриваемой части.

Если вы в своей функции add переставите местами индексы, например, у массива c (т.е. c[n,m] вместо c[m,n]), то компилятор начинает выдавать уже такую хтонь, которую я затрудняюсь проинтерпретировать.

Вы хотите сказать, что компилятор Fortran'а аналогичный код в тех же условиях сумеет векторизовать?

Далее, вы же сами должны хорошо понимать, что ваш макрос ELEMS работает только по совпадению из-за невыровненности элементов массива на границу, большую их длины.

Макрос ELEMS работает для любых случаев.

Элементы массива (и сам массив, а также массив массивов и далее рекурсивно) выровнены согласно требованию к выравниванию для типа элемента массива.
Между элементами массива нет "пропусков", они расположены "вплотную" друг к другу, и sizeof массива строго равен количеству его элементов, умноженному на sizeof его элемента.

Такое поведение не гарантируется языком Си (уже на той же самой PDP-11 нечётные адреса зачастую были запрещены).

Расположение элементов массива "влотную", без "пропусков" гарантируется стандартом, и это необходимое условие для того, чтобы адресная арифметика была в принципе работоспособной.

Макрос ELEMS прямо опирается на гарантии стандарта и именно поэтому работает для любых случаев.

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

Конечно, никакого принципиального значения этот макрос не имеет, можно перейти просто к M и N, но это ещё затруднит работу векторизатора.

Нет необходимости переходить к M и N, отказываясь от данного макроса по надуманной причине, поэтому у "векторизатора" затруднений, связанных с этим, не появится.

В том числе на некоторых векторных архитектурах размеры самих векторов должны выравниваться на длину векторных регистров.

Язык C предоставляет средства для реализации повышенных требований к выравниванию, поэтому здесь также нет никаких проблем.

Короткий пример (ссылка на godbolt):

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

int main(void) {
	int a0[16];
	alignas(sizeof a0) int a1[16];

	printf("&a0: %p, sizeof a0: %zu, alignof a0: %zu\n", (void *)&a0, sizeof a0, alignof a0);
	printf("&a1: %p, sizeof a1: %zu, alignof a1: %zu\n", (void *)&a1, sizeof a1, alignof a1);

	return EXIT_SUCCESS;
}

Результат (взятый у clang'а, потому что у gcc он не интересный):

&a0: 0x7ffe2b64b2b0, sizeof a0: 64, alignof a0: 4
&a1: 0x7ffe2b64b240, sizeof a1: 64, alignof a1: 64

По адресу видно, что a0 реально выровнен на границу 16, а a1 – уже на границу 64.
Явный код для выравнивания на 64 для массива a1 можно также увидеть в ассемблере у каждого из компиляторов.

Что касается массива указателей, то я был неправ, высказав своё утверждение в таком виде, оно неверно. В целом я думал о том, что написано ниже, и неудачно выразил свою мысль.

Но доказательств для этого потребовалось больше, чем, я думал, будет достаточно.

Теперь, о практике программирования в целом. Думаю, вы согласитесь, что по целому ряду причин мало кто в реальной жизни будет писать функции так, как это сделано в вашем демонстрационном коде.

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

Да, junior'ы не смогут такое написать, и даже, наверное, часть middle'ов не сможет, особенно вариант с VLA, но достаточно им по-настоящему объяснить, как устроены типы в языке, и на чём базируется адресная арифметика, а не как это часто делают современные преподаватели, и, после некоторого времени, потраченного на практику, такое даже junior'ы смогут, здесь нет чего-то заумного.

Программист на С и особенно С++ будет выбирать решение, более соответсвующее принципу инкапсуляции и более совместимое со стандартной библиотекой.

Использование указателя на массив никак не ограничивает инкапсуляцию, а у программиста на C++, кроме указателей, ещё наличествуют в активе и ссылки, он может использовать ссылку на массив.

Поскольку речь идёт об инкапсуляции, то реализация может быть любой.

Если кто-то ничего не знает, кроме как о std::vector, (как ещё понять "более совместимое со стандартной библиотекой", но при этом не дающее возможности применить указатель на массив?) это не повод огульно обобщать на большинство (как ещё понять "программист на С и особенно С++ будет выбирать"?).

К тому же, сейчас активно используется такая процедура как review, что нивелирует огрехи, которые могут допустить не слишком искушённые программисты.

 А это значит, что все эти трюки с массивами в С крайне малоприменимы, на практике там будет другая структура данных.

Повторюсь, это никакой не трюк, это совершенно штатная и абсолютно переносимая возможность языка.

Там будет та структура данных, которую выберет программист.

Если программист слабо владеет языком, он легко может выбрать неподходящее решение.
Для этого обычно в компаниях бывают программисты, кототрые хорошо владеют используемым языком, и которые на review укажут, как следует изменить решение, чтобы оно стало эффективным.

И уже после этого там будет та структура данных, которую выберет грамотный программист, и которая эффективна для используемого языка.

В отличие от Фортрана, где податься некуда, и сложные структуры данных, как правило, представляются в виде набора массивов.

То есть, получается, что в этом месте C имеет преимущество перед Fortran'ом, поскольку даёт больше возможностей и более гибок.

Наконец, как тут уже отмечалось в комментариях выше, не будем забывать, что фортрановскую программу можно простой перекомпиляцией перевести с SSE на CUDA.

Выходит, это единственное преимущество Fortran'а в данной части.

Видите как, уже пришлось перейти к void*

Почему вы решили, что именно "пришлось"?

Смотрите, как можно совсем без `void *`:

#define ELEMS(a) (sizeof (a) / sizeof *(a))

void fun(size_t M, size_t N, char (*a)[M][N]) {
	printf("sizeof a: %zu\n", sizeof *a);
	printf("&a: %p\n", (void *)a);

	for (size_t m = 0; m < ELEMS(*a); ++ m) {
		printf("\tsizeof a[%zu]: %zu\n", m, sizeof (*a)[m]);
		printf("\t&a[%zu]: %p\n", m, (void *)&(*a)[m]);

		for (size_t n = 0; n < ELEMS((*a)[m]); ++ n) {
			printf("\t\t&a[%zu][%zu]: %p\n", m, n, (void *)&(*a)[m][n]);
		}
	}
}

int main(int argc, char **argv) {
	(void)argv;

	size_t M = argc + argc;
	size_t N = argc * (argc + argc) + argc;

	char (*a)[M][N] = calloc(M, N);

	if (a) {
		fun(M, N, a);
		free(a);
	}

	return EXIT_SUCCESS;
}

Результат:

sizeof a: 6
&a: 0x19fb2a0
	sizeof a[0]: 3
	&a[0]: 0x19fb2a0
		&a[0][0]: 0x19fb2a0
		&a[0][1]: 0x19fb2a1
		&a[0][2]: 0x19fb2a2
	sizeof a[1]: 3
	&a[1]: 0x19fb2a3
		&a[1][0]: 0x19fb2a3
		&a[1][1]: 0x19fb2a4
		&a[1][2]: 0x19fb2a5

передаче отдельно от массива его размеров

Верно, это же C, и вы сами выбрали случай, когда размерности становятся известны только в run-time'е.

Их в любом случае необходимо передавать, только в других языках это происходит "под капотом", и отменить это нельзя, а здесь, хоть и требуется выполнять явную передачу, но, зато этим можно управлять (передавать или не передавать, если не требуется).

и адресной арифметике полувручную

Где здесь, особенно в последнем варианте, адресная арифметика? (1)

Что значит, "полувручную"? (2)

А когда вы ещё учтёте возможность выравнивания, будет совсем вручную.

Функции malloc/calloc/realloc возвращают адрес с выравниванием, достаточным для хранения любого стандартного объекта.

Если же передаётся адрес настоящего массива, то он, по определению, выровнен.

Зачем вы пытаетесь найти изъяны там, где их нет?

Причём компилятор-то не знает, что значение M - это длина строки массива, и не может им воспользоваться при загрузке вектора в векторный процессор.

Возьмём следующий код (ссылка на godbolt):

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

#define K 1000

#define ELEMS(a) (sizeof (a) / sizeof *(a))

typedef int type;

void add(size_t M, size_t N, type (*a)[M][N], type (*b)[M][N], type (*c)[M][N]) {
        for (size_t m = 0; m < ELEMS(*a); ++m) {
                for (size_t n = 0; n < ELEMS((*a)[m]); ++n) {
                        (*a)[m][n] = (*b)[m][n] + (*c)[m][n];
                }
        }
}

void fill(size_t M, size_t N, type (*x)[M][N], type y) {
        for (size_t i = 0; i < ELEMS(*x); ++i) {
                for (size_t j = 0; j < ELEMS((*x)[i]); ++j) {
                        (*x)[i][j] = y;
                }
        }
}

int main(int argc, char **argv) {
        (void)argv;

        int m = argc + argc;
        int n = m + argc * argc;

        size_t M = m * K;
        size_t N = n * K;

        type (*a)[M][N] = malloc(sizeof *a);

        if (a) {
                type (*b)[M][N] = malloc(sizeof *b);

                if (b) {
                        fill(M, N, b, m);

                        type (*c)[M][N] = malloc(sizeof *c);

                        if (c) {
                                fill(M, N, c, n);
                                add(M, N, a, b, c);
                                printf("a[0][0]: %i\n", (*a)[0][0]);
                                free(c);
                        }

                        free(b);
                }

                free(a);
        }

        return EXIT_SUCCESS;
}

и посмотрим, векторизуется ли он в функциях add и fill.

Функция fill заполняет массив одним и тем же значением.

Функция add складывает поэлементно два массива и поэлементно же кладёт результат в третий.

В данном коде во все элементы массива b кладётся 2, во все элементы массива c кладётся 3, затем они суммируются, а результат кладётся в массив a.

Первый элемент массива a распечатывается, чтобы icc не "думал", что, раз массив a после заполнения никак не используется, то можно ничего и не вычислять, и не выбрасывал вызов функции add.

В функции add видим в ассемблерном коде gcc следующее:

.L5:
        vmovdqu ymm1, YMMWORD PTR [r15+rdx]
        vpaddd  ymm0, ymm1, YMMWORD PTR [rax+rdx]
        vmovdqu YMMWORD PTR [r9+rdx], ymm0
        add     rdx, 32
        cmp     rdx, r14
        jne     .L5

Видите векторизацию?
Компилятор смог.

В функции fill видим в ассемблерном коде gcc следующее:

        vpbroadcastd    ymm0, ebx
        ...
.L36:
        vmovdqu YMMWORD PTR [rdx], ymm0
        add     rdx, 32
        cmp     rcx, rdx
        jne     .L36

А здесь видите векторизацию?
Компилятор опять смог.

Для clang'а и icc от Intel'а всё аналогично.

Если прогнать perf'ом, то получается следующее (у меня локально установлена другая версия gcc, поэтому используемые регистры в ассемблерном коде слегка другие):

 14.99 │118:   vmovdqu    ymm1,YMMWORD PTR [r14+r12*1]     
 15.00 │       vpaddd     ymm0,ymm1,YMMWORD PTR [rax+r12*1]
 65.00 │       vmovdqu    YMMWORD PTR [rdx+r12*1],ymm0     
       │       add        r12,0x20                         
       │       cmp        r12,rdi                          
  5.00 │     ↑ jne        118 

Видно, что эти инструкции на самом деле работают, а не просто "валяются в коде рядом".

Это означает, что компилятор смог.
Более того, смогли все опробованные мной компиляторы.

Вы можете ответить на вопросы, помеченные мной как (1) и (2)?

Итак:

  1. В C массив массивов понимается как массив массивов, причём независимо от того, какой это массив, обычный или VLA.

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

  3. Никакой дополнительной адресной арифметики при этом не требуется.

  4. Никаких прочих накладных расходов, связанных с использованием указателей на void, нет.

  5. Передача размерностей массива в рассматриваемом случае не зависит от языка и будет присутствовать в том или ином виде в любом языке.

  6. Не существует никаких проблем с выравниванием, специфичных для применяемой техники.

  7. При этом все широко используемые современные компиляторы прекрасно векторизуют код.

  8. В этом смысле Fortran не имеет преимуществ перед C.

Если ещё учесть, что strict aliasing'ом можно управлять с помощью ключевого слова restrict, то станет очевидно, что в этом смысле Fortran вообще не имеет преимуществ перед C.

Напоминаю, что первый пункт в списке выше есть прямое возражение на ваше утверждение:

В Си массив массивов понимается как массив указателей на массивы, а не как двухмерный массив.

Переменными сделайте M и N, а не подставляйте вместо них препроцессором константы 2 и 3.

Да, давайте ещё и константность везде поубираем, чтобы она вас не смущала.

И чтобы компилятор не смог ничего лишнего соптимизировать, запутаем способ получения M и N, да так, чтобы в выражениях для их вычисления ещё и ни единой константы не было.

Вариант с VLA:

#define ELEMS(a) (sizeof (a) / sizeof *(a))

void fun(size_t M, size_t N) {
	char a[M][N];

	memset(&a, 0, sizeof a);

	printf("sizeof a: %zu\n", sizeof a);
	printf("&a: %p\n", (void *)&a);

	for (size_t m = 0; m < ELEMS(a); ++ m) {
		printf("\tsizeof a[%zu]: %zu\n", m, sizeof a[m]);
		printf("\t&a[%zu]: %p\n", m, (void *)&a[m]);

		for (size_t n = 0; n < ELEMS(a[m]); ++ n) {
			printf("\t\t&a[%zu][%zu]: %p\n", m, n, (void *)&a[m][n]);
		}
	}
}

int main(int argc, char **argv) {
	(void)argv;

	size_t M = argc + argc;
	size_t N = argc * (argc + argc) + argc;

	fun(M, N);
	return EXIT_SUCCESS;
}

Это я знаю, что программа будет запускаться без параметров, и argc будет равен 1.

Но компилятор из этого не может исходить, тем более, что я же могу второй раз ту же уже скомпилированную программу и с параметром запустить.

Результат:

sizeof a: 6
&a: 0x7ffca25fa910
	sizeof a[0]: 3
	&a[0]: 0x7ffca25fa910
		&a[0][0]: 0x7ffca25fa910
		&a[0][1]: 0x7ffca25fa911
		&a[0][2]: 0x7ffca25fa912
	sizeof a[1]: 3
	&a[1]: 0x7ffca25fa913
		&a[1][0]: 0x7ffca25fa913
		&a[1][1]: 0x7ffca25fa914
		&a[1][2]: 0x7ffca25fa915

Вариант с динамически выделяемой памятью:

#define ELEMS(a) (sizeof (a) / sizeof *(a))

void fun(void *p, size_t M, size_t N) {
	char (*a)[M][N] = p;

	printf("sizeof a: %zu\n", sizeof *a);
	printf("&a: %p\n", (void *)a);

	for (size_t m = 0; m < ELEMS(*a); ++ m) {
		printf("\tsizeof a[%zu]: %zu\n", m, sizeof (*a)[m]);
		printf("\t&a[%zu]: %p\n", m, (void *)&(*a)[m]);

		for (size_t n = 0; n < ELEMS((*a)[m]); ++ n) {
			printf("\t\t&a[%zu][%zu]: %p\n", m, n, (void *)&(*a)[m][n]);
		}
	}
}

int main(int argc, char **argv) {
	(void)argv;

	size_t M = argc + argc;
	size_t N = argc * (argc + argc) + argc;

	void *p = calloc(M, N);

	if (p) {
		fun(p, M, N);
		free(p);
	}

	return EXIT_SUCCESS;
}

Результат:

sizeof a: 6
&a: 0x119f2a0
	sizeof a[0]: 3
	&a[0]: 0x119f2a0
		&a[0][0]: 0x119f2a0
		&a[0][1]: 0x119f2a1
		&a[0][2]: 0x119f2a2
	sizeof a[1]: 3
	&a[1]: 0x119f2a3
		&a[1][0]: 0x119f2a3
		&a[1][1]: 0x119f2a4
		&a[1][2]: 0x119f2a5

Есть ещё идеи?

В C массив массивов понимается как массив массивов, причём независимо от того, какой это массив, обычный или VLA.

Более того, в этом стиле доступна работа и с динамически выделенной памятью, причём даже в этом случае размерности каждого массива не обязаны быть константами времени компиляции.

В этом смысле Fortran не имеет преимуществ перед C.

Теперь сделайте M и N переменными.

Сделал:

#define ELEMS(a) (sizeof (a) / sizeof *(a))

void fun(size_t const M, size_t const N) {
	char a[M][N];

	memset(&a, 0, sizeof a);

	printf("sizeof a: %zu\n", sizeof a);
	printf("&a: %p\n", (void const *)&a);

	for (size_t m = 0; m < ELEMS(a); ++ m) {
		printf("\tsizeof a[%zu]: %zu\n", m, sizeof a[m]);
		printf("\t&a[%zu]: %p\n", m, (void const *)&a[m]);

		for (size_t n = 0; n < ELEMS(a[m]); ++ n) {
			printf("\t\t&a[%zu][%zu]: %p\n", m, n, (void const *)&a[m][n]);
		}
	}
}

#define M 2
#define N 3

int main(void) {
	fun(M, N);
	return EXIT_SUCCESS;
}

Результат:

sizeof a: 6
&a: 0x7ffc19160190
	sizeof a[0]: 3
	&a[0]: 0x7ffc19160190
		&a[0][0]: 0x7ffc19160190
		&a[0][1]: 0x7ffc19160191
		&a[0][2]: 0x7ffc19160192
	sizeof a[1]: 3
	&a[1]: 0x7ffc19160193
		&a[1][0]: 0x7ffc19160193
		&a[1][1]: 0x7ffc19160194
		&a[1][2]: 0x7ffc19160195

Предвосхищая возможное дальнейшее предложение поработать в таком же стиле с динамически выделенной памятью, сделаю и это:

#define ELEMS(a) (sizeof (a) / sizeof *(a))

void fun(void const *const p, size_t const M, size_t const N) {
	char const (*const a)[M][N] = p;

	printf("sizeof a: %zu\n", sizeof *a);
	printf("&a: %p\n", (void const *)a);

	for (size_t m = 0; m < ELEMS(*a); ++ m) {
		printf("\tsizeof a[%zu]: %zu\n", m, sizeof (*a)[m]);
		printf("\t&a[%zu]: %p\n", m, (void const *)&(*a)[m]);

		for (size_t n = 0; n < ELEMS((*a)[m]); ++ n) {
			printf("\t\t&a[%zu][%zu]: %p\n", m, n, (void const *)&(*a)[m][n]);
		}
	}
}

#define M 2
#define N 3

int main(void) {
	void *const p = calloc(M, N);

	if (p) {
		fun(p, M, N);
		free(p);
	}

	return EXIT_SUCCESS;
}

Результат:

sizeof a: 6
&a: 0x45e2a0
	sizeof a[0]: 3
	&a[0]: 0x45e2a0
		&a[0][0]: 0x45e2a0
		&a[0][1]: 0x45e2a1
		&a[0][2]: 0x45e2a2
	sizeof a[1]: 3
	&a[1]: 0x45e2a3
		&a[1][0]: 0x45e2a3
		&a[1][1]: 0x45e2a4
		&a[1][2]: 0x45e2a5

В C массив массивов понимается как массив массивов.

Information

Rating
Does not participate
Registered
Activity