Принципиально на большинстве компиляторов не отличается от signed
char, у некоторых unsigned char (watcom, picc18 std, вроде, или там
ключ нужен для того). В смысле, сгенерированный код не отличается.
Но с точки зрения компилятора типы все три -- разные. И во-первых
будут ошибки и варнинги если не тот тип подсовывать, во-вторых в
C++ если есть зависимость от типа, то нужно рассматривать все три
варианта, чтоб не воспринять char как некую абстрактную структуру. Как я понимаю, это тянется от того, что изначально в стандарте знаковость типа char -- зависит от компилятора (хотя почти всегда signed). И char умышленно не совместим ни с unsigned char, ни с signed char, чтоб не было такого, что на одном компиляторе всё компилируется, а на другом перестаёт.
Выводы из этого я сделал такие для себя, что хочется работать с последовательностями байтов, то нужно использовать просто char* (потому, что везде так в интерфейсах библиотек уже, и неудобно постоянно каст делать). Варианты с unsigned char*, uint8_t* не нужны. Обычно их хочется использовать, чтоб не нарываться на знаковую конверсию -- когда printf("%u", char) может распечатать 0xfffffffa5 например, вместо просто a5. Но надо просто помнить, что знаковость char непонятно какая и при каждом арифметическом использовании из него нужно отрезать старшие биты после (автоматической, inter promotion) конверсии в int: c & 0xff, или при работе с указателем кастить к *(uint8_t*)c. Забавный факт, что спецификатор формата в printf (%d, %u, %x) никак на (не)правильность выпечатывания кода char не влияет и всё дело в правильности приведения типа самого аргумента (от char к unsigned или int). Кроме вариантов вроде %hhu, которые до сих пор не везде ещё доступны.