Effective C++学习笔记(四)

条款18

设计出“容易使用,不容易被误用”的接口

例如设计一个Date类

class Date {
public:
    // 因为三个成员的类型相同,就很容易被误用,不是所有人都会按照 月 日 年 来传入参数
    // 并且 年 月 日 都有明显的界限,若超过这个界限就是不合适的,我们设计接口时应该 使用户不会出现这种情况,不能寄希望于用户自己
    Date(int _month, int _day, int _year) : month(_month), day(_day), year(_year) {}	
private:
    int day;
    int month;
    int year;
};

因此此时我们需要对导入的新类型的错误进行预防。
所以我们要对这些数据进行封装

struct Day {
    explicit Day(int _val) :val(_val){}
    int val;
};
struct Month {
    explicit Month(int _val) :val(_val){}
    int val;
};
struct Year {
    explicit Year(int _val) :val(_val){}
    int val;
};

class Date {

}

条款19

设计新的对象的需要考虑的点:

  • 是否真的需要一个新对象?
  • 新对象应该如何被创建和销毁,在涉及到 new和delete, new[]和delete[]时尤其如此
  • 对象的初始化和赋值应该有什么差别?
  • 新对象若被值传递,意味着什么?拷贝构造函数用来定义一个类的值传递该如何实现
  • 什么是新对象的“合法值”?通常有些数值集是无效的,如上面的日期类,此时类必须维护一些约束条件,此时成员函数(特别是构造函数、赋值操作和所谓的“setter”函数)必须进行错误检查工作。
  • 新对象需要配合某个继承图谱吗?
  • 新的对象需要转换行为吗?
  • 新的对象需要什么合法的操作符和函数
  • 成员变量的访问权限
  • 新对象的“未声明接口”?对效率、异常安全性和资源运用(多任务锁定和动态内存)提供何种保证
  • 新类型有多么一般化,到底是设计为 class还是设计为 class template

条款20

最好以 past-by-reference-to-const替换 pass-by-value,用 const引用传递替换 值传递

  • 引用传递可以实现多态
  • 可以减少对象的构造次数和成员变量构造函数的执行次数,效率更好
  • 内置类型一般使用值传递效果更高

条款21

const引用使用比较方便,但是并不是所有的情况都要使用 const引用, 当必须返回对象时,别妄想返回其引用

不要使用指针或引用指向一个局部变量,或者引用指向一个堆上定义的对象,或返回指针或引用指向一个静态局部变量,并且可能同时需要多个这样的对象。

总之,必须返回返回对象时,别偷懒。

class Rational {
    // 此处一般来说必须返回对象本身,而不能返回引用
    const Rational operator*(const Rational& lhs, const Rational& rhs) {
        return Raional(lhs.denominator * rhs.denominator, lhs.numerator *rhs.numerator);
    }
private:
    int denominator;	// 分母
    int numerator;		// 分子
};

条款22

成员变量设计为private

  • 可以设计相应的 gettersetter函数来实现对所需变量的控制
  • 将成员变量隐藏在函数接口的背后,可以为“所有可能的实现”提供弹性。例如可以使成员变量被读或者被写时轻松通知其他对象。可以验证 class的约束条件以及函数的前提和事后状态。可以在多线程环境中执行同步控制。
  • 可以实现封装。封装很重要。若你对客户隐藏成员变量,你可以确保 class的约束条件总会得到维护,只有成员函数能够影响它们。
  • publicprotected成员变量,若我们取消了它。所有使用它的客户代码都会被破坏,这是一个未知的量,。因此 public完全不具有封装性。
  • 从封装的角度看,只有两种访问权限:private提供封装和 其他不提供封装

条款23

/*

​ TODO

*/

条款24

若所有参数皆需类型转换,采用 non-member函数

  • 采用成员变量时,this指向的对象不能进行类型转换
  • 类型转换只能发生在参数列表中的对象
class Rational {
    const Rational operator*(const Rational &rhs) const;	// 针对r * 2可以,但2*r错误,因为2是this,不能类型转化为Rational
};
const Rational operatro*(const Rational &lhs, const Rational &rhs);	// 两者都能进行类型转换

条款25

写一个不抛异常的 swap 函数

在赋值运算符中解决自我赋值问题的一个 常见机制

高效的swaps几乎总是基于对内置类型的操作(例如pimpl手法的底层指针),而内置类型上的操作绝对不会抛出异常,一次要实现不抛异常的swaps时需要全部地基于对内置类型的操作。

  • std::swap效率不高时,需要提供一个swap函数,并确定该函数不抛出异常。

    一般这种情况发生在自建对象变量中有指针的情况。此时我们不应该交换指针指向的所有对象内容,而是应该交换指针的指向就好了,这样效率更高,而且指针是内置类型,不会抛出异常

  • 提供的swap函数有几种形式

    • member swap,此时还需要提供一个non-member swap来调用前者
    • non-member swap
    • 对于class时,可以提供 std::swap的特化版本
    • 对于 template时,不存在 std::swap的特化版本,只能自己实现一个全特化版本,但不能放入std
// std 中的swap函数
// 在针对成员变量有指针的对象时,由于会调用复制运算符和拷贝构造函数,会发生大量数据的拷贝,效率很低
namespace std {
    tempalte<typename T>
    void swap(T &a, T &b) {
    	T temp(a);
        a = b;
        b = temp;
    }
}

class Widget {
public:   
    // 成员变量的swap
    void swap(Widget &w) {
        using std:swap();
        swap(propeties, w.propeties);
        swap(name, w.name);	//	此时只交换指向,而并不是复制每一个元素
    }
    
    private:
    int propeties;
    char *name;
};



// 此时我们希望能够告诉 std::swap,当 Widget被置换时真正应该交换的是其内部的pImpl指针
// 此时可以使用偏特化来实现
// 编译错误
namespace std{
    template<>
    void swap<Widget> (Widget &a, Widget &b) {
    	swap(a.propeties, b.properities);	// 成员变量是私有变量,我们不能这样使用它们,所以有问题
    	swap(a.name, b.name);
    }
}

// 此时我们可以声明一个名为swap的public成员函数来做真正的置换工作,然后将std::swap特化
// 这种情况可以通过编译,而且与STL具有一致性,STL容器都是这样提供的特化版本
namespace std{
    template<>
    void swap<Widget> (Widget &a, Widget &b) {
    	a.swap(b);	// 使用成员函数中的swap来真正实现
    }
}

// 若Widget是模板类,而不是单个对象
template<typename T>
class Widget {  };

// 此时想要将其swap函数放入std中,会出错
// C++只允许对类模板偏特化,而不能在函数模板上实现偏特化
namespace std {
    template<typename T> 
    void swap(T &a, T &b) {	// 错误的,不能通过编译
        a.swap(b);
    }
}

// 此时对于类模板的交换,我们需要声明一个non-member swap来调用member swap,但此时不应该将其声明为std::swap的特化版本或重载版本
template<typename T> 
void swap(T &a, T &b) {	// 错误的,不能通过编译
    a.swap(b);
}

在定义了这么多的swap后,在使用时应该调用哪个呢?

// 使用using::swap之后,编译器会先寻找当前global作用域或T所在的明明空间中的swap,不存在的化则会对std中的偏特化进行查找,不存在的话才会使用非一般化的template版本
template<typename T>
void doSomething(T &obj1, T &obj2) {
    using std::swap;
    swap(obj1, obj2);		// 将为T调用最佳的swap版本
}
// 注意std::swap(obj1, obj2)与上述不同,它是只会从std中寻找合适的函数,包括新加入的偏特化

总之:

  • 实现的swap函数不能抛出异常,应该全部实现的是内置类型的交换,效率更高,错误更少
  • 要在类的外部实现swap函数,实现与所有的STL容器具有一致性,可以实现同一函数对多个不同的类进行操作
  • 若不是模板类,最好将swap写入std中,实现偏特化
  • 若是模板类的话,就在当前的作用域中实现一个版本
  • 偏特化只能实现类的偏特化,函数不存在偏特化
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值