《Effective C++》学习记录

关于const的一些更好的做法

class 中的 const

需要注意 class member 的 声明定义

class Test {
    private:
    // enum {Num = 5};  /*enum hack*/
    static const int NUM; // 声明
    int arr[NUM];  // 使用该常量
};
const int Test::NUM = 1;  // 定义
  • 如果不需要取地址,在不严格的编译器中,可以不提供定义,使用 static const int Num = 1; 的声明写法。
  • 若编译器要求严格,或是需要使用常量地址时,可以改用 the enum hack 的补偿做法,将类内的枚举类型的数值作为 int 使用。

区别于宏

定义一种形似函数的宏,其优势在于可以避免函数调用带来的额外开销,但C++中提供了 inline 作为一种更好的解决方案。

另外,由于宏本质上是文本替换,所使用的符号并没有进入 记号表,在程序报错时将会用以原来的文本内容进行提醒。如果你的库被别人引用,用户将难以追踪错误的起源。

利用const禁止用户对返回值做修改

使用函数和操作符可能会返回值,如果不对返回结果加以 const 限定,一旦用户修改返回值,将会造成错误。

// 用户的不合法尝试
MyTestType a, b, c;
a * b = c;

// 比较运算符误用为赋值运算符,造成逻辑错误
if (a * b = c) {/*...*/}

遵循逻辑常量性

我们一般会为操作设置 const版本 和 非const版本,但两者可能在行为上有很大重复上,所以我们希望通过一方调用另一方来使得代码重复度降低。

  • const 版本:内部不应当发生任何可能修改内部成员的行为,所以也不应当调用 非const函数。
  • 非 const 版本:逻辑上允许对内部成员进行修改,所以可以调用 const函数。
class MyTestType {
    private:
        enum {NUM = 4}
        int arr[NUM];
    public:
    const int& operator[](std::size_t pos) const {
        /*...*/
        return arr[pos];
    }
    int& operator[] (std::size_t pos) {
        /*...*/
        return const_cast<int&>( /*解除返回值的const限制*/
            static_cast<const MyTestType&>(*this)  /*为*this加上const*/
            [pos]);  /*调用const版本的[]*/
    }
};

避免使用未初始化的对象

built-in type和class在初始化上的不同

内置类型并不保证自动进行初始化,所以需要用户手动初始化。而自定义类型需要经过构造,所以用户能够得到初始化后的对象。
class 在构造之时,会调用 default constructor,利用初始化列表中的参数进行拷贝构造。如果是在构造函数体的内部做赋值修改的话,虽然也能够得到预期的效果,但由于赋值操作发生于构造之后,所以效率实际上是降低了的。
一个 class 内,其成员将会按照声明的顺序来进行初始化。如果有继承关系,那么 base classes 的初始化将早于 derived classes 进行。

多编译单元的初始化问题

当涉及多编译单元时,需要注意 static 对象的 初始化次序static 对象有 local staticnon-local static 之别。local static 是指存在于函数内部的 static 对象,其余的 static 对象则称为 non-local static(包括 global 对象,以及在namespace作用域内、 class 内、 file 作用域内的静态对象。

由于 non-local static 对象的相对次序无法明确确定,所以,要解决这个问题只能将每个 non-local static 搬到自己的 专属函数 内,令其返回一个指向该对象的引用。(这是 Singleton 模式的常用实现方法)
为了更好地处理 多线程环境 下的初始化次序,可以在程序的单线程启动阶段 手工调用 所有的 reference-returning 函数,也就是在程序创建之初完成所有静态变量的初始化。


资源管理的注意要点

以对象管理资源

C++ 程序会在许多地方用到动态内存分配,如文件描述器、互斥锁、图形界面中的字型和笔刷、数据库连接、网络 sockets 等。完成这些功能后需要手动释放内存。为了更安全地对动态内存进行管理,可以通过对象来管理资源,在对象创建之初取得资源,在对象生命周期结束时自行析构释放资源。

当资源被获取时,应当立即交给管理对象进行管理。

判别资源是否能够共享

有两种常用的智能指针,一种是 auto_ptr 用于管理所有权唯一的资源,另一种是 shared_ptr ,用于管理可共享的资源。

auto_ptr 被销毁之时会释放其管理的资源,所以不应当让两个 auto_ptr 指向同一对象。为此,auto_ptr拷贝构造拷贝赋值 保证了拷贝过程中资源所有权会从 源对象 转交给 目的对象 ,原来的 auto_ptr 将会被置为 NULL

shared_ptr 通过内置引用计数器来追踪所管理的资源是否正在被使用(但无法解决环状的互相引用的问题)。

这两种智能指针在析构之时都是调用 delete 而非 delete [] ,所以都不能管理动态分配所得的数组。在 boost 中则提供了 boost::scoped_arrayboost::shared_array 来管理动态分配所取得的数组资源。

对于互斥锁而言,在对象析构之时要做的不是 ”删除“ ,而是 解除锁定 。在 shared_ptr 中则可以在初始化时指定 删除器 定义其析构时的行为。而 auto_ptr 无法如此操作,它总会在析构时释放掉所管理的资源。

限制拷贝行为

许多时候,对在构造时取得的资源进行拷贝,是不太合法的,所以我们可以通过声明 privatecopy constructorcopy assignment,这样一旦在外部尝试进行拷贝操作,编译器就会报错。

private 权限无法限制 成员函数友元函数 ,所以我们可以使用更安全的做法,设计一个通用的不可拷贝的类。该类声明 privatecopy constructorcopy assignment ,然后让需要限制拷贝行为的类继承于这样一个限制了拷贝行为的通用类。由于派生类构造需要借助基类的拷贝构造来实现,但基类 private 权限的拷贝构造和拷贝赋值不可被派生类使用,故而可限制其拷贝行为。

提供对原始资源的访问方法

有些时候,我们需要让资源管理对象提供原始资源的指针,可以考虑 显示转换隐式转换 两种方式。

显示转换可以通过重载 *-> 等操作符来使其资源管理对象表现得和原始资源指针一样。除此之外,也可以让资源管理对象提供 get() 函数返回原始资源指针。

隐式转换,即为该类提供到原始资源的 隐式转换函数 ,此做法虽然更易用,但可能会影响原始资源的拷贝构造。

选择显示转换还是隐式转换,取决于在具体情境下使用哪种方式更不容易发生 接口的误用

// FontHandle是原始资源类型,Font是管理资源对象
operator FontHandle() const { return f;}

使用new 使用及对其所取得资源的管理

typedef 中,允许为数组设置别名,但如此操作就要求客户必须用 delete[] 来析构,而不能使用 delete

将所申请的资源交由资源管理对象进行管理的过程中,不建议将 资源申请交付智能指针对象管理资源 这两部分集中在同一个语句中。虽然集中于同一语句的写法在逻辑上并没有错误,但 C++ 交给编译器的指令顺序调整的 弹性 很大,也就是说,在复杂的语句中,执行的先后顺序并不一定会按照你的逻辑顺序来执行,这就会引发异常,存在 资源泄露 的风险。故而建议用户通过独立的语句依次完成整个过程。

posted @ 2022-04-23 22:34  ZenonX  阅读(51)  评论(0编辑  收藏  举报