Pull to refresh

Comments 78

Вах, какой C интересный. А на C++ implicit function declaration запрещены, насколько я помнимаю?
Запрещены. А ABI в этом месте всё тот же.
Поэтому, кстати, несколько странно видеть это в блоге «C++» — но это я уже так, ворчу скорее ;)
а другие компиляторы ведут себя так-же?
Microsoft ® 32-bit C/C++ Optimizing Compiler Version 16.00.30319.01 for 80x86
v.a = 1, v.b = 2
a = 1, b = 2

Microsoft ® C/C++ Optimizing Compiler Version 16.00.30319.01 for x64
«Программа выполнила недопустимую недопустимость» =)
Неправильно извлекаются параметры со стека.
У меня под рукой других нет, но мне тоже интересно, что у них выйдет на GNU/Linux
Считаю, заключение должно быть в виде:
В С принято передавать функциям (и в свою очередь функции должны возврашать) указатели, а не забивать стек всяким хламом.
Считаю что разработчики i386 ABI совершили большую ошибку.
А в принципе передавать в качестве параметров и возвращать структуры — хорошо.
Ага, а разработчики Microsoft допустили большую ошибку в реализации x64 ABI =)
Ага, особенно когда структуры размером по сотни байт туда-сюда через стэк гоняются. Вообще шикарно.
Если мне не изменяет мой склероз, когда возвращаемый тип не указан компилятор считает что функция возвращает 'int', что явно отличается от реализации функции 'struct S'. И в зависимости от ABI может быть любое поведение, вплоть до порчи стека и невозврата из этой функции (например исключительная ситуация обращения по несуществующему адресу).

А если ещё отличается и механизм вызова (__attribute((__stdcall)) в GCC или __stdcall, __fastcall и т.п. в MSC)? Правда name mangling минимизирует вероятность таких ошибок, но при желании можно получить «результат».

Мораль проста и очевидна — прототипы функций необходимы. =)
Такое поведение на i386 идёт вразрез с принципом наименьшего удивления: какое мне, казалось бы, дело до возвращаемого значения, которым я даже не пользуюсь?

А прототипы — да, необходимы. Настолько, что gcc по-умолчанию даже не предупреждает об их отсутствии.
Подобные эффекты можно получить (в зависимости от ABI, конечно), если передавать больше или меньше параметров, чем требует функция, независимо от возвращаемого значения (а в ранних реализациях C так и было, декларация функции описывала только возвращаемый тип). Думается, что ожидание какого-либо определённого поведения для вызова функции с неизвестным прототипом порочная практика =).
Си требует от программиста умения соображать.
Вы хотели сказать помнить много лишнего…
Каждой задаче — свой язык разработки.
Ежу понятно что быстро набросать гуевое приложение под венду, если скорость работы его не важна, проще на C#.NET или подобном языке.
Однако если производительность имеет значение, то стоит взять Си, и иметь ввиду архитектуру машины «много лишнего», как вы выразились.
Однако если производительность имеет значение, то стоит взять Си


Если задача действительно сложна, и производительность реально имеет значение, то стоит выкинуть C/C++. И взять язык, позволяющий эффективнее заниматься оптимизацией. А не слежением за памятью или арифметикой указателей.
язык, позволяющий эффективнее заниматься оптимизацией


имя, сестра, имя!

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

Любой ЯВУ.

Где занимаешься задачей в терминах задачи, а не «байтов»/«указателей»/«шаблонов».

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


Это попытка считать себя умнее современных компиляторов. При этом значительно менее полезная по соотношению effort/result, чем высокоуровневая оптимизация алгоритма.
В любом языке программирования решать задачу вы будете в терминах языка программирования.

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

И да, я считаю, что задача, адаптированная к машине, будет в итоге скомпилирована (замечу. оптимизатор в Си тоже присутствует) в более эффективный код, чем адаптированная к некой другой платформе, логика которой отличается от логики машины.
Я не вижу особой разницы между решением задачи в терминах языка А и решением ее же в терминах языка Б

За исключением того, что в терминах одного языка она может занимать избыточно много кода, основную часть которого будет составлять не описание алгоритма, а гоняние байтиков, игры с указателями и обезопашивание себя от игр с указателями, указание и приведение типов, и прочие интересные, увлекательные, глубокомысленные, тешащие самолюбие кодера, но для решения задачи совершенно не нужные вещи. За деревьями не будет виднhttp://feedproxy.google.com/~r/habrahabr/~3/U1LxAE96jiM/о леса.

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

Вот, наглядный пример: балансировка и вставка в red-black tree. Подумайте, почему на этой странице нет кода на C/C++.

И да, я считаю, что задача, адаптированная к машине, будет в итоге скомпилирована в более эффективный код

QuickSort на бейсике будет работать быстрее, чем сортировка пузырьком, написанная не то что на С с крутым оптимизатором — хоть на ассемблере. (Ну, опускаем «на достаточном большом объёме данных» и т.п. — мы тут достаточно взрослые люди, чтобы не придираться к мелочам). Потому что высокоуровневые оптимизации приносят больше эффекта, чем низкоуровневые. Низкоуровневые — это last resort, когда высокоуровнево уже больше никак не оптимизируешь.

Лет пять-десять назад были споры уровня «на ассемблере мой код будет быстрее, чем твой код на С/C++». Хватит, переболели — сейчас редко кто способен оптимизировать код руками так хорошо, как это делают современные компиляторы того же C/C++ и прочие фреймворки типа LLVM. Сейчас понемногу проходит мода и на машинно-ориентированные языки типа Fortran/C/C++ — потому что сформулированную высокоуровнево задачу компиляторы современных ЯВУ уже понемногу обучаются спускать до уровня адаптации к машине (после чего, может статься, компилироваться они будут с оптимизациями того же gcc). И слава богу.
Вы наверное меня не так поняли.

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

Я сказал лишь, что если концептуально алгоритм одинаков, то если его реализовать с учетом особенностей машины, он будет работать быстрее.
Сферический уже готовый алгоритм в вакууме — да. А вот если алгоритм ещё предстоит продумать и реализовать — то сделать это будет проще будет на ЯВУ.
«Алгоритмизация вообще» не зависит ни коим образом от целевого языка (я имею ввиду языки одной парадигмы — например процедурные). Когда я придумываю алгоритм решения задачи, я это делаю независимо от того языка, на котором буду его реализовывать.
Гипотеа Сепира-Уорфа в программировании проявляется как никуда нагляднее. А «иметь в виду языки одной парадигмы» вредно — слишком ограниченные программисты получаются.
как то вы все к словам цепляетесь.
Я не сказал, что следует иметь ввиду языки только одной парадигмы. Я сказал, что разница между языками одной парадигмы не на столько существенна, чтобы, желая разработать алгоритм решения задачи на C# например, я вдруг написал многократно лучший алгоритм, чем желая разработать его на Си.
But you will.

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

Ну вот, например, наглядное сравнение сишника и питониста:

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

Там, где питонист думает об использовании set или dict, сишник ни о чём не думает и тихо мечтает о C++ и STL. Там, где питонист, банально заменив квадратные скобки на круглые, превращает вычисление в ленивое, а список — в итератор, сишнику придётся размышлять, городить огород из коллбэков или нет. Там, где питонист думает о лямбда-функциях, частичном определении функций, замыканиях, сишник спит — в его алгоритмах таких кубиков не будет. Та же самая банальная сортировка, но компаратор для сортировки создаётся динамически, на основании каких-то вычислений… задумается ли вообще о такой возможности сишник? Или это там будет из области «высшей алгебры»?

Что-нибудь более практическое, с реальным примером? Мемоизация. Насколько сложно мемоизовать произвольную функцию в Си? Можно ли там вообще написать «мемоизатор общего вида» для функций, так что мемоизовать ещё одну функцию можно будет одной строчкой? В питоне что то, что другое — тривиальная задача; а значит, при продумывании алгоритма питонист сможет считать, что любая функция, в случае надобности, может быть мемоизована — у него и такой кубик есть — а что делать сишнику? А есть и более другие языки, в которых мемоизация может возникать автоматически…

Так что… как ни странно, вы вполне имеете все возможности для того, чтобы написать лучший алгоритм на ЯВУ более высокого уровня. Потому что, очевидно, у вас появляется возможность мыслить на более высоком уровне.
А так ли необходимы эти инструменты для реализации некоего алгоритма? оО
Возможно конечно мы разный круг задач решаем обычно, поэтому мне вот не приходит в голову ну ни одна практическая задача, которую решать на Си будет сложно из-за отсутствия каких-либо хитрых средств.
Вот как раз читаю статью fprog.ru/2009/issue3/dmitry-astapov-recursion-memoization-dynamic-programming/, где задачи динамического программирования решаются рекурсивно и с мемоизацией. Задача водосборов мне особенно понравилась — там алгоритм с мемоизацией явно вычислительно проще.
На днях закончу текущий проект и готов заключить своего рода пари — что я напишу на чистом C более быстрое решение любой задачи, чем ваше на питоне.
P.S. В разумных пределах, готов выделить на это сутки, не больше)
Не вижу особого смысла в сравнении компилируемого и интерпретируемого языка. А на компилируемых ЯВУ я пока не готов с вами соревноваться :)

Лучше сходите на projecteuler.net, посмотрите, хорошо ли берутся на чистом С задачки из разряда «решило 10000 человек или меньше». Тут может возникнуть проблема не уровня «более/менее быстрое решение», а уровня «решение есть/нет» :)
Как будет свободное время, гляну обязательно.

До сих пор мне не встречалось ни одной задачи, не решаемой на чистом C.

Возможно это проблема опыта.

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


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

Соответственно, вопрос не в том, решаема ли задача на чистом С (теоретически). Если она решаема на другом языке, то решаема и на С. Вопрос в том, насколько далеко ушёл язык от машины Тьюринга в плане комфорта :)
А интерпретируемые языки, безусловно, медленнее, поэтому мне было немного непонятно, почему вы привели в пример питон.

Исключительно как образец достаточно известного ЯВУ, на котором можно просто реализовывать все описанные мной высокоуровневые абстракции. Можно было привести в пример тот же Haskell — но там примеры были бы слегка сложнее в понимании.
P.S. Спасибо за дискуссию, в спорах рождается истина, мне было интересно расширить кругозор (мм, скорее наметить направления его расширения).
Ну и кстати, реализация мемоизации для произвольной функции без написания лишнего кода для каждой новой функции на Си мне пришла в голову после чуть более 15 секунд размышлений.
А расскажите, интересно.

В Python-е, к слову, мемоизация происходит примерно так:

def func1(a1, a2):
    ... some calculations
    return something1

def func2(b1, b2, b3, b4, b5, b6=v1, b7=v2):
    ... some calculations
    return something2

— так выглядят немемоизованные функции.

@memoize
def func1(a1, a2):
    ... some calculations
    return something1

@memoize
def func2(b1, b2, b3, b4, b5, b6=v1, b7=v2):
    ... some calculations
    return something2

— а так, соответственно, мемоизованные. Количество аргументов и типы определяет мемоизатор, после чего любое значение функции будет считаться самой функцией только один раз, дальше мемоизатор будет брать закэшированное значение. Это на случай, чтобы у нас с вами расхождения в ожиданиях или терминологии не возникло :)
Ну саму функцию, которая будет заниматься мемоизацией (кэшированием результатов) можно объявить как что-то вроде
void *func(void *f,...);

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

Здесь разумеется я очень грубо набросал и намеренно упустил много тонкостей.

1. Вы наверное скажете, что это неудобно, пользоваться исключительно функциями void *f(...).
Отвечу: пользоваться ими вовсе необязательно, достаточно собственно написать ту же одну строчку (ну да, чуть длиннее) для каждой конкретной функции. Строка эта — макрос. Например, нам нужна функция int func1(int x, float y).
Оригинальную функцию в этом случае объявляем как
void *func1_m(...), забирая аргументы, как в обычной функции с переменным числом аргументов, но зная, что это int и float. Возвращаем требуемый нам int, приводя к типу void *
Далее, пишем макрос:
#define func1(x,y) ((int)func(func1_m,x,y))
func здесь — наш мемоизатор
И пользуемся фукцией как совершенно обычной, при этом она окажется мемоизированной.

2. есть еще проблемы с типами и количеством аргументов и сооветственно с реализацией логики самого мемоизатора, но это тоже можно решить.

Безусловно, сам мемоизатор получится достаточно сложным, но я думаю, что реализация этого атрибута @memoize в питоне ничуть не проще :)
Ошибся в макросе,
#define func1(x,y) ((int)func((void *)func1_m,x,y))
Безусловно, сам мемоизатор получится достаточно сложным, но я думаю, что реализация этого атрибута @memoize в питоне ничуть не проще :)


Я пользуюсь вот такой:
from decorator import decorator                                                                                                            
                                                                                                                                   
def getattr_(obj, name, default_thunk):                                                                                            
    "Similar to .setdefault in dictionaries."                                                                                      
    try:                                                                                                                           
        return getattr(obj, name)                                                                                                  
    except AttributeError:                                                                                                         
        default = default_thunk()                                                                                                  
        setattr(obj, name, default)                                                                                                
        return default                                                                                                             
                                                                                                                                   
@decorator                                                                                                                         
def memoize(func, *args):                                                                                                          
    dic = getattr_(func, "memoize_dic", dict)                                                                                      
    if args in dic:                                                                                                                
        return dic[args]                                                                                                           
    else:                                                                                                                          
        result = func(*args)                                                                                                       
        dic[args] = result                                                                                                         
        return result 


Она, естественно, поддерживает произвольное количество аргументов произвольных типов (с единственным ограничением: каждый аргумент должен быть hashable; иначе я бы сделал другую версию мемоизатора, чуть более длинную и слегка медленнее работающую, которая явно сериализует все аргументы), и произвольный тип возвращаемого результата. И это уже не «грубо набросанный пример», а «рабочий код».
Но вы всерьез уверены, что ваш вариант будет работать быстрее, чем мой? оО
Что вы имели в виду под словом «быстрее»? Мемоизатор? Он сам не интересен — интересно использование его в алгоритмах. Реализация на питоне? Тоже неинтересна, это уже особенности интерпретации/компиляции питона; если взять из приведённой сверху статьи мемоизатор для Haskell-а, то он будет работать сравнимо по производительности с вашим, но при этом будет намного проще и надёжнее в использовании (никаких кастингов, ага).

Интересно другое. У питонистов и хаскелистов есть уже готовые мемоизаторы (которые, в случае надобности, незначительными изменениями модифицируются как угодно — ну, например, для ограничения размера кэша, или для ограничения времени жизни закэшированного), которые можно накладывать на что попало — хоть на наивно определённую функцию вычисления чисел Фибоначчи или Аккермана, хоть на функцию SQL-запроса. И они эти мемоизаторы активно используют как компоненты более сложных алгоритмов (и именно благодаря тому, что мемоизаторы у них есть и легко используются, они и способны создавать эти «более сложные алгоритмы»).
У вас в С… ну, это ещё отлаживать и отлаживать :)
я уверен, что подобное решение для Си уже написано.
и отлажено, ага.
и кастинги != баги.
у вас я смотрю какое то совсем уж жуткое предубеждение к кастингам. как будто это вселенское зло.
и кастинги != баги.

Полностью согласен. (=) ≠ (⇒)
То, что кастинги обязательно приводят к багам, тоже неверно.
Это зависит скорее от опыта программиста.
Молотком можно гвозди забивать, а можно и пальцы ломать. Смотря кто пользоваться будет.
Впрочем, в некоторых случаях может быть нужно именно альтернативное применение молотка, но и тут нужен профессионал :)
Все зависит от задачи и от опыта исполнителя.
К слову сказать, на C++ (который 0x) мемоизатор может выглядеть так:

template<typename R, typename ... Args>
std::function<R(Args ...)> memoize(R f(Args ...))
{
  auto l_f = [=](Args ... args) -> R
  {
    typedef std::tuple<Args ...> args_pack_t;
    typedef std::tuple<args_pack_t, R> memoize_val_t;
    
    static std::vector<memoize_val_t> values;
    
    args_pack_t args_pack(args ...);
    
    for (auto p = values.begin(); p != values.end(); ++ p)
    {
      if (std::get<0>(*p) == args_pack)
        return std::get<1>(*p);
    }
    
    R ret_val = f(args ...);
    values.push_back(memoize_val_t(args_pack, ret_val));
    return ret_val;
  };
  
  return l_f;
}


Использование предельно просто:
int f(int a, double b)
{
	return (a + 10) * b;
}

int main()
{
	auto f_ = memoize(f);
	
	std::cout << f_(10, 20) << std::endl;
	std::cout << f_(10, 20) << std::endl;
	std::cout << f_(10, 40) << std::endl;
	
	return 0;
}


Лукап, понятно, в данном варианте — не самый оптимальный, но (как и в случае с питоном) можно легко сделать варианты мемоизаторов, которые будут делать это настолько оптимально, насколько захочет пользователь такого класса.
Если задача действительно сложна, и производительность реально имеет значение, то стоит выкинуть C/C++. И взять язык, позволяющий эффективнее заниматься оптимизацией. А не слежением за памятью или арифметикой указателей.

Любой ЯВУ.
Где занимаешься задачей в терминах задачи, а не «байтов»/«указателей»/«шаблонов».

Наверное именно исходя из этих соображений большинство игровых движков написано на ЯВЕ. Они кстати ещё и кросс-платформенными получаются забесплатно.
Так то оно так (ведь имеются в виду MMORPG-движки?)… Только вот какая картинка получится, если сравнить нагрузки, которые таки движки могут выдерживать?
Это был сарказм есличё :) ММОРПГ движки могут писать на ЯВЕ из соображений «должно работать долго и не течь», а не из соображений производительности. Они очень хорошо параллелятся, поэтому дешевле купить много железа, чем разрабатывать такую штуку на каком-нибудь С/С++ ради небольшого выигрыша в производительности.

А вот движки оффлайн-игрушек почему-то на ЯВЕ не пишут. Как не пишут почему-то Математики, Маткады, Матлабы, СаундФоржи, Кубейсы, ПинаклСтудии и СониВегасы.
Ну WoW явно не на яве написан…
Математики, Маткады, Матлабы

Не скажу за игрушечный Mathcad, но остальные два из них являют собой языки программирования сами. Поэтому частично написаны на себе. Сей почему-то не хватило.
Сарказм, увы, не распознал. :)
В плане MMORPG-движков (браузерных) — согласен. Был свидетелем, как один мой знакомый разрабатывал оный. Сначала пробовал на C++, потом (отчаявшись решить ряд вопросов в силу недостатка опыта в некоторых областях и очевидной сложности реализации некоторых аспектов именно на C++), переписал всё на java. Переписать получилось быстро и он остался доволен результатом (в целом), бо производительностью был не особо озабочен.

Про offline-игрушки — тоже согласен.
Однако если производительность имеет значение, то стоит взять Си, и иметь ввиду архитектуру машины «много лишнего», как вы выразились.


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

А гибкость дают, например, возможности создания анонимных функций, замыканий, или, ещё любопытнее, генерирование машинного кода в runtime. Но С/C++, к сожалению, такое и не снилось — они существуют в мире, где ещё не изобрели архитектуру фон Неймана.
Снова не соглашусь с вами. Это не хак, это как раз таки гибкость.
А пример приведения указателя к int и обратно я показал выше, в ответе на другой ваш коммент :)
Есть такие задачи, где это удобно и хорошо.
Мне совсем непонятно, почему «производительность» и «безопасность» для языка — взаимоисключающие понятия. btw, Опыта С/С++ и Java более чем достаточно.
Мне совсем непонятно, почему «производительность» и «безопасность» для языка — взаимоисключающие понятия.

Теоретически это не просто взаимоисключающие понятия, а скорее наоборот: реализация типобезопасного языка имеет все перспективы быть более производительной. чем реализация типоопасного языка. Хотя бы потому, что из строгости типов открываются возможности более агрессивных оптимизаций.
Си требует от кодера заниматься всякой ненужной фигнёй.
программист и кодер — разные вещи
Благодаря C, программисту приходится постоянно решать задачи кодера. К чему я и клоню.
Позволю себе не согласиться с вами.
ANSI C таков что вы вообще можете вызвать f() с любым набором параметров

// a.c
#include
struct S {} a;
struct S f(void)
{
printf(«hello world\n»);
return a;
}

// b.c
#include

int main()
{
int a = 1, b = 2;
f(«what's up?»);
printf(«a = %d, b = %d\n», a, b);
return 0;
}

при этом возвращаемым значением будет int —

struct D {} d;
d = f(«what's up?»);

будет давать ошибку
error: incompatible types when assigning to type ‘struct D’ from type ‘int’

В данном случае переданные параметры совпадают с тем что ожидает вызываемая функция.
Речь о куда более тонком эффекте.
функция ожидает void а отнюдь не char *
В данном — в моём.

Функции ожидающие меньше параметров чем им фактически передают отлично работают в С, пока ожидаемые параметры подходят по типу. В вашем случае char * безболезненно болтается на стеке. И если бы функция возвращала целое или указатель, всё работало бы на ура.
ок, я имел в виду что в C передать внутрь можно все что угодно, также можно все что угодно вернуть — подводных и надводных камней при этом будет вагон и маленькая тележка. то что неверное определяемый тип возвращаемого значения может портить данные внутри вызываемой функции ( даже при правильных типов параметров ) всё таки логично — в момент формирования стека ( при вызове ) у компилятора неверное понимание его структуры.
Вроде бы это даже компилироваться не должно, поскольку в момент компиляции файла b.c функция f(struct S) не объявлена и не определена, т.к. отсутствует соответствующий инклуд.
Пардон. Написал, не вчитавшись.
Однако такое компилироваться не должно. Если компилер это делает, то это неправильно.
Почему же не должно? Вполне соответствует стандарту:
«If the expression that denotes the called function has a type that does not include a
prototype, the integer promotions are performed on each argument, and arguments that
have type float are promoted to double.» Раздел 6.5.2.2, п. 6. В конце этого же пункта говориться об UB в случае несоответствия реальному прототипу функции, которая будет вызвана.
Ну а gcc по этому поводу ещё и предупреждение выдаёт.
«Соответствует стандарту» и «правильно с общечеловеческой точки зрения» — разные вещи. В чем глубокий смысл такого поведения?
Ну, общечеловеческая точка зрения — понятие весьма относительное. Глубокий смысл такого поведения раскрыть не смогу, поскольку мне он не известен, но рискну предположить (согласившись с вышеотписавшимися), что такое поведение — унаследовано от ранних реализаций C и оставлено из соображений backward compatibility. Плюс к этому добавляет определённую степень свободы (нет необходимости таскать прототипы функций), и, как следствие, определённое количество проблем.
Стандарт С89 допускал неявное объявление функций, а С99 — нет. Добавляем параметр -std=c99 и наслаждаемся ошибкой:

b.c: In function 'main':
b.c:8: warning: implicit declaration of function 'f'
Sign up to leave a comment.

Articles