Объявление экспортируемых функций


Содержание

Объявление функции

До того, как функция будет вызвана, она должна быть объявлена.

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

Объявление функции называют также её прототипом.

Тип_результата Имя_функции (Тип_пар1, Тип_пар2, . );

  • Тип_результата — некоторый существующий (например, встроенный) тип данных или ключевое слово void, указывающее на то что функция никакого значения возвращать не будет.
  • Имя_функции — уникальный для данного пространства имён идентификатор.
  • Тип_парN — некоторый существующий (например, встроенный) тип данных для N-oro аргумента.

int max (int, int);
double cube (double)
float massa();
void printarr(*int, int);

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

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

Определение (описание) функции

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

Тип_результата Имя_функции (Тип_пар1 Имя_пар1, Тип_пар2 Имя_пар2, . ) <
Оператор1;
Оператор2;
.
ОператорN;
return n;
>;

  • Имя_парN — уникальное внутри функции имя N-ro параметра. Имена параметров можно задавать и в прототипе функции, тогда в определении надо использовать те же имена.
  • ОператорN — некоторые операторы и выражения, содержащиеся внутри функции и выполняющиеся каждый раз при вызове функции. Внутри операторов мы можем обращаться к глобальным объектам программы; к локальным объектам, объявленным внутри функции; а также к аргументам функции.
  • return n — оператор, останавливающий работу функции и возвращающий n в качестве её значения (при этом тип n должен соответствовать типу результата в объявлении функции). Наличие этого оператора обязательно для функции возвращающей значение. Для функции объявленной как void можно вызывать оператор return без аргументов, это досрочно завершит функцию, иначе — будут выполнены все действия до конца блока описания функции.

Блок определения функции называется также её телом.

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

20) Понятие указателя

Указатель — это переменная. Такая же, как и любая другая. Со своими «можно» и со своими «нельзя». У нее есть свое значение и свой адрес в памяти.

Значение переменной-указателя — адрес другой переменной. Адрес переменной-указателя свой и независимый.

Переменная-указатель (далее будем говорить просто — указатель) объявляется также, как и любые другие переменные, но после имени типа ставится звездочка.

Здесь объявляется переменная pointerToInteger. Ее тип — указатель на переменную типа int.

Как следует писать звездочку относительно типа и имени переменной? Встречаются, например, такие формы записи, и все они имеют право на существование:

int* p1;int * p2;int *p3;

Аргументы за первую форму. Чтобы объявить переменную следует указать ее тип, а затем имя. Звездочка является частью типа, а не частью имени. Это также подтверждается тем, что при привидении типов пишется тип со звездочкой, а не тип отдельно. Следовательно, должна писаться слитно с типом. Минус в том, что при объявлении нескольких переменных после объявления int*, только первая из них будет указателем, а вторая будет просто переменной типа int. Не объявляйте несколько указателей в одной строчке. Это не очень хороший стиль.

Аргументы за вторую форму. Есть люди, которым нравится «когда код дышит» Они ставят пробел до скобок и после скобок. И здесь тоже ставят. Возможно, это просто такой компромисс.

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

И помните, что компилятору все это безразлично.

Не нашли то, что искали? Воспользуйтесь поиском:

Лучшие изречения: Студент — человек, постоянно откладывающий неизбежность. 10532 — | 7319 — или читать все.

188.64.174.135 © studopedia.ru Не является автором материалов, которые размещены. Но предоставляет возможность бесплатного использования. Есть нарушение авторского права? Напишите нам | Обратная связь.

Отключите adBlock!
и обновите страницу (F5)

очень нужно

В чем разница между импортом выражения функции или объявлением функции из модуля ES6?

Как я понимаю ( смотрите раздел 16.3.2.1 ), ES6 позволяет различный синтаксис для экспорта функции / класса операндов. Разница относится к нуждается ли экспортируемая функция быть интерпретирована при ввозе в объявлении функции, в этом случае вы пишете: export default function () <> // (a) или как выражение функции: export default (function () <>); // (b) .

В качестве возможной связанной заметка на полях: Я читал, что импорт водрузили, но я не совсем уверен, что это означает, что в данном контексте.

Принимая дела этого примера:

import foo from ‘my_module’; // (c)

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

Самое главное, в чем разница (с точки зрения настройки foo ) , когда my_module экспортирует функцию , используя (a) и когда она экспортирует его с помощью (b) ?

Ваш вопрос немного запутанным, но я буду стараться изо всех сил, чтобы все объяснить.

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

Я читал, что импорт водрузил, но я не совсем уверен, что это означает, что в этом контексте:

Да, декларация импорта поднимается. Точно так же к var или function (и на самом деле , как и любой другой декларации ) идентификатор foo доступен с самого начала, прежде чем любые заявления в модуле выполняются. Фактически связывание даже создано до тех объявленных var iables.

Разница заключается в том, как они инициализируются:

  • var s инициализируются undefined
  • function с и function* s инициализируются с объектом функции
  • let , const И class эс остались неинициализированные
  • импортированные привязки даже не очень инициализированы, они созданы как указатель на локальную переменный , что экспортируемые название относится в импортируемом модуле
  • импортируемые модули ( import * as … ) инициализируются с объектом модуля (свойства которого являются такие указатели, а)

Когда foo установлено , обратитесь к моей экспортируемой функции?

Короткий ответ: прежде, чем все остальное.

Длинный ответ: это на самом деле не установлено. Это ссылка на локальную переменную в импортируемого модуля , который вы ожидаете , чтобы держать функцию. Локальная переменная может измениться , когда это не const — но мы обычно не ожидаем , что конечно. И как правило , он содержит эту функцию уже, так как импортированный модуль полностью оценен до модуля (ей) , что импортировать его есть. Так что, если вы боитесь , что есть проблема с вар FunctionName = функция () <> против функции FunctionName () <> может быть освобожден — нет.

Теперь вернемся к вашему вопросу в заголовке:

В чем разница между экспортом выражение функции и объявление функции в модуле ES6?

Ничего особенного, эти два аспекта на самом деле не имеет много общего друг с другом:

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

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

Хорошо, последние два по умолчанию декларации экспорта являются на самом деле немного отличается от первых двух. Локальный идентификатор , который связан с именем экспортируемого default не foo , но *default* — это не может быть переназначен. Это имеет смысл в последнем случае (если нет названия foo ), но во втором предпоследнем случае следует заметить , что foo на самом деле просто локальный псевдоним, а не сама переменная экспортируется. Я рекомендую использовать этот шаблон.

Да, и , прежде чем спросить: Да, что экспорт в прошлом по умолчанию действительно является объявление функции, а не выражение. Анонимная функция декларации . Это новое с ES6 :-)

Так что же разница между export default function () <> и export default (function () <>);

Они в значительной степени то же самое для каждой цели. Они анонимные функции, с .name собственностью «default» , которые проводятся с тем , что специальной *default* привязкой к , к которому экспортируемые названию default указует на анонимную стоимость экспорта.
Их единственное различие грузоподъемное — декларация будет получить его функцию инициализируется в верхней части модуля, то выражение будет вычисляться только после выполнения кода модуля достигает оператор. Однако, учитывая , что не существует переменный с именем доступным для них, такого поведения не наблюдается для одной очень нечетного частного случая , за исключением: модуль , который импортирует себя. Гм, да.

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

В таком случае, почему оба синтаксис?

Случается. Это позволили в спецификации , потому что он не делает дополнительные исключений для запрещения определенных бессмысленных вещей. Он не предназначен для использования. В этом случае спецификация даже явно запрещает function и class выражения в export default заявлениях и рассматривает их в качестве заявлений вместо этого. Используя оператор группировки, вы нашли лазейку. Отлично сработано. Не ругайте его.

Экспортирование функций

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

int Function() export
<
>

Данный модификатор указывает компилятору внести функцию в таблицу EX5-функций, экспортируемых данным исполняемым ex5-файлом. Только функции с таким модификатором становятся доступными («видимыми») из других mql5-программ.

Свойство library указывает компилятору, что данный EX5-файл будет являться библиотекой, и компилятор проставит это в заголовке EX5.

Все функции, которые планируются как экспортируемые, должны быть помечены модификатором export .


Вопрос по javascript, ecmascript-6, es6-modules &#8211 В чем разница между импортом выражения функции или объявления функции из модуля ES6?

Как я понимаю (см. раздел 16.3.2.1), ES6 допускает разные синтаксисы для операндов экспорта функции / класса. Разница заключается в том, должна ли экспортируемая функция интерпретироваться при импорте как объявление функции, и в этом случае вы пишете: export default function () <> // (a) или как выражение функции: export default (function () <>); // (b) .

Как возможный связанный комментарий: я читал, что импорт поднят, но я не совсем уверен, что это значит в этом контексте.

Принимая случай этого примера:

import foo from ‘my_module’; // (c)

Насколько я понимаю, приведенное выше утверждение сохранит мою экспортированную функцию в foo переменная. Эта переменная поднята, или что, и когда?

Самое главное, в чем разница (с точки зрения настройки foo ) когда my_module экспортирует функцию, используя (a) и когда он экспортирует его с помощью (b) ?

Ваш вопрос немного запутан, но я постараюсь все объяснить.

Давайте сначала выясним, как работают модули в целом. Модуль имеет набор экспортируемых имен, каждое из которых ссылается на локальную переменную в этом модуле. Имя экспорта не обязательно должно совпадать с именем локальной привязки. Одно из экспортируемых имен может быть default , для которого существует специальный синтаксис (как при экспорте, так и при импорте), предназначенный для случая, когда модуль экспортирует только одну вещь.

Я читал, что импорт поднят, но я не совсем уверен, что это значит в этом контексте:

Да, импортные декларации поднимаются. Аналогично var или же function (и на самом деле нравитсялюбая другая декларация) идентификатор foo доступен с самого начала, до того, как какие-либо операторы в модуле будут выполнены. На самом деле привязка создается даже до var iables.

Разница в том, как они инициализируются:

  • var s инициализируются с undefined
  • function с и function* s инициализируются с помощью объекта функции
  • let , const а также class они остаются неинициализированными
  • импортированные привязки даже не инициализируются, они создаются как указатель на локальную переменную, на которую ссылается экспортированное имя в импортированном модуле
  • импортированные модули ( import * as … ) инициализируются с модулем объекта (свойства которого также являются такими указателями)

Когда foo установить для ссылки на мою экспортированную функцию?

Краткий ответ: прежде всего.

Длинный ответ: это на самом деле не установлено. Это ссылка на локальную переменную в импортированном модуле, которую вы ожидаете сохранить в функции. Локальная переменная может измениться, когда она не const — но мы обычно этого не ожидаем. И обычно она уже содержит эту функцию, потому чтоимпортированный модуль полностью оценен перед модули, которые его импортируют. Так что, если вы боитесь, что есть проблема сvar functionName = function () <> против функции functionName () <> Вы можете быть освобождены — нет.

Теперь вернемся к названию вопроса:

В чем разница между экспортом выражения функции и объявлением функции в модуле ES6?

Ничего особенного, эти два аспекта на самом деле не имеют ничего общего друг с другом:

  • export объявления связывают имя экспорта с локальной переменной в области видимости модуля
  • Все переменные в области видимости модуля, как обычно, поднимаются
  • объявления функций инициализируются иначе, чем объявления переменных с присваиванием выражения функции,по-прежнему

Конечно, до сих пор нет веских причин не использовать болеедекларативный объявления функций везде; это не отличается в модулях ES6, чем раньше. Если вообще, может быть даже меньше причин использовать выражения функций, так как все покрыто объявлениями:

Хорошо, последние две декларации экспорта по умолчанию немного отличаются от первых двух. Локальный идентификатор, связанный с экспортированным именем default не является foo , но *default* — это не может быть переназначено. Это имеет смысл в последнем случае (где нет названия foo ), но в последнем случае вы должны заметить, что foo на самом деле это просто локальный псевдоним, а не сама экспортируемая переменная. Я бы рекомендовал не использовать этот шаблон.

Да, и прежде чем вы спросите: Да, последний экспорт по умолчанию на самом деле также является объявлением функции, а не выражением.объявление анонимной функции, Это новое с ES6 :-)

Так в чем же разница между export default function () <> а также export default (function () <>);

Они практически одинаковы для всех целей. Это анонимные функции, с .name имущество «default» , которые проводятся этим специальным *default* привязка к которому экспортируемое имя default указывает на анонимные значения экспорта.
Их единственное отличие заключается в подъеме — декларация получит свою функцию в верхней части модуля, выражение будет оценено только после того, как выполнение кода модуля достигнет оператора. Однако, учитывая, что для них нет переменной с доступным именем, такое поведение не наблюдается, за исключением одного очень странного особого случая: модуля, который импортирует сам себя. Хм да

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

В таком случае, почему есть оба синтаксиса?

Случается. Этопозволил по спецификации, потому что он не делает лишних исключений, чтобы запретить определенные бессмысленные вещи. этоне предназначено использоваться. В этом случае спецификация даже явно запрещает function а также class выражения в export default заявления и трактует их как декларации вместо. Используя оператор группировки, вы нашли лазейку. Отлично сработано. Не злоупотребляйте этим.

В чем разница между импортом выражения функции или объявления функции из модуля ES6?

как я понимаю, он (см. раздел 16.3.2.1), ES6 допускает различные синтаксисы для операндов экспорта функций / классов. Разница заключается в том, должна ли экспортированная функция интерпретироваться при импорте как объявление функции, и в этом случае вы пишете: export default function () <> // (a) или как выражение функции: export default (function () <>); // (b) .

как возможный связанный sidenote: я читал, что импорт поднимается, но я не совсем уверен, что это означает в этом контексте.

принимая случай этого примера:

import foo from ‘my_module’; // (c)

как я понимаю, приведенный выше оператор сохранит мою экспортированную функцию в foo переменной. Эта переменная поднята или что такое и когда?

Илон Маск рекомендует:  Что такое код photoshop

самое главное, в чем разница (с точки зрения настройки foo ) при my_module экспортирует функцию с помощью (a) и когда он экспортирует его с помощью (b) ?

1 ответов

ваш вопрос немного запутан, но я постараюсь изо всех сил объяснить все.

давайте сначала установим, как работают модули в целом. Модуль имеет набор экспортированных имен, каждое из которых ссылается на локальную переменную в этом модуле. Имя экспорта не должно совпадать с именем локальной привязки. Одним из экспортируемых имен может быть default , для которого существует специальный синтаксис (как при экспорте, так и при импорте), выделенный для случая, когда модуль экспортирует только одна вещь.

я читал, что импорт поднимается, но я не совсем уверен, что это означает в этом контексте:

да, объявления импорта поднимаются. Аналогично a var или function (и на самом деле нравится другое объявление) идентификатор foo доступно с самого начала, перед выполнением любых инструкций в модуле. На самом деле привязка даже создается до тех, которые объявлены var iables.

разница в том, как они инициализируются:

  • var s инициализируются с undefined
  • function s и function* s инициализируются с помощью объекта функции
  • let , const и class es остаются неинициализированными
  • импортированный привязки даже не инициализированы, они создаются как указатель на локальную переменную, на которую ссылается экспортированное имя в импортированный модуль
  • импортированные модули ( import * as … ) инициализируются объектом модуля (свойства которого также являются такими указателями)

когда foo ссылаться на мой экспортируемой функции?

короткий ответ: прежде чем все остальное.

длинный ответ: на самом деле он не установлен. Это ссылка на локальную переменную в импортированном модуле, которая, как ожидается, будет содержать функцию. Местный житель переменная может измениться, если она не const — но мы обычно не ожидаем, что конечно. И обычно он уже содержит эту функцию, потому что импортированный модуль полностью оценивается прежде, чем модули, которые импортируют его. Поэтому, если вы боитесь, что есть проблема с ВАР имя_функции = функция() <> vs функция имя_функции() <> вы можете вздохнуть с облегчением — нет.

теперь вернемся к вашему вопросу заголовка:

что является ли разница между экспортом выражения функции и объявлением функции в модуле ES6?

ничего особенного, два аспекта на самом деле не имеют много общего друг с другом:

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

конечно, до сих пор нет веских причин не использовать больше декларативный объявления функций везде; это не отличается в модулях ES6, чем раньше. Если вообще, может быть даже меньше причин использовать выражения функций, так как все охвачено объявлениями:

Ok, последние два объявления экспорта по умолчанию на самом деле немного отличается от первых двух. Локальный идентификатор, связанный с экспортируемым именем default не foo , а *default* — его нельзя переназначить. Это имеет смысл в последнем случае (где нет имени foo ), но в предпоследнем случае вы должны заметить, что foo на самом деле просто псевдоним, а не сама экспортируемая переменная. Я бы рекомендовал не использовать этот шаблон.

О, и прежде чем вы спросите: Да, этот последний экспорт по умолчанию действительно объявление функции, а не выражение. Ан объявление анонимной функции. Это новое с ES6: -)

так в чем же разница между export default function () <> и export default (function () <>);

они практически одинаковы для любых целей. Это анонимные функции с .name свойства «default» , которые проводятся по специальной *default* привязка, к которой экспортируется имя default указывает на анонимный экспорт ценности.
Их единственное отличие-подъем-объявление получит экземпляр своей функции в верхней части модуля, выражение будет оцениваться только после того, как выполнение кода модуля достигнет оператора. Однако, учитывая, что нет переменной с доступным именем для них, это поведение не наблюдается, за исключением одного очень странного частного случая: модуля, который импортирует себя. Эм, да.

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

в таком случае, почему оба синтаксиса?

происходит. Это разрешено по спецификации, потому что он не делает дополнительных исключений, чтобы запретить некоторые бессмысленные вещи. Это не предназначен использовать. В этом случае спецификация даже явно запрещает function и class выражения export default заявления и рассматривает их как объявления. Используя оператор группировки, вы нашли лазейку. Молодец. Не злоупотребляй этим.

1.26. Использование экспортируемых шаблонов

1.26. Использование экспортируемых шаблонов

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

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


Опции для включения экспортируемых шаблонов приведены в табл 1.39. Опции для указания расположения реализаций шаблонов приведены в табл. 1.40. Если ваш инструментарий в этой таблице не указан, то он, скорее всего, не поддерживает экспортируемых шаблонов.

Табл. 1.39. Опции для включения экспортируемых шаблонов

Инструментарий Сценарий
Comeau (Unix) -export, -A или -strict
Comeau (Windows) -export или -A
Intel (Linux) -export или -strict-ansi?

? Версии компилятора Intel для Linux до 9.0 использовали опцию -strict_ansi

Табл. 1.40. Опции, указывающие расположение реализаций шаблонов

Инструментарий Сценарий
Comeau -template_directory=
Intel (Linux) -export_dir

Например, предположим, что вы хотите скомпилировать программу, показанную в примере 1.27. Она содержит три файла.

• Файл plus.hpp содержит объявление экспортируемого шаблона функции plus() .

• Файл plus.cpp содержит определение plus() .

• Файл test.cpp включает объявление — но не определение — plus() и определяет функцию main() , использующую plus() .

Пример 1.27. Простая программа, использующая экспортируемые шаблоны

#endif // #ifndef PLUS_HPP_INCLUDED

T plus(const T& lhs, const T& rhs) <

return rhs + lhs;

Чтобы скомпилировать plus.cpp в объектный файл plus.obj с помощью Comeau в Unix, перейдите в директорию, содержащую plus.cpp, plus.hpp и test.cpp, и введите следующую команду.

$ como -c —export plus.cpp

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

Затем скомпилируйте test.cpp в объектный файл test.obj с помощью команды:

$ como -c —export test.cpp

Наконец, скомпонуйте исполняемый файл test.exe.

$ como —export -о test test.obj

Две последние команды также можно объединить в одну.

$ como —export -o test test.cpp

Теперь можно запустить test.exe.

$ ./test

Теперь предположите, что файлы plus.hpp и plus.cpp находятся в директории с именем plus, a test.cpp находится в дочерней директории test. Чтобы скомпилировать и скомпоновать test.cpp, перейдите в директорию test и введите:

$ como —export —template_directory=../plus -I../plus -o test

C++ поддерживает две модели обеспечения определений шаблонов функций и статических данных-членов шаблонов классов: включающую (inclusion model) и раздельную (separation model) модели. Включающая модель знакома всем программистам, регулярно использующим шаблоны С++, но часто оказывается непонятной программистам, привыкшим писать код без шаблонов. Во включающей модели определение шаблона функции — или статических данных-членов шаблона класса — должно включаться в каждый исходный файл, ее использующий. В противоположность этому при использовании нешаблонных функций и данных достаточно включить в исходный файл только объявление; определение может быть помещено в отдельный файл .cpp.

Раздельная модель ближе к традиционной манере организации исходного кода C++. Для шаблонов, объявленных с ключевым словом export , не требуется включать определения во все исходные файлы, их использующие. Вместо этого определения помещаются в отдельные файлы .cpp. Однако параллель с традиционной организацией кода не полная, так как даже несмотря на то, что код, использующий экспортируемый шаблон, не требует включения его определения, он зависит от определения.

Раздельная модель предлагает несколько потенциальных преимуществ.

Уменьшение времени компиляции

Время компиляции при использовании раздельной модели может сократиться благодаря тому, что сканирование определений шаблонов производится реже, и потому, что раздельная модель уменьшает зависимости между модулями.

Снижение «загрязнения» символов

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

Возможность поставлять скомпилированные реализации шаблонов.

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

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

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

Как экспортировать классы C++ из DLL

Written on 24 Января 2014 .

ОГЛАВЛЕНИЕ

Назначение этой статьи – показать несколько методов экспорта классов C++ из модуля DLL. Исходный код демонстрирует разные приемы экспорта воображаемого объекта Xyz.

Введение

Динамически подключаемые библиотеки (DLL) являются составной частью платформы Windows с самого ее начала. DLL позволяют инкапсулировать часть функционала в автономном модуле с точным списком функций C, доступных внешним пользователям. В 1980-е годы, когда Windows DLL были введены, единственным практичным вариантом разговора с широкой аудиторией разработчиков был язык C. Поэтому Windows DLL предоставляли свой функционал в виде функций и данных C. Внутренне DLL может быть реализована на любом языке, но для использования из других языков и сред интерфейс DLL должен вернуться к наименьшему общему знаменателю – языку C.

Использование интерфейса C не означает автоматически, что разработчик должен отказаться от объектно-ориентированного подхода. Даже интерфейс C можно использовать для истинного объектно-ориентированного программирования, хотя это трудоемкое дело. Неудивительно, что второй наиболее используемый язык программирования, а именно C++, стал жертвой искушения DLL. Однако, в отличие от языка C, где двоичный интерфейс между вызывающей программой и вызываемой программой точно определен и общепринят, в C++ нет признанного двоичного интерфейса приложений (ABI). На деле это значит, что двоичный код, генерируемый компилятором C++, несовместим с другими компиляторами C++. Более того, двоичный код одного и того же компилятора C++ может быть несовместим с другими версиями этого компилятора. Все это затрудняет экспорт классов C++ из DLL.

Назначение этой статьи – показать несколько методов экспорта классов C++ из модуля DLL. Исходный код демонстрирует разные приемы экспорта воображаемого объекта Xyz. Объект Xyz очень простой и имеет только один метод: Foo.

Ниже приведена схема объекта Xyz:

Xyz
int Foo(int)

Реализация объекта Xyz находится внутри DLL, распространяемой по широкому диапазону клиентов. Пользователь может получить доступ к функционалу Xyz путем:
• Использования чистого C
• Использования обычного класса C++
• Использования абстрактного интерфейса C++

Исходный код состоит из двух проектов:
• XyzLibrary – проект библиотеки DLL
• XyzExecutable – консольная программа Win32, использующая «XyzLibrary.dll»

Проект XyzLibrary экспортирует свой код посредством следующего удобного макроса:

Имя XYZLIBRARY_EXPORT определено только для проекта XyzLibrary, поэтому макрос XYZAPI расширяется в __declspec(dllexport) для сборки DLL и в __declspec(dllimport) для сборки клиента.

Подход языка C

Описатели

Классический подход языка C к объектно-ориентированному программированию – использование непрозрачных указателей, т.е. описателей. Пользователь вызывает функцию, создающую объект внутри и возвращающую описатель для этого объекта. Далее пользователь вызывает различные функции, принимающие описатель в качестве параметра, и выполняет всевозможные операции над объектом. Хороший пример использования описателя – API работы с окнами Win32, представляющее окно с помощью описателя HWND. Воображаемый объект Xyz экспортируется через интерфейс C следующим образом:

Ниже приведен пример, как может выглядеть клиентский код C:

При таком подходе DLL должна предоставлять явные функции для создания и удаления объекта.

Соглашения о вызовах

Важно не забыть указать соглашение о вызовах для всех экспортируемых функций. Пропущенное соглашение о вызовах – широко распространенная ошибка, допускаемая многими новичками. Пока стандартное соглашение о вызовах клиента совпадает с соглашением о вызовах DLL – все работает. Но как только клиент изменит свое соглашение о вызовах, разработчик не замечает этого до появления аварийных завершений при выполнении. Проект XyzLibrary использует макрос APIENTRY, определенный как __stdcall в заголовочном файле «WinDef.h».

Безопасность исключений

Никакому исключению C++ не позволяется пересечь границу DLL. Язык C ничего не знает об исключениях C++ и не способен правильно обработать их. Если методу объекта надо сообщить об ошибке, следует использовать код возврата.

Плюсы
• DLL может использоваться максимально широкой аудиторией программистов. Почти все современные языки программирования поддерживают взаимодействие с функциями чистого C.
• Библиотеки времени выполнения C из DLL и из клиента не зависят друг от друга. Так как получение и освобождение ресурса происходит полностью внутри модуля DLL, на клиента не влияет выбор CRT в DLL.

Минусы
• Обязанность вызова нужных методов для нужного экземпляра объекта лежит на пользователе DLL. Например, в следующем фрагменте кода компилятор не сможет уловить ошибку:

• Требуются явные вызовы функции для создания и уничтожения экземпляров объекта. Это особенно неприятно для удаления экземпляра. Функция клиента обязана кропотливо вставлять вызов XyzRelease во всех точках выхода из функции. Если разработчик забывает вызвать XyzRelease, то происходит утечка ресурсов, потому что компилятор не отслеживает время жизни экземпляра объекта. Языки программирования, поддерживающие деструкторы или имеющие сборщик мусора, могут смягчить эту проблему, делая обертку над интерфейсом C.
• Если методы объекта возвращают или принимают другие объекты в качестве параметров, то автор DLL обязан предоставить надлежащий интерфейс C для этих объектов. Альтернатива – вернуться к наименьшему общему знаменателю, то есть языку C, и использовать только встроенные типы (например, int, double, char*, и т.д.) в качестве возвращаемых типов и параметров методов.


Примитивный подход C++: экспорт класса

Почти любой современный компилятор C++, существующий на платформе Windows, поддерживает экспорт класса C++ из DLL. Экспорт класса C++ походит на экспорт функций C. Разработчику необходимо использовать спецификатор __declspec(dllexport/dllimport) перед именем класса, если надо экспортировать целый класс, или перед объявлениями методов, если надо экспортировать лишь конкретные методы класса. Ниже приведен фрагмент кода:

Не надо явно указывать соглашение о вызовах для экспорта классов или их методов. По умолчанию компилятор C++ использует соглашение о вызовах __thiscall для методов класса. Однако из-за разных схем украшения имен, применяемых разными компиляторами, экспортированный класс C++ может использоваться только тем же самым компилятором и той же самой версией компилятора. Ниже приведен пример украшения имен, используемого компилятором MS Visual C++:

Обратите внимание, что украшенные имена отличаются от оригинальных имен C++. Далее приведен снимок экрана того же самого модуля DLL с расшифровкой украшения имен инструментом иллюстратор зависимостей:

Только компилятор MS Visual C++ может использовать эту DLL. Код DLL и клиента должны быть скомпилированы с помощью одной и той же версии MS Visual C++, чтобы гарантировать совпадение схемы украшения имен между вызывающей программой и вызываемой программой. Ниже приведен пример кода клиента, использующего объект Xyz:

Как видно, экспортированный класс используется так же, как и любой другой класс C++. Ничего особенного.

Важно: Использование DLL, экспортирующей классы C++, не отличается от использования статической библиотеки. Все правила, применяемые к статической библиотеке, содержащей код C++, полностью применимы к DLL, экспортирующей классы C++.

Получишь не то, что видишь

Внимательный читатель уже заметил, что инструмент иллюстратора зависимостей показывает дополнительный экспортированный член, то есть оператор присваивания CXyz& CXyz::operator =(const CXyz&). Это работает богатство C++. По стандарту C++, каждый класс имеет четыре специальных функции-члена:
• Конструктор по умолчанию
• Конструктор копий
• Деструктор
• Оператор присваивания (оператор =)

Если автор класса не объявляет и не предоставляет реализацию этих членов, то компилятор C++ объявляет их и генерирует неявную реализацию по умолчанию. В случае класса CXyz компилятор решил, что конструктор по умолчанию, конструктор копий и деструктор достаточно тривиальны, и оптимизировал их. Однако оператор присваивания пережил оптимизацию и был экспортирован из DLL.

Важно: Пометка класса как экспортируемого посредством спецификатора __declspec(dllexport) приказывает компилятору попытаться экспортировать все, что связано с классом. Это включает в себя всех членов данных класса, всех членов-функций класса (явно объявленных или неявно сгенерированных компилятором), все базовые классы класса и все их члены. Рассмотрите:

Во фрагменте кода выше компилятор предупредит о неэкспортированном базовом классе и неэкспортированном классе члена данных. Чтобы успешно экспортировать класс C++, разработчику необходимо экспортировать все важные базовые классы и все классы, используемые для определения членов данных. Это требование массового экспорта является существенным изъяном. Поэтому, например, очень трудно и утомительно экспортировать классы, производные от шаблонов STL, или использовать шаблоны STL как члены данных. Создание экземпляра контейнера STL вроде std::map<>, например, может требовать экспортирования десятков дополнительных внутренних классов.

Безопасность исключений

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

Плюсы
• Экспортированный класс C++ используется так же, как любой другой класс C++.
• Исключение, сгенерированное внутри DLL, перехватывается клиентом без проблем.
• При внесении только небольших изменений в модуль DLL не требуется собирать другие модули. Это очень выгодно для больших проектов, где задействуется огромное количество кода.
• Выделение логических модулей в большом проекте в модули DLL является первым шагом на пути к истинному выделению модулей. Это полезная деятельность, повышающая модульность проекта.

Минусы
• Экспорт классов C++ из DLL не избавляет от очень сильной связи между объектом и его пользователем. DLL следует воспринимать как статическую библиотеку в отношении зависимостей кода.
• Код и клиента, и DLL должен динамически компоноваться с помощью одной и той же версии CRT. Это необходимо для обеспечения правильного учета ресурсов CRT между модулями. Если клиент и DLL компонуются с разными версиями CRT, или компонуются с CRT статически, то ресурсы, полученные в одном экземпляре CRT, придется освобождать в другом экземпляре CRT. Это повредит внутреннее состояние экземпляра CRT, пытающегося производить действия над чужими ресурсами, и, скорее всего, приведет к краху.
• И код клиента, и DLL должны договориться о модели обработки/распространения исключения и использовать одни и те же настройки компилятора в отношении исключений C++.
• Экспорт класса C++ требует экспорта всего, что связано с этим классом: всех его базовых классов, всех классов, используемых для определения членов данных, и т.д.

Зрелый подход C++: использование абстрактного интерфейса

Абстрактный интерфейс C++ (т.е. класс C++, содержащий только чисто виртуальные методы и никаких членов данных) пытается получить лучшее из двух миров: независимый от компилятора чистый интерфейс для объекта и удобный объектно-ориентированный способ вызовов методов. Надо предоставить заголовочный файл с объявлением интерфейса и реализовать функцию-фабрику, возвращающую вновь созданные экземпляры объекта. Функция-фабрика должна быть объявлена со спецификатором __declspec(dllexport/dllimport). Интерфейс не требует никаких дополнительных спецификаторов.

Во фрагменте кода выше функция-фабрика GetXyz объявлена как extern «C». Это требуется для предотвращения искажения имени функции. Эта функция предоставляется как обычная функция C и легко распознается любым совместимым с C компилятором. Так выглядит код клиента при использовании абстрактного интерфейса:

C++ не предоставляет специального обозначения для интерфейса, как делают другие языки программирования (например, C# или Java). Но это не значит, что C++ не может объявлять и реализовывать интерфейсы. Типовой подход к созданию интерфейса C++ — объявить абстрактный класс без членов данных. Еще один отдельный класс наследуется от интерфейса и реализует методы интерфейса, но реализация скрыта от клиентов интерфейса. Клиент интерфейса не знает и не заботится о том, как реализован интерфейс. Он только знает, какие методы доступны и что они делают.

Как это работает

Основой этого подхода является очень простая идея. Лишенный членов класс C++, состоящий из чисто виртуальных методов, является всего лишь виртуальной таблицей, т.е. массивом указателей функций. Этот массив указателей функций заполняется внутри DLL тем, чем автор считает нужным заполнить. Далее этот массив указателей используется вне DLL для вызова фактической реализации. Ниже приведена схема, иллюстрирующая использование интерфейса IXyz.

Нажмите на изображение, чтобы просмотреть полноразмерную схему в новом окне:

Схема выше показывает интерфейс IXyz, используемый модулями DLL и EXE. Внутри модуля DLL класс XyzImpl наследуется от IXyz и реализует его методы. Вызовы методов в модуле EXE вызывают фактическую реализацию в модуле DLL через виртуальную таблицу.

Почему это работает с другими компиляторами

Краткое объяснение: потому что технология COM работает с другими компиляторами. Теперь длинное объяснение. Использование лишенного членов абстрактного класса как интерфейса между модулями – именно то, что COM делает для предоставления интерфейсов COM. Понятие виртуальной таблицы из языка C++ отлично встраивается в спецификацию стандарта COM. Это не совпадение. Язык C++, будучи преобладающим языком разработки более 10 лет, широко применялся в программировании COM вследствие естественной поддержки объектно-ориентированного программирования в языке C++.

Неудивительно, что Microsoft счел язык C++ основным мощным инструментом для промышленной разработки COM. Будучи владельцем технологии COM, Microsoft позаботился о том, чтобы двоичный стандарт COM и их собственная реализация объектной модели C++ в компиляторе Visual C++ совпадали с минимальными издержками.

Естественно, что другие разработчики компиляторов C++ присоединились к победителям и реализовали формат виртуальной таблицы в своих компиляторах так же, как сделал Microsoft. Все хотели поддерживать технологию COM и обеспечить совместимость с существующим решением от Microsoft. Гипотетический компилятор C++, не поддерживающий COM эффективно, обречен на забвение на рынке Windows. Поэтому сейчас предоставление класса C++ из DLL через абстрактный интерфейс надежно работает во всех достойных компиляторах C++ на платформе Windows.

Использование интеллектуального указателя

Чтобы обеспечить правильное освобождение ресурса, абстрактный интерфейс предоставляет дополнительный метод для уничтожения экземпляра. Вызов этого метода вручную трудоемок и подвержен ошибкам. Эта ошибка широко распространена в сфере C, где разработчик должен не забыть освободить ресурсы путем явного вызова функции. Поэтому типичный код C++ использует идиому RAII(получение ресурса является инициализацией) с помощью интеллектуальных указателей. Проект XyzExecutable использует шаблон AutoClosePtr, предоставленный с примером. Шаблон AutoClosePtr является простейшей реализацией интеллектуального указателя, вызывающего произвольный метод класса для уничтожения экземпляра вместо operator delete. Ниже приведен фрагмент кода, демонстрирующий использование интеллектуального указателя вместе с интерфейсом IXyz:

Использование интеллектуального указателя обеспечивает правильное освобождение объекта Xyz несмотря ни на что. Функция может завершиться досрочно из-за ошибки или внутреннего исключения, но язык C++ гарантирует, что деструкторы всех локальных объектов будут вызваны при завершении.

Использование стандартных интеллектуальных указателей C++

Последние версии MS Visual C++ предоставляют интеллектуальные указатели вместе со стандартной библиотекой C++. Ниже приведен пример использования объекта Xyz вместе с классом std::shared_ptr:

Безопасность исключений

Так же, как интерфейсу COM не разрешено выдавать никакие внутренние исключения, абстрактный интерфейс C++ не вправе позволять никаким внутренним исключениям прорываться через границы DLL. Методы класса должны использовать коды возврата для указания ошибки. Реализация обработки исключений C++ очень специфичная для каждого компилятора и не может разделяться. Значит, в этом смысле абстрактный интерфейс C++ должен вести себя как простая функция C.

Плюсы
• Экспортированный класс C++ используется через абстрактный интерфейс с любым компилятором C++.
• Библиотеки времени выполнения C из DLL и из клиента независимы друг от друга. Так как получение и освобождение ресурса происходит полностью внутри модуля DLL, на клиента не влияет выбор CRT в DLL.
• Достигается истинное выделение модуля. Полученный модуль DLL можно переработать и пересобрать, не влияя на остальной проект.
• Модуль DLL легко преобразуется в полноценный модуль COM при необходимости.

Минусы
• Требуется явный вызов функции для создания нового экземпляра объекта и для его удаления. Однако интеллектуальный указатель избавляет разработчика от второго вызова.
• Метод абстрактного интерфейса не может возвращать или принимать обычный объект C++ в качестве параметра. Это должен быть встроенный тип (например, int, double, char*, и т.д.) или другой абстрактный интерфейс. Это же ограничение действует для интерфейсов COM.

Как насчет шаблонных классов STL?

Контейнеры стандартной библиотеки C++ (например, vector, list или map) и другие шаблоны не были спроектированы с расчетом на DLL. Стандарт C++ умалчивает о DLL, потому что это зависящая от платформы технология, не обязательно присутствующая на других платформах, где используется язык C++. Сейчас компилятор MS Visual C++ может экспортировать и импортировать создания экземпляров классов STL, которые разработчик явно помечает спецификатором __declspec(dllexport/dllimport). Компилятор выдает пару неприятных предупреждений, но это работает. Однако надо помнить, что экспорт созданий экземпляров шаблона STL ничем не отличается от экспорта обычных классов C++ со всеми сопутствующими ограничениями. В этом смысле нет ничего особенного с STL.

Резюме

Статья рассмотрела разные методы экспорта объекта C++ из модуля DLL. Дано подробное описание плюсов и минусов каждого метода. Описаны принципы безопасности исключений. Сделаны следующие выводы:
• Плюс экспорта объекта в виде набора простых функций C в совместимости с широчайшим спектром сред разработки и языков программирования. Однако пользователю DLL приходится использовать устаревшие приемы C или обеспечивать дополнительные обертки над интерфейсом C, чтобы использовать современные парадигмы программирования.
• Экспорт обычного класса C++ ничем не отличается от предоставления отдельной статической библиотеки с кодом C++. Применение очень простое и знакомое; но есть сильная связь между DLL и ее клиентом. Должна использоваться одна и та же версия одного и того же компилятора C++ и для DLL, и для ее клиента.
• Объявление абстрактного класса без членов и его реализация внутри модуля DLL пока является лучшим подходом к экспорту объектов C++. Этот метод обеспечивает чистый, строго определённый объектно-ориентированный интерфейс между DLL и ее клиентом. Такая DLL может использоваться вместе с любым современным компилятором C++ compiler на платформе Windows. Использовать интерфейс вместе с интеллектуальными указателями почти столь же легко, как и использовать экспортированный класс C++.

Язык программирования C++ является мощным, универсальным и гибким инструментом разработки.

В чем разница между импортом выражения функции или объявлением функции из модуля ES6?

Как я понимаю (см. раздел 16.3.2.1), ES6 допускает различные синтаксисы для операндов экспорта функций/классов. Разница заключается в том, следует ли интерпретировать экспортируемую функцию при импорте в качестве объявления функции, и в этом случае вы пишете: export default function () <> // (a) или как выражение функции: export default (function () <>); // (b) .

В качестве возможного связанного сиденита: я читал, что импорт поднимается, но я не совсем уверен, что это означает в этом контексте.

Взяв пример этого примера:

import foo from ‘my_module’; // (c)

Как я понимаю, приведенный выше оператор сохранит мою экспортированную функцию в переменной foo . Является ли эта переменная поднятой, или что есть, и когда?

Самое главное, какая разница (с точки зрения установки foo ), когда my_module экспортирует функцию с помощью (a) и когда она экспортирует ее с помощью (b) ?

Ваш вопрос немного запутан, но я постараюсь все объяснить.

Сначала определим, как работают модули в целом. Модуль имеет набор экспортированных имен, каждый из которых ссылается на локальную переменную в этом модуле. Имя экспорта не должно быть таким же, как имя локальной привязки. Одним из экспортируемых имен может быть default , для которого существует специальный синтаксис (оба в экспорте и импорте), предназначенный для случая, когда модуль экспортирует только одну вещь.

Я читал, что импортируются, но я не уверен, что это означает в этом контексте:

Да, импортные объявления поднимаются. Аналогично var или function (и фактически как любое другое объявление) идентификатор foo доступен с самого начала, перед любыми утверждениями в модуле выполняются. Фактически привязка даже создается до тех, что объявлены var iables.

Разница в том, как они инициализируются:

  • var инициализируются с помощью undefined
  • function и function* инициализируются объектом функции
  • let , const и class es остаются неинициализированными
  • импортированные привязки даже не инициализированы, они создаются как указатель на локальную переменную, на которую ссылается экспортированное имя в импортированном модуле
  • импортированные модули ( import * as … ) инициализируются объектом модуля (свойства которого также являются такими указателями)

Когда foo установлен для ссылки на мою экспортированную функцию?

Короткий ответ: перед всем остальным.

Длинный ответ: он действительно не установлен. Это ссылка на локальную переменную в импортированном модуле, которую вы ожидаете сохранить. Локальная переменная может измениться, если она не const , но мы обычно этого не ожидаем. И обычно он уже содержит эту функцию, потому что импортированный модуль полностью оценивается до модуля (ов), который его импортирует. Так что если вы боитесь, что проблема с var functionName = function() <> vs function functionName() <>, вы можете освободиться — нет.

Теперь вернемся к вашему названию вопроса:

В чем разница между экспортом выражения функции и объявлением функции в модуле ES6?

Ничего особенного, два аспекта на самом деле не имеют особого отношения друг к другу:

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

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


Хорошо, последние две объявления экспорта по умолчанию на самом деле немного отличаются от первых двух. Локальный идентификатор, связанный с экспортированным именем default , не является foo , но *default* — его нельзя переназначить. Это имеет смысл в последнем случае (где нет имени foo ), но во втором-последнем случае вы должны заметить, что foo — это действительно просто локальный псевдоним, а не сама экспортированная переменная. Я бы рекомендовал не использовать этот шаблон.

О, и прежде чем вы спросите: Да, последний экспорт по умолчанию действительно является объявлением функции, а не выражением. Анонимное объявление функции. Это новое с ES6: -)

В чем же разница между export default function () <> и export default (function () <>);

Они почти одинаковы для всех целей. Это анонимные функции с .name свойством «default» , которые хранятся в этой специальной привязке *default* , к которой указывает экспортированное имя default для анонимных значений экспорта.
Их единственное различие заключается в подъеме — декларация получит свою функцию, созданную в верхней части модуля, выражение будет оцениваться только после того, как выполнение кода модуля достигнет инструкции. Однако, учитывая, что для них нет переменной с доступным именем, это поведение не наблюдается, за исключением особого особого случая: модуля, который импортирует себя. Хм, да.

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

В этом случае, почему есть оба синтаксиса?

Случается. Это разрешено спецификацией, потому что это не делает лишних исключений, чтобы запретить некоторые бессмысленные вещи. Он не предназначен для использования. В этом случае спецификация даже явно запрещает выражения function и class в выражениях export default и обрабатывает их как декларации. Используя оператор группировки, вы обнаружили лазейку. Отлично сработано. Не злоупотребляйте им.

Функции. Объявление и определение функций

Функция – это именованная последовательность описаний и операций, выполняющая законченное действие

— может принимать параметры и возвращать значения

— должна быть объявлена и определена. Объявление должно находиться по тексту раньше точки ее вызова

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

формат: [класс] тип имя ([список параметров])

Класс задает область видимости функции:

extern – глобальная видимость

static – видимость модуля (локальная)

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

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

модификатор inline – рекомендует компилятору вместо обращения к функции подставить ее код в точку вызова

(inline int funcl();)

c=sum(a,b); return 0;>

int sum(int x, int y)

Использование статических локальных переменных:

f2(10); f2(4,5); f2(4,10,”Vasia”);

Рекурсивные функции. Функция main().

-рекурсивной называется функция, которая вызывает саму себя

long func(long n)

2) тип main(int argc, char *argv[])

int argc – количество параметров

char *argv[] – текстовое описание параметров

тип – void или int

Перегрузка функций.

Перегрузка функций — использование нескольких функций с одним и тем же именем, но с различными параметрами.

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

Если точного соответствия аргументов и параметров перегруженной функции не найдено:

1) производится попытка преобразования коротких типов в более длинные в соответствии с общими правилами (bool, char=>int; float=>double)

2) выполняется стандартное преобразование типов (int=>double, указатели=>void)

3) преобразование типов заданное пользователем

Неоднозначность может появиться:

— при преобразовании типов аргументов

— при использовании параметров-ссылок

— при использовании параметров со значениями по умолчанию

Шаблоны функции.

Шаблон – средство параметризации.

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

teamplate заголовок функции

Сортировка методом выбора:

template void sort(T *b, int n)

— директива #include вставляет содержимое указанного файла в ту точку исходного файла, где она записана

— #include “имя файла” – поиск в каталоге, содержащем исходный файл

— заголовочные файлы обычно имеют расширение .h и могут содержать:

а) определения типов, констант, встроенных функций, шаблонов, перечислений

б) объявления функций, данных, имен, шаблонов

в) пространства имен

г) директивы препроцессора

— директива #define определяет подстановку в тексте программы

— используется для определения:

а) символических констант

#define имя текст_подстановки

#define имя(параметр) текст_подстановки

в) символов, управляющих условной компиляцией

#define VASIA “Василий Иванович”

y=MAX(sum1, sum2) будет заменен на

— отсутствие круглых скобок может привести к неправильному порядку вычисления

— удаляет определение символа, введенного директивой #define


Дата добавления: 2020-03-27 ; просмотров: 539 | Нарушение авторских прав

Объявление экспортируемых функций

Предполагается: знание Delphi на уровне использования DLL, а также написания собственных; знание С++ на уровне написания простейшего приложения в среде MS VC++.

Желательно: общее понимание соглашений о вызове функций; общее представление о способах передачи параметров и возврата значения.
Используемые инструменты: Borland Delphi 6, MS VC++ 6.0

2. Обоснование

Необходимость использования чужого кода в своей программе возникает регулярно. Вставка готовых удачных решений позволяет не изобретать велосипед заново. В хороших случаях чужой код написан на том же языке, что и свой, либо решение оформлено в виде DLL или компонента. Однако, бывают случаи похуже. Например, приобретается PCI-плата расширения с программным обеспечением для нее, а это ПО оказывается файлами исходного кода на С или С++, в то время как проект уже начат на Delphi, и, кроме того, в команде разработчиков С++ знают плохо.

3. Варианты решения

В принципе, можно весь проект писать на С++. Если такая возможность есть – не исключено, что это лучший выход. Но пользовательский интерфейс в Delphi разрабатывается быстрее, чем в MS VC++ (не только мое мнение, но хорошую цитату не нашел), кроме того, в группе могут плохо знать С++. И если даже С++ знают хорошо, но проект уже начат на Delphi, переписывать готовое – значит, тратить неоплачиваемое время.

Можно переписать код С++ на Delphi. Для этого требуется время, и, возможно, немалое, а главное – знание С++ на уровне существенно выше начального («читаю со словарем»). При этом, многие языковые конструкции С++ не имеют прямых аналогов в Delphi, и их перенос чреват появлением ошибок, в том числе, совершенно дурацких, и потому трудноотлавливаемых. В частности, прекрасный пример из обсуждения статьи «ЯП, ОПП и т.д. и т.п. в свете безопасности программирования»:

Можно попробовать засечь время и перевести это на Pascal. Станет примерно понятно, сколько времени уйдет на перевод класса, где подобные конструкции не единичны.

Можно воспользоваться интерфейсами и технологией СОМ (пожалуй, точнее – моделью СОМ и технологией ActiveX). Но – вот цитата из [1], глава «Модель многокомпонентных объектов»:
«И еще одно замечание: не думайте, что путь будет легким. Крейг Брокшмидт говорил, что перед тем, как он начал понимать эти концепции, у него был « шесть месяцев туман в голове.» Минимальная предпосылка – исчерпывающее знание языка С++.» Конец цитаты. И, хотя «модель СОМ предоставляет унифицированный, открытый, объектно-ориентированный протокол связи между программами» (цитата оттуда же), она требует такой квалификации от программиста, которая редко встречается в среде непрофессионалов.

Можно реализовать код, который необходимо использовать, в виде DLL. Один из существенных плюсов DLL – неважно, на каком языке она написана, если соблюдены некоторые условия, такие, как соглашение о вызове и совместимость типов.

С учетом того, что в группе разработчиков в основном о С++ поверхностные представления, а СОМ – незнакомая аббревиатура, и, при этом, срок сдачи проекта – традиционно – вчера, ничего лучше варианта с DLL у нас придумать не получилось.

4. Немного теории

Передать, точнее, экспортировать несколько функций из DLL – не проблема. Приводим типы, соглашения о вызовах, заполняем список экспортируемых функций – и всё (в основном). Об этом написано немало, например, в [2], в параграфе «Использование DLL, разработанных в С++».

Экспортировать класс несколько сложнее. Даже если и DLL, и основная программа написаны на Delphi, возникают проблемы с распределением памяти, которые решаются использованием модуля ShаreMem, первым в списке uses как проекта, так и DLL [2, 3]. Причем, этот модуль можно, в принципе, заменить самодельным менеджером памяти [там же, 3]. Но как использовать ShаreMem, если DLL написана на другом языке, или написать собственный менеджер для двух языков? Наверное, можно и так, но, напоминаю, срок сдачи проекта – вчера. Если есть и другие возражения, часто время – определяющий фактор.

Можно создавать экземпляр класса при загрузке DLL, ликвидировать при выгрузке (используя события DLL_PROCESS_ATTACH/DETACH), а для методов класса (функций-членов, раз уж класс на С++) написать однострочные функции-обертки, не являющиеся членами, а просто вызывающие соответствующие функции-члены. Некрасиво, и много лишней работы. Попробуем все же экспортировать класс.

В [2], сказано: «Библиотеки DLL не могут экспортировать классы и объекты, если только вы не используете специализированную технологию Microsoft под названием СОМ или какую-либо другую усовершенствованную технологию». Впрочем, там же есть замечание: «На самом деле объекты могут быть переданы из DLL в вызывающую программу в случае, если эти объекты спроектированы для использования в качестве интерфейсов или чистых абстрактных классов». Кроме этого замечания, в [2] об экспорте объектов почти всё, но уже хорошо, что есть шанс «сделать это по-быстрому».

И, наконец, в [4] находим параграф «Экспорт объектов из DLL». Там сказано: «К объекту и его методам можно получить доступ, даже если этот объект содержится внутри DLL. Но к определению такого объекта внутри DLL и его использованию предъявляются определенные требования. Иллюстрируемый здесь подход применяется в весьма специфических ситуациях, и, как правило, такого же эффекта можно достичь путем применения пакетов или интерфейсов». Наша ситуация вполне специфическая; пакеты здесь неприменимы, так как они все же для использования с Delphi, про использование интерфейсов и СОМ уже сказано, а использовать интерфейсы без СОМ вне Delphi, судя по [2], нельзя.

И, пожалуй, самое важное из [4]:
«На экспорт объектов из DLL накладываются следующие ограничения:

  1. Вызывающее приложение может использовать лишь те методы объекта, которые были объявлены как виртуальные.
  2. Экземпляры объектов должны создаваться внутри DLL.
  3. Экспортируемый объект должен быть определен как в DLL, так и в вызывающем приложении с помощью методов, определенных в том же порядке.
  4. Из объекта, содержащегося вутри DLL, нельзя создать объект-потомок.

Здесь перечислены лишь основные ограничения, но возможны и некоторые другие.»

Далее там рассказывается о работе с DLL, написанной в Delphi, но полученной информации достаточно для работы с DLL, создаваемой в MS VC++.

Мастер MS VC++ позволяет создать обычную (regular) DLL и DLL-расширение (extension). Обычная DLL может экспортировать только С-функции и не способна экспортировать С++-классы, функции-члены или переопределенные функции [1]. Стало быть, надо использовать DLL-расширение. Мастер создаст заготовку, затем в каталог проекта надо будет скопировать два файла – заголовочный и файл кода (*.h и *.cpp), содержащие класс, с экземпляром которого предстоит поработать. Затем подключить их к проекту DLL и немного доработать в соответствии с указанными ограничениями.

Во-первых, все экспортируемые открытые методы (ну, или функции-члены, как хотите) необходимо объявить с директивой __stdcall, по понятным причинам. Во-вторых, их также необходимо объявить виртуальными. Это делается для того, чтобы точки входа оказались записанными в таблицу виртуальных функций (VMT), через которую и будет осуществлятся экспорт-импорт. В-третьих, класс требуется объявить с макроопределенной директивой AFX_EXT_CLASS или AFX_CLASS_EXPORT, это синонимы. Сделанные изменения не влияют на работоспособность класса в ехе-проекте, даже директива экспортируемого класса.

Далее в файл .срр проекта DLL нужно добавить функции создания и ликвидации объекта. Пример в [4] обходится без функции ликвидации, видимо, потому, что в приведенном там примере и DLL и импортирующее приложение написаны на Delphi, так что можно освободить память методом Free, который есть у всех наследников TObject и отсутствует у объектов С++, не имеющих общего класса-предка. Функция создания объекта должна просто вызывать конструктор, передать ему полученные от приложения параметры и вернуть указатель на созданный объект. Функция ликвидации принимает указатель на объект и вызывает деструктор. И обязятельно вписать эти функции в список экспортируемых.

И всё! Пятнадцать минут работы, при минимальном знании С++. Остальное в Delphi.

В импортирующей программе необходимо объявить класс, содержащий виртуальные открытые функции в том же порядке. Также необходимо объявить сложные структуры данных, используемые в DLL и передаваемые через ее границу в любом направлении. Имеются в виду структуры С++, они же записи Паскаля. И, конечно же, нужно объявить импортируемые функции создания и уничтожения класса. Теперь для создания экземпляра класса вызывается соответствующая функция DLL, когда объект перестает быть нужным – снова вызывается соответствующая функция DLL, а методы вызываются традиционно – «Объект.Метод(Параметры)». При этом обзывать методы в Delphi можно как угодно, важен лишь порядок их следования и списки параметров.

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

Если в С++ параметр передается по значению, то и в Delphi тоже, если же по ссылке (то есть как указатель), то в Delphi такой параметр должен быть объявлен с ключевым словом var.

Чуть подробнее о параметрах и их типах. Практически везде, где говорится о DLL, упоминается, что, если хотите обеспечить совместимость DLL с программами на других языках, необходимо обеспечить совместимость типов. То есть, стремиться использовать стандартные типы ОС Windоws. Такие типы, как string или file вообще не совместимы с С++, с TDateTime можно поэкспериментировать, вообще-то, он соответствует стандарту, принятому в OLE-автоматизации ([3]). Опять же, [3] заявляет о соответствии типов single и double Delphi с float и double в С++ соответственно. Хотя в [5] есть такой совет со ссылкой на News Group: «Если вы создаете DLL не с помощью Delphi, то избегайте чисел с плавающей точкой в возвращаемом значении. Вместо этого используйте var-параметр (указатель или ссылочный параметр в С++) Причина кроется в том, что Borland и Microsoft применяют различные способы возврата чисел с плавающей точкой. Borland С++ и Delphi могут использовать один и тот же метод».

Тип С++ Байтов Тип Delphi
int ?(4) integer
unsigned int ?(4) cardinal
char, __int8 1 shortint
short, __int16 2 smallint
long, __int32 (int) 4 longint (integer)
__int64 8 int64
unsigned char 1 byte
unsigned short 2 word
unsigned long 4 longword
float 4 single
double 8 double
char * PChar

Таблица соответствия типов Delphi и С++

5. Немного практики

Для примера будет использован несложный и бесполезный класс на С++, состряпанный на ходу. В MS VC++ создадим проект, используя MFC AppWizard(exe), без использования представления «Документ-вид», на основе диалога, и обзовем его «example_exe». Добавим два новых файла – example.cpp и example.h.

В классе есть пара закрытых полей, закрытая функция-член, набор открытых функций. Конструктор принимает два параметра. Строковый параметр будем интерпретировать, как имя объекта. Функция Message нужна для отображения на экране хоть каких-то сообщений, демонстрирующих, что что-то происходит. Proc имитирует процедуру, то есть, не возвращает значения, зато изменяет что-то в программе, в нашем случае, переданный параметр. Func и есть функция, то есть, ничего не изменяет, зато вычисляет некоторое значение и возвращает его. Плюс здесь же установщик и считыватель закрытого поля, а также простенькая демонстрация работы со структурами.

Для примера более, чем достаточно. Теперь надо посмотреть, как это работает.

В файле Example_exeDlg.h в описании класса CExample_exeDlg где-нибудь в секции public надо вписать
то есть, объявить переменную-член, указатель на наш учебно-тренировочный класс, и в конструкторе Example_exeDlg вписать
Можно ex сделать и не членом, в принципе, и инициализировать при объявлении. И, конечно, не забыть наверху этого же файла вклеить заголовочный файл класса:

На диалоговую форму накидаем кнопок и создадим их обработчики: Если объект еще не создан – создаем и инициализируем пару закрытых полей.

Освобождаем память и устанавливаем указатель в «пусто»

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

Примерно то же самое – значение до выполнения, значение после выполнения и результат выполнения.

Эти две – без комментариев. Должно быть так все понятно. Функцию для работы со структурами в этом проекте не буду трогать, не интересно, тут весь фокус, как их передать через границу DLL. Кроме того, не будем возиться с полями ввода, а передадим параметры непосредственно в коде. Наглядность это уменьшает ненамного, а работы меньше. Еще момент – ID кнопок по-умолчанию поменял с BUTTON1 на BT_CREATE и так далее, для наглядности.

Всё! На форме только кнопки, вывод информации через MessageBox. Можно проверить работу.

Сделаем DLL для этого класса. В MS VC++ создадим проект, используя MFC AppWizard(dll), назовем «example_dll». В каталог этого проекта копируем готовые example.cpp и example.h, добавим их к проекту. Будем изменять, в соответствии с выясненными правилами. Начнем с объявления класса:

Затем из
делаем
и так со всеми открытыми методами, кроме конструктора и деструктора. И на этом бы всё, да CString – несовместимый, опасный тип. Меняем объявление:
и определение:
то есть, приходим к совместимому типу «указатель на нуль-терминальную строку», но, чтобы не потерять функциональность класса CString, объявляем локальную переменную этого класса и используем ее. Осталось еще полторы детали.
Первая деталь – в файле example_dll.cpp в конце пишем:

И половина детали – в файле EXAMPLE_DLL.def в конце дописываем пару строчек, так, чтобы получилось:

После компиляции DLL готова. Подготовим проект в Delphi, чтобы продемонстрировать ее работу. Создадим проект «Example_Delphi», и в модуле главной формы, перед объявлением класса формы, впишем четыре типа. Два — структуры struct1 и 2:

Третий – указатель на вторую структуру:

А четвертый – наш класс, с которым будем работать:

Директивы virtual и stdcall в пояснениях не нуждаются. О них сказано выше. А зачем abstract? Очень просто. Без нее компилятор будет ругаться на неправильное упреждающее объявление функции, ведь описания ее у нас нет, описание – в DLL. Директивы должны идти именно в этом порядке. Иначе компилятору не нравится.

Обратите внимание на первый метод. Остальные названы так же, как и в С++, но слово Message в Delphi зарезервированное, и использовать его не по назначению не стоит. Хорошо, назовем иначе, важно, что она стоит на первом месте среди виртуальных функций, как и в С++, значит, ее найдут по номеру в VMT. Имя роли не играет.

Еще надо добавить объявление экспортируемых функций создания/ликвидации, в конце секции interface:

Здесь предполагается, что DLL лежит там, где и появилась после компиляции, а директории «Example_dll» и «Example_Delphi» имеют общую родительскую. Больше нигде ее искать не будут. Если же указать только имя, приложение будет искать библиотеку в своей папке, папках WINDOWS, SYSTEM32 и прописанных в переменной окружения PATH. Впрочем, это азбука.

Всё, класс можно использовать. Давайте опять наделаем кнопок, а вывод в поле Memo, благо, в Delphi с ним работать быстрее и проще, чем в MS VС++.

Вот обработчики кнопок:

То же самое, что и в С++, и работает так же. Что и требовалось. И добавим кнопку для функции, которая принимает и возвращает структуры. Вот ее обработчик:

Что делает функция – понятно, тут другая тонкость. Конструктор возвращает (в коде на С++) указатель на класс, а мы присваиваем возвращаемое значение переменной, которая, вроде бы, не указатель. Struct1to2 тоже возвращает указатель – и его надо подготовить. Это объясняется в [3]: «Объект – это динамический экземпляр класса. Объект всегда создается динамически, в «куче», поэтому ссылка на объект фактически является указателем (но при этом не требует обычного оператора разыменования «^»). Когда вы присваиваете переменной ссылку на объект, Delphi копирует только указатель, а не весь объект. Используемый объект должен быть освобожден явно.»

А в С++ структура отличается от класса несколько меньше, и работа с ними почти одинакова.

И еще пара тонкостей. Если в DLL добавить еще виртуальную функцию-член, обязательно в конце, после имеющихся, такая DLL будет совместима со старой программой, где в абстрактном классе эта функция не объявлена. И если изменить имеющуюся функцию, добавив в конец параметров параметр по-умолчанию, такая DLL будет совместима со старой программой, где в абстрактном классе эта функция не имеет такого параметра.

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

6. Заключение

При таком подходе нельзя обращаться к полям данных напрямую. Хотя, это не проблема, можно использовать функции-считыватели и установщики. Нельзя использовать закрытые функции. А зачем их использовать? Для использования есть открытые. Те, кто знакомы с моделью СОМ, могут сказать, что это извращенный вариант нормальной технологии. Но для создания полноценного СОМ-сервера нужно несколько больше знаний. Показанный способ позволяет использовать открытые методы класса, не вдаваясь в подробности реализации класса и не затрачивая много времени. Это дает возможность быстро получить работоспособный вариант программы, и уже потом доводить ее до ума. А то и просто получить удовлетворительный результат.

7. Используемая литература

1.обратно Круглински Д., Уингоу С., Шеферд Дж. Программирование на Microsoft Visual C++ 6.0 для профессионалов
Пер. с англ. – СПб: Питер; М.:Издательско-торговый дом «Русская редакция», 2001 – 864 с.:ил.
2.обратно Кэнту М. Delphi 6 для профессионалов (+СD). – Питер, 2002. – 1088с.: ил.
3.обратно Лишнер Р. Delphi. Справочник. – Пер. с англ. – СПб: Символ-Плюс, 2001. – 640 с., ил.
4.обратно С. Тейксейра, К. Пачеко Delphi 5. Руководство разработчика.
5.обратно Озеров В. Delphi. Советы программистов. – СПб: Символ-Плюс, 2002.-912 с.,ил.

К материалу прилагаются файлы:

  • Проекты, используемые в качестве примера (276 K) обновление от 8/15/2006 4:15:00 AM

Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL