SamJ's blog

EffectiveC艹笔记(二)

Word count: 1.2kReading time: 4 min
2019/11/02 Share

条款六:不想使用编译器默认提供的函数时的做法

对于一个HomeForSale 类来说,如果你不希望有拷贝构造操作和赋值操作,可以将他们声明为private

1
2
3
4
5
6
7
8
class HomeForSale
{
public:
...
private:
HomeForSale(const HomeForSale&);
HomeForSale& operator=(const HomeForSale&);
};

但是这样做在编译期只能对用户的赋值和拷贝构造报错,成员函数和友元函数调用了这样的操作时会在链接期报错。想把报错都移动到编译期时,可以构建一个uncopyable 类,并继承它。

1
2
3
4
5
6
7
8
9
class Uncopyable
{
protected:
Uncopyable();
~Uncopyable();
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator=(const Uncopyable&);
};
  • 为了禁止编译器默认提供的函数,可以将其声明为private,或者构建一个uncopyable类并继承。(使用boost库提供的noncopyable也可以

条款七:为多态基类声明virtual析构函数

多态基类是指:一个基类设计的目的是”通过基类接口处理派生类对象“。

如下面的TimeKeeper 类:

1
2
3
4
5
6
7
8
9
class TimeKeeper{
public:
TimeKeeper();
~TimeKeeper();
};

class AtomicClock : public TimeKeeper(){...};
class WaterClock : public TimeKeeper(){...};
class WristWatch : public TimeKeeper(){...};

当定义一个工厂函数TimeKeeper* getTimeKeeper() 返回三个派生类之一时

TimeKeeper* ptk = getTimeKeeper()delete ptk 只会对其中的基类部分进行销毁,派生的都有数据不会被销毁。这时需要将Timekeeper 中的析构函数声明为虚函数才能解决这个问题。

1
2
3
4
5
class TimeKeeper{
public:
TimeKeeper();
virtual ~TimeKeeper();
};
  • virtual析构函数只有像TimeKeeper 例子中的情况下才使用。

条款八:别让异常逃离析构函数

这条条款主要目的是告诫我们不能让析构函数抛出异常。

1
2
3
4
5
6
7
8
9
class Widget{
public:
...
~Widget( ) {...}
};
void doSomthing()
{
std::vector<Widget> v;
}

~Widget() v内第一个元素抛出异常,则后续的元素也会抛出异常,当多个异常被抛出,程序的执行就不收我们控制,这是很可怕的现象。对于一个析构函数必须执行可能抛出异常的类,可以再创建一个类对其进行管理,并吞下异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class DBConnection{
public:
...
static DBConnection create();
void close(); //关闭连接,失败会抛出异常
};
//用DBConn来管理DBConnection
class DBConn{
public:
...
~DBConn()
{
try{ _db.close(); }
catch(...){
将调用close()失败的信息记入日志
std::abort();
}
}
/*
//或者可以这样写
~DBConn()
{
try(_db.close())
catch(...)
{
将调用close()失败的信息记入日志
}
}
*/
//区别在于前一个使用abort()结束了程序
private:
DBConnection _db;
};

但上面两种做法使得close()异常的出现对于用户来说几乎是透明的,用户无法对其进行处理,正确做法应该是下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class DBConn{
public:
...
void close()
{
_db.close();
_closed = true;
}
~DBConn()
{
if(!_closed)
{
try
{
_db.close();
}
catch(...){
上面两种的一种
}
}
}
private:
DBConnection _db;
bool _closed;
};

这一代码给了用户调用_db.close()并处理异常的机会。如果用户没有调用该函数,则析构时也会默认调用,清理资源,并且是相对安全的。

  • 析构函数不能抛出异常。
  • 提供普通函数使用户可以处理操作可能会抛出的异常。

条款九:不要在构造和析构时调用虚函数

如果基类构造函数中有虚函数,在派生类构建时,会调用基类的版本而不会下降到派生类(析构函数同理)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Transaction{
public:
Transaction()
{
init();
}

virtual void logTransaction() const = 0;
private:
void init()
{
...
logTransaction();
}
};

这段代码调用虚函数方式比较隐蔽,他会造成上述问题。为了解决在构造函数中调用logTransaction(),并且在其派生类中实现派生类独有形式的操作,可以将其改为非虚函数形式,每次传递信息给Transaction 构造函数,然后就可以安全调用logTransaction()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Transaction{
explicit Transaction(const std::string& logInfo);
void logTransaction(const std::string& logInfo) const;
};

Transaction::Transaction(const std::string& logInfo)
{
...
logTransaction(logInfo);
}

class BuyTransaction: public Transaction{
public:
BuyTransaction(parameters)
: Transaction(createLogString(parameters))
{}
private:
static std::string createLogString(parameters);
};

注意在BuyTransaction中的private static 函数,是很有必要的,静态函数可以避免使用未初始化的成员变量(?这个没搞太懂)。

  • 构造和析构函数中不能调用虚函数,因为其不会下降到派生类,只执行基类的定义。

条款十:是operator=返回一个*this的引用

在出现连续赋值,如x = y = z = 15时,赋值操作符必须返回*this的引用。这相当于规定,一定要遵守他。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Widget{
public:
Widget& operator=(const Widget &rhs)
{
...
return *this;
}
Widget& operator+=(const Widget &rhs)
{
...
return *this;
}
Widget& operator-=(const Widget &rhs)
{
...
return *this;
}
};
  • 赋值操作符返回一个*this的引用
CATALOG
  1. 1. 条款六:不想使用编译器默认提供的函数时的做法
  2. 2. 条款七:为多态基类声明virtual析构函数
  3. 3. 条款八:别让异常逃离析构函数
  4. 4. 条款九:不要在构造和析构时调用虚函数
  5. 5. 条款十:是operator=返回一个*this的引用