Effective C++ 读书笔记(四)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_40028201/article/details/89673273

Effective C++ 读书笔记(四)

4、设计与声明

条款18:让接口容易被正确使用,不容易被误用

  1. 好的接口容易被正确使用,不容易被误用。
  2. ”促进正确使用“的办法包括接口一致性,以及于内置类型兼容。
  3. ”阻止误用“方法包括建立新类型、限制类型上的操作、束缚对象值,以及消除客户的资源管理责任。
  4. shared_ptr支持特定的删除器。可以防范cross-DLL problem。
    • shared_ptr一个特别好的性质是:它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在客户的错误:Corss-DLL Problem。这个问题发生于:对象在一个动态链接库DLL中被new创建,却在另一个DLL内被delete销毁。在许多平台上,这一类跨DLL之new/delete成对使用会导致运行期错误。shared_ptr没有这个问题,因为它的删除器来自其所诞生的那个DLL的delete。

条款19 :设计class犹如设计type

​ 要注意解决以下问题:

  1. 新type的对象应该如何被创建和销毁?
  2. 对象初始化和对象赋值该有什么样的区别? 条款4
  3. 新type的对象如果被pass by value,意味着什么
  4. 什么是新type的合法值?
  5. 新type需要配合某个继承图系(inheritance graph)吗? (条款34和条款36)
  6. 新type需要什么样的转换?
  7. 什么样的操作符和函数对此新type而言是合理的?
  8. 什么样的函数应该被驳回?
  9. 谁该取用新type的成员?
  10. 什么是新type的“未声明接口”(undeclared interface)?
  11. 你的新type有多么一般化?
  12. 你真的需要一个新type吗?

条款20: 宁以pass-by-reference-to-const替换pass-by-value

  1. 在默认情况下,C++函数传递参数是继承C的方式,是值传递(pass by value)。这样传递的都是实际实参的副本,这个副本是通过调用复制构造函数来创建的。有时候创建副本代价非常昂贵
  2. 以pass by reference-to-const方式传递,可以回避所有构造函数和析构函数。这种方式传递,没有新对象创建,所以自然没有构造和析构函数的调用参数中,以const修饰是比较重要的,原先的pass by value,原先的值自然不会被修改。现在以pass by reference方式传递,函数validateStudent内使用的对象和传进来的同同一个对象,为了防止在函数内修改,加上const限制。
  3. 以pass by reference方式传递,还可以避免对象切割(slicing)问题。一个派生类(derived class)对象以pass by value方式传递,当被视为一个基类对象(base class)时,基类对象的copy构造函数会被调用,此时派生类部分全部被切割掉了,仅仅留下一个base class部分。
  4. 对于内置类型,pass by value往往比pass by reference更高效。所以在使用STL函数和迭代器时,习惯上都被设计出pass by value
  5. 对象小并不意味着copy构造函数代价小,许多对象(包括STL容器),内涵的成员只不过是一两个指针,但是复制这种对象时,要复制指针指向的每一样东西,这个代价很可能十分昂贵。
  6. 一般情况下,可以假设内置类型和STL迭代器和函数对象以pass by value是代价不昂贵。其他时候最好以pass by reference to const替换掉pass by value。

条款21: 必须返回对象时,别妄想返回其reference

  1. 如下这种会出现错误,因为引用只是对象的别名,返回的是局部Rational对象的别名,但是离开函数后该对象就被析构了,返回的是一个无用值,所以要返回一个值
inline const Rational& operator*(const Rational& lhs, const Rational& rhs)
{
	return Rational(lhs.n* rhs.n, lhs.d* rhs.d);//对象析构了,引用别名也是空对象
}

inline const Rational operator*(const Rational& lhs, const Rational& rhs)
{
	return Rational(lhs.n* rhs.n, lhs.d* rhs.d);//返回一个rational对象的拷贝
}
  1. 在返回一个reference和返回一个object之间抉择时,挑出行为正确的那个。让编译器厂商为你尽可能降低成本吧!

条款22:将成员变量声明为private

  1. 封装。如果通过函数访问成员变量,日后可以用某个计算替换这个变量,这时class的客户却不知道内部实现已经变化。
  2. 将成员变量声明为private。这可以赋予客户访问数据的一致性、可细微划分访问控制、允许约束条件获得保证,并提供class作者以充分弹性实现。
  3. protected并不比public更具有封装性。

条款23:宁以non-member、non-friend替换member函数

1. 用non-member、non-friend函数替换member函数,这样可以增加封装性、包裹弹性和机能扩充性,因为不能访问私有变量。
2. namespace可以跨越多个源码文件,class不能,将所有的便利函数放在多个头文件但隶属于同一个命名空间,意味着客户可以轻松扩展这一组遍历函数。他们需要做的是添加更多的非成员函数和非友元函数到这个命名空间内

条款24:若所有参数皆需要类型转换,请为此采用non-member函数

  1. 通常情况下,class不应该支持隐式类型转换

  2. 也有类外,比如建立一个分数管理器,允许隐式类型转换

    class Rational{
    public:
    	Rational(int numerator=0, int denominator=1);//非explicit,允许隐式转换
    	……
    };
    

    当然,若作为成员函数,this指针为隐形的参数,只需要一个变量参数传进去

    
    class Rational{
    public:
    	……
    	const Rational operator*(const Rational& rhs);
    	……
    };
    
    

    进行混合运算时

    result=oneHalf*2;//正确,相当于oneHalf.operator*(2);
    result=2*oneHalf;//错误,相当于2.operator*(oneHalf);
    

    这是错误的,2是this指向的对象,必须是该类本身的类型。这是因为

    1. 只有参数列于参数表,才是隐式类型的参与者
    2. 2不是该类型,不能调用成员函数operator *;

    因此可以定义为一个非成员函数,可以进行隐式转换的

    const Rational operator*(const Rational& lhs, const Rational& rhs);
    
  3. 总结:如果需要为某个函数的所有参数(包括this指针所指向的隐喻参数)进行类型转换,这个函数必须是个non-member函数

条款25:考虑写出一个不抛出异常的swap函数

  1. 传统的做法,调用拷贝构造函数与赋值运算符多次复制,对于一些数据类型大的元素,效率低下

    namespace std{
        template<typename T>
        void swap(T& a, T& b)
        {
            T temp(a);
            a=b;
            b=temp;
        }
    }
    
  2. 以“一个指针指向一个对象,内含真正的数据”,只需交换指针(pimpl手法)

    //针对widget设计的一个类
    class WidgetImpl{
    public:
        ……
    private:
        int a,b,c;              //数据很多,复制意味时间很长
        std::vector<double> b;
        ……
    };
    class Widget{
    public:
        Widget(const Widget& rhs);
        Widget& operator=(const Widget& rhs
        {
            ……          //复制Widget时,复制WidgetImpl对象              
            *pImpl=*(ths.pImpl);
            ……
        }
        ……
    private:
        WidgetImpl* pImpl;//指针,含有Widget的数据
    };
    
  3. STL的swap函数,成为异常安全编程(29)以及用来处理自我赋值的顶梁柱(见11)

    calss Widget{
    public:
        ……
        void swap(Widget& other)
        {
            using std::swap;//这个声明有必要
            swap(pImpl, other.pImpl);
        }
        ……
    }namespace std{
        template<> //修订后的swap 特化版本
        void swap<Widget>(Widget& a, Widget& b)
        {
            a.swap(b);  //调用其成员函数
        }
    }
    

    注意:

    一般而言,重载function template没有任何问题,但std是个特殊的命名空间,其管理规则也比较特殊。客户可以全特化std内的templates,但是不可以添加新的classes或functions到std里面。std的内容有c++标准委员会决定,标准委员会禁止我们膨胀那些已经 声明好的东西

  4. 总结

    1. 如果std::swap不高效时,提供一个swap成员函数,并且确定这个函数不抛出异常。
    2. 如果提供一个member-swap,也应该提供一个non-member swap来调用前者。对于class(非class template),要特化std::swap。
    3. 调用swap时,针对std::swap使用using形式,然后调用swap并且不带任何命名空间资格修饰。
    4. 为“用户定义类型”进行std template全特化时,不要试图在std内加入某些对std而言是全新的东西

猜你喜欢

转载自blog.csdn.net/qq_40028201/article/details/89673273