franksunny的个人技术空间
获得人生中的成功需要的专注与坚持不懈多过天才与机会。 ——C.W. Wendte

symbian官方推荐使用活动服务对象(CActive)来代替多线程的使用,我想这个道理是很明了的,在手机这样的小内存设备里,运行多线程的程序是非常耗资源的,为了节约资源,symbian提供了一个活动服务对象的框架,允许把程序里并发执行对象(其实不是并发,不过宏观上看来是)放在一个线程里面执行,这些并发工作的对象就通过活动规划器(ActiveScheduler)来进行管理.

关于这两个东西的介绍,网上有一大堆的文档,我就不在这里废话了,如何使用呢?这里我先举一个简单的计数器的例子.我选择写一个exe的程序,也就是说程序是以E32Main为入口的.

GLDEF_C TInt E32Main()

{

    CTrapCleanup* cleanup=CTrapCleanup::New();

    TRAPD(error,callInstanceL());

    if (error != KErrNone)

    {

        printf("get error %d\r\n", error);

    }

    delete cleanup;

    return 0;

}

以上的内容是每一个exe文件都应该做的,CTrapCleanup* cleanup=CTrapCleanup::New()建立一个清除堆栈,以便程序在异常退出的时候把清除堆栈里面的资源都释放掉.当然你也可以加上堆检测宏(__UHEAP_MARK,__UHEAP_MARKEND,这里我就不多说了。TRAPDsymbian里面经常使用的宏,功能类似于try,第一个参数是让定义一个错误返回值变量的名字, 后面就是可能有异常的你写的函数.当这个函数异常时,程序不会crash, 你可以得到异常的原因.可以参考nokia论坛上的一些关于这些使用的文档.

接下来是vcallInstanceL函数,在这个函数里面我来建立ActiveScheduler.

LOCAL_C void callInstanceL()

{

    CActiveScheduler* scheduler = new(ELeave) CActiveScheduler();

    CleanupStack::PushL(scheduler);

    CActiveScheduler::Install(scheduler);

    TRAPD(error,doInstanceL());

    if(error)

    {

        printf("error code=%d\r\n",error);

    }

    else

    {

        printf("OK!\r\n[press any key]");

    }

    CleanupStack::PopAndDestroy(scheduler);

}

这段程序很简单就是创建一个活动规划器,并压入清除栈,然后安装活动规划器,这样就可以用了.再执行真正的实例函数,最后出栈销毁。doinstance(该函数将在最后的代码中给出,主要的功能就是调用我们自己写的活动计数器)我们放到最后来写,现在来构造我们的活动计数器对象。

class TimeCount : public CActive

{

public :

    static TimeCount* NewLC(); // 构造函数

    ~TimeCount();

    void StartL();             // 计数开始

    void ConstructL();

    void RunL();             // 延时事件到达以后的处理函数

    void DoCancel();         // 取消请求提交

    void setDelayTime(int delayTime);

private:

    TimeCount();

    RTimer iTimer;          // 定时器

    int iTimeCount;         // 计数器

     int mTime;            // 计数间隔时间 单位秒

};

 

TimeCount::TimeCount():CActive(0)  // 这里可以设置活动对象的优先级

{

    // 把自己加入活动规划器

    CActiveScheduler::Add(this);

}

 

TimeCount* TimeCount::NewLC()

{

    TimeCount* result = new (ELeave) TimeCount();

    CleanupStack::PushL( result );

    result->ConstructL();

    return result;

}

 

void TimeCount::DoCancel(void)

{

    iTimer.Cancel();

}

 

void TimeCount::setDelayTime(int mTime)

{

    DelayTime = mTime;

}

 

TimeCount::~TimeCount()

{

    Cancel();

    iTimer.Close();

}

 

void TimeCount::StartL()

{

    // 设定定时器状态为每隔mTime秒钟状态完成一次

    iTimer.After(iStatus, 10000 * 100 * mTime);

    // 提交异步请求

    SetActive();

}

 

void TimeCount::ConstructL()

{

    // 初始化计数器和定时器

    iTimeCount = 0;

    User::LeaveIfError(iTimer.CreateLocal());

}

 

void TimeCount::RunL()

{

    // 计数器+1以后继续提交延时请求事件

    printf("The Count is ->>%d", iTimeCount++);

    StartL();

}

每一个活动服务对象都有一个iStatus来标识当前对象的状态.在这里我们把iStatus设定为iTimer.After(iStatus, 10000 * 100 * mTime);也就是定时器定时mTime秒钟以后iStatus发生改变,这个时候活动规划器会收到这个状态的改变,从而调用相应活动对象的处理函数,也就是RunL函数.RunL函数里面进行计数和输出,然后调用startL重新设置定时器和对象状态,再提交给活动规划器。这样mTime秒钟以后活动规划器会再次调用RunL函数.一直这样重复,这样就达到了计数器的效果。

最后我们来写doinstanceL函数

LOCAL_C void doInstanceL()

{

    TimeCount* timeCount = TimeCount::NewLC();

    // 每隔一秒钟打印一次

    TimeCount->setDelayTime(1);

    TimeCount->StartL();

    CActiveScheduler::Start();

    CleanupStack::PopAndDestroy(1);

}

创建好对象以后,加上CActiveScheduler::Start()程序就开始运行了,这句话告诉活动规划器该等待对象的状态的改变了(正常情况下,一旦CActiveScheduler::Start()之后,程序直到CActiveScheduler::Stop()才能终止运行),在这里就是timeCountiStatus的改变.iStatus改变并调用了RunL以后,继续等待iStstus的改变,这样我们使用活动对象的计数器就能够通过消息驱动运行起来了.

这里的CActiveScheduler只管理了一个CActive对象,就是timeCount,可以用类似的方法实现多个CActive,并且都加入CActiveScheduler,CActiveScheduler将会等待所有加入它的CActive的状态的改变,其中有一个的状态改变就会去执行对应的活动对象的处理函数,当状态同时发生的时候,会通过对象的优先级来决定先调用谁的RunL函数.CActiveScheduler也是非抢占式的,当一个RunL函数还没有执行完的时候,如果另一个CActive的状态改变,会等待RunL执行完以后再执行另一个CActive的处理函数(正因为这一点,所以通常RunL函数不能设计为长函数,否则会阻塞活动对象)

 本文在网上根据网上用人提供的原本阅读学习而成,可算是转载类型的。

 

 

posted @ 2008-10-11 21:03 frank.sunny 阅读(2545) | 评论 (0)编辑 收藏

Active Object (AO) 框架,是Symbian的基本工作部分。它是为了满足多个任务同时执行的要求。在 Windows/Unix 平台上,我们可以不加思索的使用多线程来完成多任务。可是在嵌入式平台上,系统的资源是有限的。比如CPU、内存都比我们平时用的个人计算机要低。这就要求嵌入式系统能够合理的使用系统资源。不能频繁的切换线程或者进程。

Symbian为这种特别需求设计了Active Object (AO)框架。AO框架是运行于一个线程内部的调度框架。其基本思想就是把一个单线程分为多个时间片,来运行不同的任务

这和多线程有很大区别。多线程之间是可以被抢占的(由操作系统调度),但是AO框架中的各个任务是不可被抢占的,一个任务必须完成,才能开始下一个任务

下面是多线程和AO框架的简单比较:

多线程                                       AO框架

可以被抢占                                   不可被抢占

上下文切换耗费CPU时间                       没有上下文切换

由操作系统调度                               由线程自己的AO框架调度

每个线程都有至少4K Stack.                    AO没有单独的Stack.

操作系统还要分配额外的资源记录线程            只是一个Active Object.

Symbian系统本身使用了大量的AO框架来实现一些系统服务。这使得Symbian和其他嵌入式系统相比较,对系统资源的使用更加合理。

AO框架包括CActiveScheduler CActive (Active Object)。一个线程的所有的Active Object对象都被安装在该线程的CActiveScheduler对象内.CActiveScheduler对象监控每个Active Object是否完成了当前任务,如果完成了,就调度下一个Active Object来执行。CActiveScheduler根据优先级来调度各个Active Object.

 

关于CActiveScheduler的创建和安装,CActive的创建和安装,和CActive的任务处理,可以参看 SDK 文档。理解起来不难。下面要说一个比较容易忽略的地方。这对理解AO框架非常重要。

创建调度器:

CActiveScheduler * scheduler = new (ELeave) CActiveScheduler;

CleanupStack::PushL(scheduler);

CActiveScheduler::Install(scheduler);

 

运行调度器:

CActiveScheduler::Start();

 

停止调度器:

CActiveScheduler::Stop();

 

以上代码都是运行在一个线程中的,一般来讲,一个EXE只有一个主线程.

可是如果真的有2个线程呢?

为什么在当前线程下调用CActiveScheduler::Start(),CActiveScheduler::Stop()运行/停止的就是当前线程的调度器而不是另一个线程的调度器?

每个线程都有自己的CActiveScheduler,那么这个CActiveScheduler类是怎么调用CActiveScheduler::Start(),CActiveScheduler::Stop()来运行/停止当前的调度器的呢?

我们看到Start/Stop并没有参数.

打开CActiveScheduler的类定义:

class CActiveScheduler : public CBase

    {

public:

    IMPORT_C CActiveScheduler();

    IMPORT_C ~CActiveScheduler();

    IMPORT_C static void Install(CActiveScheduler* aScheduler);

    IMPORT_C static CActiveScheduler* Current();

    IMPORT_C static void Add(CActive* anActive);

    IMPORT_C static void Start();

    IMPORT_C static void Stop();

    IMPORT_C static TBool RunIfReady(TInt& aError, TInt aMinimumPriority);

    IMPORT_C static CActiveScheduler* Replace(CActiveScheduler* aNewActiveScheduler);

    IMPORT_C virtual void WaitForAnyRequest();

    IMPORT_C virtual void Error(TInt anError) const;

private:

    void DoStart();

    void OwnedStartLoop(TInt& aRunning);

    IMPORT_C virtual void OnStarting();

    IMPORT_C virtual void OnStopping();

    IMPORT_C virtual void Reserved_1();

    IMPORT_C virtual void Reserved_2();

private:

    // private interface used through by CActiveSchedulerWait objects

    friend class CActiveSchedulerWait;

    static void OwnedStart(CActiveSchedulerWait& aOwner);

protected:

    inline TInt Level() const;

private:

    TInt iLevel;

    TPriQue<CActive> iActiveQ;

    };

 

我们并没有看到静态的成员来表示线程,但是却有一个static函数CActiveScheduler* Current();返回当前线程的调度器.

现在猜想奥秘就在这个函数是怎么实现的。这个静态的函数怎么就能得到当前这个运行线程的调度器,而不是别的线程的调度器。我们可以猜想,肯定是Current()内部实现能取到当前线程的标识信息.用这个标识,静态函数能取到这个线程的CActiveScheduler.这个具体如何实现呢?

答案就是:当前线程的线程ID可以这样得到:

创建一个缺省的线程对象:

RThread thread;

取得当前线程的ID:

TThreadId threadId = thread.Id();

能得到当前线程的threadId,当然可以得到和当前线程关联的CActiveScheduler。因此以上两个问题也就迎刃而解了,在一个线程内调用CActiveScheduler::Start(),CActiveScheduler::Stop()开启的就是当前线程。

既然回复了上面的问题,那么我们自然就能明确,在一个线程内是不能通过StartStop函数来开启和停止另一个线程内的活动对象规划器CActiveScheduler

 

补充点其他东西:

Symbian操作系统中,每个进程都有一个或多个线程。线程是执行的基本单位。一个进程的主线程是在进程启动时生成的。Symbian属于抢占式多任务操作系统,这意味着每个线程都有自己的执行时间,直到系统将CPU使用权给予其他线程。当系统调度时,具有最高优先权的线程将首先获得执行。

进程边界是受内存保护的。所有的用户进程都有自己的内存地址空间,同一进程中的所有线程共享这一空间,用户进程不能直接访问其他进程的地址空间。

每个线程都有它自己的stackheap,这里heap可以是私有的,也可以被其他线程共享。应用程序框架生成并安装了一个active scheduler,并且为主线程准备了清除栈。如果没有使用框架(如编写exe程序)那就要手动生成这些了。

Symbian操作系统专为单线程应用优化,因此强烈推荐使用“活动对象”代替多线程。

posted @ 2008-10-11 20:34 frank.sunny 阅读(2665) | 评论 (2)编辑 收藏

 

如何在C++中调用C的代码

 

以前曾经总结过一篇(http://www.cppblog.com/franksunny/archive/2007/11/29/37510.html),关于在C中如何调用C++的代码,当时并未做完全的展开,只是简单的做了下调试,最近看到一个题目要求实现CC++中代码的互相调用,其结果虽然都是通过extern “C”来实现,但是具体还是有些差别的。

先对C中调用C++代码作个简单回顾:

1、              对于C++中非类的成员函数,可以简单的在函数声明前面加extern “C”,通常函数声明位于头文件中,当然也可以将声明和函数定义一起放在cpp,在没有声明的情况下,直接在定义前添加extern “C”也可

2、              对于C++类的成员函数,则需要另外做一个cpp文件,将需要调用的函数进行包装

以上两项的实例参看前面C中如何调用C++代码的文章。

要实现C++中调用C的代码,具体操作:

对于C中的函数代码,要么C代码的头文件进行修改,在其被含入C++代码时在声明中加入extern “C”或者C++代码中重新声明一下C函数,重新声明时添加上extern “C”

通过以上的说明,我明白一点,那就是extern “C”头一定是加在C++的代码文件中才能起作用的

 

下面分析一下这个现象的实质原因:

C编译器编译函数时不带函数的类型信息,只包含函数符号名字,如C编译器把函数int a(float x)编译成类似_a这样的符号,C连接器只要找到了调用函数的符号,就可以连接成功,它假设参数类型信息是正确的,这是C编译连接器的缺点。而C++编译器为了实现函数重载,编译时会带上函数的类型信息,如他把上面的a函数可能编译成_a_float这样的符号为了实现重载,注意它还是没有带返回值得信息,这也是为什么C++不支持采用函数返回值来区别函数重载的原因之一,当然,函数的使用者对函数返回值的处理方式(如忽略)也是重要原因。

基于以上,C调用C++,首先需要用封装函数把对C++的类等的调用封装成C函数以便C调用,于是extern "C"的作用是:让编译器知道这件事,然后C语言的方式编译和连接封装函数通常是把封装函数用C++编译器按C++方式编译,用了extern "C" 后,编译器便依C的方式编译封装接口,当然接口函数里面的C++语法还是按C++方式编译;对于C语言部分--调用者,还是按C语言编译;分别对C++接口部分和C部分编译后,再连接就可以实现C调用C++)。相反,C++调用C函数,extern "C" 的作用是:让C++连接器找调用函数的符号时采用C的方式,即使用_a而不是_a_float来找调用函数。

 

具体示例请见http://www.cppblog.com/Files/franksunny/CCallCpp.rar

注:如果你用VC6.0编译附件时遇到类似“fatal error C1010: unexpected end of file while looking for precompiled header directive”报错的话,请将bb.c文件做如下处理右键点击项目工程中的该文件,选择setting,在c/c++栏,选择PreCompiled headers,然后设置第一选项,选择不使用预编译头。

 

posted @ 2008-10-10 17:54 frank.sunny 阅读(8950) | 评论 (1)编辑 收藏
     摘要:   活动对象框架原理   一、概述: Symbian OS是一个多任务的操作系统,那么为了实现多任务,同时使系统能够快速响应,高效的进行事件处理,并减轻应用程序员的工作负担(申请大多数耗时的操作(例如文件系统)由服务提供器来完成,服务提供器完成程序员提交的请求后,将会返回给程序员一个成功或失败的信号。),Symbian OS特意引入了活动对象的概念。 服务提供器API...  阅读全文
posted @ 2008-10-09 20:42 frank.sunny 阅读(1800) | 评论 (0)编辑 收藏

这种在Symbian C/S架构中,服务器程序与客户UI进程主动通信中用的比较多。

对于在往UI框架应用程序发送消息,可以通过Symbian OSApplication Architecture Services可以进行应用程序间的通信,主要用到的类包括:TApaTaskListTApaTask

TApaTaskList:用于访问设备中正在运行的任务(假如有些任务隐藏了的话,那么通过这种方法也无法进行访问)。

TApaTask:表示设备中某个运行的任务,通过与程序关联的窗口组(window group)标识。

具体的解决方案:

发送消息端:使用TApaTaskList找到等待接收消息的任务,TApaTaskList::FindApp()提供了两个重载版本,可以使用程序的标题,也可以使用程序的UID进行查找。获得需要发消息的任务后就可以通过TApaTask:: SendMessage()发送消息了,它有两个参数,第一个参数用于标识消息,第二个参数是一个描述符的引用,可以用来提供不同消息时附加的具体信息。

TUid uid( TUid::Uid( 0x0116C9D3 ) );

TApaTaskList taskList( iCoeEnv->WsSession() );

TApaTask task = taskList.FindApp(uid );

 

if( task.Exists() ) //判断任务是否正在运行

{

    LIT8( KTestMsg, "CustomMessage" );

    TUid msgUid( TUid::Uid( 1 ) );

    task.SendMessage( uid, KTestMsg );

}

 

接收消息端可以使用如下两种方案:

第一种方案:由于MCoeMessageObserver是处理来自窗口服务器消息的接口类,而CEikAppUi已经继承自MCoeMessageObserver,所以我们只需要在自己的UI类中重现实现MCoeMessageObserver的唯一成员函数HandleMessageL()用来处理接收到的消息即可,代码如下:

MCoeMessageObserver::TMessageResponse CXXXAppUi::HandleMessageL(TUint32 aClientHandleOfTargetWindowGroup, TUid aMessageUid, const TDesC8& aMessageParameters)

{

    _LIT( KFormatStr, "%x" );

    TBuf<32> bufUid;

    TBuf<32> bufPara;

    bufUid.AppendFormat( KFormatStr, aMessageUid.iUid );

    bufPara.Copy( aMessageParameters );

    iEikonEnv->InfoWinL( bufUid, bufPara );

    return MCoeMessageObserver::EMessageHandled;

}

 

第二种方案:由于TApaTask::SendMessage()发送的消息可以被CEikAppUI的成员函数ProcessMessageL()拦截并处理,不过必须在没有重载HandleMessageL()函数的前提下,而且函数ProcessMessageL()只负责拦截消息标识为KUidApaMessageSwitchOpenFileValueKUidApaMessageSwitchCreateFileValue的这两个消息,其它标识值的消息不会被传到ProcessMessageL()中,所以这种方案个人觉得很受限制,不自由,还是采用第一种方案好,具体代码代码如下:

//发送:

TUid uid( TUid::Uid( 0x0116C9D3 ) );

TApaTaskList taskList( iCoeEnv->WsSession() );

TApaTask task = taskList.FindApp(uid );

 

if( task.Exists() ) //判断任务是否正在运行

{

    LIT8( KTestMsg, "CustomMessage" );

    //这里的Uid不能使用自定义的,而且只有系统提供的两个

    TUid msgUid( TUid::Uid(KUidApaMessageSwitchCreateFileValue) );

    task.SendMessage( uid, KTestMsg );

}

 

//接收:

void CXXXAppUi::ProcessMessageL(TUid aUid,const TDesC8& aParams)

{

    RFileLogger iLog;

    iLog.Connect();

    iLog.CreateLog(_L("tb"), _L("UpdateListener2.txt"), EFileLoggingModeOverwrite);

    iLog.Write(_L("smms appui"));

 

    if (aUid.iUid == KUidApaMessageSwitchCreateFileValue)

    {

        TBuf<256> buf;

        buf.Copy(aParams);

        iLog.Write(aParams);

        BringMeToFront();

        ShowCreateFile(buf,CFileMonitorEngine::EImageType);

    }

    else

    {

        CAknViewAppUi::ProcessMessageL(aUid,aParams);

    }

    iLog.Close();

}


明天就是中秋了,恭祝大家中秋节快乐
posted @ 2008-09-13 07:46 frank.sunny 阅读(2056) | 评论 (1)编辑 收藏
     摘要:   关于vCard和Symbian上的操作   前阵子关于Symbian通讯录操作的时候曾提到vCard,但是由于当时项目比较紧,所以也没有时间整理,今天特意抽了点时间小试了一下,发现很多手机(我试了下索爱的和诺基亚的)如果选中通讯录中的记录发送联系人或者发送名片之类的操作,就是会以vcf文件格式进行发送。不过手机上的vcf文件通常是用UTF-8编码的,所以虽然可以用ou...  阅读全文
posted @ 2008-09-13 07:20 frank.sunny 阅读(3679) | 评论 (0)编辑 收藏
     摘要: Symbian OS平台简体汉字编程编码处理   相信大家都在处理symbian中文显示的时候遇到了编码的问题,我现在就给总结一下这种问题的解决方法: 字符串编码中文表示常用的有:GB2312,GBK,Unicode,UTF-8 其中GBK是GB2312的超集,也就是涵盖了GB2312编码的所有内容; UTF-8是Unicode的在网络传输中的一种编码格式。 如果我们使用...  阅读全文
posted @ 2008-09-10 20:11 frank.sunny 阅读(4043) | 评论 (1)编辑 收藏
     摘要: Symbian OS中的消息存储与常用操作 说明:本文前面消息的基本知识主要参考《Series60应用程序开发》中的有关内容,后面是前段做MTM开发中用到的代码。 一、消息存储基本知识 Symbian OS提供的消息传送架构基于Client/Server机制,服务器负责管理手机上的各种消息,在进行消息相关操作之前我们需要了解Symbian OS是如何组织和存储消息的。 手机中的各种消息...  阅读全文
posted @ 2008-07-30 21:04 frank.sunny 阅读(3233) | 评论 (2)编辑 收藏
     摘要:   Symbian OS应用开发学习笔记之通讯录(电话薄Contacts)   Symbian OS通讯录模型 Symbian OS手机的通讯录采用文件方式存储,用symbian自己的说法就是通讯录数据库。每个Symbian OS手机都有一个默认的通讯录数据库,这个通讯录数据库在2nd和3rd两个版本手机中的位置是不同的,前者是c:\ system\data\Conta...  阅读全文
posted @ 2008-06-27 08:05 frank.sunny 阅读(6412) | 评论 (8)编辑 收藏

 

今天接到电话面试,被问到几个问题,汗颜之余,小结一下

1、      多态是如何实现绑定的

多态的绑定可以分为运行是多态和编译时多态

编译时的多态性

编译时的多态性是通过重载来实现的。对于非虚的成员来说,系统在编译时,根据传递的参数、返回的类型等信息决定实现何种操作。

运行时的多态性

运行时的多态性就是指直到系统运行时,才根据实际情况决定实现何种操作。C#中,运行时的多态性通过虚成员实现。

编译时的多态性为我们提供了运行速度快的特点,而运行时的多态性则带来了高度灵活和抽象的特点。

今天才正式弄清楚原来虚函数是可以实现运行时多态的,以前只知道虚函数可以使得基类对象的的方法调用派生类的方法。

2、      析构函数是虚函数的优点是什么

C++开发的时候,用来做基类的类的析构函数一般都是虚函数。可是,为什么要这样做呢?下面用一个小例子来说明:

有下面的两个类:

class ClxBase

{

public:

    ClxBase() {};

    virtual ~ClxBase() {};

 

    virtual void DoSomething() { cout << "Do something in class ClxBase!" << endl; };

};

 

class ClxDerived : public ClxBase

{

public:

    ClxDerived() {};

    ~ClxDerived() { cout << "Output from the destructor of class ClxDerived!" << endl; };

 

    void DoSomething() { cout << "Do something in class ClxDerived!" << endl; };

};

 

代码

 

ClxBase *pTest = new ClxDerived;

pTest->DoSomething();

delete pTest;

 

输出结果是:

 

Do something in class ClxDerived!

Output from the destructor of class ClxDerived!

 

这个很简单,非常好理解。

但是,如果把类ClxBase析构函数前的virtual去掉,那输出结果就是下面的样子了:

Do something in class ClxDerived!

也就是说,类ClxDerived的析构函数根本没有被调用!一般情况下类的析构函数里面都是释放内存资源,而析构函数不被调用的话就会造成内存泄漏。我想所有的C++程序员都知道这样的危险性。当然,如果在析构函数中做了其他工作的话,那你的所有努力也都是白费力气。

所以,文章开头的那个问题的答案就是--这样做是为了当用一个基类的指针删除一个派生类的对象时,派生类的析构函数会被调用。

当然,并不是要把所有类的析构函数都写成虚函数。因为当类里面有虚函数的时候,编译器会给类添加一个虚函数表,里面来存放虚函数指针,这样就会增加类的存储空间。所以,只有当一个类被用来作为基类的时候,才把析构函数写成虚函数。

 

说实话,这个也是今天才深刻认识到的。

 

当然还问到很多数据结构和算法方面(空间复杂度和时间复杂度之类的东东,说真的也是基础性的)的问题,至于那些东西,自己说实话抛开没用他们已经很长时间了,真可以说忘的差不多了,考这种真的很怕,也怪平时没怎么用到。不知道大家用的多不?

好久没有正式参加过面试了,今天突然来一次觉得自己基础还是不够扎实。

posted @ 2008-05-19 20:30 frank.sunny 阅读(18079) | 评论 (12)编辑 收藏
仅列出标题
共7页: 1 2 3 4 5 6 7 

常用链接

留言簿(13)

随笔分类

个人其它博客

基础知识链接

最新评论

阅读排行榜

评论排行榜