Как стать автором
Обновить

Комментарии 309

На iPad-e ответы не select-ятся :) может сделать еще отдельный pastebin? :)
Оно надо выделить мышью или чем там у Вас на йападе
айпальцем сойдет.
Особенности webkit-а
Ну эти тонкости знает тот кто знает как устроен Chrome, я же об этом WebKit вспоминаю когда запускаю #make install clean. Но видя что я могу увидеть, а чел нет, то стремлюсь помочь.
Отправил ЛС.
Спасибо
Написал комментарий.
Копипасти в блокнот и там читай — я так на айфоне делал.
Ого, прям лайф хак :) спасибо :)
Попробуйте в opera mini. Айпада под рукой нет, но на андроиде текст выделяется привычным образом.
Вообще, постоянно ей обхожу похожие проблемы с сафари, например когда выделение принудительно пытается выбрать большой кусок текста вместо строки.
Вот оно, легендарное юзабилити :) Это почти как Джобс говорил — «Просто не держите его таким образом (про четвертый айфон)»
Я думаю, в понятие юзабилити не входит такая операция, как чтение белого текста на белом фоне.
Да ну, ты брось, зато как весело потроллить можно — айпэд не показывает белый текст на белом фоне? Фуу! Да это делает даже IE6!"
Не переживайте, отсутствие чувства юмора еще не делает вас потерянным для общества!
Хреновое чувство юмора тоже.
На iOS нельзя выделить текст? Весьма удивлен.
Можно, но цвет текста не инвертируется
Яблочники сошли с ума. Минусуют за вопрос.
По вашему как можно скопипастить текст, не выделив его?
Если нажать кнопку «Reader» в строке адреса текст становится читаемым.
В 5. может всё-таки return еще нужен?
Спасибо, исправлено.
И в 8.?
Нет, в 8 не нужен, ибо
reaching the } that terminates the main function returns a value of 0.
5.1.2.2.3/1.
Я не знаю Си, и у меня начал дергаться глаз
Си знает, что я его не знаю и ему по фиг.
«Си» в вашем случает от «Советский интерпретатор»?
Юмор про Soviet Russia оказался непосильным?
Неканоничная форма же.
Хороший пост.
Еще бы подобный для «плюсовщиков», которые пихают эти самые плюсы даже туда, где можно средствами обычного С спокойно обойтись.
>>где можно средствами обычного С спокойно обойтись
Спасибо, но после вышеперечисленных граблей, я буду еще больше избегать C и использовать C++.
2.
После анализа строки (1) компилятор считает, что x не может быть нулевым указателем и удаляет (2) и (3) как недоступный код (dead code elimination).

Какой компилятор удаляет код? Это весьма дивное поведение и относится к компиляторам, а не стандарту.
Компилятор может делать всё, что хочет если это не меняет observable behavior. В строке (1) есть два варианта: или x не нулевой указатель и (2) не сработает, или x нулевой указатель и дальше можно хоть запускать форматирование диска.
тогда правильный ответ будет «я не знаю всех компиляторов», а не «я не знаю Си».
Покажите в чём именно нарушение стандарта.
в том что ожидается выполнение if (!x) при x == (void*)0, что как раз таки меняет observable behavior
if (!x) не observable. return observable. Поэтому if() не обязан вычисляться. return мы не можем увидеть, потому что для того, чтобы он выполнился, должен был произойти undefined behavior, а дальше может происходить что угодно.
А как предполагается обращаться к нулевой ячейке (если это действительно нужно, в каком-нибудь микропроцессоре или на других компьютерах с 64К памяти)?
Менять null pointer constant — очень и очень сложно, так как очень многое в компиляторах завязано на то, что null pointer constant (который определяется как 0 любого целого типа, приведённый к указателю) имеет битовое значение 0 как указатель. Иначе, к сожалению, средствами языка никак. Но можно определить переменную и затем linker script'ом разместить её по нужному адресу.
Верно ли, что компиляторы обязаны на строчку *(int*)0x0000=0xCD выдавать ошибку?
Компиляторы — не обязаны, в рантайме падение тоже не гарантируется (undefined behavior — это всё что угодно, в том числе и игнорирование проблемы: как, например, переполнение чисел со знаком игнорируется в большинстве компиляторов Си для x86).
А с чего вы взяли, что нулевой указатель это undefined behavior? В эмбеддеде где нету MPU и MMU по нулевому адресу может храниться вектор ресета.
ISO/IEC 9899:1999
6.5.3.2 p4

> Among the invalid values for dereferencing a pointer by the unary * operator are a null pointer

Я согласен что по физическому адресу, соответствующему null pointer может что-то лежать и тем, что лежит ниже реализации Си (процессор/ВМ) разрешается читать/писать эту ячейку памяти, но в стандарте это UB. Понимаете, UB может означать «аварийное завершение», а может «какое-то действие». Поэтому на практике у вас даже может получаться записывать *(int *)0 = 42; и считывать обратно, но это всё равно UB.
Просто я со своей колокольни смотрю, и там где я использую C (это эмбеддед) разыменование нулевого указатель вещь вполне обыденная, и предсказуемая. Хоть и не красивая. Самая часто встречающаяся ошибка это:

((void(*)(void))0)();

Это предсказуемая вещь пока компилятор не станет умнее и кто-то (из лучших убеждений, стараясь помочь программистам отлавливать UB) не будет вставлять abort() вместо разыменовывания null pointer если это видно статически. И компилятор, что, самое интересное, будет прав, и подавляющее большинство программистов будут только рады такому новому поведению.
HP aC++ так делает уже давно.
Ой, ошибся. Он abort() не делает, он просто игнорирует такое разыменование.
Это всё же ошибка оптимизатора. UB должно относиться только к тому выражению, где оно используется, а не ко всему коду вообще. В данном случае ваша версия компилятора решила сначала, что *x при x == NULL вызовет pagefault (вы же транслировали для pc-linux?), поэтому можно if выкинуть, а потом она решила выкинуть y, забыв сказать самой себе, что теперь никакого pagefault не будет. То есть, когда abort для x == NULL будет гарантирован if можно и выкинуть, и всё ОК, код будет вести себя так, как ему положено. А тут оптимизатор запутался в гарантиях. Но при чём тут стандарт?
> UB должно относиться только к тому выражению, где оно используется
UB относится ко всему выполнению программы, где имеется UB. Не гарантируется даже что действия до UB будут выполнены корректно. Вся программа с UB не имеет смысла.
Суровый Вы человек :) Или мы читаем разные стандартны. В моём Draft для C99 написано, что Undefined Behaviour — это ситуация, когда поведение компилятора не определено. И нигде не сказано, что вся программа с UB не имеет смысла. Вам же уже тут приводили пример с доступом по NULL — иногда это часть архитектуры системы, иногда нам необходимо туда писать. Иногда нам нужен aliasing. Просто такие операции будут специфичными для данной версии компилятора/системы и не будут переносимыми. И именно для того, чтобы специально не перечислять все частные случаи и было придумано UB. А совсем не для того, чтобы лишать программы смысла. Это просто свободный такой смысл.

Я вот не знаю, каким Вы транслятором транслировали код с этим примером на NULL, но вот, например тот же Clang генерирует для него различный код для kernel-режима трансляции и для режима по-умолчанию.

То есть, всё не так уж и сурово :) Скорее, свободно. Чем Си и полезен.
> Undefined Behaviour — это ситуация, когда поведение компилятора не определено. И нигде не сказано, что вся программа с UB не имеет смысла.

Программа не имеет смысла без всего этого контекста: определённый ЦП, конкретная версия тулчейна, конкретная версия ОС и может быть даже дата и время.

> Я вот не знаю, каким Вы транслятором транслировали код с этим примером на NULL
pastebin.com/MpqKKpyU Вот полный код. gcc 4.6 -O2 убирает проверку на нулевой указатель. Это широко известный (в узких кругах) баг в ядре.
госпадя
госпадя
В MSXDOS это был выход из программы. Где тут ошибка?
А в эмбеддеде так часто делают перезагрузку, но ведь это не перезагрузка, а вызов функции по адресу 0, с проталкиванием в стек адреса возврата. Если так часто перезагружать, долго работающую систему, то она зависнет от переполнения стека.
Понятно. В операционке первой командой, выполняющейся после JP 0000 было LD SP,(0006) — так что там стек не переполнялся.
Не волнуйтесь вы так, стековый указатель переинициализируется заново :)
программа на старте настраивает стек заново, то есть что там запушили неважно — стек переустановится заново
Хорошая подача материала. Нифига не смыслю в С, но дождусь полуночи и плюсану )
Тогда всё сходится)
Перешел по ссылке, проголосовал, увидел результат. У меня просто вырвалось: — ЧТО?!!!
Тоже сходил по ссылке, и, вы не поверите… то же самое сказал непроизвольно)
Интересно почему народ считает что си нужно давать в школе?..
ЗЫ: не забываем, что из класса примерно 10% (2-3 человека) пойдут по ИТ дорожке…
Думаю, надо бы уточнить, что в 1-м пункте i — глобальная переменная в пределах модуля. Если такое написать внутри функции, то это будет ошибочный код из-за редекларации.
Сделал уточнение.
Славные грабельки :-)
«За свободу надо платить отвественностью» вкупе с «Незнание не освобождает от ответственности».

Собсна меня в си практически всё устраивает, кроме строк. Как говорил Спольски «история развития си++ может быть проиллюстрирована исторей борьбы с изначально идиотскими строками си».

Особенно чудесно заморочен Борман — есть TString и есть AnsiString, и для оперирования ими приходится извращаться.

Эх, если бы не строки… С остальной памятью я уж как нть разберусь.
А чем не устраивают строки в Си?
Во первых — в С строк нет.
Во вторых — схема работы со строками в С оптимальна!
В третьих — язык, разработанный практиком отличается от языка, разработанного теоретиком наличием встроенного типа 'string'.

Обработка строки внутри довольно сложный процесс. Если его не сделать, хотя-бы визуально, немного более сложным, то это приведёт к огромной потери производительности в большом проекте. В статье Спольски об этом упомянуто, но не написано, что при использовании встроенных типов строк всё, что он рассказал про malloc() будет применяться в неявном виде.
Вы правы: в строгом смысле строки в си отсутствуют.

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

И если замена килограммов на double имеет некоторые грабли (например в виде отрицательных значений массы), то замена строк на char * имеет массовые и массивные грабли в силу самой попытки использовать char * (или char []).

На месте авторов языков я бы ввёл два строковых типа — короткие в 255 символов без гемора с malloc и длинные с таковым. Плюс механизмы взаимного перевода между этими типами с соответствующими проверками.

Вы ведь согласитесь с тем, что 255 символов в 95% случаев устроили бы прикладные задачи? Да и операции вроде преобразования адреса в фамилию или аннотации в город чрезвычайно редко встречаются в жизни.

P.S. ладно, для денег не требую вводить отдельный вещественный тип. Хотя тоже неплохо бы ;-)
А то мы уже к коболу приближаемся
Насколько я понимаю, речь идет о том, что не надо пытаться самому реализовывать std::string (C++) и уже тем более String (Java). Потому что се уже сделано до тебя, причем сделано прилично. Вроде.
Мы говорим только про Си
Мы говорим про С.
Но если взять С++ и стандарт разработанный теоретиками, в виде std::string, то мы увидим, что в нём отсутствует возможность сравнения строк без учёта регистра, что критично для строк из набора ASCII (латинских). Теоретики, мать их, кампухтер сайенс, мать её.
За это мне больше Qt шные строки нравятся, их как раз практики делали :)
Я, вообще-то, очень тупой, и поэтому использую Java. Но, скаже честно — Qt — первая библиотека C++, которая мне нравится. То есть там так много общих концепций с Java, что создается обманчивое впечатление, что я смогу легко перейти с одного на другое )
В стандарте С тоже нет сравнения строк без учета регистра. strcasecmp это BSD или POSIX, что вам больше нравится, но не С99.
> Во вторых — схема работы со строками в С оптимальна!

До тех пор, пока мы не вспоминаем, что есть Unicode
и что с юникодом не так?
Допустим, хранить UTF-8 можно и в Сишных char-ах. А вы попробуйте вывести на микроконтроллере на 40-сегментный индикатор. А Си всё больше используется там, ниже чем айфон и дот нет.
если он не поддерживает национальные символы то ничего не получится это к гадалке не ходи. если речь идет о английских символах то они кодируются в utf-8 точто так же как и в 7-bit ASCII, т.е. занимают 1 байт.
просто я как раз занимался приведением одной 8-битной программы к работе с utf-8… там всё в принципе тривиально. нужно читать из массива пока не встретится начало следующего символа а это начало определено стандартом.
То, что юникод не однобайтовая кодировка. Все, приплыли со стандартным сишным представлением строки.
Не приплыли ни разу. В char* можно отлично хранить UTF8 строки, т.к. ни один из code points в UTF8 не может содержать 0x00.
Угу. И как с ними потом работать?
Задавайте более конкретные вопросы.
Длина в символах, поиск подстроки, разбиение на символы — короче все, что связано с символами.
Забыл главное — сравнение и сортировка (которая завязана на сравнение).
Длина в символах — зачем она нужна? В байтах — понятно, чтобы, например, для копирования нужное количество памяти выделить, а в символах зачем?

Поиск подстроки будет работать, ему всё равно в каких кодировках строки.

Разбиение на символы — зачем? Каждый символ разным цветом печатать что-ли? Практика показывает, что прямой доступ к символам строки не нужен. А последовательный доступ в случае UTF8 пишется достаточно просто.

Сравнение с учетом локали — ну так эта задача решается из коробки по-моему только для Latin1, все равно используются какие-то библиотеки, которые с большой вероятностью умеют и UTF8.
> а в символах зачем?

Банально вывести количество отсавшихся символов в клиенте для твиттера :-\

> Поиск подстроки будет работать, ему всё равно в каких кодировках строки.

Не будет. Особенно case-insensitive поиск. Потому что SS и ß — это одна буква.

Не говоря уже о том, что ü и комбинация знаков ¨ и u — это тоже одна буква.

> Разбиение на символы — зачем?

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

> А последовательный доступ в случае UTF8 пишется достаточно просто.

Не надо рассказывать сказки.

> Сравнение с учетом локали — ну так эта задача решается из коробки по-моему только для Latin1

Именно.

Вы получили ответ на вопрос «и что с юникодом не так?»
Не надо рассказывать сказки.

Ок, вот вам и подсчет символов и итерирование по UTF8 строке, ничего сложного там нет.

Вы получили ответ на вопрос «и что с юникодом не так?»
Я получил ответ на вопрос «что не так с мультибайтовыми кодировками?» :) А однобайтные кодировки своё уже отжили. Более того, не забывайте, что всякие там китайские и японские локальные кодировки — они уже не однобайтовые, а multibyte и обладают теми же проблемами.

Если вы хотите, чтобы у вас символ был ровно N байт — вам нужен либо UCS2 (который не покрывает весь Unicode), либо UTF32 (который оверхед), потому что в UTF-16 символ может занимать как 2 так и 4 байта.
> Ок, вот вам и подсчет символов и итерирование по UTF8 строке, ничего сложного там нет.

Дадада. Совсем-совсем ничего, ага. Кроме того, что этого нет в стандартной поставке, и кроме того, что вы решили вырезать любые другие проблемы, которые так не решаются

> Я получил ответ на вопрос «что не так с мультибайтовыми кодировками?» :) А однобайтные кодировки своё уже отжили. Более того, не забывайте, что всякие там китайские и японские локальные кодировки — они уже не однобайтовые, а multibyte и обладают теми же проблемами.

Теперь возвращаемся к моему изначальному заявлению про юникод и ваш вопрос «что не так с юникодом»? Я ответил на этот вопрос?
Ну так если в стандартной поставке есть только работа с однобайтовыми кодировками — продолжать сидеть в 90-х из-за этого? Ну то есть проблемы, которые вы описали присущи не только UTF8 (не юникоду), а любой мультибайтной кодировке, без которых вы всё равно в 2012 году не обойдётесь. Ну то есть по-вашему получается, что юникод плох тем, что с ним нельзя работать используя старые добрые str… функции, но альтернативы то нет.

Что не так с юникодом вообще некорректная постановка вопроса по-моему. Мы сейчас обсуждаем «что не так с UTF-8?». И я как раз пытаюсь сказать о том, что с точки зрения работы с сишными строками с UTF-8 работать удобнее, чем с UTF-16, например.

Теперь возвращаемся к моему изначальному заявлению про юникод и ваш вопрос «что не так с юникодом»? Я ответил на этот вопрос?
Вопрос был не мой, но мне кажется, я понял вашу мысль.
> Вопрос был не мой, но мне кажется, я понял вашу мысль.

Претензии снимаются :)
Отвечу более развёрнуто, в прошлый раз было мало времени.

Итерирование по символам UTF8 строки — функция в 7 строк. Подсчет длины в символах реализуется через неё же в 5 строк. Что тут сложного? Алгоритмическая сложность подсчета количества символов в строке — как и у strlen — линейная, дополнительного оверхеда не вносится.

По поводу поиска подстроки. Как проблема
Потому что SS и ß — это одна буква. Не говоря уже о том, что ü и комбинация знаков ¨ и u — это тоже одна буква.
решается в случае однобайтовых кодировок?

То есть в случае, когда мы имеем дело с чем-либо сложнее Latin1, дополнительные действия совершать всё равно приходится, и эти дополнительные действия для UTF8 вовсе не такие уж сложные, а даже наоборот.

Зато есть определенные задачи, где переход от однобайтовых кодировок к UTF8 практически безболезненный. Вот например файловая система. Ей не надо знать, сколько в имени файла символов. Ей не надо по ним итерироваться. Она просто работает с именами файлов как с 0-терминированными строками. Раньше эти строки были в локальной однобайтовой кодировке. Если мы захотим складывать туда UTF8 строки всё продолжит работать. Потому что сравнение строк на идентичность будет работать, копироваться строки будут так же отлично как и раньше, и так далее, но при этом мы не будем ограничены 255 символами в имени файла, а сможем использовать весь Unicode. То есть, возвращаясь к «приплыли со стандартным сишным представлением строки» — в данном примере не приплыли. И юникод поддержали и код, рассчитанный на стандартные сишные строки, продолжает работать.
> Итерирование по символам UTF8 строки — функция в 7 строк. Подсчет длины в символах реализуется через неё же в 5 строк. Что тут сложного?

В том, что на каждый чих в С надо реализовывать собственные функции, поому что родные строки умеют работать только с однобайтовыми кодировками.

> По поводу поиска подстроки. Как проблема SS и ß, ü и комбинации знаков ¨ и u решается в случае однобайтовых кодировок?

В том-то и дело, что никак :)

> и так далее, но при этом мы не будем ограничены 255 символами в имени файла, а сможем использовать весь Unicode.

Будем. Причем с глюками. Если в файловой системе хоть где-то стоит if(strlen(filename) > 255) strncpy(что-то-там), при условии, что и strlen и strncpy однобайтовые, то все, приплыли. По все тем же причинам, что и выше ;)

Единственное, то нас спасает в этом примере, это то, что основные ОСи давно имеют внутреннее представление в многобайтовых кодировках.

Будем. Причем с глюками. Если в файловой системе хоть где-то стоит if(strlen(filename) > 255) strncpy(что-то-там), при условии, что и strlen и strncpy однобайтовые, то все, приплыли.
strlen(filename)
Долбанный ктрл-ентер.

strlen(filename) вернет длину UTF-8 строки в байтах. Потом strncpy правильно скопирует эту строку (потому что UTF-8 строка не содержит внутри '\0', а длину (в байтах) мы только что правильно измерили. Где ж мы приплыли то? Функциям strlen, strcat, strcpy, strncpy и прочим всё равно, Latin1 у нас в char* или UTF8, они будут правильно работать и с тем и с другим.
> strlen(filename) вернет длину UTF-8 строки в байтах.

Правильно. А у нас какое требование? ЧТобы длина строки была не больше 255 символов. ;)
С чего это у нас такое требование?
Ни или не 25 символов. Любое другое число. Обычно имя файла в ОСи не может быть безграничным
И чем по-вашему это обусловлено? Не тем ли, что в формате служебных данных под это имя отводится определённое количество байт? Какая в общем-то файловой системе разница, сколько в имени символов?
Эо может быть обусловлено самыми разными причинами. В том числе и указанной причиной тоже.
Отвечу более развернуто. Если в файловой системе есть хоть какое-то ограничение на длину имени файла, то в случае с С, strlen и длиной в байтах мы приплыли. Потому что если последние два байта — это знаки «¨» и «u», и u не влезает в длину, то при обрезании мы получим в имени файла мусор. Хорошо, если это будет ¨, а не неотображаемый первый байт из двухбайтовой буквы.
Обрезание строк должно быть на уровне клиента файловой системы. API ФС должно не обрезать длинные строки, а возвращать ошибку типа некорректный аргумент.
Это уже философский вопрос о том, где это должно быть реализовано ;)
я правильно понимаю что вы не в теории столкнулись с проблемой а на практике? что именно у вас вызывает затруднения?
С какой проблемой? Проблемой, что в С практически нет нормальных способов работы с многобайтными кодировками и, в частности, с Unicode?
у вас есть проблемы с юникодом или нет?
У меня были проблемы с Юникодом. Более того, в С/С++ их не может не быть по определению. Спас ICU
посмотрите как в mc реалисована работа с utf-8 там есть и поис и подсчёт и сортировка, ничего сложного там нет… утвержтаю это т.к. собственно кое чего реализовывал в этом плане…
А причем тут mc?

> ничего сложного там нет… утвержтаю это т.к. собственно кое чего реализовывал в этом плане…

Расскажите мне про case-insensitive сортировку для немецкого, например и про посчет символов в нем же.

Напомню:
SS и ß — это одна буква. Не говоря уже о том, что ü и комбинация знаков ¨ и u — это тоже одна буква.
я в курне про немецкий, mc при том что там это организовано и реализовано.
То, что организовать и реализовать можно, это и так понятно. Хотя бы тот же ICU. Вспоминаем контекст подветки.
А чего это нет в стандартной поставке? В поставке есть wchar, пожалуйста, используйте. Будет вам перекодирование на лету в соответствии с локалью.
«The width of wchar_t is compiler-specific and can be as small as 8 bits. Consequently, programs that need to be portable across any C or C++ compiler should not use wchar_t for storing Unicode text. The wchar_t type is intended for storing compiler-defined wide characters, which may be Unicode characters in some compilers.»

;)
c utf-16 и 32 всё элементарно, с utf-8 есть нюансы но не более того.
С utf-16 ничего не просто. Просто только с UCS2. Некоторые символы в представлении UTF-16 могут занимать больше 16 бит. Так же не следует забывать о UTF-16LE и UTF-16ВE.
>т.к. символы Unicode после кода 0x10000 используются крайне редко. ©

но в любом случае забираю слова про «элементано» назад, (вы меня уделали:) ) да я имел ввиду UCS2. однако же и сказать что уж сильно сложно тоже нельзя…
О да, нуль-терминированные строки это так оптимально, так оптимально — не зря их кличут «самым дорогим решением в ИТ»
Нормальное решение. И это значительно, на порядки лучшее решение, чем создавать новые строки динамически выделяя память на каждый чих, что и происходит при выделенном базовом типе string или при использовании класса std::string или его самописного аналога. Здесь постоянный проход asciiz строки для измерения размера как-то теряется в безднах системных вызовов.
Нормальным оно является только в контексте создания Си — Керниган и Ричи делали инструмент для себя, а не ультракроссплатформенный системный язык, который в результате получился.
Динамическое выделение памяти к нуль-терминированности отношения не имеет, заниматься им при наличии выделенного типа или класса не обязательно, хотя и желательно-статические буфера фиксированного размера чреваты неожиданными повреждениями памяти, а на стеке — еще и эксплойтами.
А вот постоянный проход по всей строке и исключение нуля остаются тяжким бременем всегда, везде и повсюду. На уровне системных вызовов в том числе.
Ты не догоняешь.
В результате того, что сложный, я бы сказал — сложнейший тип выделен в «простые» программист не видит динамического выделения памяти. Принципиально, концептуально — не видит. А динамическое выделение памяти, между прочим, к рантайму языка и его библиотеке отношения не имеет, оно имеет отношение к операционной системе и платформе.

И теперь сравни это с «постоянным проходом» и исключением нуля. Да хер с ними, это непринципиально!

Ну, и, дальше.

После того, как сложнейший тип обёрнут в простой, программист лишается возможности оперировать с ним как с массивом простых типов. А это чревато. Идиотские конструкции 'left', 'right', 'mid' проблем только добавляют и вынуждают неявно использовать динамическое выделение памяти.

Вообще, эта надуманная проблема сишных строк решается довольно просто:

struct string {
short size;
char content[];
};

… и вся хрень должна иметь размер sizeof(short)+sizeof content[];

а нахера, собственно?
>В результате того, что сложный, я бы сказал — сложнейший тип выделен в «простые» программист не видит динамического выделения памяти. Принципиально, концептуально — не видит.
В результате использования си вместо ассемблера программист не видит отдельных машинных команд. Принципиально, концептуально не видит. Но надо же — именно это позволило писать на си лучше, чем на ассемблере.

> А динамическое выделение памяти, между прочим, к рантайму языка и его библиотеке отношения не имеет, оно имеет отношение к операционной системе и платформе.
Ага, как же, своей кучи помимо системной не бывает :)

> И теперь сравни это с «постоянным проходом» и исключением нуля. Да хер с ними, это непринципиально!
Это принципиально. Потому что почти любая «платформа» написана таки на си. Со всеми вытекающими.

>После того, как сложнейший тип обёрнут в простой, программист лишается возможности оперировать с ним как с массивом простых типов.
Расскажите это дельфистам.

>Вообще, эта надуманная проблема сишных строк решается довольно просто:
Это «просто» означает порчу памяти и эксплойты.

>а нахера, собственно?
Потому что это дешевле, чем нуль-терминированные строки.
>Но надо же — именно это позволило писать на си лучше, чем на ассемблере.
Не лучше, а быстрее.

> Ага, как же, своей кучи помимо системной не бывает :)
Бывает, но своя куча стандартной библиотекой не поддерживается.

>Это принципиально. Потому что почти любая «платформа» написана таки на си. Со всеми вытекающими.
И что?

>Расскажите это дельфистам.
Кому?

>Это «просто» означает порчу памяти и эксплойты.
Да ну?

> Потому что это дешевле, чем нуль-терминированные строки.
Где ты увидел дешевизну?
> Не лучше, а быстрее.
Лучше, качественно лучше. Собственно триумфальное шествие юникса стало следствием именно замены ассемблера на си в качестве основного языка системного программирования.

>Бывает, но своя куча стандартной библиотекой не поддерживается.
Как раз стандартная библиотека си, наряду с модульностью на включаемых файлах — его слабое место.

>И что?
И то. ASCIIZ-строки достанут везде, даже если избегать их как огня: через стандартную библиотеку, через сторонние библиотеки, через системные вызовы, через требования портируемости…

>Кому?
Есть такие языки, для которых «сложнейший объект» является с одной стороны простым, с другой — вполне совместим с массивом символов.

>Да ну?
Ну да. В любой нормальной книжке оно есть.

>Где ты увидел дешевизну?
В совокупной стоимости использования. Получение размера как O(1) и отсутствие запрещенных элементов имеет колоссальный мультипликатор полезности за счет использования на всех уровнях, от драйверов до скриптов.
>Лучше, качественно лучше. Собственно триумфальное шествие юникса >стало следствием именно замены ассемблера на си в качестве основного >языка системного программирования.
Триумфальное шествие юникса произошло потому, что он попал в лапы студентов в 70е, и их на нём учили. А ещё в нём есть юзнет.

>Как раз стандартная библиотека си, наряду с модульностью на включаемых >файлах — его слабое место.
Модульность! На включаемых файлах!

>Есть такие языки, для которых «сложнейший объект» является с одной стороны >простым, с другой — вполне совместим с массивом символов.
Вот я и говорю — за это надо бить.

>В совокупной стоимости использования. Получение размера как O(1) и >отсутствие запрещенных элементов имеет колоссальный мультипликатор >полезности за счет использования на всех уровнях, от драйверов до
>скриптов.
Кошмар.
Единственная область применения, где имеет смысл заранее измеренная длина это приём/передача данных.
> Триумфальное шествие юникса произошло потому, что он попал в лапы студентов в 70е, и их на нём учили. А ещё в нём есть юзнет.
Очевидно, что с системой на языке ассемблера такого случиться не могло по определению. А си оказался «студентопригоден» как для учебы, так и для портирования ОС на другие машины, при этом ничего не потеряв для профи.

> Кошмар. Единственная область применения, где имеет смысл заранее измеренная длина это приём/передача данных.
В остальных случаях тормозная конкатенация и нулевые символы нисколько не мешают, ага, как же :)
Конкатенация будет тормозна в любом случае.
А нулевые символы вообще ничему не мешают.
По обоим пунктам львиная доля разработчиков с вами не согласится.
Понравилось. Напоминает о том, что поведение любимого компилятора — не стандарт. А с (6) сам в своё время столкнулся.
Скорее, undefined behavior в любимом компиляторе слишком часто совпадает с желаемым.
4 — скорее знание особенностей вещественных чисел, потому что на другой платформе это может быть не так.
3, 12 — не сообразил.
6 — слишком очевидно для данного списка.
Помнится меня в своё время поразило, что код
#include <stdio.h>

int main()
{
    printf( "%c\n", 4["abcdefgh"] );
    return 0;
}

корректен.
4. IEEE 754 настолько распространён, что сложно назвать устройство где какой-то другой floating point.

3. Если yp = zp, то выражение *xp + *yp меняет значение после первой итерации и поэтому его нельзя вынести за цикл.

12. INT_MIN / -1 должно быть равно -INT_MIN, но это больше INT_MAX, поэтому получаем переполнение числа со знаком.
3, 12 — имелось в виду не сообразил до тех пор пока не прочитал белый текст :) Остальные понял сам без объяснений — когда-то копался в неопределённом поведении C/C++.
4 — в принципе оно конечно так, но всё же проблема не Си-специфична, а скажем так, имеет пересечение, — одновременно шире (проявляется и в других языках в т.ч. ассемблере) и уже (может не проявиться на каком-либо софтверной реализации floating point, например).
НЛО прилетело и опубликовало эту надпись здесь
О_О, это что за процессор?
есть множество процессоров где sizeof(char)==sizeof(int)==sizeof(long)==1 и при этом char реально занимет 16 либо 32 бит. Обычно это всякие DSP.
Стоп стоп стоп. Вы пишете sizeof(char) == 1, потом что char занимает 16-32 бита.
Либо вы имели ввиду sizeof(int)==sizeof(long) ==1, а чар занимает 2-4 байта.
Либо вы имеете ввиду другой размер байта
(пухнет голова)
#define CHAR_BIT 32, всё просто.
Ну если байтом называть не 8 бит, а минимально адресуему единицу информации (то, на что может указывать указатель), то да — байт может занимать и 16 и 32 бита.
да, хочу напомнить что стандарт C требует только одного: что бы sizeof(char)<=sizeof(int)<=sizeof(long) и т.д…
sizeof(char) == sizeof(signed char) == sizeof(unsigned char) == 1 всегда, в не зависимости от того, сколько в нём бит.
6.5.3.4/3 в C99 если что.
INT_MIN / -1 НЕ должно быть равно -INT_MIN. Ошибочно думать, что int представляет целые числа, точно так же, как ошибочно думать, что double представляет числа действительные. Тут совсем другая математика. И когда мы пишем x/-1 компилятор должен для нас сгенерировать соответствующий код, но он совсем не должен нам гарантировать получение каких-то результатов. UB в данном случае прописано в стандартне не для компилятора, а для программистов.
> INT_MIN / -1 НЕ должно быть равно -INT_MIN
Я имел ввиду алгебру. В алгебре это действительно равно.
Алгебра — тоже штука тонкая. Не во всякой алгебре x / -1 = -x :) Просто мой вопрос-то вот в чём: зачем так педалировать эти моменты? Любой нормальный программист, который пишет на Си вполне себе в курсе, что он не работает в кольце целых чисел. Если он думает иначе, значет, это программист, ну, на Clojure, например.

Какие проблемы-то? То, что div можно выполнять по-разному — так это известные истины. В чём Вы видите тут сложности и трудности связанные с Си?
В других языках (тот же clojure, python) все эти тонкости успешно скрывают от программиста. А в си повсюду разбросаны UB.
Успешно ли? Например, в Clojure, конечно, скрыто, что у числа длина конечная за счёт длинной арифметики, но потом программеры долго удивляются, а чего это у них простой алгоритм преобразования фурье всю память в системе пожирает. Как бы, закон сохранения косяков в природе: в одном месте подпилили, так оно в другом вылезет. Невозможно скрыть то, что программа для компьютера работает на компьютере с его ограничениями, вопрос лишь в том, как эти ограничения проявяться.
То, что код
int x;

if(abs(x)<10){

}
может работать не так, как ожидалось, для меня было большой и неприятной неожиданностью.
Действительно, занятный пример. А как объясняется такое поведение? Ведь, по сути, мы имеем аналог
char s[] = "abcdefgh";
printf( "%c\n", s[4] );

?
Да, и можно написать 4[s] — это тоже будет корректно. Объясняется очень просто: x[y] является эквивалентом *(x+y), и порядок действий не важен.
Спасибо большое ) Ваше объяснение позволило посмотреть на код под другим углом зрения. Очень познавательно, благодарю.
Для PHP-кодеров тоже актуальна фраза «Я не знаю PHP».
Мне кажется, для большинства языков такое высказывание верно. С PHP точно беда. Когда совсем не знаешь его и смотришь на код быдлопрограммера, с трудом представляешь, на сколько можно так не знать :)
Так что выводит 8?
1 2 10?
Да, 10 2 10. Добавил в топик.
Все таки ошибся про прохождении 7 задания.
По поводу 5: вторая ошибка в том, что функция не принимает на вход параметр длины стороки, предполагая, что входная строка будет NULL-terminated:
const char str[] = {'a', 'b'};
int len = my_strlen(str);
Имелась ввиду null-terminated строка. Сделал уточнение.
Передавать длину строки в функцию вычисления длины строки?
Хм. Интересно получилось…
В любом случае длина верно будет вычислена только для null-terminated строк.
А, вдруг?
Хех, я почти знаю Си :)

Второй пример смутил, хотя бы потому что подобные оптимизации всегда нужно держать в голове и проверять что там генерируется. Собственно любой человек который писал защиту он нелегального копирования на языках высокого уровня с этим точно сталкивался. Я даже затрудняюсь сказать сколько раз компилятор меня удивлял в этом отношении. А так — это действительно зависит от компилятора, специально дополз до компьютера проверить (clang) — заведена локальная переменная, есть получение значения, сравнение и корректные джапмы.
По-моему это не примеры на знание Си, а примеры «как не надо писать код». Особенно п.7 — убейся об стену тот, кто такое напишет в программе.
Это минимальные wtf-примеы. Все примеры взяты из реальных программ, просто уменьшены, сложные типы заменены на встроенные и так далее.
Я понимаю, что это все из реальных примеров. Но тут есть примеры которые:
— я знаю;
— я знаю, но толку от знания никакого (п.2 только по невнимательности мог быть написан, т.е. не зависит знал человек о том как эта фигня будет работать или нет);
— я не знаю, но никогда так не напишу (п.7-8)

Выходит я не знаю С, но умею писать неплохие программы на С :)
2. Представьте себе, что это критически важная функция, например в ядре, пусть если bar() выполняется, то это уязвимость. Вы исправляете баг, который случайно нашли «глазами». Если вы не выпустите security bulletin, то вы «тихо» закрыли уязвимость и поэтому кто-то мог не пропатчить свою систему.

7. Вы никогда не приводите указатели в Си? Всегда дотошно проверяете, что не нарушаете алиасинг?

8. Запятая широко используется в макросах. Код в примере вполне может быть результатом препроцессирования нескольких макросов.
fix: если bar() выполняется при нулевом x.
Конечно же привожу указатели к другим типам. Но передавать один и тот же указатель в качестве аргументов ф-ии, которая принимает 2 не константных указателя (т.е. меняет оба?) да еще и разных типов — я бы очень задумался.
Это пример. Упрощённый пример. Эти указатели не обязательно передавать в функцию, их можно получать через вызов другой функции, или найти в какой-то структуре данных или ещё как-нибудь.
А если я напишу
int x=300;
printf("%d ",x);
((char*)&x)[1]=0;
printf("%d\n",x);
то тоже будет шанс увидеть «300 300»?
Для char* есть исключение, он может алиасить любой указатель.
то что это wtf и не оспаривается.

я ответил «некорректный код» на первый вопрос.
и почему то не считаю что ответил неправильно.

тут же попробовал скомпилить это, visual studio выдал error C2086: redefinition.
можно попробовать и на других компиляторах, но даже если это где то и заработает, меня не убедит в том, что это не гогнокод и что это корректно.
А на два цикла
for(int i = 0; i < 10; i++) {
//smth
}
for(int i = 0; i < 10; i++) {
//smth
}

вижуал студия уже перестала ругаться, что redefinition?
Последняя версия студии, на которой переменная, объявленная в заголовке цикла считалась глобальной — VC6.
А в VS2005 этой проблемы уже не было.
проверил. не ругается. не встречал такой буг.
Вы пробовали код на Си или C++ писать? Код
#include <stdio.h>                                                                                                                                                   
                                                                                                                                                                     
int i;                                                                                                                                                               
int i = 0;                                                                                                                                                           
                                                                                                                                                                     
int main()                                                                                                                                                           
{
    return 0;
}

нормален с точки зрения Си, но не С++.
если только воспринимать это как баг, который был в компиляторах си, и пофиксили по пути к внедрению с++. это не нормально.
Это не баг. Это особенность. А то так можно и такое:
int main(argc,argv)
int argc;
const char** argv;
{
    return 0;
}

багом объявить ;)
Ах, да. Хотел сказать и забыл. В Си это просто декларация, подобная «extern int i;» в C++. Проверьте, добавление extern сделает код валидным для C++.
а в стандарте то оказалось прямо написано чего это обозначает. к экстерну отношения не имеет.
экстерн работает на уровне линкера. а неполное определение на уровне компилятора.
Подобие ≠ эквивалентность.
Подобие в том, что если в одном модуле
int i;
в другом
int i =5;
то это одна переменная равная 5.
баг в чистом виде.
тоже возникло такое впечатление. редкостной маразматичности куски кода. индус-стайл.
«хороший программист простым кодом пишет гениальные алгоритмы. а не наоборот.»(с) не помню чьё
НЛО прилетело и опубликовало эту надпись здесь
Вот интересный пример на С++:
int a = 2;
unsigned int b = 4;
cout << a - b << endl;

Однажды пришлось помучаться :(
Usual arithmetic conversions есть и в Си. Правда нет ни перегрузки, ни вывода типов и придётся результату тип вручную вписать.
Как-то неочевидно, на мой взгляд. Тем не менее, спасибо за то, что подсказали как гуглить. Уже просветился)
Да, часто спрашивают на собеседованиях. Хотя основное, что здесь надо знать — все преобразования с unsigned int ведутся к этому типу.
Допустить такую ошибку очень легко даже зная что результат операции будет unsigned int. Так что основное что надо знать — это то, что компилятор выдаст варнинг и что варнинги надо внимательно читать.
g++ варнинг не выдаёт. clang выдаёт. Это, конечно, ещё раз показывает преимущество clang в диагностиках, но программистам, использующим gcc от этого не легче.
Да, никогда не любил всякие выскакивающие варнинги, потому всегда искал, как от них избавится. Но очень напрягает что, кроме как во всяких форумах и мелких статейках, ничего не расписано, а стандарт, даже на английском, вообще за деньги распространяется. =(
Многие даже про стандарт с99 ничего до сих пор не знают. =(
Студия сдержанно промолчала.
интересные дачки. к сожалению, верно ответил только на 9 из 12. пример с int заместо size_t выбивается из общего ряда. ошибка очевидна, но кажется, что не эта мелочь имелась ввиду. ну у кого длина строки не влезет в int?
За подобный код разработчика надо увольнять. Можно сразу при коммите/чекине делать автоматизированый анализ и в случае обнаружения генерить письмо ПМу ))
Давайте конкретные паттерны и будут варнинги в компиляторе. Только знаете в чём проблема? Этого не будет, потому что все приведённые в статье конструкции используются во вполне корректном коде.
1 — уже был коммент, уточните что i — глобальная.
2, 3, 6, 9, 10, 11 — если этого не понимать, то надо писать на java
4 — грабли будут на любом языке, хоть на javascript
5 — задумался ))) хотя и очевидно
7 — даже из буханки можно сделать троллейбус, но зачем? очевидно что так делать нельзя, а знать как конкретный компилятор в вакууме оптимизирует этот код — не нужно.
8 — буханка + gcc даёт вонрингами по рукам
12 — относится не только к C. боянчик, хотя очень интересный.

вывод — по сравнению с C++ — C — ясный и понятный язык.
7. В любом коде на Си есть приведения типов указателей. Кто даст гарантию, что они корректны? И далеко не всем очевидно, что так делать нельзя. И не обязательно передавать указатели вот так явно в функцию, их можно доставать из структур ланных или ещё как-то. К тому же, не все компиляторы используют sctrict aliasing при оптимизации и поэтому многие прораммисты даже не догадываются об этом (пока им не приходится портировать свой код на более строгий компилятор).

8. В макросах достаточно часто используются запятые. Замените константы на вызовы функций и варнинги исчезнут.
И да, не согласен с выводом. Поясню: а по сравнению с malbolge С++ вообще кристально ясен. Моё мнение: в Си слишком много UB и встроенных правил (те же usual arithmetic conversions).
забыл добавить, что имхо на C как раз и пишут из-за того, что можно достичь максимальной производительности используя особенности CPU напрямую (9, 10, 11) и/или иногда перекрывающиеся области памяти (3)
если этого не нужно — на C писать тоже не стоит.
9, 10, 11. То есть, вы считаете, что если сложение чисел со знаком реализовано через add на x86, то вы можете рассчитывать на свойства переполнения чисел со знаком (конечно, расставив нужные макросы условной компиляции «только x86»)?
x86, PPC, M68K, ARM, можно ещё добавить Z80 — мне хватает ))
Тогда выражение (x+1)<x истинно тогда и только тогда, когда в x+1 происходит переполнение, так?
что есть x?
Извиняюсь. int x;
я честно говоря не очень понимаю что вы хотите доказать.

int x;
x = ....;
if ( (x + 1) < x ) { printf(«here\n»); }

для 32-битного компилятора будет истинно при x = 0x7FFFFFFF
т.е. хочу сказать что бесполезно рассматривать сферические примеры в вакууме (особенно в случае C).
Это вполне конкретный пример (определения того, а не произойдёт ли переполнение при инкременте), который был написан для twos complement машин и работал до тех пор, пока его не скомпилировали с оптимизатором, который пользуется правом, данным ему по стандарту.
А Вы знаете такой оптимизатор, который умеет определить ситуацию, что в данном случае будет UB, чего-то там соптимизировать, но при этом не предупреждает программиста об этом UB?

Я пользуюсь Clang и GCC, и оба из них вполне себе предупреждают о подобных местах в коде.

Насколько я могу судить, компилятор от Intel тоже такие предупреждения выдаёт.
Оптимизатор не обязан это делать. Хорошо что в данном случае это выражение соптимизировалось в clang, у которого есть AST с выражениями и номерами строк. Но если какое-то другое выражение сворачивается на уровне LLVM (например, посмотрите вот этот доклад www.youtube.com/watch?v=r3ic-IHJTuA&list=PL970A5BD02C11F80C&index=12&feature=plpp_video и какие выражения LLVM вскоре будет уметь сворачивать) то на уровне LLVM даже может не быть нужной информации о строках. Или хуже того, такое выражение появится в результате двух уровней инлайнинга и трёх шагов раскрутки цикла: просто нет такой одной строки, где это выражение записал программист.
Это я не то написал. Оптимизатор — это компилятор, конечно. А остальное — вопрос технологий.
Так в чём суть вашего ответа? Вы спросили существует ли компилятор, который [...]. Да, существует. И какая разница, вопрос технологий или нет. Лучшего у нас нет. А то, что теоретически можно было бы и выдавать диагностику — к вопросу не относится.

Подождите. Clang 3.0, как раз, навыдавал кучу предупреждение об UB на ваших примерах. GCC 4.6.2 тоже. То есть, я понимаю, конечно, что существуют абстрактные примеры, когда вот тут пойдёт всё через LLVM и кто-то что-то не заметит… Но я думаю, в реальности, достаточно сложно и неестественно сгенерировать такие примеры.
Ещё раз, предупреждения появились потому что тут всё сворачиваемое выражение видно сразу на этапе обработки AST. Давайте я вам приведу простой пример. LLVM умеет сворачивать x*2/2 -> x. Но не потому что программисты это пишут в коде, а потому что в результате инлайнинга или вынесения общих подвыражений такое часто получается.
А теперь я вам говорю, что компилятор имеет право (и clang, например, пользуется этим правом) сказать что (x+1)<x всегда false, потому что или это false по стандарту или тут undefined behavior и тут false потому что clang'у так хочется.
и такое бывает.
печально что clang не показывает ворнинг в таком случае (gcc, к примеру, должен).

собственно что я говорю — сферические примеры в вакууме ничего не дают.

есть конкретные вещи в которых это удобно и оправданно (написание эмуляторов, например).
Это не сферические примеры. Это настоящий пример, в котором программист рассчитывал, что раз сложение реализуется командой add, то и переполнение в Си будет такое же. Пишете эмулятор и нужно переполнение чисел со знаком mod 2^n — напишите inline asm.
что-то мне наш спор начинает напоминать анектод про суровых сибирских мужиков и японскую бензопилу.
нормальный здравомыслящий программист в нормальном не сфарическом коде так никогда не напишет. даже если и напишет — компилятор он умный, он скажет. программист возьмёт и исправит.

эмулятор, кстати, давно написан, работает на x86, ARM и PPC. компилируется gcc или visual studio, естественно с оптимизацией (насколько я помню, ни одного ворнинга).

inlne asm использовать вредно. во-1 кроме x86 есть и другие архитектуры, а во-2, на современном процесоре писать руками код, лучше чем тот, который сгерерирует компилятор нецелесообразно по времени.

это я к чему — если не пытаться сломать компилятор, то всё будет работать именно так, как рассчитывается и никак по другому.

если же заниматься не пойми чем, то конечно, ничего не будет работать правильно, пиши хоть на ассемблере, хоть на C#
> (насколько я помню, ни одного ворнинга).

Скомпилируйте clang -ftrapv и запустите.

> во-1 кроме x86 есть и другие архитектуры

Спасибо Кеп, я этот комментарий пишу с ARM ноутбука.

> во-2, на современном процесоре писать руками код, лучше чем тот, который сгерерирует компилятор нецелесообразно по времени

inline функция c одной командой add в inline asm. Я вам не предлагаю переписать весь код на asm, я вам предлагаю показать в коде явно какая семантика вам нужна вместо того, чтобы надеяться на непонятно что.
Эмс… А чего вы хотите от -ftrapv? Он вам генерирует проверку на переполнения и поэтому не предупреждает, о том, что при статическом анализе предполагается, что переполнений не происходит.
Да, именно. А (b<0? a+b>a: a+b<a) — контроль переполнения при сложении. Как его вообще иначе можно написать (предполагая, что a и b — уже числа максимальной разрядности, например, long long)?
Именно. Только компилятор сворачивает (x+1)<x в false, а так ничего, контроль.
А на какой из современных архитектур знаковая арифметика работает по-другому?
В одном из проектов мне нужно было много работать с числами по модулю 1 (арифметика углов), так использование в качестве значений целых чисел было самым очевидным подходом (32 бит точности хватало с головой — это 1/3000 угловой секунды). Теперь, конечно, подумаю, не переписать ли его на unsigned long…
Нужна семантика переполнения по mod 2^n — используйте unsigned.
8/12. Хрѣново (упустил 1, 2, 7, 12).
Хотя я такими дебрями стараюсь не писать, что такое size_t, знаю, и как ведёт себя целочисленная арифметики на IBM-совместимых машинах — в курсе, Позор только, что 12 неправильно ответил.
Простите уж за ошибки. Что запостил энтером — того не вырубишь топором.
… попробуйте рубить модератором.
Да какой позор? Авторы стандарта Си сами о большинстве случаев UB узнали не с первого раза :), это же не априорные вещи. А появившиеся в результате практики программирования с различиями в арихитектурах компиляторов и процессоров.
Дело не в стандарте, а в том самом поведении целочисленной арифметики на IBM PC, которое я вроде бы знаю.
В данном случае UB означает, что компилятор может генерировать разный код. Дело не в самой арифметике, а в том, что некоторые компиляторы могут вставлять проверки на знаковое переполнение, некоторые могут не вставлять.

x86 процессору-то по барабану, на самом деле. Он выполнит MIN_INT/-1 и получит 0x8000..., установит флаг O и всё будет вполне себе определено.

UB тут означает, что компилятор может вставить INTO для проверки переполнения, может проверить возможность переполнения другим кодом, может ничего не делать, на переполнение может реагировать сам процессор через генерацию исключения и т.д. Но это всё не должно, теоретически, для программиста на Си быть сюрпризом, ибо Си — это такой удобный ассемблер, а когда мы пишем на ассемблере мы же знаем, что целочисленное значение — это совсем не представитель кольца целых чисел.
10ый сразу входным аргументом намекнул, что засада в переполнении. 11ый и 12ый сразу как то сами собой стали рассматриваться и с этой точки зрения.
если бы 10ого не было, то я б в них не понял бы, в чем ошибка.

тут кстати есть такой нюанс — типа да, каким то приемом из языка не пользовался никогда или с каким то конкретным багом не сталкивался, приходишь на собеседование, задают вопрос, тыкву чешешь и даешь неправильный ответ. потом приходишь домой, компилируешь в различных позах и все становится очевидно в течение нескольких минут. а поздно, интервьюер уже сделал о тебе мнение, что ты язык не знаешь и вообще безнадежен…
С другой стороны с Си и, тем более, Си++ всегда есть куда развиваться. Можно изучать их годами и постоянно открывать что-то новое.
зачем писать такой код, который не смогут поять приемники
Паять приёмники?
А в чём собственно проблема? Юный падаван всегда может использовать опции -Wall -pedantic (в нормальном компиляторе), и ему с удовольствием при обработке добрых 2/3 приведённых примерах с удовольствием подскажут, где находится лес. Ну, и про запятую у Вас не правильно написано. Запятая — она как раз порядок не гарантирует, и порядок выполнения операторов, записанных через запятую, зависит от компилятора. Из этой же серии всякие радости с f(a, a+=1, a-=1). Как бы…
мне подсказали, что для gcc нужно юзать не просто -Wall, а -W -Wall
Запятая включает sequence point. Просто запятая при вызове функции — это не оператор запятая.
Точно, моя вина… Почему-то решил, что со времён ANSI C ничего не поменялось в этом вопросе.
Ещё одно злобство Си (если стараться делать слишком красивые комментарии), пусть и не очень актуальное в наши дни:

#include "stdio.h"

void main()
{
// Эта программа должна вывести /
// первые 8 чисел Фибоначчи: /
// 1, 1, 2, 3, 5, 8, 13, 21. /

// Определим первое и второе /
// значение ряда - это единицы: /
int first = 1, second = 1;

// Сразу же выведем первое число: /
printf("%d, ", first);

// Цикл "делать следующее, пока /
// второе значение меньше 20". /
while (second < 20) { // проверили/
// Следующее - это сумма /
// первого и второго. Вычислим /
int next = first + second;

// Теперь надо записать второе /
// значение в первое, а следую- /
// щее - во второе, так ведь ???/
first = second;
second = next;

// Выведем на экран первое число/
printf("%d, ", first);
}
// Выведем последнее значение ряда/
printf("%d\n", second);
}


Все понимают, почему эта программа выводит не ожидаемый ряд «1, 1, 2, 3, 5, 8, 13, 21», а неожиданный «1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 20»? И почему это проявляется не у всех?
Н-да. А в предпросмотре не было этих переносов строк, а всё аккуратненько отображалось :(
$ cc 1.c -o 123
1.c: In function ‘main’:
1.c:25:53: warning: trigraph ??/ ignored, use -trigraphs to enable [-Wtrigraphs]
Для продолжения нажмите любую клавишу...

$ clang 1.c -o 124
1.c:3:1: error: 'main' must return 'int'
void main()
^
1 error generated.
Для продолжения нажмите любую клавишу...

поправляем...

$ clang 1.c -o 124
$ ./124
1, 1, 2, 3, 5, 8, 13, 21
Для продолжения нажмите любую клавишу...

$ ./123
1, 1, 2, 3, 5, 8, 13, 21
Для продолжения нажмите любую клавишу...

Ну да, эффект заметен как минимум в OpenWatcom и Lcc. А gcc, кстати, прямо игнорирует требование стандарта (впрочем, я с ним в этом согласен :)
Да за такой код — пожизненный эцих с гвоздями без права переписки :)
увы,
только 50% знания Си, хотя более половины примеров в жизни неприменимы.
Надо стараться писать «безопастный» код
по поводу (1)

$ cc 1.c -o 111
1.c: In function ‘main’:
1.c:6:9: error: redeclaration of ‘i’ with no linkage
1.c:5:9: note: previous declaration of ‘i’ was here
Для продолжения нажмите любую клавишу...

$ clang 1.c -o 111
1.c:6:9: error: redefinition of 'i'
int i = 10;
^
1.c:5:9: note: previous definition is here
int i;
^
1 error generated.

Все примеры — отдельные файлы исходного кода. Откуда у вас в сообщении компилятора In function ‘main’?

Так и знал, что народ будет в это вступать. Вы уже не первый, судя по каментам :)
понял, что вы имели ввиду, извините. Когда переменная глобальная то не ругает…
компилятором под vxWorks и солярис проверил, тоже не ругается на глобальные.
однако стало непонятно почему глобальная переменная так объявляется без ошибки, а локальная с ошибкой? особенности кода компилятора?

vs же ругается одинаково и на локальную и на глобальную.
Да объявлений глобальных может быть сколько угодно в месте объявления не выполняется никаких операций с памятью (собственно и места то никакого нет). (это если в одном модуле, если в разных то следует добавлять extern )

А вот в случае локального объявления (даже без инициализации) требуется выделить место на стеке и тут уже «бяда-бяда огорчение» ему что 2 раза там место выделять? вот и ругается.
И вообще «локальное объявление» ===«определение» только не инициализированное
получается что отличается синтаксис объявления локальной переменной и глобальной.
А должны они отличаться лишь в областью видимости. По определению.

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

Глобальную переменную тоже можно определить (точнее нужно и только 1 раз), но кроме того ее можно и декларировать, те намекнуть компилятору что будет где то далее она определена.

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

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

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

www.cplusplus.com/doc/tutorial/variables/
For example, if we want to declare an int variable called a initialized with a value of 0 at the moment in which it is declared, we could write:
int a = 0;

вот они говорят, что это декларация, то бишь объявление. включающее инициализацию.

о
ISO/IEC 9899:1999, 6.9.2 p 2

A declaration of an identifier for an object that has file scope without an initializer, and without a storage-class specifier or with the storage-class specifier static, constitutes a tentative definition. If a translation unit contains one or more tentative definitions for an identifier, and the translation unit contains no external definition for that identifier, en the behavior is exactly as if the translation unit contains a file scope declaration of that identifier, with the composite type as of the end of the translation unit, with an initializer equal to 0.

int i1 = 1; // definition, external linkage
int i1; // valid tentative definition, refers to previous
итак, все встало на свои места.

пример использования этой главы стандарта:
файлы
source1.c:
int i = 10;

source2.c:
#include «source1.c»
int i;

source3.c
#include «source2.c»
int i;

в момент компиляции файла source3.c все три файла представляют собой т.н. translation unit. (почитал, что это такое в этом стандарте).

в файле source1.c — определение переменной с инициализацией, получается, что, понятия «объявление переменной» не существует. объявление(declaration) согласно стандарту есть только у функций.
в файле source2.c — «неполное определение», которое использует int i=1; из файла source1.c как external (не навело это слово на мысль?)
в файле source3.c такое же «неполное определение», которое также использует ту же переменную из source1.c (слова one or more как бы намекают, что неполное определение в файлах состоящих в translation unit, может присутствовать более одного раза).

в случае, же если в файле source1.c определение переменной затереть, то подразумевается, что переменная i = 0 по умолчанию, и одна и та же для файлов source2.c и source3.c.

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

по примеру автора попрошу объяснить сакральный смысл зачем бы это могло быть использовано. «это» — два определения в пределах одного файла.
и кстати даже интересно, каким образом это породило багу в коде?
кто то увидел верхнее определение и подумал, что оно по умолчанию 0, а оно оказалось 10?

ЗЫ и все таки баг компилятора.
Большинство подобных UB выявляются компиляторами (clang лучше чем gcc) и статическими анализаторами (чем дороже тем эффективнее от cpplint до prevent)

2 и 7 могут проявится только при высоком уровне оптимизации (больше -O2 в gcc), когда компилятор начинает делать небезопасные оптимизации. А, вообще, компиляторы всегда стараются придерживатся консервативной стратегии по умолчанию.

Ответил неправильно на 5 из 12 в основном на вопросы касающиеся переполнения.

на 4й вопрос есть еще один ответ. если в коде выше будет определен #define NDEBUG, то ассерты будут выключены и функция может получить 0.

Автору так же, наверное, стоило бы добавить что таких «темных мест» в c++ гораздо больше ;)
Я не сяшник, на большинство вопросов ответил правильно. Но в вопросах, которые не касаются устройства процессора(а СИ тут вообще не при чем в половине случаев), поведение больше похоже на ебанутый выпендреж и запудривание мозгов, если честно. Впрочем, опять-же, я не СЯшник, возможно такое усложнение правда зачем-то нужно.
Мда. Я не знаю C и люблю python теперь гораздо сильнее.
Да ладно тебе. Автор привел примеры «как не надо писать код», не спорю некоторые могут появиться в виду неопытности или не знания особенностей языка. А уж специфика оптимизаторов эт вообще к языку отношения никакого не имеет.
Боюсь если я сейчас сяду писать на питоне, то тоже нагорожу огорода и тоже будут примеры «как не надо писать на питоне»
У питона изначально другой подход:

Simple is better than complex.
Complex is better than complicated.
Readability counts.
If the implementation is hard to explain, it's a bad idea.

И так далее (http://www.python.org/dev/peps/pep-0020/)

Понятно, что Perl-программист может на любом языке написать программу на Perl, но в питоне это ему будет сложнее всего, и это я считаю очень важным достоинством языка.
Да ладно. У Питона те же проблемы с арифметикой, например. Просто некоторые проблемы вызваны тем, что работа идёт на компьютере, а не в идеальном абстрактном мире математики.
Да сколько можно. Это минимальные wtf-примеры. Все примеры взяты из реальных программ, просто уменьшены, сложные типы заменены на встроенные и так далее, поэтому конкретно такой код кажется ненастоящим. Но настоящий код просто нельзя поместить в статью — никто с этими огромными листингами бы не разбирался.
Это глупости программерские. Работал 2,5 года с индусами и японцами. И тоже видел много разного кода из реальных программ. Если програмер не понимает чего он делает, то он хреновый программер.

Если мои попробуют в проекте написать что то подобное 3, или им по архитектуре понадобиться strlen для 2Gb строки (или просто списки число которых не поместятся в 2G и они по ним будут циклами бегать), или написать чтото подобное 6 — получат «Review Failed» по рукам, и я даже разбираться не буду че там defined, а что undefined.

Если код такой что требуются знания тонких материй чтобы понять как он работает (исключая случаи вырожденной оптимизации) — то это говнокод, и совершенно не важно какой там использовался язык.

Не умаляю ваших заслуг, но так поиграть мышцами перед очередным кандидатом на собеседовании — годиться. Ну а реальность она на то и реальность. И быстро пофикшенное, косяком не считается :).
Видимо вы ничего не поняли.

Суть примера 3 не в том, что в программе строки 2 Gb и по ним циклами бегают. Хотите я вам аналогичный пример приведу где размер матрицы вычисляется в int?

Суть примера 6 не в том, как вывести строку в обратном порядке, а в типовых граблях циклов с беззнаковым счётчиком.
я про пример 5 речь с 2 Gb вел. Но с трудом представляю как можно считать 2Gb матрицы не представляя диапазон интов. Тут просто вопрос кому доверили такое писать.

ну а типовые грабли на то и типовые, что опытный программер на них уже наступил.

Странно, почему нету баянчика с бесконечным циклом при увеличении float на 1
С матрицами — элементарно. На 32-битной машине код спокойно работал годами. Потом купили 64-разрядную так как потребовалась больше размерность.
ну эт боян боянистый, спорим что косяки со счетчиками у них при этом были у них не самой большой проблемой ;)
Я вам даже больше скажу, вот прототип функции MPI_Send, на которой весь HPC держится:
int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm)

int count. И эти интерфейсы разрабатывали не кто-то-там, а комитет, в который входят все именитые вендоры.

ну когда эти интерфейсы разрабатывали 2 GB за одну посылку было нечто заоблачным. да и сейчас пару гектар за раз отсылать — моветон.
MPI 2.2 — сентябрь 2009.
> пару гектар за раз отсылать — моветон

?! Нужно переслать матрицу. Зачем её бить на несколько посылок, чтобы скорость передачи уменьшить за счёт нескольких синхронизаций разве что.
необозначенный int равен разрядности указателя, разве нет?

то бишь на 64х-битной платформе int = int64
Нет (например, на x86-64 int 32-разрядный). Единственный целый тип, который гарантированно не меньше указателя — intptr_t.
Так MPI_Datatype же не для красоты предусмотрен :) Там можно хоть террабайт задать при помощи count=1.
Спасибо, я знаю. Но существующему коду от этого не легче.
Почему Вы думаете, что не легче? Это же интерфейс для системы, которая должна работать на куче различных архитектур. Ну написали бы туда uint64_t count и получили бы целую кучу радости по отладке поддержки длинных чисел на всевозможных процессорах, которые не умеют 64-битными значениями оперировать. Да ещё нужна была совместимость с Fortran.

А тут выбрано вполне адекватное решение. Переносимое, простое, более гибкое, которое может работать даже на 16-битовых системах.

То есть как бы… вот… UNIX Way.
Почему туда нельзя написать size_t?
Так Fortran же. Он только об int знает. Кроме того, как мне кажется, это плохо для переносимости. Потому что MPI-программы пишут не профессиональные программисты, а научные коллективы. У них другие идеалы красоты, поэтому они даже и не почешуться, чтобы выяснить пределы size_t на конкретной платформе и учесть их. А тут int он и есть int. Он даже в большинстве случаев остаётся 32-битовым.
Да что ж меня на инфинитивы-то тянет так!?
Fortran знает про integer(C_SIZE_T).

> поэтому они даже и не почешуться, чтобы выяснить пределы size_t на конкретной платформе и учесть их

Как раз наоборот, если использовать size_t для представления размеров, то не нужно ничего выяснять и учитывать.
НЛО прилетело и опубликовало эту надпись здесь
wtf в плане оптимизаций есть в любом языке, без этого не как. А я про ежедневное использование. В питоне в этом плане почти не приходится заморачиваться, а в C undefined behaviours — это что-то крышесносящее.
да не это просто дело привычки. Иногда смотришь в код — не понимаешь что же тебе в нем не нравиться но пятой точкой чуешь что где то подвох.
Ну и потом это таки низкоуровневый язык и чето я не припомню там много всяких «undefined» приведеные выше из разряда «не надо так делать, а если уж сделали ...».

Ну например ТС с лукавил в примере №2. Т.е. прога падать при разыменовании нуля не должна, но при этом оптимизатор таки должен был вынести блок if :).

Знатокам железа: «я не знаю архитектуру PC»: Пример №1: в DVI порт втыкаем пъезозажигалку и нажимаем кнопку: сгорит память на видюхе? или только проц?
По хорошему, должна сгореть гальваническая развязка. Но если её нет, то…
:) молодец аналогию понял. Тут тоже по хорошему… а на деле индусы могли много че в оптимизаторе понаписать :)
Вот чувствую, многие собеседующие воспримут статью, как руководство к действию.
А я все правильно ответил. Что я делаю не так?
А если серьезно, то полный бред — в статье указаны проблемы с переполнением и floating point, которые вполне задокументированы, а остальные примеры проблемного кода видимо взяты из записок сумасшедшего. Это ж надо писать, например, такое:
long a = 10;
f((int *) &a, &a);
Я бы тоже полюбил питон, если б думал, что это в си нормальный подход.
Комментарии не читай @ сразу отвечай. Все примеры взяты из реальных программ, но упрощены, потому что с огромными листингами никто бы не возился.

7. В любом коде на Си есть приведения типов указателей. Кто даст гарантию, что они корректны? И далеко не всем очевидно, что так делать нельзя. И не обязательно передавать указатели вот так явно в функцию, их можно доставать из структур данных или ещё как-то.
> И далеко не всем очевидно, что так делать нельзя.

Тогда этим всем надо посидеть еще в junior-coderах и почитать книжки.
Название книги, где объясняются правила strict aliasing'а, в студию!
ну гугол говорит вот:
habrahabr.ru/blogs/cpp/114117/ «Стандарт: N1124, 6.5(7)»

а как связана проблема strict aliasing и собственно си? Сначала увидели возможности оптимизации, заюзали ее, потом бац увидали косячок… ну значит придумаем правило для оптимизации. Кому то очевидно кому то нет.

Так можно гораздо более жизненные примеры привести с не volatile флагом и циклом ожидания его в соседнем потоке
Книгу давайте. Про алисинг нельзя гуглить если не знать что (1) он такой существует и (2) как он называется. Таким образом про алиасинг гуглят только те, кто уже в курсе. Если у джуниора код как-то не так себя ведёт, он может только у кого-то более опытного спросить и внезапно узнать про алисинг или прочитать в книге. Так что давайте книгу.

> а как связана проблема strict aliasing и собственно си?
Это неотъемлимая часть стандарта Си, собственно.

> Сначала увидели возможности оптимизации, заюзали ее, потом бац увидали косячок…

Не понял.

> примеры привести с не volatile флагом и циклом ожидания его в соседнем потоке

О, вижу ещё драконов. Можно по-подробнее, что там с volatile и какие гарантии, по вашему мнению, он даёт в многопоточных программах?
Гарантию вам даст только страховой полис (с).

А volatile частично отключает оптимизацию — запрещает оптимизировать блоки использующие переменную так она может изменяться из нескольких потоков…

Например оптимизатор может выкинуть блок цикла заменив условие цикла на if если увидит что внутри него счетчик цикла не меняется. volatile запрещает это делать.
Если я правильно помню еще отключается кеширование значения переменной и вынуждает каждый раз читать его.
> Если я правильно помню еще отключается кеширование значения переменной и вынуждает каждый раз читать его.

Вот это (и аналогично про запись: нельзя заменить несколько последовательных записей на одну последнюю) единственное, что записано в стандарте. Про потоки там ни слова.

Вы ничего не сказали про потоки, поэтому скажу я. Для синхронизаций, критических участков и lock-free алгоритмов использовать volatile нельзя, потому что компилятор и ЭВМ могу переупорядочить (и переупорядочивают) записи/чтения volatile и не-volatile переменных. То есть, запись, которая стоит после точки синхронизации, сделанной на цикле из volatile, может переехать в точку до этого цикла.
Компилятор не имеет права переупорядочивать записи в volatile переменную. Потому что, эти переменные нужны для работы с MMIO. А там переупорядочишь записи и всё, взорвётся винчестер :)

Локальный порядок доступа для volatile-переменных гарантируется. Другое дело, что в многопроцессорной системе уже всё перемешивается и надо использовать всческие барьеры памяти.
volatile и не-volatile друг относительно друга — имеет. Это имелось ввиду.
Использовать можно что угодно для чего угодно, надо только понимать что делаешь и какие это будет иметь последствия.

Понятно что про потоки стандарт ничего не будет упоминать, это все таки к языку отношения не имеет.

Я просто вел речь про более простые и наверно чаще всего сталкивающиеся с этим примеры.

Сначала ничего не знают про мутексы и работают с тупыми флагами — все глючит, лечат sleep-ами.

Потом узнают про всякие мутексы — радуются используют мутексы и убирают слипы — все начинает тормозить

Далее начинают понимать что есть гонка и опять возвращаются к глобальным переменным, но уже юзают их «локально» заинкапсулировав до безобразия — программа в дебаге и в релизе начинает работать по разному, причем в релизе «то работает то нет»

Ну и следующие шаги просвящения — volatile ну и высшая ступень — барьеры…

Но в целом один из Великих сказал: «Я понимаю, что я ничего не понимаю» так что чем лучше знаешь Си (да и Си-кресты) тем меньше ставишь себе самооценку. Ибо знаешь уже столько партизан в этом лесу, что начинаешь предполагать сколько их еще там прячется.
Глупость написали. Только идиот сразу после того, как узнает про потоки не дочитав главу до конца пойдёт лепить флаги.
А про джуниора. Ну вот я нутром чую что не стоит передавать по указателю два адреса. И использовать long* и int* одновременно и использовать два разных типа там где достаточно одного
В пятом примере еще есть потенциальный момент, если передана строка без нуль-терминального символа.
Но решение у этого уже не тривиальное.
это нетривиальное решение называется strnlen.

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

ЗЫ и кстати да, с переполнением это ошибки не от незнания С, то есть это не ошибка непостредственно кодера, это архитектурная ошибка, если результат вычисления может непоместиться в переменную.
Пункт 5 спорен. В реальном коде это не ошибка (и то же Qt вполне себе использует int для всевозможных функций размера). Разница становится критичной, только если строки в 2 ГиБ — это нормальный кейс. Впрочем, лично я предпочитаю использовать size_t.

Пункт 2 понравился :)

10 тоже интересный. Я считал, что поведение как в 9.
Сегодня нечто не может быть 2 Gb, а завтра — вполне. Вот прямо сейчас разработчики OpenMPI в списке рассылки разгребают проблемы, связанные с тем, что там размеры объектов в int вычисляются. А ведь раньше такие огромные сообщения «нормальным кейсом» не были.

Qt использует int для размеров и создаёт этим кучу проблем для кода, который в остальных местах использует size_t. По-моему очень спорное решение, не ясно какая была мотивация.

Публикации

Истории