Palm OS上的C++程序设计
本文作者: Scott Gruby,Palm OS 认证开发人员
为什么我要考虑这个问题?
作为一个长期从事 Palm OS 开发的人员,我见证了 Palm OS 的成熟和开发技术的变化。几年前,开发人员很关心内存空间(堆栈空间)的问题,因此尽量写小型、紧凑的代码。随着应用程序可用内存的增加,内存再也不是开发人员最担心的问题了。几年前,当 Metrowerks 开始允许开发人员使用 C++ 编程时(有一些限制),包括我本人在内的一些开发人员由于“代码膨胀”而不去使用它。那时候,由于设备上有限的可用空间,一些人认为面向对象/C++ 编程带来的价值不足以抵消它所造成的开销。我看到很多关于 C 和 C++ 的争论,认为 C++ 增加了开销。而在我看来,C++ 会增加开销只是个感觉问题。
从那时起,事情有了很大的变化。新的设备带有大量的内存,这使我不得不重新考虑使用 C++。去年,当我正在编写后来的 Mark/Space Mail 软件的时候,有机会使用了 Brian Hall 开发的一些 C++ 类库。虽然这些类库并不像 POL 那样丰富,而且还没有把计算机课上教授的所有 C++ 技术都用上,但它确实开阔了我的眼界,使我意识到在不同的应用程序中反复使用这些类是多么的简单。
象其它编程语言一样,使用 C++ 同样有优缺点。我见过一些商业代码,它们的作者认为所有程序都必须用 C++ 实现。我认为这些开发人员是刚刚从大学的计算机系毕业的学生,想在一个 Palm OS 程序中把从学校学到的每一样东西都用上。我觉得在 Palm OS 上这样使用C++ 并不是一个较好的方法。Palm OS 编程某些部分很好地使用了 C++,而有些部分并不使用。相反,我看到一些代码使用非常简单的C++ 特性来组织函数,并且可以很容易地访问到这些函数的公共变量。当然,肯定有开发人员不同意这个观点。如果代码由于继承、虚函数等变得太复杂而无法理解的话,那么可能就不太适合使用 C++ 了。我并不是想成为一个 C++ 专家,C++ 中的很多特性(比如,模板)我都还没有使用过。但是,由于从事了大量的 Palm OS 和 C++ 的程序设计,我知道即使是对 C++ 最基本的使用,也能通过代码重用来加快开发速度。
我发现在Palm OS 应用程序中使用 C++ 比使用 C 有如下主要优点:
- 代码重用。大多数开发人员都编写过多个 Palm OS 应用程序,这些程序之间有很多相同的部分。没有理由一遍又一遍地重新编写相同的代码。
- 一个 bug 一经修正,所有使用这个类的应用程序都会被修正。反之,如果一个类出现一个 bug,那么所有使用它的应用程序也会同时出现此 bug。在我的某些开发中使用了基于 TCP/IP 协议的类库,只要修正了主要类中的错误,就会影响所有使用它的应用程序。例如,Mark/Space Mail 和 Mark/Space Online 都使用了一个公共的 TCP 类,这样,一个 bug 的修正将同时影响这两个产品。
- 数据和函数封装。我总是教导你们,在你们的应用程序中不应该使用全局变量。然而这种做法并不实际,所以我会限制全局变量的使用,这样能提高代码的可读性,也更容易维护。把近似的功能封装成一个类,能提高代码的可读性。
在 Palm OS 和其它平台上使用 C++ 还有其他的优点,上面只是我个人喜欢使用 C++ 的原因。随着 Palm OS 设备上可用内存的增加,我没发现使用 C++ 有什么坏处。
用 C 写过多个数据库程序的人都会知道,要一遍又一遍地重写那些访问数据库的常用函数是多么枯燥。毫无疑问,你肯定会把那些主要的数据库代码一遍又一遍地复制、粘贴。这时,使用 C++ 还是比较完美的解决方案。当第一次见到数据库类的实现时,我想这是对 C++ 的最好使用。我这里有几个数据库类的实现,在这篇文章中,我将给出一个简单的数据库类,来演示在应用程序中使用 C++ 是多么容易的一件事。
本文中的代码可以在任何开发环境中使用。这些代码是免费的,但还没有经过全面测试。写这篇文章的时候,我使用 gcc 在 Mac OS X 上测试过这些代码。
让我们开始吧。Palm OS SDK 中的大部分示例代码都访问了一个或多个数据库。对数据库的访问遍布整个程序,而且每个程序用来访问数据库的函数都很相似。例如,示例 Mail 通过下面的代码来取得一条记录:
Err MailGetRecord (DmOpenRef dbP, UInt16 index, MailDBRecordPtr r,
MemHandle * handleP)
{
MemHandle handle;
MailPackedDBRecordPtr src;
handle = DmQueryRecord(dbP, index);
ErrFatalDisplayIf(DmGetLastErr(), "Error Querying record");
src = (MailPackedDBRecordPtr) MemHandleLock (handle);
if (DmGetLastErr())
{
*handleP = 0;
return DmGetLastErr();
}
MailUnpackRecord (src, r);
*handleP = handle;
return 0;
}
而示例 Address 的代码如下:
Err AddrDBGetRecord(DmOpenRef dbP, UInt16 index, AddrDBRecordPtr recordP,
MemHandle *recordH)
{
PrvAddrPackedDBRecord *src;
*recordH = DmQueryRecord(dbP, index);
src = (PrvAddrPackedDBRecord *) MemHandleLock(*recordH);
if (src == NULL)
return dmErrIndexOutOfRange;
PrvAddrDBUnpack(src, recordP);
return 0;
}
你们也看到了,这两个函数做的事情实际上是一样的。但是,它们在错误检查时稍微有一点区别。我怀疑其中一个曾经修改过,而另一个却没有。如果这两个函数都被封装在类 Cdatabase 中,那么对其中一个的修改也会应用到另一个上。这样,就不必在几个项目之间查找相同的 bug,进行相同的修改。
我编写过的所有数据库访问程序差不多都会用到以下几个简单的函数:打开、关闭、查询记录、创建记录、删除记录和排序。在对数据库进行处理时还会用到很多其他的函数,这里只是简单介绍一下除排序和创建记录之外的函数。我们使用基本的 Cdatabase 类和固有的 CmailDatabase 类来演示重用类十分容易。记住,这只是个示例,或许并不能直接用在您的程序中。另外,其他开发人员都有不同的编码风格,以不同的方式组织类。这里只是用示例来演示 C++ 类是如何简化开发过程的。只要编写完基本的 Cdatabase 类,所有的应用程序都可以使用它。创建这个类需要一些时间,但从这个类衍生其他类时会节省更多的时间。
Cdatabase 类的头文件类似下列代码:
#ifndef __CDatabase_h__
#define __CDatabase_h__
#define noRecordSelected 0xFFFF
class CDatabase
{
public:
CDatabase(void);
~CDatabase(void);
Err Open(UInt32 type, UInt32 creator, UInt16 mode, Char *name);
Err RemoveRecord(UInt16 recordIndex);
Err RemoveCurrentRecord(void);
protected:
UInt32 mType;
UInt32 mCreator;
Err Create(Char *name);
private:
DmOpenRef mDatabaseRef;
UInt16 mCurrentRecordIndex;
Err QueryRecord(UInt16 recordIndex, void *recordP, MemHandle *recordHandle);
Err UnpackRecord(void *src, void *destination);
};
#endif
这个类的构造函数和析构函数都很直观。
// CDatabase constructor
CDatabase::CDatabase()
{
mCurrentRecordIndex = noRecordSelected;
mDatabaseRef = NULL;
mType = 0;
mCreator = 0;
}
// CDatabase destructor
CDatabase::~CDatabase()
{
if (mDatabaseRef != NULL)
{
(void) DmCloseDatabase(mDatabaseRef);
}
}
如果数据库处于打开状态,析构函数会关闭它。在析构函数中关闭数据库并且确保删除用完的对象,这样可以保证数据库总能关闭。
我发现在大多数情况下,当打开数据库时,如果数据库不存在,就需要创建一个。因此,Open 方法考虑了这一点,它会在数据库不存在的时候试图创建一个。
// CDatabase open
Err CDatabase::Open(UInt32 type, UInt32 creator, UInt16 mode, Char *name)
{
Err err = errNone;
if (type == 0 creator == 0)
{
return dmErrCantFind;
}
mType = type;
mCreator = creator;
mDatabaseRef = DmOpenDatabaseByTypeCreator(type, creator, mode);
if (mDatabaseRef == NULL)
{
err = DmGetLastErr();
// If we can't open the database because it doesn't exist, create it
if (err == dmErrCantFind)
{
err = Create(name);
if (err == errNone)
{
mDatabaseRef = DmOpenDatabaseByTypeCreator(type, creator, mode);
if (mDatabaseRef == NULL)
{
err = DmGetLastErr();
}
}
}
}
return err;
}
// Create the database given a name
Err CDatabase::Create(Char *name)
{
Err err = errNone;
if (name == NULL)
{
return dmErrCantFind;
}
err = DmCreateDatabase(0, name, mCreator, mType, false);
return err;
}
对于熟悉 Palm OS 数据库调用的开发人员来说,这些代码无需解释。这一段代码主要是试图以指定的类型和创建者打开数据库(当然,这一段代码没有处理需要访问一个或多个相同类型和创建者的数据库的情况,例如,从一组电子图书中读取数据)。这一段代码可以很好地用在 Address Book 和 Mail 程序中。
所有使用数据库的应用程序都要访问数据库中的记录。您会记得,在以只读的方式检索记录时使用 Query 调用。Cdatabase 类的查询例程中,查询到记录后会把它“解压缩”。如 Address Book 和 Mail 应用程序的示例所示,数据库中的记录是以压缩格式保存的,这样可以节省空间。由于每个数据库以不同的方法对记录进行压缩和解压缩,所以我只创建了一个通用的解压缩例程,在 Address Book 和 Mail 程序中将由类覆盖。
Err CDatabase::QueryRecord(UInt16 recordIndex, void *recordP, MemHandle *recordHandle)
{
void *src;
Err err = errNone;
mCurrentRecordIndex = noRecordSelected;
if (mDatabaseRef == NULL)
{
return dmErrNoOpenDatabase;
}
*recordHandle = DmQueryRecord(mDatabaseRef, recordIndex);
if (*recordHandle == NULL)
{
return dmErrNotValidRecord;
}
src = MemHandleLock(*recordHandle);
if (src == NULL)
{
return dmErrIndexOutOfRange;
}
err = UnpackRecord(src, recordP);
if (err != errNone)
{
MemHandleUnlock(*recordHandle);
}
mCurrentRecordIndex = recordIndex;
return err;
}
Err CDatabase::UnpackRecord(void *src, void *dest)
{
Err err = errNone;
return err;
}
我在基类中提供的最后一个方法用来删除记录。必须首先创建记录,然后才能删除。创建记录的方法留给读者作为练习。
Err CDatabase::RemoveRecord(UInt16 inRecordIndex)
{
Err err = errNone;
if (mDatabaseRef == NULL)
{
return dmErrNoOpenDatabase;
}
err = DmRemoveRecord(mDatabaseRef, inRecordIndex);
if (inRecordIndex == mCurrentRecordIndex)
{
mCurrentRecordIndex = noRecordSelected;
}
return err;
}
Err CDatabase::RemoveCurrentRecord(void)
{
return RemoveRecord(mCurrentRecordIndex);
}
这些函数并不复杂,却演示了如何在基类中添加简单的错误处理机制,这样就不至于整个程序都要进行同样的错误处理。你们在自己的代码里也执行了错误处理,是这样吗?由于 RemoveRecord 方法已经处理了数据库引用为空的情况,所以就不需要每次调用 DmRemoveRecord 的时候都去检查。
现在基类创建完成,本文剩下的部分将给出一个简单的衍生类,这个类可以用在 Mail 应用程序中,并且在这个新类中可以使用一些调用。
要建立 CmailDatabase 类,只需覆盖一个方法(在本例中为 UnpackRecord方法)。
#ifndef __CMailDatabase_h__
#define __CMailDatabase_h__
#include "CDatabase.h"
class CMailDatabase : public CDatabase
{
private:
Err UnpackRecord(void *src, void *destination);
};
#endif
UnpackRecord 函数如下所示:
Err CMailDatabase::UnpackRecord(void *inSrc, void *inDest)
{
Err err = errNone;
MailPackedDBRecordPtr src = inSrc;
MailDBRecordPtr dest = inDest;
char *p;
dest->date = src->date;
dest->time = src->time;
dest->flags = src->flags;
p = &src->firstField;
// Get the "subject" field.
if (*p)
{
dest->subject = p;
p += StrLen(p) + 1;
}
else
{
dest->subject = p;
p++;
}
// Etc.
return err;
}
正如你所看到的,只要很少的工作,就可以创建一个能打开数据库并访问记录的 CmailDatabase 类。在我们的主代码中,访问数据库的所有代码是:
gMailDatabase = new CMailDatabase;
gMailDatabase->Open(mailDBType, sysFileCMail, dmModeReadWrite, "MailDatabase");
使用完毕后,只要删除对象即可:
delete gMailDatabase;
虽然这个例子非常简单,但它确实演示了为常用函数创建通用 C++ 类的功能。要使这个类的功能比较全面,还要给它添加用于创建记录、排序等的方法。另外,可能还要添加一个方法,用于把 QueryRecord 方法的返回值转换到正确类型,这样就不用每次调用的时候都要进行转换了。
使用 C++ 时有一些注意事项,本文尚未提及。它们包括:
- 非全局代码使用虚函数(大部分情况下做不到这一点)。
- 异常处理会占用额外内存。如果使用了 C++ 异常处理,编译器将会为此生成额外的代码。如果不需要异常处理,就要确信在编译器中已经关闭了这一项。
- 异常处理的局限性。在某些特定情况下,异常处理机制无法工作。例如,不能跳过 OS 调用抛出异常(在事件处理函数中,不能跳过 FrmDispatchEvent 抛出异常然后再返回到主事件循环)。
虽然 C++ 在 Palm OS 的开发中有这么多优点,但遗憾的是大多数基本的 Palm OS 程序设计书籍都没有提及 C++。另外,示例代码也许会对 C++ 进行检查修改,并简化代码。正如本文所演示的一样,至少有两个(或许更多)示例使用了重复的非常相似的例程(不完全相同)。只要熟悉 C++ 的使用方法,就没有理由不把它用在 Palm OS 的程序设计中。对于大部分代码而言,C++ 的代码复用和方便的数据封装这些基本使用都能使你受益匪浅。
感谢 Neil Rhodes、Steve Lemke 和 Brian Hall 为本文进行审核和提供录入。
关于作者
Scott Gruby 是一个独立的软件开发人员,他住在圣地亚哥。他从事 Palm OS 开发工作已经 6 年多了。从智能电话开发到电子邮件应用程序和键盘驱动程序的开发,他做了大量的项目。如果你需要一个有经验的开发人员或是一个用户界面的专家,他是一个很合适的合作者。通过 palmdeveloper@gruby.com 可以联系到作者。