Приоритет статических функций в C - PullRequest
8 голосов
/ 14 мая 2019

Допустим, у вас есть файл File1.c со статической функцией Foo и эта функция вызывается из File1.c.Кроме того, в другом файле (File2.c) у вас есть другая функция Foo, которая не является статической.Я понимаю, что статическая функция не видна за пределами файла, в котором она объявлена, и фактически невидима для компоновщика.

Но означает ли это, что внутренний вызов функции Foo в File1.c всегда разрешается во время компиляции?

Существуют ли случаи, когда вызов Foo в File1.c может быть связан с глобальной функцией Fooиз File2.c?

Ответы [ 3 ]

5 голосов
/ 14 мая 2019

Сводка

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

. При использовании объявлений, как описано ниже, идентификатор в теории может ссылаться на функцию из другой единицы перевода после * 1011.* объявление того же имени в этом переводе.К сожалению, поведение не определено стандартом C из-за C 2018 6.2.2 7:

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

Это означает, что вы не можете полагаться только на стандарт C, чтобы гарантировать это поведение, но реализация C может определить его как расширение.

Подробности

На эти вопросы отвечают правила С. для области видимости и связи.

Предположим, в File1.c у нас есть статическое определение функции:

static int foo(int x) { return x*x; }

Поскольку объявлен идентификатор fooвне какой-либо функции он имеет область действия файла (C 2018 6.2.1 4).Это означает, что идентификатор foo видим и обозначает это определение функции для оставшейся части File1.c.Кроме того, поскольку использовался static, он имеет внутреннюю связь (6.2.2 3).

Исключение составляет область действия.Для областей внутри других областей, таких как блок { … }, который определяет функцию внутри файла, или блок внутри блока, объявление того же идентификатора может скрывать внешнее объявление.Итак, давайте рассмотрим повторное выделение foo внутри блока.

Чтобы сослаться на foo, определенный вне File1.c, нам нужно объявить foo с внешней связью, чтобы этот новый foo может быть связано с внешним определением foo.Есть ли способ сделать это в C?

Если мы попытаемся объявить extern int foo(int x); внутри блока, то применяется 6.2.2 4:

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

Таким образом, это объявление будет просто повторно объявить то же самое foo.

Если мы объявим это без extern, применяется int foo(int x);, 6.2.2 5 применяется:

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

Итак, похоже, что мы не можем объявить другой foo с extern или без него.Но подождите, у нас есть еще один трюк.Мы можем сделать предыдущее объявление, которое указывает внутреннюю или внешнюю связь невидимым, скрыв его с объявлением без связи.Чтобы получить объявление без привязки, мы можем объявить объект (а не функцию) без extern:

#include <stdio.h>

static int foo(int x) { return x*x; }

void bar(void)
{
    int foo; // Not used except to hide the function foo.
    {
        extern int foo(int x);
        printf("%d\n", foo(3));
    }
}

Поскольку, где появляется extern int foo(int x);, предшествующее объявление foo с внутреннимсвязь не видна, что первое условие в 6.2.2 4, приведенное выше, не применяется, а остальное в 6.2.2 4:

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

Это «допустимый» C-код.К сожалению, он не определен в 6.2.2 7:

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

4 голосов
/ 14 мая 2019

означает ли это, что внутренний вызов функции Foo в File1.c всегда разрешается во время компиляции?

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

Мы можем проверить, как данный набор инструментов (в моем случае linux / gcc) выбираетсделать это с помощью быстрого теста:

Начиная с простого файла (test.c):

#include <stdio.h>

static void foo() {
    printf("hello");
}

void bar() {
    foo();
}

А затем скомпилировать и проверить полученный объектный файл:

gcc -c -o test.o test.cpp
nm test.o

0000000000000018 T bar
0000000000000000 t foo
                 U _GLOBAL_OFFSET_TABLE_
                 U printf

Мы видим, что foo() и bar() находятся в таблице символов, но с разными флагами.

Мы также можем посмотреть на сборку:

objdump -d test.o

0000000000000018 <bar>:
  18:   55                      push   %rbp
  19:   48 89 e5                mov    %rsp,%rbp
  1c:   b8 00 00 00 00          mov    $0x0,%eax
  21:   e8 da ff ff ff          callq  0 <foo>
  26:   90                      nop
  27:   5d                      pop    %rbp
  28:   c3                      retq  

И видим, чтовызов foo еще не связан (указывает на заполнитель 0).Таким образом, мы можем с уверенностью сказать, что в этом случае разрешение может произойти и действительно происходит во время соединения.

Существуют ли случаи, когда вызов Foo в File1.c может быть связан с глобальной функцией Foo File2.с?

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

0 голосов
/ 15 мая 2019

Вот конкретный пример:

// a1.c
static void foo(void) { }
void bar(void) { foo(); }

и

// a2.c
void bar(void);
void foo(void) { bar(); }
int main(void) { foo(); }

В этом примере код верен:

  • a1.c имеет идентификаторfoo объявлено с внутренней связью и одним подходящим определением.
  • a2.c имеет идентификатор foo, объявленный с внешней связью и одно соответствующее определение.

Проблема, с которой вы столкнетесь, - это попытка включить в a1.c объявление для foo в a2.

Например: скажем, a2.h имеет содержимое void foo(void);, а a1.c начинается с #include "a2.h".Здесь, скорее всего, произойдет ошибка компиляции, однако другие ответы показывают, как может быть молчаливо неопределенное поведение при использовании злых конструкций, таких как объявления функций блочной области видимости.

Также возможно иметь четко определенные, нонепреднамеренное поведение.Если a1.c делает #include "a2.h" после static void foo(void);, то ошибки нет, поскольку существует правило, согласно которому объявление функции ни static, ни extern не соответствует связи более раннего объявлениятот же идентификатор, если таковой существует;но в этом случае вызов foo() из a1.c все равно находит a1 foo.Если бы a2.h также содержал макрос, который вызывал foo(), то макрос не работал бы должным образом.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...