#arrays #c #pointers #casting #endianness
#массивы #c #указатели #Кастинг #порядковый номер
Вопрос:
У меня есть uint8_t
массив. Иногда мне нужно обработать два последовательных элемента как uint16_t
, и скопировать их в другую uint16_t
переменную. Элементы могут не обязательно начинаться с uint16_t
границы слова.
В настоящее время я использую memcpy для этого:
uint8_t bytes[] = { 0x1A, 0x2B, 0x3C, 0x4D };
uint16_t word = 0;
memcpy(amp;word, amp;bytes[1], sizeof(word));
gdb показывает, что это работает так, как ожидалось:
(gdb) x/2bx amp;word
0x7fffffffe2e2: 0x2b 0x3c
Приведение ссылки на элемент массива к uint16_t
тому, что будет скопирован только элемент массива, на который есть прямая ссылка:
word = (uint16_t)bytes[1];
(gdb) x/2bx amp;word
0x7fffffffe2e2: 0x2b 0x00
Использование uint16_t
указателя и указание его на адрес элемента массива, приведенного к uint16_t *
результатам, приводит к тому, что на два последовательных элемента ссылаются, но не копируются:
uint16_t *wordp = (uint16_t *)amp;bytes[1];
(gdb) x/2bx wordp
0x7fffffffe2e5: 0x2b 0x3c
bytes[1] = 0x5E;
(gdb) x/2bx wordp
0x7fffffffe2e5: 0x5e 0x3c
Присвоение обычной uint16_t
переменной значения, разыменованного из uint16_t
указателя, копирует оба последовательных элемента массива:
word = *wordp;
bytes[1] = 0x6F;
(gdb) x/2bx wordp
0x7fffffffe2e5: 0x6f 0x3c
(gdb) x/2bx amp;word
0x7fffffffe2e2: 0x5e 0x3c
gcc не выдает никаких предупреждений при использовании -Wall
опции.
Как я могу добиться того же результата без промежуточного указателя?
Есть ли какие-либо проблемы с использованием указателя, как описано выше, или с попыткой сделать это без промежуточного указателя?
Будет ли использование memcpy считаться предпочтительным при определенных сценариях?
Конечным использованием этого является обработка большого конечного байтового потока, поэтому я использую byteorder(3)
функции по мере необходимости.
Комментарии:
1. Есть ли какая-либо причина, по которой вы не хотите использовать
memcpy
версию? Это самый безопасный способ избежать неопределенного поведения , и я бы ожидал, что любой приличный оптимизатор оптимизирует копии, если это возможно2.
memcpy
считается предпочтительным из-за строгого правила псевдонимов. Другой вариант — сдвинуть и ИЛИ создатьuint16_t
. Это также устраняет проблемы с порядковым номером.3. Нет особой причины не хотеть использовать
memcpy
; это то, что я использую в 99% случаев. Первоначально я использовал shift и OR, но заменил их наmemcpy
. Затем я заметил связанное использованиеmemcpy
вызовов функций между цепочками, и возникли сомнения. Я думаю, что я знаю о вариантах, я просто не чувствую, что у меня достаточно опыта, чтобы сравнить их и оценить, что я должен использовать и почему.4. Стоит отметить, что оптимизаторы компилятора, которые видят константу
2
как размер дляmemcpy
вызова, почти всегда будут переписывать, чтобы вообще не включать вызов функции или цикл, и просто манипулировать байтами с помощью регистров. Тогда стоимость этого крайне незначительна, не беспокойтесь о накладных расходах функций или ненужном доступе к памяти за пределами кэшей.
Ответ №1:
uint8_t bytes[] = { 0x1A, 0x2B, 0x3C, 0x4D };
uint16_t word = 0;
memcpy(amp;word, amp;bytes[1], sizeof(word));
Вышесказанное хорошо. Копирует байты напрямую. Если вы с удовольствием копируете байты по порядку и не беспокоитесь о порядке следования, это способ сделать это.
См. Ответ P__J supports women в Польше для доказательства того, что компиляторы достаточно умны, чтобы оптимизировать это использование, поэтому вам не нужно беспокоиться об оптимизации самостоятельно.
word = (uint16_t)bytes[1];
Приведенное выше просто обнуляет ваше 8-битное значение до 16-битного значения (0x002B вместо 0x2B); предположительно, это не то, что вы хотите.
uint16_t *wordp = (uint16_t *)amp;bytes[1];
word = *wordp;
Не делайте вышеописанного. Это приводит к неопределенному поведению. A uint16_t
имеет более строгие требования к выравниванию, чем a uint8_t
, и вы можете получить недопустимый адрес для типа указателя. То есть, будучи двухбайтовым типом данных, он требует адресов, которые существуют на границах, кратных двум (например, 0x2, 0x4, 0x6 и т.д.), Хотя uint8_t
не имеет такого ограничения и может существовать в 0x1, 0x2, 0x3 и т.д.. (Подробнее о выравнивании см. Раздел 6.2.8 рабочего проекта C11)
При разыменовании вы также нарушили строгое правило псевдонимов, разыменовав объект с несовместимым типом. (Подробнее о совместимых типах см. Раздел 6.2.7 рабочего проекта C11)
Комментарии:
1. Спасибо, Кристиан. В ответ на ваше утверждение о порядке следования. Это зависит от того, что я делаю с данными. Для ситуаций, когда меня волнует порядковый номер, который я использую
ntohs()
для преобразования байтов после их копирования вuint16_t
переменную.2. Не могли бы вы также подробнее рассказать о строгих требованиях к выравниванию и о том, как я мог получить неверный адрес? Есть ли какой-нибудь способ заставить gcc предупредить меня? Или приведение меня твердо говорит, что я рад выстрелить себе в ногу?
3. Обновлено, добавлено немного больше о выравнивании и строгом сглаживании.
Ответ №2:
memcpy — самый безопасный способ. И при использовании современных компиляторов наиболее эффективный as memcpy вызываться не будет.
Пример (изменчивый для оптимизации orevent):
#include <stdint.h>
#include <string.h>
int main()
{
volatile uint8_t bytes[4];
volatile uint16_t word = 0;
memcpy(amp;word, amp;bytes[1], sizeof(word));
}
и memcpy преобразуется в
movzx eax, WORD PTR [rsp-3]
mov WORD PTR [rsp-6], ax