《Effective C++:改善程序与设计的55个具体做法》阅读笔记 4——设计与声明

让接口容易被正确使用,不容易被误用.

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

class Date {
pub1ic:
    Date (int month, int day, int year) ;
    ...
};

用户可能犯的错:

  • 错误的次序传递参数:用户可能以年月日次序输入参数,而不是上面的月日年的次序。
    解决方法:
    年月日都分别用一个类或结构体表示。如果传递参数的次序不对的话,就会导致类型不对应,就会报错。

  • 无效的月份或天数
    解决方法:
    (1)使用enum表示月份,因为月份只有12个。但是enums可被拿来当一个int使用,而年和日也可以使用int,这就可能和“年月日都分别用一个类或结构体表示”冲突了。代码在本小节的最后面。
    (2)使用类构建特殊的枚举类型,如下:

#include<iostream>

class Month {
public:
    static Month Jan() { return Month(1); }
    // ...
    static Month Feb() { return Month(2); }
    static Month Dec() { return Month(12); }
private:
    explicit Month(int m){};
};

int main(){
    Month a = Month::Jan();

    return 0;
}

【为什么Month类中可以创建Month对象?这里没有static可以吗?】
答:static只是为了不建立对象就可以访问函数。Month类中可以创建Month对象

其他防止借口被误用的方法:

  • 设置特定的类型,如当operator*是返回的类型是const类型,那么a*b就不能被赋值,也就是a*b=c会报错。

  • 下面是另一个一般性准则“让types容易被正确使用,不容易被误用”的表现形式: “除非有好理由,否则应该尽量令你的types的行为与内置types一致”。客户已经知道像int这样的type有些什么行为,所以你应该努力让你的types在合样合理的前提下也有相同表现。例如,如果a和b都是int,那么对a*b赋值并不合法,所以除非你有好的理由与此行为分道扬镳,否则应该让你的types也有相同的表现。是的,一旦怀疑,就请拿int做范本。

  • std::tr1::shared_ptr中的删除器

std::tr1::shared_ptr<Investment> pInv(static_cast<Investment*>(0) ,getRidOfInvestment); // 创建一个空指针,删除器为getRidOfInvestment
...
pInv = ...;         // pInv指向正确对象

如果直接知道pInv指向哪,那就不用先置空,直接赋值就可以了。

  • tr1::shared_ptr有一个特别好的性质是:它会自动使用它的“每个指针专属的删除器”,从而避免以下问题:对象在动态连接程序库(DLL) 中被new创建,却在另一个DLL内被delete销毁。在许多平台上,这一类“跨DLL之new/delete成对运用”会导致运行期错误。

当然,使用智能指针也是需要代价的,它比原始指针大(Boost库中实现的shared_ptr体积是原始指针的2倍)且慢,而且使用辅助动态内存。任何事物都具有两面性,权衡一下就会发现,智能指针能避免的资源泄露问题(好的接口),相较于它的空间和时间代价而言,都是值得的。

使用enum表示月份(参考链接):

#include <cassert>

class Month
{
private:
    int m_month;
public:
    explicit Month(int month): m_month(month){assert(m_month >= 1 && m_month <= 12);}
    enum
    {
        Jan = 1,
        Feb,
        Mar,
        Apr,
        May,
        Jun,
        July,
        Aus,
        Sep,
        Oct,
        Nov,
        Dec
    };
    int GetMonth() const
    {
        return m_month;
    };
};

int main()
{
    Month a = Month(Month::May);
}

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

直接看书和书中我的标注就可以了。

书还没看呢。。。

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

本节讲的是实参的传递方式。

  • pass-by-value可能导致调用大量的拷贝构造函数和析构函数,所以建议采用pass-by-reference-to const。
    形参设置为const类型的引用const Student &s,其中const是为了保证函数中不对s进行修改,只对s的复件进行修改。
    【只对s的复件进行修改,还是需要生成复件,还是需要调用拷贝构造函数啊?那和直接传值有什么区别?】
    答:我觉得s需要修改时,可以直接传值。但是如果我们将形参同一都设置成“const类型的引用”,这样形式上比较统一,在编写形参的时候,也不需要考虑采用pass-by-reference-to const,还是采用pass-by-value,只有当s需要修改的时候,才在函数中建立复件。

  • pass-by-reference-to const可以避免slicing (对象切割)问题。
    slicing (对象切割)问题:子类对象以值传递给父类的形参,此时子类对象的特性会被忽略掉。

  • “形参设置为const类型的引用”并不适用于内置类型(string不是内置类型),以及STL的迭代器和函数对象。对它们而言,pass-by-value往往比较适当。

  • 小型的类可以通过pass-by-value,这是错误的。因为小型类中可能要有指针指向一个大的空间,调用copy构造函数,还是需要比较大的代价。而且你不能保证小型类未来不会被扩充成大型类。

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

函数返回引用的问题:

  • 要返回引用(别名)的前提指向对象必须存在,就是说要先创建一个对象,然后使用引用指向这个对象。

  • 返回stack空间创建对象的引用:此对象在退出函数时,就会被销毁,所以不能返回此对象的引用或指针。

  • 返回heap空间创建对象(new等创建的对象)的引用:很可能忘记delete,而且当函数只是作为一条语句的一部分时是无法获取函数返回值所在空间的地址,如文中提到的w=x*y*z就无法获取x*y返回值所在空间的地址,除非将w=x*y*z拆成两个语句。无法获取函数返回值所在空间的地址,就无法对其进行释放。
    【为什么不使用shared_ptr对heap空间创建对象进行管理,那不就不用关心delete问题??答:这样当然不行,因为在退出函数时,shared_ptr对象就会被析构掉】

  • 返回静态对象(new等创建的对象)的引用,由于类中成员函数中定义的静态变量也是被所有对象所共享的,所以这种方法也不行。Item 4中返回静态对象的引用,是为了确保对象的初始化而使用“函数代替对象”。

结论:不要返回指向函数内部对象的引用或指针,直接通过pass-by-value的方式返回新的对象。因为编译器可以对代码进行自动的优化,pass-by-value的方式不一定额外调用多余的构造和析构函数。

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

将成员变量声明为private,而不是public或者protect,这样做的好处:

  • 接口统一性:对对象的所有访问都是通过函数进行,就不需要考虑要不要使用()
  • 可以对成员变量的访问进行限制,如只读、只写还是可读可写
  • 封装。在成员函数中可能会对成员变量进行计算,根据不同的环境可能有不同的计算方法。进行封装以后,即使成员函数内部发生变化,客户不会知道,客户还是使用以前的接口就可以了。
  • 为什么protected不行?
    答:如果不存在继承关系,protected的作用与private等同,除了本类之外,其他类或者类外都不能访问,但如果存在继承关系时,protected标识的成员变量,在它的子类中仍可以直接访问,所以封装性就会受到冲击。这时如果更改父类的功能,相应子类涉及到的代码也要修改,这就麻烦了。而如果使用private标识,则根据规则,所有private标识的变量或者函数根本不被继承的,这样就可以保护父类的封装性了,仍可以在子类中通过父类的成员函数进行访问。

条款23 使用non-member、non-friend函数替换member函数

  • 封装的越多,那么我们就有越大的自由度对封装内部进行改变,因为改变的是封装的内部,对外的接口始终都是不变的。

  • 如果可以使用non-member、non-friend函数替换member函数,那么就这么干,因为non-member、non-friend函数无法对私有成员进行访问,所以封装性会比member函数好。
    这里所说的non-member、non-friend函数可以是另一个类的成员函数。可以将non-member、non-friend函数和被使用的类都放在一个namespace下保证函数的封装性。

  • 多个头文件中的内容,属于同一个namespace。可以将不同类型的功能放在不同的头文件中,但是这些功能可以同属于一个namespace。比如std就包含了<vector>, <algorithm>, <memory>等头文件。这样我们就需要什么功能就包含什么头文件,不需要将整个namespace都包含进来。

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

  • 成员函数隐含this作为实参,如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行自动类型转换,那么这个函数必须是个non-member。
  • 错误的观点:如果一个函数与某class相关,那么此函数不是成为该类的成员函数,就成为该类的友元函数。比如条款23和条款24就说明了此观点是错误的。

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

看不懂本条款,所以大量参考了链接,链接的内容总结如下:

  • Sample中只有一个Element类型的指针作为成员变量。Sample有指针时,大多数情况下都需要自定义析构函数、拷贝构造函数和赋值运算符

  • 条款十一中提到了使用swap实现operator=

  • Sample里面存放的是指向Element的指针,所以要实现swap直接交换指针地址就可以了。指针实际是int类型,所以直接使用std::swap就可以。

  • Sample中实现了成员函数swap(SampleObj1)。
    类外全特化了std::swap,实现了swap(SampleObj1, SampleObj2)。swap(SampleObj1, SampleObj2)实际调用了Sample中的成员函数swap(SampleObj1),如果不直接调用swap(SampleObj1),那么还需要将swap(SampleObj1, SampleObj2)声明为类Sample的友元函数,才能访问Sample中的private成员变量。(std namespace是不能随便添加东西的,只允许添加类似于swap这样的全特化版本)

  • 假设Sample现在是一个模板类,Element也是模板类。但在模板下特化std的swap是不合法的(这叫做偏特化,编译器不允许在std里面偏特化),只能将之定义在自定义的namespace中。

总结:当是普通类时,可以将swap的特化版本放在std的namespace中,swap指定函数时会优先调用这个特化版本;当是模板类时,只能将swap的偏特化版本放在自定义的namespace中。

好了,最后一段终于说到了不抛异常的问题,书上提到的是不要在成员函数的那个swap里抛出异常,因为成员函数的swap往往都是简单私有成员(包括指针)的置换,比如交换两个int值之类,都是交换基本类型的,不需要抛出异常,把抛出异常的任务交给non-member的swap吧。【异常是你说交给谁就交给谁的吗,异常不是随时随地可能发生的吗??,什么叫不抛出异常,直接使用使用try catch处理一下,就算不抛出吗???有可能指的是不主动使用throw抛出异常】

还是不太明白链接中的下面这句话:
“当是模板类时,只能将swap的偏特化版本放在自定义的namespace中。好了,问题来了,这时候用swap(SampleObj1, SampleObj2)时,调用的是std版本的swap,还是自定义namespace的swap?

事实上,编译器还是会优先考虑用户定义的特化版本,只有当这个版本不符合调用类型时,才会去调用std的swap”

posted @ 2022-10-23 20:45  好人~  阅读(20)  评论(0编辑  收藏  举报