char * hi;
char * wh;
char ** what;
int main(void) {
char z[4] = "wx\0";
char a[4] = "ab\0";
hi = &z;
wh = &a;
First mistake; the types of the expressions &z and &a are both char (*)[4] (pointer to 4-element array of char), not char *. To fix this, drop the & from both:
hi = z;
wh = a;
Except when it is the operand of the sizeof or unary & operators, or is a string literal being used to initialize an array, an expression of type "N-element array of T" will be converted ("decay") to an expression of type "pointer to T", and the value of the expression is the address of the first element in the array.
what = (char **) malloc( 25 * sizeof(char));
Don't cast the result of malloc; it isn't necessary1, and depending on the compiler version it can suppress a useful diagnostic. You also have a type mismatch. You want to space for 25 pointers to char, but you're only allocating enough for 25 plain chars. Rewrite that as
what = malloc( sizeof *what * 25 );
The type of the expression what is char **; thus, the type of the expression *what is char *. So sizeof *what will give you the same result as sizeof (char *). The above line allocates enough memory to store 25 pointers to char, and assigns the resulting pointer to what.
what[0] = &hi;
what[1] = &wh;
Again, drop the &; hi and wh are already pointers to char:
what[0] = hi;
what[1] = wh;
printf("%s | %s\n", hi, wh);
hi = &what[1];
Type mismatch; &what[1] has type char **, hi has type char *. Again, drop the &.
hi = what[1];
hi and wh now point to the same thing (what[1] == wh and hi == what[1], so hi == wh; that is, both hi and wh contain the same pointer value).
printf("%s | %s\n", hi, wh);
return EXIT_SUCCESS;
}
1. In C, that is; C++ is a different story, but if you're writing C++, you shouldn't be using malloc anyway.