四、设计与声明
条款18:让接口容易被正确使用,不易被误用
1、保证参数一致性:
如果直接传入三个 int 值的参数代表年月日,那么很有可能会因为将其中传入参数位置写反从而发生不可预料的错误。因此,我们可以通过将年月日设计成类,将类作为参数传入就能让编译器帮我们检错是否写反。
更进一步的可以在年月日类中再细分采用静态函数,进一步保证可靠性。
2、保证接口行为一致性:
内置数据类型(ints, double…)可以进行加减乘除的操作,STL 中不同容器也有相同函数(比如 size,都是返回其有多少对象),所以尽量保证用户自定义接口的行为一致性。
3、如果一个接口必须有什么操作,那么在它外面套一个新类型:
employee* createmp();//其创建的堆对象要求用户必须删除
如果用户忘记使用资源管理类,就有错误使用这个接口的可能,所以必须先下手为强,直接将 createmp() 返回一个资源管理对象,比如智能指针 share_ptr 等等:
tr1::share_ptr<employee> createmp();
如此就避免了误用的可能性。
4、有些接口可以定制删除器,就像 STL 容器可以自定义排序,比较函数一样:
tr1::share_ptr<employee> p(0, my_delete());//error! 0 不是指针 tr1::share_ptr<employee> p(static_cast<employee*>(0), my_delete());//定义一个 null 指针
第一个参数是被管理的指针,第二个是自定义删除器。
条款19:12问帮你高效设计class
对于每一个 class 都要精心设计,要考虑其构造析构函数,初始化和赋值,继承,类型转换,运算符重载,值传递等问题。
条款20:宁以 pass-by-reference-to-const 替换 pass-by-value
释义:用 const 引用传递替换值传递
值传递是新创建一个对象,将这个对象和原对象相等,如果用在类里面,当类中成员变量数目较少的时候,也许问题不大(在类里值传递先调用构造,再调用析构)。但当类中成员变量数目过大时,每一次值传递就会造成时间浪费。
引用传递是生成一个别名指向这个地址,其本身是个指针,无论原对象有多少个成员变量,都能在一瞬间找到某一个。用上 const 令其成为常量指针,即“只读”。
class Number { public: int m_a; ... int m_n;//有 n 个变量 ... }; void print1_num(Number num); void print1_num(const Number& num); Number num1; print1_num(num1);//构造,析构一个Number对象 print2_num(num1);//传地址
此外,值传递在类里还有一个问题:容易造成切割问题。
比如一个子类继续父类,传递子类对象的时候,可能只创建了一个父类的对象,子类部分缺失了:
如果不传入引用,则可能无法表现出多态性。
但是这条规则并不适用于所有类型,比如内置类型、STL 迭代器和函数对象。
举个例子,内置类型 int 在 64 位下占 4 个字节,但改成指针就占了 8 个字节。
条款21:必须返回对象时,别妄想返回其 reference
class number{ public: number(int a); const number operator+(const number& n1, const number& n2); //创建一个新对象,返回它 //const number& operator+(const number& n1, const number& n2); private: int m_value; }; number n1(1);//n1 = 1 number n2(2);//n2 = 2 number n3 = n1 + n2;
周所周知,return 返回的是一个浅拷贝副本,返回一个对象是没有问题的,但如果返回一个引用,原对象被销毁之后,引用的指向也被销毁了,也就是引用指空,出错。
我们当然可以用 new 解决这个问题,但是当变量数目多的时候,程序员往往不知道怎么使用 delete:
n3 = n1 + n2 + n4 + ...
或许有人想到创建一个 static 对象,但这也是有问题的,我们每次调用都是对同一个 static 操作:
既然调用的都是同一个 result,那么 if 判断语句就一定是对的,这就出现了逻辑上的错误。
正确的方法就是一开始我们看到的,直接返回一个值即可。
但并不是所有场景都不能返回引用,比如在 vector 中用索引操作时就可以返回引用。
因为此时我们定义的 v 一直存在,需要通过返回引用对 v 中的值进行修改。
总结:虽然返回一个对象需要构造,析构等操作而产生一些代价,但是如果我们不想改变已有的值,就最好不要返回一个引用,而是支付这些代价。(这在时间上会多一点,但创建的对象会随运算符的结束而被销毁。这比“未定义行为(返回一个新建对象的引用)”,“资源泄漏”,“结果出错”要好得多了。)
条款22:将成员变量声明为 private
声明类型:
- public:所有都能访问
- protected:类对象不可以访问
- private:只有类成员函数可以访问
private 优点:
1、使成员变量的控制更为精准:
class person { public: void setage(int id); void getage(); void setname(string name); void getid(); private: int m_age; string m_name; int m_id; };
用户通过某一函数控制一个私有变量,防止被误用。
2、使类更有封装性:
我们可以通过将方法封装到类中,以 public 的方式暴露给外部使用。比如将计算平均值的函数封装到类中,这样外部就不用在每个需要用到平均值的地方都进行一次计算操作,这样当有变动时修改起来很麻烦,每添加一个变量我都要去所有地方改。封装成函数后直接向外提供一个接口,每次要用到平均值就调用这个函数计算一次即可。
protected 不比 private 有封装性,因为 protected 子类也可以实现上述代码的操作。
条款23:宁以 non-member, non-friend 替换 member 函数
释义:如果一个成员函数调用了其他的成员函数,那么就要用一个非成员函数替换这个成员函数。
根据条款 22,对类变量的操作只能通过类成员函数实现(因为它是私有变量),那么如果一个成员函数内部实现是调用其他的成员函数,则一个非成员函数也可以做到这样的效果:
class preson { public: void func1(); void func2(); void func3() { func1(); func2(); } }; void use_all(const person& p) { p.func1(); p.func2(); }
func3() 和 use_all() 的效果是一样的,但这时候我们倾向于选择 use_all 函数,因为 func3() 作为一个成员函数,其本身也是个可以访问私有变量的函数。use_all() 函数其本身不可以访问私有变量。所以 use_all() 比 func3() 更有封装性。(能够访问私有变量的函数越少越好)
在了解这点之后,我们做一些更深层次的探讨:
我们称 use_all()(func3() 的非成员函数版本)为便利函数。假设一个类有多个诸如 func1() 的函数,根据排列组合,也就有很多便利函数。为了让这些便利函数和它的类看上去更像一个整体,我们把便利函数和类放在一个 namespace 中。于是,我们可以更为轻松地拓展这些便利函数 —— 多做一些排列组合。
再举个例子:
总结:若一个成员函数调用其他成员函数,那么这个成员函数的非成员函数版本比之拥有更多的封装性,和机能扩充性。
条款24:若所有参数皆需类型转换,请为此采用 non-member 函数
举例:有理数类和整数的运算
class Rational { public: Rational(int numerator = 0, int denominator = 1)//分子与分母 ... const Rational operator*(const Rational& right_number) const; ... }; Rational oneEighth(1, 8); Rational onehalf(1, 2); Rational result1 = onehalf * oneEighth; Rational result2 = onehalf * 2; Rational result3 = 2 * onehalf;//error!
onehalf*2 相当于 onehalf.operator*(2)
首先创建了一个临时对象 const Rational temp(2);
再让两个 Rational 对象运算。
2*onehalf 是 2 调用了 operator*。因为 2 是需要被转换的参数,而 2 的位置和 this(调用 operator *) 对象的位置是一样的,所以无法将 2 转换成 Rational 类型,也就无法调用 operator* 函数。
解决办法:使用 non-member 函数,让左右参数的地位平等:
const Rational operator*(const Rational& left_number, const Rational& right_number) {...}
总结:如果所有参数(运算符左边或者右边的参数)都需要类型转换,用 non-member 函数。
条款25:考虑写一个不抛异常的 swap 函数
周所周知,swap 可以交换两个数的值,标准库的 swap 函数是通过拷贝完成这种运算的。想想,如果是交换两个类对象的值,如果类中变量的个数很少,那么 swap 是有一定效率的,但如果变量个数很多呢?
你一定联想到了之前提过的,引用传递替换值传递。没错,交换两个类对象的地址就可以很有效率地完成大量变量的 swap 操作。不幸的是,标准库的 swap 并无交换对象地址的行为,所以我们需要自己写 swap 函数。
class person{...}; void my_swap(person& p1, person& p2) { swap(p1.ptr, p2.ptr); }
这个函数无法通过编译,因为类变量是 private,无法通过对象访问。所以要把它变成成员函数。
class person { public: void my_swap(person& p) { swap(this->ptr, p.ptr); } ... };
如果你觉得 p1.my_swap(p2) 的调用形式太 low 了,你可以设计一个 non-member 函数(如果是在同一个命名空间那就再好不过了),实现 swap(p1, p2),这里不做演示。你还可以特化 std 里的 swap 函数:
namespace std { template<> void swap<person> (person& p1, person& p2) { p1.my_swap(p2); } }
值得注意的是,如果你设计的是类模板,而尝试对 swap 特化,那么会在 std 里发生重载,这是不允许的,因为用户可以特化 std 的模板,但不可以添加新的东西到 std 里。
还有一点:在上面工作全部完成后,如果想使用 swap ,请确定包含一个 using 声明式,一边让 std::swap 可见,然后直接使用 swap。
template<class T> void do_something(T& a, T& b) { using std::swap; ... swap(a, b); ... }
其中过程:
如果 T 在其命名空间有专属的 swap 则调用,否则调用 std 的 swap。
如果在 std 有特化的 swap 则调用,否则调用一般的 swap。(也即是拷贝)
再举个例子:
也可以利用 swap 函数去除重复代码。
总结:
1、当 std::swap 效率不高时,考虑自定义一个成员函数 swap
2、为成员函数提供非成员函数版本
3、类模板不要特化 swap,类特化 swap
4、使用 swap 前要写 std::swap,以便在更多的语境下使用