条款29: 避免返回内部数据的句柄
尽量不要返回类成员变量的地址或引用等句柄,以防止被外部轻易的改变。
而且很容易出问题。如返回了一个类内部成员变量指针指向某个缓冲区,在外部调用delete等操作这个缓冲区,势必造成类内部成员变量不能再有效的使用了。
也不要返回了一个局部对象,局部对象出了作用域之后就被销毁。
条款30: 避免这样的成员函数:其返回值是指向成员的非const指针或引用,但成员的访问级比这个函数要低
被返回不仅仅可以是数据成员,还要考虑到成员函数。因为返回一个成员函数的指针也是有可能的,而这个成员函数为私有的时候,通过返回它的指针,外部就可以调用它。
如果你想提高效率而返回成员的指针或引用,那么请加上const。
条款31: 千万不要返回局部对象的引用,也不要返回函数内部用new初始化的指针的引用
返回局部对象的引用的话,局部对象有可能随时被销毁。
也不要返回函数内部用new初始化的指针的引用,以防止在外部,用户忘记释放这个空间。
条款32: 尽可能地推迟变量的定义
因为在某些时候,还不等不到执行某个变量的定义,函数就返回了。
这样,为了提高效率,在使用对象的地方再定义它。
如:
fun()
{
somecode;
if (bVal) return;
Object a(10);
}
如果bVal为true,则可以避免调用a的构造函数来对a进行初始化,可以提高效率。
条款33: 明智地使用内联
内联会导致代码体积膨胀。
虚函数不能为内联。
函数中有循环或递归时,不能用内联。
构造函数和析构函数不适合内联。因为它们里面常常含有很多隐式的代码。比如说,类成员变量A a; 则会在构造函数里面调用它的默认构造函数进行初始化。如果A里面还有其它
对象,还会进一步的调用,实际上它的代码体积很大。
如果函数中包含静态对象,通常要避免将它声明为内联函数。
条款34: 将文件间的编译依赖性降至最低
很多情况下,改变某个类的定义,会导致很多文件重新被编译。将其定义与声明分离,可能可以解决这种情况。
分离的关键在于,"对类定义的依赖" 被 "对类声明的依赖" 取代了。
只要有可能,尽量让头文件不要依赖于别的文件;如果不可能,就借助于类的声明,不要依靠类的定义。
下面就是这一思想直接深化后的含义:
· 如果可以使用对象的引用和指针,就要避免使用对象本身。定义某个类型的引用和指针只会涉及到这个类型的声明。定义此类型的对象则需要类型定义的参与。
· 尽可能使用类的声明,而不使用类的定义。因为在声明一个函数时,如果用到某个类,是绝对不需要这个类的定义的,即使函数是通过传值来传递和返回这个类
如:
class Date; // 类的声明
Date returnADate(); // 正确 ---- 不需要Date的定义
void takeADate(Date d);
这些情况下只需要知道一个类的声明即可,用不着类的定义。
在头文件中尽量不依赖别的文件,而在cpp中包含。
也可以通过指针,来将对象的实现隐藏起来。
如:
Person *p; //此时就不用知道Person的定义
工厂模式,也有这些思想在里面。
条款35: 使公有继承体现 "是一个" 的含义
公有继承意味着 "是一个" 。当写下类D("Derived" )从类B("Base")公有继承时,你实际上是在告诉编译器(以及读这段代码的人):类型D的每一个对象也是类型B的一个对
象。你是在声明:任何可以使用类型B的对象的地方,类型D的对象也可以使用,因为每个类型D的对象是一个类型B的对象。
但要注意,如企鹅是鸟,可以从鸟里面仅有继承下来,但是企鹅不会飞。所以,好的设计是和软件系统现在和将来所要完成的功能密不可分的。
条款36: 区分接口继承和实现继承
可以为纯虚函数提供实现,但调用它的唯一方式是通过类名完整地指明是哪个调用。
条款37: 决不要重新定义继承而来的非虚函数
因为非虚函数在静态编译的时候就已经确定了,无法实现多态,所以,重写继承来的非虚函数,调用这个非虚函数的时候,只会和指针类型相同。
如: Base *p = new Derive;
p->fun(); //如果fun为非虚的,就会调用 Base::fun(),而不会调用Derive::fun();
条款38: 决不要重新定义继承而来的缺省参数值
成员函数有非虚函数和虚函数。根据上个条款可知,非虚函数在派生类中重写一般是错误的。 所以,我们完全可以把讨论的范围缩小为 "继承一个有缺省参数值的虚函数" 的情况。
但是,虚函数是动态绑定而缺省参数值是静态绑定的。
所以,继承而来的缺省参数和父类是相同的,而不受子类指定的默认参数的影响。
如果重写继承而来的缺省参数,这意味着你最终可能调用的是一个定义在派生类,但使用了基类中的缺省参数值的虚函数。
条款39: 避免 "向下转换" 继承层次
向下转换难看、容易导致错误,而且使得代码难于理解、升级和维护
不过有时候向下转换还是很有用处的。如:
A是父类
B B2 是子类
C C2 C3 C是B的子类,C2 C3是B2的子类。
fun (A *pa)
{
if (dynamic_cast <B *> (pa)) {...} //如果传给fun的是B或B的子类(即C)的指针,这时候就会返回TRUE,就是有效的。
else if (dynamic_cast <B2 *> (pa)) {...} //如果fun是B2或B2的子类(即C2 C3)的指针,成功。
}
用typeid运算符则达不到这样的效果,它只能具体到某个类,而不能判断是否为它的子类。
条款40: 通过分层来体现 "有一个" 或 "用...来实现"
使某个类的对象成为另一个类的数据成员,从而实现将一个类构筑在另一个类之上,这一过程称为 "分层"(Layering)。
"分层" 这一术语有很多同义词,它也常被称为:构成(composition),包含(containment)或嵌入(embedding)。
公有继承的含义是 "是一个"。对应地,分层的含义是 "有一个" 或 "用...来实现"。
条款41: 区分继承和模板
类型T影响类的行为吗?如果T不影响行为,你可以使用模板。如果T影响行为,你就需要虚函数,从而要使用继承。
条款42: 明智地使用私有继承
如果两个类之间的继承关系为私有,编译器一般不会将派生类对象(如Student)转换成基类对象(如Person)。
私有继承意味着只是继承实现,接口会被忽略。而且调用私有继承的父类的函数,只能用父类名称::函数名()的方式来调用。
类的组合也是只使用某个类的实现,但与私有继承是有区别的,不过一般情况下能使用组合就使用组合。
私有继承意味着 "用...来实现"。如果类D私有继承于类B,类型D的对象只不过是用类型B的对象来实现而已;类型B和类型D的对象之间不存在概念上的关系。
条款43: 明智地使用多继承
多继承带来二义性。如两个父类含有相同的成员函数名子的时候。
此时只能通过指定父类名+函数名来调用,但当显式地用一个类名来限制修饰一个虚函数时,函数的行为将不再具有虚拟的特征。
多继承有可能导致钻石结构。
但使用好多继承,避免钻石结构,还是很有用处的。
条款44: 说你想说的;理解你所说的
条款45: 弄清C++在幕后为你所写、所调用的函数
如果你没声明如下函数,编译器将会为你自动生成:
一个拷贝构造函数,一个赋值运算符,一个析构函数,一对取址运算符。另外,如果你没有声明任何构造函数,它也将为你声明一个缺省构造函数。
注意,生成的析构函数一般是非虚拟的。生成的默认拷贝和赋值构造函数,都是潜拷贝,只按位拷贝。
如果想禁止使用这些函数,显示地将它们声明为private型的。
条款46: 宁可编译和链接时出错,也不要运行时出错
条款47: 确保非局部静态对象在使用前被初始化
如果在某个被编译单元中,一个对象的初始化要依赖于另一个被编译单元中的另一个对象的值,并且这第二个对象本身也需要初始化,但,第一个对象的初始化可能在第二个对象
的初始化之前进行的,这时候就会出问题。
而且,你绝对无法控制不同被编译单元中非局部静态对象的初始化顺序。
使用单件模式(singleton pattern)可以解决这个问题,把每个非局部静态对象转移到函数中,声明它为static。其次,让函数返回这个对象的引用。
原因:C++却明确指出:它们在函数调用过程中初次碰到对象的定义时被初始化。所以,如果你不对非局部静态对象直接访问,而用返回局部静态对象引用的函数调用来代替,就能
保证从函数得到的引用指向的是被初始化了的对象。这样做的另一个好处是,如果这个模拟非局部静态对象的函数从没有被调用,也就永远不会带来对象构造和销毁的开销;而对
于非局部静态对象来说就没有这样的好事。
条款48: 重视编译器警告
只要谈到警告,就要想到警告是和编译器紧密相关的,所以在编程时不要马马虎虎,寄希望于编译器为你找出每一条错误。
条款49: 熟悉标准库
条款50: 提高对C++的认识
C++的设计目标在于:
· 和C的兼容性。
· 效率。
· 和传统开发工具及环境的兼容性。
· 解决真实问题的可应用性。