上篇文章中,小小展示了下指針的強大威力,也揭示了C++中類的不安全性。
但在實際應用中,如果你寫的類考慮周全,功能完善的話,類的用戶沒有必要通過這種方式來訪問類的私有成員。同時類的用戶自己也有對安全的訴求,因此也一般不會通過此種非正常方式來隨意訪問類的私有成員。
但是,這里又要提到“但是在實際應用中”——你也許無法一次寫出一個完全可靠的類,不可避免地會在以后的編碼中逐步對類進行不同程度的修改,有時甚至會大刀闊斧地刪除多余的成員,增加其他新的成員。這時頭文件就會改變,類成員的地址偏移也會發(fā)生變化。你需要向其他編碼者更新你的頭文件,其他文件中如果用到你的這個類,那么這些文件就需要重新編譯、連接,很多問題隨之而來。
現(xiàn)在我們要做的就是最大可能地隱藏數(shù)據(jù)成員的細節(jié),只在頭文件中展示使用這個類最必要的部分。
聰明的你一定想到另外再定義一個結構體或類 class MemberData ,把所有數(shù)據(jù)成員都放到 class MemberData 里面,然后在你的類中聲明一個 class MemberData 的對象作為類的私有數(shù)據(jù)成員。
也許你會這樣做:
/*
* memberdata.hpp
*/
#ifndef MEMBERDATA_HPP
#define MEMBERDATA_HPP
class MemberData
{
private:
int a;
double b;
char c;
//





friend class MyClass;
};
#endif // MEMBERDATA_HPP
//////////////////////////////////////////////////////////////////////////
/*
* myclass.h
*/
#ifndef MYCLASS_H
#define MYCLASS_H
#include "memberdata.hpp"
class MemberData;
class MyClass
{
public:
MyClass();
~MyClass();
private:
MemberData members;
};
#endif // MYCLASS_H
但問題是細節(jié)隱藏得還不夠深,要提供 myclass.h 必須要連同 memberdata.hpp 一起提供,其他人打開 memberdata.hpp 照樣能看見實際的數(shù)據(jù)成員。
還有更好的辦法嗎?將 class MemberData 寫在 myclass.cpp 里,甚至直接將整個 class MemberData 作為 class MyClass 的私有類?就像下面這樣:
注意下面的代碼是分成頭文件和實現(xiàn)文件兩部分的,需要分開放。不然那句 #endif 會作怪。
/*
* myclass.h
*/
#ifndef MYCLASS_H
#define MYCLASS_H
#include "memberdata.hpp"
class MyClass
{
public:
MyClass();
~MyClass(){};
private:
class MemberData;
MemberData members;
};
#endif // MYCLASS_H
//////////////////////////////////////////////////////////////////////////
/*
* myclass.cpp
*/
#include "myclass.h"
class MyClass::MemberData
{
int a;
double b;
char c;
//





MemberData(int _a=0, double _b=0, char _c='\0')
: a(_a)
, b(_b)
, c(_c)
{
}
};
MyClass::MyClass()
: members(1, 1, 'c')
{
}
好,按照我的要求分成兩部分了,但編譯器看到 MemberData members; 這行時就會提示使用了未定義的 class MemberData。是的,編譯器不認同這樣的代碼,即使我已經(jīng)在前面給出了 class MemberData 的聲明,即使在 myclass.cpp 里我還專門將 class MemberData 的定義放到構造函數(shù)前面。
雖然在頭文件里只對 class MyClass 的成員做了聲明,但卻是個實實在在的類定義。編譯器看到類定義就會考慮確定這個類中成員的地址偏移,從而進一步確定整個類的大小。而這里的 class MemberData 還是沒有定義的,因此無法確定 members 對象的大小,那么這個類型所占用的內(nèi)存空間也是無法確定的。編譯器太心急了,雖然它還未看到 myclass.cpp 中 class MemberData 的定義,雖然 class MyClass 仍然未實例化,它就已經(jīng)想到以后的事情了。
那么我們考慮將 MemberData members; 這句聲明換成 MemberData* pMembers; 。一個指針,無論是什么類型,總是占用 4 個字節(jié)的空間,因此其大小是確定的。
果然,沒有任何的錯誤,順利通過編譯和連接。
實際上很多商業(yè)代碼就是用類似的做法。不過他們更絕,類的其他使用者在頭文件中連 class MemberData 的聲明都看不到,只看到一個 LPVOID pData 。是的,每個類可能會包含不同的數(shù)據(jù),但我們只需要一個指針即可。pData 所指向一個什么樣的類型無所謂,需要的時候用 reinterpret_cast 將指針轉(zhuǎn)換到相應的類型即可。
但新的問題隨之而來。pMembers (或 pData )未指向任何實在的內(nèi)存空間,我們必須在構造函數(shù)中為 pMembers 分配空間,否則 MyClass 的數(shù)據(jù)成員并不存在。既然分配了空間,那就還要在析構函數(shù)中釋放空間。為了穩(wěn)妥,還必須為 class MyClass 編寫拷貝構造函數(shù)和賦值函數(shù)。
一個類沒有什么,但如果每寫一個類都要這樣做的話,代碼量將劇增。相比以前我們只需要簡單的 private 一下,那可是麻煩多了。可,我是懶人一個啊。
懶人自有懶人的辦法,而且一定要緊跟流行趨勢。這幾年流行泛型,我們就用寫個類模板來實現(xiàn)想要的功能。
1 #ifndef IWONG_IMPLEMENT_HPP
2 #define IWONG_IMPLEMENT_HPP
3 namespace iwong {
4
5 //////////////////////////////////////////////////////////////////////////
6 // noncopyable
7 template<typename tClass> class Implement_noncopyable
8 {
9 public:
10 typedef typename tClass element_type;
11 typedef typename element_type* element_type_pointer;
12 typedef typename element_type& element_type_reference;
13 typedef typename element_type const& element_type_const_reference;
14 typedef typename Implement_noncopyable<element_type> this_type;
15 typedef typename this_type& reference;
16 typedef typename this_type const& const_reference;
17
18 public:
19 Implement_noncopyable() : pImp(NewPtr()) {}
20 ~Implement_noncopyable() { Release(); }
21
22 public:
23 element_type_pointer get_ptr() { return pImp; }
24 element_type_reference get() { return *pImp; }
25 element_type_const_reference get() const { return *pImp; }
26 element_type_pointer operator->() { return get_ptr(); }
27
28 protected:
29 Implement_noncopyable(const_reference _other) : pImp(NewPtr(_other)) {}
30
31 virtual const_reference operator=(const_reference _other)
32 {
33 ValueCopy(_other);
34 return *this;
35 }
36
37 private:
38 element_type_pointer NewPtr() { return new element_type; }
39 element_type_pointer NewPtr(const_reference _other) { return new element_type(_other.get()); }
40 void ValueCopy(const_reference _ohter) { get() = _ohter.get(); }
41 void Release() { delete pImp; }
42
43 private:
44 element_type_pointer pImp;
45 };
46
47 //////////////////////////////////////////////////////////////////////////
48 // copyable
49 template<typename tClass> class Implement : public Implement_noncopyable<tClass>
50 {
51 public:
52 typedef typename const Implement<tClass>& const_reference;
53
54 public:
55 Implement() : Implement_noncopyable() {}
56 Implement(const_reference _other) : Implement_noncopyable(_other) {}
57
58 const_reference operator=(const_reference _other)
59 {
60 Implement_noncopyable::operator=(_other);
61 return *this;
62 }
63 };
64
65 //////////////////////////////////////////////////////////////////////////
66
67 } // namespace iwong
68
69 #endif // IWONG_IMPLEMENT_HPP
在這個類模板里,我們封裝了堆空間的分配和釋放,還加上了一些必要的操作。
Implement::get() 返回數(shù)據(jù)類的對象的引用;
Implement::get() const 返回數(shù)據(jù)類的對象的常引用;
Implement::get_ptr() 返回數(shù)據(jù)類的指針;
Implement::operator->() 返回數(shù)據(jù)類的指針,是為某些我這樣的懶人準備的,用的時候少寫幾個字母而已。但注意由于這里重載的是 -> 操作符,因此實際得到的是數(shù)據(jù)類的對象!于是其功能同 Implement::get() 是一樣的。
class Implement_noncopyable 的對象除不可拷貝外,其他用法同 class Implement 一樣。
class MyClassImp 的細節(jié)都隱藏在 cpp 文件中,只要不公開 cpp 實現(xiàn),從外部是根本沒法進行直接訪問的。
下面是一個使用實例:
//////////////////////////////////////////////////////////////////
// MyClass.h
#ifndef MYCLASS_H
#define MYCLASS_H
#include <Implement.hpp>
using namespace iwong;
class MyClass
{
public:
MyClass(){}
~MyClass(){}
int GetA();
void SetA();
private:
class MyClassImp;
Implement<MyClassImp> Imp;
};
#endif // MYCLASS_H
///////////////////////////////////////////////////////////////////
// MyClass.cpp
#include "MyClass.h"
MyClass::MyClass()
{}
class MyClass::MyClassImp
{
public:
int a;
int* b;
double c;
char d;
MyClassImp()
: a(0)
, b(new int(0))
, c(0.0)
, d('d')
{
}
/*
* 若在 Imp 類中定義了指針,并為其分配了堆空間
* 在析構函數(shù)中仍然需要釋放這個指針指向的堆空間
*/
~MyClassImp() { delete b; }
};
int MyClass::GetA()
{
return Imp.get().a;
}
void MyClass::SetA(int _a)
{
Imp->a = _a;
}
需要的注意的是,類模板 Implement 中雖然封裝了堆空間的分配和釋放操作,但這是針對 class MyClassImp 的。而對于 class MyClassImp 中的數(shù)據(jù)成員,仍然需要自行進行空間的分配和釋放,這一點同以前是沒有兩樣的。