1、C++由四个次语言组成:c、c++类对象、template C++、STL
c++高效编程守则视状况而变化,取决于使用c++的哪一部分。
2、尽量以const、enum、inline替换#define
a、常量定义式通常被放在头文件中(以便被不同的源码包含进去),如果想将常量指针(即指向常量的指针)放入头文
件中,必须指定指针也是const的,这就需要写const两次,例如:const char * const author="musun";
通常string对象比char *-based合适。即const std::string author("musun");
b、class的专属常量:为了将常量的作用域限制于class内,须让它成为class的一个成员,而为确保此常量至多只有一份
实体,必须让它成为一个static成员。
class GamePlayer{
private:
static const int NumTurns=5;//常量定义式
int scores[NumTurns];//使用该常量
}
无法利用#define创建一个class专属常量,因为#define不重视域,不能被封装,一旦宏被定义,就在其后的编译过
程中有效。
c、“the enum hack”:当不想让别人获得一个pointer或reference指向某个整数常量,enum可以实现这个约束。
d、宏函数方面:用inline函数替换
最后:对于单纯常量,最好以const对象或enum替换#define
对于形似函数的宏,最好改用inline函数替换#define
3、尽可能使用const
a、const char * p <==> char const * p ;//都表示p指向的值不可变
char * const p;//表示p不可指向不同的东西
b、在迭代器中:
const vector<int>::iterator iter; <==> T* const p,即iter不可变
vector<int>::const_iterator cIter;<==> const T * p,即cIter指向的值不可变
c、令函数返回一个常量值,使用const参数 例如:
const Rational operator*(const Rational& lhs,const Rational& rhs);
d、const成员函数:一是接口容易理解,二可以使“操作const对象”成为可能
两个成员函数如果只是常量性不同,也够成重载,例如:
class test{
void print() const
{
_text="abc";//如果_text不声明为mutable,会报错
cout<<"print const"<<endl;
}
void print()
{
cout<<"print"<<endl;
}
mutable string _text;
};
在const成员函数中,需要对成员变量做修改时,可将成员变量声明为mutable。
e、在const和non-const成员函数中避免重复:
运用const函数实现其非const版本的技术,反过来,用non-const实现const则是不安全的
例如:
const char& operator[](size_t position) const
{
...
}
char& operator[](size_t position)
{
return const_cast<char&>( //将op[]返回值的const转除
static_cast<const T&>(*this) //为*this加上const
[position] //调用const op[]
);
}
最后:多使用const,当const和non-const成员函数有着实质等价的实现时,令non-const版本调用const版本可避免重复
。
4、确定对象被使用前已先被初始化:
a、明确初始化和赋值
class ABEntry
{
public:
ABEntry(const string& name,const string& address,const list<PhoneNumber>& phones);
private:
string theName;
string theAddress;
list<PhoneNumber> thePhones;
int numTimesConsulted;
};
ABEntry(const string& name,const string& address,const list<PhoneNumber>& phones)
{
theName=name; //这些都是赋值
theAddress=address; //而非初始化
thePhones=phones;
numTimesConsulted=0;
}
对象的成员变量的初始化动作发生在进入构造函数本体之前,发生于这些成员的default构造函数被自动调用之时(对内
置类型不为真)。最好使用初始化列表形式。对于内置类型,成员变量是const或references时,就一定需要初始化列表。
c++成员初始化次序:根据成员变量声明的次序来被初始化。
b、不同编译单元内定义之non-local static 对象(某编译单元内的某个non-local static对象的初始化用到了另一个编
译单元内的某个non-static对象):
编译单元:是指产出单一目标文件的那些源码,基本上是单一源码文件加上其所含入的头文件。
除函数内的static对象外,都叫non-local static对象。
例如:
class FileSystem{...};
FileSystem& tfs()
{
static FileSystem fs;
return fs;
}
(1)
Directory::Directory(params)
{
...
std::size_t disks=tfs.numDisks();//依赖tfs先被初始化
...
}
(2)
class Directory{...};
Directory::Directory(params)
{
...
std::size_t disks=tfs().numDisks();
...
}
第二种可以有效解决跨编译单元初始化次序问题
最后:为内置型对象进行手工初始化,因为c++不保证初始化它们。
构造函数使用初始化列表。
为免除“跨编译单元之初始化次序”问题,以local static对象替换non-local static对象。
5、了解c++默认编写并调用哪些函数
类中如果自己没有声明,编译器默认会声明一个copy构造函数,一个copy赋值操作符,一个default构造函数和一个析构
函数。这些函数都是public且为inline。惟有当这些函数被需要(被调用)才会被编译器创建出来。
6、若不想使用编译器自动生成的函数,就应该明确拒绝。
可将相应的成员函数声明为private且不为实现。
也可以使用像uncopyable这样的基类,也是一种做法。
class Uncopyable{
protected:
Uncopyable(){} //允许子类对象构造和析构
~Uncopyable(){}
private:
Uncopyable(const Uncopyable&);//阻止copying
Uncopyable& operator=(const Uncopyable&);
}
class HomeForSale:private Uncopyable{ //子类class不再声明
... // copy构造函数或
//copy赋值操作符
}
7、为多态基类声明virtual析构函数
如果一个class不含其他virtual函数时,通常表示它并不意图被用作一个baseclass,当class不企图被当作base class时
,令其析构函数为virtual往往是个馊主意。一般的做法是:当class内含至少一个virtual函数,才为它声明virtual析构函
数。
最后:带多态性质的base class应该声明一个virtual 析构函数。如果class带有任何virtual函数,它就应该拥有一个
virtual析构函数。如果class的设计目的不是作为base classes使用,或是不是为了具备多态性,就不该声明virtual析构函
数。
8、别让异常逃离析构函数。
析构函数绝对不要吐出异常,如果一个被析构函数调用的函数可能抛出异常,析构函数应该捕捉任何异常,然后吞下或者
结束程序。如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数(而非在析构中)
执行该操作。
9、绝不在构造和析构过程中调用virtual函数。因为这类调用不会降至子类。
10、令operator=返回一个reference to *this
11、在operator=中处理“自我赋值”
可使用的技术有:比较“来源对象”和“目标对象”的地址;精心周到的语句顺序;以及copy-and-swap
a、Widget& Widget::operator=(const Widget& rhs)
{
if(this==&rhs) return * this; //可以防止自我赋值,但不具备"异常安全性"
delete pb;
pb=new Bitmap(*rhs.pb);//new Bitmap如果失败,pb将指向一块已经被删除的Bitmap
return *this;
}
b、Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrig=pb;
pb=new Bitmap(*rhs.pb);//这样即使new失败,pb仍指向原来的bitmap
delete pOrig; //对于自我赋值,也不会导致错误,只是效率不是最高
return *this;
}
c、待续。。。
12、复制对象时勿忘其每一个成分:
copying函数(构造、赋值)应该确保复制"对象内的所有成员变量"及“所有base class成分”;
不要尝试以某个copying函数实现另一个copying函数,应该将共同机能放进第三个函数中,并由两个copying函数共同调
用。
13、以对象管理资源
获得资源后立刻放进管理对象内;即所谓“资源取得时机便是初始化时机”(RAII)
管理对象运用析构函数确保资源被释放。
最后:为防止资源泄漏,请使用RAII对象,它们在构造函数中获得资源并在析构函数中释放资源。
两个常被使用的RAII classes分别是tr1::shared_ptr和auto_ptr;前者通常是较佳选择,因为其copy行为比较直观。若
选择auto_ptr,复制动作会使它指向null。
14、在资源管理类中小心coping行为:
复制RAII对象必须一并复制它所管理的资源,所以资源的copying行为决定RAII对象的copying行为。
普遍而常见的RAII class copying行为是:抑制copying、施行引用计数法。
15、在资源管理类中提供对原始资源的访问:
APIs往往要求访问原始资源,所以每一个RAII class应该提供一个“取得其所管理资源”的办法。
对原始资源的访问可能经由显式转换或隐式转换,一般而言显式转换比较安全,但隐式转换对客户比较方便。
16、成对使用new和delete时要采取相同形式。
17、以独立语句将newed对象置入智能指针。如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄漏。
a. processWidget(std::tr1::shared_ptr<Widget>(new Widget),priority());
对于参数的调用顺序未知,有可能new出对象后,未来得及放入智能指针中,就有异常退出,造成资源泄露。
b. std::tr1::shared_ptr<Widget> pw(new Widget);
processWidget(pw,priority()); //这种不会造成资源泄露。
18、让接口容易被正确使用,不易被误用。
“促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容。
“阻止误用”的办法包括建立新类型、限制类型上的操作,束缚对象值,以及消除客户的资源管理责任。
tr1::shared_ptr支持定制型删除器,这可以防范DLL问题,可被用来自动解除互斥锁等。
19、设计class犹如设计type。
20、宁以pass-by-reference-to-const 替换pass-by-value
最后:请尽量以pass-by-reference-to-const 替换 pass-by-value。前者通常比较高效,并可避免切割问题。
以上规则并不适用内置类型,以及STL的迭代器和函数对象,对它们而言,pass-by-value往往比较适当。
21、必须返回对象时,别妄想返回其reference:
绝对不要返回pointer或reference指向一个local stack对象,或返回reference指向一个heap-allocated对象,或返回
pointer或referece指向一个local static对象而有可能同时需要多个这样的对象。
22、将成员变量声明为private:
最后:切记将成员变量声明为private,这可赋予客户访问数据的一致性、可细微划分访问控制、允诺约束条件获得保证,并
提供class作者以充分的实现弹性。
protected并不比public更具封装性。
23、宁以non-member non-friend(二者条件缺一不可)替换member函数。
这样做可以增加封装性、包裹弹性和机能扩充性。
24、若所有参数皆需类型转换(包括被this指针所指的那个隐喻参数),请为此采用non-member函数。
25、考虑写出一个不抛异常的swap函数。//这个较为复杂
26、尽可能延后变量定义式的出现时间。这样做可增加程序的清晰度并改善程序效率。
27、尽量少做转型动作。
最后:如果可以,尽量避免转型,特别是注重效率的代码中避免dynamic_casts,如果转型是必要的,试着将它隐藏于某个函
数背后;宁可使用c++ 新式转型,不要使用旧式转型。
28、避免返回handles指向对象内部成分:可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”的可能性降至最低。
29、为“异常安全”而努力是值得的。
异常安全有两个条件:不泄漏任何资源,不允许数据败坏。
异常安全函数提供以下三个保证之一:
基本承诺:如果异常被抛出,程序内的任何事物仍然保持在有效状态下。
强烈保证:如果异常被抛出,程序状态不改变。
不抛掷保证:承诺绝不抛出异常,
copy and swap:为打算修改的对象(原件)做出一份副本,然后在副本身上做一切必要修改,若有任何修改动作异常,原对象仍保持未改变状态。再将修改过的那个副本和原对象在一个不抛出异常的操作中转换。
最后:“强烈保证”往往能够以copy-and-swap实现出来,但“强烈保证”并非对所有函数都可实现或具备现实意义。
函数提供的“异常安全保证”通常最高只等于其所调用之各个函数的“异常安全保证”中的最弱者。
30、透彻的了解inline的里里外外:
隐喻的提出inline:将函数定义于class定义式内。friend函数也可被定义于class内,如果真如此,也被隐喻声明为inline。
Inline函数通常一定被置于头文件内。
31、将文件间的编译依存关系降至最低:
如果使用object 引用或object指针可以完成任务,就不要使用objects;
如果能够,尽量以class声明式替换class定义式。
为声明式和定义式提供不同的头文件。
最后:支持“编译依存性最小化”的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是handle classes和interface classes。
程序库头文件应该以“完全且仅有声明式”的形式存在,这种做法不论是否涉及templates都适用。
32、确定你的public继承朔模出is-a关系:
“public 继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。
33、避免遮掩继承而来的名称:
子类内的名称会遮掩基类内的名称。在public继承下,从来没有人希望如此。
为了让被遮掩在名称再见天日,可使用using声明式或转交函数。
34、区分接口继承和实现继承:
声明一个pure virtual函数的目的是为了是derived classes只继承
函数接口。
可以为pure virtual函数提供定义,但调用的唯一途径是“调用时明
确指出其class名称”
例:ps->Shape::draw();
声明一个impure virtual函数的目的,是让derived classes继承该
函数的接口和缺省实现。
声明non-virtual函数的目的是为了令derived classes继承函数的接
口及一份强制性实现。绝不该在derived class中重新定义。
35、考虑virtual函数以外的其他选择:
NVI手法:public non-virtual调用private virtual
strategy模式:函数指针或tr1::function
36、绝不重新定义继承而来的non-virtual函数。
37、绝不重新定义继承而来的缺省参数值,因为缺省参数是静态绑定,
而virtual函数却是动态绑定。
38、通过复合塑模出has-a或“根据某物实现出”:
复合的意义和public继承完全不同;
在应用域,复合意思着has-a,在实现域,复合意味“根据某物实现出
”。
39、明智而审慎地使用private继承:
private继承不会自动将一个derived class对象转换成一个base
class对象;由private base class继承而来的所有成员,在derived
class中都会变成private属性。
private继承意味着只有实现部分被继承,接口部分应略去。意味着
根据某物实现出(等同于复合,但级别比复合低,即要优先选择复合)
。但是当derived class 需要访问Protected base class的成员,或需
要重新定义继承而来的virtual函数时,才用private继承。
和复合不同,private继承可以造成empty base最优化。
40、明智而审慎地使用多重继承:
多重继承比单一继承复杂,可能导致新的歧义性以及对virtual继承的
需要。
virtual继承会增加大小、速度、初始化复杂度等成本,如果virtual
base class不带任何数据,将是最具实用价值的情况。
多重继承的确有正当用途。其中一个情节涉及“public 继承某个接口
class”和“private继承某人协助实现的class"的两相组合。
41、了解隐式接口和编译期多态:
对template参数而言,接口是隐式的,奠基于有效表达式。多态则是
通过template具现化和函数重载解析发生于编译期。
42、了解typename的双重意义:
声明template参数时,前缀关键字class和typename可互换。
用关键字typename标识嵌套从属类型名称;但不得在基类列或成员初始
化列内以它作为base class修饰符。
43、学习处理模板化基类的名称:
可在derived class templates内通过“this->”指涉base class
templates内的成员名称,或藉由一个明白写出的“base class资格修饰
符”完成。
44、将与参数无关的代码抽离templates:
Templates生成多个classes和多个函数,所以任何template代码都不
该与某个造成膨胀的template参数产生相依关系。
因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参
数或class成员变量替换template参数。
因类型参数而造成的代码膨胀,往往可降低,做法是让带有完全相同
二进制表述的具现类型共享实现码。
45、运用成员函数模板接受所有兼容类型:
在class内声明泛化copy构造函数并不会阻止编译器生成自己的coPy
构造函数(non-template)。赋值构造函数也是。
最后:
使用member function templates(成员函数模板)生成“可接受
所有兼容类型”的函数。
如果声明member templates用于“泛化coPy构造”或“泛化赋值操
作”,还是需要声明正常的copy构造函数和copy assignment操作符。
46、需要类型转换时请为模板定义非成员函数:
当我们编写一个class template,而它所提供之“与此template相关
的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义
为“class template内部的friend函数”
47、请使用traits classes表现类型信息:
Traits classes使得“类型相关信息”在编译期可用。它们以
templates和“templates 特化"完成实现。
整合重载技术后,traits classes有可能在编译期对类型执行
if...else测试。
48、认识template元编程:
TMP是编写template-based c++程序并执行于编译期的过程。
49、了解new-handler的行为:
set_new_handler允许客户指定一个函数,在内存分配无法获得满足时
被调用。
50、了解new和delete的合理替换时机。
51、编写new和delete时需固守常规
53、不要轻忽编译器的警告。
54、让自己熟悉包括TR1在内的标准程序库。
55、让自己熟悉boost库。