Недавно у меня было собеседование, и один вопрос был задан: какая польза от extern "C"
в коде C ++. Я ответил, что это означает использование функций C в коде C ++, поскольку C не использует искажение имен. Меня спросили, почему Си не использует искажение имен и, честно говоря, я не мог ответить.
Я понимаю, что когда компилятор C ++ компилирует функции, он дает специальное имя функции главным образом потому, что мы можем иметь перегруженные функции с тем же именем в C ++, которые должны быть разрешены во время компиляции. В C имя функции останется прежним, или, возможно, с _ перед ним.
Мой вопрос: что плохого в том, чтобы позволить компилятору C ++ также манипулировать функциями C? Я бы предположил, что не имеет значения, какие имена им дает компилятор. Мы вызываем функции одинаково в C и C ++.
Это было своего рода ответом выше, но я постараюсь поместить вещи в контекст.
Сначала С пришел первым. Таким образом, то, что делает C, является своего рода «по умолчанию». Он не искажает имена, потому что это не так. Имя функции — это имя функции. Глобал есть глобал и так далее.
Затем появился C ++. C ++ хотел иметь возможность использовать тот же компоновщик, что и C, и иметь возможность ссылаться с кодом, написанным на C. Но C ++ не мог оставить C «искаженным» (или его отсутствием) как есть. Посмотрите на следующий пример:
int function(int a);
int function();
В C ++ это разные функции с разными телами. Если ни один из них не искажен, оба будут называться «function» (или «_function»), и компоновщик будет жаловаться на переопределение символа. Решением C ++ было преобразование типов аргументов в имя функции. Итак, один называется _function_int
а другой называется _function_void
(не фактическая схема искажения), и столкновения избегают.
Теперь мы остались с проблемой. Если int function(int a)
был определен в модуле C, и мы просто берем его заголовок (т.е. объявление) в коде C ++ и, используя его, компилятор сгенерирует инструкцию для компоновщика для импорта _function_int
, Когда функция была определена, в модуле C она не называлась так. Он назывался _function
, Это приведет к ошибке компоновщика.
Чтобы избежать этой ошибки, во время декларация о функции, мы говорим компилятору, что это функция, предназначенная для связи или компиляции с помощью компилятора C:
extern "C" int function(int a);
Компилятор C ++ теперь знает, как импортировать _function
скорее, чем _function_int
и все хорошо.
Дело не в том, что они «не могут», они не, в общем.
Если вы хотите вызвать функцию в библиотеке C с именем foo(int x, const char *y)
, нехорошо позволять вашему компилятору C ++ исправлять это в foo_I_cCP()
(или что-то еще, просто надумали здесь схему искажения) только потому, что это возможно.
Это имя не разрешается, функция находится на C, а ее имя не зависит от списка типов аргументов. Поэтому компилятор C ++ должен это знать и пометить эту функцию как C, чтобы избежать искажения.
Помните, что указанная функция C может находиться в библиотеке, исходный код которой у вас нет, все, что у вас есть, это предварительно скомпилированный двоичный файл и заголовок. Таким образом, ваш компилятор C ++ не может делать «свое дело», он не может изменить то, что находится в библиотеке.
что плохого в том, чтобы позволить компилятору C ++ также искажать функции C?
Они больше не были бы C-функциями.
Функция — это не просто подпись и определение; как работает функция, во многом определяется такими факторами, как соглашение о вызовах. «Двоичный интерфейс приложения», указанный для использования на вашей платформе, описывает, как системы взаимодействуют друг с другом. Интерфейс C ++ ABI, используемый вашей системой, определяет схему искажения имени, так что программы в этой системе знают, как вызывать функции в библиотеках и так далее. (Прочитайте C ++ Itanium ABI для отличного примера. Вы очень быстро поймете, почему это необходимо.)
То же самое относится и к C ABI в вашей системе. Некоторые C ABI на самом деле имеют схему искажения имен (например, Visual Studio), поэтому речь идет не столько об «отключении искажения имен», сколько о переключении с C ++ ABI на C ABI для определенных функций. Мы помечаем функции C как функции C, к которым относится C ABI (а не C ++ ABI). Объявление должно соответствовать определению (будь то в том же проекте или в какой-нибудь сторонней библиотеке), иначе объявление бессмысленно. Без этого ваша система просто не будет знать, как найти / вызвать эти функции.
Что касается того, почему платформы не определяют CI и C ++ ABI одинаковыми и избавляются от этой «проблемы», то это отчасти исторически — исходных C ABI не хватало для C ++, который имеет пространства имен, классы и перегрузку операторов, все из которых нужно каким-то образом представлять в имени символа в удобной для компьютера форме, но можно также утверждать, что создание программ на C, в настоящее время подчиняющихся C ++, несправедливо по отношению к сообществу C, которому пришлось бы мириться с гораздо более сложным ABI только ради некоторых других людей, которые хотят взаимодействия.
MSVC на самом деле делает искажать имена C, хотя и простым способом. Иногда добавляет @4
или другое небольшое число. Это относится к соглашениям о вызовах и необходимости очистки стека.
Так что предпосылка просто ошибочна.
Очень часто есть программы, которые частично написаны на C и частично написаны на каком-то другом языке (часто на ассемблере, но иногда на Pascal, FORTRAN или чем-то еще). Также часто программы содержат разные компоненты, написанные разными людьми, которые могут не иметь исходного кода для всего.
На большинстве платформ существует спецификация — часто называемая ABI [Application Binary Interface], которая описывает, что должен делать компилятор для создания функции с определенным именем, которая принимает аргументы некоторых определенных типов и возвращает значение некоторого определенного типа. В некоторых случаях ABI может определять более одного «соглашения о вызовах»; Компиляторы для таких систем часто предоставляют средства указания, какое соглашение о вызовах следует использовать для конкретной функции. Например, в Macintosh большинство подпрограмм Toolbox используют соглашение о вызовах Pascal, поэтому прототип для чего-то вроде «LineTo» будет выглядеть примерно так:
/* Note that there are no underscores before the "pascal" keyword because
the Toolbox was written in the early 1980s, before the Standard and its
underscore convention were published */
pascal void LineTo(short x, short y);
Если весь код в проекте был скомпилирован с использованием одного и того же компилятора, он
не имеет значения, какое имя компилятор экспортирует для каждой функции, но в
во многих ситуациях для кода на C будет необходимо вызывать функции, которые были
скомпилирован с использованием других инструментов и не может быть перекомпилирован с помощью настоящего компилятора
[и вполне может даже не быть в С]. Возможность определить имя компоновщика
Таким образом, имеет решающее значение для использования таких функций.
Я добавлю еще один ответ, чтобы рассмотреть некоторые из обсуждений, которые имели место.
C ABI (двоичный интерфейс приложения) первоначально вызывал передачу аргументов в стеке в обратном порядке (т. Е. Толкает справа налево), где вызывающая сторона также освобождает хранилище стека. Современный ABI фактически использует регистры для передачи аргументов, но многие из искажающих соображений восходят к передаче оригинального стека.
Оригинальный ABI Pascal, напротив, выдвигал аргументы слева направо, и вызываемый должен был выдвигать аргументы. Оригинальный C ABI превосходит оригинальный Pascal ABI в двух важных моментах. Порядок выталкивания аргументов означает, что смещение стека первого аргумента всегда известно, что позволяет функциям с неизвестным числом аргументов, где ранние аргументы контролируют, сколько других аргументов существует (ala printf
).
Второй способ превосходства C ABI — это поведение в том случае, если вызывающий и вызываемый абоненты не согласны с тем, сколько аргументов существует. В случае C, если вы на самом деле не обращаетесь к аргументам после последнего, ничего плохого не происходит. В Паскале неправильное количество аргументов извлекается из стека, и весь стек поврежден.
Оригинальный Windows 3.1 ABI был основан на Pascal. Как таковой, он использовал Паскаль ABI (аргументы в порядке слева направо, Callee Pops). Поскольку любое несоответствие номера аргумента может привести к повреждению стека, была сформирована схема искажения. Каждое имя функции было искажено числом, указывающим размер в байтах ее аргументов. Итак, на 16-битной машине, следующая функция (синтаксис C):
int function(int a)
Был искалечен function@2
, так как int
имеет ширину два байта. Это было сделано для того, чтобы в случае несоответствия объявления и определения компоновщик не смог найти функцию, а не повредил стек во время выполнения. И наоборот, если программа связывается, то вы можете быть уверены, что в конце вызова выбрано правильное количество байтов из стека.
32-битная Windows и далее использовать stdcall
ABI вместо. Это похоже на Паскаль ABI, за исключением того, что порядок нажатия такой же, как в C, справа налево. Как и в Pascal ABI, искажение имени меняет размер аргумента в байтах на имя функции, чтобы избежать повреждения стека.
В отличие от заявлений, сделанных в другом месте, C ABI не искажает имена функций, даже в Visual Studio. И наоборот, калечащие функции украшены stdcall
Спецификация ABI не уникальна для VS. GCC также поддерживает этот ABI, даже при компиляции для Linux. Это широко используется Вино, он использует собственный загрузчик, чтобы разрешить связывание исполняемых двоичных файлов Linux со скомпилированными библиотеками Windows во время выполнения.
Компиляторы C ++ используют искажение имен, чтобы разрешить уникальные имена символов для перегруженных функций, сигнатура которых в противном случае была бы одинаковой. В основном он также кодирует типы аргументов, что позволяет осуществлять полиморфизм на уровне функций.
C не требует этого, поскольку не допускает перегрузки функций.
Обратите внимание, что искажение имен является одной (но, конечно, не единственной!) Причиной, по которой нельзя полагаться на C ++ ABI.
C ++ хочет иметь возможность взаимодействовать с кодом C, который ссылается на него или на который он ссылается.
C ожидает неискаженные имена функций.
Если C ++ искажает его, он не найдет экспортированные не искаженные функции из C, или C не найдет экспортированные функции C ++. Компоновщик C должен получить имя, которое он сам ожидает, потому что он не знает, прибывает ли он или идет в C ++.