Pull to refresh

Comments 134

очень хорошая постпятничная статья
Спасибо за комплимент. =)
Буду благодарен за совет, в какой блог ее лучше перенести.
постпятничная это я с юмором, по другому относится к такому нельзя :) Ведь суть hello, world именно в том чтобы поскорее выплюнуть программу в реальный мир, если строчка появилась, это значит что все работает. Сломал psp, запустил там hello world и душа радуется, можно работать. Поставил питон, написал print «hello, world» — ок, значит питон работает как надо можно приступать. Сравнивать с реальными программами глупо
Ну а суть статьи в том, что даже hello world на самом деле сложнее чем может показаться. =)
Суть — показать что при желании можно заморочиться даже с hello world.
я бы еще учел неблокируемую запись.
Вам не кажется, что для начала изучения первый вариант намного проще и понятнее последнего?
Может в начале обучения не стоит всё усложнять?

Ведь сперва идёт рассказ о том, что это за оператор и для чего он нужен.
Так ведь это и не для начала обучения. Это скорее вроде того, как в институте говорят «а теперь забудьте все, чему вас учили в школе».
А на собеседавании понимаешь, что лучше забыть все что говорили в институте
на собеседовании понимаешь что за те деньги которые предлагают можно только пару раз покушать.
Здесь скорее вопрос не в простоте, а в том люди не отдают себе отчет, каким инструментом они пользуются и как легко можно выстрелить себе в ногу, если все время не думать о том, что все может выйти из строя.

Т.е. стандартный Hello World на C в схеме «what I wanted — what I expected — what I got» ломается на втором пункте и это ужасно.
Вы бы еще обработчик на SIGPIPE поставили.
Тут это не нужно — логи ведь мы не ведем, а следующая попытка записи все равно ошибку вернет.
Дело в том, что если мы его не поймаем нас «убьет» AFAIK
Спасибо за совет, но я и так предпочитаю обходиться без подобных веществ.
хм, ну и коноплю тоже курить не следует
Да простят меня хабралюди за отступление от темы, но меня ужасно раздражают люди оставляющие комментарии в стиле «Что курил автор?»

Для творческого и креативного мышления не требуется употреблять психотропные вещества. Если человек думает иначе, то мне его искренне жалко.
Ууууупс.
Тут должен быть демотиватор на тему троллей.
Тут должен быть монохромный Ленин!
А получился демотиватор на тему демотиваторов )
Если человек думает иначе, то мне его искренне жалко.

Довольно двусмысленно звучит однако…
Весьма мило.
Эту тему Александреску в шутку поднимал в своей статье Case of D.
Даже не знал, что иду по стопам классиков. =)
UFO just landed and posted this here
Если Эта статья хабражителям понравится, будут еще статьи на тему «чем учебники отличаются от реальной жизни».

Я нифига не понимаю в C++, но сама идея про отличие учебников от реальной жизни просто отличная — пишите ещё!
С учетом, что это не С++, а Си.
Отличная статья!
Такие надо ставить второй главой в учебники в стиле «порадовались? а теперь смотрите что на самом деле.»… Чтобы не расслаблялись.
Думаю, лучше было бы так: две книги, в которых рассматриваются одни и те же примеры. Первая как учебник по языку, а вторая — с точки зрения реальной жизни.
Тогда вторую книгу будут читать единицы, а так хоть «говнокодеров» может меньше станет — они на второй главе все отвалятся =)
Говнокодерство, к сожалению, неистребимо. Зато можно будет что-то понять о человеке по ответу на вопрос «Читали ли Вы эту книгу?»
Два человека:
1) Не делает проверок при написании важных системных библиотек;
2) Усложняет код элементарной программы, которая не является критически важной, из-за недоверия стандартным функциям и возможности возникновения «сферического коня в вакууме».

Внимание вопрос «Кто из них быдлокодер?»
1) Первый;
2) Второй;
3) Оба.
Я точно знаю, кто из них параноик!
Я бы читал только нечетные главы.
тогда перемешать все — 1. смотрите как все просто
2. а вот и не все так просто
3. смотрите какой аццкий код
4. а можно и проще
На самом деле ваш пример забавен (только во втором фрагменте незачем писать if x return 1 else return 0 — вполне достаточно return x), но он к реальной жизни имеет не больше отношения, чем код из книги.

Конечно, возникают ситуации, когда фраза «а теперь представим, что у вас на выходе многопоточный радиоуправляемый тостер с LED-дисплеем» имеет смысл. Но чаще всё действительно просто.

Пример: если надо реализовать суммирование чисел, я сказал бы, что int sum(int a, int b) { return a + b; } — правильный ответ.
Однако теперь тоже можно начать придираться: а как же быть с переполнением разрядной сетки, а что если ошибка процессора, а что, если многопоточность и прочее и прочее.

У любой задачи есть предусловия, из которых надо исходить. Но обзывать хороший код «книжной выдумкой», а произведение для тостера со свистулькой «реальной жизнью» я бы поостерегся.
Или даже return !!x если хочется точно такого результата.
Ну, так можно и к полной параноидальности скатиться…
Давайте еще проверим, есть ли права на запись в консоль?
Тем не менее, автору — спасибо! Неожиданно.
А с тем, что любая программа должна выдавать код возврата, согласен.
На самом деле по современному стандарту С++ (по-моему, и С тоже) у вас просто нет выбора — описание «void main()» устарело, и в новых адекватных учебниках не встречается.
Если считать, что true это не ноль (по стандарту), то можно вместо такого:
if (printf_res < strlen(msg))
{
return 1;
} else {
return 0;
}
Написать:
return (printf_res >= strlen(msg));

Сокращает число строк кода, хотя и несколько ухуджает читаемость
Нельзя. Системе не все равно, какой из кодов возврата она получит (например, если следующая программа использует этот код). Если нужен был 1, а она получила «не 0», то можно и обидеться.
И вывод может отправляться, например, в какое-нибудь странное устройство, которое просто не умеет записывать больше одного символа за раз.


И все нормальные реализации printf() имеют внутри такой же цикл как и у вас. Разве нет?
UFO just landed and posted this here
Ключевое слово — нормальные реализации. Программа, работа которой сильно зависит от того, чем ее компилировали — не наш метод. =)
К сожалению, нормальная работа любой программы очень сильно зависит от того, чем её компилировали и с какими библиотеками, и какие библиотеки доступны во время выполнения.

Если вы не доверяете нормальной работе printf(), то почему доверяете тому, что макрос STDOUT_FILENO определён корректно? Или почему доверяете тому, что в обёртке стандартной библиотеки для системного вызова write нет ошибок?

Вот я запущу вашу программу с библиотекой в LD_PRELOAD, которая подменяет write() на пустышку, которая всегда возвращает успех. Значит ли это, что необходимо написать собственную обёртку для вызова ядра?
Еще можно таймауты учесть, можно лимиты проверять, и т.д. Нет предела совершенству.
И вывод может отправляться, например, в какое-нибудь странное устройство, которое просто не умеет записывать больше одного символа за раз.


И все нормальные реализации printf() имеют внутри такой же цикл как и у вас. Разве нет?
#include <stdio.h>
int main()
{
return printf(«Hello world\n») < 0? 1: 0;
}

в документации написано, что ошибка — это отрицательное число. Из чего я делаю вывод, что положительное ошибкой не является.
Это вы скажете пользователю, который вместо «Hello world!» увидит в выводе «Hell» =)
Расскажите мне в каких случаях это может случиться?
Когда при перенаправлении вывода в файл произойдет переполнение диска.
эх… как раз скоро придется учиться писать на С в линухе… спасибо за статью!
Там где идет проверка на EINTR еще по хорошему надо воткнуть проверку кода EAGAIN (во всяких олдскульных юниксах может попадаться вариант EWOULDBLOCK).

Но в целом до того как извращаться с неблокируемым write() следовало бы хотя бы дать ссылку RTFM :)
Очень правильная статья. Приучаться писать правильный код нужно с младых ногтей, имхо. Переучиваться потом гораздо труднее.
К сожалению, в вузах у нас этому не учат. По крайней мере, нас не учили (далеко не самый последний ВУЗ в России).
В дополнение к топику советую всем интересующимся книжку «Linux Системное программирование», автор Роберт Лав. Вот там тоже читаешь — и простейшие вещи, о которых раньше не задумывался, пишутся в двое большее число строчек. Ну и системные вызовы разобраны, чтобы было ясно, что такое write(), а что такое puts().
>const char const * msg = «Hello World!\n»;
с -fPIE будет лишний relocation из-за указателя… так что лучше const char msg[] = «Hello World!\n»;
извиняюсь… проглядел static :) его же там нет.
>… и послал всем процессам SIGHUP, который обычно значит «пересмотри конфиги»?

который обычно означает ваш терминал сдох
Очень многие демоны используют SIGHUP именно как «твои конфиги поменялись, посмотри новые».
демоны по определению не имеют управляющего терминала, и поэтому этот сигнал свободен.
не надо путать причину и следствие.
Демоны — да, но для простых программ это обозначает «терминал сдох».
Кстати, само название HUP это сокращенное Hang UP — то есть «модем повесил трубку» и идет из времен динозавров, когда тонкие клиенты подключались к мейнфрейму. Когда терминал отваливался, программа на мейнфрейме получала этот сигнал и в норме должна была по нему завершиться. Какой ей смысл висеть на дохлом теримнале?
Уже потом, когда появились демоны, то есть программы не подключеннные к терминалу, им нужен был какой-то сигнал для перечитывания конфигов. Было решено использовать SIGHUP — т.к. для демонов он не использовался.
Всё это конечно весело, но по-моему главной задачей программиста как раз и является найти «золотую середину» между… эээм… корректностью программы и надежностью для каждого конкретного случая. Так что и первый вариант с такой точки зрения очень даже ничего ;)
Вот так вот, с казалось бы интересных рассуждений о том, что printf не сможет вывести на некое устройство символы, и рождается подобный код? Еще чуть-чуть и вам уже пора будет выносить часть кода в функции и это только вывод строки…
очень познавательная неординарная статья, побольше бы таких. я думаю она очень полезна большинству разработчиков
const char const * msg = «Hello World!\n»;


const char *const msg =…
Ой, и правда, бред написал. Говорила мне мама, не пиши ничего в 4 часа ночи, обязательно какая-нибудь «кАрова» получится. Видимо, к C это тоже относится. =)
Только одно — какие еще конфиги в программе с единственной константой?))
PS: Спасибо за отличную статью!
В абсолютном большинстве программ, которые я видел, никто даже не задумывается о проверке статуса стандартных потоков — разве что в программах GNU Coreutils, где всё серьёзно. Меня очень радует, что есть люди, понимающие, что проверка нужна, и стремящиеся рассказать это другим. Спасибо!

Результат printf проверять не надо, поскольку он может быть необъективным из-за буферизации. Зато известно, что любая функция stdio при наличии ошибки устанавливает флаг ошибки у потока, что можно проверить вызовом ferror(). К тому же, некоторые ошибки не проявляются, пока не закрыть поток. Вот пример на чистом C, без POSIX: http://codepad.org/WubHENcE. Правда, только с помощью библиотеки stdio конкретную ошибку определить не получится. Проверить работоспособность можно командой "./a.out > /dev/full".

С более низким API можно и не связываться, поскольку стандартная библиотека C учитывает EINTR и перезапускает системный вызов сама, так что printf() и другие функции не требуют обработки этого случая вручную.
Еще можно, например, проверять, что malloc вернул не NULL.
Только этого все равно никто не делает (по крайней мере, мне так кажется)
Потому что все используют xmalloc().
Или пишут на C++ (new не возвращает NULL).

Или не проверяют потому что всё равно выделенный кусок памяти тут же используется и если что — то получим segfault. Всё равно когда память исчерпана кроме как вызвать exit(1) среднестатистическая программа мало что сможет сделать.
Перегруженная версия

void* operator new(std::size_t, const std::nothrow_t&) throw();

, кстати, возвращает. Пример:

#include <new>

int *p = new (std::nothrow) int;
if (p == NULL)
    cerr << "memory allocation failure\n";
Спасибо, я знаю. Я говорил про самый обычный new.
прошу прощения. не заметил вашего поста
new может вернуть null, если передать дополнительный параметр std::nothrow
Насколько велики при этом сопутствующие потери производительности?
Вот я например, прогаю на чистом C под SoC. И мне каждый лишний вызов обходится неимоверно дорого.
Так что мне значительно проще написать malloc, ничего не проверять, но помнить, что сейчас нельзя поставить меньше 64 метров памяти.
Полностью зависит от того, насколько часто ваш код выделяет динамическую память.

Например, если обходится только статической и автоматической — то нет и потерь :)
Чего стоит скорость, если программа может работать неправильно? Сначала следует написать работающий код, а потом его оптимизировать.

Что же касается потерь производительности, то такая версия по сути является дополнительным if'ом, что есть мелочь даже для embedded-систем:

#include <stdlib.h>

static inline void *xmalloc (size_t size)
{
    void *p = malloc(size);

    if (p)
        return p;
    else
    {
        fprintf(stderr, "error: not enough memory\n");
        exit(EXIT_FAILURE);
    }
}


Если компилятор достаточно умён, то дополнительной памяти в стеке этот код занимать не будет.
Я занимаюсь DSP. Все (почти!) SoC для DSP имеют скалярную архитектуру. Это означает, что всякое ветвление замедляет работу программы в разы.
Никто же не сказал, что xmalloc() надо вызывать на каждом шаге.
И сколько раз в секунду Вы вызываете malloc()?
Вообще ВСЕГДА нужно проверять, что возвращаяет malloc, но его тут нет. А использовать exit как в случае про xmalloc, как мне кажется странно. А если нужно завершить соединения, сделать shutdown сокетам, записать в файл, что еще не записал и тд. В более-менее сложной программе нельзя использовать exit.
Верно, xmalloc(), как правило, больше используется в программах, просто обрабатывающих данные, например, cp или gcc.

Ещё как вариант вместо exit() можно писать abort(), это приведёт к сигналу SIGABRT, который можно поймать и обработать.
Это, кстати интересный вариант. Никогда почему то не додумывался использовать сигналы в С, только обработку некоторых типа SIGTERM, делал корректной. Выходит почти как исключения в C++.
Речь идет о том, что всегда (!) есть такие вещи, которые стоило бы проверить.
Но никто этого на практике не делает, поскольку это ухудшает читаемость кода (как в примере из топика) или замедляет работу (как в случае с xmalloc — почему именно для меня критично это замедление можно прочесть немногим выше).
Я лично всегда проверяю.
Потому что это трудноуловимые грабли: если вдруг malloc по какой то причине все же вернет NULL (а это необязательно нехватка памяти) — мы увидим Segmentation fault.
Если же результат вызова malloc() проверить и в случае NULL выдать вменяемое сообщение об ошибке — сэкономите кучу времени при отладке.
«Только этого все равно никто не делает (по крайней мере, мне так кажется)»

Дааа… А Вы — делаете?
Подходит под «Hello world» для сетевого программирования.
Когда в сокет пишешь, чтобы обрабатывать сигналы используют write.
Интересно, но кое-где вы все же перегибаете палку. Например, фраза:

А теперь, что если у нас не «Hello World!\n»? А если там строчка вроде «Use %s to print string»? Нельзя нам здесь пользоваться printf, если мы хотим, чтобы этот код можно было еще где-нибудь использовать. А мы ведь этого хотим, иначе какие же мы программисты.


как раз и есть такой перегиб. Мы же пишем «Hello world». Требования к такой программе довольно точно определены — она выводит «Hello world» и от нее не ждут ничего другого…

А в результате мы делаем вывод, что лучше написать свою реализацию printf =) «А также неизвестно на чем будет компилироваться наша программа и с какими библиотеками» — так может поэтому лучше бы переписать стандартную библиотеку, да и компилятор свой наваять? =)

ЗЫ: а вот про обработку ошибок в *nix было реально интересно. Спасибо.
Осталось разобраться с EAGAIN и EWOULDBLOCK :)
… которые появляются только при работе с неблокирующимися файловыми дескрипторами (O_NONBLOCK), которых здесь нет :)
Ну дык, кто его знает куда стандартный вывод отдаётся :)
отличная статься, ждем продолжения)
Следующим шагом разработчиков будет формальная верификация программного продукта с помощью систем автоматического доказательства теорем. Только после этого они смогут с уверенностью заявить, что их детище действительно явлется эталонной реализацией хелловорлда.
> Нельзя нам здесь пользоваться printf, если мы хотим, чтобы этот код можно было еще где-нибудь использовать.

Вполне можно, только писать надо правильно:
printf("%s", «Hello World!\n»);
> «const char const * msg = "Hello World!\n";»

Автор, очевидно, думает, что он объявил константный указатель на константу? В таком случае ему стоит взять учебник по Си за первый класс и таки обращать внимание на предупреждения компилятора.

Правильно так: «const char* const msg = "Hello World!\n";»
Или так: «char const* const msg = "Hello World!\n";»

Facepalm.
не вижу смысла во всем этом.
если Вы решаете задачи посложнее чем Hello World, и если будете так изгаляться, то проект никогда не закончите.

UFO just landed and posted this here
Где вероятность «всплытия» этого бага 0.001%
Для некоторых программирование — творчество. Для некоторых — всего лишь способ заработать денег.
Раскройте мысль, а то до конца не понятно, что вы имели ввиду.
UFO just landed and posted this here
Как вариант можно сделать.
char const msg[] = «Hello World!\n»;

char const * end = begin + sizeof(msg)-1;

Таким образом не нужно будет вызывать strlen, и длина строки будет посчитана на этапе компиляции.
В реальном мире не будут использоваться ни первая, ни вторая реализация, а будет использоваться библиотека, в которой все секьюрно, надежно, оптимизировано и предусмотрено заранее, а программисту останется лишь проверить статус ошибки.
Объявлять переменные внутри циклов — это не классический Си.
Пользуйтесь стандартом ANSI C (c89) и будет вам счастье.
c99 позволяет.
Пользуйтесь современными стандартами!
C99 в полной мере не поддерживается GNU :)
(Кусок man gcc)
> ISO C99. Note that this standard is not yet fully supported; see
> <gcc.gnu.org/gcc-4.2/c99status.html> for more information.
Не пользуйтесь коряво-поддерживаемыми стандартами. Кошерный Си — это C89.
А остальное — non-KISS way :)
Разве в ANSI C нельзя объявлять переменные в начале блока? (параграф 3.6.2, «Compound statement, or block»)
В ANSI C (C89) переменные объявляются как и в K&R C, в начале блока _функции_, либо в глобальном пространстве.
Всякие переменные_в_цикле, переменные_в_ифе и прочие — это 99.
Вы ошибаетесь.

flash-gordon.me.uk/ansi.c.txt

— это один из драфтов С89. Пункт 3.1.2.4, третий параграф.

В С99 стало возможным объявлять переменные после операторов внутри блока, в С89 и раньше допускают объявления/инициализации только в начале блока.
В C89 можно объявлять переменные внутри и циклов, и ифов, и вообще везде внутри фигурных скобок:

$ gcc -v 2>&1 |grep 'gcc version'
gcc version 4.3.2 (Debian 4.3.2-1.1)
$ echo "int main() {int a; {int b; } if (0) {int c;} while(0) {int d;} return 0;}" | gcc --std=c89 -x c - -o scope && ./scope && echo Everything is ok || echo Something is wrong
Everything is ok


Printf, вообще говоря, не может написать меньше, чем ему дали на вход. В случае ошибки он вернет отрицательное число, но с такой ошибкой уже ничего не сделаешь. Поэтому правильный Hello World с элементами паранойи выглядит так:

#include <stdio.h>

int main() {
return printf("Hello world!") < 0;
}

И, кстати, вот тут полная ерунда написана:
const char const* msg = "something";

Второй const здесь лишний. Это все равно что написать «const const char*». Если Вы хотели сделать постоянным указатель, следовало написать так:
const char* const msg = "something";

Да-да, проверьте — напишите там ++msg и компилятор все соберет.
А еще лучше — объявить строку как массив:
const char msg[] = "something";

Статью я бы перенес в блог «Я безмуный» или «Ненормальное программирование».
А если заниматься настоящей паранойей, то нужно доводить дело до конца. Например, не мешало бы еще обрабатывать EAGAIN, а то вдруг у нас стандартный вывод оказался в неблокирующем режиме. И что же делать, если write будет все время возвращать ноль?
Все сигналы обрабатывать не забываем )
Ещё да, fcntl на fd сделать надо )
Проверить режим ввода-вывода.
Вдруг в этом юниксе fd==1 — стандартный ввод? :-D
Теперь привязать обработку сигналов и вынести весь этот костыль во внешнюю библиотеку. Получится опять чистенько и красиво.
То есть написать свой stdio? :-)
А смысл?
Не угадал. Функционал даже близко не сравним с stdio.
Для пущей портируемости надо возвращать не 1 и 0, а EXIT_FAILURE и EXIT_SUCCESS из stdlib.h
«быть на C в Linux»
а потом кто-нибудь посмотрит и скажет: ну его нафиг этот линукс.
Sign up to leave a comment.

Articles

Change theme settings