欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 家装 > Effective C++ | 读书笔记 (二)

Effective C++ | 读书笔记 (二)

2024/10/27 1:22:10 来源:https://blog.csdn.net/m0_74795952/article/details/143239753  浏览:    关键词:Effective C++ | 读书笔记 (二)

3、 资源管理

文章目录

      • 3、 资源管理
        • 条款 13:以对象管理资源
        • 条款 14: 在资源管理类中小心coping行为
        • 条款 15 :在资源管理类中提供对原始资源的访问
        • 条款 16 : 成对使用new和delete时要采取相同形式
        • 条款 17 :以独立语句将newed对象置入智能指针
      • 4、设计与声明
        • 条款18:让接口容易被正确使用,不容易被误用
        • 条款19 :设计class犹如设计type
        • 条款20: 宁以pass-by-reference-to-const替换pass-by-value(本条第六点要常看)
        • 条款21: 必须返回对象时,别妄想返回其reference
        • 条款22:将成员变量声明为private
        • 条款23:宁以non-member、non-friend替换member函数
        • 条款24:若所有参数皆需要类型转换,请为此采用non-member函数
        • 条款25:考虑写出一个不抛出异常的swap函数

条款 13:以对象管理资源
  1. 在一个作用域内,在delete 之前就return了,会造成内存泄漏,所以delete管理内存远远不够

    void fun()
    {Investment* pInv=CreateInvestment();……//这里提前 returndelete pInv;//释放资源
    }
    
  2. 用对象控制对象,离开了作用域自然会调用析构函数析构,比如使用智能指针auto_ptr(唯一资源使用权,对它的拷贝动作为让旧指针变为nullptr)

    1. RAII:资源获取时机即是初始化时机(resource acquisition is initialization)。获取资源后立即放进对象内进行管理。
    2. 管理对象运用析构函数确保资源释放。管理对象是开辟在栈上面的,离开作用域系统会自动释放管理对象,自然会调用管理对象的析构函数。
    3. 还有一种指针是引用计数器型指针,会记录多少个对象在使用资源,计数器为0,就释放,如share_ptr
    4. auto_ptr和shared_ptr释放资源用的都是delete,而不是delete[],对于数组指针,shared_array来对应。类似的还有scope_array

请记住:

为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。

两个常被使用的RAII classes 分别是tr1 :: shared_ptr和auto_ptr。前者通常是较佳选择,因为其copy行为比较直观。若选择auto_ptr,复制动作会使它(被复制物)指向null。

条款 14: 在资源管理类中小心coping行为
  1. 但是并不是所有资源都是开辟在堆上,有时候我们需要自己建立资源管理类

    class Lock{
    public:explicit Lock(Mutex* mu):mutexPtr(mu){lock(mutexPtr);}~Lock(){unlock(mutexPtr);}
    private:Mutex* mutexPtr;
    };
    

    这样客户对Lock的使用方法符合RAII方式:

    Mutex m;//定义互斥器
    ……
    {//建立区块来定义critical sectionLock(&m);……//执行critical section 内的操作
    }//在区块末尾,自动解除互斥器的锁

    当一个RAII对象被复制,会发生什么?有以下做法

    1. 禁止复制,将coping函数设置为私有,条款6

    2. 对管理资源使用引用计数法,复制的时候就加1 。mutexPrt变为类型从Mutex*变为shared即可

      class Lockprivate Uncopyable{
      public:explicit Lock(Mutex* mu):mutexPtr(mu,unlock)//以某个Mutex初始化,unlock作为删除其{lock(mutexPtr);}
      private:shared_prt<Mutex> mutexPtr;
      };
      

      注意的是在这个类中并没有自己编写析构函数。因为mutexPtr是类中的普通成员变量,编译器会自动生成析构函数类析构这样的变量。这个在条款5中有说明。

    3. 拷贝底部资源(深浅拷贝)

      使用资源管理类的目的是保证不需要这个资源时能正确释放。如果这种资源可以任意复制,我们只需编写好适当的copying函数即可。确保拷贝时是深拷贝。
      比如:C++中的string类,内部是指向heap的指针。当string复制时,底层的指针指向的内容都会多出一份拷贝。

    4. 转移底层资源的拥有权。

      有时候资源的拥有权只能给一个对象,这时候当资源复制时,就需要剥夺原RAII类对该资源的拥有权。像auto_ptr。在C++11新标准中的std::move便是这个功能。可以把一个左值转换为一个右值。

    copying函数如果你不编写,编译器会帮你合成,其合成版本行为可参考条款5。要记住的是不论是自己编写还是编译器合成,都要符合自己资源管理类的需要。

请记住:

复制RALL对象必须一并复制它所管理的资源,所以资源的copying行为决定RALL对象的copying行为

普通而常见的RALL class copying行为是:抑制copying、实行引用计数等。

条款 15 :在资源管理类中提供对原始资源的访问
  1. 原始资源,没有经过封装的指针(可以这样理解)

    //用智能指针来保存返回值
    shared_prt<Investment> pInv=(createInvestment());
    //有这样一个函数,显然是无法将只能指针对象的,这时就需要一个函数将管理的原始资源暴露出来
    int dayHeld(const Investment* pi);
    //shared_ptr和auto_ptr都提供一个get函数,用于执行这样的显示转换
    dayHeld(pInv.get());
    
  2. 为了使智能指针使用起来像普通指针一样,它们要重载指针取值(pointerdereferencing)操作符(operator->和operator*),它们允许转换至底部原始指针

  3. RAII class内的返回资源的函数和封装资源之间有矛盾。的确是这样,但这样不是什么灾难。RAII class不是为了封装资源,而是为确保资源释放。

请记住:

APIs往往要求访问原始资源(raw resources),所以每一个RAII class应该提供一个“取得其所管理之资源”的办法。

对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。

条款 16 : 成对使用new和delete时要采取相同形式
  1. 如果使用new开辟内存,就使用delete释放。如果使用new[]开辟内存,就使用delete[]释放。
  2. 尽量不使用对数组做typedef动作。在C++的STL中有string、vector等templates(条款54),可以将数组需求降至几乎为零
条款 17 :以独立语句将newed对象置入智能指针
//这样写是不行的,因为shared_ptr用普通指针构造的构造函数是explict的,不允许隐式转换
int processWidget(shared_ptr<Widget> pw, int priority);
processWidget(new Widget,priority());
//这样写可以过编译但是会有资源泄露的问题
int processWidget(shared_ptr<Widget>(new Widget), int priority);
  1. 在使用智能指针时,应该用独立的语句把新创建的对象指针放入智能指针,否则可能会造成内存泄露

    //对于这个的传参
    int processWidget(shared_ptr<Widget> pw, int priority);在调用processWidget之前有三件事:1、执行priority()函数
    2、执行new Widget
    3、执行shared_ptr构造函数
    

    C++编译器会以什么样的次序来完成这些事情呢?弹性很大。在Java和C#中,总是以特定的次序来完成这样函数参数的计算,但在C++中却不一定。唯一可以确定的是new Widget在shared_ptr之前调用。但是函数priority排在第几却不一定。假设排在第二,那么顺序就是1、执行new Widget。2、执行函数priority()。3执行shared_ptr构造函数。

    如果对函数priority()调用出现异常,那么new Widget返回的指针还没来得及放入shared_ptr中。这样会造成内存泄露。

    因此可以分开写,先创建,然后在传参

    shared_prt<Widget> pw(new Widget);
    processWidget(pw,priority());
    

请记住:

以独立语句将 newed 对象存储于(置入)智能指针内。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。

4、设计与声明

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

1、保证参数一致性:

void print_date(int year, int month, int day)
{......}
print_date(2022, 28, 9);//1
print_date(2022, 9, 28);//2

在这样一个打印时间的函数接口中,我们按照年月日的顺序输出,但是1式却输出年日月。错误的参数传递顺序造成了接口的误用。

解决办法:

class day{...};
class month{...};
class year{...};
void pd(const year& y, const month& m, const day& d){...}

当然,传递某个有返回值的函数也是可以解决的,但这种方法看起来很奇怪。

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 指针

第一个参数是被管理的指针,第二个是自定义删除器。

  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部分(因为传参的时候是base类创建的副本对象)。
  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

如下这种会出现错误,因为引用只是对象的别名,返回的是局部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对象的拷贝
}

在返回一个reference和返回一个object之间抉择时,挑出行为正确的那个。让编译器厂商为你尽可能降低成本吧!

条款22:将成员变量声明为private
  1. 封装。如果通过函数访问成员变量,日后可以用某个计算替换这个变量,这时class的客户却不知道内部实现已经变化。
  2. 将成员变量声明为private。这可以赋予客户访问数据的一致性、可细微划分访问控制、允许约束条件获得保证,并提供class作者以充分弹性实现。
  3. protected并不比public更具有封装性。
条款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_func()(func3()的非成员函数版本)为便利函数。假设一个类有多个诸如 func1() 的函数,根据排列组合,也就有很多便利函数。为了让这些便利函数和它的类看上去更像一个整体,我们把便利函数和类放在一个 namespace 中。于是,我们可以更为轻松地拓展这些便利函数——多做一些排列组合。

若一个成员函数调用其他成员函数,那么这个成员函数的非成员函数版本比之拥有更多的封装性,和机能扩充性。

总结:

  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函数

    另一说法:如果所有参数(运算符左边或者右边的参数)都需要类型转换,用 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。(也即是拷贝)

总结

  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而言是全新的东西。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com