C++语言中的类型(二)
——分门别类是简化事物最有效的方式。
C++语言的强大能力的体现在对程序员自定义数据类型的支持。C++语言主要的一个设计目标就是让程序员自定义的数据类型像内置类型一样好用。
一、自定义数据类型
数据类型告诉我们数据的意义以及我们能在数据上执行的操作。因而自定义数据类型把数据及其对应操作组合在一起。
类的定义形式:
class ClassName
{
//数据
…
//操作
…
};
二、类的实例化方式
如何实例化一个类呢?最简单直接的方式就是模具铸造技术。就是把类的定义作为一个模具,铸造出一个个对象。这种方式最直接,也最容易理解,但效率却不高。那么应该如何实例化呢?其实也很简单,同中国的活字印刷术一样,只需要稍微转变一下观念。何必非要坚持对象的内容在物理上的连续性呢,只要对象的内容是完整的,就足够了。因而我们的眼中不再是一页一页的文字(类),而是一个个字(类成员)。
1、按照这种思路,可以把类成员抽象为两种类型
1) 同一类型的所有实例都相同的类成员
这种类成员,一个程序中保存一份就足够了。
2) 同一类型的各个实例不相同的类成员
这种类成员,每个对象都需要一份,以便呈现自己的独特风采。
2、数据成员与成员函数的实例化
(1)数据成员
按照上述对类成员的抽象,数据成员被区分为两类:
1) 属于类的数据成员(static)
一个程序保留一份即可,由类进行初始化。
2) 属于对象的数据成员(默认状态)
每个对象包含一份,由每个对象进行初始化。
(2)成员函数(方法)
其实对于同一类型的所有实例,它们所具有的方法是相同的,即方法只属于类,而不独属于某个对象,因而一个程序中具有一份就足够了。然而由于数据成员具有两种类型,因而C++根据方法对数据成员的操作方式的不同,分为两种类型:
1) 不可以直接(其实并不是真正的直接,下面会谈到)操作属于对象的数据成员(static)
2) 可以直接操作属于对象的数据成员(默认状态)
试图把数据成员和成员函数的分类统一起来,其实反而会带来更多的困扰,因为它们的分类依据本来就不相同。成员函数的分类依据其实很简单,就是this参数(隐式参数)的声明形式不同而已。
1) 常量成员函数(const)
const ClassName *const this;//指向常量对象的常量指针
因而常量函数不能改变对象的数据成员,常量对象不能调用非常量函数(this形参无法初始化)。
2) 静态成员函数(static)
没有声明this参数,因而不可以通过this指针操作对象的成员。
3) 普通成员函数(默认状态)
ClassName *const this;//指向普通对象的常量指针
从这里我们可以看出,本质上,成员函数和其他的函数并没有什么不同(仅仅书写形式上不同而已),都需要借助参数来访问函数之外的对象。
三、读写属性的控制
仅仅把数据和操作简单地组合在一起,还不能称之为是一个真正的类,只能称之为一个代码的集合而已。还需要添加对成员的读写权限。对于自定义类型,C++语言把类的成员的读写属性抽象为两个级别。
1、 特殊读写属性(可见性)的控制
成员的可见性(可见性是对于类外而言)是读写属性中比较特殊的一种,它区分了类内和类外,达到了对数据的封装。成员的可见性具有两个状态(暂不考虑继承,其实有了继承也不过是再增加一个protected而已。):
1) 可见(public)
2) 不可见(private)
为了代码的清晰,C++语言不再依赖默认状态,允许以更明确的方式控制成员的可见性。
2、 普通读写属性的控制
普通级别的读写属性控制和普通变量一样,是专门针对数据成员的。由类的实例化方式可知,无论是类外还是类的成员函数,其实都是通过对象以间接的方式访问其数据成员。因而都是典型的二层结构。
如图A是一个类对象,A包含3个public权限的数据成员:a、b和c。
对于对象A,它和普通变量一样,用const声明其为常量对象,用默认状态声明其为非常量对象。当A为一个常量对象时,相当于在对象的最外层镶了一层特殊的铁皮,只能通过A对A中的数据成员进行读操作,而不能进行写操作。如果变量a、b和c都是默认读写状态,声明A为常量对象就相当于(实际上并不等同,因为常量属性只施加在了外层对象上,只要突破了这层保护,该声明也就没有意义了。)变量a、b和c都是常量了。这样很不灵活,无法对数据成员的读写属性进行精细化控制,因而C++允许类的数据成员具有三种读写状态:
1) 总是仅可读(const)
2) 总是可读写(mutable)
3) 与类对象的声明一致(默认状态)
这样,我们就可以对任一个数据成员施加我们期望的读写权限了。
四、类型间沟通的门户
我们把事物分割在一个个相互隔离的框架内,既是为了简化问题,也是为了安全。但不同事物间是需要沟通的,因而C++语言在设置了一个个条条框框之后也开辟了几个门户,以达到沟通的目的,同时又可以设置重兵监督把守。
C++语言中的门户主要有以下几种:
1) 类型转换
通过定义转换构造函数,可以实现不同类型间的转换。但需要注意的是,与内置数据类型不同,一般的类型之间并没有那么强的联系,因而类型转换往往是没有必要的,而且会带来很大的安全风险。因此,通常情况下,应该抑制类型间的转换,把转换构造函数声明为explicit。
2) 成员函数
上面说到了,类型转换是危险的,而且往往是没有必要的。当两个类型间只有一种操作有联系时,定义一个函数应该是最合适的。
3) 友元
类的公共接口的设计应当是满足用户最常见的一般性需求。但是有一般的用户,肯定就有一些需求***钻的用户。为了满足所有用户的需求,而把类设计得异常庞大是没有必要的。于是C++提供了友元,授予友元对类的所有成员的访问权限。通过友元,你可以如设计成员函数一样,设计出满足自己需求的高效接口,同时又不改变类的一般结构。可以说,友元提出的初衷是非常好的,但是这种不负责任的态度却也是危险的——你给别人开了后门,然后就理所当然地撒手不管了,那么有心人就可能背地里干些坏事了。