第一节 封装
类通常包含两类成员:
成员变量(描述相关信息,例如人的姓名,汽车的价格等)
成员函数(操作相关信息,例如给汽车定价,给小婴儿起名字等)
现在假设我们有一个描述汽车的类,对于这个类来说有两种人:使用它的人(用户)和实现它的人(实现者)。类的封装是实现者需要考虑的问题,为了避免用户因为使用不当而引起的一些错误。
如果你是实现者,你就需要考虑到封装问题,尽可能避免别人错误地使用它。在封装中需要注意一些什么呢?
假设我们需要实现一个汽车类,可以使用这样的形式:
它并不完美,甚至在你看来有一些缺陷,但我只是想利用它来讲明一些问题。
这个类里面有两个成员变量,质量和价格,之所以将它们设置为私有,是因为不希望用户在别的地方随意更改并访问它。毕竟更改价格是一件很敏感且慎重的事情,越是容易修改就越是容易出错......然而,随着时间的推移,汽车的价格可能会不断下降,修改价格不可避免,这时我们希望用户能够谨慎的对待这个事情,因此提供了一个含义明显且显眼的函数:
void SetPrice(double price){ //do something... }
用户只能通过这个接口来修改价格,当他们在调用这个接口时,他们也更容易知道自己在干什么。
通过唯一且明显的接口完成相应的功能,这就减少了使用出错的几率。
需要额外指出的是,对于单参构造函数,推荐使用explicit关键字,它的作用是禁止隐式转换。
假设粗心的用户在重新设置汽车重量car.weight() = 1528.6时不小心写成了
car = 1528.6; 这里编译器会首先使用CCar(double n)创建一个临时对象,然后赋值给car。而explicit关键字可以让编译器拒绝这样的操作,假设公司的老板是一位令人尊敬而且讲诚信的人,这意味着编译器的反应可以让公司免遭破产的风险。
然而,如果这个悲惨用户发现编译器发出了抱怨,改成了这样:car= CCar(1528.6);
一切合理又合法......这个世界上总会发生一些令人感到遗憾的事情,而我们却无能为力。
或许有人认为这些东西不值一提,使用变量简单而且方便,假设用户足够小心......你或许应该好好想一想当假设不成立时会发生些什么。
总而言之,如果你是类的实现者,你就应该考虑到用户各种误用的情况,正如如果你是软件的设计者,你就必须考虑到用户哪些操作(哪怕是不合理的)会触发崩溃一样,避免它!以一种显眼的方式让用户知道他们调用代码的含义。
如果你对重载有一定的了解,你或许会发现类中有两个相同的成员函数weight(),它似乎不符合重载的要求:同名函数的形参列表(参数个数,类型,顺序)必须不同。原因在于函数后面的const关键字:类的成员函数会隐式传入一个指向当前对象的this指针,如果函数后面添加了const关键字,那么表示该成员函数传入的this指针是const类型.我们可以认为这两个weight()函数并不一样,一个传入的是const指针,而另一个传入的是非const指针。这就满足重载的要求了。
但我更倾向于以权限的方式来区分它们:const版本的函数是只读函数,只能访问而不能修改类的成员变量,甚至不能调用非const成员函数;而非const版本的函数是读写函数,我们可以使用它来访问以及修改类的成员变量,它也可以调用任何其他的成员函数。
如果你能确保你类中的成员函数不会修改成员变量,那么最好在函数定义和声明的后面加上const关键字,如果你的成员函数会返回成员变量的引用,那么你也应该为这个成员函数实现一个const版本,因为const对象不能调用非const成员函数,原因在于const对象的成员函数传入的是const指针,而非const成员函数传入的是非const指针,const指针不能赋值给非const指针。
如果你有一定的开发经验,你可能还会有这样的顾虑:通过函数返回类的成员是否比直接访问要慢一点呢,函数跳转可是会消耗CPU周期的。不限于这个类,这是一个通用的问题。这很有道理......但在后面的代码优化部分解释这个问题似乎更为恰当,现在我只能告诉你它们的几乎没有区别,而且使函数内联就能够消除这方面的开销。你可以自己创建一个类或者使用CCar类进行简单的测试。
我们再简单回顾一下这一节的内容:
尽量将成员变量设为private属性
如果外部使用者需要修改成员变量,提供的接口尽可能明显且唯一
对于单参构造函数,使用explicit关键字,它的作用是禁止隐式转换
const对象只能调用const成员函数,非const对象可以调用普通成员函数和const成员函数
如果你能确保你的成员函数不会修改成员变量,最好将它声明为const类型,方法是在函数的声明和定义后面加上const关键字
如果你的成员函数返回成员变量的引用,你应该同时实现一个const版本
不必担心访问成员时函数跳转带来的消耗,请放心将成员变量设置为私有属性