随笔 - 2, 文章 - 0, 评论 - 4, 引用 - 0
数据加载中……

在类定义中实现对私有数据成员的隐藏

上篇文章中,小小展示了下指针的强大威力,也揭示了C++中类的不安全性。

但在实际应用中,如果你写的类考虑周全,功能完善的话,类的用户没有必要通过这种方式来访问类的私有成员。同时类的用户自己也有对安全的诉求,因此也一般不会通过此种非正常方式来随意访问类的私有成员。

但是,这里又要提到“但是在实际应用中”——你也许无法一次写出一个完全可靠的类,不可避免地会在以后的编码中逐步对类进行不同程度的修改,有时甚至会大刀阔斧地删除多余的成员,增加其他新的成员。这时头文件就会改变,类成员的地址偏移也会发生变化。你需要向其他编码者更新你的头文件,其他文件中如果用到你的这个类,那么这些文件就需要重新编译、连接,很多问题随之而来。

现在我们要做的就是最大可能地隐藏数据成员的细节,只在头文件中展示使用这个类最必要的部分。

聪明的你一定想到另外再定义一个结构体或类 class MemberData ,把所有数据成员都放到 class MemberData 里面,然后在你的类中声明一个 class MemberData 的对象作为类的私有数据成员。


也许你会这样做:

/*
 *    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

但问题是细节隐藏得还不够深,要提供 myclass.h 必须要连同 memberdata.hpp 一起提供,其他人打开 memberdata.hpp 照样能看见实际的数据成员。

还有更好的办法吗?将 class MemberData 写在 myclass.cpp 里,甚至直接将整个 class MemberData 作为 class MyClass 的私有类?就像下面这样:

注意下面的代码是分成头文件和实现文件两部分的,需要分开放。不然那句 #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=0double _b=0char _c='\0')
        : a(_a)
        , b(_b)
        , c(_c)
    {
    }
};

MyClass::MyClass()
: members(
11'c')
{
}

好,按照我的要求分成两部分了,但编译器看到 MemberData members; 这行时就会提示使用了未定义的 class MemberData。是的,编译器不认同这样的代码,即使我已经在前面给出了 class MemberData 的声明,即使在 myclass.cpp 里我还专门将 class MemberData 的定义放到构造函数前面。

虽然在头文件里只对 class MyClass 的成员做了声明,但却是个实实在在的类定义。编译器看到类定义就会考虑确定这个类中成员的地址偏移,从而进一步确定整个类的大小。而这里的 class MemberData 还是没有定义的,因此无法确定 members 对象的大小,那么这个类型所占用的内存空间也是无法确定的。编译器太心急了,虽然它还未看到  myclass.cpp 中 class MemberData 的定义,虽然 class MyClass 仍然未实例化,它就已经想到以后的事情了。

那么我们考虑将 MemberData members; 这句声明换成 MemberData* pMembers; 。一个指针,无论是什么类型,总是占用 4 个字节的空间,因此其大小是确定的。

果然,没有任何的错误,顺利通过编译和连接。

实际上很多商业代码就是用类似的做法。不过他们更绝,类的其他使用者在头文件中连 class MemberData 的声明都看不到,只看到一个 LPVOID pData 。是的,每个类可能会包含不同的数据,但我们只需要一个指针即可。pData 所指向一个什么样的类型无所谓,需要的时候用 reinterpret_cast 将指针转换到相应的类型即可。

但新的问题随之而来。pMembers (或 pData )未指向任何实在的内存空间,我们必须在构造函数中为  pMembers 分配空间,否则 MyClass 的数据成员并不存在。既然分配了空间,那就还要在析构函数中释放空间。为了稳妥,还必须为 class MyClass 编写拷贝构造函数和赋值函数。

一个类没有什么,但如果每写一个类都要这样做的话,代码量将剧增。相比以前我们只需要简单的 private 一下,那可是麻烦多了。可,我是懒人一个啊。

懒人自有懒人的办法,而且一定要紧跟流行趋势。这几年流行泛型,我们就用写个类模板来实现想要的功能。

 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() 返回数据类的对象的引用;
Implement::get() const 返回数据类的对象的常引用;
Implement::get_ptr() 返回数据类的指针;
Implement::operator->() 返回数据类的指针,是为某些我这样的懒人准备的,用的时候少写几个字母而已。但注意由于这里重载的是 -> 操作符,因此实际得到的是数据类的对象!于是其功能同 Implement::get() 是一样的。

class Implement_noncopyable 的对象除不可拷贝外,其他用法同 class Implement 一样。

class MyClassImp 的细节都隐藏在 cpp 文件中,只要不公开 cpp 实现,从外部是根本没法进行直接访问的。

下面是一个使用实例:

//////////////////////////////////////////////////////////////////
// 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 类中定义了指针,并为其分配了堆空间
     *    在析构函数中仍然需要释放这个指针指向的堆空间
     
*/
    
~MyClassImp() { delete b; }
};

int MyClass::GetA()
{
    
return Imp.get().a;
}

void MyClass::SetA(int _a)
{
    Imp
->= _a;
}

需要的注意的是,类模板 Implement 中虽然封装了堆空间的分配和释放操作,但这是针对 class MyClassImp 的。而对于 class MyClassImp 中的数据成员,仍然需要自行进行空间的分配和释放,这一点同以前是没有两样的。

posted on 2008-08-11 01:32 iwong 阅读(888) 评论(0)  编辑 收藏 引用


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   知识库   博问   管理