...
...

Программируем смартфоны Symbian Series 60. Symbian C++

Сегодня мы с вами разберем ключевые особенности программирования операционной системы Symbian. То, о чем мы с вами сегодня поговорим, крайне важно для написания программ для Symbian OS. Кому-то сегодняшняя статья покажется скучноватой, и он ее пропустит, но, заверяю вас, впоследствии вы к ней еще вернетесь и перечитаете более внимательно.

Соглашение об именах


При написании программ для Symbian OS рекомендуется придерживаться определенных правил при именовании классов, структур, типов и функций. Имена классов рекомендуется начинать с большой буквы C, T, R или M.

С буквы C должны начинаться имена динамически создаваемых классов. Как правило, экземпляры таких классов создаются с помощью специальных статических функций с именами New, NewL и NewLC (об этих функциях см. ниже). Для системных классов обычно после буквы C идут три буквы, обозначающие подсистему, к которой класс относится. Наиболее часто используемые — CEik (классы пользовательского интерфейса Symbian OS) и CAkn (классы пользовательского интерфейса Series 60). Если вы посмотрите наш пример Hello World, то имена всех классов, которые мы там создавали, начинались с буквы C, и почти все расширяемые классы (за исключением CCoeControl) относились к пользовательскому интерфейсу Series 60 (имена начинаются с CAkn). Новые классы рекомендуется порождать от класса CBase.

С буквы T должны начинаться имена классов, которые ни от кого не наследуются и не должны расширяться. Так же с буквы T должны начинаться имена типов, объявленных через typedef, и имена структур, объединений и перечислений. Переменные данного типа могут быть как статическими, так и динамическими.

С буквы R должны начинаться имена классов, которые предназначены для работы с различными ресурсами (ресурсы приложения, файлы, сетевые подключения, потоки, таймеры и др.). Отличительной чертой данного вида классов является наличие функций Open() и Close(). Обычно от данных классов новые не порождаются. Переменные данного типа рекомендуется делать статическими.

С буквы M должны начинаться имена интерфейсов. Интерфейсы должны содержать только чисто виртуальные функции (pure virtual function) и не должны содержать данных.
Имена констант принято начинать с буквы K, имена элементов перечислений — с E.

Имена переменных членов классов принято начинать с маленькой буквы i вне зависимости от типа, имена локальных переменных — с маленькой буквы a. Заметьте: в отличие от Windows, первая буква имени переменной здесь не обозначает тип переменной, а только ее принадлежность к классу или к локальным переменным. Например, класс TAppEntry (точка входа в приложение) имеет две переменные — члены класса: iFullName (строка текста) и iUidType (тип TUidType).

Стандартные типы Symbian C++

Разработчики рекомендуют использовать вместо стандартных типов языка С++ типы Symbian OS:
целые знаковые: TInt, TInt8, TInt16, TInt32, TInt64;
целые беззнаковые: TUint, TUint8, TUint16, TUint32, TUint64;
вещественный тип: TReal, TReal32, TReal64;
булевский тип: TBool (значения ETrue и EFalse).

Что это за типы, я надеюсь, понятно из их имен. Тип TInt эквивалентен TInt32, а TUint — TUint32. Использование стандартных типов C++ тоже не будет ошибкой. Единственное — не рекомендуется использовать типы float и double. Дело в том, что у процессоров, используемых в устройствах с Symbian OS, нет сопроцессоров, и тип TReal специально адаптирован для такого случая.

Глобальные переменные

В программах для Symbian OS запрещено использовать глобальные переменные и переменные, объявленные как static. Переменные могут быть только локальными или членами классов. Глобальными могут быть только константы. Программу с глобальными переменными вы просто не сможете откомпилировать для устройства, хотя и сможете для эмулятора.

Обработка исключений в Symbian C++

В Symbian OS исключения играют очень важную роль. Под исключением понимается прерывание нормального выполнения потока программы в ответ на исключительную ситуацию. В стандарте языка C++ для обработки исключений используются операторы try, catch и throw. Однако механизм обработки исключений в Symbian OS отличается от стандартного. Мало того, использовать в программах для Symbian OS операторы try, catch и throw запрещено. Это связано с тем, что Symbian OS — это модифицированная система Epoc32, которая использовалась в КПК Psion. Программы для Epoc32 писались не на C++, а на специальном языке OPL. Symbian OS делалась в очень сжатые сроки, и программисты просто не успевали переделать механизм исключений языка OPL в механизм исключений C++. Исключения в Symbian OS инициируются с помощью функции User::Leave(<код ошибки>). Данная функция имеет один аргумент — код ошибки. Код ошибки представляет собой целое число. Основные коды ошибок Symbian OS объявлены в файле e32std.h.

Идентификаторы кодов ошибки обычно начинаются с KErr (подробнее, что означает каждый код, вы можете посмотреть в справке SDK — для этого просто в разделе указателя наберите KErr, и вы увидите немаленький список ошибок). Особый вариант ошибки — это код KErrNone. Этот код означает отсутствие ошибок. Его нельзя инициировать с помощью User::Leave. Этот код используется только при проверке результатов функций и перехвате исключений. Кроме функции User::Leave(), для инициирования исключения могут использоваться следующие функции:

User::LeaveNoMemory() — инициируется исключение с кодом KErrNoMemory (нехватка памяти);
User::LeaveIfError(<код ошибки>) — инициируется исключение только если код ошибки не равен KErrNone;
User::LeaveIfNull(<указатель>) — инициируется исключение с кодом KErrNoMemory, если указатель равен NULL.

Для перехвата исключений используются макросы TRAP и TRAPD. Пример:
TInt error;
TRAP(error, FuncL());

Первым аргументом макроса должна быть переменная типа TInt, в которую будет записан код ошибки. Вторым аргументом будет код, в котором могут инициироваться исключения (в нашем примере это функция FuncL()). Если во время выполнения функции FuncL() возникнет исключение, то его код ошибки будет записан в переменную error. Если исключений не возникнет, то в error будет записано значение KErrNone.
TRAPD отличается от TRAP тем, что он предварительно объявляет переменную, в которую будет записываться код ошибки.
TRAPD(error, FuncL());

Будет эквивалентен приведенному выше примеру. Т.е. переменную error в данном случае не надо объявлять.

У вас, наверное, возникает вопрос: а как определить, может ли некоторая функция выкидывать исключения или нет? Чтобы сразу было видно, какие функции могут инициировать исключения, разработчики Symbian OS решили в конце имен таких функций добавлять большую букву L. Допустим, возьмем класс CHelloWorldApplication из нашего примера (см. предыдущую статью). У него две функции: AppDllUid() и CreateDocumentL(). AppDllUid() не имеет в конце буквы L, и значит, она не может выкидывать исключения, а CreateDocumentL() — может.

Теперь о том, в каких случаях вы обязаны в вашей программе перехватывать исключения, а в каких можете не перехватывать. Когда вы пишете программу, то все пользовательские действия вы инициируете в переопределенных системных функциях (например, HandleCommandL в нашем примере Hello World). Так вот, если имя переопределяемая функция заканчивается на L (а таких большинство), то вы можете не перехватывать исключения — система сама их обработает. А если в конце имени нет L, то вы обязаны перехватывать все исключения, иначе они могут привести к аварийному закрытию программы.

Исключения при выделении памяти

Результатом работы стандартного оператора new в случае нехватки памяти (ресурсы смартфона ограничены, и такая ситуация — не редкость) является нулевой указатель NULL. Далее мы можем проанализировать полученный указатель и инициировать исключение в случае, если он нулевой. Этот код будет выглядеть так:
CObject *a = new CObject;
User::LeaveIfNull(a);

Так как в программе обычно память выделяется довольно часто, то разработчики Symbian OS ввели модифицированный оператор new (ELeave), который выделяет память, а в случае ее нехватки инициирует исключение с кодом KErrNoMemory. При использовании new (ELeave) предыдущий пример будет выглядеть так:
CObject *a = new (ELeave) CObject;

CleanupStack

Одним из главных принципов создания приложений для Symbian OS является отсутствие утечек памяти. Давайте рассмотрим пример:
CObjectA *object = new (ELeave) CObjectA;
FuncL();

delete object;

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

CObjectA *object = new (ELeave) CObjectA;
TRAPD(error, FuncL());
if(error != KErrNone) {
delete object;
User::Leave(error);
}

delete object;

Ну, если у нас один объект и одна функция, то этот код еще может сойти, а если локальных динамических объектов несколько, и вызывается несколько функций, инициирующих исключения, представляете, во что превратится код программы? Чтобы упростить написание программ, в Symbian OS ввели специальный объект, который называется стек очистки CleanupStack. В стек очистки с помощью функции CleanupStack::PushL() заносятся указатели на локальные динамические объекты. В случае возникновения исключения обработчик TRAP (или TRAPD) автоматически удаляет все объекты, занесенные в стек очистки. Если исключений не было, то перед удалением объекта его указатель надо извлечь из стека очистки с помощью функции CleanupStack::Pop(). CleanupStack::Pop() удаляет из стека последний занесенный туда указатель. Если надо удалить несколько указателей, то можно просто указать в функции их количество — например, CleanupStack::Pop(3) удаляет из стека очистки три последних внесенных туда указателя. Если надо удалить не последний внесенный туда указатель, то он просто указывается в функции — например: CleanupStack::Pop (object). С использование стека очистки наш пример будет выглядеть так:

CObjectA *object = new (ELeave) CObjectA;
CleanupStack::PushL(object);
FuncL();

CleanupStack::Pop();
delete object;

Как видите, код стал проще. Однако его можно еще упростить. Для этого надо использовать функцию CleanupStack::PopAndDestroy(). Эта функция удаляет не только указатель из стека, но и сам объект. Т.е.:
CleanupStack::PopAndDestroy();

эквивалентно
CleanupStack::Pop();
delete object;

Однако, если вы попытаетесь сделать так:
TInt *array = new (ELeave) TInt[100];
CleanupStack::PushL(array);

CleanupStack::PopAndDestroy();
то при возникновении исключения или при вызове CleanupStack::PopAndDestroy() ваша программа аварийно закроется. Дело в том, то массивы удаляются оператором delete[], а не delete. Поэтому для занесения в стек очистки указателей на массивы используется специальная функция CleanupArrayDeletePushL(). Правильный вариант последнего фрагмента будет такой:
TInt *array = new (ELeave) TInt[100];
CleanupArrayDeletePushL(array);

CleanupStack::PopAndDestroy();

Функция CleanupArrayDeletePushL() работает так: она создает еще один объект, который и заноситься в стек очистки. В деструкторе этого нового объекта удаляется исходный массив. Некоторые функции, создающие объекты, перед возвращением указателя на созданный объект заносят его в стек очистки. Такие функции имеют в конце имени большую букву C. Например:
HBufC *buffer = HBufC::NewLC(100);

В данном примере у функции NewLC в конце имени есть буква L, означающая, что функция может выкидывать исключения, и буква C, говорящая о том, что возвращаемый указатель уже занесен в стек очистки. Для корректной работы стека очистки необходимо, чтобы все пользовательские классы (не порожденные от системных) порождались от класса CBase. В конце рассказа о стеке очистки хочу еще раз заострить ваше внимание на следующем: в стек очистки заносятся только указатели на локальные динамические объекты. Не заносите туда указатели на динамические объекты — члены класса. Члены класса должны удаляться в деструкторе, а если вы их занесете в стек очистки, и они удалятся при возникновении исключения, то потом в деструкторе произойдет попытка повторного удаления с непредсказуемыми для программы последствиями.

Двухфазный конструктор

Давайте рассмотрим простой пример:
class CObjectA {
private: CObjectB *b;
public: CObjectA() {
b = new CObjectB;
funcL();
}
~CObjectA() { delete b;}
};

В конструкторе данного класса создается динамическая переменная — член класса, а затем вызывается некоторая функция funcL(). Если функция funcL() инициирует исключение, то объект создан не будет, однако память под переменную — член класса уже выделена. В результате получаем утечку памяти. Можно, конечно, использовать стек очистки, но разработчики Symbian OS рекомендуют поступать по-другому.

Конструктор рекомендуется разделять на две части (так называемый двухфазный конструктор): первая — непосредственно конструктор класса, вторая — функция ConstructL(). В конструкторе класса всем указателям — членам класса надо присваивать значение NULL, а создавать динамические члены класса в функции ConstructL(). Конструктор класса и функцию ConstructL() рекомендуется объявлять как private или protected, а создавать экземпляр класса с помощью статической функции с именем New, NewL или NewLС. Таким образом, согласно рекомендациям разработчиков Symbian OS, правильная реализация нашего класса будет иметь следующий вид:

class CObjectA {
private: CObjectB *b;
public: static CObjectA* NewLC() {
CObjectA *a = new (ELeave) CObjectA;
CleanupStack::PushL(a);
a->ConstructL();
return a;
}
static CObjectA* NewL() {
CObjectA *a = NewLC();
CleanupStack::Pop();
return a;
}
private: CObjectA() { b = NULL;}
void ConstructL() {
b = new (ELeave) CObjectB;
funcL();
}
public: ~CObjectA() { if(b != NULL) delete b;}
};
Именно по такой схеме создаются все сервисные классы ОС.

Работа с текстом

Родным форматом для Symbian OS является Unicode. Это означает, что в данной системе нет проблем с поддержкой любого языка (главное — чтобы шрифты были). Symbian OS использует свой специфический формат для представления строк текста. Базовым классом для строк является тип TDesC. В этом классе объединены все основные функции для работы со строками. Однако переменные данного типа создавать нельзя. Для создания переменных типа строка используются следующие типы:

TBuf — используется для создания статических строк, располагающихся в стеке. Данный тип объявлен как шаблон. При объявлении переменных данного типа необходимо указывать максимальную длину строки. Пример — строка с максимальной длиной 50 символов:
TBuf<50> aStr;

HBufC — используется для создания динамических строк, располагающихся в куче (heap). Создается с помощью статических функций New, NewL и NewLC. Пример — также строка с максимальной длиной 50 символов.
HBufC *aStr = HBufC::NewL(50);
TPtr — используется для создания строк на основе массива символов. Переменные данного типа обычно статические. В качестве аргументов конструктор получает адрес массива и его максимальную длину. Пример:
TUint16 aBuffer[50];
TPtr aStr(aBuffer, 50);

Для объявления именованных строковых констант используется макрос _LIT, который имеет два аргумента: первый — имя константы, второй — строка, связываемая с этой константой. Пример:
_LIT(KName, "Name");
TBuf<25> aStr;
aStr = KName;

В этом примере объявляется константа KName, содержащая текст "Name", затем этот текст копируется в текстовую переменную aStr. Заметьте: копирование текста выполняется с помощью оператора присваивания. Часто нет необходимости в том, чтобы давать строковой константе имя (например, она используется только один раз). В этом случае необходимо использовать макрос _L. У него только один аргумент — строка текста. Пример: TBuf<25> aStr;
aStr = _L("Name");

Когда вы видите, что какой-то функции надо передать строку типа TDesC, вы должны указать в качестве данного аргумента переменную типа TBuf, HBufC или TPtr. Если аргумент указан как const TDesC&, то вы еще можете использовать именованную константу или макрос _L. Подробнее о строковых типах вы можете почитать в справке SDK.

На сегодня все. В следующий раз мы займемся оформлением основного окна приложения.

Алексей Аношенко

© Компьютерная газета

полезные ссылки
Корпусные камеры видеонаблюдения
IP камеры видеонаблюдения