Чтобы помочь отпраздновать 25-летие C++Builder, я переиздаю свою очень старую статью о расширениях языка C++, представленных Borland в то время.
Как вы, наверное, слышали, вскоре после 27-летия Delphi C++ Builder отмечает свое 25-летие. Вы можете прочитать больше об истории продукта в замечательном блоге Дэвида Миллингтона, премьер-министра C++Builder, на тему « Празднование 25-летия C++Builder! «Есть еще одна очень интересная запись в блоге Дэвида I на тему « 25-летие C++Builder: визуальная разработка, мощь языка C++ и 2,5 десятилетия непрерывного совершенства ». Даже если вас в основном интересует Delphi, обе статьи помогут правильно рассмотреть два продукта-близнеца.
В то время, когда Delphi был выпущен, я был в основном сосредоточен на C++ (и, в частности, на библиотеке Borland C++ OWL для программирования под Windows) и написал 3 книги по этому языку , Borland IDE и библиотеке. Когда я перешел на Delphi, C++ все еще был активной частью моего внимания, когда появился C++Builder. Я провел сессию на SD West Conference по расширениям языка C++ Builder и довольно много писал на эту тему, даже если я никогда не писал по ней полную книгу, будучи довольно занятым со стороны Delphi.
Чтобы отпраздновать 25-летие C++Builder, я решил опубликовать одну из своих статей по этому языку именно в таком виде. Это из материала конференции SD 97 West. Не все остается точным спустя 25 лет, но большая часть общего описания остается в силе. Но вместо того, чтобы редактировать на современные языки, я подумал, что было бы неплохо сохранить его в точности таким, каким он был изначально написан. Надеюсь, вам понравится это читать.
Table of Contents
Расширения языка C++ Builder (или: программирование Delphi на C++)
Марко Канту
Когда Delphi был впервые выпущен, многие программисты жаловались: «Почему он не основан на языке C++?». Некоторые из них (в том числе и я) ответили: «Просто потому, что это невозможно из-за многих отсутствующих возможностей языка C++». Теперь, когда Borland выпустила C++Builder или Delphi для C++, вы можете подумать, что я начну искать оправдания своим неправильным утверждениям. Я не! Я по-прежнему считаю, что мое утверждение верно. На самом деле Borland C++ Builder основан не на языке ANSI C++, а на сильно расширенной версии этого языка, которая включает почти все основные функции языка Object Pascal, имеющиеся в Delphi.
В этой статье я собираюсь подробно обсудить все эти языковые расширения, предоставив некоторую информацию для программистов, не имеющих опыта работы с Delphi. Я не буду описывать все новые возможности C++Builder, такие как макросы, используемые для реализации параметров открытого массива, или шаблон, используемый для клонирования наборов Pascal. Я собираюсь сосредоточиться только на основных функциях.
Что такое собственность?
Одним из ключевых элементов визуального программирования является представление о свойствах объекта. Но что такое собственность? Свойство — это имя типа , относящееся к некоторым данным или функциям доступа к данным класса.
Одна из основных идей ООП заключается в том, что элементы данных всегда должны быть закрытыми. Затем вы часто будете писать общедоступные функции-члены для получения и установки этого частного значения. Если позже вы измените реализацию, вы сможете легко изменить код этих функций доступа и избежать каких-либо изменений в коде, использующем класс.
Вот базовый пример (написанный в соответствии со стандартными соглашениями об именах свойств, полей и функций доступа):
1 2 3 4 5 |
private: int fTotal; protected: void __fastcall SetTotal (int Value); int __fastcall GetTotal (); |
Код этих двух функций-членов (не показаны) просто устанавливает или возвращает значение закрытого члена данных fTotal . Вы можете использовать их следующим образом:
1 2 |
int x = Form1->GetTotal(); Form1->SetTotal (x + 5); |
Это стандартный подход С++. В C++Builder мы можем определить внутри класса свойство, обертывающее эти функции доступа:
1 2 3 4 |
public: __property int Total = { read = GetTotal, write = SetTotal }; |
Это означает, что теперь мы можем получить доступ к этому значению более унифицированным способом, поскольку мы используем одну и ту же нотацию как для чтения, так и для записи свойства:
1 2 |
int x = Form1->Total; Form1->Total = x + 5; |
В зависимости от своей роли в операторе выражение Form1->Total транслируется компилятором при вызове функции чтения или записи. На самом деле компилятор может транслировать подобное выражение и в прямой доступ к данным. Посмотрите на следующее объявление свойства:
1 2 3 4 5 6 |
private: int fValue; public: __property int Value = { read = fValue, write = fValue }; |
Хотя на первый взгляд этот код кажется странным, он полностью демонстрирует роль свойств как механизма инкапсуляции. На самом деле мы можем позже изменить объявление этого свойства, введя одну из двух функций вместо прямого доступа к данным. В этом случае нам потребуется перекомпилировать код классов, использующих это свойство, но изменять его не нужно.
Очевидно, что функции доступа не ограничены чтением и записью частных данных, но могут делать что угодно. Одним из типичных эффектов функций записи является обновление пользовательского интерфейса. Вот новая версия свойства Value , за которой следует код его функции записи (написанный в стандартном стиле):
1 2 3 4 5 6 7 8 9 10 11 12 13 |
__property int Value = { read = fValue, write = SetValue }; void __fastcall TForm1::SetValue (int newValue) { if (fValue != newValue) { fValue = newValue; Label2->Caption = "Value = " + IntToStr (fValue); }; }; |
Теперь, если мы напишем следующие утверждения:
1 2 3 |
Value = Value + 2; Value += 2; Value ++; |
эффект изменения данных будет автоматически отражаться в пользовательском интерфейсе. Обратите внимание, что это работает и с операторами. Когда вы применяете оператор приращения (++) к свойству, его значение считывается и вызывается метод Set. Я действительно думаю, что свойства — это надежный механизм инкапсуляции ООП!
Дополнительные языковые правила для свойств
Помимо базовой структуры, которую мы видели, объявления свойств предоставляют множество альтернатив. Фундаментальная идея заключается в том, что свойства имеют тип данных, но ограничены заданным набором типов данных (включая большинство предопределенных типов, строк и классов).
Свойства могут быть доступны для чтения и записи, как в приведенных выше примерах, но они также могут быть доступны только для чтения или только для записи. Что является общим, так это видеть свойства только для чтения, то есть значения, которые вы можете прочитать, но не изменить. Очевидным примером свойства только для чтения в VCL (библиотеке визуальных компонентов, иерархии классов Delphi, используемой также C++Builder) является свойство Handle оконных элементов управления. Возможно, вам захочется запросить у элемента управления его дескриптор Windows, когда вы хотите напрямую вызывать функции Windows API, но вы не должны изменять дескриптор окна. Чтобы определить свойство только для чтения, вы просто опускаете объявление записи :
1 2 |
__property int ReadValue = { read = GetReadValue }; |
Можно объявить свойства массива, то есть свойства с индексом. В этом случае необходимые функции чтения и записи имеют дополнительный параметр, сам индекс. Этот индекс также необходимо использовать для доступа к значениям, поскольку вы не можете получить доступ к массиву в целом. Массив может не существовать как фактическое поле класса: когда вы читаете элементы списка, вы фактически запрашиваете у Windows значение элемента (которое не дублируется внутри объекта TListBox ). Также возможно объявить свойства массива с несколькими индексами (см. в качестве примера свойство Pixels класса TCanvas VCL).
Доступ к свойству
Свойства могут быть объявлены с использованием любого из спецификаторов доступа, включая private (хотя в этом мало смысла), protected и public, а также два новых спецификатора: __published и __automated. Кстати, свойства только для чтения нельзя публиковать.
Опубликованное поле или метод доступны не только во время выполнения (как общедоступный элемент), но и во время разработки. Компилятор Borland C++Builder генерирует идентификатор времени выполнения (RTTI) в стиле Delphi для опубликованных свойств класса, производного от TPersistent . Эта информация о типе используется средой разработки, начиная с инспектора объектов, но она также доступна программистам, которые хотят углубиться в недокументированный модуль TypInfo.
Опубликованное ключевое слово обычно используется для свойств или событий, но формы обычно имеют опубликованный интерфейс, включающий также подкомпоненты и методы обработчиков событий. Эта информация автоматически анализируется средой разработки и становится доступной в инспекторе объектов еще до того, как вы скомпилируете программу.
Например, в то время как опубликованный интерфейс компонента используется инспектором объектов для отображения и редактирования значений свойств во время разработки, опубликованный интерфейс формы используется инспектором объектов для поиска компонентов, совместимых с заданным типом данных и функциями-членами. совместимые с данным событием.
Существует пятый спецификатор доступа, automatic , который используется для определения общедоступного интерфейса с соответствующей информацией о типе OLE-автоматизации, что позволяет создавать серверы OLE-автоматизации. Ключевое слово __automated используется в подклассах TAutoObject .
Имейте также в виду, что видимость свойства может быть расширена в производных классах. Защищенное свойство, например, может быть повторно объявлено как опубликованное свойство. Для этого вам не нужно переопределять свойство, а нужно только повторно объявить его (в Borland для обозначения такого поведения используется термин «поднятые свойства»). При повторном объявлении свойства вы также можете изменить его, например, изменив его значение по умолчанию.
Закрытия и события
Когда свойства имеют тип данных «указатель на функцию-член» (также известный как замыкание ), они называются событиями. Но что такое замыкание? Еще одно дополнение к стандартному языку C++. Замыкание — это своего рода указатель на функцию-член. На самом деле он связывает указатель на функцию-член с указателем на экземпляр класса, объект. Указатель на экземпляр класса используется как указатель this при вызове связанной функции-члена. Это определение замыкания:
1 |
typedef void __fastcall (__closure *TNotifyEvent)(TObject* Sender); |
Поскольку в C++ указатели на функции-члены также доступны, но редко используются, вы можете задаться вопросом, для чего нам нужна эта неуклюжая штука? Замыкания действительно важны в модели Delphi. На самом деле события — это замыкания, содержащие значение функции-члена формы, в которой размещается связанный компонент. Например, у кнопки есть замыкание с именем OnClick , и вы можете назначить ей функцию-член формы. Когда пользователь нажимает кнопку, эта функция-член выполняется, даже если вы определили ее внутри другого класса (обычно в форме). Вот что вы можете написать (и C++ Builder обычно пишет за вас):
1 2 |
BtnBeep->OnClick = BtnHelloClick; BtnHello->OnClick = BtnBeepClick; |
Приведенный выше код обменивается двумя обработчиками событий во время выполнения. Вы также можете явно объявить переменную типа замыкания, как общий тип TNotifyEvent , и использовать ее для обмена обработчиками двух событий:
1 2 3 |
TNotifyEvent event = BtnBeep->OnClick; BtnBeep->OnClick = BtnHello->OnClick; BtnHello->OnClick = event; |
Это означает, что вы можете назначить функцию-член (как BtnHelloClick ) замыканию или событию как во время разработки, так и во время выполнения.
Потоковые свойства и объекты
Классы производные от TPersistent имеют еще одну важную особенность. Вы можете сохранять объекты этих классов в поток. VCL не сохраняет внутренние данные объекта, а просто сохраняет значения всех его опубликованных свойств (и событий). Когда объект загружается, VCL сначала создает новый объект, а затем переназначает каждое из его свойств (и событий). Наиболее очевидным примером потоковой передачи объектов является использование файлов DFM. В этих бинарных файлах хранятся свойства и компоненты формы, и я предлагаю вам загрузить их в редакторе C++Builder для изучения их текстовой версии (вы также можете использовать программу командной строки CONVERT.EXE для преобразования файла DFM в TXT или наоборот)
Процесс потоковой передачи можно настроить по-разному: добавив значение по умолчанию или хранимое предложение в объявление свойств или переопределив метод DefineProperties . Когда вы добавляете свойство к компоненту, вы обычно добавляете в определение предложение по умолчанию . Если значение свойства соответствует его значению по умолчанию, свойство не сохраняется в поток вместе с другими свойствами объекта. Конструктор класса должен инициализировать свойство тем же значением по умолчанию, иначе этот метод не будет работать. Вместо этого директива Stored указывает, должно ли свойство быть сохранено в файл вместе с объектом или нет . Сохраненный _ за директивой может следовать логическое значение или функция-член, возвращающая логический результат (так что вы можете выбрать, сохранять значение или нет, в зависимости от текущего состояния объекта). Наконец, метод DefineProperties позволяет создавать псевдосвойства, добавляя дополнительные значения к передаваемому объекту и перезагружая их должным образом.
Больше RTTI ( dynamic_cast и др.)
Помимо информации RTTI, сгенерированной ключевым словом __published , у каждого объекта есть несколько методов, которые вы можете использовать для запроса самого себя. Эти методы являются частью класса TObject , базового класса каждого объекта на основе VCL. Вы можете увидеть список методов класса TObject (включая статические функции-члены) в таблице 1.
Таблица 1: Функции-члены TObject
// общедоступные функции-члены
объект
Бесплатно
Тип класса
CleanupInstance
Адрес поля
после строительства
Перед Разрушением
Отправлять
Обработчик по умолчанию
Бесплатный экземпляр
~TОбъект
// статическая публичная функция членов
InitInstance
имя класса
ClassNameIs
ClassParent
Информация о классе
размер экземпляра
Наследует от
адрес метода
имя метода
Чтобы проверить, относится ли класс к заданному типу, вы можете использовать метод ClassType . Это довольно часто встречается с параметром Sender обработчика событий. Эти параметры относятся к объекту, сгенерировавшему событие, но имеют общий тип TObject . Когда вы связываете один и тот же метод с событиями разных объектов, вы можете захотеть проверить тип объекта, вызвавшего событие:
1 2 |
if (Sender->ClassType() == __classid(TButton)) Beep(); |
__classid — еще одно ключевое слово, добавленное в C++Builder. Он возвращает метакласс объекта (см. следующий раздел). Альтернативой (не всегда допустимой) является использование параметра Sender обработчика событий и приведение его к заданному типу данных с использованием стандартной техники C++ dynamic_cast . Это имеет смысл, если мы знаем тип данных или тип общего класса-предка, как в этом случае:
1 2 3 |
TWinControl* wc = dynamic_cast<TWinControl*> (Sender); if (wc != 0) wc->Left = wc->Left + 2; |
Как я уже упоминал, помимо этих возможностей RTTI, Delphi и C++ Builder совместно используют обширную информацию о типах, доступную во время выполнения для каждого объекта класса, производного от TObject , и имеющего опубликованные поля, свойства или методы. Например, вы можете получить динамический доступ к свойству по имени; вы можете получить список имен свойств класса; вы можете получить список параметров замыкания. Это позволяет программистам создавать очень мощные дополнительные инструменты для среды и используется самой системой в качестве основы инструментов визуальной разработки, начиная с инспектора объектов (на самом деле я создал клон инспектора объектов во время выполнения) .
Метаклассы и виртуальные конструкторы
Delphi представила концепцию ссылки на класс, указателя на информацию о типе класса. В C++ Builder это отражено в идее класса TMetaclass, а тип TClass — не что иное, как указатель на этот метакласс. В Delphi у TClass аналогичная роль, но другое определение (хотя две реализации полностью совместимы).
Доступные для метакласса методы точно соответствуют статическим методам класса TObject (на самом деле в Delphi нет класса TMetaclass , а есть ссылка на класс, которая может напрямую использовать статические методы TObject ). Если у вас есть переменная TClass , вы можете присвоить ей класс, извлеченный из объекта (с помощью функции ClassType ) или полученный из самого класса с помощью ключевого слова __classid . Вот пример:
1 |
TClass myclass = __classid (TEdit); |
Вы можете использовать метакласс почти так же, как вы используете ссылку на класс в Delphi. Что невозможно в C++Builder, так это создать новый объект на основе метакласса. Это странно: C++Builder позволяет вам определять виртуальные конструкторы, но не дает возможности их вызова, если вы не вызываете метод Delphi. Вот что происходит, когда вы определяете новый компонент в C++Builder, а затем позволяете VCL обрабатывать его (например, когда вы загружаете компонент из потока, вызывается его виртуальный конструктор).
Чтобы создать объект из ссылки на класс, нам нужно добавить простой модуль Pascal в наше приложение C++Builder. Вот код функции Pascal, которую мы можем использовать:
1 2 3 4 |
function DynCreate (myclass: TComponentClass; Owner: TComponent): TComponent; begin Result := myclass.Create (Owner); end; |
Мы можем использовать эту функцию в приложении C++Builder. Это простой код, который вы можете использовать для создания компонента данного класса во время выполнения и размещения его внутри элемента управления ScrollBox:
1 2 3 4 5 6 7 8 |
void __fastcall TForm1::BtnNewEditClick(TObject *Sender) { TEdit* edit = new TEdit (this); edit->Parent = ScrollBox1; edit->Left = random (ScrollBox1->Width); edit->Top = random (ScrollBox1->Height); edit->Text = "Edit" + IntToStr (++Counter); } |
Это код, который вы можете использовать для создания компонента того же типа, что и объект Sender :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
void __fastcall TForm1::OnDynNew(TObject *Sender) { TClass myclass = Sender->ClassType(); // illegal: // TComponent* Comp = new myclass (this); TComponent* Comp = DynCreate (myclass, this); TControl* Control1 = dynamic_cast<TControl*> (Comp); if (Control1 != 0) { Control1->Parent = ScrollBox1; Control1->Left = random (ScrollBox1->Width); Control1->Top = random (ScrollBox1->Height); Control1->Name = "Comp" + IntToStr (++Counter); } }; |
Заключение
Как мы видели, Borland добавила много новых функций в язык C++ в C++ Builder, чтобы сделать этот язык совместимым с Object Pascal и адаптированным для компонентного программирования. Такое расширение C++ может показаться немного неудобным, поскольку мы пытаемся преобразовать конструкции одного языка в другой язык. Тем не менее, простота использования окружения и визуальная проработка, вероятно, стоят этой дополнительной сложности, которая в большинстве случаев остается за кадром.
В то же время C++ Builder сохраняет полную совместимость со стандартом ANSI C++, включая шаблоны и STL (чтобы назвать две функции, недоступные программистам Delphi), и позволяет смешивать код на основе VCL с кодом MFC или OWL, хотя это делает смысл только в том случае, если у вас есть существующие приложения. Библиотека VCL фактически позволяет использовать визуальное программирование и работать с более высокой абстракцией, чем типичные библиотеки классов C++ для Windows.
Есть много других различий между Delphi и C++Builder, но реализация свойств — это функция, оказывающая наибольшее влияние с точки зрения языковых расширений C++. Большинство других проблем (карты сообщений, наборы и параметры открытого массива, и это лишь некоторые из них) были решены с использованием существующих ключевых слов и языковых функций C++.
Марко Канту; внештатный писатель и консультант из Италии. Он написал книги по программированию на C++ и Delphi, переведенные на 10 языков мира. Он публикуется в нескольких журналах, любит выступать на конференциях и ведет семинары по Delphi и C++Builder по всему миру. Вы можете связаться с ним по телефону [email protected] или http://ourworld. компьютерный com/домашние страницы/marcocantu.
Излишне говорить, что приведенная выше контактная информация и веб-сайт Compuserve больше недействительны, но я решил оставить статью в том виде, в каком она была первоначально опубликована, включая мою биографию.
Design. Code. Compile. Deploy.
Start Free Trial Upgrade Today
Free Delphi Community Edition Free C++Builder Community Edition