Pull to refresh

Comments 122

После совета лепить везде calloc вместо malloc некоторый критический к производительности код станет чуточку ХУЖЕ. Предварительное забивание памяти нулем из соображений «как бы чего не вышло» гораздо хуже программирования без ошибок.

А такие низкоуровневые системные места нужно формально доказывать, а не проверять при помощи VALGRIND.
А такие низкоуровневые системные места нужно формально доказывать, а не проверять при помощи VALGRIND.

Покажите, какой-нибудь интересный низкоуровневый код, правильность которого вы формально доказали
Если вы хотите знать метод доказательства, или подход к доказательству, то он описан Дейкстрой еще в 70е годы.

1. Берем все системные функции за аксиомы, то есть считаем, что они работают в соответствии со своей спецификацией.

2. Предполагаем, что компилятор работает строго в соответствии со стандартом языка.

3. Исходя из этого, формально доказываем, что для всех входных данных программа работает в соответствии со своей спецификацией.

Если удалось доказать — ура, новых багов мы не внесли.
Если вы хотите знать метод доказательства, или подход к доказательству

Нет, хочу посмотреть на интересный низкоуровневый код, правильность которого вы формально доказали
Это такой странный формат аргумента «сперва добейся»? Извините, я в таких переходах на личности участия не принимаю. Спасибо.
Спасибо и вам — это вот яркая характеристика любителей разного рода «формальных доказательств» и эффектных фраз типа «да выкиньте вы свои валгринды, а лучше смотрите, какую я клевую книжку прочитал». Прямо хоть на стенку вешай.
Очень интересная штука!
Понятно, что по существу верификации сложно что-то сказать, но насколько я понимаю ради верифицируемости запрещено DMA, драйверы выселены в user-mode ну и функциональность, понятное дело минимальна ( однако есть треды! ). При этом предполагать корректность работы компилятора не надо, корректность трансляции в машинные коды тоже доказывается.
Ну и, конечно же, утверждать что в такой системе «нет багов» или она «невзламываемая» можно только в рекламных целях.
Почему вы не доверяете формальному доказательству?
Нет, я как раз к тому, что проверить формальное доказательство у меня и близко нет возможности и умения и поэтому можно только поверить им, что оно верно.
Проверить формальное доказательство относительно несложно (было бы, если бы они дали на него ссылку). Гораздо сложнее проверять неформализуемую часть — а именно формализацию (что они действительно доказывают «отсутствие утечек», например).
Итого вы утверждаете, что «сложно найти изъян в чьем-то определении условия отсутствия утечек»?
насколько я понимаю ради верифицируемости запрещено DMA, драйверы выселены в user-mode ну и функциональность, понятное дело минимальна

Нет. Это фишка микроядерной архитектуры самой по себе. Повышенная безопасность в ущерб перформансу.


Ну и, конечно же, утверждать что в такой системе «нет багов» или она «невзламываемая» можно только в рекламных целях.

Почему же? На уровне software вполне себе можно. Но кроме собственно ядра есть ещё hardware и userspace. И от физического доступа это очевидно не защищает. Только отсеивает некоторый класс атак. В целом даже такой уровень безопасности часто излишен.

Нет. Это фишка микроядерной архитектуры самой по себе.


Но позвольте:
The formal verification of seL4 on the ARM platform assumes that the MMU has complete control over memory, which means the proof assumes that DMA is off.
https://wiki.sel4.systems/FrequentlyAskedQuestions#What_about_DMA.3F
Здесь именно сказано, что доказательство предполагает, что DMA выключен.
Другими словами, сами вы этого ни когда не делали. Или я не правильно понял, и вы сможете (за определённую плату) выполнять формальное доказательство, для каждого изменения кода программы. Если можете, то хотелось бы подробностей, как это делается на практике.
На практике это делается так — новый алгоритм публикуется в рецензируемых научных журналах, а его авторы выступают на научных конференциях. В частности, в сфере вычислительной гидродинамики, действуют именно так.
Алгоритм — последовательность действий. Динамическая память — инструмент реализации.
Я не говорю, что вы не правы, просто я немного не могу понять суть.

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

Простите, а как публикация алгоритма и выступления автора докажут, что алгоритм правильно работает ( для всех случаев и без багов )?
Извините, но математика именно так примерно развивается с 16 века. Тогда было популярно искать, например всякие корни многочленов, и устраивались состязания между математиками. Тогда начал формироваться подход к строгому математическому рассуждению (в математике перестали верить на слово джентельменам).

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

Это доказательство будет опираться на следующие посылки:

1) Все библиотеки работают по спецификации.
2) Компилятор работает в соответствии со стандартом языка
3) Вероятность аппаратного сбоя пренебрежимо мала.

Теоретически, никаких фундаментальных запретов на это нет.

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

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

Если алгоритм представим в виде одного ДКА (на бумажке), то его реализация на компьютере будет сменой состояний уже второго ДКА (в кремнии). Как правило, алгоритм смены состояний большого ДКА (программа) содержит баги, поскольку этот ДКА большой и сложный (и тоже с багами!). Для того и тестируют.
Например, для написания «безопасного» кода при динамическом выделении памяти рекомендуется все же использовать функцию calloc() вместо malloc()
.

Речь шла о безопасном коде, а не производительном в данном случае.
Как забивка нулями повышает безопасность? Мусор вылезет сразу, а нули будут маскировать реальную ошибку.
Отнюдь.
Рассмотрим ситуацию, когда кусок памяти, выделяемой под строку (допустим, тот же «Hello, Habr!\n»), выглядит вот так:
0x602010: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x602018: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

Тогда после записи туда строки «Hello, Habr!\n» (без символа конца строки), этот кусок будет выглядеть так:
0x602010: 0x48 0x65 0x6c 0x6c 0x6f 0x2c 0x20 0x48
0x602018: 0x61 0x62 0x72 0x21 0x0a 0x00 0x00 0x00

Проблем нет, если мы попробуем распечатать эту строку функцией printf(), которая выводит все символы до тех пор, пока не встретить нулевой символ (конец строки), то она дойдет до первого куска памяти, в котором есть 0x00 (14 символ), и интерпретирует его как конец строки.

Теперь история с мусором:
0x602010: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x602018: 0x00 0x00 0x00 0x00 0x65 0x65 0x65 0x00


Собственно, после записи:
0x602010: 0x48 0x65 0x6c 0x6c 0x6f 0x2c 0x20 0x48
0x602018: 0x61 0x62 0x72 0x21 0x0a 0x65 0x65 0x00

Вывод программы:
-> Hello, Habr!
ee

Примерно так :)
Запись в буфер строки без символа конца строки — это явная логическая ошибка в программе.
Хорошо, давайте посмотрим с другой стороны.
Если нам придется дебажить программу в рантайме, как мы отличим инициализированные переменные от неинициализированных? Ну, отловили мы контекст, начинаем смотреть память.

А мусор по адресу бывает нередко «похож на правду». Очень часто это может запутать.

По поводу вашего предыдущего комментария — я, очевидно, не так прочел. Я соглашусь, что мусор вылезет сразу, а нули могут замаскировать ошибку. Тем не менее, зануление памяти в Си — очень полезная штука. Это не C# и не Java, здесь занулением никто, кроме программиста заниматься не будет.

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

Так точно. А заметание под ковер ошибок с неверно посчитанной длиной и недописанными терминаторами с помощью предварительной очистки всё равно рано или поздно вылезет в ещё более труднодиагностируемых местах.

Здесь есть другой аспект, с которым иногда приходится встречаться. Если по некоему адресу хранится указатель, или блок указателей, его лучше инициализировать, обнулять, потому что проверка валидности указателя, которая ставится в ifах и assertах, может выстрелить только при нулевом указателе, дав нам стек для отладки, а в ином случае там может оказаться мусор, который при этом является валидным адресом памяти, и ошибка уплывет дальше, где ее фиг поймаешь.
UFO just landed and posted this here
Разумеется. Память при выделении по идее должна обnilиваться, а не обнуляться, но я не проверял.
В системном программировании бывает невозможно использовать точки останова и другие прелести отладки, для примера написание встраиваемой ОС для какого нибудь специфичного МК, с ограниченным набором инструментов для разработки. В таких случаях для дебага только какая то запись в гарантированно неиспользуемую область памяти и её анализ после принудительной остановки.

Я в таком случае при разработке/отладке зануляю всё (можно c командами условной компиляции, определив, что DEBUG = 1 ), а после отладки и перед выпуском компилирую c DEBUG = 0 или лишние операции по занулению руками вырезаю, если код не огромный или не были применены команды условной компиляции.

И отлаживать удобно, и производительность в конечном счёте НЕ страдает.

Для безопасности не сильно важно, чем заполнена память при выделении. Для безопасности важно обнулить (или заполнить мусором) память перед освобождением.
Автор трактует «безопасность» в контексте fail safe. Т.е. речь идёт не о затирании приватных данных после окончания работы сними, а о сокрытии ошибок в ПО.
char *str = (char *)malloc(sizeof(char) * strlen(buffer));

И в чём проблема? Строка не обязательно должна заканчиваться нулём, если её длина известна и неизменна. Например, если мы меняем одни символы на другие, а потом сливаем результат в файл. Фиговатенько у автора с С.
Проблема в том, что подавляющее большинство строк в си, обработанных стандартными функциями, содержит в конце символ '\0'.
Хотите работать без него — тогда придется озаботиться использованием везде функций, которые ограничивают размер перемещаемой памяти (strncpy, snprintf, strncat), а в случае, когда у нас строки формируются из аргументов их использование (по назначению) не всегда возможно (только если не использовать подобие макроса strnsize, описанного в статье).

Это очень усложнит как процесс написания, так и процесс восприятия кода + количество функциональных вызовов на строку увеличится. Да и сам я не встречал проектов, где подобная практика используется :)
А между тем это очень правильная практика. Строка оканчивающаяся нулём пришла к нам из 60 годов. Великий замысел был в возможности сэкономить драгоценные байты памяти. За эту экономию мы до сих пор расплачиваемся стринг билдерами и переполнениями строк. Внезапно, сложение десяти строк через strcat даст экспоненциальную сложность, в то время как строки со счётчиком количества символов будут сложены с линейной (и та уйдёт на копирование байт). И никакого переполнения — циклы не будут выходить за допустимые рамки если они будут сразу знать длину строки.
Внезапно, сложение десяти строк через strcat даст экспоненциальную сложность, в то время как строки со счётчиком количества символов будут сложены с линейной (и та уйдёт на копирование байт).

Ну никто не мешает не использовать в цикле strcat, а посчитать и просуммировать длины, выделить один раз и сделать memcpy.


И никакого переполнения — циклы не будут выходить за допустимые рамки если они будут сразу знать длину строки.

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

А зачем вы конкатенируете строки через strcat(), я стесняюсь спросить, если вам хоть в какой-то мере интересна скорость? И откуда вы получаете строку, не узнавая там же её длину? Я только два места знаю: аргументы командной строки и переменные окружения. Оба случая не про скорость. Зато ASCIIZ строки безумно быстрые, если уметь читать доки. И, в отличие от «строк с длиной» позволяют избегать лишних выделений памяти или копирования.
Зато ASCIIZ строки безумно быстрые, если уметь читать доки. И, в отличие от «строк с длиной» позволяют избегать лишних выделений памяти или копирования.


1. За счет чего более быстрые? копирование в любом случае происходит более-менее одинаково, а в zero-terminated строках длину надо дополнительно пересчитывать. А в вариантах, когда 1 символ >= 1 байт (UTF-8 если) — это реально долго.
2. Откуда лишние выделения памяти? Сосчитали, сколько памяти нужно — выделили. В обоих случаях одна операция, только в zero-terminated строках считать размер нужно дольше.
А в вариантах, когда 1 символ >= 1 байт (UTF-8 если) — это реально долго.

С какого перепугу? Длина строки — это количество байт, а не количество codepoint'ов или графемных кластеров в ней.

Пардону прошу, для копирования действительно не важно, сколько там символов. Для других действий со строками — важно. А бывает и критично. Вот, например, в некоторых языках есть буквы, в UPPERCASE и lowercase занимающие разное количество байт.
Для других действий со строками — важно. А бывает и критично.

В каких операциях вам важно/критично количество codepoint'ов в строке? В большинстве случаев вы будете итерироваться по codepoint'ам, порождать и записывать codepoint'ы в массив.


Вот, например, в некоторых языках есть буквы, в UPPERCASE и lowercase занимающие разное количество байт.

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

Ну, сколько нужно памяти, чтобы записать строку в UPPER case? В общем случае — нужно считать. Если выделить sizeof(char)*strlen(src)+1 — рано или поздно оно выстрелит.

Считайте, что вы не можете этого сделать. Т. к. to_upper_case сложная локалезависимая операция, которая может производить частичную композицию/декомпозицию codepoint'ов.


А как основной подход к решению — выделять чуть больше и использовать функции, принимающие саму строку и максимальный размер (как snprintf). Ну и в случае, если не получилось — делать realloc.


Интересно, как сделано в icu-project, надо будет потом глянуть.

Например, в турецком i (маленькое с точкой, U+0069, 1 байт в UTF8) преобразуется в İ (большое с точкой, U+0130, 2 байта в UTF8).

Ок, я думал, что они используют специальную small dotted i, а не переиспользуют обычную small latin i.

1. За счёт того, что в ASCIIZ строках копирование не всегда нужно. Например, надо разбить строку по разделителю — мы можем просто менять разделитель на '\0' и дальше мы, во-первых, уже знаем длину (если нормально разбор пишем), во-вторых, так же можем использовать функции, которые принимают ASCIIZ, а не массив байт с длиной.
1.1. Чем utf-8 отличается? Длина считается так же (нам ж длина массива байт нужна, а не кол-во символов — во втором варианте всё вообще по-другому)

2. Либо мы выделяем место под {длина строки, строка} — выделение + копрование, либо под {длина строки, указатель на первый байт} — выделение + как реализовать c_str(), который обязан завершаться нулём? Зачастую это всё равно надо, но всё-таки не всегда в случае ASCIIZ. А размер считать не надо. Про подсчёт длины: строки не берутся божественным промыслом, длину мы почти всегда знаем, когда получаем строку откуда-нибудь (будь то возвращаемые значения из функций, данные протоколов или что угодно ещё). Исключений мало — либо по определению некритичные для производительности вещи (аргументы CL, environment), либо криво спроектированные библиотеки.
Можно использовать комбинированный подход, как в Delphi: есть и длина строки (и refcount еще для меньшего кол-ва операций копирования) и терминальный 0. Поэтому можно и всегда быстро длину узнать, и передать в C, где нужен ноль на конце. Можно ту же самую замену разделителей на 0 провести, с выделением указателей на каждую подстроку, формальная длина строки при этом останется той же.
Надо постоянно следить за соответствием длины строки данным внутри. Как быть, например, когда мы при разборе строки пишем в разные места \0? Нужно тогда делать свои функции на все случаи жизни, с поддержкой длины и \0. Или перейти на С++ ;)
Не вижу никакой проблемы. Длина строки остается прежней, т.е. равна длине буфера.
А уж если мы начинаем играть в упомянутые выше ASCIIZ игры, то и пользоваться будем, соответственно, указателями и функциями типа strlen.
Ну, не знаю…
1. Наверное, нужно для начала определить новый тип данных, раз в С строках нет места под длину. Не проблема
struct {
   unsigned int len;
   char * data;
}

— где-то так.
2. Как это инициализировать константной строкой?
char * x;
x = "the string";
— уже не катит (пора писать макрос для инициализации этих полей)
3. Как прочитать туда строку из файла? fread (и прочие функции, выводящие данные в буфер) с таким не работает — натягиваем другой макрос. Или потихоньку переписываем все функции для строк.

А так особых проблем нет ;)

fread() вообще строки не читает, он читает бинарные данные. Там и в аргументах void *. Так что измени́те добавление NUL байта на сохранение длины, вот и вся разница. Про другие функции вы правы, и часть придётся именно переписать (практически все, если вам нужны NUL байты внутри строки). Макрос для литерала этой новой строки довольно прост: к примеру, Neovim использует такой.


Кстати, под длину есть стандартный тип: size_t.


Ещё напомню, что строки с длиной хотят много кто. В большинстве современных языков с GC (типа Python), чьи внутренности я приблизительно знаю они именно такие: строка + длина. Вроде где‐то есть и C’шная библиотека под этот вариант, но я её не трогал.

А, и структура ещё может выглядеть как typedef struct { size_t len; char data[]; } String; (память выделяется так: xmalloc(offsetof(String, data) + len)). В зависимости от ситуации, так может быть оптимальнее или наоборот.

Вы решаете вопрос «как средствами языка Си достичь xyz», в то время, как я просто отметил, что комбинированный подход — удобен, потому что наметился спор (см. выше) на тему, какие строки лучше — asciiz или паскалевского типа.
Статья про строки в Си, так что почему бы и не поспорить ;)
Достоинство строк с нулем — они уже есть ;) то есть изобретать ничего не нужно. Строки с длиной могут иногда обрабатываться быстрее, хотя бы потому, что длину считать нужно не более 1 раза. Но в Си нужно стоить велосипед, или искать готовый, или переходить на С++ ;).
UFO just landed and posted this here
Вот именно поэтому ASCIIZ строки быстрые, а pascal-like — нет.
const char str[] = "1,2,3,4,5"; // внезапно строка
const char *p = str + 4; // внезапно тоже строка в ASCIIZ
char *endp = NULL;
long val = strtol(p, &endp, 10); // и endp тоже не менее внезапно строка
// check errors
...
if (endp - str >= sizeof(str) - 1) // strlen()? А шо ента и, главное, зачем?
  fprintf(stderr, "unexpected EOS\n");


Этот момент очень важен, когда нужно быстро разбирать какую-нибудь простыню околотекстовых данных (например, pdf). Да, возникает такая необходимость редко, да, это неудобно и требует крайней аккуратности (собственно, а чего вы хотели-то от условно-переносимого ассемблера?), но я не могу придумать ни одного другого варианта, который предоставлял бы такую же гибкость, скорость и относительное удобство в тех случаях, когда нам эта гибкость и скорость не нужна (я про семейство str*() функций).
То, что на С можно быстро и эффективно стрелять себе по ногам — это известно давно ;)
А если вспомнить про паскалевские строки и работу с ними в том самом Паскале — там начудить с указателями невозможно.
Вы, собственно, прочитали комментарий, на который отвечали? Судя по всему, нет.
В смешанном подходе вы сможете все сделать точно так же, за исключением инициализации константой (но это именно языкозависимая фишка, конкретно относящаяся к упомянутому мною Delphi, из которого я притащил пример смешанного подхода).

const char *p = str + 4; // внезапно тоже строка в ASCIIZ

Будет
var
  s: string;
  p: pchar;
s := '1,2,3,4,5';
p := pchar(str) + 4;

Здесь мы можем использовать p как ASCIIZ строку.

char *endp = NULL;
long val = strtol(p, &endp, 10); // и endp тоже не менее внезапно строка

То же самое, если мпортируем strtol откуда-нибудь
var
  val: integer;
  endp: pchar;
val := strtol(p, @endp, 10);


if (endp — str >= sizeof(str) — 1) // strlen()? А шо ента и, главное, зачем?

А здесь вместо sizeof я буду использовать обычный стандартный length:
if cardinal(endp) - cardinal(str) >= length(str) - 1 then

И этот length имеет сложность O(1).
А все потому, что встроенная в язык строка выглядит как:
refcount (4 bytes) | length(4 bytes) | string data itself | 0

и поле типа string — это указатель на «string itself», который напрямую cast'ится в ASCIIZ строку. И нам достпуна вся та же магия с указателями, мы можем так же понавставлять нулей и работать с созданными подстроками функциями сишной библиотеки, если очень хочется.
> Здесь мы можем использовать p как ASCIIZ строку.

Только у вас нет ASCIIZ строк, они плохие, медленные и вообще, их выкинули. Упс.

> То же самое, если мпортируем strtol откуда-нибудь

strtol тоже нет, он с гадостью работал. val := StrToInt(p) — упс, а чего это у нас компилятор ругается? Копируем в string, жуём кактус, громко радуемся, что у нас быстрые строки, а не ужасный медленный ASCIIZ. 8))

> И этот length имеет сложность O(1).

Как и арифметика. Удивительно, не правда ли?
Только у вас нет ASCIIZ строк, они плохие, медленные и вообще, их выкинули. Упс.

Неправда, и ровно об этом я и написал. Но кое у кого дислексия, судя по всему.

strtol тоже нет

Простите, у вас русский язык не родной? Я русским по белому написал: "если импортируем strtol откуда-нибудь"

Как и арифметика. Удивительно, не правда ли?

Ничего удивительного. Комбинированный подход, о котором я написал, берет лучшее из обоих миров — в нем можно работать и так и так.
Внезапно, сложение десяти строк через strcat даст экспоненциальную сложность
Внезапно, нет.
Подсказать, где посмотреть исходники strcat или понятие экспоненты?
(strlen(str) + 1) — такое себе решение. Перед нами встают 2 проблемы:
А если нам надо выделить память под формируемую с помощью, например, s(n)printf(..) строку?

А если нам это не надо? Зачем решать несуществующую "проблему"? Вот когда нам в каком-либо конкретном случае это действительно потребуется, тогда и сделаем по-другому.


Внешний вид.

Нормальный внешний вид. А если не нравится — сделайте макрос (особенно для программы "где регулярно требуется обрабатывать строки").


работа со строками в C — это очень сложная тема

Ничего сложного. Это вы всё слишком усложняете. Вместо того, чтобы сразу написать "под катом" (или даже перед ним) вот это:


Весь «прикол» в том, что функция strlen не учитывает символ конца строки

вы зачем-то затеяли целое "исследование" с дампами из valgrind, а в конце привели решение, избыточное для простого копирования.


И чем больше тонкостей языка вы знаете, тем лучше ваш код.

Одна из "тонкостей" заключается в том, что вот это:


char *str = malloc(sizeof(char) * (strlen(BUF) + 1));
strcpy(str, BUF);

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


для написания «безопасного» кода при динамическом выделении памяти рекомендуется все же использовать функцию calloc() вместо malloc() — calloc забивает выделяемую память нулями. Ну или после выделения памяти использовать функцию memset(). Иначе мусор, который изначально лежал на выделяемом участке памяти, может вызвать вопросы при дебаге, а иногда и при работе со строкой.

Зачем такие "сложности"? Вы же используете strcpy(), а она копирует всю строку, включая терминальный 0.

strdup прекрасно заменил бы заклинание с malloc иsizeof.
sizeof(char) при выделение памяти необходим, так как на некоторых системах размер char равен 1 байту, на других же — 2м.
Идите и учите Стандарт C99, раздел 6.5.3.4:
When applied to an operand that has type char, unsigned char, or signed char, (or a qualified version thereof) the result is 1.
В общем случае размер типа char на конкретной платформе регулируется значением константы CHAR_BITS, определённой в заголовочном файле limits.h. По умолчанию и на платформах x86 она равна 8
— википедия. Хотя там так же есть и про стандарт.
Из этого не следует, что в стандартном компиляторе C, sizeof(char) вернет не 1. Даже если машина работает с char, который в памяти занимает 32 бита, sizeof(char) будет равен 1. Это прям так в стандарте английским по белому написано.
Да, вы правы.
Только что проверил у себя.
Вопросов нет :)

Вообще-то по стандарту С sizeof(char) = 1. В стандарте С++ — ровно точно также.

Я бы ещё тогда проверил разницу между sizeof('\0') на С компиляторе, и на С++ компиляторе.
Вот тут хорошо описано.

… системах размер char равен 1 байту, на других же — 2м.

Относительно размера равного 1 Вам уже объяснили, не заостряю внимание, а вот относительно слова байт, почему-то никто не написал. Просто 1, ни о каких байтах, сколько я помню, в стандарте не говорится (но, конечно же, для подавляющего числа платформ будет 1 байт или 8 бит). Если я не прав, поправьте, пожалуйста!
Хотя вот здесь пишут, что в байтах (но опять же, не обязательно 8 бит)

http://en.cppreference.com/w/cpp/language/sizeof

Странно, видимо меня слегка приглючило.

И в C99 (ISO/IEC 9899:TC3) в 6.5.3.4 пишут:


The sizeof operator yields the size (in bytes) of its operand

Просто байтом называют размер, соответствующий char'у.

В живом проекте там будет скорее всего sizeof(wchar_t), размер которого может быть и 2 байта, и 4.

Использование NTWS (null-terminated wide strings) вообще боль и страдание, не говоря уже о том, что они часто неудобны (например, при использовании UTF-8). Часто предпочтительнее использовать NTMBS (null-terminated multibyte strings), которые представляются char *.

А никто и не просит хранить в них UTF-8. В wide-строках хранится, обычно, UTF-32, если компилятор соответствует стандарту.

Где-то, скажем, в C99 это сказано? Из того что я нашел следует, что он должен вмещать символ текущей локали, но не более. На windows он вполне себе 2 байта.

Прошу прощения, я по дефолту в плюсах мыслю, а тема ж про Си.
На самом деле, по стандарту, размер wchar_t должен быть таким, чтобы в него помещались все кодпоинты юникода. Сейчас их 0x10FFFF, в 2 байта их помещает только винда с помощью суррогатных пар, это означает только то, что винда не следует стандарту.

Ткните на пункт в стандарте C99, где говорится, что wchar_t должен вмещать все codepoint'ы unicode'а, я видел только про то, что он должен быть способен вместить любые символы в текущей локали, про unicode вообще не говорится.

«Винда» с зари своих юникодных времён использует UCS-2 и (wchar_t == 2), так что тут всё следует стандарту, вот только ни кто не обещал для wchar_t использовать UTF32
«Винда» с зари своих юникодных времён использует UCS-2
Возможно, на «заре» так оно и было. Но эта заря закончилась в 96 году, когда вышел стандарт Unicode 2.0, в котором понятие UCS-2 вообще объявили obsolete, т. к. были добавлены UTF-16 и суррогатные пары.

так что тут всё следует стандарту
Только если, стандарту Unicode 1.1 1993 года.

Согласно MSDN, поддержку UTF-16 добавили начиная с Windows 2000, и вплоть до настоящего времени, широкие символы у винды реализованы через UTF-16, а не UCS-2.
На дворе 2017 год, на носу Unicode 10.0, все, уважающие себя (и других), средства работы с кодировками поддерживают суррогатные пары, забудьте о понятии UCS-2.
Другое дело — то, как винды поддерживают этот UTF-16. А глюков с этим там — более, чем хватает. Но это проблема уже тех, кто объявил, что «мы поддерживаем», а на самом деле забили на это.

вот только ни кто не обещал для wchar_t использовать UTF32
Да, в стандарте указано, что «implementation defined». Что тупо ломает бинарную совместимость в зависимости от локали. При этом, ещё с версии C99, существует макрос, который гарантирует некое множество символов определённой минимальной версии Юникода, которое должно быть представлено единым широким символом.
MSVC не парится по поводу этого макроса вообще, в отличие от той же glibc, которая в текущей стабильной версии обещает реализацию юникода 8.0. А меньше, чем в UTF-32 целиком множество кодпоинтов 8-й версии юникода, не входит.
Я к тому, что лучше бы следовать, какому-никакому, более-менее допускающему вольности, пункту стандарта, чем костылить свои хотелки, как в голову взбредёт.
Вот именно поэтому я и говорю, что, не смотря на все заявления, MSWin поддерживает UCS-2, а не UTF-16. Так как реализация полноценного юникода хромает на обе ноги.
Только если, стандарту Unicode 1.1 1993 года.
Это же Микрософт, поддержка legacy у них на уровне :)
При этом, ещё с версии C99, существует макрос...MSVC не парится по поводу этого макроса вообще
А зачем им париться если у них полная поддержка только С90 и частично С94, о С99 и С11 они даже не задумываются? :)

Если хочется использовать юникод в своих проектах — придётся отказаться от нативного WinAPI. Но нужно так же помнить, что в NT системах (а сейчас они все такие) в основе лежит вызов именно «юникодовых» функций, те которые заканчиваются на W и принимают в качестве параметров wchar_t*. При вызове функций с char* и <имя_функции>A будет производится конвертация символов. Или шашечки, или ехать… в этом весь микрософт.
Во, чего откопал:
Оказывается, при выделении памяти под указатель на char, sizeof() является одним из способов дать понять, зачем нам нужна эта переменная.
То есть.
Тот же char * иногда используют, например, когда упаковывают пакеты перед отправкой на RAW-сокет, и тогда выделение памяти будет выглядеть так:
char *packet = malloc(sizeof(ether_header) + sizeof(struct iphdr) + sizeof(struct udphdr) + sizeof(payload));

Тому, кто будет сопровождать проект будет ясно, что это переменная под пакет. (заголовки и данные)
А если мы напишем sizeof(char) * n, это указание, что эта переменная будет использована как строка.

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

О каких кругах идет речь? Можно хоть ссылочку, чтобы почитать объяснение такому замусориванию кода?


Когда-то прочитал, что код надо писать так, как будто его будет читать неуравновешенный психопат, который знает где ты живешь.


Когда я вижу конструкцию sizeof(char)*n — то сразу вижу, что человек не понимает смысла этой конструкции, а потому его код надо проверять с двойным вниманием и усердием. А это расход моего времени… и вот я уже незаметно превращаюсь в того психопата...

В качестве оправдания я лично использую: «Самодокументированный код». packet — слишком общо, если пример вырван из контекста. А когда я ковыряюсь в своих старых программах, пример как правило именно вырван из общего контеста. Коллега, правивший мой код не задал ни одного вопроса. Это и его заслуга, и моя.
В таком случае лучше использовать uint8_t * и сразу будет понятно, что речь не о строках идет.

И, кстати, еще и с битами можно безопасно работать при таком объявлении.

поправьте меня, если я что-то путаю, но предварительное вычисление длины результата с помощью "snprintf(NULL, ..." — в итоге выполняет одну и ту же работу по форматированию два раза?

За исключением непосредственной записи в результирующую строку. Альтернатива — динамическое увеличение длины строки в процессе printf'a.
UFO just landed and posted this here
Проблема препода, что он не знает про

char* strdup (const char *src);

Функция появилась в BSD, включена в POSIX, но не является частью стандартов ANSI/ISO, хотя поддерживается почти всеми компиляторами.

IIRC, sizeof(char) == 1 по стандарту и писать его не имеет смысла.

Вангую тут изначально было sizeof(TCHAR), но в итоге решили не усложнять текст холиварами про Unicode

Уже упомянут wchar_t, так что они уже неизбежны ,)

Если кто-то не согласен, он может привести пункт(ы) стандарта из которых следует иное.

Кстати, чтобы найти ошибку в первом фрагменте кода, вовсе не обязательно запускать программу под присмотром valgrind. Подобные простые случаи сходу выявляет и статический анализатор кода. Например, PVS-Studio выдаёт 2 предупреждения:
  • V512: A call of the 'strcpy' function will lead to overflow of the buffer 'str'.
  • V575: The potential null pointer is passed into 'strcpy' function. Inspect the first argument.

Первое по делу. Второе про то, что указатель, выделенный через malloc() хорошо-бы проверить перед использованием.
Это совершенно разные инструменты, решающие разные задачи. Не хотите платить, есть CLANG Static Analyzer

Можно выделить три стадии развития кода и существования ошибки

1) Этап написания
2) Этап компиляции
3) Этап выполнения

VALGRIND, как динамический анализатор, работает на третьем, статический анализатор работает на первом.

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

На этапе выполнения у вас добавляется внешняя среда, которая может существенно влиять на поиск ошибки.

Чем больше ошибок удается отловить на первых двух этапах, тем лучше.
Как и Cppcheck:
(error) Buffer is accessed out of bounds.

, как и AddressSanitizer компилятора:

Скрытый текст
=================================================================
==21454==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x60200000effd at pc 0x7f7182fd729b bp 0x7ffe039c1940 sp 0x7ffe039c10e8
WRITE of size 14 at 0x60200000effd thread T0
#0 0x7f7182fd729a (/lib64/libasan.so.3+0x5f29a)
#1 0x4008d1 in main /tmp/c.c:9
#2 0x7f7182bd2400 in __libc_start_main (/lib64/libc.so.6+0x20400)
#3 0x4007d9 in _start (/tmp/a.out+0x4007d9)

0x60200000effd is located 0 bytes to the right of 13-byte region [0x60200000eff0,0x60200000effd)
allocated by thread T0 here:
#0 0x7f718303ee60 in malloc (/lib64/libasan.so.3+0xc6e60)
#1 0x4008b7 in main /tmp/c.c:8
#2 0x7f7182bd2400 in __libc_start_main (/lib64/libc.so.6+0x20400)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/lib64/libasan.so.3+0x5f29a)
Shadow bytes around the buggy address:
0x0c047fff9da0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9db0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9dc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9dd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9de0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c047fff9df0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa 00[05]
0x0c047fff9e00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9e10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9e20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9e30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c047fff9e40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Heap right redzone: fb
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack partial redzone: f4
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==21454==ABORTING



Сейчас, к счастью, много инструментов, позволяющих улучшить качество кода.
Да очень просто. Весь «прикол» в том, что функция strlen не учитывает символ конца строки — '\0'. Даже если его явно указать во входящей строке (#define HELLO_STRING «Hello, Habr!\n\0»), он будет проигнорирован.

«Hello, Habr!\n\0» будет на один байт длиннее «Hello, Habr!\n», и естественно последний байт проигнорируется точно так же (хотя один завершающий ноль уж будет точно). Строго говоря строковый литерал содержит в себе все указанные символы + завершающий нулевой. Причем вне зависимости указаны ли нулевые символы внутри и тем более от их позиций. Нулевые символы внутри строкового литерала хоть и необычны, но вполне легитимные и иногда полезные. Например, я так экономил память: «Hello\0Привет», сдвигая строку перед печатью в зависимости от выбранного языка.

sizeof('\0') совершенно точно делает не то, что вы просили. Согласно C99 sizeof('\0') == sizeof(int), если ваш компилятор выдаёт что‐либо иное, то в компиляторе ошибка. 6.4.4.4, параграф 10: «An integer character constant has type int.»

Ещё относительно snprintf(NULL, …): оно не всегда работает: http://demin.ws/blog/russian/2013/01/28/use-snprintf-on-different-platforms/. Статья довольно старая, но она говорит в том числе о Windows 7, которая ещё актуальна. Я не знаю, починили ли что‐то в более новых версиях или вам нужно делать #define snprintf _snprintf_s.

Хотел написать про это, но вы уже сделали :)


Поэтому только про макросы с переменным числом параметров.


В той форме, что они применены в статье:


#define macro(args...) foo(args)

это расширение препроцессора Gcc: https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html


Легальная форма:


#define macro(...) foo(__VA_ARGS__)

Макросы с переменным числом параметров доступны с C99.

Не ведитесь на поролон — предыдущая статьи автора, опубликованная буквально недавно, написана в том же духе. Это какая-то дикая смесь троллинга и эффекта Даннинга — Крюгера.

UFO just landed and posted this here
Вызывать snprintf для расчёта длины строки — это какое-то особенное извращение, функция совсем не дешёвая… Если вам надо писать скроку неизвестной длины, есть vsnprintf. Вот кусок кода (вырезал всё лишнее):
cor_log_put(const char *format, ...)
{
    /*  пропущена часть кода... */
    char buf[COR_LOG_STR_SIZE];
    char *p = buf;
    char *begin = p;
    char *end = p + COR_LOG_STR_SIZE;
    va_list args;
    while (1) {
        /*  пишем сколько влезет  */
        va_start(args, format);
        int rc = vsnprintf(p, end - p, format, args);
        va_end(args);
        if (rc < 0) {
            if (begin != buf) {
                free(begin);
            }
            return;
        }
        /*  проверяем, не мало ли места  */
        if (rc >= end - p) {
            size_t size = COR_LOG_STR_SIZE;
            while (size <= rc) {
                size = (size << 1) - (size >> 1);
            }
            char *nb = (char *) malloc(size);
            if (!nb) {
                if (begin != buf) {
                    free(begin);
                }
                return;
            }
            memcpy(nb, begin, p - begin);
            p = nb + (p - begin);
            end = nb + size;
            if (begin != buf) {
                free(begin);
            }
            begin = nb;
            continue;
        }
        p += rc;
        break;
    }
    /*  пропущена часть кода... */
}

Идея в том, что на стеке выделяется строка размером COR_LOG_STR_SIZE байт, которой обычно хватает для записи строк. А если надо будет записать больше, выделяем нужное количество памяти на куче.
Тот момент когда студенты намного умней всяких там преподавателей и профессоров!
У нас в универе был препод который постоянно всем твердил — «Да я вам говорю что вы тупой».
Всем студентам.
Тысяча и одна вариация функции xxxprintf отпугивает от C++ злых духов гораздо круче святой воды и чеснока.
#define strsize(args...) snprintf(NULL, 0, args) + sizeof('\0')

Нужно скобки писать, вот так:
#define strsize(args...) (snprintf(NULL, 0, args) + sizeof('\0'))

Иначе всякое там strsize("abc") * 2 неправильно будет работать. Решили поучить других о C, а сами оплошали

Так зачем этот strsize выдаёт длину строки на 3 больше?

Sign up to leave a comment.

Articles