To understand why one way works and the other doesn't, you have to understand some of the strange things about how C allocates strings and arrays, and why sometimes strings are arrays and sometimes they are pointers.
Your array is declared as char S[][4]. All the characters in S are in a contiguous block in memory. When you access S[i][j], the compiled code determines the address of the character you are referencing by calculating the offset represented by i and j like char *indexed_character_address = S + i*4 + j. Now the compiled code uses indexed_character_address to load from or store into, depending on how S[i][j] is being used.
Another very different but confusingly similar way to declare an array of strings is like this:
char *T[] = { "tuv", "wxyz" };
What the compiler does behind the scenes to produce this array is very similar to this:
const char hidden_tuv[4] = { 't', 'u', 'v', '\0' };
const char hidden_wxyz[5] = { 'w', 'x', 'y', 'z', '\0' };
char *T[2] = { hidden_tuv, hidden_wxyz };
The "tuv" and "wxyz" strings are first placed in some arbitrary place, there is no guarantee which will come first or that they will be anywhere close to each other. Then another array is created and initialized with those two arbitrary pointers.
Code the compiler generates to access T[i][j] works in a manner very different from that for S. First it calculates the address of the ith string pointer in T, then it loads one of the string's starting addresses from memory, then it uses j to offset from the start of that address. Like this:
char **address_in_T = T + i;
char *string_start_address = *address_in_T;
// now string_start_address is either hidden_tuv or hidden_wxyz
char *indexed_character_address = string_start_address + j;
That verbose code is equivalent to this small expression:
char *indexed_character_address = T[i] + j;
// or &(T[i][j])
// or *(*(T + i) + j)
Your S variable is a 2-dimensional array of characters, initialized with string data, with type char[2][4]. My T variable is a 1-dimensional array of character pointers, of type (char *)[2].
The really really confusing part here is that even though these two variables are of very different types, C syntax can make accessing them look very similar
char c1 = S[i][j]; // actually reads *(S + i*4 + j)
char c2 = T[i][j]; // actually reads *( *(T + i) + j)
*( *(arr + i) + j)is a super convoluted way of writingarr[i][j].