Я новичок в C и читал, что каждая функция может быть определена только один раз, но я не могу сопоставить это с тем, что вижу в консоли. Например, я могу перезаписать определение printf без ошибки или предупреждения:
#include <stdio.h>
extern int printf(const char *__restrict__format, ...) {
putchar('a');
}
int main() {
printf("Hello, world!");
return 0;
}
Итак, я попытался найти правило с одним определением в стандарте и нашел Раздел 6.9 (5) на странице 155, в котором говорится (выделение добавлено):
Внешнее определение — это внешнее объявление, которое также является определением функции (кроме встроенного определения) или объекта. Если идентификатор, связанный с внешней связью, используется в выражении [...], где-то во всей программе должно быть ровно одно внешнее определение для этого идентификатора; в противном случае их должно быть не более одного.
Мое понимание связи очень шаткое, поэтому я не уверен, является ли это соответствующим пунктом или что именно подразумевается под «всей программой». Но если я под «вся программой» подразумеваю все, что есть в <stdio.h> + мой исходный файл, то не должно ли мне быть запрещено переопределять printf в моем исходном файле, поскольку он уже был определен ранее во «всей программе» (т.е. в stdio часть программы)?
Мои извинения, если этот вопрос является обманом, я не смог найти существующие ответы.
Вы, вероятно, обнаружите, что ваш компилятор знает о printf() и сопоставляет это с fputs("Hello, World!", stdout) (а не с puts(), поскольку в конце строки формата нет новой строки). Следовательно, printf() не вызывается. Вы не можете надежно перехватить или заменить функции из стандартной библиотеки C — компиляторы знают об этих функциях.
@JonathanLeffler Вы можете использовать #undef printf, чтобы переопределить это.
@Locke да, но как тогда моему printf разрешено скрывать тот, что в libc? Это потому, что реализация библиотеки предварительно скомпилирована, поэтому компилятору все равно, поскольку он компилирует мой исходный файл только тогда, когда я делаю gcc mysourcefile.c?
@ Бармар - правда? Как? Компилятор отображает printf() в puts() или fputs(), а не препроцессор. Тем не менее, я полагаю, что компилятор может обрабатывать этот случай, но не очевидно, что отмена определения printf() вызовет функцию.
Я только что поэкспериментировал с GCC 11.2.0 и обнаружил, что (а) он не использует fputs() и (б) отмена определения printf() не мешает компилятору вызывать puts().
@ user51462 Ваш printf не может затенять тот, что в libc. Попытка затенения printf приводит к неопределенному поведению в соответствии с разделом 7.1.3/2. Это означает, что компилятору не нужно выдавать предупреждение или ошибку, и он может выбрать, использовать вашу функцию или нет, как ему заблагорассудится.
@ user3386109, ах, понятно. Таким образом, в основном, под «всей программой» в приведенной выше цитате они подразумевают только то, что в исходных файлах фактически компилируется? А поскольку в моем примере «вся программа» — это всего лишь один исходный файл, содержащий только одно определение printf, компилятор не ругается — это правильно?
Похоже, вы не были должным образом ознакомлены с неопределённым поведением.
@ user3386109, спасибо за ссылку. Я вижу, что неопределенное поведение на самом деле определено в стандарте (раздел 3.4.3) и отличается от неопределенного поведения (3.4.4), о чем я не знал. Но я все еще не понимаю, что именно происходит в моем примере. Таким образом, ODR будет применяться, но поскольку printf является зарезервированным идентификатором, его переопределение приводит к неопределенному поведению в соответствии с 7.1.3 (2)? Извините, если я далеко, это все еще непрозрачно для меня :(
Я думаю, ты понял. Если в коде есть определение для printf, то он нарушает 7.1.3(2) и имеет неопределенное поведение. Компилятору не требуется диагностировать проблему, и исполняемый файл, который выводит компилятор, не должен вести себя так, как мог бы ожидать программист. С другой стороны, если в коде есть определение myprintf, то применяется ODR. Если существует более одного внешнего определения для myprintf, это нарушение ограничения согласно 6.9(3), и компилятор должен выдать сообщение об ошибке.
Обратите внимание, что «правило одного определения» — это правило C++. C имеет аналогичные правила, но не то, что конкретно называется «правилом одного определения».
Спасибо @ user3386109. Кажется, что переопределение myprintf является нарушением ограничения, только если оно внутренне связано в соответствии с 6.9(3), но неопределенным поведением, если оно связано извне в соответствии с 6.9(5).
Обратите внимание на declaration и definition. Термин совсем другой.
declaration
. И поэтому, когда вы объявляете/определяете в своем файле, пока прототип похож, с этим все в порядке.Неправильно, что программа будет (всегда) ссылаться на printf, который пользователь определяет в своем собственном исходном коде, вместо того, чтобы использовать стандартное поведение для стандартных библиотечных функций. Компиляторы имеют встроенные знания о функциях стандартной библиотеки и могут заменять их различными оптимизациями, как показано здесь и объяснено в моем ответе.
Ваш пример очень интересен.
Стандарт C не определяет, что произойдет, если существует более одного определения функции.
… разве мне нельзя запрещать…
Стандарт C не имеет юрисдикции над тем, что вы делаете. Он определяет, как интерпретируются программы C, а не то, как люди могут себя вести. Хотя некоторые из его правил написаны с использованием «должен», это не команда для программиста о том, что они могут или не могут делать. Это риторический прием для определения семантики программ на C. C 2018 4 2 говорит нам, что это на самом деле означает:
Если нарушается требование «должен» или «не должен», которое появляется за пределами ограничения или ограничения времени выполнения, поведение не определено…
Таким образом, когда вы предоставляете определение printf, а стандартная библиотека C предоставляет определение printf, стандарт C не указывает, что происходит. В обычной практике может произойти несколько вещей:
printf
. printf
в библиотеке не используется.printf
и использует их, несмотря на ваше определение printf
.printf
находится в отдельном исходном модуле, и этот модуль скомпилирован и вставлен в библиотеку, то, какой printf
использует программа, зависит от порядка, в котором библиотеки указываются компоновщику.В то время как стандарт C не определяет, что произойдет, если есть несколько определений функции (или внешнего символа в целом), компоновщики обычно это делают. Обычно, когда компоновщик обрабатывает библиотечный файл, он ведет себя следующим образом:
Таким образом, для обычных функций поведение нескольких определений, которые появляются в библиотечных файлах, определяется компоновщиком, даже если это не определено стандартом C. (Однако могут возникнуть сложности. Предположим, что программа использует cos и sin, а компоновщик уже включил модуль, который определяет cos, когда он находит библиотечный модуль, который определяет и sin, и cos. Поскольку компоновщик имеет неразрешенную ссылку на sin, он включает этот библиотечный модуль, который вводит второе определение cos, вызывая ошибку множественного определения.)
Хотя поведение компоновщика может быть хорошо определено, это по-прежнему оставляет проблему, заключающуюся в том, что компиляторы имеют встроенные знания о стандартных библиотечных функциях. Рассмотрим этот пример. Здесь я добавил второй printf, поэтому в программе есть:
printf("Hello, world!");
printf("Hello, world!\n");
Вывод программы: «aHello, world.\n». Это показывает, что программа использовала ваше определение для первого вызова printf, но использовала стандартное поведение для второго вызова printf. Программа ведет себя так, как будто в одной программе есть два разных определения printf.
Глядя на язык ассемблера, видно, что происходит. Для второго вызова компилятор решил, что, поскольку printf("Hello, world!\n"); печатает строку без спецификаций преобразования и заканчивается символом новой строки, вместо этого он может использовать более эффективную процедуру puts. Так что в ассемблере есть call puts вместо второго printf. Компилятор не может сделать это для первого printf, потому что он не заканчивается символом новой строки, который puts добавляется автоматически.
Спасибо @EricPostpischil, я не знал о C11 4(2) . Просто чтобы уточнить, я заметил, что переопределение идентификаторов с внутренней связью появляется в разделе «Ограничения» в C11 6.9(3), тогда как переопределение идентификаторов с внешней ссылкой появляется в разделе «Семантика» в C11 6.9(5)...
... Я не был уверен, почему это так, но, узнав о C11 4 (2), кажется, что нарушение 6.9 (3) должно привести к ошибке (поскольку 6.9 (3) появляется в разделе ограничений), тогда как нарушение 6.9(5) — это просто неопределенное поведение (поскольку 6.9(5) появляется за пределами раздела ограничений). Таким образом, ошибка, которую я получаю, когда пытаюсь переопределить extern int foo() {}, на самом деле не требуется стандартом. Принимая во внимание, что ошибка, которую я получаю, когда пытаюсь переопределить static int foo() {}, требуется стандартом, поскольку она нарушает ограничение 6.9 (3). Это правильно?
@ user51462: Да. И я полагаю, что причина, по которой несколько определений с внешней связью не находятся в разделе ограничений (поэтому не требуется никакого диагностического сообщения) и остаются неопределенными, заключается в том, что было обычной практикой разрешать идентификаторы путем рисования модулей из библиотек и не жаловаться по умолчанию, когда есть являются множественными определениями. В то же время несколько определений в прямых объектных модулях, а не в библиотеках, вызывают ошибки компоновки. И комитет C, вероятно, не хотел вмешиваться в определение того, как должны выполняться ссылки и когда ошибки будут и не будут возникать.
Большое спасибо @EricPostpischil, я застрял на этом некоторое время, поэтому очень ценю вашу помощь и подробный ответ.
Хитрость в том, что <stdio.h> также не определяет функцию. Он просто предоставляет сигнатуру функции для ссылки, а фактическая функция определяется в разделяемой библиотеке libc.