Вы столкнулись с неопределённым поведением. То, что вы видите, может меняться от компилятора к компилятору, и от компьютера к компьютеру.
В своём коде вы складываете и вычитаете не строки, а указатели на эти строки:
4 + "0"; // const char*
"h" - "0" // ptrdiff_t
В первом случае вы получаете указатель на некоторый участок памяти. И пробуете вывести строку, начинающуюся с этого участка. Компьютер будет пытаться представить байты этой памяти как символы в некоторой кодировке до тех пор, пока не встретит нуль-терминатор.
Скорее всего, ваш компилятор поместил строки "0"
, "h"
, "e"
, "l"
, "o"
, "g"
достаточно близко в памяти. Строка "0"
находится раньше всех. И когда вы прибавляете к её указателю небольшое число, вы попадаете на строку "h"
.
Обратите внимание, что я говорю именно строки, не символы. Потому что "h"
содержит 2 символа — 'h'
и '\0'
. Последний как раз и представляет собой нуль-терминатор. И если вы попадаете указателем в 'h'
, то компьютер заканчивает печать на следующем же символе.
Во втором случае результат выражения неявно приводится к числу. И вы просто видите разницу между указателями.