好的接口容易被正确使用,不容易被误用。你应该在你的所有接口中努力达成这些性质。
“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁等等。
若希望将对象使用智能指针管理,在工厂中要返回智能指针,这样可以使用户不会忘记使用智能指针而导致内存问题。
class Duck{};
std::shared_ptr<Duck> createDuck(){}
使用shared_ptr避免cross-DLL-problem
cross-DLL-problem:对象在动态连接程序库(DLL)中被new创建,却在另一个DLL内被delete销毁。这样会导致运行期错误。使用shared_ptr缺省使用原来(new出对象的)的delete.
Class的设计就是type的设计。在定义一个新type之前,请确定你已经考虑过本条款覆盖的所有讨论主题。
设计高效classes需要考虑的问题:
1.新type的对象应如何被创建和销毁
2.对象的初始化和对象的赋值该有什么样的差别
3.新type的对象如果被passed by value,意味着什么?(copy构造函数定义了passed by value如何实现。)
4.什么是新type的合法值。(构造函数,赋值操作符setter函数需做错误检查。)
5.你的新type需要配合某个继承图系吗?(如果其他class继承你的class则你的析构函数需要声明为virtual,见条款7)
6.你的新type需要什么样的转换?(使用类型转换函数配合,见条款15 )
7.什么样的操作符和函数对此新type而言是合理的?
8.什么样的函数应该被驳回?(声明为private,见条款6)
9.谁该取用新type成员?(决定成员是public,private,protected。和哪个function或class是friend.)
10.什么是新type的”未声明接口”?
11.你的新type有多么一般化?(定义class template还是class)
12.你真的需要一个新的type吗?
尽量以pass-by-reference-to-const替换pass-by-value。前者通常比较高效,并可避免切割问题。
以上规则并不适用于内置类型,以及STL的迭代器和函数对象。对他们而言,pass-by-value往往比较适当。
缺省情况下C++以by value方式传递对象至函数,调用端所获得的亦是函数返回值的一个附件,这样做会调用copy构造函数和析构函数,产生费时的操作。
使用by reference可防止调用copy构造函数和析构函数,使用const可防止被修改。
切割问题:
当一个derived class对象以by value方式传递并被视为一个base class对象,base class的copy构造函数就会被调用,造成derived所添加的功能被切割。
#include <iostream>
class Base{
public:
Base(){
printf("Base constructor\n");
}
Base(const Base& rhs){
printf("Base copy\n");
}
virtual ~Base(){
printf("Base DEL\n");
}
virtual void display() const{
printf("Base\n");
}
};
class Derived: public Base{
public:
Derived(){
printf("Derived constructor\n");
}
Derived(const Derived& rhs)
:Base(rhs)
{
printf("Derived copy\n");
}
~Derived(){
printf("Derived DEL\n");
}
virtual void display() const{
printf("Derived\n");
}
};
void function(Base c){
//切割问题,pass-by-value的效率问题
//输出Base copy
c.display();//输出Base,切割问题
//输出Base DEL
}
int main(int argc, const char * argv[]) {
Derived d; //输出Base constructor、Derived constructor
function(d); //输出Base copy、Base、Base DEL
return 0; //输出Derived DEL、Base DEL
}
输出:
Base constructor
Derived constructor
Base copy
Base
Base DEL
Derived DEL
Base DEL
将function修改为pass-by-reference-to-const后
void function(const Base& c){ c.display();//输出Derived } 输出 Base constructor //main中Derived d;语句输出的
Derived constructor //main中Derived d;语句输出的
Derived //function输出的
Derived DEL //main结束输出的
Base DEL //main结束输出的
调用function并没有使用copy构造函数和析构函数,并解决了切割问题。
一般而言pass-by-value并不昂贵的唯一对象就是内置类型和STL的迭代器和函数对象。
绝不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回pointer或reference指向一个local static对象而有可能同时需要多个这样的对象。条款4已经为”在单线程环境中合理返回reference指向一个local static对象”提供了一份设计实例。
返回一个指向stack空间的reference,当离开作用域后reference将指向一个被删除了的对象。造成未定义结果。
若函数使用new在heap上创建对象并以reference方式返回。则会造成内存泄露,因为谁new了对象谁就delete他,但是用这种方式我们并没有合理的办法delete该reference。
切记将成员变量声明为private。这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并提供class作者以充分的实现弹性。
protected并不比public更具封装性。
为保持语法一致性。我们应该使public接口内每样东西都是函数,客户访问member data的唯一方式是通过public function。
使用private成员变量可以实现访问控制,例如读写访问,错误输入筛选。
使用private成员变量可以实现良好的封装性。
宁可拿non-member、non-friend函数替换member函数。这样做可以增加封装性、包裹弹性和机能扩充性。
若我们需要一个便利函数,他的作用是调用类中其他的函数来完成自己的任务,那么我们需要将该还是设计为non-member还是member?
例如下面的例子,我们需要在浏览器类中提供一个便利函数以清除数据。
class WebBrowser{ public: void clearCache(); void clearHistory(); void removeCookies(); //方法一:使用member函数 void clearEverything(){ clearCache(); clearHistory(); removeCookies(); } }; //方法二:使用non-member函数 void clearBrowser(WebBrowser& wb){ wb.clearCache(); wb.clearHistory(); wb.removeCookies(); }
结果是使用方法二更好,下面将论证:
面向对象的守则要求数据应该尽可能的封装,member函数带来的封装性比non-member函数低。在对象内的数据,越少的代码可以访问他,那么封装性将会越高。所以应该尽可能的降低访问对象内数据的代码。如member函数。
而使用很多non-member便利函数将会带来代码混乱,使我们不清楚哪个函数匹配哪一个类,解决方法是使用namespace。
//头文件WebBrowser.h
namespace WebBrowserStuff {
class WebBrowser{
public:
void clearCache();
void clearHistory();
void removeCookies();
};
void clearBrowser(WebBrowser& wb){
wb.clearCache();
wb.clearHistory();
wb.removeCookies();
}
}
//头文件WebBrowserBookmarks.h
namespace WebBrowserStuff {
//与书签相关的便利函数
}
使用namespace不仅可以清晰的使用non-member函数,还可以在不同的文件中声明同一namespace下的其他non-member函数。这也是c++标准程序库使用的组织方式,在数十个头文件(vector,memory等)中声明namespace std内的不同功能。从而降低编译依存性(客户只对他们所用的一小部分系统形成编译相依),并使客户可以自定义一些功能到namespace中, 而member函数并不能这样。
使用non-member函数可以提供封装性,包裹弹性,机能扩充性。
如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个non-member.
一个有理数类,有分子和分母两个成员变量,我们希望通过operator*实现一个有理数类对象与一个int类型相乘,得到正确的结果。我们需要将int转换为有理数类在调用operator*。现考虑使用类内声明的operator*。
#include <iostream>
class Rational{
public:
//non-explicit的构造函数
Rational(int numerator = 0,int denominator = 1):
_numerator(numerator),
_denominator(denominator)
{}
//getter函数
int numerator() const{
return _numerator;
}
int denominator() const{
return _denominator;
}
//member方式的operator
const Rational operator*(const Rational& rhs) const{
return Rational(this->numerator()*rhs.numerator(),
this->denominator()*rhs.denominator()
);
}
private:
int _numerator;//分子
int _denominator;//分母
};
int main(int argc, const char * argv[]) {
Rational onHalf(1,2);
Rational result = onHalf * 2;//OK
result = 2 * onHalf;//ERREOR:Invalid operands to binary expression ('int' and 'Rational')
return 0;
}
我们通过测试可以看出onHalf * 2可以得到正确的结果。但是2 * onHalf缺导致了error。其原因是2 * onHalf将会被转换为:
2.operator*(oneHalf);
2并没转换为Rational类型,也没有operator*函数。
当函数中的参数需要进行类型转换时,声明为member函数会导致一些情况下的错误。解决方法是声明为non-member函数。
const Rational operator*(const Rational& lhs,const Rational& rhs){
return Rational(lhs.numerator()*rhs.numerator(),
lhs.denominator()*rhs.denominator()
);
}
int main(int argc, const char * argv[]) {
Rational onHalf(1,2);
Rational result = onHalf * 2;//OK
result = 2 * onHalf;//OK
return 0;
}
当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常。
如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于class(而非templates),也请特化std::swap
调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何”命名空间资格修饰”。
为”用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
如果swap缺省实现码对你的class或class template提供可接受的效率,你不需要额外做任何事。
如果swap缺省版本的效率不足,几乎因为使用了pimpl手法(pimpl:pointer to implementation),试着做以下事情:
1.提供一个public swap成员函数,让他高效的置换你的类型的两个对象值。
2.在你的class或template所在的命名空间内提供一个non-member swap,并令他调用上述swap成员函数。
3.如果你正编写一个class(非class temple),为你的class特化std::swap。并令它调用你的swap成员函数。
4.如果你调用swap,请确定包含一个using声明式,以便让std::swap在你的函数内曝光可见,然后不加任何namespace修饰符,赤裸裸地调用swap。
并注意成员版swap绝不要抛出异常,因为许多线程安全是由此保证的。
#include <iostream>
class Data{
public:
Data(int data):
_data(data)
{}
void serData(int data){ _data = data; }
int getData(){ return _data; }
private:
int _data;
};
namespace WidgetStuff {
template<typename T>
class Widget{
public:
Widget(Data* data = new Data(100) ,T Tdata = nullptr):
_data(data),
_Tdata(Tdata)
{}
void swap(Widget& other){
printf("swap\n");
using std::swap;
swap(_data, other._data);
swap(_Tdata, other._Tdata);
}
T getTdata(){
return _Tdata;
}
int getData(){
return _data->getData();
}
private:
T _Tdata;
Data* _data;
};
//c++的名称查找法则确保将找到global作用域或T所在之命名空间内的任何T专属的swap
template<typename T>
void swap(Widget<T>& a,Widget<T>& b){
a.swap(b);
}
}
int main(int argc, const char * argv[]) {
WidgetStuff::Widget<double> a(new Data(200),205.5);
WidgetStuff::Widget<double> b(new Data(100),101.5);
//使用std的swap,为T类型对象调用最佳swap版本。但是并不能以std::swap方式调用,这样会强迫使用std的swap
using std::swap;
swap(a, b);
printf("a.data = %d,a.Tdata = %f\nb.data = %d,b.Tdata = %f\n",a.getData(),a.getTdata(),b.getData(),b.getTdata());
return 0;
}
输出:
swap
a.data = 100,a.Tdata = 101.500000
b.data = 200,b.Tdata = 205.500000