术语
defalut构造函数一般定义成explicit,防止编译器做一些预期外的转换
class B{ public: explicit B(int x=0,bool b=true); }; void doSomething(class B); B b1; doSomething(b1); B b2(28); doSomething(28);//此处如果没有将构造函数声明为explicit,那编译器将用这个28去构造一个B传给doSomething,这就是预期之外的,由于声明了explicit此处就会报错。
条款1:尽量以const,enum,inline替换#define
因为#define不被视为语言的一部分,也许在某些情况下编译器从来不会见到#define 后面的东西
- 尽量用常量去替换#defines.
一个例子是常量指针,如#define name “rouze”可以替换为 const char* const name = “rouze”;或者用STL的const string name(“rouze”);
再者就是class的专属常量,其实#define是无法定义一个class的专属常量的,因为#define出来的东西对该条语句后面的所有语句有效,无法提供私有性。要想使它专属那就要放在private里面,要想只留一份就要static,如果是常量还要加const,类似
class obj{ private: static const int num=5; };
上面这种份代码是一份声明,放在头文件当中,但即使是static成员在旧编译器也是不允许的,因此需要在实现文件里赋值const obj::num=5;
“enum hack”–enum类型在定义出来后可以当作一个整型常量
class Player{ private: static const int NumTurns=10; int score[NumTurns]; }; //如果编译器不支持类中初始化上述定义会报错,但又实在需要NumTurns为一个确定的大小才能通过编译使用enum如下 class Player{ private: enum {NumTurns=10}; int score[NumTurns]; }; //上述定义就能通过编译
综上所述,尽量用const或者enum替换掉#defines,尽量用inline替换形似函数的宏
条款2: 尽可能用const
const的作用是允许指定一个“约束”,编译器会强制实施这个约束,const 在不同场景有着不同的作用,主要讲一下在member funciton 的
class TextBlock{
public:
const char& operator[]const{}
char& operator[]{}
};
//注意上面函数中两个const 各自的意义
编译器强制实施的是bitwise-constness,但在编程过程当中可以通过与const对应的mutable实现conceptual constness
当const和non-const版本的实现等价只有返回不同时应在non-const版本里调用const版本来避免代码重复
条款3:确定初始化
条款4:如果不想编译器自动生成函数,应该把相应的成员函数声明为private并且不予实现
默认生成的copy assignment运算会自动调用所有成员对象的copy assignment运算完成复制
默认的析构函数会自动调用所有的non-static成员的析构函数进行析构
//版本一 class BankAcount{ private: int num; BankAcount& operator=(const BankAcount&); BankAcount(BankAcount&); }; //当复制操作被外部调用时编译会发生错误表示不可调用,当内部函数或者friend函数调用复制操作时会发生链接错误,因为没有函数的实现 //版本二通过继承一个不可复制的类来阻止复制操作 class Uncopyable{ public: Uncopyable(){} ~Uncopyable(){} private: Uncopyable& operator=(const Uncopyable&); Uncopyable(const Uncopyable); }; class BankAcount:private Uncopyable{ ... }; //相比版本一好处就是错误会被放到编译时期来,因为当BankAcount对象在进行复制行为时势必会调用Uncopyable的复制函数(除非你写的BankAcount是错误的在copy操作没有考虑基类成员,这有悖于条款11)
条款5:带多态性质的基类应该声明一个virtual析构函数,如果一个类设计出来不是为了作为base class,那就不应该声明virtual析构,因为virtual的声明会增大类的空间
条款6:析构函数不要吐出异常
如果析构函数吐出异常可能导致其后需要的析构无法进行
条款10: operator=应该返回一个reference to *this
条款11:operator=应该处理好自我赋值安全与异常安全
//通过精心安排的语句能避免一些自我赋值与异常处理
class Widget{
private:
Bitmap *pb;
public:
...
};
//错误版本一,如果pb等于rhs.pb即自我赋值的情况,delete pb之后rhs.pb将指向一块删除的地方,这样就会产生错误
Widget& Widget::operator=(const Widget& rhs){
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
//版本二,解决了自我赋值,但在new 操作错误时pb将指向错误的地方
Widget& Widget::operator=(const Widget& rhs){
if(pb == rhs.pb)
return *this;
delete pb;
pb = new Bitmap(*rhs.pb);
return *this;
}
//正确版本
Widget& Widget::operator=(const Widget& rhs)
{
//主要思想就是在复制好原本的资源之前不要delete掉那个指针
Bitmap * pOrig=pb;
pb = new Bitmap(*rhs.pb);
delete pOrig;
return *this
}
条款12:copying操作要记得copy处理好每一个成员变量以及“base class”成分
class Base{
private:
int base;
public:
Base& operator=(const Base&rhs);
};
class Derived:public Base{
private:
int derive;
public:
Derived& operator=(const Derived&rhs){
derive = rhs.derive;
return *this;
}
};
//Base部分的成员变量没有被copy
//正确版本
Derived& Derived::operator=(const Derived&rhs){
derive = rhs.derive;
Base::operator=(rhs);
return *this;
}
**另外copy构造函数与copy assignment函数可能代码很大部分一样,但不要在一个copying 函数里面去调用另一个copying函数来实现自己,好的方法是把共同的代码单独写成一个函数供两者调用**
==资源管理==
条款13:用对象管理资源
- 为了防止资源泄露,最好使用RALL对象来管理资源,这样可以通过C++对象的析构函数去自动释放资源
- 常见的RALL class有auto_ptr(其复制行为有点诡异,当赋值发生时被赋值的对象拥有资源的管理权,而另一个将被置为null),tr1::shared_ptr通过持续的追踪有多少对象指向某笔资源来确定是否释放该资源,因此赋值正常
- 这两个对象在释放资源时都是进行的delete行为而非delete[],因此不要将数组指针交给这两个对象管理
条款14:在资源管理类中小心处理copying行为
如果想要禁止copying行为可以参考条款6将copying函数声明为私有并且不予实现
如果想表现出RCSP的特性可以将底层的资源用tr1::shared_ptr控制,并且其可以指定指定删除行为,缺省时表现为释放资源*std::tr1::shared_ptr
<typename>
obj(typename,function)*,其中的function就是自己定义的函数class Lock{ public: explicit Lock(Mutex* pm): mutexPtr(pm,unlock){ lock(mutexPtr.get()); } private: std::tr1::shared_ptr<Mutex> mutexPtr; }; //将普通的Mutex换成shared_ptr管理,可以表现出RCSP的特性,注意没有写析构函数是因为默认的析构函数会调用non-static的析构函数,而shared_ptr的析构会去执行deleter的行为即指定的unl
条款15:资源管理类要提供好对原始资源的访问
资源管理类并不是为了封装而存在,它只是为了确保“资源释放”这个特殊行为会发生而存在,因此它做的是隐藏客户不需要看的部分,但要备足客户需要的所有东
//当把一个指针交给一个资源管理对象如auto_ptr时,很多函数接口需要的还是原始的指针类型,如果直接将auto_ptr传给函数会发生编译错误,因此资源管理对象就需要提供相应的类型转换
//方法一显示转换
class Manage{
private:
Something s;
public:
explicit Manage(Something x):
s(x)
{
...
}
~Manage(){
...
}
Something get()const{
return s;
}
};//通过Manage对象调用get()返回内部资源
//方法二隐式转换
class Manage{
public:
...
operator Something()const{
return s;
}
};//在需要类型转换时编译器会自动调用转换函数,这样对客户来说比较轻松,但这样可能就会隐藏危险
条款17:用独立的语句将new对象置入智能指针
考虑下面这样的代码
int priority();
void process(std::auto_ptr<Widget> pw,pty);
//以下方式调用process函数
process(new Widget,priority());
//首先会报错,因为智能指针的初始化是显式的
process(std::auto_ptr<Widget>(new Widget),priority());
//乍看没问题,但可能造成资源泄露,在调用process之前编译器知道要先做下面几件事
/*1.执行new Widget
2.执行 priority()
3. 执行auto_ptr的构造函数*/
//但只能确定new Widget在auto_ptr之前发生,priority()的执行是无法确定的,如果恰好是按上述顺序,那在priority()出错时new出来的对象将无法放到智能指针里面去
//正确形式
std::auto_ptr<Widget> pw(new Widget);
process(pw,priority());
==声明与设计==
条款25:当需要自己写一个swap函数时先确定其不要抛出异常,同时提供一个non-member的版本调用该swap函数,此non-member可以放在class的同一命名空间内,最后可以对std::swap进行全特化但不能往std内加入全新的东西
//假设有一个类,它只管理一个对象指针,定义如下
class WidgetImpl{
public:
...
private:
std::vector<int> v;
...
};
class Widget{
public:
...
Widget&operator=(const Widget&rhs){
...
*pImpl = *(rhs.pImpl);//这里是通过WidgetImpl对象的operator=完成一个深拷贝而不是直接pImpl=rhs.pImpl
...
}
private:
WidgetImpl* pImpl;
};
//如果直接交换两个,std::swap只是最简单的交换
namespace std{
template<typename T>
void swap(T&a,T&b){
T temp(a);
a = b;
b = tmp;
}
}
//如果调用std::swap交换那swap会调用最底层的WidgetImpl的operator=,会有ector拷贝的操作,而实际中我们只需要交换两个Widget管理的指针即可,所以有了特化的std::swap
namespace std{
template<>
void swap<Widget>(Wiget&lhs,Widget&rhs){
//由于成员是private,所以应该定义一个swap成员函数来对指针进行操作,而这里只是进行一个调用
lhs.swap(rhs);
}
}
//这样在使用std::swap时编译器会自动找到该版本进行相应的特化操作
/*后面还有如果类是一个模板类需要注意的地方,以后再写吧-_-*/
==实现==
条款26:尽量延后变量定义的出现时间,以增加程序清晰度和改善效率
最好就是在得到能够初始化这个变量的数据出现时定义该变量,一方面可读性较高另一方面也能减少一些初始化引起的问题
条款27:尽量少做转型动作
C++提供的四种新转型方式
const_cast
<T>
(expression)–>用来移除常量性,也是唯一有这个能力的转型操作符dynamic_cast
<T>
(expression)reinterpret_cast
<T>
(expression)static_cast
<T>
(expression)
类型转换并不是简单的一个类似声明一样的东西,任何一个类型转换往往令编译器编译出运行期执行的代码
class Base1{...};
class Base2{...};
class Derived:public Base1,public Base2{...};
Derived d;
Base1* p1 = &d;
Base2* p2 = &d;
Derived* p3 = &d;
//将d的地址转型成Base1的指针,只有编译器知道在继承过程当中属于Base1的那个部分在d的哪里也就是有一个偏移量需要调整,因此p1,p2,p3很可能不同,这就是转型d
条款29: 尽量避免返回对象的handles
当对象成员的handles(reference,pointer,迭代器等)被成员函数返回,这会破坏封装性,即使返回的是const reference,当对象内部的成员被销毁时,这个留在外面的handle就会成为一个空掉着的handle。但并不代表这是一定不行的,像string的operator[]就需要返回内部字符的reference,以及各种迭代器都需要这样。
条款30:inling的里里外外
inline函数除了能免除函数调用的成本,简单的inlined函数还有可能被编译器优化成更短的目标码,这样有较高的指令高速缓存装置击中率。
但是,如果对一个程序库来说,一个inline 函数f,如果设计者改变了f,那么客户中所有使用了f函数的地方都要重新编译,而如果没有inline只需要重新连接一下,另外不是说某些看起来很简单的代码就一定可以inline,例子如下
class Base{
public:
...
private:
std::string b1,b2;
};
class Derived :public Base{
public:
Derived(){}
private:
std::string d1,d2;
};
//看上去默认构造函数是一个绝佳的inline对象,但在实际的编译之后为了保证程序的正常运行,编译器会添加一些代码,其可能的样子如下
Derived(){
Base::Base();
try{d1.std::string::string();}
catch(...){
Base::~Base();
throw;
}
try{d2.std::string::string()}
catch(...){
Base::~Base();
throw;
}
...
}
//Derive函数先构造好继承的成分,之后再尝试构造自己的成员,如果出现异常就释放已经构造的资源
//上述代码只是简单的描述了一下编译器可能的操作,实际可能更加精致复杂,如果说将所有的构造函数都inlined,那Derive函数将有四份string构造函数的代码(Base成分那里两个再加上自己的两个),如果情况再稍微复杂一些构造函数的代码就可能非常巨大,不再适合inline
条款31:将文件间的编译依存将至最低
C++并没有把“接口从实现中分离”这件事做得很好,Class的定义式不仅描述了接口,还有实现细节,如下面代码所示
class Person{
//各种接口
public:
Person(const std::string&,const Date&,const Address&);
std::string name()const;
std::string birthDate()const;
std::string Address()const;
private:
//下面就是所说的实现细节
std::string theName;
Date theBirthDate;
Address theAddress;
};
首先上面的代码是无法通过编译的,因为Date和Address是为定义的类型,所以前面常会有include"date.h”这样的东西,其次Person的定义文件和每一个含入了Person class的文件就形成了一种编译依存关系,任何一个头文件的改变都将引起其它文件的的重新编译
解决方案:
- 使用Pimpl技法,将Person分割成为两个classes,一个只提供接口,另一个负责实现接口,Person的定义将如下:
#include<string>
#include<memory>//shared_ptr所在库
//实现接口的类
class PersonImpl;
//将Date与Address作为前置声明而非直接#include相应头文件这样可以减少由于#include引进的依存
//因为现在的Person只提供接口,需要知道的只是Date与Address这两种类型而非细节,即在Person的定义中Date与Address要么是函数返回类型用到要么是函数参数类型用到,这两种情况下编译都是可以通过的
//但这里无法去实现Person的成员函数
class Date;
class Address;
class Person{
//各种接口
public:
Person(const std::string&,const Date&,const Address&);
std::string name()const;
std::string birthDate()const;
std::string Address()const;
private:
//下面是一个指向实物的指针而非具体细节
std::tr1::shared_ptr<PersonImpl> PImpl;
};
现在来看,Person的使用者就完全不需要知道Person的实现细节了,那些关于Person实现的任何修改也不再需要Person的使用者重新编译了,因为从头到尾Person的使用者只是在通过一个指针来进行各种操作,并不能写出依赖于Person细节实现的代码,也就没有了这层编译依赖
条款33:避免遮掩继承来的名字
“名称遮掩规则”在类的继承中仍然起作用,想要改变可以使用using
class Base{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
void mf3();
void mf3(int);
};
class Derived :public Base{
public:
virtual void mf1();
void mf3();
void mf4();
};
Derived d;
d.mf1();//right
d.mf1(1);//wrong
d.mf3();//right
d.mf3(1);//wrong
//继承中的名字会覆盖掉所有继承而来的同名函数就像内部的同名变量会掩盖外部的同名变量,本质的原因是*作用域的嵌套*
class Derived :public Base{
public:
using Base::mf1;
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
};
//用using解决该问题
条款37:绝不重新定义继承来的non-virtual函数
继承而来的non-virtual函数指定了接口继承以及一份强制性实现,因此non-virtual函数不应该被重写,如果想要表现特异性凌驾于其不变性之上,那该函数就应该声明为virtual
条款38:绝不重新定义继承而来的缺省参数值
首先能重新定义的是virtual函数,因此本条款是针对于virtual函数的
class Shape{
public:
enum ShapeColor{Red,Black,Green};
virtual void draw(ShapeColor color=Red);
};
class Circle:public Shape{
public:
virtual void draw(ShapeColor color=Green);
};
Shape *pc = new Circle;
pc->draw();
//上面代码中pc的静态类型是Shape*,不管pc指向什么pc的静态类型都是Shape*,但此刻pc的动态类型为Circle*,由于draw函数是动态调用的,所以会调用Circle的draw,但缺省的参数值是静态绑定的,使用的是Shape类的Red,最终结果就会是这样:
pc->Circle::draw(Shape::Red);//这是诡异的状态
条款42:了解typename的双重意义
typename与class在作为template参数时对于C++的意义是完全一样的
- 在模板当中出现的名称如果依赖于模板参数,则这个名称被称为依赖名称(dependent names)
template<typename T>
void f(const T& container){
T::iterator iter(container.begin());
//typename T::iterator iter(container.begin());
int x;
}
//iter这个名称的定义需要依赖于模板参数T,因为编译器不知道T::iterator是一个什么,如果T::iterator是一个静态变量那结果就会跟我们想的完全不同,因此需要在这一行加上typename告诉编译器iterator是一个类型,即typename还能用来修饰从属性名称,但这种修饰不能在继承列表和初始化列表出现
- 以下是通过传入的迭代器来备份某个对象的函数
template<typename iterT>
void CopyWithIter(iterT iter){
typename std::iterator_traits<iterT>::value_type tmp(*iter);
}
//std::iterator_traits<iterT>::value_type,这句话是库通过iterT找到它所指向的对象,value_type就是这个对象类型,去翻stl的源码会发现每个容器的定义前面总会有一堆typedefs,其中就有一个value_type,这是STL的Traits技术,或者说一种规范,更详细的内容可参考条款47)
条款49:了解new-handler的行为
当operator new抛出异常之前,会先调用一个客户指定的错误处理函数,叫new_handler(是一个typedef,typedef void (*new_handler)())直到内存够用或返回一个null使得operator new 抛出异常
void outOfmem(){
//如果在处理函数中又有new操作且失败那又会反复调用该函数,形成无限递归一样的效果
std::cerr<<"Unable to new"<<'\n';
std::abort();
}
int main()
{
std::set_new_handler(outOfmem);//set_new_handler使用户指定自己的错误处理函数
new int[100000000000000000];
}
一个设计良好的new_handler应该可以做到以下条件中的一些:
- 让更多的内存可以被使用,如果系统的operator new失败,但new_handler可以分配出更多内存那下一次new可能就成功了,做法之一就是先申请一块大内存,当new_handler被调用时将它们一点一点归还给系统
- 安装另一个new_handler。如果当前的new_handler无法获取更多内存但它知道某个new_handler有这个能力那当前的new_handler可以调用set_new_handler替换自己,那下次调用就是最新的new_handler。
- 卸除new_handler,将null指针传给set_new_handler,这样operator new会抛出异常
- 抛出bad_alloc异常,这样的异常不会被operator new捕获,因此会传至内存申请处。
- 直接调用abort或exit
如果想为一个class制定特殊的set_new_handler则其部分声明应该如下
class Widget{
public:
static std::set_new_handler(std::new_handler p)throw();
static void* operator new(std::size_t size)throw(std::bad_alloc);
private:
std::new_handler currentHandler;
};
Widget::set_new_handler(std::new_handler p)throw(){
std::new_handler oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
但是为了确保global handler总是能被安装回去(因为Widget类在调用set_new_handler时会用自己的handler去替换掉之前的global handler,而如果替换后操作失败就无法再将之前的global handler安装回去了)使用资源管理对象管理global handler
class NewHandlerHolder{
public:
explicit NewHandlerHolder (std::new_handler nh):handler(nh){}//这个nh就是被管理的global handler
~NewHandlerHolder(){std::set_new_handler(hanlder);}//保证之前的global handler能被安装回去
private:
std::new_handler handler;
NewHandlerHolder(const NewHandlerHolder&);
NewHandlerHolder& operator=(const NewHandlerHolder&);
};
//下面客户Widget会这样定义其operator new
void * Widget::operator new(std::size_t size){
NewHanlderHolder h(std::set_new_handler(currentHandler));//先将自己的处理函数currentHandler装上去
return ::operator new(size);//如果这里失败h会被析构之前的global handler会自动装回去
};
//Widget的客户会这样使用new_handling
void outOfMem();
Widget::set_new_handler(outOfMem); //用户将自己定义的处理函数作为Widget对象分配失败的处理函数
Widget* pw1 = new Widget; //如果失败将调用outOfMem
std::string *ps = new std::string;//如果分配失败会调用global handler(如果有的话)
Widget::set_new_handler(0); //设定Widget 的专属new_handling
Widget* pw2 = new Widget; //分配失败直接报错
上述方案的实现不会因为类的不同而不同,于是一个将这部分功能单独抽离出来作为一个基类的想法就出现了,先上代码
template<typename T>
class NewHandlerSupport{
public:
static std::new_handler set_new_handler(std::new_handler p)throw();
static void*operator new(std::size_t size)(throw std::bad_alloc);
private:
static std::new_handler currentHandler;
};
template<typename T>
std::new_handler
NewHandlerSupport<T>::set_new_handler(std::new_handler p)throw()
{
std:new_hanlder oldHandler = currentHandler;
currentHandler = p;
return oldHandler;
}
template<typename T>
void * operator new(std::size_t size)throw(std::bad_alloc){
NewHanlderHolder h(std::set_new_handler(currentHandler));
return ::operator new(size);
}
//将每一个currentHandler初始化为null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler = 0;
//现在Widget只需要继承即可
class Widget : public NewHandlerSupport<Widget>{
};
有两个点说一下,首先为什么这里要使用模板,因为在NewHandlerSupport这个类中其实没有用到T的地方。
原因就是这个类是为了让不同的类继承并拥有各自的new_handler,因为currentHandler是一个静态的变量,
如果不适用模板那所有子类拥有的是同一份currentHandler。使用模板后不同的类就会产生一个不同的模板,使得它们拥有实体互异的currentHandler
其次就是这里提到的minxin风格,简而言之就是一种通过模板来达到多重继承效果的手法,下面是一个简单的例子
//这种风格的base class一般是这样的写法
template<typename T>
class A:public T{
};
//假设有一个struct,它管理一个数据,其实可以写成一个template,这里为了简单直接设定为一个int
struct Number{
int x;
typedef int value_type;
void set(int n){
x = n;
}
int get()const{
return x;
}
};
//在此基础上实现一个可以撤回一次操作的数
template<typename BASE,typename T=BASE::value_type>
struct Undoable:public BASE{
typedef T value_type;
T before;
void set(int n){
before = BASE::get();
BASE::set(n);
}
void undo(){
BASE::set(before);
}
};
//在此基础上实现一个可以再做一次的操作的数据
template<typename BASE,typename T=BASE::value_type>
struct Redoable:public BASE{
typedef T value_type;
T after;
void set(int n){
after = BASE::get();
BASE::set(n);
}
void redo(){
BASE::set(after);
}
};
//现在如果想要实现一个既可以撤回又可以重做一次的数就可以通过模板组合的方式实现
typedef Redoable<Undoable<Number>> Mynum;
int main(){
Mynum num1;
num1.set(10);
num1.set(29);
num1.undo();
cout<<num1.get();
return 0;
}
转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 2128099421@qq.com