Table of Contents


02. 尽量以const、enum、inline、代替#define

const

#define RATIO 1.5
const double ratio=1.5;
  • #define是预处理器,当出现编译错误信息时,错误信息是1.5,而非RATIO。通常将其定义成const常量。
  • #define是宏定义,编译时会替换,可能导致多次复制。

两种特殊情况:

  1. 常量指针。由于常量定义式通常在头文件中,因此有必要将指针声明为const
  2. class专属常量(static const)。通常c++的任何东西都要有定义式,但class静态常量除外,只要不取它的地址。
const char* const name = "Scott";   // 两个const
const string name("Scott");         // 建议用string

class A(){
private:
    static const int Num = 5;       // 常量声明式,类内设初值
    int arr[Num];
    static int a;                   // 静态变量,类外设初值
}
const int A::Num;                   // 常量定义式,由于在声明时有初值,此处不能再设值
int A::a = 10;

enum

enum的行为更像#define而非const,因为无法取地址。

class A(){
private:
    enum { Num = 5 };	// 让Num成为5的一个记号。
    int arr[Num];
}

inline

想要用宏定义函数时,将其改成template inline函数。

03. 尽可能使用const

const char* const p = "hello";	// 第一个const表示指针指向的数据是const
				// 第二个const表示指针本身是const
const int& f (const& int) const { }	// 最后一个const表示成员函数不修改成员值

const成员函数

成员函数只是常量性不同,也会被重载。const成员函数不能修改任何非static成员变量。

两种情况:

  1. 修改部分成员:mutable成员在const成员函数中也会被修改
  2. 进行同样操作的constnon-const函数会冗余:让non-const版本调用const版本
class A{
private:
    mutable int a;
}

class A{
public:
    const char& operator[](int pos) const { }
    char& operator[](int pos){
        return const_cast<char&>(static_cast<const A&>(*this)[pos]);
    }
// 第一次在*this加上const,是的它调用const版本函数。第二次将函数返回值的const去掉。
}

04. 确定对象使用前已被初始化

  1. 在所有对象使用之前先初始化
  2. 成员变量初始化在构造函数之前,因此构造函数的最佳写法是用初始化列表; 如果成员变量是constreference,则一定需要初值,不能被赋值; 成员变量以声明次序初始化,与初始化列表顺序无关。
  3. 不同编译单元内的非局部静态对象初始化次序没有明确定义

解决方法:将非局部静态对象搬到函数内,函数返回指向这个对象的引用

A& f(){
    static A a;
    return a;
}

05. 了解c++默认调用哪些函数

编译器不会生成拷贝构造函数的情况:★★

  • 含有referenceconst成员的类(原因:referenceconst是不可改变的,因此禁止复制)
  • 基类的拷贝成员函数为private

06. 禁止编译器生成的函数

禁止拷贝构造函数,就将其声明为private,并且不实现它

class A{
private:
    A(const A&);
    A& operator=(const A&);
}

如果在成员函数或友元函数中调用它,会发生连接器错误。可以通过定义一个基类将错误提前至编译器。

class Uncopyable{
protected:
    Uncopyable() {}
    ~Uncopyable() {}
private:
    Uncopyable(const Uncopyable&) {};
    Uncopyable& operator=(const Uncopyable&){};
}

class A: private Uncopyable { }

07. 虚析构函数

  • 带有多态性质的基类应该有虚析构函数。只要类有虚函数,就要有虚析构函数。
  • 如果类不是基类,或不具备多态性,就不该有虚析构函数。
  • string类不含虚函数,因此继承时可能发生错误★★

09. 构造和析构函数不调用virtual成员函数

构造函数期间,virtual成员函数不具备多态性,不会下降至派生类成员函数。

11. 在operator=中处理自我赋值

A& A::operator=(const A& rhs){
    Bitmap *p = pb;             // 先创建副本,再处理
    pb = new Bitmap(*rhs.pb);
    delete p;
    return *this;
}

A& A::operator=(const A& rhs){
    A temp(rhs);
    swap(temp);                 // copy and swap 技术
    return *this;
}

13. 以对象管理资源

  • shared_ptr管理资源,以便做到自动释放资源,即使中途会发生不可测的事件
  • 用一个新的对象存储需要被自动释放的资源,依靠对象的析构函数释放资源。
  • 在资源管理类中发生拷贝操作时,采用智能指针而非普通指针,避免错误释放资源
  • 获得原始指针:get()函数,注意此时内存并没有释放
  • 用独立语句将新建对象置入智能指针中
f(shared_ptr<A>(new A()), g());
// g在何时调用不确定。如果g调用出现异常,可能使A的指针丢失

20. 用常量引用传递代替值传递

  • 避免对象的复制以及相应的拷贝析构操作
  • 避免对象切割。指针可以实现派生类到基类的转换,值传递不行。
void f(A a);
void f(const A& a);

23. 宁以非成员非友元代替成员函数

  • 非成员非友元函数有更强的封装性:便利函数
  • 将便利函数放在多个头文件但隶属于同一个命名空间。类成员函数做不到。

24. 若所有参数皆需类型转换,请采用非成员函数

令类支持类型转换通常是个糟糕的想法。

class Rational{
public:
    Rational(int numerator=0, int demoninator=1);
    // 没有声明explicit,支持int->Rational隐式转换
    int numerator() const;
    int demoninator() const;
private:
    ...
}

const Rational operator*(const Rational& lhs, const Rational& rhs){
    return Rational(...);
}   // 支持 2 * Rational(...)

25. 写不抛出异常的swap函数

默认的swap函数进行了3次复制,有时相当耗时。

实现类的swap

class A{
public:
    void swap(A& other){
        using std::swap;
        swap(p, other.p);
    }
private:
	Ap* p;
}

namespace std{
// 采用非成员函数实现swap
template<> void swap<A>(A& a, A& b){
    a.swap(b);
}
}

上述例子,在类A中用std::swap实现指针的交换,在std空间实现swap的特例化。template<>表示对std::swap实现特例化,swap<A>表示特例化成A版本。

实现模板类的swap

namespace std{
template<typename T> 
void swap<A<T>>(A<T>& a, A<T>& b){// 错误:函数模板不支持偏特化
    a.swap(b);
}
}

namespace std{
template<typename T> 
void swap(A<T>& a, A<T>& b){// 通常可以重载函数,但std禁止
    a.swap(b);
}
}

namespace A{ // 解决:添加一个新名字空间
template<typename T>
class A{ };

template<typename T>
void swap(A<T>& a, A<T>& b){
    a.swap(b);
}
}

// 调用时
using std::swap;	// 令std::swap可见
swap(...)		// 调用时不加std

27. 减少转型

2种旧式转型与4种新式转型

(T)expression
T(expression)
const_cast<T>(expression)	// 移除常量性
dynamic_cast<T>(expression)	// 执行运行期安全的向下转型,可能耗费巨大,类含有虚函数
reinterpret_cast<T>(expression)	// 执行低级转型,依赖于编译器,不可移植
static_cast<T>(expression)	// 强迫隐式转换
class Derive: public Bass{
public:
    virtual void f(){
        Base::f();		      // 调用Base的f()
        static_cast<Base>(*this).f(); // 将此对象转型成Base类,然后调用f()
    }
}

28. 避免返回handles指向对象内部成分

handles:引用、指针、迭代器

如果const成员函数返回内部成员的handles,则函数的调用者依然可以修改数据,即使函数声明为const。可以将返回类型声明为const解决此问题,但依然可能导致悬空指针。

29. 为异常安全而努力是值得的

异常安全的函数:

  • 不泄露资源:shared_ptrLock
  • 不破坏数据

copy-and-swap:

struct NeedChange{		// 将需要修改的成员置于新的类中
    shared_ptr<...> ...;
}
void A::change(istream& src){
    using std::swap
    Lock ml(&mutex);	// 互斥锁
    shared_ptr<NeedChange> pNew(new NeedChange(*pSrc));	// 获得副本
    pNew->...;		// 修改副本
    swap(pSrc, pNew)	// 置换
}

30. 透彻了解inline的里里外外

inline:将对函数的每一个调用都替换成函数本体。不需要承受函数调用导致的额外开销,却可能导致代码膨胀。

定义:

  • 类内定义
  • 类外加上关键词inline

templateinline通常置于头文件内,因为编译器需要在编译时知道函数长什么样。

编译器拒绝对复杂代码进行inline,如循环、递归、虚函数。编译器通常不对通过函数指针调用的进行inline

inline无法随着程序库的升级而升级,改变f()将需要对函数重新编译,而动态链接则简单得多。

大多数调试器无法调试inline,因为无法设置断点。

31. 将文件间的编译依存关系降到最低

class B;
class A{
private:
    B b;    // 与类B产生关系,编译时需要B的定义式
}           // 当B变化时,A需要重新编译

// method 1
class AImpl;
class A{
private:
    shared_ptr<AImpl> pImpl; // 编译时不需要B的定义式,实现接口分离
}

// method 2
// 将A写成抽象类

33. 避免遮盖继承而来的名称

public继承会继承基类的所有成员,覆盖基类的成员,private不同。

class B{
public:
    void f(){..}
    void f(int x){..}
}

class D: private B{
public:
    void f(){..}		// 覆盖基类所有f
    void f(){ 
        using B::f();	// 使B的函数可见,相当于只覆盖一部分
        f();
    }
}

34. 区分接口继承和实现继承

#声明一个纯虚函数是为了让派生类继承其函数接口

告诉派生类必须继承函数f(),但不干涉它怎么实现

#声明一个非纯虚函数是为了让派生类继承其函数接口和默认实现

告诉派生类必须继承函数f(),如不继承,就用基类的默认实现

有些人认为接口和默认实现应该分离

class B{
public:
    virtual void f(...) = 0
}
void B::f() {...}		// 提供基类的默认实现

class D: public B{
public:
    void f(...) { B::f();}	// 使用基类的默认实现
    void f(...) { ...}	// 自己实现
}

#声明一个非虚函数是为了让派生类继承其函数接口和一份强制实现

35. 考虑virtual函数以外的其他选择

#由非虚接口实现模板方法模式

A的派生类可以重新定义g(),但不能调用它;A确可以调用

允许派生类重新定义虚函数,赋予它们“如何实现”的能力,而基类控制函数“何时调用”。

class A{
public:
    void f() const{
        g();		// 可以做些其他事
    }
private:
    virtual void g() const {...}
}

#由函数指针实现策略模式

  • 同一类的不同实体可以有不同的函数
  • 某个实体的函数可以在运行期变更
  • 纯粹依赖于类的public接口的信息,如果需要private信息就会有问题
class A;
void f_default(const A&);

class A{
public:
    typedef void (*fp)(const A&);
    explicit A(...);
    void f() const { return p(*this);}
private:
    fp = p
}

#由function实现策略模式

class A;
void f_default(const A&);

class A{
public:
    typedef function<void (const A&)> fp;
    explicit A(...);
    void f() const { return p(*this);}
private:
    fp = p
}

#古典策略模式

将策略函数实现为一个派生类

37. 绝不重新定义继承而来的默认参数值

虚函数动态绑定,默认参数值静态绑定。如果默认参数值也动态绑定,则编译器的效率会大幅降低。

当你想要实现默认参数的机制时,所有类需要有相同的默认参数,显得冗余又相互依赖。解决办法是使用之前的设计模式,如非虚接口

class A{
public:
    void f(int x=1) const{	// 不被继承,使得默认参数始终为1
        g(x);
    }
private:
	virtual void g(x) const = 0;
}

class B: public A{
private:
    virtual void g(x) const;
}

39. 明智而审慎的使用private继承

private继承:

  • 派生类对象不会转化成基类对象
  • 基类中的成员在派生类中都是private

private继承与复合都能实现“根据某物实现出”,尽量使用复合,除非牵扯到protected成员和虚函数。

class Timer{
public:
    virtual void onTick() const;
}

// Widget想利用Timer的计时功能,但本身不是个计时器,因此要private继承
class Widget: private Timer{
private:
    virtual void onTick() const;
}

// 复合模型
class Widget{
private:
    class WidgetTimer: public Timer{
    public:
        virtual void onTick() const;
    };
    WidgetTimer timer;
}

两者不同点:

  • private继承中,Widget的派生类不能调用虚函数,却能定义虚函数(见35);在复合模型中,WidgetTimer是私有成员,Widget的派生类无法调用,也就不能定义虚函数。
  • private继承中,Widget依赖于Timer,需要Timer的定义式;在复合模型中,WidgetTimer可单独存放,在Widget中保存指向WidgetTimer的指针,使编译依赖性最小化。
  • 当派生类想要访问基类的protected成员,或重新定义虚函数时,使用private继承。
  • 当类A不带任何数据(非静态成员、虚函数、虚基类)时,继承类A的对象不占空间(空白积累最优化),包含类A的对象占1字节。类A可以含有typedefenumstatic成员变量。

40. 明智而审慎的使用多重继承

当存在钻石型多重继承时,应该声明虚继承

由于虚继承比普通继承更耗费资源,因此有两条建议:

  • 必要时使用虚继承
  • 虚基类中尽可能避免放置数据

一个正当用途:public继承接口,private继承某个协助实现的类

41. 了解隐式接口和编译期多态

  • 类:显示接口、运行期多态
  • 模板:隐式接口、编译期多态
  • 显示接口:由函数签名式(函数名称、参数类型、返回类型)构成
  • 隐式接口:由有效表达式构成

42. 了解typename的双重意义

  1. 声明模板参数时,classtypename可以互换
  2. 模板内出现的名称如果相依于某个模板参数,称之为从属名称。如果从属名称在内呈嵌套状,称为嵌套从属名称。c++解析器在模板中遭遇嵌套从属名称,它便假设这个名称不是个类型,除非加上关键字typename
template<typename T> f(){
    T::const_iteration iter(...);		    // wrong
    typename T::const_iteration iter(...);  // right
}

例外:typename不能出现在基类列表的嵌套从属名称之前,也不能在成员初始化列表中作为基类的修饰符。

template<typename T>
class D: public B<T>::Nested{               // 基类列表,不加
public:
    explicit D(int x): Base<T>::Nested(x){  // 成员初始化列表,加
        typename Base<T>::Nested temp;      // 加
    }
}

另一个常见例子

template<typename T>
void f(T iter){
    typename std::iterator_traits<T>::value_type temp(*iter);
    // 换一种写法
    typedef typename std::iterator_traits<T>::value_type value_type;
    value_type temp(*iter);
}

43. 学习处理模板化基类内的名称

template <typename T>
class Base{
public:
    void f() {...;}
}

template <typename T>
class Derive: public Base<T>{
public:
    void g(){
        f();                    // error
        this->f();              // method 1
        using Base<T>::f();     // method 2
        f();
        Base<T>::f();           // method 3,虚函数会因此解绑定
    }
}

45. 运用成员函数模板接收所有兼容类型

构造模板

带有基类-派生类关系的两类分别具体化某个template,产生出来的两个具现体不带有基类-派生类关系。模板可能被无限量的具体化,可能有无限量的构造函数,因此需要写一个构造模板

构造模板可以使得模板的多个实例之间产生“关系”(如继承关系)。

template<typename T>
class SmartPtr{
public:
    // 没有声明explicit,支持指针的隐式转换
    template<typename U> 
    SmartPtr(const SmartPtr<U>& other)
        : heldptr(other.get()) {...}	// 支持U*到T*的转换
    T* get() const {return heldptr;}
private:
    T* heldptr;
}

支持赋值操作

当声明泛化拷贝构造函数时,也要声明普通的拷贝构造函数。

template<class T>
class shared_ptr{
public:
    template<class Y> explicit shared_ptr(Y* p);
    shared_ptr(shared_ptr const& r);
    template<class Y> shared_ptr(shared_ptr<Y> const& r);
    template<class Y> explicit shared_ptr(weak_ptr<Y> const& r);
    template<class Y> explicit shared_ptr(auto_ptr<Y>& r);
    shared_ptr& operator=(shared_ptr const& r);
    template<class Y> shared_ptr& operator=(shared_ptr<Y> const& r);
    template<class Y> shared_ptr& operator=(auto_ptr<Y>& r);
}

46. 需要类型转换时请为模板定义非成员函数

参考24

函数模板在实参推导时从不将隐式类型转换函数考虑在内。类模板不存在这种情况。

template<typename T>
class Rational{
public:
    Rational(const T& numerator=0, const T& demoninator=1);
    const T numerator() const;
    const T demoninator() const;
    // 声明时Rational<T> 可以被替换成Rational
    friend const Rational operator*(const Rational& lhs, const Rational& rhs){
        return Rational(...);
    }// 这个是函数,不是函数模板
}

解释:为了让所有可能的类型转换发生于实参上,需要非成员函数;为了让函数被具现化,将它声明在类中;在类内声明函数,让其成为friend。内联函数可能会带来巨大开销,可以考虑令友元函数调用辅助函数。

47. 请使用traits class表现类型信息

  • traits类使得类型相关信息在编译期可用
  • 重载技术使得traits类可以在编译期对类型执行if-else检测

例子:对迭代器实现advance操作

template<...>
class deque{
public:
    class iterator{
    public:	
        typedef random_access_iterator_tag iterator_category;
    };
}

template<typename IterT>
struct iterator_traits{
    typedef typename IterT::iterator_category iterator_category;
}

template<typename IterT>
struct iterator_traits<IterT*>{
    typedef random_access_iterator_tag iterator_category;
}

// 定义各种类型的重载版本
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag){
    iter += d;
}

// 调用重载函数
template<typename IterT, typename DistT>
void advance(IterT& iter, DistT d){
    doAdvence(iter, d, typename std::iterator_traits<IterT>::iterator_cateroty());
}

49. 了解new-handler的行为

operator new抛出异常之前,它会先调用客户指定的错误处理函数,称为new-handler。客户通过set_new_handler指定new-handler。

typedef void (*new_handler)();                          // new_handle为函数指针
new_handler set_new_handler(set_new_handler p) throw(); // 返回当前的new_handler,参数为新的new_handler

operator new无法申请足够的内存时,它会不断调用new-handler函数。因此,一个设计良好的new-handler必须有一下特征:

  • 让更多内存可被使用
  • 调用新的new-handler
  • 抛出bad_alloc

有时想为每个类设计专属的new-handler,只需令每个类声明自己的set_new_handleroperator new。自己声明的函数会调用系统的函数,要保证调用完后恢复系统函数的状态,因此可以利用资源处理类(13)

// handler资源管理类
class NewHandlerHolder{
public:
    explicit NewHandlerHolder(std::new_handler nh) {...}
    ~NewHandlerHolder() { std::set_new_handler(handler); }
}

class Widget{
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;
}

// 定义
std::new_handler Widget::currentHandler = 0;
std::new_handler Widget::set_new_handler(std::new_handler p) throw(){
    std::new_handler oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}
void* Widget::operator new(std::size_t size) throw(std::bad_alloc){
    // std、widget保存currentHandler, h保存之前的handler,
    // h析构时将std设为之前的handler
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    return ::operator new(size);
}

可以将类Widget重新定义成基类模板,具体的类只需继承自这个模板即可。

50. 了解new和delete的合理替换时机

定制自己的operator new的理由:

  • 检测内存运用上的错误:可以超额分配内存,以额外空间放置位签名。delete时依据签名来判断内存是否出错,并找出错误的指针
  • 收集内存使用上的数据
  • 增加分配和归还的速度
  • 将相关对象成簇集中,减少内存页错误
static const int signature = 0xDEADBEEF;
typedef unsigned char Byte;

void* operator new(std::size_t size) throw(std::bad_alloc){
    using namespace std;
    size_t realsize = size + 2 * sizeof(int);
    void* pMem = malloc(realsize);
    if(!pMem) throw bad_alloc();

    *(static_cast<int*>(*pMem)) = signature;
    *(reinterpret_cast<int*>(static_cast<Byte*>(*pMem)+realsize-sizeof(int))) = signature;

    return static_cast<Byte*>(*pMem) + sizeof(int);
}

51. 编写new和delete时需固守常规

//operator new 的继承版本
void* Base::operator new(std::size_t size) throw(std::bad_alloc){
    if(size != sizeof(Base))
        return ::operator new(size);    // 派生类调用系统版本
}

对于继承的operator new[],我们能做的只是分配一块未加工的内存。

对于operator delete,只需记得分配失败时调用了::operator new,那么删除失败时也要调用::operator delete

52. 写了定位new也要写定位delete

通常情况下,new会调用operator new操作,然后调用类的构造函数。如果构造函数出错,就会导致系统泄露,此时运行期系统会调用相应的operator delete,即参数类型和个数与之相对应的operator delete。若没有发生异常,系统调用标准delete。因此,对于定位new,要编写对应的定位delete和标准delete。

在类内编写operator new会遮盖所有全局的operator new函数。解决办法是将标准的operator new放入自定义的基类中,客户可以继承该基类。

class StandardNewDeleteForms{
public:
    // 标准版本
    static void* operator new(std::size_t size) throw(std::bad_alloc)
        { return ::operator new(size); }
    static void operator delete(void* pMem) throw()
        { ::operator delete(pMem); }
    // 定位版本
    static void* operator new(std::size_t size, void* ptr) throw()
        { return ::operator new(size, ptr); }
    static void operator delete(void* pMem, void* ptr) throw()
        { ::operator delete(pMem, ptr); }
    // nothrow版本
    static void* operator new(std::size_t size, const std::nothrow_t& nt) throw()
        { return ::operator new(size, nt); }
    static void operator delete(void* pMem, const std::nothrow_t& nt) throw()
        { ::operator delete(pMem); }
}

class Widget: public StandardNewDeleteForms{
public:
    using StandardNewDeleteForms::operator new;
    using StandardNewDeleteForms::operator delete;

    static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
    static void operator delete(void* pMem, std::ostream& logStream) throw();
}

ShengYg

Step after step the ladder is ascended.


Tags • note