SamJ's blog

EffectiveC艹笔记(四)

Word count: 1.7kReading time: 6 min
2019/11/24 Share

条款十六:成对使用 相同形式 的new和delete

使用new时,先分配内存,再构造对象。

使用delete时,先析构对象,再释放内存。

当调用delete ... 时,被删除的指针,指向的是一个单独的对象,还是对象数组,是使用delete时必须要考虑的问题。

对于数组对象必须用delete [] ..., 对于单一对象必须用delete ...

使用typedef时:

1
2
3
4
typedef std::string AddressLines[4];
std::string* pal = new AddressLines; //相当于new string[4]

delete [] pal; //正确的做法

tepedef对数组形式起名容易引起错误,所以要尽量避免这类操作。

  • 使用new时,配合其使用delete,使用new []时,配合使用delete []

条款十七:将对象装入智能指针时用一条独立语句

对于这样一段程序:

1
2
3
4
int priority();
void processWidget(shared_ptr<Widget> pw, int priority());
//调用processWidget函数时要这样写:
processWidget(shared_ptr<Widget>(new Widget), priority());

所以在调用processWidget之前,编译器必须要做三件事:

  1. 调用priority
  2. 执行new表达式
  3. 调用shared_ptr的构造函数

可以确定new 表达式肯定会首先执行,其他两项的执行顺序是根据编译器喜好确定的。

当先执行priority时,若priority执行出现异常,new Widget返回的指针就被丢失,导致资源泄露

这是在资源被创建和资源被转换为资源管理对象两个时间点之间发生的干扰。

所以要用独立语句将对象装入智能指针。

1
2
3
shared_ptr<Widget> pw(new Widget);

processWidget(pw, priority());

这防止了编译器任意选择执行顺序导致的以上情形。

  • 以独立语句将对象装入智能指针,防止在资源被创建和资源被转换为资源管理对象之间产生异常,导致资源泄漏。

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

对于提供给客户的接口,理想上,如果客户企图使用这个接口却达不到他所预期的行为,这个代码不应该通过编译。所以在设计接口时,需要考虑到客户可能犯的错误。

如果有一个日期的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Date{
public:
Date(int month, int day, int year);
...
};
//容易犯两种错误:以错误的次序传递参数或传递无效的月份或天数
//为了防止这些错误,引入类型检查:创建新的Month, Day, Year类来使得传递参数时有类型检查
//并且同时限制他们的值
class Month{
public:
static Month Jan() { return Month(1);}
static Month Feb() { return Month(2);}
...
static Month Dec() { return Month(12);}
private:
explicit Month(int m);
...
};

Date d(Month::Mar(), Day(30), Year(1995));

只举了Month的例子。

还有预防客户犯错的方法是限制类型的行为,如条款3中以const修饰operator*的返回类型,防止对a*b进行赋值。

还有,使types与内置types的行为一致。

任何接口如果要求客户必须记得做某些事情,就是有着“不正确使用”的倾向,因为客户很容易忘记做这件事。

如条款13中直接返回一个智能指针。

  • 设计接口时防范其被误用
  • 设计的接口应与内置类型行为尽量保持一致
  • 阻止误用的办法包括建立新类型,限制类型的操作,消除客户的资源管理责任
  • shared_ptr支持的定制删除器可以防范DLL问题,被用来自动解除互斥锁

条款十九:设计class时像设计内置类型一样全面考虑

设计高效class时要考虑的问题:

  • 新类型的对象如何被创建和销毁? 考虑到内存分配和释放
  • 对象的初始化和对象的赋值操作怎样进行?
  • 新类型的passed by value 怎样定义? 会调用copy构造函数
  • 新类型的“合法值”有哪些? 对class可以取得的数值集进行限制
  • 新类型需要配合某个继承图系吗? 如果新的类型继承自某些classes,就会收到他们的限制。如果你允许新类型可以被继承,要考虑析构函数是否为virtual(作为多态基类时要设置成virtual)。
  • 新的类型是否允许类型转换? 如果有转换关系,需要给出相关的类型转换函数。
  • 对此新类型需要哪些操作符?
  • 什么样的操作不被允许? 将其声明为private
  • 是否要定义其成为一个模板类?
  • 真的需要定义这样一个类型吗? 如果只是为原来的类添加功能,可以定义函数或模板达到同样的结果。

条款二十:以pass-by-reference-to-const(常量引用)替换pass-by-value(值传递)

缺省情况下C++以pass-by-value传递参数时,传递的都是实际参数的副本(由该对象的复制构造函数所产生)。这导致传参时会发生函数构造的操作,这样的操作可能会影响程序的效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Person{
public:
Person();
virtual ~Person();
private:
std::string _name;
std::string _address;
};

class Student: public Person{
public:
Student();
~Student();
private:
std::string _schoolName;
std::string _schoolAddress;
};

bool validateStudent(Student s);
Student plato;
bool paltoIsOk = vadateStudent(plato);

当上面的函数被调用时,Student的复制构造韩式会被调用,以plato为值将s初始化。当函数返回时会将s销毁。这时,对于此函数的传递成本可以计算为:一次Student的复制构造函数调用和一次Student析构函数调用。更深一步看,该传参过程包括,一次Student复制构造,一次Person复制构造,四次string的构造,总体成本是六次构造和六次析构。

而当使用pass-by-reference-to-const时可以回避掉这些构造和析构动作。

bool validateStudent(const Student& s);,这种参数传递方式高效很多,传递的过程中没有任何对象会被创建。并且以const修饰参数,使传入的参数不会在函数内部发生改变。同时这样做可以避免对象切割问题(slicing)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Window{
public:
std::string name() const;
virtual void display() const;
};

class WindowWithScrollBars: public Window{
public:
virtual void display() const;
};

void printNameAndDisplay(Window w)
{
std::cout << w.name();
w.display();
}

WindowWithScrollBars wwsb;
printNameAndDisplay(wwsb);

这时,不论传递过来的是什么对象,因为值传递的缘故,都会被构造成一个Window对象。

当函数改为void printNameAndDisplay(const Window& w);时,传递过来是什么就是什么。

但对于内置类型和STL的迭代器和函数对象来说,传参时用pass-by-value就可以,因为其设计者会保证pass-by-value的高效和正确。

  • 对于自定义的类型尽量以pass-by-reference-to-const传递。
  • 对内置类型,STL的迭代器和函数对象用pass-by-value比较好。
CATALOG
  1. 1. 条款十六:成对使用 相同形式 的new和delete
  2. 2. 条款十七:将对象装入智能指针时用一条独立语句
  3. 3. 条款十八:让接口容易被正确使用,不易被误用
  4. 4. 条款十九:设计class时像设计内置类型一样全面考虑
  5. 5. 条款二十:以pass-by-reference-to-const(常量引用)替换pass-by-value(值传递)