Трохи теорії. В C/C++ ім'я масиву в більшості випадків означає вказівник на його початковий елемент:
a == &a[0]
&a[i] == a + i
a[i] == *(a + i)
Якщо масив розташований за адресою 0x100, то вийде, що f(a) виконає, фактично, f(0x100); а &a[1] буде дорівнювати 0x100+sizeof(a[0]) - скажімо, 0x108.
А от з масивами масивів все складніше.
масив масивів - це шматок пам'яті з 4 елементів, по рядках. В пам'яті він виглядає так само, як і масив a[4]; а в функцію буде передане посилання на його початок, і головне питання - як воно буде розглядатися в функції.
Найпростіший варіант - записати всі (крім першого) розміри:
f(double b[][2]) буде працювати, як треба, бо компілятор знає, як обчислити адресу &b[1][1] = b + sizeof(b[0]).
Можна записати це також як
f(double (*b)[2]) - вказівник на масив з 2-х елементів. Адреса обчислюється так само, і, фактично, жодної різниці з попереднім варіантом нема.
А от f(double **c) - щось зовсім відмінне. Тут у нас вказівник не на елемент масиву, а на інший вказівник. Тобто нам треба створити додатковий масив вказівників double *c[2] і кожному з вказівників надати якесь значення, наприклад
double b[2][2]; //наприклад 0x100
double *c[2];//наприклад 0x200
c[0]=&b[0];// за адресою 0x200 знаходиться 0x100
c[1]=&b[1];// за адресою 0x204 знаходиться 0x110
f(c);//f(0x200)
Потрібні такі збочення, насправді, для роботи з динамічною пам'яттю, коли у нас не масив, а вказівник, а сам масив створюється динамічно в купі. У вашому випадку, я так розумію, цілком достатньо прописати всюди розміри масивів і не ламати собі голову.