Неочевидные тонкости языка С – делимся опытом

Возможно, для кого-то, описанная далее особенность давно известна. Но для меня наступление на данные грабли пару лет назад при переносе кода с одной платформы на другую это было «приятным сюрпризом».

И так, есть код:

uint32_t i = 1;
i = i << 32;
printf("%d", i);


Вопрос – что выдаст printf?

Я, честно говоря, ожидал получить 0, но printf уверенно выводил все что угодно только не 0. Вернее на одной платформе выдавал 0 (x86, Visual Studio 2008) а на другой – мусор (ARM7, GCC). Потратив N часов на ковыряние в асме и вырвав N * M волос на голове я нашел ответ в первоисточнике — стандарте С. Для операции сдвига результат будет неопределен (зависть от реализации компилятора) если мы сдвигаем N битную переменную на N или более бит. Аналогично, если сдвигать на « минус N» бит (i = i << -1;), хотя этот случай и без знаний стандарта выглядит извратно.

Сам оригинал стандарта можно почитать тут. Данная фича описана в разделе 6.5.7 Bitwise shift operators: «… If the value of the right operand is negative or is
greater than or equal to the width of the promoted left operand, the behavior is undefined.»

Если вы сталкивались с подобными тонкостями – не стесняйтесь, поделитесь с Сообществом в комментариях :)Тем самым вы сэкономите кому-то кучу времени и нервов.

UPD: в примере я привел вырожденный случай – в оригинале было что то типа x = y << a; причем «a» передавался как параметр в функцию и изменялся в диапазоне 0… 32

UPD2: Нашел полезную статью (правда на английском) по «неопределенностям» (Undefined Behavior) в С. Приведенный мной случай там тоже описан.
  • +1
  • 17 октября 2011, 17:16
  • e_mc2

Комментарии (120)

RSS свернуть / развернуть
Ничего странного здесь нет. Естественно, Си не сам сдвигает. Сдвигает процессор. Задача Си — лишь сгенерить соответствующую инструкцию. Например,
shl eax,cl
Тут уже всё зависит от платформы. Например, x86 просто обрезает cl до 5 бит (0..31).
Соответственно, 32 & 0x1f = 0. Так что сдвига не будет
-1=0xffffffff & 0x1f = 31. Сдвинет на 31 бит.
etc.
0
Дело не в процессоре а в стандарте. В конечном итоге все делает процессор. Но С не обязательно транслировать операцию сдвига «в лоб» в ассемлерный shl. Более того, процессор может не иметь инструкции сдвига в своем наборе команд.
Как раз задача высокоуровневого языка абстрагировать от набора команд и разрядности регистров конкретного процессора. Это обеспечивает переносимость. Если бы каждая операция (сложение, деление) зависла от платформы – то какой смысл в высокоуровневом ЯП?
0
Например, некоторые процессоры не имеют операции деления в своем наборе команд. Но в стандарте С деление определено. И программеру пофиг как компилятор будет выкручиваться – деление должно работать, притом не абы-как, а соответствовать стандарту.
0
Если хочешь, чтобы говнокод работал одинаково на любой платформе, юзай всякую там джаву.
Си же по умолчанию подразумевает, что ты отдаёшь себе отчёт в том, что ты делаешь.
0
Отдавать себе отчет в том, что ты делаешь нужно всегда. Даже на java :) Я просто хочу сказать, что описанная а статье фича – это фича стандарта, а не платформы. Тобишь, на одной и той-же платформе разные компиляторы могут выдавать в данном случае разный результат, и оба будут «правы» потому как в стандарте явно не указано как поступать в данном случае.
0
Ну и бредовое утверждение. Если один и тот-же код работает по разному на разных процессорах, то это недочет языка программирования а не программиста.
-1
Меньше слушай академиков и больше юзай здравый смысл. Добро пожаловать в реальный мир.
0
Встречаем в сишном коде деление, в то время как процессор его не поддерживает. Значит это гавнокод.
Встречаем в коде XOR. Снова гавнокод.
Видим С++. И опять говнокод. C++ требует развитую систему для работы со стеком. В большинстве МК этого нет.

Следуя твоей логике очень многое можно понимать как гавнокод. Но в реале это недостаток языка программирования.
0
В реале всегда можно написать так, чтобы код выполнялся по разному на различных девайсах. А можно написать так, чтобы выполнялся одинаково. Для этого нужно думать прежде, чем делать. Таки семь раз отмерь, один раз отрежь. Но почему-то на такую, вроде бы, простую и эффективную вещь все дружно стали забивать. Включая разработчиков процессорв, кстати.
Так, как хотят академики не будет никогда. Поскольку язык — это не волшебная палочка, выполняющая любые желания, а средство общения с железом. Интерфейс не тока к человеку, но и к железу. О чём быдлокодеры дружно стараются не думать.
+2
У тебя искривленное понимание быдлокода.
Язык и бинарник это разные вещи. Если у одной и той-же команды на Си разная логика работы, то:
1) это ошибка компилятора.
2) недостаток языка или стандарта.

Причем этот недостаток на уровне кода а не на уровне бинарника.
Язык должен трактоваться однозначно а не как хочется. Вот если б в стандарте четко прописали поведение команды смещения, то этого глюка никогда бы и не было.
0
Но не включили, и очень правильно поступили.
Но мы, конечно, любим критиковать то, на что умнейшие люди потратили годы, тщательно продумывая и прорабатывая. И хвалить поделки, слепленные левой пяткой.
0
С чего ты взял, что они это намерено пропустили?
Это могло быть банальной ошибкой. Или умные люди уже не ошибаются? А может умные люди спроектировали этот язык для одного процессора и на такой нюанс тупо забили.

Критика это как раз очень важный элемент. Не обращать внимание на нее весьма глупо.
0
Всё, что нужно, они учли.
Если учитывать все эти нюансы, получится джава, требующая стопицотмегабайтного рантайма и жрущая сотни памяти. Такого красивого и прозрачного инструмента, как Си не получилось бы.
Отцы-основатели справедливо полагали, что если человек будет писать прямо, то проблем у него не возникнет. А если лезет в неоднозначные, аппаратно-зависимые нюансы, то чётко осознаёт что он делает и зачем.
Никто не думал, что через некоторое время наступит подобное засилье быдлокодеров.
+1
НЕ переводи тему. Речь идет об исходнике а не об бинарнике.
0
Не разделяй «исходник» и «бинарник». Процессор не говорит на лунном языке, а предоставляет готовые типичные абстракции для использования программой.
0
Тогда использовать XOR и деление это ошибка. Или процессор научился разговаривать на лунном языке?
Это не нормально, что один и тот-же код читается по разному в зависимости от архитектуры процессора.
0
Деление это не лунный язык а довольно операция, раскладываемая на стандартные сравнение, вычитание и сдвиг.
0
довольно несложная операция *
0
А смещение это значит лунная операция???
Где можно достать словарь лунного языка?
0
Сдвиг — это стандартнейшая вещь. Посторонних вещей в си нету)
0
А почему тогда логика операции деления работает одинаково на разных процессарах а сдиг нет?
Если сдвиг стандартен то и работать он должен стандартно. Но работает он не стандартно.
0
Потому что стандарт не определяет это поведение. Хотя я и не одобряю обилие UB в С. Они похожи на непроработанные места (особенно i++ + i++ — в паскале аналогичные операторы inc/dec неспроста не возвращают результата).
0
вы явно не понимаете сущность Си как макроассемблера. Си — не язык высокого уровня. Он недостаточно выразителен и абстрактен. Все, что пишется на Си несложно за пару часов странслировать в ассемблер руками (без учета оптимизаций). Если у машины есть операция сдвига, компилятор использует ее. если у машины нет операции деления, компилятор генерит код, реализующий его. Точно так же поступили бы вы, если бы писали на ассемблере и возникла необходимость сдвигать или делить
0
По уровню абстракции он выше. В С есть функции. Ну и вообще, С соответсвует определению именно ЯВУ, а не ассемблера. А всякие «язык среднего/низкого уровня, кроссплатформенный ассемблер» — метафоры, намекающие на относительную скромность средств языка (он, все же, весьма старый) и его широкое применение в той нише, которую прежде занимал ассемблер.
0
Именно на лунном он и говорит. Выполняет некоторые операции, пусть и для большинства процессоров в их наборе много общего.
С же ЯВУ и к языку процессора отношения не имеет. Теоретически. А на практике — то не делай, это не делай — UB. И про разрядность/эндианность не забывай. Вот и получаются программы, на 50% состоящие из платформозависимых дефайнов.
0
В джаве разрядность всегда одинаковая. К тому же когда появляется неоднозначность, то ее исправляют в следующей версии.
0
Но не придуман от балды, а спроектирован для удобного выполнения программок на нём. Плевать на это и абстрагироваться от него — не всегда удачно. Авторы Си смогли и обеспечить платформонезависимость (при условии вдумчивого написания кода) и не игнорировать работу, проделанную разработчиками процессора. За что честь им и хвала. Лишний слой абстракции — это очень дорого (не только по части производительности).
0
Речь не идет об бинарнике. А об исходном коде. Это разные вещи. Бинарник нельзя привязать к исходному коду. Структура бинарного файла, в первую очередь, зависит от оптимизации а не от кода.

Мне ненравится джава, тем что они перегнули палку с переносимостью бинарника.Из-за это он работает медленно. Но зато код читается одинокого.
0
Да не так уж и медленно. Отставание от нативного, хорошо оптимизированного кода — 10-20%.
Ну кроме тех платформ, где жаба чисто интерпретируемая.
0
Он спроектирован для простой компиляции на PDP-11 для PDP-11. Делать его кроссплатформенным уже позже стали, вот косяки и полезли.
0
И что же в Си такого специфичного для этой архитектуры?
Операция сдвига в Си ведёт себя по разному исключительно потому, что она по разному ведёт себя разных процессорах. Но в целом, это одна и та же операция.
Естественно, эмулировать можно что угодно. Но Си таким не занимается.
Если бы сейчас разрабатывали минималистичный язык без оверхеда, получился бы тот же самый Си.
0
Понятия минималистичности разные. У Вирта, например, исходя из принципа минималистичности, получился Оберон. На С, мягко говоря, не похожий.
Понятия оверхеда тоже. Язык без оверхеда давно разработан. Ассемблер называется. Все остальные с оверхедом. Включая С.
0
Этих ваших Оберонов и соседних языков больше 9000. И каждый минималистичен. Мда. Простой и минималистичный инструмент должен быть универсален. Оберон требует строго определённой платформы. Ня!..
Оверхед измеряется относительно решения и изначально поставленной задачи. Иначе мона сказать, что всё кроме атомов — оверхед.
0
Задачи тоже разные бывают. Для многих и LISP не оверхед (который AFAIK вообще не компилируется).
А для attiny12 и C оверхед — негде стек с фреймами создавать.
В Си есть весьма неприятная и кривая вещь — битовые поля. Эмулируются.
Ну почему, на некоторых платформах они нативно доступны. MCS-51 например.
Зато деление почти во всем эмбеде эмулируется. И даже умножение.
0
Занимается. И С++ тоже занимается. Но только частично. Такое ощущение, что они делали делали а после им надоело.
0
В Си есть весьма неприятная и кривая вещь — битовые поля. Эмулируются. В остальном, всё очень прилично.
0
Позвольте вмешаться :) Для меня тоже загадка – почему они так поступили в стандарте. Это не ошибка – если бы это было ошибкой ее бы пофиксили в очередной версии стандарта: все равно в предыдущем стандарте результат не определен, теперь путь будет например 0, обратная совместимость не теряется. Зависимость от платформы – это стандарт волновать не должно. В результате 0 и все. Пусть на некоторых платформах пришлось бы компилятору генерить дополнительный код для обнуления результата.
0
Наверно всё дело в инженерном мышлении.
0
Пусть на некоторых платформах пришлось бы компилятору генерить дополнительный код для обнуления результата.
Потому вероятно и не объявляют. Это не единственный UB (undefined behavior) в С и вообще говоря их следует знать. Хотя, честно говоря, раньше я знал только про UB i++ + i++.
0
Как по мне — так нормально.
Представим гипотетический проц, который при сдвиге «вытягивает» еденицу. Результ что, обнулять?

И если всякой ерундой не страдать, вроде сдвига на 100500 битов и UB знать, то в целом вполне кросплатформенно можно писать.
+1
По нормальному есть два сдвига, логический и арифметический. Если они работают по разному на разных процессорах, то это не нормально.
0
Это не «гавнокод», а особенности реализации стандарта конкретным компилятором. Хочешь писать переносимо — будь добр следовать стандарту и _особенностям_. А для разовой поделки пойдет любое, но потом нечего плакаться на такие вот «особенности» и (баго)фичи. А Си тем и хорош, что позволяет даже выстрелить себе в ногу (с), если так уж сильно хочется.
0
У компилятора сменился разработчик и в новой версии изменилась логика. Программа которая работала раньше вдруг перестает работать.

Вот это «особенность»))
0
Хм. А что в этом нового? Особенно видно у IAR, например. Проект, который отлично собирается в тройке, в четверке уже выдает кучу ошибок в лучшем случае. Кейл тоже таким грешит. Неужели это такая новость?
0
Да, и винавр тоже не исключение.
И хорошо, если проект тупо не собирается. Гораздо веселее, когда собирается без единого варнинга, а работает как бог на душу положит — вот там иди ищи где что поломалось… %)
0
Иар в стандарт не лезет. Там постоянно меняется часть зависимая от микроконтроллера.
0
Хех. А кто лезет? У всех логика так или иначе меняется в зависимости от целевой платформы.
Доказательство «годного» компилятора — программа на 10к+ строк кода, спокойно компилящаяся на любой поддерживаемой платформе с процентом платформенно-зависимого кода в районе 0,5-1%. Что-то мне таких не припоминается.
0
Программа которая работала раньше вдруг перестает работать
Эта программа — говнокод.

NetBSD портирован на 53 архитектуры и ничего, не глючит.
0
Он позволяет также выстрелить себе в ногу когда не так уж сильно хочется и даже когда совсем не хочется.
0
Ну что поделаешь, это цена за скорость и удобство разработки. Асм позволяет и не такие грабли себе устроить, не будем спорить…
0
У меня вообще за правило вошло при передаче исходников (когда отдаю) оговаривать версию компилятора. И это нормально.
0
Нормально. Но это не значит, что это хорошо.
0
Эээ… Отвечу одним разом на оба коммента.
>> Точнее это цена за простоту компиляции С и скорость/размер получаемого кода.
>>Удобством разработки как раз платить приходится.
Как по мне, так на сях удобнее, чем на асме.

>>Нормально. Но это не значит, что это хорошо.
Дык я и не говорю, что это хорошо. Это даже и не очень нормально. Но что поделаешь, таковы реалии. И мир, в котором мы живем далек не то что от идеальности, а даже от «нормальности»… ;) Но мы ведь в нем живем. :)
0
А я не с асмом и сравниваю. Скорее с более проработанными в сторону устранения подобных засад ЯВУ.
Скажем, уже упоминавшийся здесь Оберон проработан в сторону минимизации вероятности возникновения ошибок и в сторону минимального, предельно простого языка. На нем писать поудобнее. Но увы, наличие средств разработки выбора на чем писать практически не оставляет.
0
«Примечание: Работает только с компилятором XXX и только версии X.X.X.X»

;)
0
Ну да. А что такого? Мы ведь говорим об эмбеддинг-программинге.
Впрочем, попробуй собери код для билдера — студией. Или даже разными версиями делфей. Скажем, проект из третей делфи в семерке. (последняя, с которой имел дело...)
0
С джавай и С# я таких «особенностей» не помню. Хотя и студию постоянно обновляю.
0
А мы разве о жабе и шарпе? =0 Еще визуальный васик приплети сюда. )))))))
0
Код от билдера в студии собрать можно. Будут проблемы с гуем. Его реализация стандартом не описывалась.
0
Это если не использовать расширения билдера. Стандарт он говорят реализует достаточно аккуратно, а вот расширениями увешан весьма обильно, чтобы VCL можно было подключить.
0
VB6 очень хорошая штука :) На рутрекере даже выложили портативную версию на 40Мб с некоторыми наворотами в плане удобства кодинга. Там правда потоков нет и консольную прогу не так просто получить, зато очень удобная среда для быстрого наброска какой-нить утилиты для винды.
0
заметь текст в скобках:
>> (x86, Visual Studio 2008) а на другой – мусор (ARM7, GCC)
ни на что не намекает? ;)
0
В дельфи-то как раз с обратной совместимостью неплохо. Хотя и есть пара версий, ломавших ее (скажем, ограничение на инициализированные переменные после пятой). Но в целом, {$IFDEF VER_XXX}… {$ENDIF} программы и библиотеки на ней не так пестрят, как исходники на С, да и те преимущественно для того, чтобы на новых версиях использовать их новые возможности.
Правда, тут один компилятор одной фирмы и одна платформа. Это сильно облегчает дело.
0
Если я скомпилировал программу для 32-разрядной системы, а пользователь перешёл на 64-р и она перестала работать, то может ли он требовать с меня доработки программы под 64-р? В каком случае? Только в случае, если он в ТЗ указал это. Если нет, то извините, предсказание будущего — это не ко мне.

Если исходник (avr) не компилируется на новом компиляторе, то какие ко мне претензии, ведь бинарик работает? Если же в ТЗ не указано требование работы в будущем компиляторе, то и притензий быть не может. Если же указано, то исходник будет отдан только после выхода этого компилятора ;)

Вообще, в ГОСТе на ТЗ, насколько я помню, (или договоре) есть такой пункт как составление Акта выполненных работ. Т.е. работа считается законченной, когда такой Акт подписан обеими сторонами. Дальше только Гарантия, а после — новое ТЗ.
0
В случае винды это будет работать без проблем. В случае линкса возможно они не догадались или слишком умные это решить.
0
В TP7 вроде тоже полная совместимость была, одно плохо — реализация задержки в стандартном модуле CRT не была продумана под новые частоты, что привело к Run time error с ошибкой деления на ноль, если не ошибаюсь.
0
Точнее это цена за простоту компиляции С и скорость/размер получаемого кода. Удобством разработки как раз платить приходится.
0
Си чуть более высокоуровневый чем ассемблер. Совсем чуть чуть :)
0
не помню, чтобы Си относили к ЯВУ. Макроассемблер — да, ЯВУ — нет
0
ru.wikipedia.org:
Код на Си можно легко писать на низком уровне абстракции, почти как на ассемблере. Иногда Си называют «универсальным ассемблером» или «ассемблером высокого уровня», что отражает различие языков ассемблера для разных платформ и единство стандарта Си, код которого может быть скомпилирован без изменений практически на любой модели компьютера. Си часто называют языком среднего уровня или даже низкого уровня, учитывая то, как близко он работает к реальным устройствам. Однако, в строгой классификации, он является языком высокого уровня.
Так что вполне обычный ЯВУ процедурного семейства. Его сверсники ничуть не высокоуровневей.
0
Кстати, изначальный вариант бейсика еще ниже уровнем, чем С. Он работает в той же парадигме программирования, что и процессор — линейной. Все переменные глобальны, функций нет — только переходы (GOTO) и подпрограммы (GOSUB), переходы адресные (номер строки в качестве адреса).
Но почему-то ни у кого и сомнений не возникает в том, что бейсик ЯВУ.
0
Мне понравился один последний мой головняк, когда я работал с битовыми флагами.

// Тип, описывающий глобальные флаги программы
typedef struct _SFLAGS {
    
    uint8_t KeyModePressed:  1; // Состояние клавиши MODE
    uint8_t KeyColorPressed: 1; // Состояние клавиши COLOR
    uint8_t GotoSleep:       1; // Команда "Заснуть" для основного потока (main)
    uint8_t RxComplete:      1; // Принята посылка в USART0
    uint8_t:                 4; // дополнение до 8 бит (резерв) 
    
} SFLAGS;

// Переменная флагов для описания состояния периферии
// Указание "volatile" обязательно, т.к. флаги являются посредниками
// между основным потоком и обработчиками прерываний. При этом
// при работе с флагами будет использоваться "атомарный" код,
// т.е. три операции рядом: загрузка, модификация, выгрузка - в одном месте.
volatile SFLAGS Flags;

История такая. Есть основной поток main() {...} и подпрограммы — обработчики прерываний (ISRs). Есть глобальная переменная Flags, которая содержит некоторые флаги. В общем, классика. Мне нужно было, чтобы при нажатии и удержании кнопки более 2 сек МК уходил в Power Down. А вот ты хоть убей — он не уходил, хотя флаг GotoSleep устанавливался, но он не доходил до основного потока… точнее говоря, как потом выяснилось, он доходил, но из-за того, что я использовал для работы с кнопками и другой флаг KeyModePressed в этой же переменной, происходило затирание только что установленного (в ISR) флага GotoSleep.

Понял я это всё только когда n-й раз под лупой смотрел состояния в отладчике и подробно разобрал листинг на асме. Может кому будет интересно.
0
  • avatar
  • uni
  • 17 октября 2011, 18:38
Есть еще вроде бы прикол с юнионами при использовнии порядка big endian и little endian
0
big endian и little endian – это известный геморрой. А что за прикол с union? По разному пакуется в памяти разными компиляторами?
0
Да, что то в этом роде. Сам еще не сталкивался, но краем уха что то подобное зацепил, что работет не так.

Или вот взять, например, такой популярный метод как разбор большой переменной на байты через обьявление однобайтного указателя и по смещению. Чисто ассемблерный метод, но на Сях. Компилится везде, но на другом эндиан или на другой величине переменных (int он везде разный) даст багу.
0
Угу. И с битовыми полями та же херь.
0
Оказывается тут целая статья этой «атомарности» посвящена. Очень хорошая статья. Надо периодически напоминать о таких штуках в коде. Я вот, по-честному, только когда в ассемблерный листинг заглянул — понял в чём дело. От своей тупости аж мурашки пробежали.

А ещё лучше, показывать каким образом использовать глобальные переменные (структуры) для настройки всей программы (хотел написать приложения).
0
Был я когда-то на одних курсах по C++ и меня там научили не забывать, что стандартом не определён размер char в байтах. Это просто квант данных, а int и прочие сотоят из char'ов. С другой стороны sizeof() выдаёт информацию именно в байтах, а операции перемещения в памяти осуществляются символами (characters). Т.е. если мы хотим использовать размер какой-нибудь структуры при стандартных методах работы с памятью, то определять размер правильно так:
sizeof( SFLAGS ) / sizeof( char )
а не просто
sizeof( SFLAGS )
0
  • avatar
  • uni
  • 17 октября 2011, 21:54
Кстати да, отличный пример. Не следует забывать, что размерность char стандартом не определена.
0
Ну так это, должно быть как рефлекс при написании кроссплатформенного кода

#include <stdint.h>

И все, у нас есть точно заданные по размеру типы: int8_t etc.
+1
С другой стороны sizeof() выдаёт информацию именно в байтах, а операции перемещения в памяти осуществляются символами (characters)
это где такие интересные курсы? операции перемещения в памяти оперируют именно байтами — посмотрите документацию string.h
0
еще sizeof( SFLAGS ) / sizeof( char ) может давать неверный результат при выравнивании данных
0
Тут надо подумать, попробовать, выяснить насколько «выравнивание данных» является одним из свойств кроссплатформенности. Может быть это специфичное свойство? Надо читать стандарт, а он большой.

Я мыслю именно исходя из переносимости. Этот код, порою, выглядит иначе, чем типовой практический.
0
Выравнивание данных из-за платформы и пошло. На сколько мне известно: пень не умел извлекать 4 байта не по границе 4 байт, потом научился аппаратно, но с тормозами; атлон не умеет а возникающее исключение обрабатывает ОС извлекая по частям.
Отличительная особенность Кортекс — он умеет извлекать не выровненные данные, а все предшествующие тдэми не умели.
В итоге при создании «упакованных» структур генерируется куда более медленный код.
Выравнивание было всегда, с появлением >8бит архитектуры.
0
Если char может иметь длину 2 байта. То каким макаром тогда выделить память под 1 байт?
Если char 2 байта, то в какую сторону произойдет округление? Сразу скажу в меньшую. Тем самым это работать не будет. Какой то стремный совет.
0
А это какой-то стрёмный ответ :) Выделяйте память символами, тогда ваш код будет переносимым. Ответ будет «не стрёмным», когда вы свою операционную систему будете портировать на разные процессоры. Вот тогда создатель до дыр страндарт дочитает, т.к. только совместимый со стандартом компилятор сможет вашу ОСь запустить на другой платформе с неизвестно каким размером char.
0
О какой ОС идет речь? Тут идет речь о char размером в 2 байта.
0
ОС, которая бы одновременно работала в системе, где байт = 8 бит и байт = 16 бит, т.е. в uC и в DSPs, к примеру (в компиляторах). Точно не знаю, uLinux вроде бы портировали на некоторые DSP. Интересно было бы узнать как влияет длина байта на работу этой ОС в DSP и в каком-нить uC (если там байт 16-битовый, конечно)? Или там не стали этим заморачиваться.
0
DESCRIPTION
The memmove() function copies n bytes from memory area src to memory area dest.
Не вижу размера в символах ;). Какой метод работы с памятью вы называете стандартным?
0
Вверху в заголоке приведена ссылка на стандарт: ISO/IEC 9899:TC3

7.21.2.1 The memcpy function
Description
The memcpy function copies n characters from the object pointed to by s2 into the object pointed to by s1. If copying takes place between objects that overlap, the behavior is undefined.

7.21.2.2 The memmove function
Description
The memmove function copies n characters from the object pointed to by s2 into the object pointed to by s1. Copying takes place as if the n characters from the object pointed to by s2 are first copied into a temporary array of n characters that does not overlap the objects pointed to by s1 and s2, and then the n characters from the temporary array are copied into the object pointed to by s1.

7.21.2.4 The strncpy function
Description
The strncpy function copies not more than n characters (characters that follow a null character are not copied) from the array pointed to by s2 to the array pointed to by s1. If copying takes place between objects that overlap, the behavior is undefined.

Если почитать MSDN, то даже там characters. Так что явного указания на 8-ми битового байта нет. И по идее не может быть, если мы хотим иметь кроссплатформенную систему. Сами подумайте, если привязываться к конкретному байту, то как потом расширять спецификацию? Проще этот character просто переопределить, а всё остальное оставить.
0
Давайте ограничимся функциями mem..., поскольку str… это все же из другой оперы. Смотрим на 7.21.1 —
each character shall be interpreted as if it had the type unsigned char
. Так что есть указание на явный тип :) и спецификацию придется переделывать, если что. ИМХО все это потому, что все эти функции в C99 почему-то висят в String manipulation разделе, хотя по факту относятся к сырой памяти. Я взял описание из man страницы (сейчас все же не 99й год). Надо будет глянуть на новый стандарт С++, как там с этим.
А вообще БАЙТ — он один, это единица объема информации, величина строго определенная в теории. Он весьма конкретен и однозначен.
0
Работу со строками я взял, т.к. выше в комментах их упоминали и попросили посмотреть документацию, вот и посмотрел.

А сколько в нём бит? И на какой платформе :) Да, сейчас не 99, но в опциях компилятора мы живём прошлым, стандарт ведь там выбирается (IAR, к примеру).
0
А бит в нем восемь. Или больше :) (5.2.4.2.1), но все же в 99.9% случаев восемь (дураков, слава Богу мало, делать невосьмибитные байты). А чтоб разночтений вообще не было, я использую везде uint8_t вместо «классического 8ми битного» байта, чего и всем желаю.
+1
Да нее, не так, до меня кажется дошло в чём путаница. Там вначале всё пояснено:
3.6 byte
addressable unit of data storage large enough to hold any member of the basic character set of the execution environment

О как! Байт — это такая штука, которая достаточно большая чтобы вмещать любое количество элементов основного набора символов в исполняемом окружении.

Т.е. если мы используем unicode, то байт — это 16 бит. Так надо понимать? Или что подразумевается под набором символов?
0
Причем, что интересно «data storage» — то есть оперативная память никого не волнует, похоже :). Теперь осталось найти определения «basic character set» (а не просто набора символов) и «execution enviroment» (особенно в контексте кросскомпиляции интересует execution чего? Компилятора, программы...). Давайте все же остановимся на 8ми битном байте, а?
0
Можно и остановиться. Просто, насколько я помню, мне это деление на sizeof(char) тогда предлагали, т.к. при переходе на unicode байт становится не 8-, а 16-битовым.

Т.е. мысль была такая, что размер структуры определяется в 8-битовых байтах, но если char — 16 бит, количество элементов-символов может быть посчитано неправильно. А досканально я в этом не разбирался, решив отложить до тех времён когда это будет.

Может быть тут есть некоторые отличия стандартов C++ и C в работе этих функций.

Вывод такой: Char — это байт, байт теоретически может иметь разную длину в битах (большую 8) и зависит это от конкретной реализации. sizeof() возвращает размер в «байтах», которые не обязательно «железные» байты. Т.е., когда мы пишем sizeof( SFLAGS ) — это размер стуктуры в «байтах», определённых компилятором. Он не обязан равняться строго (по стандарту) количеству 8-битовых байт.

Где это может пригодиться? Некоторые товарищи пишут код, который одновременно работает и в МК и на ПК. Если на ПК «байт» (char) станет 16-битовым, то результаты работы одного и того же кода могут оличаться, хотя выглядят одинаково.
0
Вы знаете, мне очень сильно кажется, что байт никогда не будет больше 8ми бит. Это уже традиция, а традиции редко меняют. Не зря же ввели для юникодных символов отдельный тип wchar_t? А про размер char вспоминается define CHAR_BITS (если не путаю), так что можно писать с оглядкой и на него.
0
Б. Страуструп в своей книжке, а также Хэзфилд, Кирби в «Искусство программирования на C», как оказалось, не забывают про CHAR_BITS.

Вот цитата из последней книжки (2001 г): «Байты могут быть различной длины, и с этим ничего не поделаешь. Язык C гарантирует, что длина байта составляет не менее восьми бит, что является обычным минимумом, принятым в большинстве систем».

Все они говорят, что байт — это, как правило, в большинстве случаев 8 бит (Страуструп, 2001):

«Практически всегда на объект типа char отводится 8 бит, так что существует 256 различных значений этого типа. Как правило, набор символов основывается на ISO-646...»

Я правильно понял набор символов как множество их значений в конкретной вычислительной среде.

Так что мой совет в том его виде как я его написал можно признать не имеющим практического смысла.
0
Нашёл в сети один коммент по поводу 16-ти битового байта:
Some people live in a small universe. IOW, don't believe everything you read. In other languages, a byte may very well be 8 bits, but C and C++ are designed to be portable languages. Assumptions like that would severely limit their usefulness, so C and C++ simply state that a byte is equal to CHAR_BIT bits <limits.h> for C, or numeric_limits::digits from in C++. It is true that the most common values for both are 8, but that isn't universal. For example, the processor I have been doing the most work on lately, Motorola's DSP56F805, has 16 bit bytes. But sizeof(char) is still 1, incidentally, so is sizeof(int)!
0
А интересно, CHAR_BITS у этой моторолы 16? Если нет, то забросать их камнями и предать анафеме :).
0
8-битный DSPs — так наверное уже не бывает, поэтому какой смысл придерживаться 8 бит в компиляторе, когда всё нутро 16 битное?
0
Да и вообще, взять ту же винду :) там все типы 32-х разрядные по факту, а мы всё с char'ами возимся (может даже уже все типы 64-разрядные, ещё не перешёл на эту платформу)
0
Не, ну не так же. А BYTE? А WORD? Не все там типы == DWORD.
Насчет DSP согласен, что смысла большого нет, но на месте разрабов я все же оставил бы байт 8ми битным. Ну да Бог им судья, по крайней мере ясна область, где ОБЯЗАТЕЛЬНО надо учитывать разрядность байта.
0
Хм, а мне почему-то всегда казалось, что при работе с WinAPI всё есть DWORD. Сейчас глянул в свой старый Platform SDK: «typedef byte BYTE;» Но на практике мне трудно вспомнить случай, когда бы я работал не с DWROD'ом.

В общем, мне видится такая ситуация, когда техпроцессы будут настолько мощны, что не будет разницы между поддержкой 8 бит и 16 бит, все постепенно перейдут на 16 бит. Вся периферия станет 16-битной, обмен данными станет 16-битным и на больших ПК скажу — ушла эра 8-битового байта, теперь байт — это 16 бит. Соберётся консорциум и на уровне стандарта заменят эту цифру с 8+ на 16+. Интересно, насколько скоро такое может случится.
0
Ну, думаю нескоро. Вообще, лучше думать, что когда ты выделяешь 16бит там, где достаточно 8ми, то где-то умирает котенок ;). Не надо приближать тепловую смерть вселенной :) увеличением потребляемых ресурсов.
0
Понял почему, Windows (не .Net) большей своей частью — это дискрипторы, указатели, HRESULT и BOOL — а они все DWORD.
0
Как я говорил, sizeof() возвращает размер структур и типов в байтах:

6.5.3.4 The sizeof operator

The sizeof operator yields the size (in bytes) of its operand, which may be an expression or the parenthesized name of a type. The size is determined from the type of the operand. The result is an integer.

Причём, оператор учитывает заполнители:
When applied to an operand that has structure or union type, the result is the total number of bytes in such an object, including internal and trailing padding.

Но, что для меня явилось новостью: sizeof(char) всегда равняется 1.

When applied to an operand that has type char, unsigned char, or signed char, (or a qualified version thereof) the result is 1.

Поэтому деление на sizeof(char) в свете нового прочтения мною стандарта является бессмысленным.

Надо чуть-чуть покапать стандарт на предмет вопроса: если работа с памятью идёт в char, sizeof() показывает в байтах, а sizeof(char)=1, то зачем везде писать characters?
0
Вот поэтому я стараюсь не использовать стандартные типы, а определяю свои, где четко ясно что это за данные, какой длины и что в них.
0
зачем? используйте stdint.h
+1
А если я захочу портировать на другую платформу? А там int не 8, а 16 бит, например. А так если я явно указал uint8, то она везде будет uint8
0
Дык, как я понял, stdint.h — это и есть uint8_t, uint16_t etc.
0
более того, там есть типы вроде uint_fast8_t — размер которого не менее 8 бит, но который явлется оптимальным с точки зрения скорости арифметических операций — на 32-битных архитектурах это скорее всего будет uint32. Зачем это нужно — например для счетчиков цикла. Мы знаем, что количество итераций будет не более N, но врожденная жадность скажет — возьми тип поуже! Хотя скорее всего счетчик будет сидеть не в ОП, а в 32-битном регистре. Плохо знаком с ARM'овским ассемблером, но кажется регистры там не делятся на части как в x86, да и то в x86 это наследие 16-битной эпохи
0
Может уже было, но

1. aliasing

Несколько имен у одного значения.


int foo(int *a, double *b)
{
	*a = 1;
	*b = 2.4;

	return *a;
}

int main()
{
	double	b;
	int	ret;

	ret = foo((int *) &b, &b);

	printf("%i\n", ret);

	return 0;
}



$ gcc -O1 -fstrict-aliasing test.c -o test
$ ./test 
1
$ gcc -O1 -fno-strict-aliasing test.c -o test
$ ./test 
858993459


Компилятор полагает что указатели разных типов не могут указывать на одно и тоже, и не перечитывает 1 после записи. Эта оптимизация включена в gcc с -02/O3/Os, в примере используется -O1, иначе main оптимизируется так, что вызов foo не производится. Могут быть более интересные случаи когда компилятор меняет порядок вычислений, сам не видел, примеров не знаю.

see also: man gcc, en.wikipedia.org/wiki/Aliasing_(computing)

2. integer promotion

weichong78.blogspot.com/2005/06/cc-integer-promotiondemotion.html
+1
Не вижу ошибки — по одному адресу пишется double 2.4, оттуда же читается int. Что вы ожидали получить?
0
Присмотритесь к выводу. При компиляции с разными флагами вывод отличается. да и ПРОЧТИТЕ коммент в конце концов, там все написано.
0
ну порядок вычислений это найболее популярно на контроллерах, но и в х86 тоже очень часто попадается. скомпилируйте дебаг версию с включенной оптимизацией и попробуйте пройти по шагам либо по инструкциям. такая пляска указателя уже что родная.
0
Нашел забавную презенташку по теме: тыц
+2
Зачетная презентация. По тонкостям С они прошлись основательно, а вот что касается С++ — тема, к сожелению, раскрыта плохо. А как-же ромбовидное множественное наследование и прочие фичи?
0
Интересная презенташка, да. Но я все же несколько огорчен, что мейнстримовыми стали языки, думающие о программере и ошибкостойкости в последнюю очередь.
0
а смысл в таких «тонкостях»?
… из того же разряда 0/0 =?

PS: cлабо было глянуть в листинга
asm'а в том же MSVC?
0
  • avatar
  • valio
  • 29 октября 2011, 21:30
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.