Объявление функции
До того, как функция будет вызвана, она должна быть объявлена.
Объявление функции, аналогично объявлению переменной, указываются имя функции, тип значения, которое она может возвращать, набор её параметров (для каждого параметра задаётся тип и, при желании, имя).
Объявление функции называют также её прототипом.
Тип_результата Имя_функции (Тип_пар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 – В чем разница между импортом выражения функции или объявления функции из модуля 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 переменной. Эта переменная поднята или что такое и когда?
самое главное, в чем разница (с точки зрения настройки 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++ из DLLWritten 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 Реализация объекта Xyz находится внутри DLL, распространяемой по широкому диапазону клиентов. Пользователь может получить доступ к функционалу Xyz путем: Исходный код состоит из двух проектов: Проект 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++ и не способен правильно обработать их. Если методу объекта надо сообщить об ошибке, следует использовать код возврата. Плюсы Минусы • Требуются явные вызовы функции для создания и уничтожения экземпляров объекта. Это особенно неприятно для удаления экземпляра. Функция клиента обязана кропотливо вставлять вызов XyzRelease во всех точках выхода из функции. Если разработчик забывает вызвать XyzRelease, то происходит утечка ресурсов, потому что компилятор не отслеживает время жизни экземпляра объекта. Языки программирования, поддерживающие деструкторы или имеющие сборщик мусора, могут смягчить эту проблему, делая обертку над интерфейсом C. Примитивный подход 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++ (т.е. класс 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. Плюсы Минусы Как насчет шаблонных классов STL?Контейнеры стандартной библиотеки C++ (например, vector, list или map) и другие шаблоны не были спроектированы с расчетом на DLL. Стандарт C++ умалчивает о DLL, потому что это зависящая от платформы технология, не обязательно присутствующая на других платформах, где используется язык C++. Сейчас компилятор MS Visual C++ может экспортировать и импортировать создания экземпляров классов STL, которые разработчик явно помечает спецификатором __declspec(dllexport/dllimport). Компилятор выдает пару неприятных предупреждений, но это работает. Однако надо помнить, что экспорт созданий экземпляров шаблона STL ничем не отличается от экспорта обычных классов C++ со всеми сопутствующими ограничениями. В этом смысле нет ничего особенного с STL. РезюмеСтатья рассмотрела разные методы экспорта объекта C++ из модуля DLL. Дано подробное описание плюсов и минусов каждого метода. Описаны принципы безопасности исключений. Сделаны следующие выводы: Язык программирования 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. Разница в том, как они инициализируются:
Короткий ответ: перед всем остальным. Длинный ответ: он действительно не установлен. Это ссылка на локальную переменную в импортированном модуле, которую вы ожидаете сохранить. Локальная переменная может измениться, если она не const , но мы обычно этого не ожидаем. И обычно он уже содержит эту функцию, потому что импортированный модуль полностью оценивается до модуля (ов), который его импортирует. Так что если вы боитесь, что проблема с var functionName = function() <> vs function functionName() <>, вы можете освободиться — нет. Теперь вернемся к вашему названию вопроса:
Ничего особенного, два аспекта на самом деле не имеют особого отношения друг к другу:
Конечно, до сих пор нет веских причин не использовать объявления декларативных функций повсюду; это не отличается в модулях ES6, чем раньше. Если вообще, может быть даже меньше причин использовать выражения функций, так как все покрывается декларациями: Хорошо, последние две объявления экспорта по умолчанию на самом деле немного отличаются от первых двух. Локальный идентификатор, связанный с экспортированным именем default , не является foo , но *default* — его нельзя переназначить. Это имеет смысл в последнем случае (где нет имени foo ), но во втором-последнем случае вы должны заметить, что foo — это действительно просто локальный псевдоним, а не сама экспортированная переменная. Я бы рекомендовал не использовать этот шаблон. О, и прежде чем вы спросите: Да, последний экспорт по умолчанию действительно является объявлением функции, а не выражением. Анонимное объявление функции. Это новое с ES6: -)
Они почти одинаковы для всех целей. Это анонимные функции с .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++. Желательно: общее понимание соглашений о вызове функций; общее представление о способах передачи параметров и возврата значения. 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, написанной в 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 и С++ 5. Немного практикиДля примера будет использован несложный и бесполезный класс на С++, состряпанный на ходу. В MS VC++ создадим проект, используя MFC AppWizard(exe), без использования представления «Документ-вид», на основе диалога, и обзовем его «example_exe». Добавим два новых файла – example.cpp и example.h. В классе есть пара закрытых полей, закрытая функция-член, набор открытых функций. Конструктор принимает два параметра. Строковый параметр будем интерпретировать, как имя объекта. Функция Message нужна для отображения на экране хоть каких-то сообщений, демонстрирующих, что что-то происходит. Proc имитирует процедуру, то есть, не возвращает значения, зато изменяет что-то в программе, в нашем случае, переданный параметр. Func и есть функция, то есть, ничего не изменяет, зато вычисляет некоторое значение и возвращает его. Плюс здесь же установщик и считыватель закрытого поля, а также простенькая демонстрация работы со структурами. Для примера более, чем достаточно. Теперь надо посмотреть, как это работает. В файле Example_exeDlg.h в описании класса CExample_exeDlg где-нибудь в секции public надо вписать На диалоговую форму накидаем кнопок и создадим их обработчики: Если объект еще не создан – создаем и инициализируем пару закрытых полей. Освобождаем память и устанавливаем указатель в «пусто» Показываем в последовательных сообщениях, какое значение переменная имела до выполнения процедуры, и какое стала иметь после. Примерно то же самое – значение до выполнения, значение после выполнения и результат выполнения. Эти две – без комментариев. Должно быть так все понятно. Функцию для работы со структурами в этом проекте не буду трогать, не интересно, тут весь фокус, как их передать через границу DLL. Кроме того, не будем возиться с полями ввода, а передадим параметры непосредственно в коде. Наглядность это уменьшает ненамного, а работы меньше. Еще момент – ID кнопок по-умолчанию поменял с BUTTON1 на BT_CREATE и так далее, для наглядности. Всё! На форме только кнопки, вывод информации через MessageBox. Можно проверить работу. Сделаем DLL для этого класса. В MS VC++ создадим проект, используя MFC AppWizard(dll), назовем «example_dll». В каталог этого проекта копируем готовые example.cpp и example.h, добавим их к проекту. Будем изменять, в соответствии с выясненными правилами. Начнем с объявления класса: Затем из И половина детали – в файле 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. Используемая литература
К материалу прилагаются файлы:
|