一、让自己习惯C++
条款1:尽量用const ,enum和inline 而不用#define(尽量用编译器而不用预处理)
- 对于单纯常量,以const,enum替换#defines
- 对于形似函数的宏(macros),改用inline函数替换#defines
enum
//实现编译期间就要知道数组的大小功能
class GAmePlayer
{
private:
static const int NumTurns = 5;//常量声明式,还未定义!static要在类外定义
int scores[NumTurns];//使用该常量
};
const int GAmePlayer::NumTurns;//声明时已经有了初值,定义时不能有了。
//也可以利用nums实现
class GamePlayer
{
//enum理解为记号
enum {NumTurns = 5};//令NumnTurns成为5的记号
int scores[NumTurns];
...
};
const
const关键点:
- const T * t表示被指物T不可改变
- T * t const 表示指针t不可改变
- const T * t const指针和被指物均不可改变.
STL的const
const成员函数(相对应mutable:可变的)
- 标记该函数可以作用于const对象,使操作const对象(改动对象)成为可能(为什么?)
- 两个成员函数如果只有常量性不同,也是可以被重载的!只不过使用的时候要用const区分
- 变量声明为mutable后,即使在const成员函数内,也可能总是被修改!
eg:
//两个函数如果只是常量性不同,可以被重载
class TextBlock
{
private:
string text;
public:
...;
//const不同也能重载
const char& operator[](size_t position) const { return text[position]; }
char& operator[](size_t position) { return text[position]; }
};
//使用
TextBlock tb("Hello");
cout << tb[0];
tb[0] = 'x';
const TextBlock ctb("World");
cout << ctb[0];
ctb[0] = 'x;'//错误!试图写一个const类型变量
note:
non-const operator[]的返回类型是个reference to char,不是char,如果是,tb[0] = 'x'通不过编译!
原因:如果函数的返回类型是个内置类型,改动函数返回值是不合法的。
1、非静态成员函数后面加const(加到非成员函数或静态成员后面会产生编译错误)
2、表示成员函数隐含传入的this指针为const指针,决定了在该成员函数中,
任意修改它所在的类的成员的操作都是不允许的(因为隐含了对this指针的const引用);
3、唯一的例外是对于mutable修饰的成员。
加了const的成员函数可以被非const对象和const对象调用
但不加const的成员函数只能被非const对象调用
char getData() const
{
return this->letter;
}
const double ASPECT_RATIO = 1.653;
//对于定义指针常量
const char * const authorName = "Scott Meyers";
//对于类中的常量
class EngineeringConstants
{ // this goes in the class
private: // header file
static const double FUDGE_FACTOR;//静态变量还要在类外定义
...
};
// this goes in the class implementation file
const double EngineeringConstants::FUDGE_FACTOR = 1.35;
const总结
-
const用于:对象、函数参数、函数返回类型、成员函数
-
当const和non-const 成员函数有着实质等价的实现时,令non-const版本调用const版本可避免代码重复。
inline
犯过的缺陷语句:无论什么时候你写了象这样的宏,你必须记住在写宏体时对每个参数都要加上括号
#define max(a,b) ((a) > (b) ? (a) : (b))
替代:以用普通函数实现宏的效率,再加上可预计的行为和类型安全,这就是内联函数
inline int max(int a, int b) { return a > b ? a : b; }
//进一步改进,实现对不同类型
template<class T>
inline const T& max(const T& a, const T& b) //因为不知道T的类型,返回时传递引用可以提高效率
{
return a > b ? a : b;
}
条款4 初始化和赋值
构造函数对成员变量的初始化是在进入构造函数内之前!如果在构造函数内,是赋值。前者比较好(效率更高)!
- 总是使用成员初始值列
ABEntry::ABEntry(const string& name,
const string& address,
const list<PhoneNumber>& phones)
:theName(name),
theAddress(address),
thePhones(phones),
numTimesConsulted(0)
{
}
- 对于non-local static对象的初始化次序(函数内的static对象称为local static对象,因为对函数而言是local)
static对象:global对象、定义域namspace作用域内的对象、在classes内、在函数内、以及在file作用域内被声明为static的对象。
二、构造、析构、赋值运算
条款6:若不想使用编译器自动生成的函数(copy构造函数和copy assignment操作符),就该明确拒绝
将相应的成员函数声明为private并且不予实现
条款7:为多态基类(polymorphic base classes)声明virtual析构函数
- polymorphic base classes应该声明一个virtual析构函数。如果class带有任何virtaul函数,他就应该拥有一个virtual析构函数
- Classes的设计目的如果不是作为base classes使用,或不是为了具备polymorphically,就不该声明virtual析构函数
条款8:析构函数遇到异常时,要暂停程序
class提供普通函数捕捉异常
//释放DBConnection类
class DBConn
{
private:
DBConnection db;
bool closed;
void close()
{
db.close(); //供客户使用的新函数
closed = true;
}
~DBConn()
{
if (!closed)
{
try
{
db.close();
}
catch (...)
{
//制作运转记录,记下对close的调用失败
}
}
}
};
条款9:绝不在构造和析构过程中调用virtual函数
这类调用从不下降至deri ved class
条款10:令operator=返回一个reference to *this
令赋值操作符assignment operator返回一个reference to *this
条款11:在operator=中处理“自我赋值”
- 确保当对象自我赋值时operator=有良好行为。技术包括比较“来源对象”和“目标对象”的地址、精心周到的语句顺序、以及copy-and-swap
- 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时(例如多个指针指向同一块内容),其行为仍然正确。
Widget& Widget::operator=(const Widget& rhs)
{
Bitmap* pOrg = pb;//记住原先的pb,指向同一块内容
pb = new Bitmap(*rhs.pb);//令pb指向*pb的一个复件(副本)
delete pOrig; //删除原先的pb
return *this;
}
条款12:Copy all parts of an object
- Copying函数应该确保复制"对象内的所有成员变量”及“所有base class成分”
- 不要尝试以某个copying函数实现另一个copying函数。应该将共同机能放进第三个函数,并由两个coping函数共同调用(减少重复性代码)。
三、资源管理(将使用过的资源交还给系统)
条款13:Use objects to manage resources
对于资源在heap-based的资源
思想,类似于利用对象的析构(自动)来释放资源。管理对象(managing object)例如智能指针shared_ptr
RAII:资源获得时机便是初始化时机
- 获得资源后立刻放进管理对象
- 管理对象运用析构函数确保资源被释放
条款14:在资源管理类中小心coping行为
并非所有资源都是heap-based,例如c的互斥器对象Mutex, shared_ptr不适用,要建立自己的资源管理类:
资源在构造期间获得,在析构期间释放
条款15:在资源管理类中提供对原始资源的访问
- APIs往往要求访问原始资源(raw resources),所以每一个RAII Class应该提供一个“”取得其所管理之资源“”的办法
- 对原始资源的访问可能经由显式转换或隐式转换。一般而言显式转换比较安全,但隐式转换对客户比较方便。
条款16:成对使用new和delete时要采取相同形式
条款17:以独立语句将newed对象置入智能指针
- 以独立语句将newed对象存储于智能指针内,如果不这样做,一旦异常被抛出,有可能导致难以察觉的资源泄露
processWidget(std::trl::shared_ptr<Widget>(new Widget), priority());
当编译器:执行“”new Widget“” -->调用prioruty-->trl::shared_otr构造函数次序时,如果对priority函数调用异常,则new Widget返回的指针会遗失,因为它尚未被置入shared_ptr内。
解决:分离语句
std::trl::shared_ptr<Widget>(new Widget);
processWidget(pw, priority());
4、设计与声明
条款18:让接口易被使用,不易被误用
Investment* createInvestment();
//改为
std::trl::shared_ptr<Investmen> createInvestment();
实质上强迫客户将返回值存储于一个shared_ptr内,几乎消弭了忘记删除底部Investment对象(当它不再被使用时)的可能性。
shared_ptr提供的某个构造函数接受两个实参:一个是被管理的指针,另一个是引用次数变为0时将被调用的“”删除器“”。
条款19:设计class犹如涉及type
条款20:Prefer pass-by-reference-to-const替换pass-by-value
例外:对于内置类型,以及STL的迭代器和函数对象,pass-by-value更恰当
条款21:Don't try to return a reference when you must return an object
绝不要返回pointer或reference指向一个local stack对象;
绝不要返回reference指向一个heap-allocated对象;
绝不要返回pointer或reference指向一个local stack对象而有可能同时需要多个这样的对象;
条款22:将成员变量声明为private
protected并不比public更具封装性
条款23:Prefer non-member non-friend functions to member functions
不增加可访问的数据量(private,enums,typedefs等),即加大了封装性
可以将non-member functions 和 class 放在同一个命名空间内。
将所有便利函数放在多个头文件内但隶属于同一个命名空间,意味着客户可以轻松扩展这一组便利函数
条款24:Declare non-member functions when type conversions should apply to all parameters
条款25:考虑写出一个不抛出异常的swap函数
- 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常
- 如果你提供一个member swap,也该提供一个non-member swap用来调用前者。对于classes(而非templates),也请特化std::swap
- 调用swap时应针对std::swap使用using声明式,然后调用swap并且不带任何“命名空间资格修饰”。
- 为“用户定义类型”进行std templates全特化是好的,但千万不要尝试在std内加入某些对std而言全新的东西。
五、实现
条款26:尽可能延后变量定义式的出现时间
条款27:尽量少做转型(casts)动作
转型风格通常有三种形式:
- C风格:(T) expression
- 函数风格:T(expression)
- C++还提供四种新式转型(c++style casts)
const_cast<T>(expression)//将对象的常量性移除!唯一由此能力的C++style转型操作符
dynamic_cast<T>(expression)//决定某对象是否归属继承体系中的某个类型。
reinterpret_cast<T>(expression)//不可移植。
static_cast<T>(expression)//强迫隐式类型转换,non-const-->const;void*-->typed*;int-->double
新式优点:在代码中易被辨别出来;转型细化,易被编译器诊断错误
条款28:避免返回handles指向对象内部成分
- 避免返回handles(包括references、指针、迭代器)指向对象内部。遵守这个条款可增加封装性,帮助const成员函数的行为像个const,并将发生“虚吊号码牌”(dangling handles)的可能性降至最低。
条款29:为“异常安全”而努力是值得的(strive for exception-safe code)
条款30:透彻了解inlining的里里外外
不要只因为function templates出现在头文件,就将它们声明为inline。
条款31:将文件的编译依存关系降至最低
六、继承与面向对象设计
条款32:确定你的public继承是is-a关系
条款33:避免遮掩继承而来的名称(Avoid hiding inherited names)
只要函数名称相同,派生类成员函数就会遮掩基类成员函数,可以使用using声明式达到继承重载函数的目标。
#include <cstring>
using namespace std;
class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived :public Base
{
public:
using Base::mf1;//让Base class 内名为mf1和mf3的所有东西在Derived作用域内都可见(并且public)
using Base::mf3;
void mf3();
void mf4();
};
- derived classes内的名称会遮掩base classes内的名称。在public继承下从来没有人希望如此
- 为了让被遮掩的名称再见天日,可使用using 声明式或转交函数(forwarding functions)
条款34:区分接口继承(inheritance of interface)和实现继承(inheritance of implementation)
有时想只继承成员函数的接口(pure virtual),有时又想继承声明和实现但是想override所继承的实现(impure virtual),有时又想继承声明和实现但是不允许覆写任何东西(non-virtual)。
条款35:考虑virtual函数以外的其他选择(consider alternatives to virtual functions)
条款36:绝不重新定义继承而来的non-virtual函数
条款37:绝不重新定义继承而来的缺省参数值
绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数-----你唯一应该覆写的东西----却是动态绑定
条款38:通过复合(composition)塑模出has-a或“根据某物实现出”
Model "has-a" or "is-implemented-in-terms-of"through composition
条款39:明智而审慎的使用private继承use private inheritance judiciously
- Private意味着is-implemented-in-terms-of。它通常比复合(composition)的级别低。但是当derived class需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
- 和复合不同,private继承可以改造empty base 最优化。这对致力于“对象尺寸最小化”的程序开发者而言,可能很重要。
条款40:明智而审慎的使用多重继承(MI)
七、模板与泛型编程 Templates and Generic Programming
条款41:了解隐式接口和编译期多态
隐式接口:在编译期,根据某个对象支持什么操作来隐式判断是什么对象。
面向对象编程世界总是以显式接口(explicit interfaces)和运行期多态(runtime polymorphism)即动态绑定解决问题
在Template及泛型编程的世界,与面向对象有根本上的不同,隐式接口(implicit interfaces)和编译期多态(compile-time polymorphism)更重要,因为templates事先并不知道具体的类型!
运行期多态和编译期多态之间的差异:类似于“哪一个重载函数该被调用,编译期”和“哪一个virtual函数该被绑定,运行期”。
条款42:了解typename的双重意义
从属名称:template内出现的名称依赖于某个template参数;
嵌套从属名称:从属名称在class内呈嵌套状;有可能导致解析(parsing)困难。
一般性规则:任何时候当你想要在template中指涉一个嵌套从属类型名称,就必须在紧邻它的前一个位置放上关键字typename,
typename只被用来验明嵌套从属类型名称,其他名称不该有它存在,eg:
template<typename C>
void f(const C& container,//并非嵌套于任何“取决于template参数”的东西内,不允许使用typename
typename C::iterator iter);//嵌套从属名称,一定要使用“typename”
例外:typename不可以出现在base class list内的嵌套从属类型名称之前,也不可在member initialization list(成员初始值)中作为nase class修饰符。eg:
template<typename T>
class Derived : public Base<T>::Nested//base class list中不允许"typename"
{
public:
explicit Derived(int x)
:Base<T>::Nested(x)//mem.init.list中不允许typename
{
typename Base<T>::Nested temp;//嵌套从属名称,需要typename
}
};
条款43:学习处理模板化基类内的名称
可在derived class templates内通过this-->“”指涉base class templates内的名称,或藉由一个明白写出的“base class资格修饰符”完成
条款44:将与参数无关的代码抽离templates
类似于,编写某个class和另一个class的某些部分相同,把共同部分搬移到新的class1去,然后使用继承和复合,令原先的classes取用这共同特性。
条款45:运用成员函数模板接受所有兼容类型
STL容器的迭代器几乎总是智能指针,无疑的你不会奢望使用++将一个内置指针从linked list的某个节点移到另一个结点,但这在list::iterators身上办得到。
- 请使用member function templates生成“可接受所有兼容类型”的函数
- 如果你声明member templates 用于“泛化copy构造”或“泛化assignment操作”,你还是需要声明正常的copy构造函数和copy assignment操作符。
条款46:需要类型转换时请为模板定义非成员函数
当我们编写一个class template,而它所提供之“与此template相关的”函数支持“所有参数之隐式类型转换”时,请将那些函数定义为“class template内部的friend函数”。
条款47:请使用traits classes表现类型信息
条款48:认识template元编程Be aware of template metaprogramming
TMP是编写template-based C++程序并执行于编译期的过程;即以C++写成、执行于C++编译器内的程序。
八、定制new和delete
条款49:了解new-handler的行为
- set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用
条款50:了解new和delete的合理替换时机
有许多理由需要写个自定义的new和delete,包括改善性能、对heap运用错误进行调试、收集heap使用信息
条款51:编写new和delete时需固守常规
- operator new 应该内含一个无穷循环,并在其中尝试分配内存,如果无法满足内存需求,就该调用new-handle。它也应该有能力处理0bytes申请。class专属版本则还应该处理“比正确大小更大的(错误)申请”
- operator delete应该在收到null指针时不做任何事情。class专属版本则还应该处理“比正确大小更大的(错误)申请”