一篇文章帶你瞭解C++特殊類的設計

設計一個類,隻能在堆上創建對象

想要的效果實際是沒法直接在棧上創建對象。

首先cpp隻要創建對象就要調用構造函數,因此先要把構造函數ban掉,把構造函數設計成private。但是單這樣自己也創建不瞭瞭。

因此提供一個創建的接口,隻能調用該接口,該接口內部寫new。而且要調用該接口需要先有對象指針調用,而要有對象先得調用構造函數實例化,因此必須設計成靜態函數

但是註意這樣還有拷貝函數可以調用HeapOnly copy(*p)。此時生成的也是棧上的對象。因此要拷貝構造私有,並且隻聲明不實現(實現也是可以的,但是沒人用)。這種方式在c++98中叫防拷貝,比如互斥鎖。

#include<iostream>
using namespace std;
class HeapOnly
{
private:
	HeapOnly()
	{ }
    //C++98——防拷貝
    HeapOnly(const HeapOnly&);
public:
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}
};
int main()
{
	HeapOnly* p = HeapOnly::CreateObj();
	return 0;
}

對於防拷貝,C++11中有新的方式。函數=delete

#include<iostream>
using namespace std;
class HeapOnly
{
private:
	HeapOnly()
	{ }
public:
	static HeapOnly* CreateObj()
	{
		return new HeapOnly;
	}
    //C++11——防拷貝
    HeapOnly(const HeapOnly&) =delete;
};
int main()
{
	HeapOnly* p = HeapOnly::CreateObj();
	return 0;
}

總結:

1.將類的構造函數私有,拷貝構造聲明成私有。防止別人調用拷貝在棧上生成對象。

2.提供一個靜態的成員函數,在該靜態成員函數中完成堆對象的創建

設計一個類,隻能在棧上創建對象

  • 方法一:同上將構造函數私有化,然後設計靜態方法創建對象返回即可。

由於返回臨時對象,因此不能禁掉拷貝構造。

class StackOnly 
{ 
    public: 
    static StackOnly CreateObject() 
    { 
        return StackOnly(); 
    }
    private:
    StackOnly() {}
};
  • 方法二:調用類自己的專屬的operator new和operator delete,設置為私有。

因為new在底層調用void* operator new(size_t size)函數,隻需將該函數屏蔽掉即可。註意:也要防止定位new。new先調用operator new申請空間,然後調用構造函數。delete先調用析構函數釋放對象所申請的空間,再調用operator delete釋放申請的對象空間。

class StackOnly 
{ 
    public: 
    StackOnly() {}
    private: //C++98
    void* operator new(size_t size);
    void operator delete(void* p);
};
int main()
{
  	static StackOnly st;//缺陷,沒有禁掉靜態區的。  
}
class StackOnly 
{ 
    public: 
    StackOnly() {}
    //C++11
    void* operator new(size_t size) = delete;
    void operator delete(void* p) = delete;
};
int main()
{
  	static StackOnly st;//缺陷,沒有禁掉靜態區的。  
}

設計一個類,不能被拷貝

拷貝隻會放生在兩個場景中:拷貝構造函數以及賦值運算符重載,因此想要讓一個類禁止拷貝,隻需讓該類不能調用拷貝構造函數以及賦值運算符重載即可。

  • C++98將拷貝構造函數與賦值運算符重載隻聲明不定義,並且將其訪問權限設置為私有即可。
class CopyBan
{
    // ...
    private:
    CopyBan(const CopyBan&);
    CopyBan& operator=(const CopyBan&);
    //...
};

原因:

1.設置成私有:如果隻聲明沒有設置成private,用戶自己如果在類外定義瞭,就可以不能禁止拷貝瞭

2.隻聲明不定義:不定義是因為該函數根本不會調用,定義瞭其實也沒有什麼意義,不寫反而還簡單,而且如果定義瞭就不會防止成員函數內部拷貝瞭。

  • C++11擴展delete的用法,delete除瞭釋放new申請的資源外,如果在默認成員函數後跟上=delete,表示讓編譯器刪除掉該默認成員函數。
class CopyBan
{
    // ...
    CopyBan(const CopyBan&)=delete;
    CopyBan& operator=(const CopyBan&)=delete;
    //...
};

設計一個類,不能繼承

C++98

// C++98中構造函數私有化,派生類中調不到基類的構造函數。則無法繼承
class NonInherit
{
    public:
    static NonInherit GetInstance()
    {
        return NonInherit();
    }
    private:
    NonInherit()
    {}
};
class B : public NonInherit
{};
int main()
{
    //C++98中這個不能被繼承的方式不夠徹底,實際是可以繼承,限制的是子類繼承後不能實例化對象
    B b;
    return 0;
}

C++11為瞭更直觀,加入瞭final關鍵字

class A final
{   };
class C: A
{};

設計一個類,隻能創建一個對象(單例模式)

之前接觸過瞭適配器模式和迭代器模式。

可以再看看工廠模式,觀察者模式等等常用一兩個的。

單例模式的概念

設計模式:設計模式(Design Pattern)是一套被反復使用、多數人知曉的、經過分類的、代碼設計經驗的總結。

為什麼會產生設計模式這樣的東西呢?就像人類歷史發展會產生兵法。最開始部落之間打仗時都是人拼人的對砍。後來春秋戰國時期,七國之間經常打仗,就發現打仗也是有套路的,後來孫子就總結出瞭《孫子兵法》。孫子兵法也是類似。

使用設計模式的目的:為瞭代碼可重用性、讓代碼更容易被他人理解、保證代碼可靠性。

設計模式使代碼編寫真正工程化;設計模式是軟件工程的基石脈絡,如同大廈的結構一樣。

  • 單例模式

一個類隻能創建一個對象,即單例模式,該模式可以保證系統中該類隻有一個實例,並提供一個訪問它的全局訪問點,該實例被所有程序模塊共享。比如在某個服務器程序中,該服務器的配置信息存放在一個文件中,這些配置數據由一個單例對象統一讀取,然後服務進程中的其他對象再通過這個單例對象獲取這些配置信息,這種方式簡化瞭在復雜環境下的配置管理。

1.如何保證全局(一個進程中)隻有一個唯一的實例對象

參考隻能在堆上創建對象和在棧上創建對象,禁止構造和拷貝構造及賦值。

提供一個GetInstance獲取單例對象。

2.如何提供隻有一個實例呢?

餓漢模式和懶漢模式。

單例模式的實現

餓漢模式

餓漢模式:程序開始main執行之前就創建單例對象,提供一個靜態指向單例對象的成員指針,初始時new一個對象給它。

class Singleton
{
    public:
    	static Singleton* GetInstance()
        {
            return _inst;
        }
    	void Print()
        {
            cout<<"Print() "<<_val<<endl;
        }
    private:
    	Singleton()
        :_val(0)
        {}
    	Singleton(const Singleton& ) =delete;
    	Singleton(const Singleton& ) =delete;
    	static Singleton* _inst;
    	int _val;
};
Singleton* Singleton::_inst = new Singleton;
int main()
{
    cout<<Singleton::GetInstance()<<endl;
    cout<<Singleton::GetInstance()<<endl;
    cout<<Singleton::GetInstance()<<endl;
    Singleton::GetInstance()->Print();
}

懶漢模式

懶漢模式

懶漢模式出現的原因,單例類的構造函數中要做很多配置初始化工作,那麼餓漢就不合適瞭,會導致程序啟動很慢。

linux是Posix的pthread庫,windows下有自己的線程庫。因此要使用條件編譯保證兼容性。因此c++11為瞭規范提供瞭語言級別的封裝(本質也是條件編譯,庫裡實現瞭)。

關於保護第一次需要加鎖,後面都不需要加鎖的場景的可以使用雙檢查加鎖。

#include<mutex>
#ifdef _WIN32
//windos 提供多線程api
#else
//linux pthread
#endif //
class Singleton
{
    public:
    	static Singleton* GetInstance()
        {
            //保護第一次需要加鎖,後面都不需要加鎖的場景,可以使用雙檢查加鎖
            //特點:第一次加鎖,後面不加鎖,保護線程安全,同時提高瞭效率
            if( _inst == nullptr)
            {
                _mtx.lock();
                if( _inst == nullptr ) 
                {
                    _inst = new Singleton;
                }
                _ntx.unlock();
            }
            return _inst;
        }
    	void Print()
        {
            cout<<"Print() "<<_val<<endl;
        }
    private:
    	Singleton()
        :_val(0)
        {}
    	Singleton(const Singleton& ) =delete;
    	Singleton(const Singleton& ) =delete;
    	static Singleton* _inst;
    	static std::mutex _mtx;
    	int _val;
};
Singleton* Singleton::_inst = nullptr;
std::mutex Singleton::_mtx;//()默認無參構造函數
int main()
{
    Singleton::GetInstance()->Print();
}

餓漢模式和懶漢模式的對比

  • 餓漢模式
    • 優點:簡單
    • 缺點:
      • 如果單例對象構造函數工作比較多,會導致程序啟動慢,遲遲進不瞭入口main函數。
      • 如果有多個單例對象,他們之間有初始化的依賴關系,餓漢模式也會有問題。比如有A和B兩個單例類,要求A單例先初始化,B必須在A之後初始化,那麼餓漢無法保證。這種場景下用懶漢模式,懶漢可以先調用A::GetInstance(),再調用B::GetInstance()。
  • 懶漢模式
    • 優點:解決瞭餓漢的缺點,因為他是第一次調用GetInstance時創建初始化單例對象
    • 缺點:相對餓漢復雜一點。

懶漢模式的優化

實現瞭”更懶“。

缺點:單例對象在靜態區,如果單例對象太大,不合適。再挑挑刺,這個靜態對象無法主動控制釋放。

#include<mutex>
#ifdef _WIN32
//windos 提供多線程api
#else
//linux pthread
#endif //
//其他版本懶漢
class Singleton
{
    public:
    	static Singleton* GetInstance()
        {
            static Singleton inst;
            return &inst;
        }
    	void Print()
        {
            cout<<"Print() "<<_val<<endl;
        }
    private:
    	Singleton()
        :_val(0)
        {}
    	Singleton(const Singleton& ) =delete;
    	Singleton(const Singleton& ) =delete;
    	static std::mutex _mtx;
    	int _val;
};
std::mutex Singleton::_mtx;//()默認無參構造函數
int main()
{
    Singleton::GetInstance()->Print();
}
#include<mutex>
#ifdef _WIN32
//windos 提供多線程api
#else
//linux pthread
#endif //
//其他版本懶漢
class Singleton
{
    public:
    	static Singleton* GetInstance()
        {
            static Singleton inst;
            return &inst;
        }
    	void Print()
        {
            cout<<"Print() "<<_val<<endl;
        }
    private:
    	Singleton()
        :_val(0)
        {}
    	Singleton(const Singleton& ) =delete;
    	Singleton(const Singleton& ) =delete;
    	static std::mutex _mtx;
    	int _val;
};
std::mutex Singleton::_mtx;//()默認無參構造函數
int main()
{
    Singleton::GetInstance()->Print();
}

單例對象的釋放

單例對象一般不需要釋放。全局一直用的不delete也沒問題,進程如果正常銷毀,進程會釋放對應資源。

單例對象的直接釋放

#include<mutex>
#ifdef _WIN32
//windos 提供多線程api
#else
//linux pthread
#endif //
class Singleton
{
    public:
    	static Singleton* GetInstance()
        {
            //保護第一次需要加鎖,後面都不需要加鎖的場景,可以使用雙檢查加鎖
            //特點:第一次加鎖,後面不加鎖,保護線程安全,同時提高瞭效率
            if( _inst == nullptr)
            {
                _mtx.lock();
                if( _inst == nullptr ) 
                {
                    _inst = new Singleton;
                }
                _ntx.unlock();
            }
            return _inst;
        }
    	static void DelInstance()/*調的很少,可以雙檢查也可以不雙檢查*/
        {
            _mtx.lock();
            if(!_inst)
            {
                delete _inst;
                _inst=nullptr;
            }
            _mtx.unlock();
        }
    	void Print()
        {
            cout<<"Print() "<<_val<<endl;
        }
    private:
    	Singleton()
        :_val(0)
        {
          	//假設單例類構造函數中,需要做很多配置初始化   
        }
    	~Singletion()
        {
            //程序結束時,需要處理一下,持久化保存一些數據
        }
    	Singleton(const Singleton& ) =delete;
    	Singleton(const Singleton& ) =delete;
    	static Singleton* _inst;
    	static std::mutex _mtx;
    	int _val;
};
Singleton* Singleton::_inst = nullptr;
std::mutex Singleton::_mtx;//()默認無參構造函數
int main()
{
    Singleton::GetInstance()->Print();
}

內部垃圾回收類

上述場景其實還是可以擴展的。

假設析構函數有一些數據需要保存一下,持久化一下,不調用析構函數會存在問題,因此需要調用析構函數的時候處理。這就得保證main函數結束的時候保證調用析構(private)。

但是顯式調用DelInstance可能會存在遺忘。

#include<mutex>
#ifdef _WIN32
//windos 提供多線程api
#else
//linux pthread
#endif //
class Singleton
{
    public:
    	static Singleton* GetInstance()
        {
            //保護第一次需要加鎖,後面都不需要加鎖的場景,可以使用雙檢查加鎖
            //特點:第一次加鎖,後面不加鎖,保護線程安全,同時提高瞭效率
            if( _inst == nullptr)
            {
                _mtx.lock();
                if( _inst == nullptr ) 
                {
                    _inst = new Singleton;
                }
                _ntx.unlock();
            }
            return _inst;
        }
    	void Print()
        {
            cout<<"Print() "<<_val<<endl;
        }
    private:
    	Singleton()
        :_val(0)
        {
          	//假設單例類構造函數中,需要做很多配置初始化   
        }
    	~Singletion()
        {
            //程序結束時,需要處理一下,持久化保存一些數據
        }
    	Singleton(const Singleton& ) =delete;
    	Singleton(const Singleton& ) =delete;
    	//實現一個內嵌垃圾回收類
    	class CGarbo{
            public:
            	~CGarbo()
                {
                    //DelInstance();
                	if(_inst)
                    {
                         delete _inst;
                        _inst = nullptr;
                    }
                }
        }
    	static Singleton* _inst;
    	static std::mutex _mtx;
    	static GCarbo _gc;//定義靜態gc對象,幫助我們進行回收
    	int _val;
};
Singleton* Singleton::_inst = nullptr;
std::mutex Singleton::_mtx;//()默認無參構造函數
Singleton::CGarbo Singleton::_gc;
int main()
{
    Singleton::GetInstance()->Print();
}

總結

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!   

推薦閱讀: