第七章 类的定制

        这个话题比较大了,所以我们往简单点说,那就是我们想要控制一个类的行为,嗯,这里说的不是一个类对象的行为,好吧,如果接触C++不是太深的话这可能不太好理解,因为似乎我们都不怎么关心这个问题,我们往往关心的是一个对象的行为,但是不表示我们就不应该关心类的行为。
 

那么类有哪些行为是我们可以或者说应该关心的?
 

首先我们来说类的构建方式,类的构建方式不都很简单吗?直接声明定义或者new出来即可,那么问题来了,直接声明定义出来的对象都是栈对象,但是计算机栈空间远不及堆空间大,所以可能我们会要求使用该类的人必须将对象分配在堆上面,那么问题来了,你不知道谁在使用你所提供的类,所以你不可能对个使用你class的人都耳提面命的告诉他们你必须要new出来,否则可能会导致某种不可预期的问题,所以为了让使用该类的人必须在堆上构建,那么就要从根源上来解决该问题,让他必须new出来,而打算在栈上构建该对象则无法通过编译,要定制这样的话很简单,我们只需要把析构函数设定为protected或者private即可。
 

//+—————————
 

class Test{
public:
Test(){ }

void Destroy(){

delete this;



}

protected:
~Test(){ }
};


 

int main(){
//
// 正确的使用方式
//
Test* obj = new Test();


obj->Destroy();


 

//
// 下面的方式无法通过编译
//
Test obj;


 

system(“pause”)
 

return 0;


}
 

 

//+—————————–
 

 

上面是让类只能在堆上构建的技巧,那么如果让类只能在栈上构建呢?也就是我们不可以使用new来完成该行为,比如Direct3D里面的XNA上的XMMATRIX就只能在栈上才能保证16字节对齐(好吧,几字节对齐记得不太清,总之如果选择堆内存会出现频繁崩溃),那么如何实现这样的类呢?实现方法也是相当的简单,我们只需要将new操作符都实现为私有的即可,既然是私有的那么就可以不需要实现,而只需要声明即可:
 

 

//+—————————–
 

class Test{
public:
Test(){ }
~Test(){ }
protected:
static void* operator new(size_t size);


static
void operator delete(void* ptr, std::size_t size);


};


 

int main(){
//
// 错误的使用方式,无法通过编译
//
Test* obj = new Test();


 

 

//
// 正确的使用方式
//
Test obj;


 

system(“pause”)
 

return 0;


}
 

 

//+———————————
 

 

这是将类设计为只能在栈上分配的技巧,当然你可以这么认为:使用malloc获取内存,然后转换为Test*:
 

 

//+————————————-
 

Test* obj = (Test*)malloc(sizeof(Test));


 

 

//+————————————-
 

但只是一块raw内存而已,并没有实质上的意义,虽然对于上面我们这个空类来说是可行的,但是当我们在构造函数中有初始化行为时这就不可行,比如:
 

 

//+————————————–
 

class Test{
public:
Test(){
m_value = 10;



}

~Test(){ }
 


int value() const{

return m_value;



}

protected:
static void* operator new(size_t size);


static
void operator delete(void* ptr, std::size_t size);


 

private:
int   m_value;


};


 

int main(){
Test* obj = (Test*)malloc(sizeof(Test));


std::cout <<obj->value() <<std::endl;


free(obj);


system(“pause”);


return 0;


}
 

 

//+—————————————–
 

我们会看到打印的结果是一个随机数,所以这不是正确的行为。
 

接下来我们设计整个进程中只存在一个类对象,其实这属于一种设计模式——单例模式,单例简单点说就是一个经过改进后的全局对象,然而我们不能简单的以为是静态数据 + 静态函数  = 单例。
 

 

//+————————————
 

class Test{…… };


 

class MyOneTest{
public:
static
void doSomething(Test&

test);


private:
static std::queue<Test>testQueue;


};


 

 

Test test;


 

 

MyOneTest::doSomething(test);


 

//+———————————–
 

看上去没啥问题,运用起来也没啥问题,但在某些使用情况下有很多问题,比如我的行为要求有所改变,doSomething不再适合(当然如果这不是个静态函数而是个虚函数那么这一切都不是问题),这一切的修改需要建立在拥有源码的基础上才行,同时静态数据加静态函数的做法对数据的初始化和清理同样带来难题。
 

关于单例的实现方式有多种,这里我就主要说说我使用的方案:
 

//+————————————
 

class Singleton{
public:
static Singleton* Instance(){
if(__sPtr){

return __sPtr;



}

else{
__sPtr = new Singleton;


return __sPtr;



}


}

 

static
void Destroy(){

if(__sPtr){
delete __sPtr;


__sPtr = nullptr;



}


}

 

protected:
 

Singleton(){ };


~Singleton(){ }
Singleton(const Singleton&

other) = delete;


Singleton&

operator(const Singleton&

other) = delete;


 

private:
static Singleton*  __sPtr;


};


 

Singleton*  Singleton::__sPtr = nullptr;


 

 

//+———————————
 

这种设计的好处是我不需要的时候就不会有Singleton对象的创建,这对于开销很大的对象来说很重要,同时还有多态的性质,因为我们是动态创建,考虑一下我们为什么不用下面的方式:
 

 

//+———————————-
 

class Singleton{
public:
static Singleton* Instance(){

return &

__sObj;



}

 

 

protected:
 

Singleton(){ };


~Singleton(){ }
Singleton(const Singleton&

other) = delete;


Singleton&

operator(const Singleton&

other) = delete;


 

private:
static Singleton  __sObj;


};


 

Singleton*  Singleton::__sObj;


 

 

//+————————————-
 

当然还有为什么Instance返回的是指针而不是引用?
 

关于资源的管理似乎有必要争论一下,如果我们采用上面的实现方式那么我们可以不用关心内存的释放,但是带来的问题是该单列不具有多态性,而且无论我们用与不用都得构造该对象出来,当然我们可以使用另一种方式:
 

 

//+————————————
 

static Singleton* Instance(){
static Singleton Obj;


return &

Obj;


}
 

//+————————————-
 

该方式解决了内存的管理同时解决了用时才进行实例化的问题,但是引入了一个新的问题,那就是你不知道编译器在实例化该对象的时候到底都做了些什么,经过别人验证该模型在某些情况下他确实会失效,所以我一直使用上面指针的模型,那么到目前为此,我们的单例在单线程模型下工作得很好,但是一旦进入多线程就可以出现多个不同的对象出来。
 

 

//+————————————–
 

static Singleton* Instance(){
if(__sPtr){ // 如果多条线程同时执行到该位置,那么都会检查到__sPtr为nullptr,自然都走到else里面

return __sPtr;



}

else{
__sPtr = new Singleton;


return __sPtr;



}

}
 

//+—————————————
 

 

具体问题如上,我们可以这样来解决该问题:
 

 

//+——————————————
 

static Singleton* Instance(){
std::mutex mtx;


std::unique_lock<std::mutex>lock(mtx);


if(__sPtr){ 

return __sPtr;



}

else{
__sPtr = new Singleton;


return __sPtr;



}

}
 

//+——————————————-
 

现在可以正常工作但是又引入了另一个问题,这是一个关于效率的问题,因为每一次进入Instance都会引起中断,这不是一个好习惯,因为后面的都是不必要的,所以我们可以重新优化一下:
 

 

//+——————————————–
 

static Singleton* Instance(){
 

if(__sPtr){ 

return __sPtr;



}

else{
std::mutex mtx;


std::unique_lock<std::mutex>lock(mtx);


if(__sPtr){

return __sPtr;



}

else{
__sPtr = new Singleton;


return __sPtr;



}


}

}
 

//+——————————————-
 

现在效率有了,安全性也有了,我们可以重新装订一下,让他支持任意类实现单例模型:
 

 

//+——————————————-
 

 

//
// 无论通过那个函数获取的实例都是同一个实例对象
// 当使用完成之后需要使用 Destroy 来销毁对象
// 可以直接使用MSingleton创建对象单例,当还可以可以使用他作为基类来让子类具有创建单例的能力
// eg:
// 1:class A;


// A* ptr = MSingleton<A>::Instance();


// 2 : class A : public MSingleton<A>{ };


// A* ptr = A::Instance();


//
template<class T>
class MSingleton
{
public:
    MSingleton(){ }
    ~MSingleton(){ }
 

    MSingleton(const MSingleton&

)=delete;


    MSingleton&

operator=(const MSingleton&

)=delete;


 

    //
    // 单例普通指针类型
    //
    static T* Instance(){
        try{
            if(__sPtr == nullptr){
std::mutex mtx;


std::unique_lock<std::mutex>lock(mtx);


if(__sPtr == nullptr){
    __sPtr = new T;



}

           
}

           

return __sPtr;


       
}

        catch(…){
           

return nullptr;


       
}

   
}

 

//
// 带参数创建
//
template<class…Args>
static T* InstanceWithArgs(Args…args){
try{
if (__sPtr == nullptr){
std::mutex mtx;


std::unique_lock<std::mutex>lock(mtx);


if (__sPtr == nullptr){
__sPtr = new T(args…);



}


}

return __sPtr;



}

catch (…){

return nullptr;



}


}

 

    //
    // 使用工厂函数进行创建对象
    //
    template<class F>
    static T* Instance(F fun){
        try{
            if(__sPtr == nullptr){
std::mutex mtx;


std::unique_lock<std::mutex>lock(mtx);


if(__sPtr == nullptr){
                __sPtr = fun();


                if(__sPtr){
                    __sIsFactory = true;


               
}


}

           
}

           

return __sPtr;


       
}

        catch(…){
           

return nullptr;


       
}

   
}

 

    static
void Destroy(){

        if(__sPtr &

&

!__sIsFactory){
            delete __sPtr;


            __sPtr = nullptr;


       
}

   
}

 

    //
    // 使用辅助函数进行释放对象
    //
    template<class F>
    static
void Destroy(F fun){

        if(__sPtr &

&

__sIsFactory){
            fun(__sPtr);


            __sPtr = nullptr;


       
}

   
}

 

protected:
    static T* __sPtr;


    static bool __sIsFactory;


};


 

template<class T>
T* MSingleton<T>::__sPtr = nullptr;


 

template<class T>
bool MSingleton<T>::__sIsFactory = false;


 

 

//
// 测试代码
//
class Test{
public:
Test(){
m_value = 10;



}

~Test(){ }
 


int value() const{

return m_value;



}

 

 

private:
int   m_value;


};


 

 

typedef MSingleton<Test>SingleTest;


 


void Destroy(){

SingleTest::Destroy();


}
 

int main(){
std::atexit(Destroy);


Test* obj = SingleTest::Instance();


std::cout <<obj->value() <<std::endl;


return 0;


}
 

 

//+—————————–
 

到此,我们实现了一个简单的单例模型,这个单例的使用方式也相当的简单,一如源码里面的说明,接下来我们再来说一个另外一种类的定制——控制类的内存分配行为,这句话不太好理解,当然是我说得不够专业的原因,我们用代码是说明会更好一些:
 

 

//+—————————–
 

int main(){

int
num = 1e8;


for(int i=0;

i<num;

++i){
int* ptr =  new int;


delete ptr;



}

return 0;


}
 

//+——————————-
 

这个例子没有任何实际的意义,但是刚好能够说明我们的问题,就是说在我们需要创建大量对象的时候我们想要对内存进行一些优化,众所周知,上面的代码执行效率相当的低,所以我们这里说的定制就是要优化这个效率,要进行该项定制我们需要实现new操作符的重写:
 

 

//+——————————–
 

class MAllocator{
public:
static void* operator new(std::size_t size);


static
void operator delete(void* p, std::size_t size);


};


 

//+———————————
 

假如我们这个类是我们的基类,而且已经重新好了新的new操作符,那么我们只需要继承至该类就可以使用高效率的new操作符了:
 

 

//+———————————-
 

class Test : public MAllocator{
public:
Test(){
m_value = 10;



}

~Test(){ }
 


int value() const{

return m_value;



}

 

 

private:
int   m_value;


};


 

//+————————————–
 

该技术的难点在于分配器 MAllocator 的实现上面,当然该分配器有各种实现方案,我这里简单的说一种:
 

 

//+————————————–
 

struct MEmptyType{ };


 

 

template<class Allocator = MEmptyType>
class MAllocator{
public:
static void* operator new(std::size_t size);


static
void operator delete(void* p, std::size_t size);


};


 

/
// 分配器转发
//
template<class Allocator>
struct __Allocator_Dispatch__{
static void* malloc(std::size_t size){

return Allocator::Instance()->Allocator(size);



}

 

static void  free(void* p, std::size_t size){
Allocator::Instance()->Deallocator(p, size);



}

};


 

template<>
struct __Allocator_Dispatch__<MEmptyType>{
static void* malloc(std::size_t size){

return ::operator new(size);



}

 

static void  free(void* p, std::size_t size){
::operator delete(p);



}

};


 

//
// 分配器实现
//
template<class Allocator>
void* MAllocator<Allocator>::operator new(std::size_t size){

return __Allocator_Dispatch__<Allocator>::malloc(size);


}
 

template<class Allocator>

void MAllocator<Allocator>::operator delete(void* p, std::size_t size){

__Allocator_Dispatch__<Allocator>::free(p, size);


}
 

 

//+——————————-
 

现在我们将问题抛给了模板参数,默认情况下我们使用常规的new操作符,效率自然不会得到提高,所以我们想要提高new的效率就需要提供一个真正的分配器作为模板参数传递进去:
 

 

//+——————————–
 

//
// 普通内存分配器
//
template<class Type,
int N = 1024, class ThreadMode = MSingleThreadMode>

class MPoolAllocator : public ThreadMode, public MSingleton<MPoolAllocator<Type, N,ThreadMode>>
{
public:
enum{ value = sizeof(Type)
};


void* Allocator(std::size_t size);



void Deallocator(void* p, std::size_t size);


 

template<class OtherType>
struct rebind
{
typedef MPoolAllocator<OtherType, N, ThreadMode>AllocatorType;



};


private:
std::vector<MFixMemroy<N>*>mPools;


MFixMemroy<N>*   pAllocator{ nullptr
};


std::deque<void*>mFastCache;


 

template<class CableLock>
class UniquiLock{
public:
UniquiLock(CableLock* obj) :pObj(obj){
pObj->Lock();



}

~UniquiLock(){
pObj->UnLock();



}

private:
CableLock*  pObj{ nullptr
};



};


};


 

 

//+———————————
 

 

到这里我们可以将分配器,中间层组装起来作为一个包装器:
 

 

//+———————————
template<class Type>
class MFasWrap : public MAllocator<MPoolAllocator<Type>>
{
public:
typedef typename MConstParamWrape<Type>::type const_reference_type;

// 也许是引用,也许不是,比如标量类型
typedef typename MParamWrape<Type>::type reference_type;

     // 也许是引用,也许不是,比如标量类型
public:
MFasWrap(const_reference_type val = Type()) :mObj(val){ }
 

MFasWrap(const MFasWrap&

other) :mObj(other.mObj){ }
 

MFasWrap(MFasWrap&

&

other){
mObj = other.mObj;


other.mObj.~Type();



}

 

MFasWrap&

operator =(const_reference_type val){
mObj = val;


return *this;



}

 

MFasWrap&

operator =(Type&

&

val){
mObj = val;


val.~Type();


return *this;



}

 

MFasWrap&

operator =(const MFasWrap&

other){
mObj = other.mObj;


return *this;



}

 

MFasWrap&

operator =(MFasWrap&

&

other){
mObj = other.mObj;


other.mObj.~Type();


return *this;



}

 

operator Type(){

return mObj;



}

 

reference_type toType(){

return mObj;



}

 

const_reference_type toType() const{

return mObj;



}

 

 

friend std::ostream&

operator<<(std::ostream&

os, const MFasWrap&

other){
os <<other.mObj;


return os;



}

 

friend std::wostream&

operator<<(std::wostream&

os, const MFasWrap&

other){
os <<other.mObj;


return os;



}

 

friend std::istream&

operator>>(std::istream&

is, MFasWrap&

other){
is >>other.mObj;


return is;



}

 

friend std::wistream&

operator>>(std::wistream&

is, MFasWrap&

other){
is >>other.mObj;


return is;



}

private:
Type mObj;


};


 

//+—————————-
 

于是我们就可以像下面进行使用啦:
 

//+—————————–
 

int main(){
 

typedef MFasWrap<int>FInt;


 


int
num = 1e7;


boost::timer t;


t.restart();



for (int i = 0;

i <num;

++i){
FInt* ptr = new FInt;


delete ptr;



}

std::cout <<“time1 = ” <<t.elapsed() * 1000 <<std::endl;


t.restart();


 


for (int i = 0;

i <num;

++i){
int* ptr = new int;


delete ptr;



}

std::cout <<“time2 = ” <<t.elapsed() * 1000 <<std::endl;


system(“pause”);


 

return 0;


}
 

 

//+—————————–
 

我们可以对比一下效率,可以看出差距是相当大的:
第七章 类的定制
 

第七章 类的定制
关于类的定制就说到这里,大家有兴趣挖掘的可以继续深入研究。

发表评论