让自己习惯C++
视C++为一个语言联邦
- C语言
- 面对对象
- C++模板
- STL容器
尽量以const,enum,inline替换#define
- const的好处:
- define直接常量替换,出现编译错误不易定位(不知道常量是哪个变量)
- define没有作用域,const有作用域提供了封装性
- enum的好处:
- 提供了封装性
- 编译器肯定不会分配额外内存空间(其实const也不会)
- inline的好处:
- define宏函数容易造成误用(下面有个例子)
//define误用举例
#define MAX(a, b) a > b ? a : b
int a = 5, b = 0;
MAX(++a, b) //a++调用2次
MAX(++a, b+10) //a++调用一次
然而,了解宏的机制以后,我们也可以用宏实现特殊的技巧。例如:C++反射,TEST
宏实现工厂模式
- 需要一个全局的map用于存储类的信息以及创建实例的函数
- 需要调用全局对象的构造函数用于注册
using namespace std;
typedef void *(*register_fun)();
class CCFactory{
public:
static void *NewInstance(string class_name){
auto it = map_.find(class_name);
if(it == map_.end()){
return NULL;
}else
return it->second();
}
static void Register(string class_name, register_fun func){
map_[class_name] = func;
}
private:
static map<string, register_fun> map_;
};
map<string, register_fun> CCFactory::map_;
class Register{
public:
Register(string class_name, register_fun func){
CCFactory::Register(class_name, func);
}
};
#define REGISTER_CLASS(class_name); \
const Register class_name_register(#class_name, []()->void *{return new class_name;});
尽可能使用const
- const定义接口,防止误用
- const成员函数,代表这个成员函数承诺不会改变对象值
- const成员只能调用const成员函数(加-fpermissive编译选项就可以了)
- 非const成员可以调用所有成员函数
确定对象使用前已被初始化
- 内置类型需要定义时初始化
- 最好使用初始化序列(序列顺序与声明顺序相同),而不是在构造函数中赋值
- 跨编译单元定义全局对象不能确保初始化顺序
- 将static对象放入一个函数
Fuck& fuck(){
static Fuck f;
return f;
}
构造/析构/赋值运算
了解C++默默编调用了哪些函数
如果类中没有定义,程序却调用了,编译器会产生一些函数
- 一个 default 构造函数
- 一个 copy 构造函数
- 一个 copy assignment 操作符
- 一个析构函数(non virtual)
- 如果自己构造了带参数的构造函数,编译器不会产生default构造函数
- base class如果把拷贝构造函数或者赋值操作符设置为private,不会产生这两个函数
- 含有引用成员变量或者const成员变量不产生赋值操作符
class Fuck{
private:
std::string& str;//引用定义后不能修改绑定对象
const std::string con_str;//const对象定义后不能修改
};
若不想使用编译器自动生成的函数,就该明确拒绝
将默认生成的函数声明为private,或者C++ 11新特性"=delete"
class Uncopyable{
private:
Uncopyable(const Uncopyable&);
Uncopyable& operator= (const Uncopyable&);
}
为多态基类声明virtual析构函数
- 给多态基类应该主动声明virtual析构函数
- 非多态基类,没有virtual函数,不要声明virtual析构函数
别让异常逃离析构函数
构造函数可以抛出异常,析构函数不能抛出异常。
因为析构函数有两个地方可能被调用。一是用户调用,这时抛出异常完全没问题。二是前面有异常抛出,正在清理堆栈,调用析构函数。这时如果再抛出异常,两个异常同时存在,异常处理机制只能terminate().
构造函数抛出异常,会有内存泄漏吗?
不会try { // 第二步,调用构造函数构造对象 new (p)T; // placement new: 只调用T的构造函数 } catch(...) { delete p; // 释放第一步分配的内存 throw; // 重抛异常,通知应用程序 }
绝不在构造和析构过程中调用virtual函数
构造和析构过程中,虚表指针指向的虚表在变化。调用的是对应虚表指针指向的函数。
令operator= 返回一个reference to *this
没什么理由,照着做就行
在operator= 里处理自我赋值
Widget& Widget::operator== (const Widget& rhs){
if(this == &rhs) return *this
···
}
复制对象时务忘其每一个成分
- 记得实现拷贝构造函数和赋值操作符的时候,调用base的相关函数
- 可以让拷贝构造函数和赋值操作符调用一个共同的函数,例如init
资源管理
以对象管理资源
- 为了防止资源泄漏,请使用RAII对象,在构造函数里面获得资源,在析构函数里面释放资源
- shared_ptr,unique_lock都是RAII对象
在资源管理类小心copy行为
- 常见的RAII对象copy行为
- 禁止copy
- 引用计数
- 深度复制
- 转移资源拥有权
在资源管理类中提供对原始资源的访问
用户可能需要原始资源作为参数传入某个接口。有两种方式:
- 提供显示调用接口
- 提供隐式转换接口(不推荐)
成对使用new和delete要采用相同的格式
new和delete对应;new []和delete []对应
//前面还分配了4个字节代表数组的个数
int *A = new int[10];
//前面分配了8个字节,分别代表对象的个数和Object的大小
Object *O = new Object[10];
以独立的语句将newd对象置入智能指针
调用std::make_shared,而不要调用new,防止new Obeject和传入智能指针的过程产生异常
process(new Widget, priority);
//其实这样也可以,独立的语句
shard_ptr<Widget> p(new Widget);
process(p, priority);
设计与声明
让接口容易被正确使用,不易被误用
- 好的接口很容易被正确使用,不容易被误用。努力达成这些性质(例如 explicit关键字)
- “促进正确使用”的办法包括接口的一致性,以及与内置类型的行为兼容
- “防治误用”b包括建立新类型,限制类型上的操作,束缚对象值,以及消除用户的资源管理责任
- shared_ptr支持定制deleter,需要灵活使用
设计class犹如设计type
宁以pass-by-refrence-to-const替换pass-by-value
- 尽量以pass-by-reference-to-const替换pass-by-value,比较高效,并且可以避免切割问题
- 以上规则并不使用内置类型,以及STL迭代器,和函数对象。它们采用pass-by-value更合适(其实采用pass-by-reference-to-const也可以)
必须返回对象时,别妄想返回其reference
- 不要返回pointer或者reference指向一个on stack对象(被析构)
- 不要返回pointer或者reference指向一个on heap对象(需要用户delete,我觉得必要的时候也不是不可以)
- 不要返回pointer或者reference指向local static对象,却需要多个这样的对象(static只能有一份)
将成员变量申明为private
- 切记将成员变量申明为private
- protected并不比public更有封装性(用户可能继承你的base class)
宁以non-member,non-friend替换member
作者说多一个成员函数,就多一分破坏封装性,好像有点道理,但是我们都没有这样遵守。直接写member函数方便一些。
若所有参数都需要类型转换,请为此采用non-member函数
如果调用member函数,就使得第一个参数的类失去一次类型转换的机会。
考虑写一个不抛出异常的swap函数
- 当std::swap效率不高(std::swap调用拷贝构造函数和赋值操作符,如果是深拷贝,效率不会高),提供一个swap成员函数,并确定不会抛出异常。
class Obj{
Obj(const Obj&){//深拷贝}
Obj& operator= (const Obj&){深拷贝
private:
OtherClass *p;
};
- 如果提供一个member swap,也该提供一个non-member swap用来调用前者
调用swap时应该针对std::swap使用using声明式,然后调用swap不带任何"命名空间修饰”
void doSomething(Obj& o1, Obj& o2){ //这样可以让编译器自己决定调用哪个swap,万一用户没有实现针对Obj的swap,还能调用std::swap using std::swap; swap(o1, o2); }
不要往std命名空间里面加东西
实现
尽可能延后变量定义式出现的时间
C语言推荐在函数开始的时候定义所有变量(最开始的C语言编译器要求,现在并不需要),C++推荐在使用对象前才定义对象
尽量少做转型动作
- 如果可以,尽量避免转型,特别是在注重效率的代码中避免dynamic_cast。
- 如果转型是必要的,试着将它隐藏于某个函数后。客户可以随时调用该函数,而不需要将转型放入自己的代码。
- 使用C++风格的转型。
避免返回handles指向对象内部成分
简单说,就是成员函数返回指针或者非const引用不要指向成员变量,这样会破坏封装性
为“异常安全”而努力是值得的
- "异常安全函数"承诺即使发生异常也不会有资源泄漏。在这个基础下,它有3个级别
- 基本保证:抛出异常,需要用户处理程序状态改变(自己写代码保证这个级别就行了把)
- 强烈保证:抛出异常,程序状态恢复到调用前
- 不抛异常:内置类型的操作就绝不会抛出异常
"强烈保证"往往可以通过copy-and-swap实现,但是"强烈保证"并非对所有函数都具有实现意义
//我反正从来没有这样写过 void doSomething(Object& obj){ Object new_obj(obj); new_obj++; swap(obj, new_obj); }
透彻了解inline函数的里里外外
这里插播一个C++处理定义的重要原则,一处定义原则:
- 全局变量,静态数据成员,非内联函数和成员函数只能整个程序定义一次
- 类类型(class,struct,union),内联函数可以每个翻译单元定义一次
- template类的成员函数或者template函数,定义在头文件中,编译器可以帮忙去重
- 普通类的template函数,定义在头文件中,需要加inline
- inline应该限制在小的,频繁调用的函数上
- inline只是给编译器的建议,编译器不一定执行
将文件的编译依存关系降到最低
- 支持"编译依存最小化"的一般构想是:相依于声明式,不要相依于定义式。基于此构想的两个手段是Handle classes(impl对象提供服务)和Interface classes。
其实就是使用前置声明,下面有个需要注意的点
//Obj.h
class ObjImpl;
class Obj{
public:
private:
std::shared_ptr<ObjImpl> pObjImpl;
};
//上面的写法会报错,因为编译器会再.h文件里面产生默认的析构函数,
//析构函数要调用ObjImpl的析构函数,然后我们现在只有声明式,不能调用ObjImpl的实现。
//下面的实现才是正确的
//Obj.h
class ObjImpl;
class Obj{
public:
//声明
~Obj();
private:
std::shared_ptr<ObjImpl> pObjImpl;
};
//Obj.cpp
//现在可以看到ObjImpl的实现
#include<ObjImpl>
Obj::~Obj(){
}
- 对于STL的对象不需要前置声明。
继承与面对对象设计
确定你的public继承塑模出is-a模型
public继承意味着is-a。适用于base class身上的每一个函数也一定适用于derived class。
避免遮掩继承而来的名称
子作用域会遮掩父作用域的名称。一般来讲,我们可以有以下几层作用域
- global作用域
- namespace作用域
- Base class作用域
- Drive class作用域
- 成员函数
- 控制块作用域
- 成员函数
- Drive class作用域
- 非成员函数作用域
- 控制块作用域
- Base class作用域
注意:遮掩的是上一层作用域的名称,重载(不同参数)的函数也会直接遮掩
class Base{
public:
void f1();
}
class Drive{
public:
//会遮掩f1(),子类并没有继承f1()
void f1(int);
}
Drive d;
d.f1(); //错误
d.f1(3); //正确
可以通过using声明式或者inline转交解决这一问题
class Base{
public:
void f1();
}
//using 声明式
class Drive{
public:
using Base::f1;
void f1(int);
}
//inline转交
class Drive{
public:
void f1(){
Base::f1();
}
void f1(int);
}
区分接口继承和实现继承
- 纯虚函数:提供接口继承
- Drived class必须实现纯虚函数
- 不能构造含有纯虚函数的类
- 纯虚函数可以有成员变量
- 可以给纯虚函数提供定义(wtf)
- 虚函数:提供接口继承和默认的实现继承
- 非虚函数:提供了接口继承和强制的实现继承(最好不要在Drived class重新定义非虚函数)
考虑virtual函数以外的选择
non-virtual interface:提供非虚接口
class Object{
public:
void Interface(){
···
doInterface();
···
}
private/protected:
virtual doInterface(){}
}
优点:
- 可以在调用虚函数的前后,做一些准备工作(抽出一段重复代码)
- 提供良好的ABI兼容性
聊一聊ABI兼容性
我们知道,程序库的优势之一是库版本升级,只要保证借口的一致性,用户不用修改任何代码。
一般一个设计完好的程序库都会提供一份C语言接口,为什么呢,我们来看看C++ ABI有哪些脆弱性。
- 虚函数的调用方式,通常是 vptr/vtbl 加偏移量调用
//Object.h
class Object{
public:
···
virtual print(){}//第3个虚函数
···
}
//用户代码
int main(){
Object *p = new Object;
p->print(); //编译器:vptr[3]()
}
//如果加了虚函数,用户代码根据偏移量找到的是newfun函数
//Object.h
class Object{
public:
···
virtual newfun()//第3个虚函数
virtual print(){}//第4个虚函数
···
}
- name mangling 名字粉碎实现重载
C++没有为name mangling制定标准。例如void fun(int),有的编译器定为fun_int_,有的编译器指定为fun%int%。
因此,C++接口的库要求用户必须和自己使用同样的编译器(这个要求好过分)
- 其实C语言接口也不完美
例如struct和class。编译阶段,编译器将struct或class的对象对成员的访问通过偏移量来实现
使用std::fun提供回调
class Object{
public:
void Interface(){
···
doInterface();
···
}
private/protected:
std::function<void()> doInterface;
}
古典策略模式
用另外一个继承体系替代
class Object{
public:
void Interface(){
···
p->doInterface();
···
}
private/protected:
BaseInterface *p;
}
class BaseInterface{
public:
virtual void doInterface(){}
}
绝不重新定义继承而来的non-virtual函数
记住就行
绝不重新定义继承而来的缺省参数值
class Base{
public:
virtual void print(int a = 1) {cout <<"Base "<< a <<endl;};
int a;
};
class Drive : public Base{
public:
void print(int a = 2){cout << "Drive " << a <<endl;}
};
int main(){
Base *b = new Drive;
b->print(); // vptr[0](1)
}
//Drive 1
- 缺省参数值是静态绑定
- 虚函数是动态绑定
- 遵守这条规定防止出错
通过复合塑模出has-a或者"根据某物实现出"
- 复合的意义和public完全不一样
- 根据某物实现出和is-a的区别:
这个也是什么时候使用继承,什么时候使用复合。复合代表使用了这个对象的某些方法,但是却不想它的接口入侵。
明智而审慎地使用private继承
- private继承是”根据某物实现出“
- 唯一一个使用private继承的理由就是,可以使用空白基类优化技术,节约内存空间
C++对空类的处理
C++ 设计者在设计这门语言要求所有的对象必须要有不同的地址(C语言没有这个要求)。C++编译器的实现方式是给让空类占据一个字节。
class Base{
public:
void fun(){}
}
//8个字节
class Object{
private:
int a;
Base b;
};
//4个字节
class Object : private Base{
private:
int a;
}
明智而审慎地使用多重继承
首先我们来了解一下多重继承的内存布局。
//包含A对象
class A{
};
//包含A,B对象
class B:public A{
};
//包含A,C对象
class C:public A{
};
//包含A,A,B,C,D对象
class D:public B, public C{
}
由于菱形继承,基类被构造了两次。其实,C++也提供了针对菱形继承的解决方案的
//包含A对象
class A{
};
//包含A,B对象
class B:virtual public A{
};
//包含A,C对象
class C:virtual public A{
};
//包含A,B,C,D对象
class D:public B, public C{
}
使用虚继承,B,C对象里面会产生一个指针指向唯一一份A对象。这样付出的代价是必须再运行期根据这个指针的偏移量寻找A对象。
多重继承唯一的那么一点点用就是一个Base class提供public继承,另一个Base class提供private继承。(还是没什么用啊,干嘛不适用复合)
模板与泛型编程
了解隐式接口和编译期多态
- 接口:强制用户实现某些函数
- 多态:相同的函数名,却有不同的实现
- 继承和模板都支持接口和多态
- 对继承而言,接口是显式的,以函数为中心,多态发生在运行期;
- 对模板而言,接口是隐式的,多态表现在template具象化和函数重载
//这里接口要求T必须实现operator >
template<typename T>
T max(T a, T b){
return (a > b) ? a : b;
}
了解typename的双重意义
- 声明template参数时,前缀关键字class和typename可以互换
- 使用typename表明嵌套类型(防止产生歧义)
学习处理模板化基类内的名称
template <typename T>
class Base{
public:
void print(T a) {cout <<"Base "<< a <<endl;};
};
template<typename T>
class Drive : public Base<T>{
public:
void printf(T a){
//error 编译器不知道基类有print函数
print(a);
}
};
//解决方案
//this->print();
//using Base<T>::print
//base<T>::print直接调用
将参数无关代码抽离template
- 非类型模板参数造成的代码膨胀:以函数参数或者成员变量替换
- 类型模板参数造成的代码膨胀:特化它们,让含义相近的类型模板参数使用同一份底层代码。例如int,long, const int
运用成员函数模版接收所有兼容类型
我们来考虑一下智能指针的拷贝构造函数和赋值操作符怎么实现。它需要子类的智能指针能够隐式转型为父类智能指针
template<typename T>
class shared_ptr{
public:
//拷贝构造函数,接受所有能够从U*隐式转换到T*的参数
template<typename U>
shared_ptr(shared_ptr<U> const &rh):p(rh.get()){
...
}
//赋值操作符,接受所有能够从U*隐式转换到T*的参数
template<typename U>
shared_ptr& operator= (shared_ptr<U> const &rh):p(rh.get()){
...
}
//声明正常的拷贝构造函数
shared_ptr(shared_ptr const &rh);
shared_ptr& operator= (shared_ptr const &rh);
private:
T *p;
}
- 使用成员函数模版生成“可接受所有兼容类型”的函数
- 即使有了“泛化拷贝构造函数”和“泛化的赋值操作符”,仍然需要声明正常的拷贝构造函数和赋值操作符
- 在一个类模版内,template名称可被用来作为作为“template和其参数”的简略表达式
所有参数需要类型转换的时候请为模版定义非成员函数
- 当我们编写一个模版类,某个相关函数都需要类型转换,需要把这个函数定义为非成员函数
- 但是模版的类型推到遇见了问题,需要把这个函数声明为友元函数帮助推导
- 模版函数只有声明编译器不会帮忙具现化,所以我们需要实现的是友元模版函数
template <class T>
class Rational
{
…
friend Rational operator* (const Rational& a, const Rational& b)
{
return Rational (a.GetNumerator() * b.GetNumerator(),
a.GetDenominator() * b.GetDenominator());
}
…
}
请使用traits classes表现类型信息
template<typename T>
class type_traits;
template<>
class type_traits<int>{
public:
static int size = 4;
}
template<>
class type_traits<char>{
public:
static int size = 1;
}
template<>
class type_traits<double>{
static int size = 8;
}
template<typename T>
int ccSizeof(T){
return type_traits<T>::size;
}
- traits采用类模版和特化的方式,为不同的类型提供了相同的类型抽象(都由size)
- 为某些类型提供编译期测试,例如is_fundamental
模版元编程
本质上就是函数式编程
//上楼梯,每次上一步或者两步,有多少种
int climb(int n){
if(n == 1)
return 1;
if(n == 2)
return 2;
return climb(n - 1) + climb(n - 2);
}
//元编程,采用类模版
template<int N>
class Climb{
public:
const static int n = Climb<N-1>::n + Climb<N-2>::n;
};
template<>
class Climb<2>{
public:
const static int n = 2;
};
template<>
class Climb<1>{
public:
const static int n = 1;
};
- C++元编程可以将计算转移到编译期,执行速度迅速(缺陷?)
定制new和delete
了解new-handler的行为
new和malloc对比:
- new构造对象,malloc不会
- new分配不出内存会抛异常,malloc返回NULL
- new分配不出内存可以调用用户设置的new-handler,malloc没有
namespace std{
typedef void (*new_handler)();
//返回旧的handler
new_handler set_new_handler(new_handler p) throw();
}
- 可以为每个类设置专属new handler
了解new和delete合理的替换时机
C++中对象的构造和析构经历了都两个阶段
- operator new, operator delete:分配和释放内存
- 调用构造函数,调用析构函数
替换new和delete的理由,就是需要收集分配内存的资源信息
编写符合常规的new和delete
- operator new应该内含一个无穷循环尝试分配内存,如果无法满足,就调用new-handler。class版本要处理“比正确大小更大的(错误)申请”
- operator deleter应该处理Null。classz专属版本还要处理“比正确大小更小的(错误)申请”
写了operator new也要写相应的operator delete
我们知道,new一个对象要经历两步。如果在调用构造函数失败,编译器会寻找一个“带相同额外参数”的operator delete,否则就不调用,造成资源泄漏
STL使用小细节
为不同的容器选择不同删除方式
删除连续容器(vector,deque,string)的元素
// 当c是vector、string,删除value
c.erase(remove(c.begin(), c.end(), value), c.end());
// 判断value是否满足某个条件,删除
bool assertFun(valuetype);
c.erase(remove_if(c.begin(), c.end(), assertFun), c.end());
// 有时候我们不得不遍历去完成,并删除
for(auto it = c.begin(); it != c.end(); ){
if(assertFun(*it)){
···
it = c.erase(it);
}
else
++it;
}
删除list中某个元素
c.remove(value);
// 判断value是否满足某个条件,删除
c.remove(assertFun);
删除关联容器(set,map)中某个元素
c.erase(value)
for(auto it = c.begin(); it != c.end(); ){
if(assertFun(*it)){
···
c.erase(it++);
}
else
++it;
}
我之前边读《Effective C++》边写下每个条款的读书笔记,这一版是C++11之前的版本。这里我将每个条款令我印象深刻的点小结一下。
1、C++包括:Plain C(面向过程)、OOP(面向对象)、模板(泛型和模板元编程)、STL(C++标准库)。
2、用inline、enum、const代替#define。#define定义的宏,一旦复杂起来,高手都很难掌控。不要带入C的习惯。
3、灵活使用const前缀。不需要进行改变的数据加上const前缀。指针的const前缀有两种形式,const放在*左边表示数据不变,放右边表示指针地址不变。返回不需要修改的成员数据的成员函数需要加上const标记。
4、使用对象前确保初始化。简单的内置数据结构(POD),有些编译器都会直接初始化为0。自定义数据结构,尤其是包含指针的类,最好要有明确定义的初始化过程,指针要赋给0或者NULL值。
5、了解C++暗地里编写和调用了什么函数。类会自动生成default 构造函数,copy 构造函数和copy assign函数,如果不明确定义拷贝构造函数,那么类对象拷贝的时候只进行浅拷贝操作,有可能与原有意图不符。因此需要明确定义这些函数,不让C++编译器自动生成。
6、可以拒绝使用编译器生成的默认函数。方法是把这些默认函数放到private里面,这样程序就无法使用默认函数了。
7、为多态基类声明virtual析构函数。如果基类没有定义virtual析构函数,那么当我们用基类指针指向derived 对象,通过基类指针析构derived对象,无法调用derived对象的析构函数。所以要预先为base类编写virtual 析构函数,这样基类和派生类都会执行析构函数。STL不是完全的面向对象思想,比如没有虚析构函数,因此不能以STL类为基类实现多态。
8、别让异常逃离析构函数。在处理异常之前,先要保证资源都析构了,不然会发生资源泄露导致bug;如果不行,那么终止程序是最保险的办法。保证资源析构的可行的办法使用资源管理类或者智能指针管理资源。
9、绝不在构造和析构中调用virtual函数。derived实例构造时,base构造部分如果调用virtual函数,则调用的是base类的virtual函数而不是derived类的virtual函数。
10、 令operator=返回一个reference to *this。这是标准,很多人会忘记。
11、 在operator=中处理“自我赋值”。判断参数是不是本身,如果是本身,就直接返回,不要做没用的事情。
12、 复制对象不要忘了任何一个成分。因为忘记复制对象是不会报警告和错误的,靠开发者自己仔细编写。
13、 以对象管理资源。由于基本数据类型是不存在资源泄露问题的,因此我们可以定义对象的行为使其与基本数据类型相似。让对象管理资源,让对象行为类似基本数据类型,这样就可以像基本数据类型一样操作自定义对象。资源管理类和智能指针是常用的两种资源管理对象。
14、 在资源管理类中小心coping行为。资源管理类可以有四种coping行为:a禁止复制,比如继承boost的Uncopyable类b引用计数,不复制资源只是增加一个引用,比如智能指针c复制资源d转移使用权,比如std::auto_ptr,比较少用。
15、 在资源管理类中提供对原始资源的访问。对资源管理类的最优设计是让资源管理类用起来和基本数据类型一样。有必要设计一个好接口用来访问资源管理类封装的原始资源。
16、 成对使用new/delete要采用相同形式。如果用new,就使用delete;如果用new [],就使用delete [];用malloc申请资源,就用free析构资源。
17、 以独立语句将newed对象置入智能指针。processWidget(std::tr1::shared_ptr<Widget> (new Widget),priority());有三种操作:1、new Widget2、构造shared_ptr3、priority函数。编译器对上面的执行步骤不确定,所以如果执行顺序恰好这样1、new Widget2、priority函数3、构造shared_ptr。如果在priority函数执行时抛出异常,那么构造shared_ptr这一步就没有发生,于是Widget泄露了。
18、 让接口容易正确使用,不易被误用。设计接口行为最好和内置数据类型一致。
19、 设计class犹如设计type。设计需要注意:1、创建与销毁2、初始化和赋值是不同的3、如何复制4、错误检查5、继承关系6、类型转化7、有哪些合法行为8、明确拒绝使用某些函数9、成员数据的存取操作接口10、是否使用模板泛化11、没有接口的属性怎么处理12、需要设计这样一个类吗。
20、 尽量用const引用传递代替值传递,来传递不可修改的数据。如果是基本数据类型,值传递和const引用传递没有区别。如果传递的数据很大那么值传递会消耗大量工作在构造复制数据上面。
21、 必须返回对象时,别妄想返回其reference。局部作用域的对象必须返回值,不可返回引用,因为程序运行走出作用域,局部对象会被自动析构。
22、 将成员变量声明为private。遵循OOP的封装原则。在测试和调试的时候其实可以临时变通的。
23、 宁可以non-member、non-friend替换member函数。使用非成员函数可以避免类变得冗杂,而且扩展性较强。第二个原因见24条。
24、 若所有参数需要类型转换,请使用non-member函数。双目重载运算符成员函数只能对第二个参数进行类型转换。重载双目运算符非成员函数,可以对运算符号前/后对象进行类型转换。
25、 考虑写出一个不抛出异常的swap函数。Swap用于高效交换两个对象的数据,内部使用了很多技巧,多用swap可以提升程序运算速度,如果swap函数鲁棒性强不抛出异常,那么程序会健壮很多。
26、 尽可能延迟变量定义式的出现时间。标准C要求在程序开头定义好变量。在C++中,可以在要用数据的时候才定义变量,这样方便程序的开发。
27、 尽量少做转型操作。转型操作会改变数据,容易让开发者写出bug。
28、 避免返回handle指向对象内部成分。使用类提供的数据存取操作函数,不要直接返回引用、指针、迭代器。
29、 为异常安全而努力是值得的。异常安全有两个条件:1、不泄露资源2、不允许影响其他数据。异常安全等级有三个:1、基本承诺:抛出异常,但是程序仍然可以使用。不过当前状态是不可知的2、强烈保证:抛出异常,程序状态不变。如果成功就完全成功,如果失败就回滚到原始状态3、不抛掷异常,绝对成功。大部分情况下需要保证前两个异常安全等级;swap要保证第三个等级。
30、 透彻了解inlining的里里外外。Inline函数会展开到调用处,相当于安全性很高的#define函数;inline函数可以预先声明;inline函数放在头文件中;类中的函数,如果放在头文件里面,就是inline函数;inline函数可以模板化;inline函数不能作为虚函数;inline函数是小函数,如果规模太大会造成程序膨胀。
31、 将文件间的编译依存关系降至最低。保持模块之间的低耦合度;采用pimpl技术,用指针指向实现模块,这样将实现和接口分离。
32、 确定你的public继承塑造出is-a关系。D继承自B,则D是B的一个特例,B能用的地方,D也可以。
33、 避免遮掩继承来的名称。如果覆盖了基类中的可用的函数foo,那么使用B.foo和使用D.foo的效果是不同的,有可能导致B能用的地方D不能用。
34、 区分接口继承和实现继承。1、non-virtual函数表示这个函数被继承之后也没有更改的必要,属于接口+强制实现继承2、pure virtual函数表示只继承接口。3、impure virtual函数表示继承接口和缺省实现
35、 考虑virtual函数接口以外的方法。虚函数可以根据类别自动调用1、非虚函数接口(NVI)接口不是虚函数,调用虚函数,安全性更好,可以继承缺省参数2、函数指针,类似于委托的方法3、C++11的std::function方法,可以将普通函数、函数对象、成员函数、lambda都当做类似于函数指针的对象4、策略设计模型
36、 不要重新定义继承而来的non-virtual函数。非虚函数是接口+实现继承,原则上不能重新定义;此外要保证基类使用的地方派生类也能使用,也不能重新定义基类自有的非虚函数。
37、 不要重新定义继承而来的缺省参数。36条说明了非虚函数是不能重定义的,因此缺省参数也最好不要修改;对于虚函数,由于虚函数是动态绑定,缺省参数是静态绑定,因此缺省函数不具有动态性,比如派生类D继承基类B, class B{public:virtual void foo(int i = 10){printf(“B :%d”,i);}};class D:public B{public:virtual void foo(int i = 20){printf(“D: %d”,i);}};,现在有指向D的B类指针,调用B.foo()实际上调用的是foo(inti = 10){printf(“D:%d”,i);}。
38、 通过复合塑模出has-a或“根据某物实现出”。has-a表示组成,组成类C通过内嵌的类D实现某种功能,C包含D;可以类比设计模式中的composite模式。
39、 明智而审慎地使用private继承。Private继承是实现继承,不继承接口,比较难以驾驭。一般使用public继承+复合或者pimpl来代替private继承。
40、 明智而审慎地使用多重继承。多继承难以驾驭,能用单继承和其他模式的,就不要用多重继承;多重继承会给大型系统带来不必要的损耗,导致程序体积膨胀、性能下降。
41、 了解隐式接口与编译期多态。模板类的模板类型T,虽然不清楚T的类型,但是实际使用中可能会使用T的接口。如果T没有给定的接口,在编译时就会报错。
42、 了解typename的双重意义。在表示模板的时候typename和class可以通用。如果使用模板类型的内部定义,那么需要添加typename。T::const_iterator it1 = ...//编译报错 typenameT::const_iterator it2 = ....//正确
43、 学习处理模板基类内的名称。模板类D<T>继承模板基类B<T>,如果直接在D中使用B的函数,那么编译器可能报错。这时候要显式标明我们使用B<T>的函数:using B<T>::foo;foo()或者B<T>::foo()
44、 将与参数无关的代码抽离template。假设类D有两个模板参数<T,U>,每个模板参数可能有4种选择,那么编译器实作所有的模板类,会产生4*4=16个类。如果类 D<T,U>的实现可以分给类B<T>和C<U>,这样编译器产生的类就有4个B,4个C和4个D,只有12个类。如果模板参数的选择更多,那么这种方式节约的资源会更加明显。
45、 运用成员函数模板接受所有兼容类型。类模板和成员模板可以用两种模板。方法类似44条。
46、 需要类型转换时请为模板定义非成员函数。原因同24。
47、 请使用traits class表现类型信息。Traits技术是;程序会根据类内部的成员类型来调用针对该类的函数。Traits技术代替了if判断语句+typeid函数的传统方法,转而利用编译器来选择相应的处理函数。该类方法广泛用在STL编程中,亦称为type-trait技术。
48、 认识template元编程。指某类计算机程序的编写,这类计算机程序编写或者操纵其他程序(或者自身)作为它们的数据,或者在运行时完成部分本应在编译时完成的工作,模板元编程常常实现后者。模板元编程的优点有:1将工作从运行期转移到编译期,使得错误更容易发现2可执行文件占用内存低,运行时间短。缺点有1十分冗长的编译时间2使用难度大,被称为“学院派编程”。
49、 了解new-handler的行为。当堆分配函数operator new分配出错,就会调用一个错误处理函数,它可以是用户指定的函数,这就是new-handler,new_handler函数无返回值,没有参数,一般最好抛出一个异常。当operator new函数失败,没有申请到内存,会不断调用new_handler函数,知道找到足够内存。因此一个不做任何事情的new_handler函数会导致程序假死等不正常的行为。正常的new_handler函数可以选择做以下事情:1释放其他内存,提供可用内存2安装另外一个new_handler,根据实际情况做些其他事情3抛出bad_alloc异常4调用abort或者exit。
50、 了解New和Delete的合理替换时机。使用自定义new和delete的情景:1在调试时检测和输出运行的错误2强化效能。典型的就是内存池技术,减少内存碎片,减少申请空间和释放空间的消耗3收集内存运行信息。普通程序猿写出高效通用的内存管理工具很难,一般使用著名的开源工具或者购买商用库。
51、 编写new和delete时需固守常规。New和delete有常规通用的标准模式。
52、 写了placement new也要写placement delete。如果我们编写了自定义的placement new函数,结果出错了,那么系统就会调用placement delete函数来释放空间。如果我们写了placement new而没有编写placement delete函数,那么系统会不会释放空间就不知道了
53、 不要轻易忽视编译器的警告。编译器的警告不一定是简单的问题:如果某些代码按照你的原始意图来说确实是错误的,但是编译器恰恰又可以按照其他语意使其编译通过,这样很难找到错误所在。GCC对 ISO C++标准贯彻的很好,在GCC最高警告级别下,编写没有警告的代码,基本上可以在任何平台的C++编译器上编译通过。
54、 熟悉包括TR1在内的标准程序库。在C++11正式版出来之前,C++11部分特性已经封装在较新的编译器,如vs2008sp1的tr1模块中。其他大部分还未正式纳入C++11标准的模块,存放在boost库中。这些准标准库可以大大减轻工作量。
55、 熟悉boost。如果已经开始使用C++11标准,那么boost库里面很多东西你会觉得眼熟。Boost库里面未纳入标准的模块,可以应用在很多开源环境中;如果是高强度的工业环境中,使用boost库需要慎重。