Я работаю с программным обеспечением, встроенным в минимальное оборудование, которое поддерживает только ANSI C и имеет минимальные версии стандартных библиотек ввода-вывода.
У меня есть переменная Int размером два байта, но мне нужно разделить ее на 2 байта отдельно, чтобы иметь возможность передать, а затем я могу, прочитав два байта, собрать исходный Int.
Я могу представить себе двоичное деление каждого байта следующим образом:
int valor = 522; // 0000 0010 0000 1010 (entero de 2 bytes)
byte superior = byteSuperior(valor); // 0000 0010
byte inferior = byteInferioror(valor); // 0000 1010
...
int valorRestaurado = bytesToInteger(superior, inferior); // 522
но мне не удается простым способом разделить целое по его весу, и это дает мне ощущение, что это должно быть тривиально (например, со смещением битов), и я этого не обнаруживаю.
На самом деле, любое решение, которое делит целое на 2 байта и собирает его заново, служит мне хорошо.
От уже большое спасибо!
Пусть вас не пугают длинные и сложные ответы. В основном это простая проблема, и легко написать достойный код, который будет работать на вашем компьютере в 100% случаев. Выберите один из ответов с участием >> и & 0xff, внимательно проверьте его, как предлагает мой ответ, и все будет в порядке.
@SteveSummit с акцентом на "на твоей машине";) Да, удивительно то, что звучит так просто, это такая сложная проблема в C. Я думаю, что все эти ответы есть, вы должны знать об этих вещах, даже если только для выбор метода, определяемого реализацией, наиболее подходящего для вашей (единственной) целевой системы :)
@FelixPalmen Раньше я был одним из самых одержимых портативностью программистов, которых я знал, но я думаю, что к старости я становлюсь слабым. И, конечно же, есть веские причины, по которым подписанные и неподписанные одинаковые размеры имеют тенденцию тихо и правильно преобразовываться друг в друга на всех популярных процессорах (и почему подписанное целочисленное переполнение имеет тенденцию повторяться так же предсказуемо, как и unsigned).





Фактически вы можете преобразовать адрес целочисленной переменной в указатель на символ (unsigned char*, если быть точным), прочитать значение и затем увеличить указатель, чтобы он указывал на следующий байт, чтобы снова прочитать значение. Это соответствует правилам псевдонима.
Я бы даже не стал писать функции для этого. Обе операции являются прямым применением побитовых операторов языка Си:
int valor = 522;
unsigned char superior = (valor >> 8) & 0xff;
unsigned char inferior = valor & 0xff;
int valorRestaurado = (superior << 8) | inferior;
Хотя это выглядит просто, при написании такого кода всегда есть несколько тонкостей, и легко ошибиться. Например, поскольку valor подписан, его сдвиг вправо с помощью >> определяется реализацией, хотя обычно это означает, что он может подписывать расширение или нет, что в конечном итоге не повлияет на значение байта, который & 0xff выбирает и назначает superior.
Кроме того, если superior или inferior определен как подписанный тип, во время реконструкции могут возникнуть проблемы. Если они меньше, чем int (как, конечно, обязательно), они будут немедленно расширены до int до того, как произойдет остальная реконструкция, уничтожив результат. (Вот почему я явно объявил superior и inferior как тип unsigned char в моем примере. Если ваш тип byte является typedef для unsigned char, это тоже будет хорошо.)
Также существует неясная возможность переполнения, скрывающаяся в подвыражении superior << 8, даже если superior беззнаковый, хотя на практике это вряд ли вызовет проблему. (Дополнительное объяснение см. В комментариях Эрика Постпишила.)
Значение valor >> 8 определяется реализацией, когда значение valor отрицательное. Хотя в показанном примере valor является положительным, этот код не предназначен для общего использования. Кроме того, учитывая 16-битный int, superior << 8 может переполниться, и в этом случае поведение не определено стандартом C.
@EricPostpischil Я был бы признателен за объяснение того, как superior << 8 может переполняться.
В вопросе указано, что int - это два байта. Предположим, 8-битные байты, максимальное значение int равно 32767. В коде в этом ответе superior - это unsigned char. Согласно C 2011 (N1570) 6.5.7 3 целочисленные рекламные акции выполняются для операндов <<. Согласно 6.3.1.1 2, целочисленные рекламные акции продвигают unsigned char в int. Значение superior может находиться в диапазоне от 0 до 255. Предположим, это 128 (или любое значение от 128 до 255). Согласно 6.5.7 4, если 128 × 2 ^ 8 не представляется в int, поведение 128 << 8 не определено. Поскольку 128 × 2 ^ 8 равно 32768, он не может быть представлен в int.
Кроме того, согласно 6.5.7 3, тип результата << - это тип продвинутого левого операнда, то есть это int. Таким образом, superior << 8 пытается сдвинуть unsigned char в старшие биты int. Если установлен старший бит unsigned char, это превышает значение int. Сдвиг влево значения со знаком математически определяется стандартом C, а не как битовая операция, поэтому он переполняется, а не определяется для установки бита знака.
@EricPostpischil А, верно. Спасибо. Возможно, мне следовало принять близко к сердцу слова «математический подход» в другом моем ответе и вместо этого пойти по дороге superior * 256 + inferior. (Разумеется, с перевернутым мышлением, и superior вынужден явно указать тип подписано.)
superior * 256 тоже переливается. Необходимо выполнить арифметику в более широком типе, или обусловить ее использование разных выражений для разных значений, или реализовать какой-то другой обходной путь.
@EricPostpischil Хм. Я не думал, что superior * 256 может переполниться, но я не думал, что superior << 8 может, поэтому держу пари, что вы собираетесь рассказать мне, как может происходить знаковое умножение. :-)
@SteveSummit с 16-битным int, может, если superior больше 127.
@FelixPalmen Конечно. Но если superior - это подписано и 8 бит (как в этом подпотоке обсуждения), конечно, он не может быть больше 127.
@SteveSummit: В коде этого вопроса superior - это unsigned char. Когда применяются целочисленные рекламные акции, он становится int, но значение не изменяется. Таким образом, 128 или 255 в superior превратятся в int со значением 128 или 255. Умножение этого на 256 в реализации с 16-битным int приводит к переполнению.
@EricPostpischil В комментарии, предлагающем умножение на 256, я также указал (в скобках), что в этом случае нам придется вернуться к использованию явно подписанных значений для таких вещей, как superior, а не без знака.
Это непростая задача.
Прежде всего, тип данных для байт в C - char. Вы, вероятно, захотите здесь unsigned char, поскольку char может быть подписанным или неподписанным, это определяется реализацией.
int - это тип со знаком, поэтому сдвиг вправо также определяется реализацией. Что касается C, int должен иметь 16 бит по меньшей мере (что было бы 2 байта, если char имеет 8 бит), но может иметь больше. Но поскольку ваш вопрос написан, вы уже знаете, что int на вашей платформе имеет 16 бит. Использование этих знаний в вашей реализации означает, что ваш код специфичен для этой платформы и не является переносимым.
На мой взгляд, у вас есть два варианта:
Вы можете работать со значением вашего int, используя маскировку и битовый сдвиг, например:
int foo = 42;
unsigned char lsb = (unsigned)foo & 0xff; // mask the lower 8 bits
unsigned char msb = (unsigned)foo >> 8; // shift the higher 8 bits
Это имеет то преимущество, что вы не зависите от расположения вашего int в памяти. Для реконструкции сделайте что-нибудь вроде:
int rec = (int)(((unsigned)msb << 8) | lsb );
Обратите внимание, что преобразование msb в unsigned здесь необходимо, иначе он будет преобразован в int (int может представлять все значения unsigned char), что может привести к переполнению при сдвиге на 8 позиций. Как вы уже сказали, ваш int имеет «два байта», в вашем случае это очень вероятно.
Последним преобразованием в int также является определяется реализацией, но он будет работать на вашей «типичной» платформе с 16-битным int в дополнении 2, если компилятор не сделает что-то «странное». Сначала проверив, не слишком ли велик unsigned для int (потому что исходный int был отрицательным), вы могли бы избежать этого, например.
unsigned tmp = ((unsigned)msb << 8 ) | lsb;
int rec;
if (tmp > INT_MAX)
{
tmp = ~tmp + 1; // 2's complement
if (tmp > INT_MAX)
{
// only possible when implementation uses 2's complement
// representation, and then only for INT_MIN
rec = INT_MIN;
}
else
{
rec = tmp;
rec = -rec;
}
}
else
{
rec = tmp;
}
Дополнение 2 здесь подходит, потому что правила преобразования отрицательного int в unsigned явно указаны в стандарте C.
Вы можете использовать представление в памяти, например:
int foo = 42;
unsigned char *rep = (unsigned char *)&foo;
unsigned char first = rep[0];
unsigned char second = rep[1];
Но будьте осторожны, будет ли first MSB или LSB, зависит от порядок байтов, используемого на вашем компьютере. Кроме того, если вашего int содержит биты заполнения (крайне маловероятно на практике, но разрешено стандартом C), вы также их прочтете. Для реконструкции сделайте что-нибудь вроде:
int rec;
unsigned char *recrep = (unsigned char *)&rec;
recrep[0] = first;
recrep[1] = second;
Я уверен, что это самый полный ответ на данный момент, поэтому любой, кто голосует против, может захотеть объяснить свои сомнения ...
@EricPostpischil уверен, что это так, он даже сделал перед, вы написали свой первый комментарий. В любом случае, «частичный» ответ все равно не даст повода для отрицательных голосов.
Извините, не знаю, как пропустил реконструкцию. Однако реконструкция в части 1 неверна, поскольку преобразование из unsigned в int определяется реализацией, когда значение не может быть представлено в int.
Для всех практических целей C является полным по Тьюрингу, и поведение преобразований, определяемое реализацией, можно обойти. Даже если бы это не удалось обойти, это можно было бы задокументировать в ответе.
@EricPostpischil это можно обойти, но не при работе с битами значения. И я четко упомянул "реализацию, определенную" во введении, я не вижу причин разбрасывать ее повсюду.
Во введении говорится, что подписанность char определяется реализацией. Это не утверждение, что код в ответе полагается на поведение, определяемое реализацией при преобразовании из unsigned в int.
@EricPostpischil во введении больше говорится о непереносимом коде. И такое поведение действительно мешает людям больше писать ответы. В любом случае, я даже добавил сюда полное объяснение.
rec = ~tmp + 1; не может быть представлен в int, если tmp представляет собой INT_MIN. Пример работающей конвертации - int foo(unsigned x) { if (x <= INT_MAX) return x; else if (x == INT_MIN) return INT_MIN; else return - (int) -x; }. Apple LLVM 9.1.0 (clang-902.0.39.2) полностью оптимизирует это. Он даже полностью оптимизирует int foo(unsigned x) { if (x <= INT_MAX) return x; else { int y = 0; while (x++) --y; return y; } }, хотя я бы не рекомендовал этого, поскольку другие компиляторы могут этого не делать.
Мой голос против (который я удалил) был связан с тем, что в этом ответе был указан код который сломается при некоторых обстоятельствах. Вы могли бы избежать этого голосования, просто указав поведение, определяемое реализацией, как предварительное условие. Вы комментируете «такое поведение», но я рад препятствовать получению плохих ответов, проголосовав за них и объяснив, почему. Нет необходимости защищать свои драгоценные ответы как личное оскорбление. Относитесь к нему как к неодушевленному объекту, к которому у вас нет никакой личной связи. Если у него есть дефекты, значит, у него есть дефекты, и нужно исправить их, а не спорить о них.
У него не было "дефекты", и я, делать, считаю, что это личное голосование (с некоторыми усилиями), особенно, когда даже не оставляю комментарий (что не относится к ваш downvote). И что касается вашего другого комментария, в моем коде tmp не может быть INT_MIN в этой строке.
Ну, я понимаю, вы имеете в виду "эквивалент" INT_MIN ... это действительно проблема, странный угловой случай, но исправляется простой дополнительной строкой.
Под tmp как INT_MIN я имел в виду, что он имеет значение, соответствующее INT_MIN, в частности (unsigned) INT_MIN, которое в реализации OP равно 32768. Если msb равен 128, а lsb равен 0, то tmp установлен на 32768, ~tmp равен 32767, ~tmp + 1 равен 32768, а rec = ~tmp + 1; переполняется, потому что int не может представлять 32768.
@EricPostpischil уже понял (см. Предыдущий комментарий) и исправил, выполнив дополнение 2 в unsigned.
В новом коде: Снова tmp, созданный из msb, и lsb может быть 32768. Затем tmp = ~tmp + 1; устанавливает tmp на 32768, и rec = tmp; пытается присвоить int значение, которое int не может представлять. В этом случае выполняется преобразование, и поведение определяется реализацией, а не неопределенным, согласно 6.3.1.3 3. OP не указал, что определяет реализация, поэтому мы не можем знать, будет ли этот код работать или нет.
Я устал. Конечно, это ничего не меняет. Вот почему в моем первом примере я решил довериться «нормальной» реализации, основанной на двух дополнениях, все остальное (без доступа к представлению) просто странно и сложно. Помимо оптимизаторов, должен быть способ избежать еще одного особого случая? : o в любом случае я напишу один.
Как Эрик прокомментировал в другом месте и в своем ответе: вы можете выполнять все сдвиги и соединения без знака и memcpy в / из int по краям. Лично я считаю, что это граничит с безумием, жертвуя реальной практической эффективностью ради теоретической переносимости на машины, которых, возможно, даже не существует. Но каждому свое. :-)
@SteveSummit во всем этом безумии, я бы назвал memcpy() в / из unsigned чем-то вроде элегантный ... но, конечно, это тоже имеет доступ к представлению :)
Я использую int shrot вместо int для dry, потому что на ПК это 4 байта, а на моей целевой платформе - 2. Используйте unsigned, чтобы упростить отладку.
Код компилируется с помощью GCC (и должен делать это практически с любым другим компилятором C). Если я не ошибаюсь, это зависит от архитектуры big endian или little endian, но это можно решить путем инвертирования строки, восстанавливающей целое число:
#include <stdio.h>
void main(){
// unsigned short int = 2 bytes in a 32 bit pc
unsigned short int valor;
unsigned short int reassembled;
unsigned char data0 = 0;
unsigned char data1 = 0;
printf("An integer is %d bytes\n", sizeof(valor));
printf("Enter a number: \n");
scanf("%d",&valor);
// Decomposes the int in 2 bytes
data0 = (char) 0x00FF & valor;
data1 = (char) 0x00FF & (valor >> 8);
// Just a bit of 'feedback'
printf("Integer: %d \n", valor);
printf("Hexa: %X \n", valor);
printf("Byte 0: %d - %X \n", data0, data0);
printf("Byte 1: %d - %X \n", data1, data1);
// Reassembles the int from 2 bytes
reassembled = (unsigned short int) (data1 << 8 | data0);
// Show the rebuilt number
printf("Reassembled Integer: %d \n", reassembled);
printf("Reassembled Hexa: %X \n", reassembled);
return;
}
OP запрашивает код для разделения байтов подписанного int, но этот ответ показывает код для unsigned short и не объясняет, как его использовать для int или short. Поскольку адаптация кода для подписанных типов подвержена ошибкам из-за семантики C в отношении подписанных типов, битовых сдвигов и переполнений, это является проблемой. Например, data1 << 8 будет переполняться в реализациях C 16-битными типами int, если установлен старший бит data1. Это связано с тем, что unsigned chardata1 будет повышен до int, поэтому сдвиг будет выполняться в подписанном типе int.
OP запрашивает решение, которое собирает байты, что я и сделал
Проблема не в том, что этот код не предоставляет решения для повторной сборки байтов, что он не предоставляет решение для повторной сборки байтов двухбайтового int, как того требует проблема. Предоставление кода для unsigned short при запросе int не является решением.
Я не вижу, чтобы он явно запрашивал int, как вы можете прочитать в OP: «На самом деле, любое решение, которое делит целое на 2 байта и повторно собирает его, мне хорошо служит». Я думаю, не имеет значения, если это int или short, если честно
«У меня есть переменная Int размером два байта, но мне нужно разделить ее на 2 байта по отдельности, чтобы иметь возможность передать ее, а затем я могу, прочитав два байта, собрать исходный Int». [Курсив добавлен.]
Я повторю это очередной раз только для вас: «На самом деле, любое решение, которое делит целое на 2 байта и собирает его заново, служит мне хорошо». [Курсив добавлен.]
Я повторю это очередной раз только для вас: вопрос требует решения для int.
Это код неправильный, потому что он выходит за пределы целевой реализации OP: data1 << 8.
вы совершенно неправильно, я только что протестировал его, и он работает на моей машине.
Ребята. Мир. Вы не собираетесь убеждать друг друга. Один из вас говорит о коде, который на сегодня достаточно хорош; один из вас говорит о коде, который гарантированно будет работать на любой машине сейчас или до скончания веков. Оба ответа имеют свое место; ни то, ни другое не является полностью правильным или полностью неправильным.
Тот факт, что он «работает» на вашем компьютере, ни в коем случае не свидетельствует о том, что он работает на реализации OP. Правила для C устанавливаются стандартом C, а не тем, как работает ваша машина, и OP конкретно запрашивает решение для своей машины, а не для вашей.
Просто определите союз:
typedef union
{
int as_int;
unsigned char as_byte[2];
} INT2BYTE;
INT2BYTE i2b;
Поместите целочисленное значение в член i2b.as_int и получите байтовый эквивалент из i2b.as_byte[0] и i2b.as_byte[1].
который имеет те же последствия (например, порядок байтов, биты заполнения), что и "вручную" псевдоним с unsigned char
@FelixPalmen Кто сказал, что оба конца имеют разный порядок байтов?
Хорошо, я? Кто сказал, что эта платформа не имеет биты заполнения? Я просто думаю, что следует добавить несколько слов предостережения, рекомендуя изучить представление.
Мое решение - простой и прямой ответ на вопрос «Как разделить Int на два байта в C». Никакой высокой переносимости или других претензий. Судя по стилю определения вопроса, я предполагаю, что ОП в этом нуждается.
Что очень плохо. Основываясь на приведенных здесь ответах, я бы не стал винить OP за то, что он испугался подхода сдвига и маски и вместо этого принял подход на основе char * или union. Но в реальном мире я считаю, что методы, основанные на char * и union, с большей вероятностью будут иметь реальные ошибки или проблемы с переносимостью, чем прилично написанный код сдвига и маски.
Метод @SteveSummit union - это «другой» метод. Когда многие ответы объясняют метод сдвига, что я могу сделать - предложить другой метод или снова повторить классическое преобразование сдвига? И это массив char, а не char *.
@ i486 Не волнуйтесь, я не говорил, что с вашим ответом что-то не так. Когда я сказал «очень плохо», я сетовал на поворот этого вопроса и всех его ответов, создавая впечатление, что техники сдвига и маски страшны и опасны, и их следует избегать. Я не верю, что это так, но чем больше слов о них пишут здесь, тем страшнее они кажутся, поэтому я постараюсь прекратить писать сейчас. :-) (P.S. Нет, вы не использовали char *, но использование указателей на символы - это еще один способ получить байты типа int.)
Как видно из нескольких ответов, есть несколько подходов и некоторые, возможно, удивительные тонкости.
«Математический» подход. Вы разделяете байты, используя сдвиг и маскирование (или, что то же самое, деление и остаток), и аналогичным образом рекомбинируете их. Это «вариант 1» в Ответ Феликса Палмена. Преимущество этого подхода в том, что он полностью не зависит от проблем с порядком следования байтов. У него есть сложность, заключающаяся в том, что он подвержен некоторым проблемам с расширением знаков и определенностью реализации. Безопаснее всего использовать тип unsigned как для составного int, так и для частей уравнения, разделенных байтами. Если вы используете подписанные типы, вам обычно потребуются дополнительные приведения и / или маски. (Но с учетом сказанного, я предпочитаю этот подход.)
"Память" подход. Вы используете указатели или union для прямого доступа к байтам, составляющим int. Это «вариант 2» в ответе Феликса Палмена. Очень важной проблемой здесь является порядок байтов, или "порядок байтов". Кроме того, в зависимости от того, как вы его реализуете, вы можете столкнуться с правило "строгого алиасинга".
Если вы используете «математический» подход, сделайте Конечно, вы протестируете его на значениях, которые имеют и не имеют старшего бита различных установленных байтов. Например, для 16 бит полный набор тестов может включать значения 0x0101, 0x0180, 0x8001 и 0x8080. Если вы напишете код неправильно (если вы реализуете его с использованием подписанных типов или если вы опустите некоторые из необходимых в противном случае масок), вы обычно обнаружите, что дополнительные 0xff вкрадываются в восстановленный результат, искажая передачу. (Кроме того, вы можете подумать о написании формального модульный тест, чтобы вы могли максимизировать вероятность того, что код будет повторно протестирован и обнаружены любые скрытые ошибки, если / когда он перенесен на машину, которая делает различные варианты реализации, которые повлиять на это.)
Если вы действительно хотите передавать значения со знаком, у вас возникнут дополнительные сложности. В частности, если вы реконструируете свое 16-битное целое число на машине, где тип int больше 16 бит, вам, возможно, придется явно подписать расширение, чтобы сохранить его значение. Опять же, всестороннее тестирование должно гарантировать, что вы адекватно устранили эти сложности (по крайней мере, на тех платформах, где вы тестировали свой код до сих пор :-)).
Возвращаясь к предложенным мной тестовым значениям (0x0101, 0x0180, 0x8001 и 0x8080), если вы передаете целые числа без знака, они соответствуют 257, 384, 32769 и 32896. Если вы передаете целые числа со знаком, они соответствуют 257, 384, -32767 и -32640. И если на другом конце вы получите значения вроде -693 или 65281 (которые соответствуют шестнадцатеричному 0xff01), или если вы получите 32896, когда вы ожидали -32640, это означает, что вам нужно вернуться и быть более осторожными со своими подписанными / неподписанными использование, с вашей маскировкой и / или с вашим явным расширением знака.
Наконец, если вы используете подход «памяти» и если ваш код отправки и получения выполняется на машинах с разным порядком байтов, вы обнаружите, что байты поменялись местами. 0x0102 превратится в 0x0201. Есть разные способы решить эту проблему, но это может доставлять неудобства. (Вот почему, как я уже сказал, я обычно предпочитаю «математический» подход, чтобы просто обойти проблему порядка байтов.)
Конечно, есть и гибридный подход. memcpy в unsigned, разделите биты, отправьте их. Для приема соберите unsigned из битов, затем memcpy в int.
Учитывая, что int состоит из двух байтов, а количество битов на байт (CHAR_BIT) равно восьми и используется дополнение до двух, int с именем valor может быть разобран в независимом от порядка порядке следования байтов с помощью:
unsigned x;
memcpy(&x, &valor, sizeof x);
unsigned char Byte0 = x & 0xff;
unsigned char Byte1 = x >> 8;
и может быть повторно собран из unsigned char Byte0 и unsigned char Byte1 с помощью:
unsigned x;
x = (unsigned) Byte1 << 8 | Byte0;
memcpy(&valor, &x, sizeof valor);
Заметки:
int и unsigned имеют одинаковый размер и выравнивание согласно C 2011 (N1570) 6.2.5 6.unsigned, поскольку C требует, чтобы значение UINT_MAX было не менее 65535, поэтому для представления значения необходимы все 16 битов.int и unsigned имеют одинаковую последовательность байтов согласно 6.2.6.2 2.Возможно, более последовательным было бы использование & 0xff при вычислении как Byte0, так и Byte1, или ни одного из них.
Вы отправляете данные из одной системы в другую, у которой другой порядок байтов? Если это так, вы можете использовать
htons()иntohs(), если они доступны в ваших системах. Вы используетеhtons(), чтобы преобразовать двухбайтовое значениеintв сетевой порядок байтов, затем, получив его, вы используетеntohs(), чтобы преобразовать его обратно в порядок байтов хоста для хоста, на котором вы его получили.