ATL炒冷饭学习之一:COM对象与C++对象的区别

2 篇文章 0 订阅
2 篇文章 0 订阅

ATL炒冷饭学习之一:COM对象与C++对象的区别

ATL(Active Template Library,活动模板库)是微软开发的一套 COM(Component Object  Model,组件对象模型)支持库。

1. 二进制对象标准

C++ 的对象布局

众所周知,在 C++里面,一个类可以拥有成员数据和成员函数,如下面的类:就是一个具有两个属性和一个函数的一个类:
class MyDemoClass
{
    int  number;
    string  name;
public:
    MyDemoClass(int,name);
    string getName() const;
    // ...
};

void print( const MyDemoClass& obj )
{
    cout<<obj.getName()<<endl;
}

      熟悉VC的程序员都知道,如果这个类和函数是用 VC编译出来的,那么就不要指望在 Borland C++ Builder 里面使用它。别说
lib 文件格式不一样,可能的原因是两个编译器下面的 string、使用的堆和 BCB都可能是有差别的,任何一个细微的差别,都可能导致问题,而且是 crash。所以,C++的对象标准不是二进制的,而是源代码级的。就连不同的 C++编译器都不遵循一个标准,更不要说跨语言的VB,pascal的交互了。

仅有 vtable 的类

     在 C++的类中,有一种函数叫做虚拟函数。虚拟函数的调用是“动态绑定的”,也就是说,是在运行的时候才决定的。譬如说这段代码:
class MyInterface
{
    virtual int getID() const = 0;
};
MyInterface* p = getSomeObject();
cout<<p->getID()<<endl;

这段代码中的 p->getID()的调用,可能和下面的伪代码差不多:
function_pointer* vtbl = (function_pointer*)p;
p->vtbl[0];
       也就是说,在c++中调用一个函数的时候,使用一个整数(虚函数表的下标)来唯一确定这个函数。并且,因为没有数据成员,所以这个类可能只有一个指针。这个指针的偏移量不用计算,就是0;或者说,如果 vtbl的布局是相同的,并且调用某个函数的调用规范也是相同的,那么可以在不同的编译器之间共享这个对象指针。要统一vtbl的布局和调用方式,比统一不同编译器类的布局,标准库的实现以及各种编译参数要简单多了。

COM

     COM,全称为 Component Object Model,是 MS提出的一个二进制的对象标准。它以来的就是vtable所说的相同的vtbl 布局。任何一种可以通过间接指针调用函数的语言都可以实现或者使用COM 对象,这些语言包括 C/C++,VB,Pascal,Java,...,甚至 DHTML。在COM 中,类用户所看到的就是一个又一个的“接口”,也就是只有一个 vtbl的类,它的每一个函数的调用方式是 __stdcall。

2. IUnknown - 生命期,发现新内容和版本

对象生命期
    OO 系统中,对象的所有权和生命期是一个永恒的话题:这是因为,在复杂的OO系统中,对象之间的关系非常复杂,使用中既不希望在引用一个对象的时候发现它已经不存在了,也不希望一个无用的对象继续在内存中占用系统的资源。C++提出了auto_ptr,shared_ptr,...,这一切都为了方便管理对象的生命期。在没有Garbage Collection的系统中,常用的管理方式之一就是引用计数。当需要使用一个对象的时候,把它的引用计数加一,使用完了,把它的引用计数减一。对象可以在内部维护一个计数器,也可以忽略这些信息(对于静态/栈上的对象来说)也就是说,通过一个对象引用计数使得对于不同对象生命期的管理具有同样的接口。
IUnknown接口具有两个方法:
ULONG AddRef();
ULONG Release();
     前者可以把接口的引用计数加一,后者可以把接口的引用计数减一(请注意,是接口的引用计数!这和对象的引用计数有区别)同时使用引用计数还可以避免一个问题,就是创建形式不同的问题。譬如说,如果你在一个dll 里面有一个函数:string* getName();。你在 exe里面调用这个函数,获得一个指针,当使用完了这个指针以后,可能会delete 它,因为在 dll 里面,这个指针是通过 new创建的。可是这样很可能会崩溃,因为你的 dll 和你的 exe可能是用不同的堆,这样,exe 的 delete在自己的堆里面寻找这个指针,往往会失败。

发现新内容

C++里面,如果拥有一个基类的指针,可以通过强制类型转换获得派生类的指针,从而获得更丰富的功能:
class Derived:public Base
{
public:
    virtual void
anotherCall();
};
Base* b = getBase();
Derived* d = (Derived*)b;

    可是,通常这被认为是一个很危险的操作,因为如果你不能确认这个 b的确指向了一个 d,那么接下去对 d 的使用带来的很可能是程序崩溃。C++提供了一个机制,可以在类中加入类型信息,并且通过 dynamic_cast来获得有效的指针。出于种种原因(早期编译器对于 dynamic_cast支持不好,或者是类型信息的加入往往是全局的,开销很大,...)有些类库,譬如说MFC,自己实现了类似的机制。这样的机制的实现,往往是基于在类的内部维护一张表,以知道“自己”是什么,和“自己”有关系的类是哪些,...;同时,提供给外界一个可以查询这些信息的接口。要做到这个,必须解决两个问题:我怎样获得这个公共的接口,以及我用什么方法来标识一个类。在标准C++ 中,我们通过运算符 typeid 来获得一个 const type_info&。在MFC 中,因为所有支持动态类型信息的类都从 CObject及其子类继承,所以我们在 CObject中提供这些方法。这样,第一个问题对它们而言都解决了。在标准 C++
中,表示一个类的动态信息就是那个type_info,你可以比较它,判断它,或者获取一个没有确定含义的名字,但是你不能用它来做更多事情了。在MFC中,使用一个字符串来标识一个类,你可以通过一个字符串来动态的获得它所表示的类的类型信息。由于使用简单的字符串非常容易发生冲突,COM使用的是一个 128 位的随机整数 GUID ( Global Unique Identifier),这样的随机整数被认为是不太可能发生冲突的。IUnknown中的另一个方法就是:
HRESULT QueryInterface( REFIID iid,void** Interface );
       这个 REFIID就是对于一个接口的唯一标识,我们也成为接口ID。对一个接口指针调用QueryInterface,来询问另一个接口。如果它支持这个接口,会返回给你一个指针,否则会返回一个错误。

一些约定

  • 任意一个 COM 接口都必须从 IUknown 接口派生
  • 对任意一个接口 QueryInterface的结果,必须支持自反性,传递性,可逆性和持久性。也就是说:对一个interface 询问它本身必须成功

        如果对类型为 A 的接口 a 询问接口类型 B 并且成功地返回了指向 B的指针 b,那么对 b 再询问 A,一定能成立。
        如果对类型为 A 的接口 a 询问接口类型 B 并且成功地返回了指向 B的指针 b,同时从 b 询问到了接口 C 的指针 c,那么必须能够成功的从a 询问到接口 C 的指针。请注意,这个 C 的指针并不一定和前面那个 C的指针相同。(也就是说,这个约定只保证询问成功,但是不保证返回的指针相同)
       如果对接口a 询问接口 B 曾经成功过,那么以后的每次询问也将成功。

  • 如果你要传一个接口给函数,那么应该是你保证这个接口在函数返回前的生命期有效。如果一个函数返回一个接口指针,那么必须在返回前AddRef,同理,如果你从一个函数获得了一个接口指针,那么不需要再AddRef 了,但是用完后,应该 Release。
  • 特别的,根据上一条,你对一个接口调用了 QueryInterface以后,这个接口返回前已经被 AddRef 了。
  • 向同一个接口指针多次查询另一个接口,每次返回的指针不一定一样,但是,如果你查询的接口ID 是 IID_IUnknown,那么每次返回的指针都相同。这种说法等价于,指向IUnknown 的指针是一个 COM对象的标识,也就是说,比较两个接口是否属于同一个对象的唯一通用方法是对比从中Query 出来的 IUnknown 接口。此外,需要注意虽然每个 COM 接口都是从IUnknown 派生的但是你不能简单地通过把一个接口指针赋给一个IUnknown 的指针来获得一个类标识,尽管这在 C++ 中是合法的。
  • 引用计数是基于接口的,所以这段代码可能有问题

       IInterface1* p1 = getObj();
       IInterface2* p2;
       p1->QueryInterface( IID_Interface2, (LPVOID*)&p2);//假设成功
       p1->Release();
      p1->func();//这个时候,p1 可能无效!!虽然这个对象有效
      p2->Release();

3. IDL

       IDL 就是接口定义语言( Interface Definition Language)。上面介绍中说明了,COM中所有的功能都是通过接口来展现的,作为一个二进制标准,需要有一个通用的方法去描述接口。C++虽然强大,但是不适合做这件事,首先,它过于复杂,其次,它的很多数据类型别的语言不一定支持。需要有一个中立的语言来定义整个接口的数据交换情况(我们并不需要用它来定义接口的功能,因为这是依赖于具体实现的。)IDL就是这种语言。可以使用 IDL 来描述一个 COM 对象或者是 COM接口的属性:这个函数接受什么参数,每个参数的流向,...。IDL 比 C++占优势的另一点是,它比较简单,可以用工具来处理,生成的二进制信息,可以用来完全描述你的接口的外部特性。

     其实在别的 ORB 系统中,通常是把 IDL作为接口的原始定义语言。譬如说,CORBA 中,先用 IDL描述接口,然后使用某些程序转换成一个 C++ 定义,并且在这个 C++定义上继续实现接口的功能,这称为从 IDL 到 C++ 的映射(mapping)。MS的 MIDL 编译器也可以实现同样的功能,但是概念上不一样。在 COM中,脱离 IDL 直接用 C++实现一个接口,是合法行为,并且是初的常用行为;在 CORB中,可以自己写一个 C++映射,实际上是一种取巧行为。

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
使用 ATL 创建窗口可以遵循以下步骤: 1. 在 Visual Studio 中创建一个 ATL 项目。 2. 在 ATL 项目中,打开 `resource.h` 文件并添加新的资源 ID。 3. 在 ATL 项目中,打开 `MyWindow.h` 文件并添加以下代码: ```c++ class CMyWindow : public CWindowImpl<CMyWindow> { public: DECLARE_WND_CLASS(_T("MyWindowClass")) BEGIN_MSG_MAP(CMyWindow) MESSAGE_HANDLER(WM_CREATE, OnCreate) MESSAGE_HANDLER(WM_PAINT, OnPaint) MESSAGE_HANDLER(WM_DESTROY, OnDestroy) END_MSG_MAP() private: LRESULT OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { return 0; } LRESULT OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { PAINTSTRUCT ps; HDC hdc = BeginPaint(&ps); EndPaint(&ps); return 0; } LRESULT OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { PostQuitMessage(0); return 0; } }; ``` 4. 在 ATL 项目中,打开 `MyWindow.cpp` 文件并添加以下代码: ```c++ #include "stdafx.h" #include "resource.h" #include "MyWindow.h" CMyWindow::CMyWindow() { } LRESULT CMyWindow::OnCreate(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { // 创建窗口 Create(NULL, CRect(0, 0, 640, 480), _T("My Window"), WS_OVERLAPPEDWINDOW); return 0; } LRESULT CMyWindow::OnPaint(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { PAINTSTRUCT ps; HDC hdc = BeginPaint(&ps); // 绘制图形 EndPaint(&ps); return 0; } LRESULT CMyWindow::OnDestroy(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled) { PostQuitMessage(0); return 0; } ``` 5. 在 ATL 项目中,打开 `stdafx.h` 文件并添加以下代码: ```c++ #define _ATL_CSTRING_EXPLICIT_CONSTRUCTORS #define _AFX_ALL_WARNINGS #include <afxwin.h> #include <atlbase.h> #include <atlapp.h> extern CAppModule _Module; #include <atlwin.h> ``` 6. 在 ATL 项目中,打开 `main.cpp` 文件并添加以下代码: ```c++ #include "stdafx.h" #include "MyWindow.h" CAppModule _Module; int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { _Module.Init(NULL, hInstance); CMyWindow wnd; wnd.Create(NULL); wnd.ShowWindow(nCmdShow); MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } _Module.Term(); return 0; } ``` 以上是使用 ATL 创建窗口的基本步骤,可以根据实际需求进行修改和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

jyl_sh

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值