对象模型学习总结 (一) . 关于封装
前言
首先感谢 @JerryZhang 在端午节百忙之中帮忙校验。里面杂七杂八的地方错了不少。自己也看了两三遍,但难保里面还有一些错误。如果有所发现请告诉一声。感激不尽。
写这个的原因是想回答上一阵子一名同学在C++奋斗乐园提问的一个关于虚拟继承的问题。后来导致了自己重新复习一下《深入探索C++对象模型》,就想着顺便把这本书总结一下。所以内容可能有点多。如果有时间的话还是推荐看一看《深入探索C++对象模型》这本书。
其中内容夹杂的东西较多,一些是总结于书上的内容,一些是自己的一些想法。我会尽量标明哪些是来自书籍资料、哪些是自己的想法,以免误导他人,请大家斟酌吸纳。另外,能力不够,错误难免也会很多。希望大神不吝赐教。
里面一些地方用上了C语言去理解C++的一些东西,所以如果看后觉得误导您的话,那么抱歉了。
对于封装我没啥使用经验,也并未能了解其精华与本质。Jerryzhang 也跟我说过“讲封装就不应该把 C 语言拉进来,用面向过程语言去实现面向对象,为什么不直接用面向对象语言去实现面向对象呢?”。最终我还是贴出来吧(T^T请勿拍砖)。把它做为一种笔记或是心得,或许在某年某月翻过来看一看也可以看到自己的一些成长过程。另外我个人觉得面向对象是一种对具体事物的一种抽象,提供更好的程序组织逻辑,提高程序复用,减少重复编码的一种思维方法,而不用太纠结于某某编程语言。就像耗子大神也曾经用UNIX去解释设计模式一样。详情请戳:从面向对象的设计模式看软件设计
另外原帖贴在:http://www.cppleyuan.com/forum.php?mod=viewthread&tid=11546
关于封装
在面向对象程式设计方法中,封装(英语:Encapsulation)是指,一种将抽象性函式接口的实作细节部份包装、隐藏起来的方法。同时,它也是一种防止外界呼叫端,去存取物件内部实作细节的手段,这个手段是由编程语言本身来提供的。这两个概念有一些不同,但通常被混合使用。封装被视为是面向对象的四项原则之一。
适当的封装,可以将物件使用接口的程式实作部份隐藏起来,不让使用者看到,同时确保使用者无法任意更改物件内部的重要资料。它可以让程式码更容易理解与维护,也加强了程式码的安全性。 (来自:wikipedia)
简单的以下面的point class 为例,把数据与函数联合,隐蔽掉用户对数据的直接操作,提供一组对外开放的函数以供用户对数据的操作,就称为封装。(个人理解)
class point{ public : point() { x = 0; y = 0; } point(int x, int y) { this->x = x; this->y = y; } void set_x(int x) { this->x = x; } void set_y(int y) { this->y = y; } int get_x() { return x; } int get_y() { return y; } private: int x; int y; };
在point class 里面, 使用private 隐藏了point 的x坐标属性和y坐标属性数据成员。用户只能通过point提供的对外开放函数 set_x()、set_y()、get_x()、get_y() 来对数据x和y进行访问、修改。但是对于计算机内部来说,数据是没有被分为private和public的(当然,那些如X86 CPU 提供对数据段\代码段的访问控制不属于Object Oriented Programming 的谈论范围内)。那么理论上只要取得point object在内存的地址,是可以对private保护起来的数据进行访问的。但是这样做却破坏了Object Oriented Programming 的封装性。
像下面的例子,通过取得point object的内存地址,就可以实现对point object中被保护的数据进行操作。如果在即使编程中使用这种编程方式可能会造成不可预料的后果。
int main (void) { point pt; int *p = (int*)&pt; p[0] = 11; p[1] = 22; std::cout << pt.get_x() << std::endl; std::cout << pt.get_y() << std::endl; return 0; }
如果用C语言,如何理解封装
先举一个反面例子, 像下面的point class,虽然使用了C++ class进行对point的封装,但能说它实现了对point的封装?这个point class中并没有提供对它应该处理的x和y 进行有效的处理。反而提供两个两个与这个object毫无相关的的eat(), sleep()函数。(个人理解)
class point{ public : point(); void eat() { std::cout << "I'm can cat"; } void sleep() { std::cout << "I'm can sleep"; } private: int x; int y; };
C语言的封装,通常的做法是以一个file作为一个object。像:point.h头文件提供给用户接口,point.c源文件对头文件的声明进行实现。用户在使用的时候在意的是point.h提供的数据结构和对这个结构进行操作的函数方法。(个人理解)
C语言方式的封装大概如下:
// point.h 头文件(定义) typedef struct { int x; int y; }point; void init_point(point *pt, int x, int y); void set_x(point *pt, int x); void set_y(point *pt, int y); int get_x(point *pt); int get_y(point *pt); void zero_point(point *pt);
//point.c 源文件(实现) void init_point(point *pt, int x, int y) { pt->x = x; pt->y = y; } void set_x(point *pt, int x) { pt->x = x; } void set_y(point *pt, int y) { pt->y = y; } int get_x(point *pt, int x) { return pt->x; } int get_y(point *pt, int y) { return pt->y; } static void zero_point(point *pt) { pt->x = 0; pt->y = 0; }
虽然C语言没有提供类似private的访问控制,但若point的开发者想隐藏某些方法(函数), 或者数据(属性)。那么可以在方法或声明属性的前面加上static关键字进行对方法或属性的隐藏。这样别的文件里面就没办法使用到point文件中被加上static属性的成员了。如代码中的 static void zero_point(point *pt) 在别的实现文件里面是无法引用到它的。
这种方式在我理解就是C语言的封装方式了。
为什么需要封装?
先看一个例子:
// point.h 头文件 typedef struct { int x; int y; }point; void init_point(point *pt, int x, int y); void set_x(point *pt, int x); void set_y(point *pt, int y); int get_x(point *pt); int get_y(point *pt); typedef animal{ char name[256]; }animal; void init_animal(animal *al, char *name); void eat(animal *al); void sleep(animal *al);
//point 源文件 #include "point.h" void init_animal(animal *al, cha *name) { strcpy(al->name, name) } void eat(animal *al) { printf ("I'm %s I can eat", al->name); } void sleep(animal *al) { printf ("I'm %s I can sleep", al->name); } void init_point(point *pt, int x, int y) { pt->x = x; pt->y = y; } void set_x(point *pt, int x) { pt->x = x; } void set_y(point *pt, int y) { pt->y = y; } int get_x(point *pt) { return pt->x; } int get_y(point *pt) { return pt->y; } static void zero_point(point *pt) { pt->x = 0; pt->y = 0; }
上面的代码看起来是没有什么问题,但是不好的地方就在把两个毫不相关的数据结构整合到一个文件里面。比较好的做法是将animal和point两种不同的数据结构放到不同的文件里面。实现功能的隔离。上面的例子中,只是提供了两个数据结构的处理而已,如果多写上几个数据结构,那么这个头文件简直就是乱七八糟了(当然,某些强人记忆力很强大,我无话可说)。当然,功能相近的在一起是没什么问题的,还是功能不相关的扔在一起,总是会让人蛋疼菊紧的。但又有一个问题,里面我的animal结构实在太小了,放到一个文件里头确实有点大题小做。(个人理解)
像C++这类面向对象的语言提供了较为优雅的处理方法。
class point{ public : point() { x = 0; y = 0; } point(int x, int y) { this->x = x; this->y = y; } void set_x(int x) { this->x = x; } void set_y(int y) { this->y = y; } int get_x() { return x; } int get_y() { return y; } private: int x; int y; }; class animal{ public: animal(char *name) { strcpy(this->name, name); } void eat() { cout << "I'm " << name << "I can eat" << endl; } void sleep() { cout << "I'm " << name << "I can sleep" << endl; } private: char name[256];
};
通过class 的封装之后,在使用animal 或point的时候,是通过animal 或point的object去操作的。这样看起来程序的组织逻辑会比对上面C语言对point和animal清晰一些,条理一些。当然,就算是在C++中,不同功能的class最好还是封装在不同的文件里面。(个人理解)
C++ class的封装与C语言相比效率如何?
在不考虑继承、多态、内存对齐的情况下,C++ object在内存上的开销就等同于这个object 的数据成员之和了。而且在无继承无多态的情况下,函数(成员函数)的调用,C语言和C++的效率应该是不相上下的,引用对象模型中的一句话 "members function虽然包含class 的声明之中,却不出现在object之内,每一个non-inline member function只会诞生一个函数实体,至于每一个“拥有0个或多个定义”的inline function则会在其每一个调用者模块身上参生一个函数实体...C++在布局以及存取时间上主要的额外负担是由virtual引起的 "。(总结于《深入探索C++对象模型》)
MSDN 的一篇文章 “C++ under the Hood” 对C++的内存布局有这么一段的描述: "...except for hidden data members introduced to implement virtual functions and virtual inheritance, the instance size is solely determined by a class’s data members and base classes."
既然说C++中class 的成员函数不在object之内(是成员函数而不是虚函数或虚继承)。那么在程序的看来只要取得class中的成员函数。那么即使在没有这个class的object情况下也是可以调用这个成员函数。如下面的测试:
class point { public: void show() { cout << "point show" << endl; } private: int a; }; int main(void) { point *pt = NULL; pt->show(); return 0; }
如果拿C来比较的话,它的调用方式大概等于这样:
void show (point *this) { printf ("point show\n"); } .... //调用方式 show (NULL);
这个例子,也从侧面说明了C++是怎么对成员函数的处理。要注意的是,如果show函数中包含了对class 中数据成员的使用,那么程序肯定崩溃。因为传入NULL的话,this所指的压根就不存在对象。
《深入探索C++对象模型》有一段话说,C++ class 在无继承,无多态(成员函数不含virtual), 无构造函数/析构函数(有构造函数/析构函数的话,object 在定义和释放的时候会调用构造函数/析构函数费去一定的时间)的情况下。效率至少要达到C语言的效率。在某种程度来说C++ 的效率不会低于C语言的效率。如果C语言想要像C++那样模拟实现封装、多态,那么会比C++付出更大的代价。
C++ class中 access level 对封装的影响
访问控制(access level)主要的影响还是在C++的内存布局上。
MSDN 中 C++ under the Hood有这么一段话:"There are public/protected/private access control declarations, member functions, static members, and nested type declarations. Only the non-virtual data members occupy space in each instance. Note that the standards committee working papers permit implementati ons to reorder data members separated by an access declarator, so these three members could have been laid out in any order. (In Visual C++, members are always laid out in declaration order, just as if they were members of a C struct)"
其中应该注意的是 "Note that the standards committee working papers permit implementati ons to reorder data members separated by an access declarator, so these three members could have been laid out in any order" 这句话。这句话的大概意思是说,C++标准委员会并未对声明在不同的acess level各段之间的数据成员在实例化时的内存布局并没有进行强制的规定。因此,不同的编译器可以不同的做法。但是目前流行的编译器 VC 和 GCC都是采取按照声明的先后进行内存布局。
总结
封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
封装提供给开发用户一个更好的程序组织的逻辑思路,而面向对象的语言的封装方式提供了更好的封装便利。
在《深入探索C++对象模型》中,对于上面介绍的那种简单的封装称之为抽象数据类型模型(abstract data type model, ADT)(接触过数据结构一定对ADT不陌生)。而面向对象模型,是通过一个抽象的base class(提供共同接口)被封装起来。(我个人感觉,设计模式似乎就是面向对象模型)。(对《深入探索C++对象模型》的概括)
(总结仅是个人理解,若有误导之处,那么抱歉)