读书笔记中涉及到的所有代码实例可通过https://github.com/LuanZheng/EffectiveCPlusPlus.git进行下载获得。
Item18: 让接口容易被正确使用,不易被误用
理想上,如果客户企图使用某个接口而却没有获得他所预期的行为,这个代码不该通过编译;如果代码通过了编译,他的作为就该是客户所想要的。
明智而审慎的导入新类型。
例:
#ifndef _DATE_H_
#define _DATE_H_
class Month
{
public:
static Month Jan() { return Month(1); }
static Month Feb() { return Month(2); }
static Month Mar() { return Month(3); }
static Month Apr() { return Month(4); }
static Month May() { return Month(5); }
static Month Jun() { return Month(6); }
static Month Jul() { return Month(7); }
static Month Aug() { return Month(8); }
static Month Sep() { return Month(9); }
static Month Oct() { return Month(10); }
static Month Nov() { return Month(11); }
static Month Dec() { return Month(12); }
private:
explicit Month(int m);
int m_Month;
};
class Day
{
public:
explicit Day(int day);
private:
int m_Day;
};
class Year
{
public:
explicit Year(int year);
private:
int m_Year;
};
class Date
{
public:
Date(Month m, Day day, Year year);
private:
Month m_Month;
Day m_Day;
Year m_Year;
};
#endif // !_DATE_H_
不使用Date(int m, int day, int year)的形式,可使得使用时一目了然。
Date today(Month::Feb(), Day(28), Year(2018));
而不是 Date today(2,28,2018)这样,容易搞错月份和日期。
限值类型内什么事可以做,什么事不能做。
任何接口如果要求客户必须记得做某些事情,就是有着“不正确使用”的倾向,因为客户可能会忘记做那件事情。
比如: Investment* createInvestment(); 返回一个Investment*, 如果客户以对象管理资源,则可能会造成内存泄露。较佳的做法是先发制人,另其返回一个智能指针:
std::tr1::shared_ptr<Investment> createInvestment();
例子见Item18
Item19 设计class犹如设计type
新type的对象应该如何被创建和销毁?
对象的初始化和对象的赋值该有什么样的差别?
新type的对象如果被pass-by-value,意味着什么?
什么是新type的合法值?
你的新type需要配合某个继承图系么?
你的新type需要什么样的转换?
什么样的操作符和函数对此新type而言是合理的?
什么样的标准函数应该驳回?
谁该取用新type的成员?
什么事新type的未声明接口?
你的新type有多么一般化?
你真的需要一个新type么?
Item20 宁以pass-by-reference-to-const替换pass-by-value
缺省情况下C++以by value方式(一个继承自C的方式)传递对象至(或来自)函数。
补充知识,Copy构造函数
调用拷贝构造函数的情形
在C++中,下面三种对象需要调用拷贝构造函数(有时也称“复制构造函数”):
1) 一个对象作为函数参数,以值传递的方式传入函数体;
2) 一个对象作为函数返回值,以值传递的方式从函数返回;
3) 一个对象用于给另外一个对象进行初始化(常称为赋值初始化);
如果在前两种情况不使用拷贝构造函数的时候,就会导致一个指针指向已经被删除的内存空间。对于第三种情况来说,初始化和赋值的不同含义是拷贝构造函数调用的原因。事实上,拷贝构造函数是由普通构造函数和赋值操作符共同实现的。描述拷贝构造函数和赋值运算符的异同的参考资料有很多。
通常的原则是:①对于凡是包含动态分配成员或包含指针成员的类都应该提供拷贝构造函数;②在提供拷贝构造函数的同时,还应该考虑重载”=”赋值操作符号。原因详见后文。
拷贝构造函数必须以引用的形式传递(参数为引用值)。其原因如下:当一个对象以传递值的方式传一个函数的时候,拷贝构造函数自动的被调用来生成函数中的对象。如果一个对象是被传入自己的拷贝构造函数,它的拷贝构造函数将会被调用来拷贝这个对象这样复制才可以传入它自己的拷贝构造函数,这会导致无限循环直至栈溢出(Stack Overflow)。除了当对象传入函数的时候被隐式调用以外,拷贝构造函数在对象被函数返回的时候也同样的被调用。
可以看到,以pass-by-value方式传输,需要调用
1.基类构造函数
2.派生类拷贝构造函数(在参数传递时发生)
3.派生类析构函数(在函数返回时发生,传入参数需要被析构)
4.基类析构函数
而以pass-by-reference-to-const方式调用,由于不涉及产生新的对象,所以以上函数都不会被调用。
#include "SchoolPerson.h"
#include <iostream>
void testPerson(Person p)
{
p.print();
}
void testPersonByRef(Person& p)
{
p.print();
}
void validateSchoolPerson(const SchoolPerson p)
{
std::cout << "validateSchoolPerson(const SchoolPerson p) called." << std::endl;
}
void validateSchoolPersonByRef(const SchoolPerson& p)
{
std::cout << "validateSchoolPerson(const SchoolPerson& p) called." << std::endl;
}
int main()
{
SchoolPerson plato;
std::cout << "Benchmark start pass by value..." << std::endl;
validateSchoolPerson(plato);
std::cout << "Benchmark start pass by ref..." << std::endl;
validateSchoolPersonByRef(plato);
SchoolPerson p;
std::cout << "Benchmark start pass by value test slice..." << std::endl;
testPerson(p);
std::cout << "Benchmark start pass by ref test slice..." << std::endl;
testPersonByRef(p);
return 0;
}
另外,以pass-by-reference-2-const传递参数,还可以避免切割问题的发生。如果使用pass-by-value方式传递参数,若参数类型写明是基类类型,而传入派生类类型的话,copy构造函数会将该派生类对象copy成为基类对象,因此,在函数内部操作的都是基类的副本。而如果以pass-by-reference-to-const传递,由于没有生成新的对象,在函数内操作的仍然是派生类的结构。
例子见Item20
Item21 必须返回对象时,别妄想返回其reference
所谓reference只是个名称,代表某个既有对象。任何时候看到一个reference声明式,你都应该立刻问自己,它的另一个名称是什么?
在函数内部,两中内存分配方式,一种是在栈上分配。
请注意,任何函数如果返回一个reference指向某个local对象,都将一败涂地。如果函数返回指针指向一个local对象,也是一样。因为函数退出时,栈上面的内存被清空了。
第二种是在堆上分配,即使用new, new[]操作符动态分配。这种情形下,仍需要创建对象,因此构造函数的成本无法避免。同时,这种做法将带来一个隐患,谁将负责delete那块内存?特别是在连续调用时,会导致使用多个new,但却无法获取所有的delete的资源。这必将导致内存泄露。
因此,在函数必须返回对象时,就让其返回一个新对象吧。
例子见Item21