条款18:
1.欲开发一个“容易被正确使用,不容易被误用”的接口,首先必须考虑客户可能做出什么样的错误。
例如:
class Date{
public:
Date(int month, int day, int year);
//...
};
客户可能很容易犯下至少两个错误:
第一,以错误的次序传递参数:
Date d(30,3,1995);
第二,传递一个无效的月份或天数:
Date d(2,30,1995);
2.许多客户端错误可以因为导入新类型而获得预防。我们导入简单的外覆类型来区别天数、月份和年份。
struct Day{
explict Day(int d):val(d){};
int val;
}
struct Month{
explicit Month(int m):val(m){}
int val;
};
struct Year{
explicit Year(int y):val(y){}
int val;
};
class Date{
public:
void Date(const Month& m, const Day& d, const Year& y);
//……
};
Date d(30, 3, 1995); //错误,不正确的类型
Date d(Day(30), Month(3), Year(1995)); //错误,不正确额类型
Date d(Month(3), Day(30), Year(1995)); //正确
3.可以利用enum表现月份,但enums不具备类型安全性,例如enums可被拿来当一个ints使用(条款2),比较安全的解法是预先定义所有有效的Months:
class Month{
public:
static Month Jan(){ return Month(1);}
static Month Feb(){ return Month(2);}
……
private:
explicit Month(int m);//只能在类内部使用,防止生成其他月份。
……
};
Date d(Month::Mar(),Day(30),Year(1995));
4.预防客户端错误的另一个办法是,限制类型内什么事可以做,什么事不可以做。经常见到的是加上限制const。例如在条款3中,用const修饰operator*的返回值,这样就可以阻止客户因“用户自定义类型而犯错”
if(a*b=c)//这里其实打算做比较,而不是赋值
5.factory函数(条款13)返回一个指向Investment继承体系内的一个动态分配对象:
Investment* CreateInvestment();
为了避免资源泄露,CreateInvestment返回的指针必须被删除,这样客户就有了两个犯错误的机会:没有删除指针,或者删除了不止一次。可以将delete交给智能指针。但是为了防止用户忘记使用智能指针,fuctory函数返回一个智能指针:
shared_ptr<Investment> CreateInvestment();
6.让智能指针自带删除器
shared_prt<Investment> createInvestment()
{
shared_prt<Investment> retVal(static_cast<Investment*>(0),
getRidOfInvestment);
retVal=……;//令retVal指向正确对象
return retVal;
}
其中使用static_cast类型转换是因为shared_ptr构造函数的第一个参数必须是指针,而0不是指针。
7.shared_ptr一个特别好的性质是:它会自动使用它的“每个指针专属的删除器”,因而消除另一个潜在客户的错误:Corss-DLL Problem。这个问题发生于:对象在一个动态链接库DLL中被new创建,却在另一个DLL内被delete销毁。在许多平台上,这一类跨DLL之new/delete成对使用会导致运行期错误。shared_ptr没有这个问题,因为它的删除器来自其所诞生的那个DLL的delete。
条款20:
1.pass-by-value会调用复制构造函数和析构函数,成本高。pass-by-reference-to-const可以回避构造和析构。
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;
};
现在考虑一个函数validateStudent,它需要一个Student实参,以pass by value方式传递。bool validateStudent(Student s);//pass by value
Student plato;
bool platIsOK = validateStudent(plato);
当函数被调用时,copy构造函数会被调用,用plato构造s。在返回时,s会被析构。那么pass by value的代价就是Student的一次构造和一次析构。但是Student构造和析构时又发生了什么?它内部有两个string对象,所以会有两个string对象的构造和析构。Student继承自Person,又加上Person的构造和析构,Person内又有两个string对象,因此还要加上2个string对象的构造和析构。总共是六次构造和六次析构。
事实上没有调用父类的copy 构造函数,而是普通构造函数,具体参考:http://blog.youkuaiyun.com/djb100316878/article/details/40857183
pass by value是正确的,但是其效率低下。以pass by reference-to-const方式传递,可以回避所有构造函数和析构函数。
pass-by-reference-to-const可以回避所有构造和析构和析构函数。bool validateStudent(const Student& s);
值传递不需const是因为只能对副本进行修改,现在以reference传递,为了防止被修改,必须加上const。2.以by reference方式传递参数可以避免slicing(对象切割)问题:当一个派生类对象以by value方式传递给一个基类对象,base class的copy构造函数会被调用,而“造成此对象像derived class对象”的那些特化性质全被切割掉了,仅仅留下一个base class对象。
解决切割问题的办法就是:以by-reference-to-const的方法传递。
#include <iostream>
using namespace std;
class Base
{
public:
Base()
{
cout << "调用父类的构造函数" << endl;
}
Base(const Base& b)
{
cout << "调用的是父类的copy 构造函数" << endl;
}
~Base()
{
}
virtual void display() const
{
cout << "调用的是父类的display函数" << endl;
}
private:
int i;
};
class Derived : public Base
{
public:
Derived()
{
cout << "调用子类的构造函数" << endl;
}
~Derived()
{
}
virtual void display() const
{
cout << "调用的是子类的display函数" << endl;
}
};
void print(Base b)
{
b.display();//参数被切割 即使传递子类对象,调用的也是父类的display函数
}
void print2(const Base& b)
{
b.display();
}
int main()
{
Derived aa;
print(aa); //这里调用了 父类的copy构造函数 父类的print函数;
print2(aa);
return 0;
}
输出:调用父类的构造函数
调用子类的构造函数
调用的是父类的copy 构造函数
调用的是父类的display函数
调用的是子类的display函数
3.对内置类型而言,当有机会选择采用pass-by-value或pass-by-reference-to-const时,选择pass-by-value并非没有道理。这适用于STL的迭代器和函数对象。(复制成本较低)。对于STL的迭代器和函数对象,用传值的比传址适合。用迭代器遍历,是常用的。如果传个常量指针或引用进去,将丧失遍历的能力。如果传个引用或指针进去,可以遍历,但是会改变外部迭代器的值,一般情况下是没这种需求的。所以不如传值进去,复制一个迭代器,内部可以用,又不影响外部,这是常见情况。
条款21:
1.不要返回一些references指向某个local对象。
#include <iostream>
using namespace std;
class Rational
{
public:
Rational(int m = 0, int n = 0)
{}
~Rational()
{}
private:
int n, d;
/*
运算符重载形式有两种,重载为类的成员函数和重载为类的友元函数。
当运算符重载为类的成员函数时,函数的参数个数比原来的操作个数要少一个;
当重载为类的友元函数时,参数个数与原操作数个数相同。原因是重载为类的成员函数时,
如果某个对象使用重载了的成员函数,自身的数据可以直接访问,就不需要再放在参数表中进行传递,
少了的操作数就是该对象本身。而重载为友元函数时,友元函数对某个对象的数据进行操作,
就必须通过该对象的名称来进行,因此使用到的参数都要进行传递,操作数的个数就不会有变化。
*/
friend const Rational operator*(const Rational& lhs, const Rational& rhs)
{
Rational temp;
temp.n = lhs.n * rhs.n;
temp.d = lhs.d * rhs.d;
return temp;
}
/*
这里为什么不能返回 const Rational& 呢?引文 temp是一个local对象,而local对象在函数退出的时候就销毁了,因此,如果
这里返回const Rational&, 其实并没有返回reference指向某个Rational,它返回的reference指向一个"从前的"Rational,一个旧的
Rational,一个曾经被当做Rational但是现在已经成空壳的残骸,因为它在函数退出的时候已经被销毁了。
任何调用者甚至只是对此函数的返回值做任何一点点运用,都将立刻坠入"无定义行为"的恶地;
总结:
任何函数如果返回一个reference指向某个local对象,都将发生错误;
任何函数如果返回一个指针指向一个local对象,结果也是一样的。
*/
};
int main()
{
Rational a(1, 2);
Rational b(3, 5);
Rational c = a*b;
return 0;
}
2.不要返回references指向在heap内构建的对象。
const Rational& operator*(const Rational& lhs,const Rational& rhs)
{
Rational* result = new Rational(lhs.n*rhs.n, lhs.d*rhs.d);
return *result;
}
这样还是必须付出一个“构造函数调用”代价,因为分配所得的内存将以一个适当的构造函数完成初始化动作。此外,谁该对new出来的对象实施delete?
Rational w, x, y, z;
w = x*y*z;
这里使用了两次new,却没有合理的办法取得operator*返回的reference背后隐藏的那个指针,这绝对是资源泄露。3.不要返回references指向一个被定义于函数内部的static Rational对象。
局部静态变量:函数调用结束后不会消失,而保留原值,即其占用的存储单元不释放,在下一次该函数调用时,该变量保留上一次函数调用结束时的值。
const Rational& operator*(const Rational& lhs,const Rational& rhs)
{
static Rational result;
//...
return result;
}
这显然会造成多线程安全问题。(线程A的返回值是线程B的计算结果),更严重的问题如下:
bool operator==(const Rational& lhs, const Rational& rhs)
{}
Rational a,b,c,d;
if ((a*b) == (c*d))
{}
(a*b)==(c*d)会始终成立。4.必要时让函数返回一个新对象
inline const Rational operator*(const Rational& lhs,const Rational& rhs)
{
return Rational(lhs.n*rhs.n,lhs.d*rhs.d);
}
条款22:
1.将成员变量声明为非public,这样可以实现语法一致性,每样东西都是函数,都需要加小括号。
2.使用函数可以让你对成员变量的处理有更加精确的控制。如果成员变量为public,那么每个人都能读和写,但是如果通过函数读或写其值,那么就能实现“不准访问”、“只读访问”以及“读写访问”,甚至实现“惟写访问”。
3.封装:如果通过函数访问成员变量,日后可以用某个计算替换这个变量,这时class的客户却不知道内部实现已经变化。
例如,写一个自动测速的程序,汽车通过,其速度便填入一个速度收集器内:
class SpeedDataCollection{
……
public:
void addValue(int speed);//添加一笔新数据
double averageSoFar() const;//返回平均速度
……
};
实现函数averageSoFar。一种做法是在class内设计一个变量,记录至今以来所有速度 的平均值;当averageSoFar被调用,只需要返回那个成员变量就好。另一种做法是让averageSoFar每次被调用时重新计算平均值,这个函数有权限读取收集器内的每一笔速度值。上述第一种做法(随时保持平均值)会使每一个SpeedDataCollection对象变大,因为必须为用来存放目前平均值、累计总量、数据点数的每一个成员变量分配空间;但是这会使averageSoFar十分高效,它可以只是一个返回目前平均值的inline函数(条款30)。第二种做法,“每次被问询才计算平均值”会使得averageSoFar执行较慢,但是这时SpeedDataCollection对象占用空间比较小。
将成员变量隐藏在函数接口背后,可以为“所有可能的实现”提供弹性。
4.protected成员变量的封装性不比public高。某些东西的封装性与“当其内容改变时可能造成的代码破坏量”成反比。
取消一个public成员变量,所有使用它的客户码都会破坏。取消一个protected成员变量,所有使用它的继承类都会被破坏。
所以private提供封装,protected和public不提供封装。
条款23:
1.使用member和non-member哪一个好?
考虑一个class用来清除浏览器的一些记录,这个class中有清除告诉缓存区的函数,有清除访问过URLs的函数,还有清除cookies的函数:
class WebBrowser{
public:
……
void clearCash();
void clearHistory();
void removeCookies();
……
};
使用member函数如下:
class WebBrowser{
public:
……
void clearEverything()
{
clearCash();
clearHistory();
removeCookies();
}
……
};
使用non-member如下:void clearBrowser(WebBrowser& wb)
{
wb.clearCash();
wb.clearHistory();
wb.removeCookies();
};
使用non-member函数比较好。member函数带来的封装性比non-member函数低。此外,提供non-member函数可允许对WebBrowser相关机能有较大的包裹弹性,这可以降低编译相依度。2.。愈多东西被封装,欲少人可以看到它,我们就有愈大的弹性去改变它。愈少代码可以看到数据(访问数据),愈多数据可被封装,我们就更有自由来改变对象数据。愈多函数可以访问它,数据的封装性就愈低。
使用non-member和non-friend函数并不增加“能够访问class内之private成分”的函数数量。
2.friend函数对class private成员的访问权力和member函数相同,因此两者对封装的冲击力道也相同。
在C++中,比较自然的做法是让clearBrowser成为一个non-member函数并且位于WebBrowser所在的同一个namespace内。
namespace WebBrowserStuff{
class WebBrowser{……};
void clearBrowser(WebBrowser& we);
……
}
3.namespace可以跨越多个源码文件而classes不能。一个WebBrowser这样的函数可能拥有大量的便利函数,书签、打印、cookie管理等有关,大多数客户对其中某些感兴趣,于是为了降低编译相依关系,可以分离他们。
将书签相关便利函数声明于一个头文件,将cookie相关函数声明于另一个头文件,再将打印相关函数声明到第三个头文件……。
//头文件webbrowser.h,这个头文件针对class WebBrowser自身及WebBrowser核心机能
namespace WebBrowserStuff{
class WebBrowser{……};//核心机能
……//non-member函数
}
//头文件webbrowserbookmarks.h
namespace WebBrowserStuff{
……//与书签相关的便利函数
}
//头文件webbrowsercookies.h
namespace WebBrowserStuff{
……//与cookie相关的便利函数
}
标准库有数十个头文件(<vector>,<algorithm>,<memroy>等等),每个头文件声明std的某些机能。如果客户想使用vector相关机能,只需要#include<vector>即可。这也允许客户只对他们所用的那一小部分形成编译相依。条款24:
1.如果所有参数都需要类型转换,则为此采用non-member函数。
class Rational{
public:
Rational(int numerator = 0, int denominator = 1);//非explicit,允许隐式转换
int numerator() const; //分子访问函数
int denominator() const; //分母访问函数
private:
//...
};
如果要支持加减乘除等运算,这时重载运算符时是应该重载为member函数还是non-member函数呢,或者non-member friend函数?如果写成member函数
class Rational{
public:
……
const Rational operator*(const Rational& rhs);
……
};
如果进行混合运算:result=oneHalf*2;//正确,相当于oneHalf.operator*(2);
result=2*oneHalf;//错误,相当于2.operator*(oneHalf);
不能满足交换律。因为2不是Rational类型,不能作为左操作数。oneHalf*2会把2隐式转换为Rational类型。上面两种做法,第一种可以发生隐式转换,第二种却不可以,这是因为只有当参数被列于参数列(parameter list)内,这个参数才是隐式类型转换的合格参与者。第二种做法,还没到到”参数被列于参数列内“,2不是Rational类型,不会调用operator*。
result=operator*(2,oneHalf);
如果要支持混合运算,可以让operator*成为一个non-member函数,这样编译器可以在实参身上执行隐式类型转换。const Rational operator*(const Rational& lhs, const Rational& rhs);
这样就可以进行混合运算了(两个参数都在参数列内,都可以隐式转换)。那么还有一个问题就是,是否应该是operator*成为friend函数。如果可以通过public接口,来获取内部数据,那么可以不是friend函数,否则,如果读取private数据,那么要成为friend函数。这里还有一个重要结论:member函数的反面是non-member函数,不是friend函数。如果可以避免成为friend函数,那么最好避免,因为friend的封装低于非friend。条款25:
1.pimpl手法
class WidgetImpl{ //针对Widget数据而设计的class
public:
//……
private:
int a, b, c; //数据很多,复制意味时间很长
std::vector<double> b;
//……
};
class Widget{
public:
Widget(const Widget& rhs);
Widget& operator=(const Widget& rhs
{
…… //复制Widget时,复制WidgetImpl对象
*pImpl=*(ths.pImpl);
……
}
……
private:
WidgetImpl* pImpl;//指针,含有Widget的数据
};
2.将std::swap针对Widget特化:当Widgets被置换时,真正该做的是置换其内部的pImpl指针。namespace std{
template<> //这是std::swap针对T是Widget的特换版本,
void swap<Widget>(Widget& a, Widget& b) //目前还无法编译
{ //只需要置换指针
swap(a.pImpl, b.pImpl);
}
}
“template<>”表示它是std::swap的一个全特化版本。3.也许你已经发现了,上面的无法通过编译,因为pImpl是private成员,一般函数无法访问。我们可以这样处理:
class Widget{
public:
……
void swap(Widget& other)
{
using std::swap;//这个声明有必要
swap(pImpl, other.pImpl); //这样可以????
}
……
};
namespace std{
template<> //修订后的swap版本
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b); //调用其成员函数
}
}
是不是很奇怪上面other.pImpl。为什么在class Widget里面可以访问other对象的私有成员???下面测试程序确实不报错。
#include <iostream>
#include <vector>
using namespace std;
class WidgetImpl{ //针对Widget数据而设计的class
public:
private:
int a1, a2, a3; //数据很多,复制意味时间很长
std::vector<double> a4;
};
class Widget{
public:
Widget() :pImpl(new WidgetImpl){}
~Widget(){ delete pImpl; }
void swap(Widget& other)
{
using std::swap;//<span style="color:#ff0000;">这个声明有必要</span>
swap(pImpl, other.pImpl); //确实没报错
}
private:
WidgetImpl* pImpl;
};
namespace std{
template<> //修订后的swap版本
void swap<Widget>(Widget& a, Widget& b)
{
a.swap(b); //调用其成员函数
}
}
int main()
{
Widget w1, w2;
std::swap(w1,w2);
return 0;
}
注:这种做法不仅可以通过编译,还与STL容器有一致性,因为所有STL容器也都提供有public swap成员函数和std::swap特化版本(用以调用前者)。
4.如果是类模板,则特化会出问题。
template<typename T>
class WidgetImpl{……};
template<typename T>
class Widget{……};
在Widget内放个swap成员函数很简单,但是在特化std::swap时却有问题。
namespace std{
template<typename T>
void swap<Widget<T> >(Widget<T>& a,//不合法,错误
Widget<T>& b)
{
a.swap(b);
}
}
错误原因:我们企图偏特化一个function template(std::swap),但C++只允许对class template偏特化,在function template身上偏特化是不行的。
5.当打算偏特化一个function template时,惯常做法是简单地为它添加一个重载版:
namespace std{
template<typename T>//std::swap一个重载版本
void swap(Widget<T>& a,//swap后面没有<……>
Widget<T>& b)//这个也不合法
{
a.swap(b);
}
}
一般重载function template没有问题,但是std管理比较特殊。客户可以全特化std内的templates,但不能添加新的templates到std里面。
namespace WidgetStuff{
……//模板化的WidgetImpl等
template<typename T>//内含swap函数
class Widget{……};
……
template<typename T>
void swap(Widget<T>& a,//non-member,不属于std命名空间
Widget<T>& b)
{
a.swap(b);
}
}
现在我们没有加在std空间里面,这样就不是std::swap()的特化或者重载版本了。6.如果想让“class专属版”swap在尽可能多的语境下被调用,需要同时在该class所在命名空间内写一个non-member版本以及一个std::swap特化版本。
template<typename T>
void doSomething(T& obj1, T& obj2)
{
using std::swap;//令std::swap在此函数内可用
……
swap(obj1, obj2);//位T类型调用最佳版本swap
……
}
C++的名称查找法则(name lookup rules)确保找到global作用域或T所在命名空间内的任何T专属的swap。如果T是Widget并在命名空间WidgetStuff内,编译器或使用“实参取决之查找规则”(argument-dependent lookup)找到WidgetStuff内的swap,如果没有专属版的swap,那么会调用std内的swap(因为使用了using std::swap)。
不要使用
std::swap(obj1,obj2);
这样会让编译器只认std::swap函数。