c++:资源管理(RAII)、new/delete的使用、接口设计与声明、swap函数

以对象管理资源的两个关键想法:1. 获得资源后立刻放进管理对象(managing object)内;2. 管理对象运用析构函数确保资源被释放;

对于被动态分配于heap且被用于单一区块或函数内的资源,应该在控制流离开该区块或函数时被释放;

所以,将资源放进对象内,(如:RAII对象能在构造函数中获得资源并在析构函数中释放资源),当控制流离开函数时,自动调用该对象的析构函数确保资源被释放,从而有效避免资源泄露。

RAII:“资源取得时机便是初始化时机”(Resource Acquisition Is Initialization)

常见的RAII class包括auto_ptrtr1::shared_ptr,是”类指针(pointer-like)对象,也称为智能指针;

1. auto_ptr:

标准程序库提供,析构函数自动对其所指对象调用delete;

使用方法

void f() {
    std::auto_ptr<Investment> pInv(createInvestment());//调用factory函数获取动态分配对象
    ...//使用pInv
       //经由auto_ptr的析构函数自动删除pInv
}

由于auto_ptr被销毁时会自动删除其所指对象,所以要避免多个auto_ptr同时指向同一对象;

auto_ptr规定若通过copy构造函数或copy assignment操作符赋值它们,被复制的原auto_ptr会变为NULL,复制所得指针取得资源的唯一拥有权,如:

std::auto_ptr<Investment> pInv1(createInvestment()); //pInv1指向动态生成的对象
std::auto_ptr<Investment> pInv2(pInv1);//现在pInv2指向对象,pInv1为NULL
pInv1 = pInv2; //现在pInv1指向对象,pInv2为NULL

2. RCSP:“引用计数型智慧指针“(reference-counting smart pointer);

RCSP能够追踪共有多少RCSP智能指针指向某个资源,并在无人指向它时自动删除该资源(类似垃圾回收garbage collection);

RCSP无法打破环状引用(cycles of reference),如:两个RCSP智能指针彼此互指,从而还处在”被使用“的状态;

RCSP通常为较佳选择,因为其copy行为比较直观;

例:TR1的tr1::shared_ptr的使用

void f() {
    std::tr1::shared_ptr<Investment> pInv(createInvestment()); //调用factory函数获取动态对象
    ...//使用pInv
        //经由shared_ptr析构函数自动删除pInv
}

例:shared_ptr的复制行为

void f() {
    std::tr1::shared_ptr<Investment> pInv1(createInvestment());//pInv1指向动态生成的对象
    std::tr1::shared_ptr<Investment> pInv2(pInv1); //pInv1和pInv2指向同一个对象
    pInv1 = pInv2; //同上
    ...//pInv1和pInv2被销毁,它们所指对象也自动销毁
}

对于非heap-based资源,可能需要创建自己的资源管理类,在编写RAII对象的资源管理类时,需要注意处理copy行为;

可根据需要实现不同的RAII class copying行为,通常采用抑制copying或引用计数法;

1. 禁止复制

通常允许RAII对象被复制不合理,如果需要禁止copy RAII对象,可以通过将copying操作声明为private(继承Uncopyable类)来禁止copy;

2. 对底层资源使用引用计数法(reference-count)

如果需要保留资源直到资源的最后一个使用者被销毁,可实现引用计数法;

通过使RAII class内含tr1::shared_ptr成员变量,可实现reference-counting copying行为(复制对象时被引用数递增);

例:原RAII class的成员变量为Mutex *mutexPtr,释放动作是解除锁定unlock(mutexPtr)。可将成员变量改为tr1::shared_ptr,并通过更改tr1::shared_ptr构造函数的第二参数删除器(deleter,当引用次数为0时调用)将当引用次数为0时的缺省行为”删除所指对象“改为解除锁定;

class Lock {
  public:
      //以某个Mutex初始化shared_ptr,并以unlock函数为删除器
      explicit Lock(Mutex* pm) : mutexPtr(pm, unlock) {
          lock(mutexPtr.get());
      }
  private:
      //用shared_ptr替换raw pointer
      std::tr1::shared_ptr<Mutex> mutexPtr;
};

auto_ptr没有删除器的机能,它总是将其指针删除;

例中RAII class不再声明析构函数,因为会自动调用其non-static成员(mutexPtr)的析构函数;

3. 可以允许针对一份资源拥有任意数量的副本,当不再需要某个副本时通过资源管理类确保它而被释放;

复制资源管理对象时注意进行深度拷贝;

4. 如果希望确保永远只有一个RAII对象指向一个raw resource,复制RAII对象时资源拥有权要从被复制物转移到目标物,类似auto_ptr;

RAII class还应该提供取得其管理的底层资源的方法,以供API访问原始资源(raw resources),可采用显式转换或隐式转换两种做法,将RAII class对象转换为其管理的原始资源;

显式转换(比较安全):

tr1::shared_ptr和auto_ptr都提供一个成员函数get()返回只能指针内部的原始指针(的副本),如:

int daysHeld(const Investment *pi);//访问原始资源的API
std::str1::shared_ptr<Investment> pInv(createInvestment());//使用智能指针管理资源
//get()显示转换后返回智能指针内部的原始指针副本,供API访问原始资源
int days = daysHeld(pInv.get());

对自定义的RAII对象,也可提供显示转换函数,如:

void changeFontSize(Font Handle f, int newSize);//访问原始资源的API
class Font { //RAII对象
  public:
      explicit Font(FontHandle fh) : f(fh) {} //获得资源
      ~Font() {releaseFont(f);} //释放资源
      FontHandle get() const {return f;} //显转式换函数
  private:
      FontHandle f; //raw resource
};

int newFontSize;
Font f(getFont());//生成原始资源并放入RAII对象
changeFontSize(f.get(), newFontSize);//通过显示转换访问原始资源

隐式转换(对客户比较方便):

智能指针如tr1::shared_ptr和auto_ptr重载了指针取值(pointer dereferencing)操作符(operator->和operator*),允许隐式转换至底部原始指针:

class Investment { //原始资源
  public:
      bool isTaxFree() const;
      ...
};
Investment* createInvestment(); //生成原始资源的factory函数

std::tr1::shared_ptr<Investment> pi1(createInvestment());//用tr1::shared_ptr管理资源
bool taxable = !(pi1->isTaxFree()); //经由operator->访问资源
...
std::auto_ptr<Investment> pi2(createInvestment());//用auto_ptr管理资源
bool taxable2 = !((*pi2).isTaxFree()); //经由operator*访问资源

对自定义的RAII对象,也可提供隐式转换函数,如将Font(RAII对象)转型为FontHandle(原始资源):

class Font {
  public:
      ...
      operator FontHandle() const {refturn f;}
      ...
  private:
      FontHandle f;
};
//客户通过隐式转换调用API
Font f(getFont());
int newFontSize;
...
changeFontSize(f, newFontSize);

隐式转换有引发错误的隐患,接口容易被误用,如客户可将f1管理的FontHandle直接拷贝到资源对象FontHandle f2,如果RAII对象f1被销毁,资源被释放,f2则会dangle;

成对使用new和delete时要采用相同形式

new表达式和delete表达式要成对使用[]或都不适用[];

如果new中使用[],针对此内存会调用一个或多个构造函数,使用delete释放内存时也需要使用[],调用相应的一个或多个析构函数。如:

std::string* stringPtr1 = new std::string;
std::string* stringPtr2 = new std::string[100];
...
delete stringPtr1; //删除一个对象
delete []stringPtr2; //删除一个由对象组成的数组

此外,以new创建typedef类对象时应该说明应以哪种形式delete,如:

typedef std::string AddressLines[4]; //每个地址有4行,每行是一个string
std::string* pal = new AddressLines; //等同new string[4]
delete pal;//错误,行为未有定义
delete []pal;//正确,必须匹配数组形式的delete

为避免出错,尽量不对数组形式做typedef动作,可通过C++标准库中的的vector等template完成,如将例子中的AddressLines定义为vector

注意:以独立语句将newed对象置入智能指针

如果在同一个语句中包含将newed对象置入智能指针及其他操作,因为C++编译器对这些操作的执行次序弹性较大,“资源被创建(new)”和“资源被转换为资源管理对象(置入智能指针)”两个时间点之间有可能发生异常干扰。如:

processWidget(std::tr1::shared_ptr<Widget>(new Widget), priority());

编译器有可能采用1. 执行”new Widget” 2. 调用priority 3. 调用tr1::shared_ptr构造函数的操作序列,如果对priority调用异常,“new Widget”返回的指针尚未置入智能指针,将会遗失,造成资源泄露。

正确做法:以独立语句将newed对象置入智能指针。如:

std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw, priority());

因为编译器只有在语句内才拥有重新排列的自由,对于跨越语句的各项操作没有自由度,确保先将newed对象置入智能指针后再执行其他调用操作。

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

tr1::shared_ptr支持定型删除器(custom delete),能够自动使用其专属删除器,可防范cross-DLL问题;

cross-DDL problem:对象在动态链接程序库(DLL)中被new创建,却在另一个DLL内被销毁的问题。在许多平台上,这一类“跨DLL的new/delete成对运用”会导致运行期错误;

tr1::shared_ptr能够避免此问题,因为它缺省的删除器是“来自tr1::shared_ptr诞生所在的那个DLL”的delete;

Boost的shared_ptr比原始指针大且慢,而且使用辅助内存,在许多应用程序中这些额外的执行成本并不显著,但其具有降低客户错误的成效。

尽量以pass-by-reference-to-const替换pass-by-value

以pass-by-value方式接受的参数受到保护,调用函数只会对其副本做修改,不会改变传入的对象;

缺省情况下C++以by value的方式传递对象至函数,除非另外指定,否则函数参数都是以实参的副本为初值,调用端所获得的也是函数返回值的副本,参数传递成本是产出副本所需的一次或若干次构造函数及相应的虚构函数调用;

pass-by-value例子:

bool validateStudent(Student s); //函数以by value方式接受参数
Student plato;
bool platoIsOK = validateStudent(plato); //调用函数

以pass-by-reference-const的方式传递参数效率更高,没有任何新对象被创建,不需要调用构造函数或析构函数;

参数声明const是必要的,保证函数不会改变传入的参数;

pass-by-reference-const例子:

bool validateStudent(const Student& s);

以by-reference方式传递参数可以避免slicing(对象切割)问题;

slicing问题:如果将derived class以by value方式传递并被视为一个base class对象,将会调用base class的copy构造函数,而derived的特化性质都被切割掉,只留下了base class对象;

解决slicing问题的办法:以by-reference-to-const的方式传递。如:

class Window { //base class
  public:
      ...
      std::string name() const;
      virtual void display() const;
};
class WindowWithScrollBars : public Window { //derived class
  public:
      ...
      virtual void display() const; //与base class中不同
};

void printNameAndDisplay(Winodw w) { //错误,以by value方式参数会被切割
    std::cout << w.name();
    w.display;
}
void printNameAndDisplay(const Window& w) {//正确,以by-reference-to-const方式参数不会被切割
    std::cout << w.name();
    w.display;
}
WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb); //若以by-value形式,参数就像一个base class对象
                    //若以by-reference形式,传入参数是什么类型就表现什么类型

小型的用户自定义类型不一定适合使用pass-by-value,一般来说pass-by-value只针对内置类型、STL迭代器和函数对象比较适当,其他情况下都倾向于使用pass-by-reference-to-const:

1. pass-by-reference的底层通常用传递指针来实现,如果对象属于内置类型(如int),pass-by-value往往比pass-by-reference高效,内置类型通常建议选择pass-by-value;

2. STL迭代器和函数对象通常也习惯采用pass-by-value,实践者有责任确认它们是否高效且不受slicing问题影响(规则之改变取决于你使用那一部分C++);

必须返回对象时,不要试图返回reference

注意绝对不要返回指向一个local stack/heap-allocated对象的pointer/reference;

绝对不要在可能同时需要多个这样对象的情况下返回指向一个local static对象的pointer或reference;

成员变量应声明为private

成员变量声明为private可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性;

从封装的角度来看,其实只有两种访问权限:private(提供封装)和其他(不提供封装);

protected成员变量并不比public更具封装性,如果成员变量被改变,都会有不可预知的大量代码受到破坏(public成员变量的改变会导致所有使用它的客户码的破坏,protected成员变量的改变会导致所有使用它的derived classes的破坏)。一旦将成员变量声明为public或protected而客户开始使用它,改变成员变量时就需要大量的重写、重新测试、编写文档和编译。

尽量以non-member、non-friend函数替换member函数

以non-member non-friend函数替换member函数,可以增加封装性、包裹弹性(packaging flexibility)和机能扩充性;

class WebBrowser {
  public:
      ...
      void clearCache();
      void clearHistory();
      void removeCookies();
      void clearEverything() { //用member函数调用member函数的方法
          this->clearCache();
          this->clearHistory();
          this->removeCookies();
      }
      ...
};
void clearBrowser(WebBrowser& wb) { //用non-member函数调用member函数的方法
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}

原因:

1. non- member non-friend函数的封装性比member函数高,不会增加“能够访问class内的private成分”的函数数量。(需注意 1注意是non-member non-friend函数,friends函数跟member函数的封装性一样低 2可以是另一个class的member函数,只要不是要处理对象的member或friend,不会影响该对象的封装性);

2. 提供non-member函数可允许对处理的类相关机能有较大的包裹弹性,增加其可延展性;

C++中一般做法是让能为处理对象提供便利的non-member函数(如clearBrowswer)位于和设计类(如WebBrowser)所在的同一个namespace中,因为namespace能够跨越多个源码文件:

namespace WebBrowserStuff {
  class WebBrowser {...};
  void clearBrowser(WebBrowser &wb);
  ...
}

包裹弹性:因为class可能拥有大量便利函数,不同客户通常只对其中某些感兴趣,可将不同的相关便利函数声明分离到不同的头文件内,如:

//头文件"webbrowser.h" 针对class WebBrowser自身
namespace WebBrowserStuff {
    class WebBrowser{...};
    ... //核心技能,例如所有客户都需要的non-member函数
}

//头文件"webbrowserbookmarks.h"
namespace WebBrowserStuff {
    ... //与书签相关的便利函数
}

//头文件"webbrowsercookies.h"
namespace WebBrowserStuff {
    ... //与cookie相关的便利函数
}
...

也是C++标准程序库的组织方式,多个头文件声明std的某些技能,允许客户只对他们所使用的一小部分系统形成编译相依。(此种切割机能方式并不适用于class成员函数,因为一个class必须整体定义)。

可延展性:以namespace方式客户可以轻松扩展便利函数,只需在命名空间内建立头文件,内含新添加的non-member non-friend函数的声明,而class定义是对客户而言不能扩展(derived classes无法访问base class中封装的private成员,扩展机能拥有的只是次级身份)。

如果某个函数的所有参数(包括this指针所指的隐喻参数)都需要类型转换,应将该函数声明为non-member

举例:

class Rational {
  public:
      Rational(int numerator = 0, int denominator = 1);
      int numerator() const;
      int denominator() const;
  private:
      ...
};

如果将operator*写成Rational成员函数

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

该设计允许两个Rational相乘,但与int进行混合算术时却有一半无法通过编译:

Rational oneEighth(1, 8);
Rational oneHalf(1, 2);
Rational result = oneHalf*oneEighth;//通过
result = result*oneEighth;//通过
result = oneHalf*2;//等同result = oneHalf.operator*(2),通过
result = 2*oneHalf;//result = 2.operator*(oneHalf),不能通过

result = oneHalf2能够通过,因为在调用Rational的成员函数operator时对整数2进行了隐式类型转换,调用Rational构造函数并赋予传入的int,为整数2建立了一个暂时性的Rational对象供成员函数operator*使用,所以能够通过;

result = 2oneHalf;不能通过,因为oneHalf对象内含operator成员函数,但整数2没有相应class,也就没有operator成员函数,并且编译器在命名空间或global作用域内也没有找到接受int和Rational作为参数,供result = operator(2, oneHalf)调用的non member operator*,所以不能通过;

为使class Rational支持混合算术运算,需让operator的参数位于参数列(parameter list)内从而允许隐式类型转换,应将oprator声明为non-member函数:

const Rational operator*(const Rational& lhs, const Rational& rhs) {
    //non-member函数
    return Rational(lhs.numerator()*rhs.numerator(), lhs.denominator()*rhs.denominator());
}
Rational oneFourth(1,4);
Rational result;
result = oneFourth*2;
result = 2*oneFourth; //都能通过编译

另:也应尽量避免将不应该是member的函数声明为friend,如例中operator*完全可以由Rational的public接口完成任务。

提供不会抛出异常的swap函数

只要类型T支持copying(copy构造函数和copy assignment操作符),缺省情况下swap动作由标准程序库std提供swap算法完成,涉及3个对象的复制(a->temp, b->a, temp->b):

namespace std {
    template<typename T>
    void swap(T& a, T& b) {
        T temp(a);
        a = b;
        b = temp;
    }
}

对于成员为指针,并以指针指向内含真正数据对象的class或template设计(pimpl手法,pointer to implementation),缺省swap会因为涉及三个对象的复制而效率不足。

例:

class WidgetImpl { //针对Widget数据而设计的class
  public:
      ...
  private:
      int a, b, c; 
      std::vector<double> v;//可能有大量数据
      ...
};

class Widget { //使用pimpl手法的class
  public:
      Widget(const Widget& rhs);
      Widget& operator=(const Widget rhs) {
          ...
          *pImpl = *(rhs.pImpl);
          ...
      }
      ...
  private:
      WidgetImpl* pImpl;//指向数据对象
};

在Widget中声明名为swap的public成员函数执行真正的置换工作(置换指针),将std::swap特化为调用成员函数swap:

class Widget {
  public:
      ...
      void swap(Widget& other) { //增加public成员函数swap
          using std::swap;
          swap(pImpl, other.pImpl);
      }
      ...
};
namespace std { 
    template<> //std::swap针对T是Widget的特化版本
    void swap<Widget>(Widget& a, Widget& b) {
        a.swap(b); //调用Widget的成员函数swap
    }
}

如果Widget和Widget Impl都是class templates,为提供高效的template特定版本swap,应在放置Widget及其所有相关机能的命名空间WidgetStuff内声明一个non-member swap(不用声明为std::swap的特化或重载版本),让它调用member swap:

namespace WidgetStuff {
    ... //模板化的WidgetImpl等
    template<typename T>
    class Widget { ... } //同前,内含public成员函数swap
    ...
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b) { //non-member swap函数,不属于std命名空间
        a.swap(b);
    }
}

编写function template中涉及置换时,如果希望调用可能存在的T专属版本swap,并在该版本不存在的情况下则调用std内的一般化版本:

template<typename T>
void doSomething(T& obj1, T& obj2) {
    using std::swap; //令std::swap在此函数内曝光可见
    ...
    swap(obj1, obj2); //为T型对象调用最佳swap版本
    //注意勿添加修饰符std::,否则编译器会被强迫只能调用std内的swap
    ...
}

注意:成员函数swap绝不可抛出异常。

原文链接: https://www.cnblogs.com/RDaneelOlivaw/p/7551345.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/260111

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月14日 下午1:13
下一篇 2023年2月14日 下午1:14

相关推荐