SamJ's blog

EffectiveC艹笔记(三)

Word count: 1.8kReading time: 7 min
2019/11/10 Share

条款十一:在赋值运算符中处理”自我赋值“

自我赋值发生在对象给自己赋值的时候。

1
2
3
4
5
6
7
class Widget{...};
Widget w;
...
w = w;
//只要有一个以上的方法指向某一个对象,就可能造成这种自我赋值问题
*px = *py; //若px和py都指向同一个对象,出现自我赋值
a[i] = a[j]; //若i == j

这项操作没有什么意义,但是是合法的操作,在用户执行这项操作时要进行相关处理。

安全的实现自我赋值有两种方法(书上有三种,但是我觉得copy and swap又复杂又不好用,就记了两种):

  1. 证同测试(就是进行删除赋值操作前先判断要赋值的是不是其自身)

    1
    2
    3
    4
    5
    6
    7
    8
    Widget& Widget::operator=(const Widget& rhs)
    {
    if(this == &rhs) return *this;

    delete _pb; //_pb是Widget的一个数据成员
    _pb = new Bitmap(*rhs.pb);
    return *this;
    }
  2. 用一个额外的变量暂时存储_pb原来的值

    1
    2
    3
    4
    5
    6
    7
    Widget& Widget::operator=(const Widget& rhs)
    {
    Bitmap* pOrig = _pb;
    _pb = new Bitmap(*rhs.pb);
    delete pOrig;
    return *this;
    }
  • 确保对象的自我赋值是安全的,上述两种方法。
  • 操作多个对象的函数,多个对象是同一个对象时,保证函数的行为仍然正确。

条款十二:复制对象时复制所有成分

当自己定义了赋值运算符时,要注意使赋值运算符能拷贝类内所有成员。

一个典型的没有全部拷贝例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Customer{
public:
...
Customer(const Customer& rhs);
Customer& operator=(const Customer& rhs);
...
private:
std::string _name;
Date _lastTransaction; //自定义的一个日期类
};
Customer::Customer(const Customer& rhs)
: _name(rhs._name)
{ std::cout << "copy constructor" << std::endl;}

Customer& Customer::operator=(const Customer& rhs)
{
std::cout << "copy constructor" << std::endl;
_name = rhs._name;
return *this;
}

很明显,都没有对_lastTransaction这一成员进行复制,但编译器不会提醒你这一问题,修改类时注意同时修改赋值运算符的行为。

在继承的时候,会发生比较严重的问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class PriorityCustomer : public Customer{
public:
...
PriorityCustomer(const PriorityCustomer& rhs);
PriorityCustomer& operator=(const PriorityCustomer& rhs);
private:
int _priority;
};
PriorityCustomer::PriorityCustomer(const PriorityCustomer& rhs)
: _priority(rhs.priority)
{}
PriorityCustomer&
PriorityCustomer::operator=(const PriorityCustomer& rhs)
{
Customer::operator=(rhs); //必须手动调用基类的赋值运算符对基类部分进行复制
_priority = rhs._priority; //只是这样没有对基类成分进行复制
return *this;
}
  • 赋值运算符或者拷贝构造函数应保证复制对象内的所有变量和其父类成分。
  • 如果赋值运算符和拷贝构造函数有相近的代码,不要互相调用,要写一个private的init()函数来降低代码重复率。

条款十三:用对象管理资源

例子是我们正在实现一个表示投资的类。

1
2
3
4
5
6
7
8
9
10
class Investment{...} 		//一个Investment类

Investment* createInvestment(); //返回指针,指向Investment继承体系内的对象
//需要删除,防止内存泄漏
void f()
{
Investment* pInv = createInvestment();
...
delete pInv;
}

f()看起来没有什么问题,但如果在...中出现了return语句等,控制流就到达不了delete语句。

为确保createInvestment()返回的资源总是可以被释放,需要将资源放入对象内,当离开f,对象进行析构,就可以对资源进行回收。

这时会用到智能指针,并且最好是用shared_ptr,其在指向的对象引用计数为0时会自动对其析构。

1
2
3
4
5
void f()
{
std::shared_ptr<Investment> pInv(createInvestment());
...
}

有两个比较关键的思想:

  • 获得资源后立即放进对象内,即资源取得即初始化(RAII)
  • 管理资源的对象可以确保资源被释放(上面例子中当pInv的生命周期结束时,会自动将资源释放)

不用auto_ptr是因为一般都不用,这个东西设计有问题。

但是还要注意一个问题,智能指针并不能释放数组,因为他对其所指向对象执行的是delete而不是delete[],所以对于动态分配的数组要自己定义对象进行管理。

  • 使用RAII对象,shared_ptr是一个可以的选择。

条款十四:资源管理类中注意处理复制行为的发生

上一条给出了RAII的概念,但上面着重讲了怎样管理在堆上申请的资源,对于一些其他的资源,我们需要创建自己的资源管理类来进行管理。如:mutex互斥锁变量,socket网络套接字等。

为管理一个C API中的一个类型为Mutex的互斥锁对象,我们可以创建一个这样的RAII 类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Lock{
public:
explicit Lock(Mutex *pm)
:_mutex(pm)
{lock(_mutexPtr);} //void lock(Mutex *pm) 加锁
//void unlock(Mutex *pm)解锁
~Lock() { unlock(_mutexPtr);}
private:
Mutex *_mutexPtr;
};

//用户使用的时候:
Mutex m;
...
{
Lock m1(&m); //加锁
... //进行操作
} //Lock析构,解锁

但是当Lock m1(&m); Lock m2(m1);时怎么处理。

通常用两种方式处理RAII对象的复制操作:

  • 禁止复制,条款6(继承Uncopyable)
  • 对底层资源增加引用计数,类似于shared_ptr的做法

使用shared_ptr的版本:

1
2
3
4
5
6
7
8
9
10
11
class Lock{
public:
explicit Lock(Mutex* pm)
: _mutexPtr(pm, unlock) //表示以unlock为删除器
{
lock(mutexPtr.get());
}
//不需要析构函数,编译器默认版本即可
private:
shared_ptr<Mutex> _mutexPtr;
};
  • 赋值RAII对象必须同时赋值它所管理的资源,资源的copying行为决定RAII对象的copying行为
  • 一般对RAII类的copying行为进行:禁止复制/使用引用计数法两种操作

条款十五:在资源管理类中提供访问原始资源的接口

前面我们介绍了资源管理类,其主要作用是防止资源泄漏。在用户和原始资源之间产生一个中介。但是,有许多的API的的使用需要访问原始资源。这时资源管理类需要提供接口使用户可以访问这些原始资源。

举个例子:

1
2
3
4
5
6
7
//条款13中的createInvestment()
shared_ptr<Investment> pInv(createInvestment());
//daysHeld需要的参数是指向Investment对象的指针
int daysHeld(const Investment* pi);
int days = daysHeld(pInv); //出错,传递的是shared_ptr<Investment>对象

int days = daysHeld(pInv.get());//正确,get()返回了原始指针

通常,对于RAII对象,要有一个get方法,获得原始资源。

shared_ptr重载了operator->和operator*,使用这两个操作符的时候,shared_ptr对象会隐式转换为原始指针。

还有一个通过资源管理类获得原始资源的方法是提供隐式转换函数:

1
2
3
4
5
6
7
8
9
10
class Font{
public:
...
operator FontHandle() const //在需要的场景下自动执行(隐式转换)
{ return f; }
...
};
//但是会增加犯错的方式
Font f1(getFont());
FontHandle f2 = f1; //原意是拷贝Font对象,但会将f1隐式转换为底部的FontHandle才进复制
  • 许多API都需要访问原始资源,所以RAII类要提供访问原始资源的接口。
  • 显示转换比较安全但是使用较麻烦,隐式转换使用方便但容易造成错误。
CATALOG
  1. 1. 条款十一:在赋值运算符中处理”自我赋值“
  2. 2. 条款十二:复制对象时复制所有成分
  3. 3. 条款十三:用对象管理资源
  4. 4. 条款十四:资源管理类中注意处理复制行为的发生
  5. 5. 条款十五:在资源管理类中提供访问原始资源的接口