C / C++ зачем использовать unsigned char для двоичных данных?

действительно ли необходимо использовать unsigned char хранить двоичные данные, как в некоторых библиотеках, которые работают с кодировкой символов или двоичными буферами? Чтобы понять мой вопрос, взгляните на код ниже -

char c[5], d[5];
c[0] = 0xF0;
c[1] = 0xA4;
c[2] = 0xAD;
c[3] = 0xA2;
c[4] = '';

printf("%sn", c);
memcpy(d, c, 5);
printf("%sn", d);

и printf's выход

8 ответов


в C unsigned char тип данных является единственным типом данных, который имеет все следующие три свойства одновременно

  • он не имеет бит заполнения, что он, где все биты хранения способствуют значению данных
  • никакая побитовая операция, начинающаяся со значения этого типа, при преобразовании обратно в этот тип, не может вызвать переполнение, представления ловушки или неопределенное поведение
  • он может псевдоним других типов данных без нарушения " сглаживания правила", то есть доступ к одним и тем же данным через указатель, который набирается по-разному, будет гарантированно видеть все модификации

если это свойства "двоичного" типа данных, который вы ищете, вы определенно должны использовать unsigned char.

для второго свойства нам нужен тип, который unsigned. Все эти преобразования задаются с дулю arihmetic, вот дулю UCHAR_MAX+1, 256 в большинстве 99% архитектур. Все преобразования шире значения unsigned char таким образом, просто соответствует усечению наименее значимого байта.

два других типа символов, как правило, не работают одинаково. signed char подписано, так или иначе, поэтому преобразование значений, которые не соответствуют ему, не определено. char не фиксируется, чтобы быть подписанным или неподписанным, но на определенной платформе, на которую переносится ваш код, он может быть подписан даже без подписи на вашем.


простой char тип проблематичен и не должен использоваться ни для чего, кроме строк. Основная проблема, с char заключается в том, что вы не можете знать, подписано оно или нет: это поведение, определяемое реализацией. Это делает char отличается от int etc,int - это всегда гарантированно будет подписан.

хотя VC дал предупреждение ... усечение постоянного значения

Он говорит вам, что вы пытаетесь сохранить литералы int внутри переменные типа char. Это может быть связано со значимостью: если вы пытаетесь сохранить целое число со значением > 0x7F внутри подписанного символа, могут произойти неожиданные вещи. Формально это неопределенное поведение в C, хотя практически Вы просто получите странный вывод, если попытаетесь напечатать результат как целое значение, хранящееся внутри (подписанного) символа.

в этом конкретном случае предупреждение не должно иметь значения.

EDIT:

в других связанные вопросы unsigned char выделяется, потому что это единственный (байт/наименьший) тип данных, который гарантированно не имеет заполнения c-спецификацией.

теоретически все целочисленные типы, кроме unsigned char и signed char, могут содержать "биты заполнения" в соответствии с C11 6.2.6.2:

" для целочисленных типов без знака, отличных от unsigned char, биты представление объекта делится на две группы: биты значений и перетяжка биты (последнее не обязательно)."

" для целочисленных типов со знаком биты представления объекта должны разделите на три группы: биты значений, биты заполнения и знак немного. Не должно быть никаких бит заполнения; подписанный char не должен иметь любая обивка биты."

стандарт C намеренно расплывчатый и нечеткий, позволяя эти теоретические биты заполнения, потому что:

  • оно позволяет различным таблицам символа чем стандартные 8-битные.
  • он позволяет реализовать определенную значимость и странные целочисленные форматы со знаком, такие как дополнение или "знак и величина".
  • целое число может не обязательно использовать все выделенные биты.

однако в реальном мире вне стандарта C применяется следующее:

  • таблицы символов почти наверняка 8 бит (UTF8 или ASCII). Некоторые странные исключения существуют, но чистые реализации используют стандарт тип тип wchar_t при реализации таблиц символов размером более 8 бит.
  • Signedness всегда является дополнением двух.
  • целое число всегда использует все выделенные биты.

таким образом, нет никакой реальной причины использовать unsigned char или signed char только для уклонения от некоторого теоретического сценария в стандарте C.


вы получите большинство ваших проблем при сравнении содержимого отдельных байтов:

char c[5];
c[0] = 0xff;
/*blah blah*/
if (c[0] == 0xff)
{
    printf("good\n");
}
else
{
    printf("bad\n");
}

может печатать "плохо", потому что, в зависимости от вашего компилятора, C[0] будет расширен до -1, что никоим образом не совпадает с 0xff


байты обычно предназначены как целые числа без знака 8 бит.

теперь char не указывает знак целого числа: на некоторых компиляторах char может быть подписан, на других он может быть без знака.

Если я добавлю немного операции сдвига в код, который вы написали, то у меня будет неопределенное поведение. Добавил сравнение также будет иметь неожиданный результат.

char c[5], d[5];
c[0] = 0xF0;
c[1] = 0xA4;
c[2] = 0xAD;
c[3] = 0xA2;
c[4] = '';
c[0] >>= 1; // If char is signed, will the 7th bit go to 0 or stay the same?

bool isBiggerThan0 = c[0] > 0; // FALSE if char is signed!

printf("%s\n", c);
memcpy(d, c, 5);
printf("%s\n", d);

относительно предупреждения во время компиляции: если char подписан, то вы пытаетесь назначьте значение 0xf0, которое не может быть представлено в знаковом символе (диапазон от -128 до +127), поэтому оно будет приведено к знаковому значению (-16).

объявление символа как подписанного удалит предупреждение и всегда хорошо иметь чистую сборку без предупреждения.


подписанность равнины char тип определяется реализацией, поэтому, если вы на самом деле не имеете дело с символьными данными (строка, использующая набор символов платформы - обычно ASCII), обычно лучше явно указать подписанность, используя signed char или unsigned char.

для двоичных данных лучшим выбором, скорее всего, является unsigned char, особенно если побитовые операции будут выполняться над данными (в частности, смещение битов, которое не ведет себя одинаково для подписанные типы как для неподписанных типов).


Я спрашиваю, почему что-то, что, кажется, работает так же хорошо с char, должно быть набрано без знака char?

Если вы делаете вещи, которые не являются "правильными" в смысле стандарта, вы полагаетесь на неопределенное поведение. Ваш компилятор может сделать это так, как вы хотите сегодня, но вы не знаете, что она делает завтра. Вы не знаете, что делает GCC или VC++ 2012. Или даже если поведение зависит от внешних факторов или компиляции отладки / выпуска и т. д. Как только вы покинете безопасный путь стандарта, вы можете нарваться на неприятности.


Ну, что вы называете "двоичные данные"? Это куча битов, без какого-либо значения, назначенного им той конкретной частью программного обеспечения, которая называет их "двоичными данными". Каков ближайший примитивный тип данных, который передает идею отсутствия какого-либо конкретного значения для любого из этих битов? Я думаю unsigned char.


действительно ли необходимо использовать unsigned char для хранения двоичных данных, как в некоторых библиотеках, которые работают с кодировкой символов или двоичными буферами?

"очень" надо? Нет.

Это правда очень хорошая идея, и есть много причин для этого.

в вашем примере используется printf, который не является типобезопасным. То есть printf принимает сигналы форматирования из строки формата, а не из типа данных. Вы могли бы так же легко попробовал:

printf("%s\n", (void*)c);

... и результат был бы тот же. Если вы попробуете то же самое с iostreams c++, результат будет другим (в зависимости от подписанности c).

какие рассуждения могли бы способствовать использованию неподписанного символа вместо простого символа?

Unsigned указывает, что наиболее значительный бит данных (для unsigned char 8-й бит) представляет знак. Поскольку вам это явно не нужно, вы должны укажите, что ваши данные не имеют знака (бит "знак" представляет данные, а не знак других битов).