C - Правило одного определения для функций

Я новичок в 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 часть программы)?


Мои извинения, если этот вопрос является обманом, я не смог найти существующие ответы.

Хитрость в том, что <stdio.h> также не определяет функцию. Он просто предоставляет сигнатуру функции для ссылки, а фактическая функция определяется в разделяемой библиотеке libc.

Locke 19.11.2022 06:19

Вы, вероятно, обнаружите, что ваш компилятор знает о printf() и сопоставляет это с fputs("Hello, World!", stdout) (а не с puts(), поскольку в конце строки формата нет новой строки). Следовательно, printf() не вызывается. Вы не можете надежно перехватить или заменить функции из стандартной библиотеки C — компиляторы знают об этих функциях.

Jonathan Leffler 19.11.2022 06:20

@JonathanLeffler Вы можете использовать #undef printf, чтобы переопределить это.

Barmar 19.11.2022 06:20

@Locke да, но как тогда моему printf разрешено скрывать тот, что в libc? Это потому, что реализация библиотеки предварительно скомпилирована, поэтому компилятору все равно, поскольку он компилирует мой исходный файл только тогда, когда я делаю gcc mysourcefile.c?

user51462 19.11.2022 06:32

@ Бармар - правда? Как? Компилятор отображает printf() в puts() или fputs(), а не препроцессор. Тем не менее, я полагаю, что компилятор может обрабатывать этот случай, но не очевидно, что отмена определения printf() вызовет функцию.

Jonathan Leffler 19.11.2022 07:03

Я только что поэкспериментировал с GCC 11.2.0 и обнаружил, что (а) он не использует fputs() и (б) отмена определения printf() не мешает компилятору вызывать puts().

Jonathan Leffler 19.11.2022 07:08

@ user51462 Ваш printf не может затенять тот, что в libc. Попытка затенения printf приводит к неопределенному поведению в соответствии с разделом 7.1.3/2. Это означает, что компилятору не нужно выдавать предупреждение или ошибку, и он может выбрать, использовать вашу функцию или нет, как ему заблагорассудится.

user3386109 19.11.2022 07:16

@ user3386109, ах, понятно. Таким образом, в основном, под «всей программой» в приведенной выше цитате они подразумевают только то, что в исходных файлах фактически компилируется? А поскольку в моем примере «вся программа» — это всего лишь один исходный файл, содержащий только одно определение printf, компилятор не ругается — это правильно?

user51462 19.11.2022 07:57

Похоже, вы не были должным образом ознакомлены с неопределённым поведением.

user3386109 19.11.2022 08:16

@ user3386109, спасибо за ссылку. Я вижу, что неопределенное поведение на самом деле определено в стандарте (раздел 3.4.3) и отличается от неопределенного поведения (3.4.4), о чем я не знал. Но я все еще не понимаю, что именно происходит в моем примере. Таким образом, ODR будет применяться, но поскольку printf является зарезервированным идентификатором, его переопределение приводит к неопределенному поведению в соответствии с 7.1.3 (2)? Извините, если я далеко, это все еще непрозрачно для меня :(

user51462 19.11.2022 09:27

Я думаю, ты понял. Если в коде есть определение для printf, то он нарушает 7.1.3(2) и имеет неопределенное поведение. Компилятору не требуется диагностировать проблему, и исполняемый файл, который выводит компилятор, не должен вести себя так, как мог бы ожидать программист. С другой стороны, если в коде есть определение myprintf, то применяется ODR. Если существует более одного внешнего определения для myprintf, это нарушение ограничения согласно 6.9(3), и компилятор должен выдать сообщение об ошибке.

user3386109 19.11.2022 10:30

Обратите внимание, что «правило одного определения» — это правило C++. C имеет аналогичные правила, но не то, что конкретно называется «правилом одного определения».

Eric Postpischil 19.11.2022 13:39

Спасибо @ user3386109. Кажется, что переопределение myprintf является нарушением ограничения, только если оно внутренне связано в соответствии с 6.9(3), но неопределенным поведением, если оно связано извне в соответствии с 6.9(5).

user51462 21.11.2022 00:23
Как настроить Tailwind CSS с React.js и Next.js?
Как настроить Tailwind CSS с React.js и Next.js?
Tailwind CSS - единственный фреймворк, который, как я убедился, масштабируется в больших командах. Он легко настраивается, адаптируется к любому...
LeetCode запись решения 2536. Увеличение подматриц на единицу
LeetCode запись решения 2536. Увеличение подматриц на единицу
Увеличение подматриц на единицу - LeetCode
Переключение светлых/темных тем
Переключение светлых/темных тем
В Microsoft Training - Guided Project - Build a simple website with web pages, CSS files and JavaScript files, мы объясняем, как CSS можно...
Отношения &quot;многие ко многим&quot; в Laravel с методами присоединения и отсоединения
Отношения &quot;многие ко многим&quot; в Laravel с методами присоединения и отсоединения
Отношения "многие ко многим" в Laravel могут быть немного сложными, но с помощью Eloquent ORM и его моделей мы можем сделать это с легкостью. В этой...
В PHP
В PHP
В большой кодовой базе с множеством различных компонентов классы, функции и константы могут иметь одинаковые имена. Это может привести к путанице и...
Карта дорог Беладжар PHP Laravel
Карта дорог Беладжар PHP Laravel
Laravel - это PHP-фреймворк, разработанный для облегчения разработки веб-приложений. Laravel предоставляет различные функции, упрощающие разработку...
1
13
81
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Обратите внимание на declaration и definition. Термин совсем другой.

  • stdio.h предоставляет только declaration. И поэтому, когда вы объявляете/определяете в своем файле, пока прототип похож, с этим все в порядке.
  • Вы можете определить в исходном файле. И если она доступна, конечная программа будет ссылаться на вашу, а не на ту, что в библиотеке.

Неправильно, что программа будет (всегда) ссылаться на printf, который пользователь определяет в своем собственном исходном коде, вместо того, чтобы использовать стандартное поведение для стандартных библиотечных функций. Компиляторы имеют встроенные знания о функциях стандартной библиотеки и могут заменять их различными оптимизациями, как показано здесь и объяснено в моем ответе.

Eric Postpischil 19.11.2022 13:39

Ваш пример очень интересен.

ThongDT 21.11.2022 07:43
Ответ принят как подходящий

Стандарт 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)...

user51462 19.11.2022 22:55

... Я не был уверен, почему это так, но, узнав о 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 19.11.2022 22:55

@ user51462: Да. И я полагаю, что причина, по которой несколько определений с внешней связью не находятся в разделе ограничений (поэтому не требуется никакого диагностического сообщения) и остаются неопределенными, заключается в том, что было обычной практикой разрешать идентификаторы путем рисования модулей из библиотек и не жаловаться по умолчанию, когда есть являются множественными определениями. В то же время несколько определений в прямых объектных модулях, а не в библиотеках, вызывают ошибки компоновки. И комитет C, вероятно, не хотел вмешиваться в определение того, как должны выполняться ссылки и когда ошибки будут и не будут возникать.

Eric Postpischil 20.11.2022 11:46

Большое спасибо @EricPostpischil, я застрял на этом некоторое время, поэтому очень ценю вашу помощь и подробный ответ.

user51462 21.11.2022 00:20

Другие вопросы по теме