NET программирование
Главная Новости
Framework .NET
Visual Studio .NET Самоучитель C# Учебники О сайте
Что такое Microsoft .NET?
Основы технологии .NET
Программирование на управляемом C++
Объектно-ориентированное программирование на управляемом C++
Управляемый C++ в .NET Framework
Создание графических пользовательских интерфейсов
Сборки и развертывание
Классы каркаса .NET Framework
Программирование в ADO.NET
ASP.NET и Web-формы
Web-службы
Web-узлы и Web-службы,работающие на основе ATL Server
Защита
Трассировка и отладка в .NET
Смешивание управляемого и неуправляемого кода
Приложение. Visual Studio.NET
Объектно-ориентированное программирование на управляемом C++

В каждой новой версии Visual C++ компания Microsoft расширяет возможности языка во многих направлениях. Visual C++.NET не является исключением, поддерживая множество новых возможностей, для использования которых введены новые ключевые слова и атрибуты. В частности, появилась поддержка разработки кода на управляемом C++ для платформы .NET. В этой главе представлены несколько примеров, которые помогут читателю познакомиться с основными классами .NET Framework и приступить к самостоятельному написанию кода на управляемом C++. На примере использования класса Console (Консоль) продемонстрированы стандартные ввод и вывод, а кроме того, рассмотрены необычайно полезные классы String (Строка) и Array (Массив). Далее представлена программа управления системой бронирования гостиничных номеров, к которой мы еще не раз вернемся в следующих главах. Затем рассмотрены важные аспекты программирования на управляемом C++ для создания кода под платформу .NET: использование управляемых, неуправляемых, значимых (value), а также абстрактных типов, интерфейсы, упаковка и распаковка, делегаты, события, свойства и управляемые обработчики исключений. В заключение рассмотрены атрибуты C++ в контексте создания проектов ATL СОМ.

Одним из достоинств платформы .NET является то, что для разработки приложений, компонентов и сервисов на основе .NET можно использовать любой из широкого круга языков. Можно применять C++ с расширением управляемости, С# и VB.NET, созданные Microsoft, а также еще многие языки, разработанные другими компаниями. Но главное даже не то, что с помощью всех этих языков можно создавать приложения на основе .NET, a то, что во всех вопросах, относящихся к инициализации объектов, вызову методов, наследованию, обработке событий и даже обработке исключений, работа приложения не будет зависеть от языков реализации его составляющих. Это стало возможным благодаря тому, что языки .NET компилируются не на родной язык, а на общий промежуточный язык Intermediate Language (IL).

Как уже говорилось в предыду,щих главах, код, выполняющийся под управлением общеязыковой среды выполнения CLR (Common Language Runtime), называется управляемым кодом. Управляемый код отличается от обычного тем, что он компилируется не на родной набор инструкций ЦПУ, а в инструкции промежуточного языка IL, определенного платформой .NET. Промежуточный язык IL подобен обычному набору инструкций ЦПУ, отличаясь от последнего тем, что он изначально разрабатывался с поддержкой объектно-ориентированных и компонентно-ориентированных общих для языков черт, таких, как классы, объекты, методы, события и исключения. Благодаря тому, что исходный код, написанный на языках, поддерживающих .NET, компилируется в инструкции промежуточного языка IL, все эти языки являются полностью совместимыми.

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

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

При разработке управляемого кода на Visual C++ используются несколько новых ключевых слов, а расширение компилятора C++, позволяющее создавать приложения для .NET, вызывается с помощью параметра /CLR (Компиляция для выполнения в общеязыковой среде). Этот параметр указывает компилятору, что в конечном файле следует применять набор инструкций промежуточного языка IL, а не обычный набор инструкций процессора. Новые ключевые слова используются при создании управляемого кода и не поддерживаются при создании обычного неуправляемого кода. Хотя наличие или отсутствие параметра /CLR (Компиляция для выполнения в общеязыковой среде) полностью определяет, будет ли компилятор генерировать управляемый (на промежуточном языке IL) или неуправляемый код, можно задавать режим компиляции для отдельных частей программы. Это осуществляется с помощью прагм #pragma:

#pragma managed 
// Последующий код компилируется как управляемый 
ttpragma unmanaged 
// Последующий код компилируется как неуправляемый

Если задан параметр компилятора /CLR (Компиляция для выполнения в общеязыковой среде), то при отсутствии директив #pragma исходный код по умолчанию компилируется как управляемый. При отсутствии параметра /CLR (Компиляция для выполнения в общеязыковой среде) прагмы #pragma компилятором игнорируются, а код компилируется как неуправляемый.

Для использования возможностей расширения управляемости в исходный файл следует вставить директиву fusing с указанием сборки (assembly) mscorlib.dll, содержащей необходимую для работы управляемого кода информацию о типах. Такие сборки являются расширением для платформы .NET и обычно состоят из файлов DLL (или ЕХЕ). Кроме того, почти всегда определяется, что будет использовано пространство имен System (Системное пространство имен); это, однако, не обязательно для применения управляемого кода. Концепция пространств имен в C++ прямо копирует концепцию пространств имен многоязычной платформы .NET, представляющей собой иерархию имен. Эти два аспекта разработки кода для .NET обусловливают необходимость включения в начало исходного файла следующих двух строк:

fusing  
// Требуется для управляемого кода на C++ 
using namespace System; 
// используется пространство имен Система 
// Не требуется, но обычно используется

Директива препроцессора fusing похожа на директиву #import в прежних версиях Visual C++ тем, что делает доступной для компилятора информацию о типах. В случае директивы #import информация о типах содержалась в библиотеках типов, обычно являвшихся файлами TLB, DLL, OCX или ЕХЕ. В случае директивы #using информация о типах представлена в форме метаданных, содержащихся в сборке .NET. Сборка mscorlib.dll содержит информацию о типах, необходимую всем приложениям .NET, включая информацию о базовом классе, являющемся предком всех управляемых классов, — классе System: :0bject (Система::Объект). Заметим, что в такой записи System (Системное пространство имен) обозначает пространство имен, a Object (Объект) — имя корневого класса иерархии управляемых типов.

Хотя вы, почти наверняка, хорошо знакомы с C++, мы начнем с рассмотрения очень простого, но традиционного примера— программы HelloWorld (Привет, мир). В этом разделе мы расскажем, как написать, скомпилировать и запустить эту и другие программы.

Программа HelloWorld (Привет, мир)

Чуть ниже приведен пример кода из очень простой управляемой программы, которая выводит на консоль одну-единственную строку. Вы можете открыть сопровождающее решение [Как и для всех других примеров в данной книге, реализация программы HelloWorld доступна читателю в готовом виде. Исходные файлы этого проета находятся в папке C:\OI\NetCpp\Chap3\HelloWorld. Для того чтобы открыть его в Visual Studio, дважды щелкните на файле HelloWorld.sIn в Проводнике.] или создать свой проект и ввести текст программы самостоятельно. Для того чтобы это сделать, необходимо создать пустой проект HelloWorld (Привет, мир), добавить исходный код, а затем скомпилировать и запустить проект.

Как создать консольное приложение на управляемом C++

  • Создайте пустой проект консольного приложения Managed C++, называющийся HelloWorld (Привет, мир):
  • Откройте Visual Studio.NET. Выберите пункт меню File => New => Project (Файл => Создать => Проект) для того чтобы открыть диалог New Project (Создание проекта).
  • Выберите пункт Visual C++ Projects (Проекты Visual C++) в списке Project Types (Типы проектов).
  • Выберите пункт Managed C++ Empty Project (Пустой проект на управляемом C++) в списке Templates (Шаблоны).
  • Введите HelloWorld (Привет, мир) в качестве названия проекта.
  • Задайте папку, в которой будет храниться проект.
  • Щелкните на ОК для того чтобы закрыть диалог New Project (Создание проекта) и завершить создание нового проекта. Добавьте исходный код:
  • Щелкните правой кнопкой на папке Source Files (Исходные файлы) в окне Solution Explorer (Поиск решений).Выберите пункт меню Add => Add New Item (Добавить => Добавить новый элемент) для того, чтобы открыть диалог Add New Item dialog (Добавить новый элемент).
  • Выберите в списке Templates (Шаблоны) пункт C++ File (Файл C++).
  • Укажите HelloWorld (Привет, мир) в качестве названия проекта.
  • Не изменяйте значение расположения (Location), принятое по умолчанию.
  • Щелкните на кнопке Open (Открыть) для того, чтобы закрыть диалог Add New Item dialog (Добавить новый элемент) и открыть Source Editor (Редактор текстов программ).
  • Введите код примера HelloWorld (Привет, мир). Скомпилируйте и запустите проект:
  • Выберите пункт меню Build => Build (Создать => Создать).
  • Используйте сочетание клавиш Ctrl-F5 для запуска программы без отладчика.
  • Директива fusing необходима для всех программ на управляемом С++. Она делает доступными для компилятора стандартные типы (такие, как Console (Консоль) и Object (Объект)), определенные в библиотеке классов NET. Класс Console (Консоль) находится в пространстве имен System (Системное пространство имен) и его полное имя — System: : Console (Система::Консоль) Данный класс содержит метод WnteLine, выводящий на консоль текст и добавляющий к нему символ новой строки.
//HelloWorld.cpp 
fusing  // требуется для кода на управляемом Ст+ 
void main(void) { 
System: : Console : : WriteLme ( "Hello Wcrla'M ; 
// ("Привет, мир"); }

Программа может быть скомпилирована либо в Visual Studio.NET, либо при помощи командной строки с параметром /CLR (Common Language Runtime compilation — компиляция для выполнения в общеязыковой среде). Если вы используете командную строку. вы должны определить соответствующую среду Простейший способ сделать это — открыть командное окно, выбирая пункты меню Start (Пуск) => Programs (Программы) => Microsoft Visual Studio.NET 7.0 => Visual Studio.NET Tools => Visual Studio.NET Command Prompt. В командной строке

cl /CLR HelioWorld.cpp

исходный файл компилируется, а затем автоматически компонуется так, что результатом является ЕХЕ-файл HelloWorld.exe. Позже мы расскажем, как создать управляемую динамически подключаемую библиотеку (DLL).

Полученную управляемую программу можно запустить в Visual Studio.NET или из командной строки, как обычный исполняемый файл. Результатом работы программы будет следующее сообщение:

Hello World (Привет, мир)

Директива fusing делает доступной для компилятора информацию о типах, содержащуюся в сборке. Сборка содержит метаданные (описание информации о типах) и код на промежуточном языке IL. C6opKamscorlib.dll содержит описания многих полезных стандартных классов, определенных в .NET Framework, в том числе класса Console (Консоль), использовавшегося в предыдущем примере, и класса Object (Объект), который является базовым для всех управляемых классов. Добавим, что директива #us_ng совершенно не похожа на директиву #include, вставляющую в компилируемый файл некоторый другой исходный файл. Как отмечено выше, директива fusing скорее напоминает по совершаемым действиям директиву # import.

В предыдущем примере System (Системное пространство имен) предсташшет пространство имен C++, прямо соответствующее пространству имен .NET, имеющему то же название. Полное название класса состоит из названия пространства имен, за которым следуют два двоеточия и название класса, например, System: :Console (Система::Консоль) Хотя выражение using namespace, в предыдущем примере не используется, оно позволяет использовать короткие имена классов, например, Console (Консоль). Обратим ваше внимание на то, что выражение using namespace (определенное стандартом ANSI C++) и директива fusing (определенная в Microsoft C++) — совершенно разные вещи. Приведем пример использования выражения using namespace, позволяющего заменить полное имя System: : Console (Система.:Консоль) укороченным Console (Консоль):

//HelloKorld.cpp 
fusing  
using namespace System; 
// использовать пространство имен Система; 
// этот оператор позволяет использовать короткие имена классов 
void main(void) { 
Console::WriteLine("Hello World"); // "Привет, Мир" 
// пространство имен опущено 
}

Класс System:: Console (Система::Консоль) обеспечивает поддержку стандартного ввода-вывода. Метод ReadLine класса System: : Console (Система::Консоль) считывает введенную с клавиатуры строку как текстовую. С помощью методов Write (Запись) и WriteLine класса System: :Console (Система::Консоль) на консоль выводится текстовая строка, и, говоря о методе WriteLine, также символ новой строки. Проще всего ввод с консоли выполняется путем считывания в объект String (Строка) с последующим преобразованием в необходимый тип данных. Чтобы выполнить это преобразование можно использовать методы ТоХхх класса System: : Convert (Система::Преобразовать).

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

//ConvertTemp.срр 
fusing  
using namespace System; 
// использовать пространство имен Система; 
_gc class InputWrapper 
// класс сборщика мусора InputWrapper 
{ 
public: 
int getlnt(String *pprompt) // Строка 
{ 
Console::Write(pprompt); // Запись 
String *pbuf = Console::ReadLine(); // Строка 
return Convert::ToInt32(pbuf); // Преобразовать 
} 
double getDouble(String *pprompt) 
{ 
Console::Write(pprompt); // Запись 
String *pbuf = Console::ReadLine(); // Строка 
return Convert::ToDouble(pbuf); // Преобразовать 
} 
Decimal getDecimal(String *pprompt) // Десятичное число 
{ 
Console::Write(pprompt); // Запись 
String *pbuf = Console::ReadLine(); // Строка 
return Convert::ToDecimal(pbuf); // Преобразовать 
} 
String *getString(String *pprompt) // Строка 
{ 
Console::Write(pprompt); // Запись 
String *pbuf = Console::ReadLine(); // Строка 
return pbuf; 
} 
}; 
void main(void) 
{ 
InputWrapper *piw = new InputWrapper; 
int numTemp = piw->getlnt("How many temp's? "); // Сколько? 
for (int i = 0; i < numTemp; i++) 
{ 
int fahr = piw->getlnt("Temp. (Fahrenheit): "); // Фаренгейт 
int Celsius = (fahr - 32) * 5 / 9; // Цельсия 
Console::WriteLine ( 
"Fahrenheit = {0}", fahr.ToString()); // Фаренгейт 
Console::WriteLine("Celsius = {0}", _box(Celsius)); // Цельсия 
} 
}

Заметим, что первым аргументом метода WriteLine является форматирующая строка. Например, при первом вызове метода WriteLine форматирующая строка имеет вид "Fahrenheit={0} ", где {0} — заглушка, указывающая, что на это место следует вставить второй аргумент WriteLine. Число, помещенное в фигурные скобки, определяет, какой именно из следующих за форматирующей строкой аргументов следует вывести в указанном месте (естественно, нумерация начинается с нуля). В нашем примере это число — 0, так как за форматирующей строкой следует только один аргумент. Подставляемые аргументы могут быть нескольких типов, включая строки или упакованные значения, что и продемонстрировано в примере. Приведем пример работы программы, в котором преобразование температур производится два раза:

How many temp's? 2 Temp. (Fahrenheit): 212 Fahrenheit = 212 Celsius = 100 Temp. (Fahrenheit): 32 Fahrenheit = 32 Celsius = 0

Перевод такой [Добавлен редактором русского перевода. — Прим. ред.]:

Сколько температур? 2 Фаренгейта: 212 Фаренгейта =212 Цельсия = 100 Фаренгейта: 32 Фаренгейта = 32 Цельсия = О

В следующей программе продемонстрировано, как выводить данные в некоторых форматах с помощью метода WriteLine. Для этого применяются коды форматирования. Чтобы получить более подробную информацию о кодах форматирования, используемых в методе WriteLine (совпадающих, кстати, с кодами для метода string: : Format (Строка::Формат)), обратитесь к документации по .NET SDK.

//FormatString.cpp #using  
using namespace System; 
// использовать пространство имен Система; 
void main(void) { 
Console::WriteLine( 
"{0:C}, {1:D}, {2:E}, {3:F}, {4:G}, {5:N}, {6:X}", 
_box(lOO), // поле валюты (currency) 
_box(200), // десятичное число (decimal) 
_Ьох(ЗОО), // экспонента (exponent) 
_box(400), // с фиксированной точкой (fixed point) 
_box(SOO), // общий (general) 
_Ьох(бОО), // число (number) 
_box(700) // шестнадцатеричное (hexadecimal) 
); }

Вот выдача:

$100.00, 200, З.ООООООЕ+002, 400.00, 500, 600.00, 2ВС

Класс System:: String (Система::Строка) инкапсулирует как управляемый объект строку символов Unicode. Класс String (Строка) определен в пространстве имен System (Системное пространство имен) и является стандартной частью .NET Framework. Тип String (Строка) представляет собой конечный (sealed) класс; это означает, что он не может быть базовым для другого класса. Сам класс String (Строка) — производный от класса System: :Object (Система::Объект), являющегося основой иерархии классов .NET. Объект String (Строка) — неизменяемый, т.е. будучи однажды инициализированным, он не может быть изменен. Класс String (Строка) содержит методы, которые можно использовать для изменения объекта String (Строка), такие, как Insert (Вставка), Replace (Замена) и PadLeft. Однако, в действительности, указанные методы никогда не изменяют исходный объект. Вместо этого они возвращают новый объект String (Строка), содержащий измененный текст. Если вы хотите получить возможность изменять исходные данные, вам следует обратить внимание на класс StringBuilder, а не на сам класс String (Строка). В следующем фрагменте кода показано, что метод Replace (Замена) не влияет на содержимое исходного объекта String (Строка), но изменяет содержимое объекта StringBuilder:

//StringReplace.срр 
#using  
using namespace System; // для консоли и строк 
// использовать пространство имен Система; 
using namespace System::Text; // для StringBuilder 
// использовать пространство имен Система::Текст; 
void main(void) { 
Console::WriteLine("String is immutable:"); 
// ("Строка является неизменной: "); 
String *psl = S"Hello World"; // Строка "Привет, Мир" 
String *ps2 = psl->Replace('Н', 'J'); // Замена 
Console::WriteLine(psl); 
Console::WriteLine(ps2); 
Console::WriteLine("StringBuilder can be modified:"); // ("StringBuilder может изменяться: "); 
StringBuilder *psbl = new StringBuilder(S"Hello World"); // Привет, Мир 
StringBuilder *psb2 = psbl->Replace('H', 'J'); // Замена 
Console::WriteLine(psb1); 
Console::WriteLine(psb2); 
}

Информация, выведенная на экран профаммой, показывает, что действительно, содержимое объекта, на который указывает psl, не изменяется, т.е. метод Replace (Замена) не изменяет исходный объект String (Строка). С другой стороны, объект *psbl изменяется методом Replace (Замена).

String is immutable: Hello World Jello World StringBuilder can be modified: Jello World Jello World

Перевод такой [Добавлен редактором русского перевода. — Прим. ред.]:

Строка является неизменной:

Привет, Мир Jello Мир StringBuilder может измениться: Jello Мир Jello Мир

В приведенном выше фрагменте кода вы можете заметить строковые литералы, определенные с префиксом S и без него. Строковый литерал, определенный с использованием только кавычек, является указателем на char (символ), т.е. указателем на последовательность символов ASCII, заканчивающуюся нулем. Такой указатель не является указателем на объект String (Строка). А строковый литерал, определенный с префиксом S, является указателем на управляемый объект String (Строка). Префикс L, не использовавшийся в предыдущем примере, обозначает строку символов Unicode, которая также не является объектом String (Строка). Следующий фрагмент демонстрирует эти три типа строк:

char *psl = "ASCII string literal"; // неуправляемый 
// символ *psl = "строковый литерал ASCII "; 
_wchar_t *ps2 = L"Unicode string literal"; // неуправляемый 
// L " строковый литерал Уникода "; 
String *ps3 = S"String object literal"; // управляемый 
// Строка *ps3 = S " строковый литерал - объект String ";

Класс String (Строка) содержит много полезных методов. Так, для сравнения объектов можно использовать метод Equals (Равняется), что продемонстрировано в следующем примере. Подробнее о методах объекта String (Строка) можно узнать из документации по .NET SDK.

//Strings.срр 
fusing  
using namespace System; 
// использовать пространство имен Система; 
void main(void) { 
String *pstrl = new String ("hello"); 
// Строка *pstrl = новая Строка ("привет"); 
String *pstr2 = new String("hello"); 
// Строка *pstr2 = новая Строка ("привет"); 
if (pstrl->Equals(pstr2)) 
// если (pstrl-> Равняется (pstr2)) 
Console::WriteLine("equal"); // равны - выполняется else 
Console::WriteLine("not equal"); // не равный - не 
// выполняется if (pstrl==pstr2) // если (pstrl == pstr2) 
Console::WriteLine("equal"); // равны - не выполняется else 
Console::WriteLine("not equal"); // не равный - выполняется }

Результат работы программы показывает разницу между сравнением объектов String (Строка) с помощью метода Equals (Равняется) и оператора ==. Метод Equals (Равняется) проверяет равенство содержимого объектов, тогда как оператор == проверяет лишь равенство указателей (т.е. равенство адресов объектов в памяти).

Equal not equal

Вот перевод [Добавлен редактором русского перевода. — Прим. ред.]:

равны не равны

Метод ToString обеспечивает представление объекта String (Строка) для любого управляемого типа данных. Хотя метод ToString не является автоматически доступным для неуправляемых классов, он доступен для упакованных значимых и упакованных примитивных типов, таких, как int или float (с плавающей точкой). Упаковка и распаковка, также как значимые типы, управляемые и неуправляемые типы, будут рассмотрены ниже в этой главе.

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

//ToString.cpp 
#using  
using namespace System; 
// использовать пространство имен Система; 
_gc class ClassWithToString 
// класс сборщика мусора ClassWithToString 
{ 
public: 
String *ToString() // отмена { 
return new String("SomeClass - override");
// возвратить новую Строку ("SomeClass - отмена"); 
} 
}; 
_gc class ClassNoToString 
// класс сборщика мусора ClassNoToString 
{ 
//ToString унаследованный, без отмены 
}; 
void main(void) 
{ 
int i = 3; 
Console::WriteLine(i.ToString()); // перегрузка String* 
Console::WriteLine(i); // перегрузка int 
ClassWithToString *psc = new ClassWithToString; 
Console::WriteLine(psc->ToString()); // перегрузка String* 
Console::WriteLine(psc); // перегрузка Object* 
ClassNoToString *psoc = new ClassNoToString; 
Console::WriteLine(psoc->ToString()); // перегрузка String* 
Console::WriteLine(psoc); // перегрузка Object* 
int array _gc[]= new int _gc[5]; // массив сборщика мусора 
Console::WriteLine(array->ToString()); // перегрузка String 
// (Строка) 
Console::WriteLine(array); // перегрузка Object* 
}

Результат работы программы приведен ниже. Заметьте, что метод ToString можно вызывать явно как аргумент перегруженного метода WriteLine объекта String (Строка), а можно вызвать перегруженный метод WriteLine объекта String (Строка), который сам вызовет метод ToString. Заметьте также, что даже управляемый массив (который, на самом деле, является управляемым типом) поддерживает метод ToString.

3 3

SomeClass - override SomeClass - override ClassNoToString 
ClassNoToString System.Int32[] System.Int32[]

Все идентичные строковые литералы типа String (Строка) автоматически представляются указателями на объекты, являющиеся экземплярами одного класса String (Строка). Это справедливо для объектов, представленных строковыми литералами типа string (Строка), — такие объекты задаются с помощью взятой в кавычки строки. Однако это не справедливо для строковых объектов String (Строка), явно создаваемых с помощью оператора new (создать). Следующий пример подтверждает это. В нем сравниваются два указателя на объект String (Строка), созданных с помощью оператора new (создать). Выведенные на консоль результаты подтверждают, что два идентичных строковых объекта string (Строка), определенных как взятые в кавычки одинаковые последовательности символов, являются одним и тем же объектом (выражение pstrl==pstr2 истинно для строковых объектов String (Строка)). С другой стороны, два одинаковых строковых объекта string (Строка), созданных с помощью оператора new (создать), являются на самом деле разными объектами (выражение pstrl==pstr2 имеет значение false (ложь)).

//StringLiteral.срр 
#using  
using namespace System; 
// использовать пространство имен Система; 
void main(void) { 
String *pstrl; // Строка 
String *pstr2; // Строка 
// сравнение объектов - строковых литералов типа String 
pstrl = S"hello"; // привет 
pstr2 = S"hello"; // привет 
if (pstrl->Equals(pstr2)) // если (pstrl-> Равняется (pstr2)) 
Console::WriteLine("equal"); // равны - выполнен else 
Console::WriteLine("not equal");
// не равны - не выполнен if (pstrl==pstr2)
 // если (pstrl == pstr2) 

Console::WriteLine("equal"); // равны - выполнен else 
Console::WriteLine("not equal");
// не равны - не выполнен 
// сравнение новых объектов String (не литералов) pstrl = new String("hello"); 
// pstrl = новая Строка ("привет"); pstr2 = new String("hello");
// pstr2 = новая Строка ("привет"); if (pstrl->Equals(pstr2) )
// если (pstrl-> Равняется (pstr2)) 

Console::WriteLine("equal"); // равны - выполнен else 
Console::WriteLine("not equal");
// не равны - не выполнен if (pstrl==pstr2) 
// если (pstrl == pstr2) 

Console::WriteLine("equal"); // равны - не выполнен else 
Console::WriteLine("not equal"); // не равны - выполнен }

Программа напечатает:

equal equal equal not equal

Вот перевод [Добавлен редактором русского перевода. — Прим. ред.]:

равны равны равны не равны

Управляемые строковые литералы String (Строка) и неуправляемые строковые литералы ASCII и Unicode (благодаря автоматической упаковке) можно использовать в выражениях, в которых ожидается использование управляемого строкового объекта String (Строка). Однако управляемый строковый объект String (Строка) нельзя использовать там, где ожидается появление переменных неуправляемых типов. Следующий пример доказывает это. Обратите внимание на закомментированные строки. Не будучи закомментированными, они привели бы к сообщению об ошибке при компиляции.

//MixingStringTypes.срр 
fusing  
using namespace System; 
// использовать пространство имен Система; 

tinclude  // для wchar_t 
void ExpectingManagedString(String *str){}
// Строка *str void ExpectingASCIIString(char *str){}
// символ *str void ExpectingUnicodeString(wchar_t *str){} void main(void) { 
// ожидается управляемый тип 

ExpectingManagedString(S"hello"); // полное соответствие 
// привет ExpectingManagedString("hello"); // нет ошибки 
// привет 

ExpectingManagedString(L"hello"); // нет ошибки 
// привет 
// ожидается неуправляемый тип 

ExpectingASCIIString("hello"); // полное соответствие 
// привет //ExpectingASCIIString(S"hello"); // ошибка! 
// привет ExpectingUnicodeString(L"hello"); // полное соответствие 
// привет //ExpectingUnicodeString(S"hello"); // ошибка! 
// привет }

В отличие от массивов в обычном C++, которые являются простым типом указателя, управляемые массивы являются полноценными управляемыми объектами, расположенными в динамически распределяемой области. System: : Array (Система::Массив) — абстрактный класс, являющийся базовым для всех управляемых массивов. Для определения неуправляемых массивов можно использовать синтаксис обычного C++; для определения же управляемых массивов следует использовать либо ключевое слово _дс (сборщик мусора), либо указывать, что элементы массива относятся к управляемому типу. Далее приведены примеры определения массивов. Ключевое слово _дс (сборщик мусора) и управляемые типы подробнее рассмотрены ниже. Обратите внимание на две закомментированные строки, в которых при определении массива задается его величина. Величину массива можно задавать при определении неуправляемого (располагаемого в стеке) массива, но не при определении управляемого массива (располагаемого в динамически распределяемой области). Причина в том, что, подобно всем остальным управляемым типам, управляемый массив располагается в динамически распределяемой области, а не в стеке.

//ArraySyntax.срр 
fusing  
using namespace System; 
// использовать пространство имен Система; 
ttpragma warning(disable : 4101) 
// уничтожить предупреждение о переменной, на которую нет ссылки: 
// предупреждение (отключить: 4101) 
void main(void) { 
// традиционный синтаксис неуправляемого массива 
int *pintUnManagedArrayOnHeap = new int [5]; 
int intUnManagedArray[5]; // нет ошибки для неуправляемого 
// массива 
// синтаксис управляемого массива, 
// используется ключевое слово _дс (сборщик мусора)
// int intManagedArrayOnHeap _дс[] = new int _дс[5];
//int intManagedArray _gc[5]; // ошибка для управляемого 
// массива 
// синтаксис управляемого массива, используется 
// управляемый тип элемента 
String *strManagedArrayOnHeap[] = new String* [5]; // Строка 
//String *strManagedArray[5]; // ошибка для управляемого 
// массива }

Управляемые массивы имеют некоторые дополнительные, по сравнению с неуправляемыми массивами, свойства и ограничения.

Управляемый массив можно определить только в управляемой динамически распределяемой области памяти. Его нельзя поместить вне кучи (т.е. он не может быть расположен в стеке).

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

Все управляемые массивы являются потомками класса System: :Array (Система::Массив), так что методы этого класса, как, например, Сору (Копировать), GetLength и GetType, также как и методы класса System: :Object (Система::Объект), наподобие ToString и Equals (Равняется), могут использоваться в любом управляемом массиве.

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

Следующий пример показывает, как можно использовать обработчик исключений при попытке доступа к несуществующему элементу управляемого массива. Обратите внимание, что массив содержит пять элементов, а в цикле производится попытка установить значение шестого. Программа в обычном C++ выполнила бы такое действие, изменив содержимое памяти за пределами массива. Никто не скажет точно, чем это могло бы закончиться. При проверке корректности адреса выполняются два действия: во-первых, предотвращается изменение содержимого памяти за пределами массива; во-вторых, программе сообщается, что возникла подобная ситуация, тем самым давая возможность исправить ошибку еще на стадии тестирования. В обычном C++ такая ошибка часто не проявляется до тех пор, пока программа, по непонятным причинам, не прекращает работу, обычно в месте кода, далеко отстоящем от самой ошибки. И, разумеется, согласно закону Мэрфи, эта ошибка обнаруживается только тогда, когда программа уже передана заказчику. //IndexOutOfRangeException.срр

#using  using namespace System; 
// использовать пространство имен Система/void main () { 
int intArray _gc[]= new int _gc[5]; // сборщик мусора [5] 
for (int i=0; i<6; i++) // больше чем есть!!! 
{ 
try { 
intArray[i] = i; } 
catch (IndexOutOfRangeException *piore) { 
//нужно сделать кое-что более полезное,
//чтобы здесь восстановиться Console::WriteLine("Oooops!");
//Console::WriteLine(piore->get_Message()); } } }

Программа напечатает:

Oooops! Exception of type System.IndexOutOfRangeException was thrown.

Перевод такой [Добавлен редактором русского перевода. — Прим. ред.]:

Возникло исключение типа Система.IndexOutOfRangeException.

Как и для неуправляемых массивов, нумерация элементов в управляемых массивах начинается с нуля. Однако, значения элементов управляемых массивов, в отличие от элементов неуправляемых массивов, автоматически инициализируются значением, принятым по умолчанию для каждого типа элемента массива. Переменным примитивных типов, таких, как int, char (символ), float (с плавающей точкой) и double (с удвоенной точностью) присваивается нуль. Элементам, указывающим на управляемые объекты, также по умолчанию присваивается нулевое значение (т.е. нулевой указатель). Элементы значимых типов инициализируются с помощью их принятого по умолчанию конструктора (т.е. конструктора, не имеющего аргументов). Значимые типы подробнее рассмотрены ниже.

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

//Arrayl.срр 
fusing  
using namespace System; 
// использовать пространство имен Система; 
void main () { 
// управляемый одномерный массив int 
// (использующий сборщик мусора) 
Console::WriteLine("managed ID array of int"); 
// ("управляемый одномерный массив int"); 
int intArray _gc[]= new int _gc[5]; 
for (int i=0; iLength; i++) 
{ 
intArray[i] = i; 
Console::Write(intArray[i]); // Запись 
Console::Write("\t"); // Запись } Console::WriteLine(); 
// управляемый двумерный массив Строк 
// (использующий управляемый тип) 
Console::WriteLine("managed 2D array of Strings"); 
// ("управляемый двумерный массив Строк "); 
String *str2DArray[,] = new String *[2,3]; // новая Строка 
for(int row=0; rowGetLength(0); row++) 
//по строкам 
{ 
for(int col=0; colGetLength(l) ; col++) 
//по столбцам 
{ 
str2DArray[row,col] = (row*10 + col).ToString(); 
// str2DArray [строка, столбец] = (row*10 + столбец) 
// .ToString (); 
Console::Write(str2DArray[row,col]); 
// Запись:: (str2DArray [строка, столбец]); 
Console::Write("\t"); // Запись } 
Console::WriteLine(); } 
// неуправляемый двумерный массив int (для сравнения) 
Console::WriteLine("unmanaged 2D array of int"); 
// ("неуправляемый двумерный массив int"); 
int int2DArray[2][3]; 
for(int row=0; row<2; row++) //по строкам 
{ 
for (int col=0; col<3; col++) // по столбцам { 
int2DArray[row][col] = row*10 + col; 
// int2DArray [строка] [столбец] = row*10 + столбец; 
Console::Write(int2DArray[row][col]);
// Запись:: (int2DArray [строка] [столбец]);
// Console::Write("\t"); // Запись } 
Console::WriteLine(); } )

Результат работы программы приведен ниже. Управляемый прямоугольный двумерный массив содержит элементы типа String*, а неуправляемый — элементы типа int. Однако управляемый массив может также содержать и элементы неуправляемых типов, между тем как неуправляемый массив — лишь элементы неуправляемых типов.

managed ID array of int 01234 managed 2D array of Strings 012 10 11 12 unmanaged 2D array of int

0 1 2 10 11 12

Перевод такой:

управляемый одномерный массив int 01234 управляемый двумерный массив Строк 0 1 2 10 11 12 неуправляемый двумерный массив int 0 1 2 10 11 12

Приведем еще один пример, в котором сравнивается использование массива массивов (синтаксис [ ] [ ]) и прямоугольного двумерного массива (синтаксис [, ]). На этот раз, ради более корректного сравнения, оба массива содержат элементы типа int.

//Array2.срр 
#using  
using namespace System; 
// использовать пространство имен Система; 
void main(void) { 
Console::WriteLine("Rectangular array using [,]"); 
// ("Использование прямоугольного массива [,] "); 
int rect2DArray [,] = new int _gc [3,41; // сборщик мусора - 
// управляемый 
for(int row=0; row< rect2DArray ->GetLength(0); row++) // по строкам { 
for(int col=0; col< rect2DArray->GetLength(1); col++) 
// по столбцам 
{ 
rect2DArray [row,col] = row*10 + col; 
// rect2DArray [строка, столбец] = row*10 + столбец; 
//Console : -.Write (rect2DArray [row, col] ) ; 
// Запись:: (rect2DArray [строка, столбец]); 
//Console::Write("\t"); // Запись } 

Console::WriteLine(); } 
Console::WriteLine("Array of arrays using [][]");
// ("использование массива массивов [] [] "); int arrayOfArray[3][4];
// неуправляемый for(int row=0; row<3 ; row++) // по строкам { 
for(int col=0; col<4; col++) // по столбцам { 
arrayOfArray[row][col] = row*10 + col; 
Добавлен редактором русского перевода. — Прим. ред. 
// arrayOfArray [строка] [столбец] = row*10 + столбец; Console::Write(arrayOfArray[row][col]); 
// Запись:: (arrayOfArray [строка] [столбец]); Console::Write("\t"); // Запись 
} 
Console::WriteLine(); } >

Программа напечатает:

Rectangular array using [,] 0 1 2 3 10 11 12 13 20 21 22 23 Array of arrays using [][] 0 1 2 3 10 11 12 13 20 21 22 23

Перевод такой:

Использование прямоугольного массива [] 0 1 2 3 10 11 12 13 20 21 22 23 Использование массива массивов [] [] 0 1 2 3 10 11 12 13 20 21 22 23

На рис. 3.1 и 3.2 показано размещение в памяти элементов массива массивов (объявленного с помощью синтаксиса [ ] [ ]) и прямоугольного двумерного массива (объявленного с помощью синтаксиса [, ]), использовавшихся в предыдущем примере.

Теперь представим первую версию программы управления системой бронирования Гостиничных номеров, которую мы будем использовать и расширять в следующих главах. Обратите внимание, что класс Hotel (Гостиница) хранится не в сборке ЕХЕ, а в динамически подключаемой библиотеке (DLL).

Рис. 3.1. Размещение в памяти прямоугольного массива

Рис. 3.2. Размещение в памяти массива массивов

Вы можете открыть готовое решение, находящееся в папке HotelRes\Hotel, или создать проект и ввести исходный код сами. Для того чтобы это сделать, необходимо создать проект библиотеки классов на управляемом C++ (Managed C++ Class Library project), называющийся Hotel (Гостиница), добавить исходный код, а затем скомпилировать проект. Заметьте, что поскольку выходной файл —динамически подключаемая библиотека (DLL), его не удастся протестировать до создания исполнимого файла (ЕХЕ) программы-клиента.

Создание библиотеки классов на управляемом C++ (Managed C++ Class Library project)

Создайте проект библиотеки классов на управляемом C++ под названием Hotel (Гостиница):

  • 1. Откройте Visual Studio.NET.
  • 2. Выберите пункт меню File^New^Project (ФайлОСоздаты^Проект) для того чтобы вызвать диалог New Project (Создание проекта).
  • 3. Выберите в списке Project Туре (Тип проекта) Visual C++ Projects (Проекты на Visual C++).
  • 4. Выберите в списке Template (Шаблон) Managed C++ Class Library Project (Проект библиотеки классов на управляемом C++),
  • 5. Введите Hotel (Гостиница) в поле Name (Название).
  • 6. Задайте папку, в которой будет сохранен проект.
  • 7. Щелкните на кнопке ОК. для того чтобы закрыть диалог New Project (Создание проекта) и создать проект.
  • Добавьте исходный код:
  • 8. Дважды щелкните на файле Hotel.cpp в окне Solution Explorer (Поиск решения).
  • 9. Введите исходный код примера Hotel (Гостиница). Скомпилируйте проект:
  • 10. Выберите пункт меню Build^Build (СборкаОСобрать).
  • Хотя определение класса уже присутствует в заголовочном файле и используется в качестве типа данных в срр-файлах, мы, ради простоты и наглядности, поместили данное ниже определение класса Hotel (Гостиница) непосредственно в исходный файл Hotel . срр. Это привело также к тому, что указанный файл стал больше похож на исходный файл С#, в котором директива I include отсутствует. Visual Studio создала, конечно, файл Hotel. h, но это не имеет значения, т.к. соответствующая директива #include была удалена из файла Hotel. срр.
//Hotel.cpp 
finclude "stdafx.h" // имеет #using  
using namespace System; 
// использовать пространство имен Система; 
public _gc class Hotel 
// класс сборщика мусора Гостиница 
{ 
private: // частный 
String *pcity; // Строка 
String *pname; // Строка 
int number; 
Decimal rate; // Десятичное 
public: 
Hotel(String *pcity, String *pname, // Гостиница 
int number, double rate) 
{ 
this->pcity = pcity; 
this->pname = pname; 
this->number = number; 
this->rate = rate; 
} 
Hotel() // Гостиница 
{ 
this->pcity = 0; 
this->pname = 0; 
this->number = 50; // значение по умолчанию 50 
this->rate = 0; 
} 
String *GetCity() // Строка 
{ 
return pcity; 
} 
String *GetName() // Строка 
{ 
return pname; 
} 
int GetNumber() 
{ 
return number; 
} 
void SetNumber(int val) 
{ 
number = val; 
} 
Decimal GetRate() // Десятичное число 
{ 
x return rate; 
} 
void SetRate(Decimal val) // Десятичное число 
{ 
rate = val; } void RaisePrice(Decimal amount) // Десятичное количество 
{ 
rate = rate+1; 
} 
};

Приведенный код компилируется затем в сборку NET, называющуюся Hotel dll Это можно сделать в Visual Studio NET, а можно — с помощью командной строки Если вы используете командную строку, то должны определить соответствующее окружение Простейший способ сделать это — открыть командное окно, выбрав пункты меню Start (Пуск) => Programs (Программы) => Microsoft Visual Studio NET 7 => Visual Studio NET Tools => Visual Studio NET Command Prompt В командной строке, приведенной ниже, компилируется исходный файл Hotel cpp:

cl /CLR Hotel.cpp /LD

Параметр /LD указывает, что компоновщик должен создать динамически подключаемую библиотеку (DLL), а не ЕХЕ-файл Класс Hotel (Гостиница) содержит частные (private) данные, два конструктора для инициализации данных и несколько общедоступных (public) методов

Для того чтобы продемонстрировать возможность использования в NET разных языков, следующая программа, которая тестирует созданный ранее компонент Hotel (Гостиница), реализована на С# Можно либо самостоятельно ее реализовать с использованием Visual Studio NET, либо просто открыть готовое решение, находящееся в папке HotelRes\TestHotel Для создания программы необходимо создать проект консольного приложения на С# (С# Console Application) TestHotel, добавить исходный код, затем ссылку на сборку Hotel (Гостиница), после чего скомпилировать и запустить программу

Создание консольного приложения на С# (С# Console Application):

Создайте проект консольного приложения С#, называющийся TestHotel

  • 1. Откройте Visual Studio NET
  • 2. Выберите пункт меню Fue => New => Project (Файл => Создать => Проект) для того чтобы вызвать диалог New Project (Создание проекта).
  • 3 Выберите в списке Project Type (Тип проекта) Visual C# Projects (Проекты на Visual C#).
  • 4 Выберите в списке Template (Шаблон) Console Application (Консольное приложение)
  • 5 Введите 'TestHotel" в поле Name (Название)
  • 6. Задайте папку, в которой будет сохранен проект
  • 7. Щелкните на кнопке ОК для того чтобы закрыть диалог New Project (Создание проекта) и создать проект
  • Добавьте исходный код:
  • 8. Щелкните правой кнопкой на файле Class.cs в окне Solution Explorer (Поиск решения) и выберите в меню пункт Rename (Переименовать)
  • 9. Введите новое имя исходного файла — TestHotel.cs
  • 10. Дважды щелкните на файле TestHotel cs в окне Solution Explorer (Поиск решения) для того чтобы открыть файл для редактирования.
  • 11. Добавьте в файл TestHotel cs соответствующий исходный код
  • Добавьте ссылку на сборку Hotel (Гостиница):
  • 12. Выберите пуню меню Project => Add Reference (Проект => Добавить ссылку).
  • 13. Щелкните на кнопке Browse (Обзор)
  • 14. Найдите папку, в которой хранится сборка Hotel (Гостиница).
  • 15. Дважды щелкните на сборке Hotel dll.
  • 16. Щелкните на кнопке ОК.
  • Скомпилируйте и запустите проект:
  • 17. Выберите пункт меню Build => Build (Сборка => Собрать)
  • 18. Нажмите сочетание клавиш Ctrl-F5 для запуска программы без отладчика
//TestHotel.cs 
using System; 
JI использование Системы; 
public class TestHotel 
// общедоступный класс TestHotel 
public static void Main() 
{ 
Hotel generic = new Hotel (); // универсальная новая 
// Гостиница 
ShowHotel (generic) ; // универсальная 
Hotel ritz = new Hotel("Atlanta", "Ritz", 100, 95); 
// Роскошная гостиница = новая Гостиница 
// ("Атланта", "Роскошь", 100, 95), 
ShowHotel(ritz); 
ritz.RaisePrice(50m); 
ritz.SetNumber(125); 
ShowHotel(ritz); 
; } 
private static void ShowHotel (Hotel hotel) 
// частный статический 
ShowHotel (Гостиница гостиница) 
{ 
Console.WriteLine( 
"{0} {1}: number = {2}, rate = {3:C}", 
hotel.GetCity(), hotel.GetName(), // гостиница 
hotel.GetNumber(), hotel.GetRate()); // гостиница 
} 
}

Обратите внимание, что Visual Studio автоматически копирует Hotel.dll в ту же папку, в которую помещает файл TestHotel. exe Так происходит потому, что в проект С# была добавлена ссылка на данную сборку Это удобно, ведь если запустить программу-клиент, а общеязыковая среда выполнения CLR не найдет соответствующей сборки, возникнет исключение времени выполнения Приведем строки, выведенные на экран программой, созданной на С# с компонентом на C++:

number = 50, rate = $0.00 Atlanta Ritz: number = 100, rate = $95.00 Atlanta Ritz: number = 125, rate = $96.00

C++ — мощный язык программирования, предоставляющий широкий выбор примитивных типов и позволяющий расширять возможности типов определением классов и интерфейсов. Однако одна из главных идей .NET — возможность создания кода на разных языках и объединение скомпилированного кода в интегрированное решение, работающее на основе общей платформы (общеязыковая среда выполнения CLR). Чтобы такое осуществить, программист, использующий C++, должен понимать, какие типы данных этого языка совместимы с общеязыковой средой выполнения CLR и .NET Framework.

Многие типы данных C++ соответствуют типам данных промежуточного языка IL .NET, определенным спецификацией общего (универсального) языка CLS (Common Language Specification). Некоторые из этих типов, совместимые со спецификацией общего (универсального) языка CLS, гарантированно поддерживаются всеми языками .NET. Они определены в рамках общей системы типов CTS (Common Type System). Спецификация общего (универсального) языка CLS и общая система типов CTS обеспечивают возможность взаимодействия языков, и, хотя C++ поддерживает использование многих типов, несовместимых со спецификацией общего (универсального) языка CLS, такие заблудшие типы следует использовать только в реализациях компонентов, и никогда не открывать в общих сборках. Соблюдение этого правила гарантирует, что программы, использующие подобные сборки, можно будет создавать на любом другом языке .NET, не опасаясь проблем с несовместимостью типов. В табл. 3.1 перечислены типы данных промежуточного языка IL, совместимые со спецификацией общего (универсального) языка CLS. Заметим, что это типы данных промежуточного языка IL, а не C++; но в C++ (и во всех других языках .NET) есть типы, эквивалентные приведенным.

С другой стороны, некоторым типам C++ соответствуют классы .NET Framework. Для примитивных типов, таких, как int и float (с плавающей точкой), соответствующие классы .NET являются оберточными (wrapping) или, как их еще называют, упаковочными (boxing). Упаковка данных примитивных типов будет рассмотрена в этой главе несколько позже. В следующем примере объявляются переменные разных типов C++ и показываются соответствующие классы .NET Framework, для чего используется метод GetType класса System: :Object (Система::Объект).

Таблица 3.1. Типы данных промежуточного языка

Типы данных промежуточного языка Содержимое
Bool (логический, булев) True (Истина) или false (ложь)
char (символ) Символ Unicode (16-битовый)
System.Object (Система.Объект) Объект или упакованный значимый тип
System.String (Система.Строка) Строка Unicode
f Ioat32 (32-разрядный с плавающей точкой) 32-разрядное с плавающей точкой в формате IEEE 754
ftoat64 (64-разрядное с плавающей Точкой) 64-разрядное с плавающей точкой в формате IEEE 754
«its 8-разрядное целое число со знаком
Int16 1 6-разрядное целое число со знаком
k*32 32-разрядное целое число со знаком
kit64 64-разрядное целое число со знаком
unsigned int8 8-разрядное целое число без знака
unsigned int16 16-разрядное целое число без знака
unsigned int32 32-разрядное целое число без знака
unsigned int64 64-разрядное целое число без знака
//MappingDataTypes.срр 
#using  
using namespace System; 
// использовать пространство имен Система; 
void main(void) 
{ 
bool b = false; // логический (булев) b = ложь; Булева переменная 
Char ch = '\0'; // Символ 
Object *pobj = new Object; // 
Объект String *pstr = S""; // Строка 
float f = 1.OF; // f с плавающей точкой = l.OF - одинарная 
//точность 
double d = 1.0; // Двойная точность 
char с = '\0'; // символ SByte 
unsigned char uc = '\0'; // Байт - символ без знака 
short s = 0; //Intl6 (короткий) 
unsigned short us = 0; //UIntl6 - короткий без знака 
int i = 0; //Int32 
unsigned int ui = 0; //UInt32 - int без знака 
long l = 0; //Xnt64 
unsigned long ul = 0; //UInt64 - длинный без знака 
int intManagedArray _gc[] // System.Int32[] - сборщик мусора 
= new int _gc[5]; // сборщик мусора 
Console::WriteLine(_box(b)->GetType() ) ; 
Console::WriteLine(_box(ch)->GetType()) ; 
Console::WriteLine(pobj->GetType() ) ; 
Console::WriteLine(pstr->GetType()) ; 
Console::WriteLine(_box(f)->GetType()); 
Console::WriteLine(_box(d)->GetType()) ; 
Console::WriteLine(_box(c)->GetType()); 
Console::WriteLine(_box(uc)->GetType()); 
Console :: WriteLine (_box ( s) ->GetType () ) ; 
Console::WriteLine(_box(us)->GetType() ) ; 
Console::WriteLine(_box(i)->GetType() ) ; 
Console::WriteLine(_box(ui)->GetType<)); 
Console::WriteLine(_box(1)->GetType()) ; 
Console::WriteLine(_box(ul)->GetType()); 
Console::WriteLine(intManagedArray->GetType()); 
}

Программа напечатает:

System.Boolean // Система. Булева переменная 
System.Char // Система. Символ 
System.Object // Система. Объект 
System.String // Система. Строка 
System.Single // Система. Одинарный 
System.Double // Система. Двойной 
System.SByte 
System.Byte // Система. Байт 
System.Intl6 
System.UIntl6 
System.Int32 
System.UInt32 
System.Int32 
System.UInt32 
System.Int32 []

Рис. 3.3. Использование утилиты Ildasm.exe для просмотра типов данных в программе, реализованной на управляемом C++

В этом разделе главы мы изучим основные аспекты создания кода на управляемом C++. В частности, будут рассмотрены все ключевые слова расширения управляемости C++, поддерживаемые Visual C++.NET. Заметим, что это далеко не все ключевые слова Visual C++ 7.0, не определенные стандартом ANSI C++, — ведь мы концентрируем ваше внимание именно на расширении управляемости C++. Однако в рассмотрении затрагиваются некоторые аспекты, не относящиеся к управляемому коду. Например, использование ключевого слова _interface (интерфейс) не ограничивается лишь управляемым кодом. И в заключение мы кратко опишем атрибуты, технически не относящиеся к управляемости.

Соответствие VC++.NET и ANSI C++

Стоит сказать, что все эти особые ключевые слова, связанные с управляемостью, не Противоречат ANSI C++, так что фактически VC++.NET является более совместимым с ANSI C++, нежели предыдущие версии VC++.

При использовании командной строки следует задавать параметр /CLR (Компиляция для выполнения в общеязыковой среде) компилятора, иначе применение ключевых слов, связанных с управляемостью, не допускается. В Visual Studio корректные установки параметров обеспечиваются при выборе соответствующего шаблона автоматически. Тем не менее, если возникла необходимость установить корректные значения параметров, выполните следующие указания:

  • 1. Щелкните в окне Solution Explorer (Поиск решения) правой кнопкой на узле проекта (но не на узле решения).
  • 2. Выберите пункт меню Properties (Свойства) При этом откроется диалог Project Property Pages (Страницы свойств проекта)
  • 3. Выберите узел General (Общие) под узлом C/C++ и выберите Assembly Support (/clr) для опции Compile As Managed (Компилировать как управляемый).
  • 4. Щелкните на кнопке ОК.

Управляемый тип — тип данных, инициализируемый (обычно с помощью оператора new (создать)) в управляемой динамически распределяемой области памяти, но ни в коем случае не в неуправляемой динамически распределяемой области памяти или стеке Попросту говоря, управляемый тип — тип, для которого сборка мусора осуществляется автоматически, потому для освобождения ресурсов, используемых объектами этого типа, нет необходимости использовать оператор delete (удалить). Вместо того чтобы явно удалять объект, можно либо сделать так, чтобы на него не указывал ни один указатель, либо явно приравнять этот указатель нулю. Неуправляемый тип — тип, который игнорируется автоматическим сборщиком мусора, вследствие чего программист должен освобождать занимаемую объектом память с помощью оператора delete (удалить).

Объекты неуправляемых типов никогда не создаются в управляемой динамически распределяемой области памяти, а только либо в неуправляемой динамически распределяемой области памяти, либо в каком-нибудь другом месте памяти, как переменные в стеке или элементы данных другого неуправляемого класса. Поэтому именно неуправляемые типы — это то, с чем привыкли иметь дело программисты C++, тогда как управляемые типы больше похожи на ссылочные типы языка Java, для которых применяется автоматическая сборка мусора.

Ключевое слово _дс (сокращение от "garbage collection" — "сборка мусора") используется для объявления управляемых классов, или структур, и может использоваться для указателей и массивов. Ключевое слово _поде (сокращение от "no garbage collection" — "без сборки мусора") является антонимом _дс (сборщик мусора). Надо иметь в виду, что ключевое слово _дс (сборка мусора) можно использовать только в управляемом коде, а значит, при этом следует использовать параметр компилятора /CLR (Компиляция для выполнения в общеязыковой среде), причем прагма Ipragma unman-aged должна быть неактивна. Ключевое слово _nogc (без сборки мусора) можно использовать как в управляемом, так и в неуправляемом коде. Следующий фрагмент демонстрирует типичное использование _дс (сборщик мусора) при определении управляемого класса:

_gc class ManagedClass 
// класс сборщика мусора ManagedClass 
{ 
};

Ключевое слово _поде (без сборки мусора) просто означает, что класс, структура, массив или объект, на который указывает определенный с этим словом указатель, не управляется сборщиком мусора .NET. Данное ключевое слово используется для явного указания, что объект никогда не создается в управляемой динамически распределяемой области памяти. Недопустимо наследование типа, определенного с ключевым словом _дс (сборщик мусора) или _поде (без сборки мусора), от типа, определенного с другим из этих ключевых слов, равно, как не допускается использование _дс (сборщик мусора) в неуправляемом коде.

_nogc class UnmanagedClass 
{ 
};

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

В документации по языкам .NET вы могли встречать описание метода Finalize (Завершить), используемого для освобождения ресурсов, не находящихся в управляемой динамически распределяемой области памяти, но созданных управляемыми объектами. Однако в C++ реализовывать данный метод не надо. Если же все-таки сделать это, компилятор выдаст сообщение об ошибке, указав, что вместо метода Finalize (Завершить) для управляемого класса требуется определить деструктор. Сборщик мусора автоматически вызовет деструктор (в отдельном потоке) при освобождении памяти, занятой объектом; но момент вызова деструктора не определен. А это значит: не следует рассчитывать на то, что деструктор будет вызван при удалении ссылки на объект.

Если вы реализовали деструктор и удаляете управляемый объект явно, деструктор будет вызван сразу, и сборщик мусора уже не будет его вызывать. Можно также, вызвав статический метод GC: :Collect () (Сборщик мусо-ра::Собрать()), вынудить сборщик мусора попытаться освободить память из-под объекта, а вызов деструктора синхронизировать с завершением работы сборщика мусора при помощи статического метода GC: :WaitForPending-Finalizers. Впрочем, обычно неудобно и неэффективно вызывать сборку мусора принудительно или синхронизовать ее с вызовом деструктора, поэтому, если необходимо выполнять очистку в определенный момент, рекомендуется реализовать это независимо в отдельном методе, а затем вызывать его явным образом. Этот метод рекомендуется называть Dispose (Ликвидировать). Рассмотрим следующую программу.

//ManagingGC.срр 
fusing  
using namespace System; 
// использовать пространство имен Система; 
_gc class ManagedClass 
// класс сборщика мусора ManagedClass 
{ 
public: 
ManagedClass () 
{ 
Console::WriteLine("c'tor"); 
} 
~ManagedClass () 
{ 
Console::WriteLine("d'tor"); 
} 
}; 
void main(void) 
{ 
Console::WriteLine("start"); // начало 
ManagedClass *pmc = new ManagedClass; 
// Раскомментируйте следующую строку 
// для предотвращения вызова деструктора 
//GC::SuppressFinalize(pmc); // СБОРЩИК МУСОРА 
Console::WriteLine("middle"); // середина 
// Раскомментируйте следующую строку 
// чтобы вызвать деструктор пораньше 
//delete pmc; // удалить 
pmc = 0; 
// ... или две следующие строки для того, 
// чтобы вызвать деструктор пораньше 
//GC::Collect(); // СБОРЩИК МУСОРА:: Собрать 
//GC:-.WaitForPendingFinalizers () ; // СБОРЩИК МУСОРА 
Console::WriteLine("end"); // конец 
}

Приведем результат работы программы. Обратите внимание, что деструктор вызывается после того, как программа напечатает end (конец).

start // начало 
c'tor 
middle // середина 
end // конец 
d'tor

Однако, если раскомментировать строку, содержащую вызов метода SuppressFi-nalize, деструктор не будет вызван вообще, что доказывается следующей выдачей.

start // начало 
с' tor 
middle // середина 
end // конец

Кроме того, если раскомментировать оператор, в котором используется delete (удалить), деструктор будет вызван до того, как программа напечатает end (конец).

start // начало 
c'tor 
middle // середина 
d'tor 
end // конец

Наконец, если раскомментировать только два оператора, содержащих вызовы методов Collect (Собрать) и WaitForPendingFinalizers, деструктор опять будет вызван до того, как программа напечатает end (конец) В этом случае вызов метода Collect (Собрать) приводит к вы зову деструктора, а метод WaitForPendingFinalizers приостанавливает выполнение текущего потока до завершения работы деструктора.

start // начало 
c'tor 
middle // середина 
d'tor 
end // конец

Программы, написанные на C++, не обладают свойством типовой безопасности Программы же на управляемом C++ должны гарантированно обладать указанным свойством Однако, из-за того, что программы C++ могут содержать неуправляемый код, они не обязательно обладают свойством типовой безопасности Нельзя производить арифметические операции с управляемыми указателями Кроме того, нельзя приводить тип управляемого указателя к неуправляемому Поэтому можно доказать безопасность только тех программ на C++, которые содержат лишь управляемые код и данные [Управляемый C++ может генерировать код, гарантированно обладающий свойством типовой безопасности, если избегать использования некоторых особенностей языка, таких, как неуправляемые указатели или приведение типов Для проверки типовой безопасности сборки можно использовать утилиту Pevenfy.exe]. Тем не менее, любая программа на C++, в которой выполняются арифметические действия над неуправляемыми указателями или приводятся типы управляемых указателей к неуправляемым, является потенциально опасной.

Следующая программа является примером небезопасного кода на C++, в котором выполняется приведение указателя pumc на неуправляемый объект к указателю на переменную типа j_nt В этом случае подобная операция не является опасной, но в общем случае ее выполнение может представлять опасность Затем выполняется арифметическое действие над указателем на объект, которое уже в этом примере небезопасно, так как получающийся в результате указатель не указывает на какой-либо объект Еще ниже в этом примере, в закомментированных строках, те же действия совершаются над управляемым указателем рте Если бы строки были не закомментированы, компилятор выдал бы сообщение об ошибке

// Unmanaged.срр 
# using  
class UnmanagedClass 
// класс UnmanagedClass 
{ 
public: 
int i; 
}; 
_gc class ManagedClass 
// класс сборщика мусора ManagedClass 
{ 
public: 
int i; 
}; 
void main(void) 
{ 
UnmanagedClass *pumc = new UnmanagedClass; 
pumc->i = 10; 
int * pi = (int *}pumc; // Опасно приведение указателя 
pi = (int *)(pumc+1); // Опасность: арифметика над указателями 
ManagedClass *pmc = new ManagedClass; pmc->i = 10; 
//pi = (int *)pmc; // Ошибка: приведение _gc //(сборщик мусора) * к * 
//pi = (int *)(pmc+1); // Ошибка, арифметика над _gc // (сборщик мусора) * 
}

Ключевое слово _value (значение) похоже на _nogc (без сборки мусора), поскольку оно используется для того, чтобы класс или структура не участвовали в сборке мусора Это полезно для определения объектов в стеке, а не в управляемой динамически распределяемой области памяти Основная цель использования таких типов — возможность создания объектов, не требующих затрат на сборку мусора Использование ключевого слова _value (значение) имеет побочный эффект — класс автоматически становится конечным (ключевое слово _sealed) и не может быть абстрактным (ключевое слово _abstract к нему неприменимо)

_value struct ValueStruct { 
int i; 
};

Может показаться удивительным, что правила позволяют определять тип _value (значение) там, где не позволяется определять тип _дс (сборщик мусора). В следующем фрагменте кода показан пример этого (вместе с несколькими другими конструкциями). Заметьте, что объекты классов Мап-agedClass, NonManagedClass и ValueClass можно создавать в динамически распределяемой области памяти, тогда как в стек можно поместить объекты только классов NonManagedClass и ValueClass. Последний оператор во фрагменте закомментирован, так как иначе компилятор выдал бы сообщение о недопустимости объявления управляемого объекта как переменной, помещаемой в стек.

//ValueType.срр 
#using  
using namespace System; 
// использовать пространство имен Система; 
_nogc class NonManagedClass 
{ 
}; 
_value class ValueClass 
// класс значения ValueClass 
{ 
}; 
_gc class ManagedClass 
// класс сборщика мусора ManagedClass 
{ 
NonManagedClass nmc; // Странно! Но для компилятора это не ошибка! 
ValueClass vc; // Это не ошибка, здесь допускается тип значения 
}; 
void main(void) 
{ 
NonManagedClass *pnmc = new NonManagedClass; //Нет ошибки 
ValueClass *pvc = _nogc new ValueClass; //Нет ошибки 
ManagedClass *pmc = new ManagedClass; //Нет ошибки 
NonManagedClass; //Нет ошибки в стеке 
ValueClass vc; //Нет ошибки в стеке 
//ManagedClass me; // ошибка, не может быть размещен в стеке 
}

Значение ключевого слова _abstract (абстрактный) очень похоже на значение ключевого слова abstract (абстрактный) в языке Java. Оно также напоминает о сложившейся традиции рассматривать класс C++, содержащий хотя бы одну чистую (pure) виртуальную функцию, как абстрактный. Ключевое слово _abstract (абстрактный) делает это объявление явным. Как и в случае ключевого слова _interface (интерфейс), ключевое слово _abstract (абстрактный) используется для обозначения того, что класс определяет некоторые общие обязательные соглашения между кодом, реализующим методы этого абстрактного класса, и кодом клиентов, вызывающих эти методы. Обратите внимание, что, если абстрактный класс определяется как управляемый, в его описании следует использовать также и ключевое слово _gс (сборщик мусора).

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

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

//AbstractExample.срр 
#using  
using namespace System; 
// использовать пространство имен Система; 
_abstract class AbstractClass 
// абстрактный класс AbstractClass 
{ 
public: 
virtual void Methodlf) = 0; // виртуальный; не реализован здесь 
virtual void Method2() // виртуальный; реализован здесь 
{ 
Console::WriteLine("Method2"); 
} 
}; 
class DerivedClass : public AbstractClass 
{ 
public: 
void Method1() // реализован здесь 
{ 
Console::WriteLine("Method1"); 
} 
}; 
void main(void) » { 
//AbstractClass *pac = new AbstractClass; // ошибка 
AbstractClass *pac = new DerivedClass; // указатель 
pac->Methodl(); 
pac->Method2(); 
AbstractClass &ac = *new DerivedClass; // ссылка 
ас.Methodl(); 
ac.Method2() ; }

Профамма напечатает:

Method1 Method2 Method1 Method2

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

Интерфейсы используются как обобщенные базовые типы для классов, при реализации которых применяются некоторые общие соглашения (контракты). Эти контракты используются для согласования реализации основной программы и программы-клиента посредством определения общего полиморфного набора методов. Интерфейс можно считать крайней формой абстрактного класса, поскольку цели их существования сходны, но интерфейсы — наименее конкретная его разновидность. Так сложилось, что программисты, работающие с C++, используют термин "интерфейс" для обозначения любого класса, содержащего лишь чистые виртуальные методы. Поэтому новое ключевое слово _interface (интерфейс) лишь делает эту договоренность явной.

Класс, определенный с использованием ключевого слова _interface (интерфейс), может содержать лишь общедоступные (public) чистые виртуальные методы. В частности, ни один из методов класса не должен быть реализован, класс не может содержать статические или нестатические элементы данных, конструкторы, деструкторы, статические методы, и не может перегружать операторы. Интерфейс может быть потомком любого количества базовых интерфейсов, но не потомком какого бы то ни было абстрактного или неабстрактного класса. Обратите внимание, что, хотя интерфейс не может содержать элементы данных, он может содержать свойства (доступ к которым осуществляется методами получения/установки (get/set)). О свойствах будет рассказано ниже. Как и в случае абстрактных классов, создать экземпляр интерфейса нельзя, так что они используются как полиморфные базовые классы.

В описании интерфейса можно использовать только спецификатор общего доступа (public); однако его использование не обязательно, поскольку в качестве спецификатора доступа по умолчанию принимается именно public (общедоступный). Исходя из того, что задача интерфейса — определять базовый контракт для производных классов, несложно сделать вывод, что описывать интерфейс с ключевым словом _sealed (конечный) бессмысленно.

К управляемым интерфейсам (т.е. определенным с ключевым словом _дс (сборщик мусора)) предъявляются некоторые дополнительные требования. Они не могут быть производными от неуправляемых интерфейсов. Однако они могут быть непосредственными потомками произвольного количества управляемых интерфейсов. Следующий фрагмент представляет пример типичного использования ключевого слова _interface (интерфейс):

//InterfaceExample.срр 
fusing  
using namespace System; 
// использовать пространство имен Система; 
_interface Somelnterfасе // интерфейс 
{ 
public: 
virtual void Methodl() = 0; // чистый виртуальный явный 
void Method2(); // чистый виртуальный подразумеваемый 
}; 
class DerivedClass : public Somelnterface 
{ 
public: 
void Methodl() // реализован здесь 
{ 
Console::WriteLine("Methodl"); 
} 
void Method2() // реализован здесь 
{ 
Console::WriteLine("Method2"); 
} 
}; 
void main(void) 
{ 
//Somelnterface *psi = new Somelnterface; // ошибка 
Somelnterface *psi = new DerivedClass; // указатель 
psi->Methodl(); 
psi->Method2(); 
Somelnterface &si = *new DerivedClass; // ссылка 
si.Methodl (); 
si.Method2 () ; 
}

Программа напечатает:

Method1 Method2 Method1 Method2

Упаковка и распаковка — важная концепция программирования в .NET вне зависимости от того, какой именно язык программирования вы используете. Одно из самых важных преимуществ .NET — унифицированная система типов. Каждый тип, в том числе простые упакованные встроенные типы, такие как _box (int), является потомком класса System.Object (Система.Объект). В языках, подобных Smalltalk, все типы являются объектами, но это приводит к неэффективности использования простых типов. В стандартном C++ простые встроенные типы данных и объекты обрабатываются по-разному, — это повышает эффективность использования типов, но исключает возможность унификации системы типов. Управляемый C++ объединяет преимущества обоих подходов, используя прием, называемый упаковкой (boxing). Упаковка — преобразование типов значений, таких, как int или double (с удвоенной точностью), в ссылку на объект, хранимый в динамически распределяемой области памяти. Упаковка производится с помощью ключевого слова _box. Распаковка — преобразование упакованного типа (хранимого в динамически распределяемой области памяти) в неупакованное значение (хранимое в стеке). Распаковка выполняется приведением типов. Проиллюстрируем упаковку и распаковку следующим фрагментом кода:

int x = 5; // простой встроенный тип int 
_box int *po = _box(x); // упаковка 
x = *ро; // распаковывание

Ключевое слово _box создает в управляемой динамически распределяемой области памяти управляемый объект, инкапсулирующий копию выражения, имеющего тип значения. Под выражением, имеющим тип значения, подразумевается примитивный тип данных, такой как int, float (с плавающей точкой), double (с удвоенной точностью), или char (символ), либо тип значения, определенный как класс или структура и описанный с использованием ключевого слова _value (значение). Например, предопределенный управляемый тип _boxed_System_Int32 инкапсулирует упакованный int, a управляемый тип _boxed_ValueStruct — упакованный тип значения ValueStruct. Эти странные названия типов (_boxed_System_Int32 и _boxed_ValueStruct) не обязательно будут встречаться в вашем исходном коде, но они показываются утилитой Ildasm.exe. Обратите внимание, что _box int * — альтернативное имя управляемого типа _boxed_System_Int32, a _box ValueStruct* — альтернативное имя управляемого типа _boxed_ValueStruct.

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

Доступ к типам значений осуществляется так же, как и доступ к неупакованным типам. В приведенном ниже коде это делается в присваивании plntBox = 50. Несмотря на то, что plntBox указывает на управляемый объект, разыменованный указатель используется так, как будто он является просто указателем на неупакованный тип int.

//BoxExample.срр 
#using  
using namespace System; 
// использовать пространство имен Система; 
_value struct ValueStruct 
{ 
public: 
int i; 
}; 
// функция ожидает получить управляемый указатель на объект 
void ExpectManagedObjectPointer( 
_box ValueStruct* pManagedObject) 
{ 
pManagedOb]ect->i = 20; // изменяет упакованную копию 
Console::WriteLine(pManagedObject->i) ; 
} 
// функция ожидает получить управляемый указатель на объект 
void ExpectBoxedPrimitivePointer(_box int* plntBox) 
{ 
*pIntBox = 50; //изменяет упакованную копию примитивного типа 
Console::WriteLine(*рIntBox); 
} 
void main(void) 
{ 
ValueStruct ValueStruct; // объект типа значение в стеке 
ValueStruct.i = 10; // изменяет оригинал распакованной копии 
Console::WriteLine(ValueStruct.i); 
_box ValueStruct* pManagedObject 
= _box(valueStruct); //_boxed_ValueStruct 
ExpectManagedObjectPointer(pManagedObject) ; 
pManagedObject->i = 30;* // изменяет упакованную копию 
Console::WriteLine(pManagedObject->i); 
int j; // тип значения - примитивный тип данных 
j = 40; // изменяет первоначальный распакованный 
// примитивный тип 
Console::WriteLine(j); 
_box int *p!ntBox = _box(j); // ynaKOBaHHbm_System_Int32 
ExpectBoxedPrimitivePointer(plntBox); 
}

Приведенная программа напечатает:

10 20 30 40 50

Ключевое слово _delegate (делегат) используется для объявления класса-делегата, основанного на описании сигнатуры метода. Делегаты очень сходны с указателями на функции в обычном C++, с той лишь разницей, что делегат может указывать только на метод управляемого класса. Чаще всего в приложениях .NET Framework делегаты используются для реализации функций обратного вызова или обработки событий. Однако они могут найти применение во всех случаях, когда необходимо вызывать методы динамически.

В .NET Framework определены (как абстрактные классы) два типа делегатов — System: : Delegate (Система::Делегат) и System: :MulticastDelegate. Эти два типа делегатов используются как базовые классы для одноадресных (или делегатов единственного приведения — single-cast) и многоадресных (или групповых — multicast) делегатов соответственно. Одноадресный делегат связывает указатель на метод с методом одного управляемого объекта, тогда как многоадресный делегат связывает указатель на метод с одним или несколькими методами управляемого объекта. Вызов одноадресного делегата приводит к вызову только одного метода, а при вызове многоадресного делегата может выполняться неограниченное количество методов. В связи с тем, что многоадресный делегат можно использовать и для вызова одного метода, одноадресная форма делегата является излишней. Обычно в программах используются лишь многоадресные делегаты.

Встретив в программе ключевое слово _delegate (делегат) компилятор создает особый управляемый класс, производный от System: :MulticastDelegate. Конструктор этого класса имеет два аргумента: указатель на экземпляр управляемого класса (который равен нулю, если делегат связывает статический метод), и сам метод, вызываемый с помощью делегата. Этот класс также содержит метод Invoke (Запустить), сигнатура которого совпадает с сигнатурой метода, вызываемого делегатом. Следующий пример демонстрирует использование делегатов:

//DelegateExample.срр 
#using  
using namespace System; 
// использовать пространство имен Система; 
// определить управляемые классы для использования 
// в качестве делегатов 
_delegate int SomeDelegate // делегат 
(int i, int j); 
_delegate // делегат 
void SomeOtherDelegate (int i); 
_gc class SomeClass 
// класс сборщика мусора SomeClass содержит методы, 
// вызываемые делегатами 
{ 
public: 
int SomeMethod(int i, int j) 
{ 
Console::WriteLine( 
"SomeMethod({0}, {!})", _box(i), _box(j)); 
return i+j; } 
static int SomeStaticMethod(int i, int j) // статический 
{ 
Console::WriteLine( 
"SomeStaticMethod({0}, {!})", _box(i), _box(j)); 
return i+j; } 
void SomeOtherMethod(int i) { 
Console::WriteLine( 
11 SomeOtherMethod ({0}) ", _box(i) ) ; 
} 
}; 
Void main () 
{ 
SomeDelegate *pscd; int sum; 
// сумма 
// связать делегат с нестатическим методом 
// требуется экземпляр 

SomeClass SomeClass * psc = newSomeClass(); pscd = 
// создать экземпляр класса делегат sc new SomeDelegate( 

psc, SSomeClass::SomeMethod);
// нестатический sum = pscd->Invoke(3, 4);
// вызвать метод через делегат 
// сумма = pscd->Bbi3BaTb (3, 4); Console::WriteLine(sum);
// сумма 
// связать делегат со статическим методом, - нет нужды 
// ни в каком экземпляре 
pscd = // создать другой экземпляр класса делегата sc new SomeDelegate( 
О, SSomeClass::SomeStaticMethod); 
// статический sum = pscd->Invoke(3, 4); 
// вызвать метод через делегата 
// сумма = pscd->Bbi3B3Tb (3, 4); Console::WriteLine(sum);
// сумма 
// объединить два делегата SomeClass * pscl = new SomeClass(); 
//SomeClass * psc2 = new SomeClass(); SomeOtherDelegate *pmcdl = new SomeOtherDelegate( 

pscl, &SomeClass::SomeOtherMethod); SomeOtherDelegate *pmcd2 = new SomeOtherDelegate( 
psc2, SSomeClass::SomeOtherMethod); SomeOtherDelegate *pmcd = 
static_cast(Delegate:.Combine( 
// Объединение делегатов pmcdl, pmcd2)); pmcd->Invoke(1);
// Вызвать } 

SomeMethod(3, 4) 7 SomeStaticMethod(3, 4) 7 SomeOtherMethod(I) SomeOtherMethod(1)

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

В обычном C++ для работы с событиями реализуют функции обратного вызова, для выполнения которых используются указатели на функции В модели компонентных объектов Microsoft (COM) для работы с событиями используются интерфейсы IConnec-tionPomt и IConnectionPointContainer В NET используются управляемые события Все эти подходы по сути одинаковы, так что для их объединения Microsoft предложила Унифицированную модель событий (Unified Event Model) Для поддержки этой новой Унифицированной модели событий в C++ введены новые ключевые слова _event (событие), _hook (привязать) и _unhook (отцепить), а также атрибуты event_source (источник события) и event_receiver (приемник события)

Ключевое слово _event (событие) используется для описания события, которое может быть сгенерировано источником события Это слово можно использовать не только в управляемых классах, оно может применяться к следующим объявлениям

  • 1. Описание метода класса обычного C++ (обычный обратный вызов)
  • 2. Описание интерфейса модели компонентных объектов Microsoft (COM) (точка стыковки)
  • 3. Описание метода управляемого класса (управляемое событие)
  • 4. Описание элемента данных управляемого класса (управляемое событие с использованием делегата)

Мы рассмотрим только третий случай, т е случай, в котором источником события является метод управляемого класса Для того чтобы объявить обработчиком какого-то события метод класса-получателя этого события, используется ключевое слово _hook (привязать) После того, как это сделано, при каждом возникновении события будет вызываться его обработчик А чтобы такое объявление метода аннулировать, используется ключевое слово _unhook (отцепить) В следующем примере демонстрируется использование ключевых слов _event (событие), _hook (привязать) и _unhook (отцепить), а также атрибутов event_source (источник события) и event_receiver (приемник события) для реализации механизма обратного вызова

//Event.cpp 
fusing  
using namespace System; 
// использовать пространство имен Система, 
[event_source(managed)] // управляемый 
public _gc class ManagedEventSource 
// класс сборщика мусора ManagedEventSource 
{ public: 
_event void ManagedEvent(} ; // нет реализации 
void Fire_ManagedEvent() 
{ 
ManagedEvent(); 
} 
}; 
[event_receiver(managed)] // управляемый 
gc class ManagedEventReceiver // класс сборщика мусора ManagedEventReceiver 
{ 
public: 
void HandleManagedEvent() // вызывается через ManagedEvent 
{ 
Console::WriteLine("HandleManagedEvent called"); 
} 
void HookEvent(ManagedEventSource *pEventSource) 
{ 
_hook( // обработчик 
SManagedEventSource::ManagedEvent, 
pEventSource, 
SManagedEventReceiver.:HandleManagedEvent) , 
} 
void UnhookEvent(ManagedEventSource* pEventSource) 
{ 
_unhook( // отцепиться 
&ManagedEventSourсе::ManagedEvent, 
pEventSource, 
SManagedEventReceiver:HandleManagedEvent) ; } 
}; 
void main () 
{ 
ManagedEventSource* pEventSource = 
new ManagedEventSource; 
ManagedEventReceiver* pReceiver = 
new ManagedEventReceiver; 
pReceiver->HookEvent(pEventSource) ; 
pEventSource->Fire_ManagedEvent(); // вызывается обработчик 
pReceiver->UnhookEvent(pEventSource); 
}

Профамма напечатает:

HandleManagedEvent called

Ключевое слово _property (свойство) используется для указания на то, что метод получения и/или установки реализует свойство управляемого класса. В отличие от элемента данных (называемого также полем), который однообразен и негибок, свойство может быть доступно для чтения и записи, либо только для чтения и только для записи, может быть реализовано как обычная переменная или как вычисляемое значение. Например, свойство только для чтения должно быть реализовано методом get_, но не методом set_. Свойство доступно другим программам, написанным на любых языках .NET, посредством использования обычного синтаксиса доступа к элементам данных этого языка, как если бы свойство было обычным элементом данных (точнее псевдоэлементом данных). Это продемонстрировано в следующем фрагменте кода, в котором pmcwp->someProperty используется так, как если бы оно было элементом данных с именем someProperty. Фактически такого элемента данных не существует, но то, что класс содержит методы get_someProperty и set_someProperty, объявленные с ключевым словом _property (свойство), делает возможным такой способ доступа. В действительности класс содержит защищенный (protected) элемент данных m_someProperty, но это уже подробности реализации, инкапсулированные в компоненте. Свойство компонента в .NET похоже на свойства OLE-автоматизации в компонентах ActiveX или свойства компонентов (bean) в JavaBean.

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

//PropertyExample.срр 
#using  
using namespace System; 
// использовать пространство имен Система; 
_gc class ManagedClassWithProperty 
// класс сборщика мусора ManagedClassWithProperty 
{ 
public: 
ManagedClassWithProperty() : m_someProperty(0) {} 
_property int get_someProperty() // свойство - 
// должно быть "get_" 
{ 
return m_someProperty; 
} 
_property void set_someProperty( // свойство 
int propertyValue) // должно быть "set__" 
{ 
m_someProperty = propertyValue; 
} 
protected: // защищенный 
int m_someProperty; // можно реализовать как элемент данных 
}; 
void main() { 
ManagedClassWithProperty* pmcwp = new ManagedClassWithProperty; 
pmcwp->someProperty = 7; // псевдоэлемент данных 
Console::WriteLine(pmcwp->someProperty) ; 
}

Вышеприведенная программа выведет:

7

Ключевое слово _pin (закрепить) указывает на то, что указатель на управляемый объект будет оставаться корректным (т.е. общеязыковая среда выполнения CLR не переместит Объект в памяти) на протяжении существования закрепленного указателя. Закрепленный Объект остается на своем месте в памяти до тех пор, пока на него указывает закрепленный указатель. Если изменить указатель так, что он будет указывать на другой объект или присвоить ему нулевое значение, объект может быть перемещен сборщиком мусора. Когда при определении указателя не задано ключевое слово _pin (закрепить), общеязыковая среда Выполнения CLR может в любой момент переместить объект, на который указывает этот указатель. Перемещение объектов происходит вследствие сборки мусора и уплотнения динамически распределяемой области памяти, выполняемых общеязыковой средой выполнения CLR. Эти перемещения не сказываются на управляемом коде, так как общеязыковая среда выполнения CLR автоматически изменяет значения управляемых указателей при перемещении объектов, но могут повлиять на выполнение неуправляемого кода, в котором Используются неуправляемые указатели на управляемые объекты.

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

Ниже приведен фрагмент, иллюстрирующий использование ключевого слова _pin (закрепить) в описанной ситуации. Обратите внимание, что объект pPinnedObject закреплен в памяти, так что передача указателя на него методам SetGlobalPointerValue и GetGlobalPointerValue в качестве аргумента является допустимой. Реализация этих методов основана на том, что глобальный указатель дх остается корректным, а это может быть верны только в случае, когда общеязыковая среда выполнения CLR не будет перемещать объект класса ManagedClass. Заметим, что компилятор способен предсказать возникновение такой ситуации и выдаст сообщение об ошибке, если из приведенного примера удалить ключевое слово _pin (закрепить).

//PinExample.срр 
#using  
using namespace System; 
// использовать пространство имен Система; 
_gc class ManagedClass 
// класс сборщика мусора ManagedClass 
{ 
public: 
int x; }; 
ttpragma unmanaged // неуправляемый 
int *gx; // глобальный указатель void SetGlobalPointer(int* pi) 
{ 
// установить глобальный указатель, 
// чтобы указать на управляемый объект 
gx = pi; 
} 
void SetGlobalPointerValue(int i) 
{ 
// установить управляемый объектный элемент данных 
// через глобальный указатель 
*gx = i; 
} 
int GetGlobalPointerValue() 
{ 
// получить управляемый объектный элемент данных 
// через глобальный указатель 
return *gx; 
} 
Ipragma managed // управляемый 
void main() 
{ 
ManagedClass _pin * pPinnedObject = new ManagedClass; 
// обратите внимание на ошибку, генерируемую компилятором 
//в следующей инструкции... 
// если ключевое слово _pin удалить из предыдущей инструкции 
SetGlobalPointer(&pPinnedObject->x); // неуправляемый 
SetGlobalPointerValue(1); // неуправляемый 
int x = GetGlobalPointerValue();//неуправляемый 
}

Ключевое слово _sealed (конечный) указывает на то, что класс или структуру нельзя использовать в качестве базового типа. Другими словами, в иерархии наследования этот класс или структура— терминальный тип. Ключевое слово _sealed (конечный) можно также применять к отдельному методу класса. Конечный метод не может быть переопределен в производных классах. В стандарте C++ подобная возможность не предусмотрена; однако в Java такая возможность реализована с помощью ключевого слова final (конечный). Следующий фрагмент кода является некорректным, так как конечный класс не может быть базовым:

_sealed class SomeSealedClass 
{ 
}; 
class SomeDerivedClass : public SomeSealedClass // ошибка 
{ 
};

Одной из причин использования ключевого слова _sealed (конечный) является повышение стабильности работы классов за счет препятствования слишком самоуверенным и/или недостаточно квалифицированным программистам испортить важные и сложные элементы поведения классов в производных от них. Другой аргумент использования _sealed (конечный) — предотвращение попыток изменить возможности, обеспечивающие безопасность. Например, предопределенный класс string (Строка) объявлен как конечный, а вдобавок к тому — еще и как неизменяемый (он не содержит общедоступных методов или элементов данных, что могло бы позволить изменять его содержимое). Это делает его идеальным для использования в защитных целях, например, для хранения паролей. При попытке скомпилировать следующий код будет выдана ошибка, так как класс string (Строка) является конечным:

// не допустимо, потому что Система::Строка - конечный класс 
class MyString : public String 
// класс MyString: общедоступная Строка 
{ 
};

Ключевое слово _try_cast приводит к возникновению исключения System: : InvalidCastException при попытке выполнить приведение типов, не поддерживаемое общеязыковой средой выполнения CLR. Это похоже на возникновение исключения bad_cast при выполнении оператора dy-namic_cast в C++ и на исключение ClassCastException, возникающее при некорректном приведении типов в Java. Хотя по своему действию оператор _try_cast больше похож на оператор dynamic_cast, чем на оператор static_cast, _try_cast в действительности задуман как временная замена оператора static_cast, применяемая на стадии разработки приложений. После анализа всех возникающих при выполнении _try_cast исключений и внесения соответствующих исправлений в программу, операторы _try_cast обычно заменяются операторами static_cast. В следующем примере продемонстрировано использование операторов _try_cast для выявления некорректных приведений типов.

//TryCastExample.cpp 
fusing  
using namespace System; 
// использовать пространство имен Система; 
_gc class Mammal 
// класс сборщика мусора Млекопитающее 
{ 
}; 
_gc class Dog : public Mammal 
// класс сборщика мусора Собака: общедоступное Млекопитающее 
{ 
}; 
_gc struct Cat : public Mammal 
// сборщик мусора; Кот: общедоступное Млекопитающее 
{ 
}; 
void main() 
{ 
Mammal *pMammal = new Dog; 
// Млекопитающее *pMammal = новая Собака; 
try // пробовать 
{ 
Dog *pDog = _try_cast (pMammal); // хорошо 
// Собака 
*pDog = _ try_cast <Собака *> (pMammal); 
Console::WriteLine("_try_cast "); 
// Собака - 
// хорошо 
Cat *pCat = _try_cast (pMammal); // плохо! 
// Кот *pCat = _ try_cast <Кот *> (pMammal); 
Console::WriteLine("_try_cast "); // Кот - 
// пропустить 
} 
catch(InvalidCastException *pe) 
{ 
Console::WriteLine("Ooops: {0}", pe->get_Message()); 
} 
}

Приведенная программа напечатает:

_try_cast Ooops: Exception of type System.InvalidCastException was thrown.

Ключевое слово _identifier (идентификатор) позволяет использовать любое слово, включая и ключевое, в качестве идентификатора. Его можно использовать и для слов, не являющихся ключевыми, но это не дает никаких преимуществ, и потому является бессмысленным. На первый взгляд кажется нелепым, что такая черта может вообще понадобиться; однако, из-за того, что

платформа .NET допускает использование в разработке приложений одновременно нескольких языков, может оказаться, что имя класса или переменной, определенное в части программы, написанной на другом языке, совпадет с каким-либо ключевым словом C++. Очевидно, что использование в качестве имен ключевых слов значительно усложнит чтение и понимание исходного кода, так что к этому приему следует прибегать только в крайнем случае. Выглядящий несколько странно код, приведенный ниже, демонстрирует этот прием. В нем описывается класс, называющийся if, элемент данных которого называется while (эксцентричное сочетание). Затем создается экземпляр класса if и вызывается метод while. (О, меня уже тошнит!!!) Удивительно, но это компилируется и работает!

//IdentifierExample.срр 
#using  
using namespace System; 
// использовать пространство имен Система; 
_gc class _identifier(if) 
// класс сборщика мусора _ идентификатор (если) 
{ 
public: 
int _identifier(while) ; 
// int _ идентификатор (while); 
}; 
void main(void) 
{ 
_identifier(if)* pif = new _identifier(if); 
// _ идентификатор (если) 
// * pif = новый _ идентификатор (если); 
pif->_identifier(while)= 1; 
// pif-> _ идентификатор (while) = 1; 
Console::WriteLine(pif->_identifier(while) ) ; 
// (pif-> _ идентификатор (while)); 
}

Без сомнения, вы уже хорошо знакомы с механизмом исключений в стандартном C++, так что хорошо понимаете, как работают управляемые исключения. Напомним, что платформа .NET (точнее, общеязыковая среда выполнения CLR) поддерживает расширения, совместимые с расширением управляемости C++, и управляемые исключения, возникшие при выполнении кода, созданного на одном из языков .NET, могут быть перехвачены и обработаны кодом, написанным на любом другом языке .NET.

Кроме обработки предопределенных исключений, таких, как Invalid-CastException или OverflowException, вы можете определить ваши собственные производные от Exception (Исключение) классы, инкапсулирующие некоторую специфичную для приложения информацию. Рассмотрим следующий пример:

//Exceptions.cpp 
#using  
using namespace System; 
// использовать пространство имен Система; 
_gc class MyException : public Exception 
// класс сборщика мусора MyException: общедоступное Исключение 
{ 
}; 
void TemperamentalFunction(int i) // ненавидит нечетные числа 
{ 
Console::WriteLine( 
"TemperamentalFunction called with {0}", 
i.ToString()); 
if (i%2 != 0) // если (i%2 != 0), т.е. нечетное 
throw new MyException; 
Console::WriteLine("No exception thrown"); // Нет исключения 
} 
void main() 
{ 
try 
{ 
TemperamentalFunction(2); // вызов с четным числом 
TemperamentalFunction(3); // вызов с нечетным числом 
} 
catch (MyException *pe) 
{ 
Console::WriteLine("Exception thrown!"); // Исключение! 
Console::WriteLine(pe->get_StackTrace()); 
} 
}

Приведем результат работы программы:

TemperamentalFunction called with 2 No exception thrown TemperamentalFunction called with 3 Exception thrown! at TemperamentalFunction(Int32 i) in с:\netcppcode\ chap03\exceptions\exceptions.cpp:line 16 at main() in c:\netcppcode\chap03\exceptions \exceptions.cpp:line 25

Вот более русифицированная версия этой выдачи .

TemperamentalFunction вызвана с 2 Нет исключения TemperamentalFunction вызвана с 3 Исключение! в TemperamentalFunction (Int32 i) в с:\netcppcode\ chap03\exceptions\exceptions.cpp:line 16 в главном () в c:\netcppcode\chap03\exceptions \exceptions.cpp:line 25

Обратите внимание на метод StackTrace, позволяющий получить текстовую строку, представляющую состояние стека в момент возникновения исключения. Хотя в этом примере ключевое слово _finally (наконец) и не используется, но следует помнить, что такое расширение стандарта ANSI C++ поддерживается в Visual C++. Ключевое слово _finally (наконец) позволяет вставлять в программу код, который выполняется вне зависимости от того, возникло или нет исключение в блоке try. Следует также упомянуть, что ключевое слово _finally (наконец) полностью совместимо с механизмом исключений, поддерживаемым другими языками .NET.

При желании предыдущий пример можно разбить на две части. Первая часть могла быть реализована на С# (в виде динамически подключаемой библиотеки (DLL)) и содержала бы код, при выполнении которого возникало бы исключение. Вторая часть была бы приложением на C++, вызывающим метод TemperamentalFunction. Этим способом можно было бы наглядно продемонстрировать, что исключения действительно являются мостом, соединяющим разные языки .NET.

Visual C++.NET поддерживает использование атрибутов, позволяющих создавать Обычный неуправляемый код, такой, как компоненты модели компонентных объектов Microsoft (COM), даже с меньшими усилиями, чем раньше. Кроме того, Visual C++.NET Поддерживает новые возможности .NET, такие, как Унифицированная модель событий (Unified Event Model). Изначально атрибуты, относящиеся к модели компонентных объектов Microsoft (COM), использовались в отдельном файле IDL (Interface Definition Language — язык описания интерфейсов) для описания информации о типе компонентов Модели компонентных объектов Microsoft (COM). Теперь же эти атрибуты можно использовать непосредственно в исходном коде C++, поэтому необходимость в отдельном файле IDL отпадает. Компилятор использует эти атрибуты для генерации исполняемого Кода и информации о типе. Одно из преимуществ применения атрибутов C++ для программирования с использованием модели компонентных объектов Microsoft (COM) со-СТоит в том, что вам придется возиться только с исходными файлами C++, но не с фай-Дами IDL или RGS (Registry Script — Сценарий системного реестра). Это делает проекты С компонентами на основе модели компонентных объектов Microsoft (COM) более простыми и понятными.

Использование атрибутов значительно изменяет язык C++ и расширяет возможности Модульности программ. Источники атрибутов, в качестве которых могут выступать независимые динамически подключаемые библиотеки (DLL), реализуют механизм динамического расширения возможностей компилятора C++. Атрибуты предназначены для по-Ьышения производительности труда программистов, особенно при разработке компонентов на основе модели компонентных объектов Microsoft (COM). Генерируемый при Использовании атрибутов код автоматически вставляется в конечный файл. Атрибуты используются для создания кода таких элементов:

  • интерфейсы и компоненты на основе модели компонентных объектов (СОМ);
  • события модели компонентных объектов Microsoft (COM) (точки стыковки);
  • события унифицированной модели событий (управляемые события);
  • код ATL Server;
  • код пользователя OLE для баз данных;
  • код со счетчиками производительности;
  • входные точки модулей.

Хотя данная глава называется "Программирование на управляемом C++", а атрибуты используются при создании управляемого кода, в этом разделе мы рассмотрим лишь использование атрибутов для создания неуправляемого кода на основе модели компонентных объектов Microsoft (COM). Информацию об использовании атрибутов при работе с управляемыми событиями можно найти в разделе этой главы "События". Информацию об использовании атрибутов для других целей можно найти в документации по комплексу инструментальных средств разработки программ .NET ( NET SDK). Атрибуты, определяемые разработчиком, рассмотрены в главе 8 "Классы каркаса .NET Framework".

Продемонстрируем необходимость использования атрибутов в C++ на примере описания функции AddEmUp, приведенного в следующей строке кода. Заметим, что в рамках ANSI C++ эта функция не может быть описана полностью. Так, невозможно указать, какие из аргументов являются входными, а какие — выходными Обычно эта дополнительная информация, важная при создании кода на основе модели компонентных объектов Microsoft (COM), использующего маршалинг, описывается с помощью атрибутов языка описания интерфейсов (IDL) в отдельном файле IDL. При этом атрибуты языка описания интерфейсов (IDL) заключаются в квадратные скобки и могут использоваться для описания многих черт компонентов на основе модели компонентных объектов Microsoft (СОМ), в том числе интерфейсов, соклассов и библиотек типов.

// нет важной информации маршалинга 
HRESULT AddEmUp (int i, int j, int.* psum) ;

Приведенную функцию C++ можно описать более подробно в файле IDL, как это показано ниже. Здесь используются атрибуты in (входной), out (выходной) и retval.

.HRESULT AddEmUp( 
[in]int i, [in]int 3, [out,retval]int *psum);

Для синтаксического разбора файла IDL, создания библиотек типов и исходных файлов, использующих маршалинг (для заместителя (proxy) и заглушки (stub)), для методов интерфейса модели компонентных объектов Microsoft (COM) (поддерживаются также интерфейсы удаленного вызова процедур (RPC)) используется компилятор языка описания интерфейсов IDL, разработанный фирмой Microsoft, — Midi. exe.

Добавлять атрибуты в исходный код можно вручную Однако полезно будет увидеть, как атрибуты вставляются в проект, генерируемый Мастером приложений библиотеки шаблонных классов (ATL) на основе модели компонентных объектов Microsoft (COM) (ATL COM Application Wizard), который является высокопроизводительным инструментальным средством, предназначенным для создания компонентов на основе модели компонентных объектов Microsoft (COM). На рис. 3.4 показано, как разрешить использование атрибутов в Мастере приложений библиотеки шаблонных классов (ATL) на основе модели компонентных объектов Microsoft (COM) (ATL COM Application Wizard).

Рис. 3.4. Окно ATL Project Wizard (Мастер проектов на основе библиотеки шаблонных классов (ATL)) с установленным флажком Attributed

Следующий фрагмент кода взят из файла MyATLProject, созданного Мастером проектов на основе библиотеки шаблонных классов (ATL) (ATL Project Wizard) при установленном флажке Attributed Обратите внимание, что атрибут module (модуль) применен к проекту в целом, благодаря чему в проекте автоматически генерируются функции DllMain, DllRegisterServer и DllUnregisterServer.

// MyATLProject.срр: Реализация экспорта динамически 
// подключаемой библиотеки (DLL). 
#include "stdafx.h" 
#include "resource.h" 
// Атрибут module (модуль) вызывает автоматическую генерацию 
// DllMain, DllRegisterServer и DllUnregisterServer 
[ module(dll, uuid = "{50434D6D-AAEA-405C-AC49-B9CA769E5D6D}", 
// модуль 
name = "MyATLProject", // название 
helpstring = "MyATLProject 1.0 Type Library", 
// Библиотеки Типа 
resource_name = "IDR_MYATLPROJECT") ];

На рис 3.5 показано использование Мастера простых объектов на основе библиотеки шаблонных классов (ATL) (ATL Simple Object Wizard) для добавления к проекту простого класса на основе модели компонентных объектов Microsoft (COM), называющегося MyATLClass.

Рис. 3.5. ATL Simple Object Wizard (Мастер простых объектов на основе библиотеки шаблонных классов (ATL))

Мастер простых объектов на основе библиотеки шаблонных классов (ATL) (ATL Simple Object Wizard) добавит приведенный ниже исходный код с атрибутами в файл MyAtlClass .h, а не в файл IDL. Обратите внимание, что атрибуты object (объект), uuid (universally unique identifier— универсальный уникальный идентификатор) и dual (двойной) используются для описания интерфейсов, а атрибуты coclass, progid (программный идентификатор) и version (версия)— для описания классов на основе модели компонентных объектов Microsoft (COM). Эти атрибуты находятся непосредственно в исходном коде C++, поэтому файл IDL не нужен.

// IMyAtlClass 
[ 
object, // объект

uuid("lF9401D8-58BF-469D-845B-A2069CBAFC84") , 
dual, helpstring("IMyAtlClass Interface"),
// двойной, Интерфейс 
pointer_default(unique) // уникальный 
] 
_interface IMyAtlClass : IDispatch // интерфейс 
{ 
}; 
// CMyAtlClass 
[ 
coclass, 
threading("apartment") , 
vi_progid("MyATLProject.MyAtlClass"), 
progid("MyATLProject.MyAtlClass.1"), // программный идентификатор 
version(1.0), // версия 
uuid("B3321AD5-3ACE-4820-B134-35FD67120A48"), 
helpstring("MyAtIClass Class") // Класс 
{ 
class ATL_NO_VTA5LE CKyAtlClass : // классс 
public IMyAtlClass 
{ 
public: 
CMyAtlClass() 
{ 
} 
DECLARE_PROTECT_FINAL_CONSTRuCT() 
HRESULT FinalCor.struct () 
{ 
return S_OK; 
} 
void FinalReiease() 
{ 
} 
public: 
};

В приведенном коде используется атрибут сoсlass, но при этом опущена некоторая информация, присутствующая в коде, сгенерированное прежними версиями АТ!_. Например, отсутствуют карты объектов, карты интерфейсов и сценарии системного реестра. Атрибут coclass обеспечивает все эти, казалось бы, пропущенные возможности. Пропущенный код будет автоматически добавлен компилятором при обработке атрибута coclass.

На рис. 3.6 показан диалог Add Method (Добавление метода), используемый для добавления метода AadEmUp. Обратите внимание на указание атрибутов in (входной), cut (выходной)и retval.

Они обусловливают добавление в файл KyAr iciass . h приведенного ниже кода. Еще раз обратим ваше внимание на то, что атрибуты используются и исходном файле СРР, а не в отдельном файле IDL.

Создание простого проекта на основе библиотеки шаблонных классов ATL (Simple ATL Project)

Создайте проект на основе ATL Server (ATL Server Project):

  • 1. Выберите пункт меню Fiie => New (Файл => Создать). Откроется диалог New Project (Создание проекта).
  • 2. Выберите Visual C++ Projects (Проекты на Visual C++) в окне Project Types (Типы проектов).
  • 3. Выберите ATL Project (Проект на основе библиотеки шаблонных классов ATL) в окне Templates (Шаблоны).
  • 4. Введите MyATLProject в поле Name (Название)
  • 5. Задайте в поле Location (Расположение) папку, в которой будет сохранен проект.
  • 6. Щелкните на кнопке ОК для вызова Мастера проектов на основе библиотеки шаблонных классов (ATL) (ATL Project Wizard).
  • 7. Выберите вкладку Application Settings (Параметры приложения).
  • 8. Убедитесь, что флажок Attributed установлен.
  • 9. Щелкните на кнопке Finish (Готово). Создайте простой объект на основе библиотеки шаблонных классов (ATL):
  • 10. Выберите пункт меню Project => Add Class (Проект => Добавить класс). Откроется диалог Add Class (Добавление класса).
  • 11. Выберите ATL Simple Object (Простой объект на основе библиотеки шаблонных классов (ATL)) в качестве шаблона (Template).
  • 12. Щелкните на кнопке Open (Открыть) для вызова Мастера простых объектов на основе библиотеки шаблонных классов (ATL) (ATL Simple Object Wizard).
  • 13. Введите MyAtlClass в поле Short name (Короткое название).
  • 14. Щелкните на кнопке Finish (Готово) для создания проекта.
  • Добавьте метод в простой объект на основе библиотеки шаблонных классов (ATL):
  • 15. Щелкните правой кнопкой на интерфейсе IMyAtlClass в окне Class View (Просмотр классов).
  • 16. Выберите пункт меню Add => Add Method (Добавить => Добавить метод). Откроется Мастер добавления методов (Add Method wizard).
  • 17. В качестве названия метода введите AddEmUp в поле Method name (Название метода).
  • 18. Выберите LONG в списке Parameter type (Тип параметра).
  • 19. В качестве имени параметра введите i в поле Parameter name (Имя параметра).
  • 20. Установите флажок in (входной).
  • 21. Щелкните на кнопке Add (Добавить).
  • 22. Выберите LONG в списке Parameter type (Тип параметра).
  • 23. В качестве имени параметра введите j в поле Parameter name (Имя параметра).
  • 24. Установите флажок in (входной).
  • 25. Щелкните на кнопке Add (Добавить).
  • 26. Выберите LONG в списке Parameter type (Тип параметра).
  • 27. В качестве имени параметра введите psum в поле Parameter name (Имя параметра).
  • 28. Установите флажки out (выходной) и retval.
  • 29. Щелкните на кнопке Add (Добавить).
  • 30. Щелкните на кнопке Finish (Готово).

Рис. 3.6. Диалог Мастера добавления методов (Add Method wizard) позволяет указать атрибуты параметра

// IMyAtlClass 
[ 
object, // объект 
uuid("lF9401D8-58BF-469D-845B-A2069CBAFC84") , 
dual, helpstring("IMyAtlClass Interface"), // двойной, 
// интерфейс 
pointer_default(unique) // уникальный 
] 
_interface IMyAtlClass : IDispatch // интерфейс 
[id(l), helpstring("method AddEmUp")] HRESULT AddEmUp([in] 
LONG i, [in] LONG j, [out,retval] LONG* psum); 
};

Единственное, что осталось сделать в рассматриваемом примере, — вручную реализовать метод AddEmUp в файле MyAtlClass . cpp и скомпилировать приложение. Реализация этого метода приведена ниже

STDMETHODIMP CMyAtlClass::AddEmUp(LONG i, LONG ], LONG* psum) 
{ 
// TODO: Add your implementation code here 
// TODO: Добавьте ваш код реализации здесь 
*psum = i+j; // добавлен вручную 
return S_OK; 
}

Создав приложение, его можно протестировать с помощью программы-клиента на основе моде in компонентных объектов Microsoft (COM) Мы не будем рассматривать здесь как это можно Lделать поскольку создание такой программы предполагает применение обычного стиля программирования на устаревшем Visual Basic или Visual C++ Для полччения более подробной информации оекомендлем обратиться к документации по Visual Studio или к любой из множества отличных книг в этой области (например, Understanding and Programming СОМ+, написанной Робертом Дж Обергом (Robert J. Oberg))

В этой паве рассмотрено использование расширений C++ при создании программ и компонентов для платформы NET Изложение основных концепций создания управляемого кода на С-г+ проил пострировано на MHOIHX примерах, таких, как HelloWorld (Привет, мир), ConvertTemp и др. Были рассмотрены классы String (Строка) и Array (Массив) активно используемые почти во всех типах приложений NET Подробно изучены ключевые слова расширения управляемости C++ Рассмотрены также делегаты, события и управление обработкой исключений Напоследок проанализировано использование атрибутов С++ для создания компонентов на основе модели компонентных объектов Miciosott (COM)

назад главная вперед

Главная Новости Visual Studio .NET Самоучитель C# Учебники О сайте
Framework 3.0
Microsoft официально заявила о переименовании своего API WinFX в более привычный для слуха .NET Framework 3. Это, вероятно, последнее переименование. Нет уже Longhorn (Vista), канули в прошлое Avalon (WPF), Indigo (WCF). Время пришло переименовать WinFX…
DirectX 10
Новый набор интерфейсов программирования приложений (API) DirectX 10 увеличит скорость работы компьютеров при запуске ряда приложений в 4 раза. Об этом заявили разработчики DirectX компания Microsoft, сообщается на сайте The Inquirer...
Microsoft в России
Компания Microsoft открыла производство в России, пишет газета "Ведомости". Диски с Windows и Microsoft Office с марта печатаются на ярославском полиграфическом комбинате ООО "Сонопресс". Sonopress входит в концерн Bertelsmann и производит диски также для Sony BMG...
счетчик посещений
Используются технологии uCoz