Я не знаю Си

    Цель этой статьи — заставить всех, особенно программистов на Си, сказать «я не знаю Си».
    Хочется показать, что тёмные углы в Си значительно ближе, чем кажется и даже тривиальные строки кода несут в себе undefined behavior.



    Статья организована как набор вопросов. Ответы написаны белым. Все примеры — отдельные файлы исходного кода.

    1.
    int i;
    int i = 10;
    


    Q: Это корректный код? (Не возникнет ли ошибка, связанная с тем, что переменная определяется два раза? Напоминаю, это отдельный файл исходного кода, не на уровне функции или compound statement)

    A: Да, это корректный код. Первая строка это предварительное объявление (tentative definition), которое становится объявлением после того, как компилятор обработал определение (вторую строку).

    2.
    extern void bar(void);
    
    void foo(int *x)
    {
      int y = *x;  /* (1) */
      if(!x)       /* (2) */
      {
        return;    /* (3) */
      }
    
      bar();
      return;
    }
    


    Q: Оказалось, что bar() вызывается даже тогда, когда x — нулевой указатель (и программа не завершается аварийно). Ошибка оптимизатора или всё корректно?

    A: Да, всё корректно. Если x — нулевой указатель, то в строке (1) появляется undefined behavior, и тут уже никто ничего программисту не обязан: программа не обязана ни упасть в строке (1), ни сделать return в строке (2) если вдруг удалось выполнить строку (1). Если говорить о том, какими правилами руководствовался компилятор, то всё происходило так. После анализа строки (1) компилятор считает, что x не может быть нулевым указателем и удаляет (2) и (3) как недоступный код (dead code elimination). Переменная y удаляется как неиспользуемая и так как тип *x не квалифицирован volatile, то и чтение из памяти тоже удаляется.

    Вот так неиспользуемая переменная убрала проверку на нулевой указатель.


    3.
    Была вот такая функция:
    #define ZP_COUNT 10
    
    void func_original(int *xp, int *yp, int *zp)
    {
      int i;
      for(i = 0; i < ZP_COUNT; i++)
      {
        *zp++ = *xp + *yp;
      }
    }
    


    Её захотели соптимизировать так:
    void func_optimized(int *xp, int *yp, int *zp)
    {
      int tmp = *xp + *yp;
      int i;
      for(i = 0; i < ZP_COUNT; i++)
      {
        *zp++ = tmp;
      }
    }
    


    Q: Можно ли вызвать исходную и оптимизированную функцию так, чтобы получились разные результаты в zp?

    A: Можно, пусть yp == zp.

    4.
    double f(double x)
    {
      assert(x != 0.);
      return 1. / x;
    }
    


    Q: Может ли эта функция вернуть inf (бесконечность)? Считать, что числа с плавающей запятой реализованы по IEEE 754 (подавляющее большинство машин). assert включен (NDEBUG не определён).

    A: Да. Достаточно передать денормализованный x, например, 1e-309.

    5.
    int my_strlen(const char *x)
    {
      int res = 0;
      while(*x)
      {
        res++;
        x++;
      }
      return res;
    }
    


    Q: Приведённая выше функция должна возвращать длину null-terminated строки. Найдите ошибку.

    A: Использование типа int для хранения размеров объектов является ошибочным: не гарантируется, что int сможет вместить размер любого объекта. Следует использовать size_t.

    6.
    #include <stdio.h>
    #include <string.h>
    
    int main()
    {
      const char *str = "hello";
      size_t length = strlen(str);
      size_t i;
      for(i = length - 1; i >= 0; i--)
      {
        putchar(str[i]);
      }
      putchar('\n');
      return 0;
    }
    


    Q: Цикл является вечным. Почему?

    A: size_t — беззнаковый тип. Если i беззнаковое, то i >= 0 всегда выполняется.

    7.
    #include <stdio.h>
    
    void f(int *i, long *l)
    {
      printf("1. v=%ld\n", *l); /* (1) */
      *i = 11;                  /* (2) */
      printf("2. v=%ld\n", *l); /* (3) */
    }
    
    int main()
    {
      long a = 10;
      f((int *) &a, &a);
      printf("3. v=%ld\n", a);
      return 0;
    }
    


    Данную программу скомпилировали двумя разными компиляторами и запустили на little-endian машине. Получили два разных результата:
    1. v=10    2. v=11    3. v=11
    1. v=10    2. v=10    3. v=11
    


    Q: Как объяснить второй результат?

    A: В данной программе есть undefined behavior, а именно, нарушены правила строгого алиасинга (strict aliasing). В строке (2) изменяется int, поэтому можно считать что любой long не изменился. (Нельзя разыменовывать указатель, который алиасит другой указатель несовместимого типа.) Поэтому компилятор может передать в строке (3) тот же long, который был считан в процессе выполнения строки (1).

    8.
    #include <stdio.h>
    
    int main()
    {
      int array[] = { 0, 1, 2 };
      printf("%d %d %d\n", 10, (5, array[1, 2]), 10);
    }
    


    Q: Это корректный код? Если здесь нет undefined behavior, то что он выводит?

    A: Да, тут использован оператор запятая. Сначала вычисляется левый аргумент запятой и отбрасывается, затем вычисляется правый аргумент и используется как значение всего оператора. Вывод: 10 2 10.

    Обратите внимание, что символ запятой в вызове функции (например, f(a(), b())) не является оператором запятой и поэтому не гарантирует порядок вычислений: a(), b() могут быть вызваны в любом порядке.


    9.
    unsigned int add(unsigned int a, unsigned int b)
    {
      return a + b;
    }
    


    Q: Каков результат add(UINT_MAX, 1)?

    A: Переполнение беззнаковых чисел определено, вычисляется по модулю 2^(CHAR_BIT * sizeof(unsigned int)). Результат 0.

    10.
    int add(int a, int b)
    {
      return a + b;
    }
    


    Q: Каков результат add(INT_MAX, 1)?

    A: Переполнение знаковых чисел — undefined behavior.

    11.
    int neg(int a)
    {
      return -a;
    }
    


    Q: Возможен ли тут undefined behavior? Если да, то при каких аргументах?

    A: neg(INT_MIN). Если ЭВМ представляет отрицательные числа в дополнительном коде (англ. twos complement, подавляющее большинство машин), то абслютное значение INT_MIN на единицу больше, чем абсолютное значение INT_MAX. В этом случае -INT_MIN вызывает знаковое переполнение — undefined behavior.

    12.
    int div(int a, int b)
    {
      assert(b != 0);
      return a / b;
    }
    


    Q: Возможен ли тут undefined behavior? Если да, то при каких аргументах?

    A: Если ЭВМ представляет отрицательные числа в дополнительном коде, то div(INT_MIN, -1) — см. предыдущий вопрос.

    — Dmitri Gribenko <gribozavr@gmail.com>

    Creative Commons License
    This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 Unported License.
    Поделиться публикацией
    Реклама помогает поддерживать и развивать наши сервисы

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

                            Какой компилятор удаляет код? Это весьма дивное поведение и относится к компиляторам, а не стандарту.
                            • +8
                              Компилятор может делать всё, что хочет если это не меняет observable behavior. В строке (1) есть два варианта: или x не нулевой указатель и (2) не сработает, или x нулевой указатель и дальше можно хоть запускать форматирование диска.
                              • +1
                                тогда правильный ответ будет «я не знаю всех компиляторов», а не «я не знаю Си».
                                • 0
                                  Покажите в чём именно нарушение стандарта.
                                  • 0
                                    в том что ожидается выполнение if (!x) при x == (void*)0, что как раз таки меняет observable behavior
                                    • +1
                                      if (!x) не observable. return observable. Поэтому if() не обязан вычисляться. return мы не можем увидеть, потому что для того, чтобы он выполнился, должен был произойти undefined behavior, а дальше может происходить что угодно.
                                      • +2
                                        А как предполагается обращаться к нулевой ячейке (если это действительно нужно, в каком-нибудь микропроцессоре или на других компьютерах с 64К памяти)?
                                        • +1
                                          Менять null pointer constant — очень и очень сложно, так как очень многое в компиляторах завязано на то, что null pointer constant (который определяется как 0 любого целого типа, приведённый к указателю) имеет битовое значение 0 как указатель. Иначе, к сожалению, средствами языка никак. Но можно определить переменную и затем linker script'ом разместить её по нужному адресу.
                                          • 0
                                            Верно ли, что компиляторы обязаны на строчку *(int*)0x0000=0xCD выдавать ошибку?
                                            • +1
                                              Компиляторы — не обязаны, в рантайме падение тоже не гарантируется (undefined behavior — это всё что угодно, в том числе и игнорирование проблемы: как, например, переполнение чисел со знаком игнорируется в большинстве компиляторов Си для x86).
                                              • +3
                                                А с чего вы взяли, что нулевой указатель это undefined behavior? В эмбеддеде где нету MPU и MMU по нулевому адресу может храниться вектор ресета.
                                                • +5
                                                  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.
                                                  • 0
                                                    Просто я со своей колокольни смотрю, и там где я использую C (это эмбеддед) разыменование нулевого указатель вещь вполне обыденная, и предсказуемая. Хоть и не красивая. Самая часто встречающаяся ошибка это:

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

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

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

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

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

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

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

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

                                                  Эх, если бы не строки… С остальной памятью я уж как нть разберусь.
                                                  • +1
                                                    А чем не устраивают строки в Си?
                                                    • +8
                                                      Они прекрасны, просты и понятны до тех пор, пока не начинаешь с ними работать.
                                                      russian.joelonsoftware.com/Articles/BacktoBasics.html
                                                      • +4
                                                        Во первых — в С строк нет.
                                                        Во вторых — схема работы со строками в С оптимальна!
                                                        В третьих — язык, разработанный практиком отличается от языка, разработанного теоретиком наличием встроенного типа 'string'.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                                                                      Именно.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                                                                                          Напомню:
                                                                                                          SS и ß — это одна буква. Не говоря уже о том, что ü и комбинация знаков ¨ и u — это тоже одна буква.
                                                                                                          • 0
                                                                                                            я в курне про немецкий, mc при том что там это организовано и реализовано.
                                                                                                  • 0
                                                                                                    А чего это нет в стандартной поставке? В поставке есть wchar, пожалуйста, используйте. Будет вам перекодирование на лету в соответствии с локалью.
                                                                                                    • +1
                                                                                                      «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.»

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

                                                                                                > Кошмар. Единственная область применения, где имеет смысл заранее измеренная длина это приём/передача данных.
                                                                                                В остальных случаях тормозная конкатенация и нулевые символы нисколько не мешают, ага, как же :)
                                                                                                • 0
                                                                                                  Конкатенация будет тормозна в любом случае.
                                                                                                  А нулевые символы вообще ничему не мешают.
                                                                                                  • 0
                                                                                                    По обоим пунктам львиная доля разработчиков с вами не согласится.
                                                                        • +1
                                                                          Понравилось. Напоминает о том, что поведение любимого компилятора — не стандарт. А с (6) сам в своё время столкнулся.
                                                                          • +2
                                                                            Скорее, undefined behavior в любимом компиляторе слишком часто совпадает с желаемым.
                                                                          • +2
                                                                            4 — скорее знание особенностей вещественных чисел, потому что на другой платформе это может быть не так.
                                                                            3, 12 — не сообразил.
                                                                            6 — слишком очевидно для данного списка.
                                                                            Помнится меня в своё время поразило, что код
                                                                            #include <stdio.h>
                                                                            
                                                                            int main()
                                                                            {
                                                                                printf( "%c\n", 4["abcdefgh"] );
                                                                                return 0;
                                                                            }
                                                                            

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

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

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

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

                                                                                                if(abs(x)<10){

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

                                                                                          ?