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

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

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

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

и printf's вывод 𤭢 правильно, где f0 a4 ad a2 - это кодировка для кодовой точки Unicode U+24B62 (𤭢) в шестнадцатеричном формате.

Даже memcpy также правильно скопировал бит, хранящийся в char.

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

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

Я использовал VС++ Express 2010 и MinGW для компиляции вышеизложенного. Хотя VC дал предупреждение

warning C4309: '=' : truncation of constant value

вывод, похоже, не отражает этого.

P.S. Это можно было бы обозначить как возможный дубликат Если буфер байтов подписан или без знака char buffer?, но мои намерения различны. Я спрашиваю, почему что-то, что, кажется, работает нормально с char, должно быть напечатано unsigned char?

Обновление: Для цитаты из N3337,

Section 3.9 Types

2 Для любого объекта (кроме подобъекта базового класса) тривиально тип копирования T, независимо от того, имеет ли объект допустимое значение типа T, базовые байты (1.7), составляющие объект, могут быть скопированы в массив char или без знака char. Если содержимое массива charили unsigned char копируется обратно в объект, объект должен впоследствии сохраняют свое первоначальное значение.

В свете приведенного выше факта и моего первоначального примера на компьютере Intel, где char по умолчанию signed char, я все еще не убежден, что unsigned char должен быть предпочтительнее char.

Что-нибудь еще?

Ответ 1

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

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

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

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

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

Ответ 2

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

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

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

Ответ 3

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

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

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

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

EDIT:

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

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

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

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

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

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

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

  • Таблицы символов - почти наверняка 8 бит (UTF8 или ASCII). Существуют некоторые странные исключения, но при реализации таблиц символов, превышающих 8 бит, в чистых реализациях используется стандартный тип wchar_t.
  • Подпись - это всегда два дополнения.
  • Целое число всегда использует все выделенные биты.

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

Ответ 4

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

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

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

char c[5], d[5];
c[0] = 0xF0;
c[1] = 0xA4;
c[2] = 0xAD;
c[3] = 0xA2;
c[4] = '\0';
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, которое не может быть представлено в подписанном char (диапазон -128 - +127), поэтому оно будет приведено к значение со знаком (-16).

Объявление char как unsigned удалит предупреждение, и всегда полезно иметь чистую сборку без какого-либо предупреждения.

Ответ 5

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

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

Ответ 6

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

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

Ответ 7

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

Ответ 8

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

"действительно" необходимо? Нет.

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

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

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

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

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

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