Предварительно стандартный синтаксис заголовка функции C после явного прямого объявления функции - PullRequest
0 голосов
/ 12 апреля 2019

Я хотел бы получить информацию о поведении предстандартного синтаксиса объявления функций в стиле K & R при использовании в сочетании с явными прототипами функций, представленными ANSI. В частности, синтаксис, который выглядит следующим образом:

int foo(a)
    int a;
{
    /* ... */
}

в противоположность этому:

int foo(int a) {
    /* ... */
}

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

Многое было сделано о том, как прежний синтаксис не создает прототип функции. Мои исследования показывают, что если бы функция была определена, как указано выше, последующий вызов foo(8, 6, 7, 5, 3, 0, 9) привел бы к неопределенному поведению; тогда как с последним синтаксисом foo(8, 6, 7, 5, 3, 0, 9) фактически будет недействительным. Это имеет смысл, но я в первую очередь явно объявляю все мои функции. Если бы компилятору приходилось полагаться на прототип, сгенерированный из определения, я бы уже посчитал это недостатком в моем коде; поэтому я обязательно использую предупреждения компилятора, которые сообщают мне, если я когда-либо не смогу объявить функцию вперед.

Предполагая, что имеются надлежащие предварительные объявления (в данном случае int foo(int);), является ли синтаксис объявления функции K & R все еще небезопасным? Если так, то как? Отменяет ли использование нового синтаксиса прототип, который уже существует? По крайней мере, один человек , по-видимому, утверждал, что декларативные функции перед определением их в стиле K & R на самом деле недопустимы , но я сделал это, и он прекрасно компилируется и работает.

Рассмотрим следующий код:

/*  1 */ #include <stdio.h>
/*  2 */ void f(int); /*** <- PROTOTYPE IS RIGHT HERE ****/
/*  3 */ void f(a)
/*  4 */     int a;
/*  5 */ {
/*  6 */     printf("YOUR LUCKY NUMBER IS %d\n", a);
/*  7 */ }
/*  8 */ 
/*  9 */ int main(argc, argv)
/* 10 */ int argc;
/* 11 */ char **argv;
/* 12 */ {
/* 13 */    f(1);
/* 14 */    return 0;
/* 15 */ }

При дословном коде gcc -Wall и clang -Weverything не выдают никаких предупреждений и создают программы, которые при запуске выводят YOUR LUCKY NUMBER IS 1 с последующим переводом строки.

Если f(1) в main() заменить на f(1, 2), gcc выдает ошибку «слишком много аргументов» в этой строке, причем примечание «объявлено здесь», в частности, указывает на строку 3, а не строка 2. В clang это предупреждение, а не ошибка, и примечание включения, указывающее, что строка объявления включена, не включено.

Если f(1) в main() заменить на f("hello world"), gcc выдает предупреждение о целочисленном преобразовании в этой строке с примечанием, указывающим строку 3 и читающим «ожидаемое» int, но аргумент имеет тип «char» * '». clang выдает аналогичную ошибку, без примечания.

Если f(1) в main() заменить на f("hello", "world"), оба приведенных выше результата будут приведены в последовательности.

У меня такой вопрос: при условии, что уже предоставлены прототипы функций , является ли синтаксис K & R менее безопасным, чем стиль с ключевыми словами встроенного типа? Ответ, на который указывают мои исследования: «Нет, не совсем», но крайне негативное, по-видимому, почти единодушное мнение о более старом стиле объявления типов заставляет меня задуматься, не пропущено ли что-то. Есть что-нибудь, что я пропускаю?

Ответы [ 5 ]

2 голосов
/ 12 апреля 2019

Мои исследования показывают, что если бы функция была определена, как указано выше, последующий вызов foo (8, 6, 7, 5, 3, 0, 9) привел бы к неопределенному поведению;тогда как с последним синтаксисом foo (8, 6, 7, 5, 3, 0, 9) фактически будет недействительным.

Это правильно.Учитывая int foo(a) int a; {}, вызов имеет неопределенное поведение в соответствии с C 2018 6.5.2.2 6:

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

И, учитывая int foo(int a);, вызов нарушает ограничение, согласно C 2018 6.5.2.2 2:

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

Предполагая, что имеются надлежащие предварительные объявления (в этом случаеint foo (int);), синтаксис объявления функции K & R все еще небезопасен?

Если у функции есть как объявление с прототипом, как это было бы в вашем предварительном объявлении, так и определение без прототипа (с использованием старого синтаксиса K & R), результирующий тип идентификатора таков:прототипной версии.Тип функции, объявленной с помощью списка параметров, можно объединить с типом функции, объявленной с синтаксисом K & R.В первом C 2018 6.7.6.3 15 говорится, что два типа совместимы:

Для совместимости двух типов функций оба должны указывать совместимые типы возврата.… Если один тип имеет список типов параметров, а другой тип определен в определении функции, которое содержит (возможно, пустой) список идентификаторов, оба должны совпадать по количеству параметров, а тип каждого параметра прототипа должен быть совместим сtype, полученный в результате применения продвижений аргументов по умолчанию к типу соответствующего идентификатора.…

Затем C 2018 6.2.7 3 сообщает нам, что их можно объединить:

A составной тип может быть сконструирован из двух совместимых типов;это тип, который совместим с обоими этими двумя типами и удовлетворяет следующим условиям:

- если только один тип является типом функции со списком типов параметров (прототип функции), составной тип является прототипом функции со списком типов параметров.

И C 2018 6.2.7 4 сообщает нам, что идентификатор принимает составной тип:

Для идентификатора с внутренней или внешней связью, объявленной в области видимости, в которой видно предыдущее объявление этого идентификатора, если в предыдущем объявлении указана внутренняя или внешняя связь, тип идентификатора в последующем объявлении становитсясоставной тип.

Таким образом, если у вас есть int foo(int a); и int foo() int a; {}, foo имеет тип int foo(int a).

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

Обратите внимание, что типыв прототипе должны соответствовать типы в определении стиля K & R после продвижения аргумента по умолчанию .Например, эти типы совместимы:

void foo(int a);
void foo(a)
char a; // Promotion changes char to int.
{
}

void bar(double a);
void bar(a)
float a; // Promotion changes float to double.
{
}

, а эти типы не являются:

void foo(char a);
void foo(a)
char a; // char is promoted to int, which is not compatible with char.
{
}

void bar(float a);
void bar(a)
float a; // float is promoted to double, which is not compatible with float.
{
}
1 голос
/ 12 апреля 2019

Код в вопросе не очень проблематичен; обработка int представляет несколько проблем. Там, где это сложно, работает вот так:

int another(int c, int s, double f);

int another(c, s, f)
    short s;
    float f;
    char c;
{
    return f * (s + c);  // Nonsense - but it compiles cleanly enough
}

Обратите внимание, что прототип для этого не

int another(char c, short s, float f);

Интересно, что GCC принимает оба прототипа, если вы не добавите -pedantic (или -Wpedantic) к опциям компиляции. Это документированное расширение GCC - §6.38 Прототипы и определения функций старого стиля . Напротив, clang жалуется (как предупреждение, если опция -Werror не указана - и жалуется по умолчанию, даже без -Wall или -Wextra и т. Д.):

$ clang -O3 -g -std=c11 -Wall -Wextra -Werror -Wmissing-prototypes -Wstrict-prototypes -c kr19.c
kr19.c:7:10: error: promoted type 'int' of K&R function parameter is not compatible with the
      parameter type 'char' declared in a previous prototype [-Werror,-Wknr-promoted-parameter]
    char c;
         ^
kr19.c:2:18: note: previous declaration is here
int another(char c, short s, float f);
                 ^
kr19.c:5:11: error: promoted type 'int' of K&R function parameter is not compatible with the
      parameter type 'short' declared in a previous prototype [-Werror,-Wknr-promoted-parameter]
    short s;
          ^
kr19.c:2:27: note: previous declaration is here
int another(char c, short s, float f);
                          ^
kr19.c:6:11: error: promoted type 'double' of K&R function parameter is not compatible with the
      parameter type 'float' declared in a previous prototype [-Werror,-Wknr-promoted-parameter]
    float f;
          ^
kr19.c:2:36: note: previous declaration is here
int another(char c, short s, float f);
                                   ^
3 errors generated.
$

Пока вы распознаете это несоответствие для более коротких типов и ваши прототипы совпадают с продвигаемыми типами, вы фактически не должны сталкиваться с проблемами при определении функций с использованием нотации K & R и объявлении их с помощью нотации прототипа.

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

Я все еще работаю над базой кода, которая имеет некоторые определения функций K & R 80-х и начала 90-х годов. У большинства таких функций есть прототип в заголовке с продвигаемыми типами в прототипах. Я активно очищаю его, чтобы преобразовать все определения функций в нотацию прототипа, гарантируя, что в области действия всегда есть прототип для функций до определения (не static) функций или вызова любой функции. Локальное соглашение заключается в избыточном объявлении static функций в верхней части файла; это тоже работает.

В своем собственном коде я никогда не использую нотацию K & R - даже для функций без параметров (я всегда использую function(void) для них).

Поскольку в стандарте оно явно помечено как «устаревшее» (C11 §6.11. Будущие направления , я настоятельно рекомендую не использовать нотацию K & R в любом современном коде C:

  • §6.11.6 Деклараторы функций

    Использование деклараторов функций с пустыми скобками (не деклараторы типа параметров в формате prototype) является устаревшей функцией.

  • §6.11.7 Определения функций

    Использование определений функций с отдельными идентификаторами параметров и списками объявлений (а не тип параметров формата прототипа и объявления идентификаторов) является устаревшей функцией.

0 голосов
/ 13 апреля 2019

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

Вы, кажется, играете немного быстро и свободно с языком. Я думаю, вы имеете в виду, что вы говорите о влиянии выбора функции определение синтаксис. Определения функций предоставляют объявления функций, и эти объявления могут (стиль ANSI) или не включать (стиль K & R), включая прототипы. Объявления функций, которые не являются частью определений (предварительные объявления), также могут предоставлять или не предоставлять прототипы. Важно понимать, что поведение немного отличается в зависимости от того, имеется ли прототип в области применения.

Многое было сделано о том, что прежний синтаксис не создает функцию прототип. Мое исследование показывает, что если функция была определена как выше, последующий вызов foo (8, 6, 7, 5, 3, 0, 9) приведет к неопределенное поведение;

Да . Это вытекает из языковых ограничений (поэтому не только поведение не определено, но и нарушения должны быть диагностированы). В C11 соответствующий текст - пункт 6.5.2.2/2:


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


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

тогда как с последним синтаксисом foo (8, 6, 7, 5, 3, 0, 9) фактически будет недействительным.

Да ? Похоже, вы проводите различие, которое я не наблюдаю между тем, что является «недействительным», и тем, что имеет неопределенное поведение. Код, который является «недействительным» по любому определению, о котором я могу думать, определенно имеет неопределенное поведение. В любом случае, поведение в этом случае также явно не определено, так как при наличии прототипа в области действия 6.5.2.2/2 (см. Выше) применяется независимо от формы определения функции. Если имеется , а не прототип, находящийся в области применения, то вместо этого применяется пункт 6.5.2.2 / 6 (в C11). В нем говорится, частично:


Если выражение, обозначающее вызываемую функцию, имеет тип, который не включает прототип [...] Если количество аргументов не При равном количестве параметров поведение не определено.


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

У меня такой вопрос: предполагается, что прототипы функций уже при условии, является ли синтаксис K & R менее безопасным, чем стиль со встроенным введите ключевые слова?

Если один и тот же прототип для функции f() находится в области действия и там, где функция определена, и где она вызывается (хорошая практика в любом случае), и если определение стиля K & R для f() задает тот же тип возврата и типы параметров, которые делает прототип, тогда все будет работать нормально. Несколько совместимых объявлений в области видимости в определении функции объединяются, чтобы сформировать составной тип для функции, который будет совпадать с типом, объявленным прототипом. Семантика точно такая, как если бы в определении использовался синтаксис ANSI напрямую. Об этом говорится в разделе 6.2.7 и соответствующих разделах стандарта.

Однако существует дополнительное бремя обслуживания в поддержании определения функции, которое не соответствует лексическому типу его прототипа.Также повышается вероятность ошибок, возникающих из-за того, что типы параметров в списке объявлений параметров не соответствуют прототипу, и в этом смысле подход менее безопасен, чем простое использование стиля ANSI.

Однако, если нетпрототип для f() в области видимости в точке его определения в стиле K & R, и если какой-либо из его параметров имеет тип float или целочисленные типы, меньшие чем int, тогда существует больше возможностей для ошибки.Несмотря на объявленные типы параметров, реализация функции будет ожидать, что аргументы будут подвергнуты продвижению аргументов по умолчанию, и поведение не определено, если на самом деле это не так.Можно написать прототип, который предвидит эту потребность, для использования там, где вызывается f(), но слишком легко ошибиться.В этом смысле предложенная вами комбинация действительно менее безопасна.

0 голосов
/ 13 апреля 2019

У меня такой вопрос: при условии, что прототипы функций уже предоставлены, является ли синтаксис K & R менее безопасным, чем стиль с ключевыми словами встроенного типа?

Это прямо НЕПРАВИЛЬНО .Подробнее см. Продвижение аргументов по умолчанию в вызовах функций C .

Это также неопределенное поведение.Согласно 6.5.2.2 Вызовы функций , параграф 9 стандарта C11 :

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

Вы должны НИКОГДА не смешивать прототипы с функциями, определенными в старом стиле K & R.

Функция, написанная в стиле K & R, ожидает, что ее аргументы подверглись продвижение аргументов по умолчанию когда они были переданы.

Код, который вызывает функцию с прототипом, не продвигает аргументы функции.

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

Конец истории.

Не делайте этого.

0 голосов
/ 12 апреля 2019

Разницы нет вообще.Вы можете определить их, как вы предпочитаете.Они обмениваются.Но, конечно, ни один здравомыслящий человек не посоветовал бы использовать доисторические обозначения.

Мои исследования показывают, что, если функция была определена, как указано выше, последующий вызов foo (8, 6, 7, 5, 3, 0, 9) приведет к неопределенному поведению;тогда как с последним синтаксисом foo (8, 6, 7, 5, 3, 0, 9) фактически будет недействительным.

Ваше исследование неверно.Если языковой стандарт позволяет использовать функции без прототипов,

https://onlinegdb.com/SkKF4DAtE

int main()
{
    printf("%d\n", foo(2,3,4,5,6,7,8,9));
    printf("%d\n", foo1(2,3,4,5,6,7,8,9));

    return 0;
}

int foo(a)
int a;
{
    return a*a;
}

int foo1(int a)
{
    return a*a;
}
...