COM---编程工作的简化
来源:互联网 发布:诲女知之乎读音 编辑:程序博客网 时间:2024/06/05 07:09
智能指针将引用计数隐藏起来,简化客户端。
CUnkown和CFactory通过提供可服用的IUnkown和ICfactory,简化服务器端,即COM的编写。
简化客户端
智能接口指针
智能指针的好处在于无需记住去调用Release
智能指针是重载了->操作符的类,智能指针接口类包含指向另一个对象的指针。此外,为使客户相信智能指针和指针是相同的,智能指针还需要重载其他操作符,如*、&。
//// Ptr.h// - Smart interface pointer//#include <assert.h>//// IPtr - Smart Interface Pointer// Use: IPtr<IX, &IID_IX> spIX ;// Do not use with IUnknown; IPtr<IUnknown, &IID_IUnknown>// will not compile. Instead, use IUnknownPtr.//template <class T, const IID* piid> class IPtr{public: // Constructors IPtr() { m_pI = NULL ; } IPtr(T* lp) { m_pI = lp ; if ( m_pI != NULL) { m_pI->AddRef() ; } } IPtr(IUnknown* pI) { m_pI = NULL ; if (pI != NULL) { pI->QueryInterface(*piid, (void **)&m_pI) ; } } // Destructor ~IPtr() { Release() ; } // Reset void Release() { if (m_pI != NULL) { T* pOld = m_pI ; m_pI = NULL ; pOld->Release() ; } } // Conversion operator T*() { return m_pI ;} // Pointer operations T& operator*() { assert(m_pI != NULL) ; return *m_pI ;} T** operator&() { assert(m_pI == NULL) ; return &m_pI ;} T* operator->() { assert(m_pI != NULL) ; return m_pI ;} // Assignment from the same interface //调用了AddRef和Release,使得程序无需进行这些调用 //在给m_pI设置了新值值之后释放当前指针,可防止在赋值之前相应的组件 //被从内存中删除 T* operator=(T* pI) { if (m_pI != pI) { IUnknown* pOld = m_pI ; // Save current value. m_pI = pI ; // Assign new value. if (m_pI != NULL) { m_pI->AddRef() ; } if (pOld != NULL) { pOld->Release() ; // Release the old interface. } } return m_pI ; } // 将另外一个不同类型的接口指针赋值给一个智能指针,赋值操作自动调用 // QueryInterface,应对赋值后的智能指针进行检查,以决定是否赋值成功 T* operator=(IUnknown* pI) { IUnknown* pOld = m_pI ; // Save current value. m_pI == NULL ; // Query for correct interface. if (pI != NULL) { HRESULT hr = pI->QueryInterface(*piid, (void**)&m_pI) ; assert(SUCCEEDED(hr) && (m_pI != NULL)) ; } if (pOld != NULL) { pOld->Release() ; // Release old pointer. } return m_pI ; } // Boolean functions BOOL operator!() { return (m_pI == NULL) ? TRUE : FALSE ;} // Requires a compiler that supports BOOL operator BOOL() const { return (m_pI != NULL) ? TRUE : FALSE ; } // GUID const IID& iid() { return *piid ;}private: // Pointer variable T* m_pI ;} ;
调用智能指针
void Think() { trace("Create Component 1.") ; IPtr<IX, &IID_IX> spIX ; HRESULT hr = CoCreateInstance(CLSID_Component1, NULL, CLSCTX_INPROC_SERVER, spIX.iid(), (void**)&spIX) ; if (SUCCEEDED(hr)) { trace("Succeeded creating component.") ; spIX->Fx() ; } }
IPtr专用于IUknown的一个特殊版本—IUnknownPtr
之所以定义IUnknownPtr,是因为IPtr不能调用IUknown来实例化,否则会导致两个具有相同原型的赋值运算符。
IPtr<IUknown &IID_IUKnown> spIUkown; //错误IUknownPtr spIUkown; //正确
IUnknownPtr:
//// IUnknownPtr is a smart interface for IUnknown.//class IUnknownPtr{public: // Constructors IUnknownPtr() { m_pI = NULL ; } IUnknownPtr(IUnknown* lp) { m_pI = lp ; if ( m_pI != NULL) { m_pI->AddRef() ; } } // Destructor ~IUnknownPtr() { Release() ; } // Reset void Release() { if (m_pI) { IUnknown* pOld = m_pI ; m_pI = NULL ; pOld->Release() ; } } // Conversion operator IUnknown*() { return (IUnknown*)m_pI ;} // Pointer operations IUnknown& operator*() { assert(m_pI != NULL) ; return *m_pI ;} IUnknown** operator&() { assert(m_pI == NULL) ; return &m_pI ;} IUnknown* operator->() { assert(m_pI != NULL) ; return m_pI ;} // Assignment IUnknown* operator=(IUnknown* pI) { if (m_pI != pI) { IUnknown* pOld = m_pI ; // Save current value. m_pI = pI ; // Assign new value. if (m_pI != NULL) { m_pI->AddRef() ; } if (pOld != NULL) // Release the old interface. { pOld->Release() ; } } return m_pI ; } // Boolean functions BOOL operator!() { return (m_pI == NULL) ? TRUE : FALSE ;} operator BOOL() const { return (m_pI != NULL) ? TRUE : FALSE ; }private: // Pointer variable IUnknown* m_pI ;} ;
带智能指针的客户端
代码中所有的QueryInterface都被隐藏,提高了程序的可读性
//// Client2.cpp - Client implementation with smart pointers//#include <objbase.h>#include "Iface.h"#include "Util.h" // Traces with labels for our output#include "Ptr.h" // Smart pointer classesstatic inline void trace(const char* msg) { Util::Trace("Client 2", msg, S_OK) ;} static inline void trace(const char* msg, HRESULT hr) { Util::Trace("Client 2", msg, hr) ;}void Think() { trace("Create Component 1.") ; IPtr<IX, &IID_IX> spIX ; HRESULT hr = CoCreateInstance(CLSID_Component1, NULL, CLSCTX_INPROC_SERVER, spIX.iid(), (void**)&spIX) ; if (SUCCEEDED(hr)) { trace("Succeeded creating component.") ; spIX->Fx() ; trace("Get interface IY.") ; IPtr<IY, &IID_IY> spIY ; spIY = spIX ; // Use Assignment. if (spIY) { spIY->Fy() ; trace("Get interface IX from IY.") ; IPtr<IX, &IID_IX> spIX2(spIY) ; // Use Constructor. if (!spIX2) { trace("Could not get interface IX from IY.") ; } else { spIX2->Fx() ; } } trace("Get interface IZ.") ; IPtr<IZ, &IID_IZ> spIZ ; spIZ = spIX ; if (spIZ) { spIZ->Fz() ; trace("Get interface IX from IZ.") ; IPtr<IX, &IID_IX> spIX2(spIZ) ; if (!spIX2) { trace("Could not get interface IX from IZ.") ; } else { spIX2->Fx() ; } } } else { trace("Could not create component.", hr) ; }}int main(){ // Initialize COM Library. CoInitialize(NULL) ; // Exercise the smart pointers. Think() ; // Uninitialize COM Library. CoUninitialize() ; return 0 ;}
注意:
智能指针中的成员都是通过“.”来访问的,而不是“->”
C++包装类
一个包装类是一个或多个COM接口的客户,提供的是对这些接口使用的抽象。无论包装类是否要改变接口的行为,都必须重新实现它所包装的接口中的所有成员。
使用包装类的原因:
1、如果希望封装某个接口或某个接口对象,智能指针便不适合了,此时,可使用C++包装类。
2、可以将几个单独的接口组合成一个逻辑单位。
简化服务器端
未知接口基类
定义一个实现IUnkown的基类CUnkown,对于从CUnkown继承的类,将不再考虑其AddRef或Release的实现问题,并且QueryInterface的实现也将得以简化。
基类CUnkown将实现一个非代理IUnknown接口,而宏DECLARE_IUNKNOWN将实现一个代理IUnkown接口。
CUnkown实现非代理接口,代理接口由组件实现。
CUnkown实现INondelegatingUnkown接口的方法同被聚合组件实现此接口的方法相同。
CUnkown之所以要实现非代理接口,原因:
1、支持聚合
2、如果实现了代理接口,继承它的组件重新改写了IUnkown接口的纯虚函数,CUnkown相当于无效了。
#ifndef __CUnknown_h__#define __CUnknown_h__#include <objbase.h>/////////////////////////////////////////////////////////////// Nondelegating IUnknown interface// - Nondelegating version of IUnknown//interface INondelegatingUnknown{ virtual HRESULT __stdcall NondelegatingQueryInterface(const IID& iid, void** ppv) = 0 ; virtual ULONG __stdcall NondelegatingAddRef() = 0 ; virtual ULONG __stdcall NondelegatingRelease() = 0 ;} ;/////////////////////////////////////////////////////////////// Declaration of CUnknown // - Base class for implementing IUnknown//class CUnknown : public INondelegatingUnknown{public: // Nondelegating IUnknown implementation virtual HRESULT __stdcall NondelegatingQueryInterface(const IID&, void**) ; virtual ULONG __stdcall NondelegatingAddRef() ; virtual ULONG __stdcall NondelegatingRelease() ; // Constructor //接收一个指向外部IUnkown接口的指针作为参数,并保存起来 //供GetOuterUnkown函数使用 CUnknown(IUnknown* pUnknownOuter) ; // Destructor virtual ~CUnknown() ; //Init和FinalRelease //都是虚函数,帮助派生类对内部组件进行管理 // Initialization (especially for aggregates) //帮助派生类创建被包容或聚合的组件 virtual HRESULT Init() { return S_OK ;} // Notification to derived classes that we are releasing //给被删除组件提供一个释放其所保持的指向内部组件接口指针的机会 //将增大引用计数,以免相应组件被释放被包容组件上的接口时 //引起Release的递归调用 virtual void FinalRelease() ; // Count of currently active components static long ActiveComponents() { return s_cActiveComponents ;} // Helper function // 是为了简化派生类中对NondelegatingQueryInterface的实现 HRESULT FinishQI(IUnknown* pI, void** ppv) ;protected: // Support for delegation IUnknown* GetOuterUnknown() const { return m_pUnknownOuter ;}private: // Reference count for this object long m_cRef ; // Pointer to (external) outer IUnknown IUnknown* m_pUnknownOuter ; // Count of all active instances static long s_cActiveComponents ; } ;/////////////////////////////////////////////////////////////// 代理 IUnknown// - Delegates to the nondelegating IUnknown, or to the// outer IUnknown if the component is aggregated.//避免每次实现一个组件都去实现代理IUnkown接口//#define DECLARE_IUNKNOWN \ virtual HRESULT __stdcall \ QueryInterface(const IID& iid, void** ppv) \ { \ return GetOuterUnknown()->QueryInterface(iid,ppv) ; \ } ; \ virtual ULONG __stdcall AddRef() \ { \ return GetOuterUnknown()->AddRef() ; \ } ; \ virtual ULONG __stdcall Release() \ { \ return GetOuterUnknown()->Release() ; \ } ;///////////////////////////////////////////////////////////#endif
/////////////////////////////////////////////////////////////// CUnknown.cpp // - Implementation of IUnknown base class//#include "CUnknown.h"#include "Util.h"static inline void trace(char* msg) {Util::Trace("CUnknown", msg, S_OK) ;} static inline void trace(char* msg, HRESULT hr) {Util::Trace("CUnknown", msg, hr) ;}/////////////////////////////////////////////////////////////// Count of active objects// - Use to determine if we can unload the DLL.//long CUnknown::s_cActiveComponents = 0 ;/////////////////////////////////////////////////////////////// Constructor//CUnknown::CUnknown(IUnknown* pUnknownOuter): m_cRef(1){ // Set m_pUnknownOuter pointer. if (pUnknownOuter == NULL) { trace("Not aggregating; delegate to nondelegating IUnknown.") ; m_pUnknownOuter = reinterpret_cast<IUnknown*> (static_cast<INondelegatingUnknown*> (this)) ; // notice cast } else { trace("Aggregating; delegate to outer IUnknown.") ; m_pUnknownOuter = pUnknownOuter ; } // Increment count of active components. ::InterlockedIncrement(&s_cActiveComponents) ;}//// Destructor//CUnknown::~CUnknown(){ InterlockedDecrement(&s_cActiveComponents) ;}//// FinalRelease - called by Release before it deletes the component//void CUnknown::FinalRelease(){ trace("Increment reference count for final release.") ; m_cRef = 1 ;}//// Nondelegating IUnknown// - Override to handle custom interfaces.//HRESULT __stdcall CUnknown::NondelegatingQueryInterface(const IID& iid, void** ppv){ // CUnknown supports only IUnknown. if (iid == IID_IUnknown) { return FinishQI(reinterpret_cast<IUnknown*> (static_cast<INondelegatingUnknown*>(this)), ppv) ; } else { *ppv = NULL ; return E_NOINTERFACE ; }}//// AddRef//ULONG __stdcall CUnknown::NondelegatingAddRef(){ return ::InterlockedIncrement(&m_cRef) ;}//// Release//ULONG __stdcall CUnknown::NondelegatingRelease(){ ::InterlockedDecrement(&m_cRef) ; if (m_cRef == 0) { FinalRelease() ; delete this ; return 0 ; } return m_cRef ;}//// FinishQI// - 是为了简化派生类中对NondelegatingQueryInterface的实现// //HRESULT CUnknown::FinishQI(IUnknown* pI, void** ppv) { *ppv = pI ; pI->AddRef() ; return S_OK ;}
类厂
在实现了一个组件后,需要为该组件实现一个类厂。
CFactory实现IClassFactory,可以同任何从CUnknown派生的COM组件一起工作。借助于CLSID和其他信息,CFactory将能够创建相应的组件。
CFactory还提供了可用于实现DLL入口点如DllGetObject的代码。
———–组件数据———–
typedef HRESULT (*FPCREATEINSTANCE)(IUnknown*, CUnknown**) ;/////////////////////////////////////////////////////////////// CFactoryData// - Information CFactory needs to create a component// supported by the DLL//class CFactoryData{public: // 组件类标识符 const CLSID* m_pCLSID ; // 指向组件创建函数的指针 FPCREATEINSTANCE CreateInstance ; // 保存在Windows注册表中的一个易记名称 const char* m_RegistryName ; // ProgID const char* m_szProgID ; // 与版本无关的 ProgID const char* m_szVerIndProgID ; // 查找CLSID的帮助函数 BOOL IsClassID(const CLSID& clsid) const { return (*m_pCLSID== clsid) ;}} ;
———–每个组件的CFactoryData结构填充———
#include "CFactory.h"#include "Iface.h"#include "Cmpnt1.h"#include "Cmpnt2.h"#include "Cmpnt3.h"/////////////////////////////////////////////////////////////// Server.cpp//// This file contains the component server code.// The FactoryDataArray contains the components that // can be served.//// Each component derived from CUnknown defines a static function// for creating the component with the following prototype. // HRESULT CreateInstance(IUnknown* pUnknownOuter, // CUnknown** ppNewComponent) ;// This function is used to create the component.////// The following array contains the data used by CFactory// to create components. Each element in the array contains// the CLSID, the pointer to the creation function, and the name// of the component to place in the Registry.//CFactoryData g_FactoryDataArray[] ={ {&CLSID_Component1, CA::CreateInstance, "Inside COM, Chapter 9 Example, Component 1", //易记名称 "InsideCOM.Chap09.Cmpnt1.1", // ProgID "InsideCOM.Chap09.Cmpnt1"}, // 版本无关ProgID {&CLSID_Component2, CB::CreateInstance, "Inside COM, Chapter 9 Example, Component 2", "InsideCOM.Chap09.Cmpnt2.1", "InsideCOM.Chap09.Cmpnt2"}, {&CLSID_Component3, CC::CreateInstance, "Inside COM, Chapter 9 Example, Component 3", "InsideCOM.Chap09.Cmpnt3.1", "InsideCOM.Chap09.Cmpnt3"}} ;int g_cFactoryDataEntries = sizeof(g_FactoryDataArray) / sizeof(CFactoryData) ;
————进程中服务器———–
//// GetClassObject// - 基于 CLSID 创建类厂//HRESULT CFactory::GetClassObject(const CLSID& clsid, const IID& iid, void** ppv){ if ((iid != IID_IUnknown) && (iid != IID_IClassFactory)) { return E_NOINTERFACE ; } // 遍历数组,查找与客户待创建的组件的相应的CFactoryData结构 // g_FactoryDataArray在server.cpp中实现 for (int i = 0; i < g_cFactoryDataEntries; i++) { const CFactoryData* pData = &g_FactoryDataArray[i] ; if (pData->IsClassID(clsid)) { // 将CFactoryData结构传给类厂 // 这样就能知道需要创建哪种组件 *ppv = (IUnknown*) new CFactory(pData) ; if (*ppv == NULL) { return E_OUTOFMEMORY ; } return NOERROR ; } } return CLASS_E_CLASSNOTAVAILABLE ;}//// IClassFactory implementation// 创建组件//HRESULT __stdcall CFactory::CreateInstance(IUnknown* pUnknownOuter, const IID& iid, void** ppv){ // 只有当所请求的IID是IID_IUnknown,才进行聚合 if ((pUnknownOuter != NULL) && (iid != IID_IUnknown)) { return CLASS_E_NOAGGREGATION ; } // 创建组件,使用保存在CFactoryData数组创建函数实例指针 // 调用被创建组件的构造函数 // 将相应的指针通过pNewComponent返回给调用者 // 若第一个参数非空,则组件将是被聚合的 CUnknown* pNewComponent ; HRESULT hr = m_pFactoryData->CreateInstance(pUnknownOuter, &pNewComponent) ; if (FAILED(hr)) { return hr ; } // 初始化组件 hr = pNewComponent->Init() ; if (FAILED(hr)) { // 初始化失败,释放组件 pNewComponent->NondelegatingRelease() ; return hr ; } // 得到要求的接口 hr = pNewComponent->NondelegatingQueryInterface(iid, ppv) ; // 释放类厂所持有的引用 pNewComponent->NondelegatingRelease() ; return hr ;}
未知接口基类和类厂的使用
组件将不再重复实现AddRef和Release了,只需将新接口加入到QueryInterface实现中。并且只需要实现一个创建函数,无需实现一个完整的类厂。
———使用CUnkown实现IUnkown接口的组件———
组件2实现了IY,并聚合了组件3,而后者实现了接口IZ。
组件2被组件1聚合。
所以,组件2既是聚合组件,也是被聚合组件。
//// Cmpnt2.h - Component 2//#include "Iface.h"#include "CUnknown.h" // IUnknown基类/////////////////////////////////////////////////////////////// Component B// 继承了CUnkown,CUnkown提供了非代理IUnkown的实现//class CB : public CUnknown, public IY{public: // 创建 static HRESULT CreateInstance(IUnknown* pUnknownOuter, CUnknown** ppNewComponent) ;private: // 声明代理IUnknown. DECLARE_IUNKNOWN // 非代理 IUnknown // CUnkown不能完全实现QueryInterface,因为不知道组件所支持的接口 // 因此需要NondelegatingQueryInterface处理所支持的接口 virtual HRESULT __stdcall NondelegatingQueryInterface(const IID& iid, void** ppv) ; // 接口 IY virtual void __stdcall Fy() ; // 初始化 // 重载Init,以建立其他用于包容或聚合的组件 virtual HRESULT Init() ; // 清除 // 在CFactory::NondelegatingRelease删除对象之前,将调用此函数 // 对于需要释放指向内部组件指针的组件可以重载此函数 // CUnkown::FinalRelease将增大引用计数,防止对组件的析构递归 virtual void FinalRelease() ; // 构造函数 CB(IUnknown* pUnknownOuter) ; // 析构 ~CB() ; // 被聚合组件 IUnknown* m_pUnknownInner ; // 内部组件所支持的接口 IZ* m_pIZ ;} ;
//// Cmpnt2.cpp - Component 2//#include <objbase.h>#include "Iface.h"#include "Util.h"#include "CUnknown.h" // IUnknown基类#include "Cmpnt2.h"static inline void trace(char* msg) {Util::Trace("Component 2", msg, S_OK) ;} static inline void trace(char* msg, HRESULT hr) {Util::Trace("Component 2", msg, hr) ;}/////////////////////////////////////////////////////////////// Interface IY implementation//void __stdcall CB::Fy(){ trace("Fy") ;}//// Constructor//CB::CB(IUnknown* pUnknownOuter): CUnknown(pUnknownOuter), m_pUnknownInner(NULL), m_pIZ(NULL){ // Empty}//// Destructor//CB::~CB(){ trace("Destroy self.") ;}//// NondelegatingQueryInterface implementation//HRESULT __stdcall CB::NondelegatingQueryInterface(const IID& iid, void** ppv){ if (iid == IID_IY) { //FinishQI是使在派生类中NondelegatingQueryInterface容易一些 return FinishQI(static_cast<IY*>(this), ppv) ; } else if (iid == IID_IZ) { return m_pUnknownInner->QueryInterface(iid, ppv) ; } else { //IUnkown和其它不知道的接口交由基类处理 return CUnknown::NondelegatingQueryInterface(iid, ppv) ; }}//// Initialize the component and create the contained component.//HRESULT CB::Init(){ trace("Create Component 3, which is aggregated.") ; HRESULT hr = CoCreateInstance(CLSID_Component3, GetOuterUnknown(), CLSCTX_INPROC_SERVER, IID_IUnknown, (void**)&m_pUnknownInner) ; if (FAILED(hr)) { trace("Could not create inner component.", hr) ; return E_FAIL ; } trace("Get pointer to interface IZ to cache.") ; hr = m_pUnknownInner->QueryInterface(IID_IZ, (void**)&m_pIZ) ; if (FAILED(hr)) { trace("Inner component does not support IZ.", hr) ; m_pUnknownInner->Release() ; m_pUnknownInner = NULL ; return E_FAIL ; } // Decrement the reference count caused by the QI call. trace("Got IZ interface pointer. Release reference.") ; GetOuterUnknown()->Release() ; return S_OK ;}//// FinalRelease - Called by Release before it deletes the component//void CB::FinalRelease(){ // 使用基类的函数,增加引用计数m_cRef,防止递归析构 CUnknown::FinalRelease() ; // Counter the GetOuterUnknown()->Release in the Init method. GetOuterUnknown()->AddRef() ; // Properly release the pointer, as there might be // per-interface reference counts. m_pIZ->Release(); // Release the contained component. // (We can do this now since we've released the interfaces.) if (m_pUnknownInner != NULL) { m_pUnknownInner->Release() ; }}/////////////////////////////////////////////////////////////// Creation function used by CFactory//HRESULT CB::CreateInstance(IUnknown* pUnknownOuter, CUnknown** ppNewComponent) { *ppNewComponent = new CB(pUnknownOuter) ; return S_OK ;}
———-集成步骤————
使用CUnkown和CFactory编写组件是比较简单的。
1、编写实现组件的类
1> 可以从CUnkown或其他CUnkown派生的类派生出待实现的组件
2> 使用DELCARE_IUNKOWN宏实现代理IUnkown接口
3> 在组件的构造函数中初始化CUnkown
4> 实现NondelegatingQueryInterface,在其中加入此组件支持而基类不支持的接口。对那些组件不支持的接口,可调用相应的基类。
5> 若需要在构建了组件之后进行其他一些初始化处理,可重载Init函数,以建立被包容或被聚合的组件。
6> 若需要在组件被删除之前进行一些其他清理工作,可重载FinalRelease,以释放被包容或被聚合的组件。
7> 给组件实现一个静态的CreateInstance
8> 实现组件支持的那些借口。
2、对将要放到同一DLL中的组件重复上述步骤
3、编写类厂
1> 建立一个文件,以包含全局CFactoryData数组g_FactoryDataArray
2> 定义g_FactoryDataArray数组并用DLL中所提供的信息填充此组件。
3> 定义g_FactoryDataEntires,其中包含g_FactoryDataArray中组件的个数。
4、编写一个定义DLL入口点的DEF文件。
5、将上述所编写的代码同CUnkown.cpp和CFactory.cpp一块编译。
- COM---编程工作的简化
- COM读书笔记---- 编程工作的简化
- COM技术内幕--编程工作的简化
- COM学习笔记(十二):编程工作的简化
- 第9章 编程工作的简化
- com学习笔记(8)编成工作的简化
- 简化工作的利器
- 使用批处理简化自己的工作
- 代码生成器,极大简化你的工作
- 为简化使用Excel COM写的类
- 为简化使用Excel COM写的类[ZT]
- 为简化使用Excel COM写的类
- C#4.0 新特性 dynamic 简化Com调用的复杂度
- 可爱的 Python: Decorator 简化元编程
- 可爱的 Python:Decorator 简化元编程
- 可爱的 Python: Decorator 简化元编程
- 可爱的 Python: Decorator 简化元编程
- 年过半百后,如何才能继续做喜爱的编程工作?http://blog.jobbole.com/26933/
- 大数据时代的技术hive:hive介绍
- PHP警告Cannot use a scalar value as an array
- 教大家如何在windows下配置mysql免安装版
- Hive安装及使用攻略
- java创建Excel并下载至本地指定位置
- COM---编程工作的简化
- 使用Java原生进行压缩和解压缩
- Android加载动画__仿贪吃蛇
- ViewPager实现APP的引导页面(小圆点联动)
- opengl es 基本使用
- Uncaught TypeError: Cannot read property 'attr' of undefined
- Linux(CentOS)下,下载安装Nginx并配置
- 数独
- oracle中over()分析函数的用法