Первый и второй принт выдадут разные результаты.
Массив размещается в стеке, там же размещается переменная name, где хранится адрес на первый элемент массива. В конец массива char компилятор добавляет символ с кодом 0x00 — признак конца строки. Именно по этому printf выводит правильное количество символов.
Первый принт выдаст адрес переменной name, а второй — содержимое (адрес символа 'H').
Третий же выведет все символы начиная с адреса, хранящегося в переменной name, пока не встретится 0x00.
sizeof выполняется на этапе компиляции, в машинном коде вместо него будут константы.
sizeof(name) вернёт размер указателя, а не размер массива, потому что вы определили не массив, а указатель, и присвоили ему адрес массива.
char name_array[] = "Hello World!"
Вот это уже массив, и sizeoff вернёт его размер, потому что он известен на этапе компиляции.
С указателями можно проводить арифметические операции. К примеру *(name+i) равносильно name[i].
К адресу прибавится не i, а i*sizeof(*name), т.е. размер типа указателя помноженный на число. Получаем i-ый элемент. Об этом позаботится компилятор. Для получения произвольного адреса необходимо привести указатель к типу void.
Массивы не являются указателями. Есть массивы, есть указатели. Есть адреса. Берём адрес массива и сохраняем его в указатель. В свою очередь указатель — это переменная, и она тоже располагается по какому-то адресу. Тут появляются указатели на указатели (иерархия не ограничена). К примеру многомерные массивы.