Effective C++ 总结提炼版

导读

  • size_t 只是一个typedef,是unsigned类型,也是vector,deque,string内的operator[]函数接受的类型

  • definition定义式,对于变量:任务是编译器拨发内存,对于function:提供代码本体

  • 判别拷贝构造和拷贝赋值:有无新对象实例的产生,如果是对已有对象赋值->拷贝赋值

    • 参数传递,函数返回,都调用的是拷贝构造。(因为还不是已有对象嘛)
    • 对于已经初始化的对象,再次调用 = 是拷贝赋值
    • 需要注意的是,调用“=”不一定就是拷贝赋值哦,如果还没构造出的话,就是拷贝构造
  • operator= 返回的类型是引用类型&,赋值操作是在函数内就完成的,再次返回对象是为了能连等。

    如果不返回引用的话,就会调用拷贝构造创建出临时对象temp,加大了开销

  • lhs -> left hand size、rhs -> right hand size

  • TR1 -> Technical Report 1(c++新机能)

  • boost 一个组织(网站)提供可移植,源码开放的c++程序库

1. 视C++为一个语言联邦

  • C
  • Object-Oriented C++
  • Template C++
  • STL

额外补充:

  1. 内置类型pass by value 即可, pass by reference 开销更大

2. 尽量以const, enum, inline替换 #define

const:

  1. 首先,从处理阶段来说,const是在编译阶段,#define是在预处理阶段,当它出错时无法追踪

  2. 对于class类内的专属常量来说,要把它的作用域限制在类内(static const xxx)。而对于define来说,define不能定义任何class专属的常量,因为它没有scope的概念,也不提供任何封装性。

    需要注意的是:类里的static const xxx是声明而非定义。声明时就可以给出初始化值了(新特性),定义时就不能再给初值。当然也可以选择声明时不给初值,定义时给初值。

enum:

  1. 对于类中的数组来说,要声明其长度,如果编译器不允许在编译时完成const int N, int a[N] 的设定,可用enum{xxx = x};

inline:

​ 1. 用template + inline 代替 define定义的形似函数宏,不需要在函数本体中为参数加上括号,操心参数被核算

3. 尽可能用const

const 在* 后表示指针不可改,const 在 * 前表示指针指向的内容不可改

  1. 对于迭代器来说:

    • 想声明它本身为const(类似T* const),就直接 const xxx::iterator x
    • 声明它指向的内容为const(类似 const T *)xxx::const_iterator x
  2. 将函数的返回值声明为const,避免造成意外错误,例如operator*的返回值为const,不应该再对它进行修改

  3. **const成员函数:**参数列表后加const:

    • 接口更规范,更容易理解
    • 使得操作const对象成为可能(const对象只能调const成员函数),函数里传入参数大部分都是const引用

    需要注意的:operator[] 可以定义const成员函数,返回const char&,也可以定义非const,返回char&,总之是要char&,因为我们还要对其实现可以赋值的。因为如果仅仅是简单内置类型char,它是一个临时左值引用,没法赋值

  4. bitwise constness 和 logical constness:

    bitwise:const成员函数内不更改任何一个bit**(编译器强制实施)**

    logical:没在类内改,operator[]返回char&后取出地址给指针,通过指针改

    当const成员函数内对非const成员进行了赋值,违反bitwise constness,可以通过在定义变量时在最前面加mutable打破约束

  5. const和non-const成员函数避免重复:

    它们里面有重复的代码,可以通过在non-const里调用const函数。两个转型动作:

    1. 将*this通过static_cast转为const xxx&类型,之后调用它的[]函数(对应的调到的就是const函数)
    2. 拿到之后,再将它转为非常量 xxx&类型,常转非常:const_cast<xxx&>
    return const_cast<char&>(static_cast<const TextBlock&>(*this)[position]);
    

    为什么要在non-const里调const?因为在const里不允许非const值改动的,不能用它调non-const

额外补充:

对于形参为const的和non-const的来说,函数构成重载吗?

  • pass-by-value不构成重载,因为无论是不是const,他们都改变不了传入的参数的值,而且还会构成二义性,到时候不知道调哪个。
  • pass-by-reference 构成重载(传指针或者引用),因为接收的范围不一样了,const A&可以接收const、也可以接收non-const、A&只能接收non-const。当传入参数为non-const时,有带const形参的函数时候,优先调用它。

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

  1. 在构造函数体内初始化不是初始化,而是赋值。它的初始化是在默认构造函数里做的。

    (开销更大,一遍初始化,一遍赋值)

  2. 可以使用初始化列表代替不使用默认构造函数。

  3. 顺序:基类先初始化,对象包含类初始化、class成员变量按声明顺序初始化

  4. static对象:local static 和 non-local static

    • non-local static:global对象、声明于namespace作用域的对象,在class、file作用域的static对象
    • local static:在 function 内的 static对象

    无论non-local还是local static都是在main结束之后析构的,先构造的后析构。

    对于 non-local static 变量是在main函数执行之前初始化的,多个编译单元下的non-local static变量初始化顺序是随机的。

    对于 local static 变量,在其所属的函数被调用之前,该对象并不存在,即只有在第一次调用对应函数时,local static对象才被构造出来。

    对于non-local static 初始化有依赖时,无法确定他们的先后初始化顺序,因此把他们转为local-static。

  5. Singleton模式:

    static XX& GetInstance函数中创建local static 变量并且返回。

    假设有两个类Context类和Log类,Context类和Log类都是单例模式,通过GetInstance得到唯一的局部static变量。Context类里还有一个func函数,它里面调用了Log的GetInstance,并且调用Log里的Output函数。所以调用顺序就是Context->GetInstance->func()->LogGetInstance()->Output()。所以是Context先构造,Log后构造,Log先析构,Context后析构。当Log已经析构了的时候,在Context析构时又调用了Log的Output函数,就会报错。

    解决办法:

    • 对于析构的顺序,我们可以用一个容器来管理它,根据单例之间的依赖关系释放实例,对所有的实例的析构顺序进行排序,之后调用各个单例实例的析构方法,如果出现了循环依赖关系,就给出异常,并输出循环依赖环。
    • 手动先调用GetInstance,先调用Log的,再调用Context的
  6. static T& GetInstance() {
        static T a;		// 多次调用GetInstance时, 它这里只调一次,初始化一次,有个guard变量追踪它
        return a;
    }
    
  7. 懒汉模式

    static T* value;
    // value是最后析构的, 因为它static, 堆里的内容也是最后出main的时候才放的
    // 没有直接创建singleton对象, 没析构,而是最后在 static class CGarbo里析构的
    

5. C++默认编写并调用哪些函数

  1. 都是public且inline:
  • 如果没有生成构造函数,生成默认构造函数
  • 拷贝构造函数
  • 拷贝赋值函数
  • 析构函数(non-virtual)
  1. C++必须使用初始化列表的:
    • 类成员为const(新特性可以声明中初始化)
    • 引用类型
    • 没有默认构造函数
    • 继承时基类没有默认构造函数

6. 不使用编译器自动生成的函数,就明确拒绝

  • A (const A& a) = delete; A& operator=(const A& a) = delete;
  • 继承Uncopyable class(把不想要的设为private,继承下来就不能用了,可以只声明)
  • 声明copy ctor 、copy assignment为private,且不予实现

7. 为多态基类声明virtual 析构函数

  1. 对于工厂创建出对象的记得要delete:

    TimeKeeper* ptk = getTimeKeeper();
    ...
    delete ptk;
    
  2. 对于不做父类不用多态的类,不需要将析构设为虚,会多出来一个指针占内存。导致放不进去64bit的寄存器。可移植性降低

8. 不要让异常逃离出析构函数

  • 析构不要吐出异常,如果析构可能抛出异常,要及时吞下或者结束程序
  • 如果客户(使用代码的人)要对异常做出反应,不应该放在析构里,而是应该放在一个普通函数里做

9. 不在构造和析构中调用虚函数

10. 令operator= 返回一个reference to *this

减小内存开销,减少临时对象生成,减少拷贝构造函数调用次数

(注意返回reference时不能返回栈里的临时局部变量,再用的时候已经销毁了。一般返回 *this,或者堆内的对象,或者是传入的引用参数)

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

对于对象中含有指针指向的内容。在赋值前,我们要先delete掉它再赋值

Widget& operator=(const Widget& rhs) {
    // 已经提前要把赋值的删掉了
    delete pb;
    pb = new Bitmap(*rhs.pb);
    return *this;
}
	// 所以要在delete前判断一下, 是不是自我赋值
	if (this == &rhs) return *this;

用if的话,新的控制流会降低执行速度,所以用copy-and-swap代替。

先拷贝构造一份rhs出来,再swap。因为参数中传入的是const

Widget temp(rhs);
swap(temp);
return *this;

12. 复制对象时勿忘其每一个成分

首先,拷贝构造函数也要用初始化列表。如果不用的话就必须有默认构造。

对于派生类,在定义其复制函数(拷贝构造和拷贝赋值)时,不要忘记对父类进行拷贝构造和拷贝赋值

A是基类,B是派生类,B中有local成员ext

// 拷贝构造
// 注意对于父类的初始化列表, 是把派生类rhs传入了. 对应地去调用A的拷贝构造函数。它的参数是一个const引用
// 在拷贝构造函数中取了它对应需要的内容, 同时因为拷贝构造函数的输入参数是引用(指针), 并未做截断, 仍能多态
B::B(const B& rhs): A(rhs), ext(rhs.ext) {}

B& operator=(const B& rhs) {
    if (this == &rhs)	return *this;
    A::operator=(rhs);
    ext = rhs.ext;
    return *this;
}

注意如果没有对父类进行初始化列表构造的话(它调用父类的拷贝构造),如果父类没有默认构造的话,是会报错

当类的数据成员中有指针类型时,我们就必须定义一个特定的拷贝构造函数,该拷贝构造函数不仅可以实现原对象和新对象之间数据成员的拷贝,而且可以为新的对象分配单独的内存资源,这就是深拷贝构造函数。

(29条消息) 关于带指针的类的拷贝构造、拷贝赋值、析构函数的常用写法以及意义_Hurry_upp的博客-CSDN博客_类拷贝构造函数的指针定义

13. 以对象管理资源

对于new出的对象,我们可能因为下面这几种情况没能delete:

  • continue
  • goto
  • 过早的return

如果把资源放进对象内,我们便可倚赖c++的析构函数自动调用机制,确保资源正确释放。

RAII对象 -> Recourse Acquisition is Initialization 常用的:auto_ptr,shared_ptr

例如auto_ptr,在离开那个区块或函数时被释放。

智能指针就不加*了

std::auto_ptr<Investment> pInv(createInvestment());

对于auto_ptr来说,如果使用拷贝赋值或者拷贝构造,另一个就变成空了,只有一个智能指针能保有它。取而代之的是shared_ptr

注意,在auto_ptr,shared_ptr里的析构函数做的都是delete而非delete[]。在动态分配的数组上用他们不好

14. 在资源管理类中小心copying 行为

并非所有资源都是heap-based,例如定义一个class去操作mutex,Lock类中存一个Mutex*,在构造函数中传入Mutex*,在初始化列表中初始化,并在函数体中使用lock上锁。在析构函数中使用unlock解锁。

当Lock对象被析构时,自动解锁。

当RAII被复制时有如下两种可能:

  • 禁止复制(继承Uncopyable / delete掉拷贝构造和拷贝赋值)

  • 可以复制

    • 深copy

    • 转移权限

    • 对底层资源引用计数法(通常内含一个shared_ptr成员)

      由Mutex *mutexPtr 变为 shared_ptr<Mutex> mutexPtr;

      **它不需要再特地定义析构函数,因为shared_ptr可以传两个参数,一个是new出的指针,第二个是指定删除器。在删除器里完成unlock。**在构造函数体上锁时:lock(mutexPtr.get());

15. 在资源管理类中提供对原始资源的访问

(1)get函数显示转换

(2)shared_ptr、auto_ptr中都提供了operator*、operator->来完成隐式转换为底部指针

FontHandle f2 = f1; 注意隐式转换优先于将一个对象直接拷贝构造给另一个对象

16. new和delete成对使用时要采用相同形式

对于内置类型和有默认析构函数的:

  • new[] + delete:

    new[]不会在前面+4个字节存放需要析构的次数。因此在delete的时候只析构了一次,且并没有-4,正确释放

  • new + delete[]:

    delete[] 并不会真的-4个字节后free释放,因此可以正确释放

对于有析构函数的:

  • new[] + delete:

    new[]在前面+4个字节存放需要析构的次数。delete时只析构了一次,如果有指针指向的变量,会造成内存泄露,且没有-4,free时找错位置,程序崩溃

  • new + delete[]:

    将地址-4了,越界了,释放的也不是原来的地址

17. 以独立语句将newed对象置入智能指针

函数形参是shared_ptr<Widget> pw时,不能传入 new Widget作为参数给shared_ptr<Widget>接收,因为它的构造函数是显示调用的explicit。

只能先:定义shared_ptr<Widget> pw(new Widget); 再将pw作为参数传入。

它俩不能分家!以独立语句将newed对象置入智能指针,否则可能会内存泄露

18. 让接口容易被正确使用,不易被误用

tr1::shared_ptr支持定制型删除器。这可防范DLL问题,可被用来自动解除互斥锁

对象在DLL中被new创建,却在另一个DLL内被delete销毁。

在一个DLL中定义一个函数返回shared_ptr对象,但是调用它的delete时,是调用最后一个销毁下的DLL的delete

19. 设计class犹如设计type

运算符重载:

可以定义在类内,也可以将运算符重载定义在类外,但是需要在类中将运算符重载函数声明为友元函数

注意

注意!不是必要地一定要把它定义为友元函数,既然我们让它不当member,当non-member、就不必要让它再friend了,non-member的封装性更大
// 要把 << 定义在类外, 因为定义为成员函数的话, cout就只能再右侧了, 无法在左侧
// 这里是要返回引用, 不是说要连着用就一定是返回&, operator=也是连着用, 但是它可以返回引用, 也可以返回value
friend ostream& operator<<(ostream &cout, Person p);

判断是否返回引用的根据是:看它是否要当左值,如果要继续当左值,就要返回引用

类比前++和后++,前++可以连用的原因就是因为它返回引用,它是左值,可以继续操作它

前置++(无参):

Point& operator++() {
    x++;
    y++;
    return *this;
}

后置++(有参,一个int)

Point operator++(int) {
    Point temp = *this;
    ++this;
    return temp;
}

20. 宁以pass-by-reference-to-const 替换pass-by-value

注意!pass-by-value不止是拷贝构造一次,析构一次。

考虑拷贝构造次数时,要把自己成员变量的构造和父类的构造都考虑进去。

例如一个类里有两个string,它有一个父类,里面也有俩string,每pass-by-value一次,它要拷贝构造10次,析构10次。

好处:

  • 减少拷贝构造/析构函数调用次数
  • 避免对象被切割(特质化被切割后,实现不了多态了,无法调用虚函数)

什么时候不用:

  • 内置类型、STL迭代器、函数对象用pass-by-value;

21. 必须返回对象时,别妄想返回reference

const Rational operator*

*之后不能再赋值了,它只能当右值,我们给他加const

不要返回pointer/reference指向一个local stack对象,如果这个对象不存在时,就返回一个value吧!

返回reference:

  • heap里的
  • *this
  • 传入的形参

22. 将成员变量声明为private

将成员变量声明为private。赋予客服访问数据的一致性

protected不比public更具封装性。

衡量方法:一但更改属性,会有很多代码失效。

成员对象改变(从class中移除)时破坏的代码量与封装性成反比。

23. 宁以non-member、non-friend替换member函数

member成员函数带来的封装性比non-member函数低

因为它并没有增加能访问class内部private成员变量函数的数量

将所有遍历函数放在多个头文件内但隶属于同一个命名空间,意味着客户可以轻松扩展这一组遍历函数。他们需要做的就是添加更多non-member non-friend函数到此命名空间中。

24. 若所有参数皆需类型转换,请为此采用non-member函数

例如要对一个对象写operator *操作,当写成member函数,*后面的参数可以使用隐式转换,*前面的参数无法隐式转换,而对于比如int类型本身,并未定义和该class的operator函数。

因此要使用non-member函数,支持混合式算术运算

注意,当成为non-member函数时,并不意味着他就一定要设置为friend函数。如果想访问对象的私有变量,还是可以通过公共接口来访问的。都定义为friend函数,封装性还是低的

25. 考虑写出一个不抛出异常的swap函数

普通的swap函数:传入两个对象的reference(不加const),定义一个临时对象,通过赋值进行值的整体交换。

(=直接调用了operator =,会负责把指针指向的对象也进行重新赋值(如果!=this,delete掉旧的, new 新的))

Widget类使用Pimpl手法,当swap Widget对象时,只需要交换其指针即可,不需要复制指针对象里的数据

Pimpl

Pointer to implementation

pimpl idiom 如何改善软件开发生命周期:

  • 编译依赖项的最小化。
  • 接口和实现的分离。
  • 可移植性。

情况1:

特化版本:

namespace std{
    template<>
    void swap<Widget> (Widget& a, Widget& b) {
        swap(a.pImpl, b.pImpl);
    }
}
// 但是PImpl是private属性, 不能直接这样交换

所以在Widget类内需要声明一个名为swap的public成员函数做真正的置换工作

class Widget{
public:
    void swap(Widget& other) {
        // 注意这里要使用using std::swap, 表示这里是要使用std下普通的swap
        using std::swap;
        swap(pImpl, other.pImpl);
    }
};
namespace std{
    template<>
    void swap<Widget>(Widget& a, Widget& b) {
        a.swap(b);
    }
}

STL容器也都提供public swap成员函数和 std::swap特化版本

情况2:

专属版本

如果Widget是class template,不是class,而是class template

如果我们想用偏特化来实现swap函数,就大错特错了

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

c++只允许偏特化class template,不允许偏特化 function template

当打算偏特化一个function template时,惯常的做法是为它添加一个函数重载版本

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

但是std内禁止添加新的templates到里面。

所以最终我们还是声明了一个non-member swap函数, 让它调用public member swap,但是是栖身于另一特定的命名空间

namespace WidgetStuff{
    template<typename T>
    class Widget{...};
    template<typename T>
    void swap(Widget<T>& a, Widget<T>& b) {
        a.swap(b);
    }
}

总结:

  • 编译器会优先调用特化版本 / 专属版本的swap,如果没有,才要用std内的swap
  • public member swap + non-member 特化版本swap / 专属版本swap(另一命名空间)

26. 尽可能延后变量定义式的出现时间

  1. 避免无意义的默认构造函数和析构函数(因为你早早的就定义了),而且如果当异常发生了,它就真的没被用,但是构造和析构也确实都做了,应该直到使用前一刻再定义
  2. 对于循环。把变量定义在外面,一次构造+一次析构+n次赋值 要好过n次构造和n次析构

27. 尽量少做转型动作

  • static_cast 强迫隐式转换(pointer-to-base 转为 pointer-to-derived,无法将const to non-const)
  • const_cast 可以将常量性转除
  • dynamic_cast 安全的向下转型,成本高
  • reinterpret_cast 低级转型,可以将pointer to int 转为 int

Window父类,SpecialWindow是子类

如果想在SpecialWindow里的OnResize函数里调父类Window的OnResize函数只能通过Window:OnResize()调,不能static_cast<Window>(*this).OnResize(),因为当前对象实际上没改动,改动的是副本。

dynamic_cast会耗用多达四次的strcmp调用

使用dynamic_cast想使用的原因:你想调derived class对象的方法,但是你只有一个指向base的指针。

两个解决方法:

  1. 使用容器并在其中存储直接指向derived class对象的指针

    (不要把base指针转为derived指针,容器里直接存储derived的指针)

  2. 在base class内提供virtual 函数做你想对各个派生类做的事情

如果在vector中保存Window,然后再循环迭代器用的时候再用dynamic_cast转成对应的子类指针再调用他们的方法。产生出的代码又大又慢,而且基础不稳定,只要继承体系一有改变,就得再重新修改代码,加入新的条件分支

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

当函数的方法返回的是引用时,为了避免在外面被改变,返回值要加上const。

但是这还是会造成悬空号码牌

GUIObject * pgo;
const Point* pUpperLeft = &(boundingBox(*pgo;).upperLeft());

语句结束之后对象已经不在了,临时对象已经析构了。pUpperLeft指向一个不存在的对象

但是这并不意味着绝对不可以让member函数返回handle,例如operator[]就必须给返回handles

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

用对象管理资源(条款14的Lock,析构的时候释放),这样不会导致异常抛出了,但是最终锁没法释放。

异常的三个保证:

  • 基本承诺(异常被抛出,任何事物都还在有效状态下,不能崩溃了)
  • 强烈保证(异常抛出,程序状态不改变,如果函数失败,程序回复到调用函数前的状态)
  • 不抛掷保证

利用智能指针std::tr1::shared_ptr去存Img,当要更换背景图的时候直接reset就好了,不需要你先delete旧的,在new新的,系统自动就做了。

std::tr1::shared_ptr(new Image(imgSrc));这个删除动作只会在new 成功之后才进行,不会出现已经删了但是没new成功的现象。

一个实现强烈保证的设计策略:copy and swap

当要修改的时候,先做出一份副本,在副本上做一切必要的修改。

以对象管理资源 + 要变的都放到一个结构体里用指针指向(Pimpl) + copy and write

比如说都放到 struct PMImpl里,然后使用shared_ptr<PMImpl>。

注意使用swap前,要先using std::swap,不用害怕,如果有特化或专属会优先调用的

在操作的函数中:先创建一个副本(调用拷贝构造函数),再在副本上做操作,然后swap

代码见 P131

对象管理资源:对于锁来说,防止异常抛出后无法释放锁

对于其他指针数据,要用shared_ptr的指针来说,防止delete了旧的指针后,新的new不出来

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

inlining会让程序体积过大,膨胀导致额外的换页行为。

inlining是编译期的行为。

写成Inlining不一定是真inlining

函数指针指向inline函数还是普通函数

inline可以操作类的私有成员,宏不可以。

virtual 会使 inline 落空,因为virtual是运行时的

过程调用时:P调用Q

  • 传递控制,程序计数器PC设置为Q的起始地址
  • 传递数据,P向Q提供一个或多个参数
  • 分配和释放内存

返回的地址P调用Q的下一行插入到栈帧内,P的栈帧中:先是参数,再是返回地址,Q的栈帧中:被保存的寄存器,局部变量,参数构造区。 参数可以放在寄存器中,寄存器不够放时才放栈帧中。

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

类中直接引入其他类的成员,需要引入它们对应的头文件,造成了连串的编译依存关系。

  • Handle Class(PImpl)

  • Interface class

Handle class:

因此可以把类的接口和实现分离 -> PImpl

  • 依赖关系降至最低
  • 接口和实现分离
  • 可移植性

不需要引入对应类成员的头文件了,只需要它们的前置声明。

Person类中有PersonImpl的shared_ptr,在构造函数时构造这个指针。因为构造时需要用到Date、Address成员,所以引入它们的前置声明即可。

如果能够,尽量以class声明式替换class定义式。当声明一函数而它用到了某个class时,并不需要class的定义

class定义式一般是通过include完成,可以为类声明和类定义提供不同的头文件,这样符合规格。

在定义Person.h的时候,PersonImpl也是只用前置声明就ok

在实现Person成员函数的时候,当然需要引入Person.h,也必须引入PersonImpl.h,因为要使用它的成员函数

在Person的构造函数中要new出PersonImpl,注意哦,对于shared_ptr的初始化,是在初始化列表里做的

Interface Class:

抽象类:包含纯虚函数

不给类添加任何成员变量,只是定义虚析构函数和纯虚函数

Interface Class定义一个static成员函数,返回shared_ptr指针指向创建好的对象,再通过这个指针调用函数

在RealPerson里定义具体的方法,并且给出自己的构造函数,在Person的static函数create中调用RP的构造函数

具体使用到的.h文件,是要引入到PersonImpl.h和RealPerson.h中

32. public继承 -> is-a关系

另外两个常见的关系:has-a、is-implemented-in-terms-of

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

对于同名的函数,查看作用域的先后顺序如下:

  1. local (在哪里调用的这个函数?调用的这个范围内有没有这个函数?)
  2. class derived
  3. class base
  4. 含base的namespace
  5. global namespace

子类中出现同名函数,无论父类中的参数类型和子类的是否相同,都会被遮掩

静态多态(重载)只能出现在本类中

**希望隐藏的可见,用using加进来。**那么对于父类和子类同名但参数不同的函数,也能调用了

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

pure virtual 函数是可以提供定义的

  • 声明为pure virtual function是为了让derived classes只继承其接口,并且必须在自己这给出定义

  • 声明为impure virtual function是为了让derived classes继承接口和缺省实现,继承来的可以不重写

    一般来说,我们都要定义一个缺省操作,protected default函数,给Derived class可以用它作为缺省实现

    也可以不定义这个方法,而是在pure virtual function时就给出函数

  • 声明为non-virtual函数的目的就是为了令derived classes继承函数的接口和一份强制性实现

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

  1. 籍由Non-virtual Interface手法实现Template Method模式

    virtual函数都是private,用public non-virtual函数调private virtual函数

    (non-virtual interface NVI)

    确保在virtual函数调用之前完成事先工作和事后处理工作

  2. 籍由Function Pointer实现Strategy模式,函数指针替代virtual函数

    在类里面存函数指针变量(private)

    在构造的时候就传不同的函数指针进去

    typedef int (*HealthCalcFunc) (const GameCharacter&);
    private:
    	HealthCalcFunc healthFunc;//(在构造函数中构造它, 在一个public方法里再调这个函数指针)
    public:
    	int healthValue() const {
            healthFunc(*this);
        }
    
  3. 籍由tr1::function完成Strategy模式(tr1::function替换virtual函数

    tr1::function可持有调用物:

    • 函数指针
    • 函数对象
    • 成员函数指针
    typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
    

    其他的调用方法和上面一样,就是可以传入的范围变大了。

  4. 古典的Strategy(将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。

    GameCharacter类里复合一个HealthCalcFunc的对象指针。

    在HealthCalcFunc对象里还能通过不同的子类重写虚函数

36.绝不重新定义继承而来的non-virtual函数

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

调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型

当实现动态多态时,都是通过基类指针来调用的,所使用到缺省默认参数都是基类那个虚函数的默认参数,而非调用时,深入到子类具体函数时,它的那份参数。

virtual函数是动态绑定,缺省参数值是静态绑定

当重复定义继承而来的缺省参数值时,如果基类的那个缺省参数值一变,那后面的也都得跟着变

聪明的做法:NVI -> 把缺省函数放到外面套着的那个public member function里,virtual 定为private

38. 通过复合实现has-a

用list实现Set,但是Set不是list,所以要用has-a实现,复合。

is-implemented-in-terms-of

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

首先:private继承不是is-a,而是is-implemented-in-terms-of(根据某物实现出)

private继承而来的成员,都会变成private

什么时候要用private继承:不是is-a,且要访问protected成员变量,或者重写virtual函数时

替代private继承的方式:复合 + public继承

在类中复合一个另一个类的指针,另一个类public继承我们要访问protected/修改virtual函数的类

什么时候必要用private?空白基类优化

当一个空白类组装到另一个类中,编译器自动会给它填充一个char大小。不会让它大小为0的,当然后续如果涉及到字节对齐,那么占用的空间更大。因此可以通过private继承来代替掉这部分开销。

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

如果出现菱形继承,那就要在菱形腰部,继承上面的时候,把两个/多个向上继承改为virtual继承。防止再往下继承的时候出现多份成员变量/成员函数。

多重继承:public继承接口,private继承实现

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

显示接口:函数名称 + 参数类型 + 返回类型

隐式接口:造成template具现化,具现行为发生在编译期

对于template参数而言,接口是隐式的,基于有效表达式。

多态是通过template具现化和函数重载解析发生于编译期

42. 了解typename的双重意义

template 和 template 没有什么不同

从属名称:template内出现的名称相依于某个template参数 -> C::const_iterator

在知道C是什么前,没有任何办法可以知道C::const_iterator是否为一个类型,甚至它有可能是一个全局变量。因此嵌套从属名称默认被认为不是类型,除非+typename

typename只被用来验明嵌套从属类型名称;其他名称不该有他存在

例外:typename不能出现在base class list里,也不能出现在member initialization list里

常用:从萃取机取出value_type要加typename

//用*iter建立temp副本
typename std::iterator_traits<IterT>::value_type temp(*iter);

太长的时候用typedef 定义这个typename

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

对于要继承类模版的类,在没具现化之前不知道它是什么,不知道它到底有没有某个函数,比如sendClear函数。为什么会没有呢?因为对于类模版来说,它可能有特化版本,特化版本下可能没有普通版本下的某个函数。因此编译器也不知道它究竟有没有那个函数。可能删去了。

有三个办法解决这个问题:

  1. 在base class的函数在derived class类的函数中被调用之前加上"this->"

  2. 在derived class类中的public下声明using,再调用

    using MsgSender<Company>::sendClear;

  3. 明白指出被调用的函数位于base calss内(但是这样就不能多态virtual了)

    MsgSender<Company>::sendClear(info);

上述的处理方法的目的是:向编译器保证base calss template的任何特化版本都将支持一般(泛化)版本所提供的接口。但是若继承了特化版本之后,它真的没有那个函数。编译器还是会报错的。

44. 将与参数无关的代码抽离templates

非类型模版参数:

template<typename T, std::size_t n>
//不同的n, 但是它们具现化了两份相同的invert函数。除了矩阵规模大小不同, 这两份函数内部并没差

解决办法:using + private继承 + 把与参数无关的代码抽离出templates

用private继承只是为了帮助实现,它不是is-a

减少执行文件大小,也就因此降低程序working set(在一个虚拟内存环境下执行的进程而言使用的一组内存页)

在基类中去除掉非类型模版参数,把这个数字(非类型相关)的参数的函数单独做出来放到父类模版中。在子类中也定义invert函数(在里面想调父类的invert(n)函数时要用"this->“)每个不同的类都生成一份相同的模版方法。

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

对任何类型T和任何类型U,可以根据SmartPrt<U>生成一个SmartPtr<T> -> 泛化copy 构造函数

template<typename T>
class SmartPtr {
public:
	template<typename U>
    SmartPtr(const SmartPtr<U>& other):heldPtr(other.get()) {...}
    T* get() const {return heldPtr;}
private:
    T* heldPtr;
};

这个初始化操作需要存在某个隐式转换能将一个U*指针转为T*指针时才能通过编译

对于shared_ptr,要留出多个成员函数模版来接收其兼容类型

  1. 有最普通的裸指针
  2. shared_ptr
  3. weak_ptr
  4. auto_ptr
// 成员函数模版构造函数
template<class Y>
    explicit shared_ptr(Y* p);
template<class Y>
    explicit shared_ptr(weak_ptr<Y> const& r);
template<class Y>
    explicit shared_ptr(auto_ptr<Y> const& r);
// 成员函数模版拷贝构造函数
template<class Y>
    shared_ptr(shared_ptr<Y> const& r);

除了定义成员函数模版构造函数、成员函数模版拷贝构造函数之外,还要定义成员函数模版copy assignment函数

template<class Y>
    shared_ptr& operator=(shared_ptr<Y> const& r);
template<class Y>
    shared_ptr& operator=(auto_ptr<Y>& r);
...

同时还需要定义自己的(非泛化)构造函数、拷贝构造函数、拷贝赋值函数

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

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

Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2;
// 错误, 无法通过编译 -> template实参推导过程中不考虑隐式转换

解决办法:定义一个非成员函数,调用它时,可以让后面的对象完成隐式转换

但是必须要让oneHalf对象被定义时,这个非成员函数就对应地根据template<calss T>自动声明出来

把它定义为friend函数可以很好地实现这个需求。

目的:

  1. 在类内声明non-member function,类具化时函数一并具化出来
  2. 具化出来之后,就可以进行隐式转换了
friend const Rational operator*(const Rational& lhs, const Rational& rhs){
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

注意在类模版中再次声明的函数模版,其参数,返回值就可以不带<T>了,可以省略

可以在类外再定义一个helper template function,在friend函数里面再调它。

friend const Rational operator*(const Rational& lhs, const Rational& rhs) {
    return doMultiply(lhs, rhs);
}

template<typename T>
const Rational<T> doMultiply(const Rational<T>& lhs, const Rational<T>& rhs) {
    return Rational(lhs.numerator() * rhs.numerator(), lhs.denominator() * rhs.denominator());
}

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

迭代器分类:

  1. input_iterator_tag 一次移动一步,只能读取(只能读一次)
  2. output_iterator_tag 一次移动一步,只能涂写(只能涂写一次)
  3. forward_iterator_tag 单向,结合前两个,可读可写,且能多次读写
  4. bidirectional_iterator_tag 双向(set、multiset、map、multimap)关联式容器
  5. random access 常量时间内向前或向后跳跃任意的距离(vector, deque, string)

这些都是struct,他们之间是有效的is-a关系

容器内(deque里、list里,它们也都是template)会设置对应的iterator类。

iterator类内会

class iterator{
public:
    typedef bidirectional_iterator_tag iterator_category;
    ...
};

traits:

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

//偏特化 (对于内置指针)
template<IterT>
struct iterator_traits<IterT*>
{
    typedef random_access_iterator_tag iterator_category;
}

可以在if里判断迭代器的类型,并决定是让他+=,还是++/–

但是迭代器类型判断是template是在编译期就可以得知,if要到运行期才知道要走哪一步。

因此要提前

提前的方法:

  • 建立一组重载函数模版,彼此间的差异只在于各自的traits参数
  • 建立一个控制函数模版,调用上述重载函数
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::random_access_iterator_tag);

template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::bidirectional_iterator_tag);

//forwad_iterator_tag也是input
template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d, std::input_iterator_tag);

template<typename IterT, typename DistT>
void doAdvance(IterT& iter, DistT d)
{
    doAdvance(iter, d, 
              typename std::iterator_traits<IterT>::iterator_catogory());
}

48. 认识template元编程

template metaprograms执行于C++编译期,将工作从运行期转移到编译期。编译时间变长了。

用template,在编译期就可以计算阶乘

template<unsigned n>
struct Factorial{
    enum {value = n * Factorial<n-1>::value};
};
template<>
struct Factorial<0> {
    enum {value = 1};
};

更高执行效率,更早期的错误侦测

49. 了解new-handler的行为

当operator new抛出异常以反应一个未获满足的内存需求之前,它会先调用一个客户指定的错误处理函数,new-handler

namespace std{
    typedef void (*new_handler)();
    new_handler set_new_handler(new_handler p) throw();
}

​ 可以理解为有一个current_hanlder的static成员,里面存了要调用的函数。当通过set_new_handler函数设置新的new_handler时,当前的值就会被替换为设置的这个handler,之前旧的handler会被返回。

​ 当operator new无法满足内存申请时,会不断调用new-handler函数直到找到足够内存。

new-handler会做如下事情:

  • 让更多内存可被使用
  • 安装另一个new-handler
  • 卸除new-handler(将null指针传给set_new_handler,分配不成功时抛出异常)
  • 抛出bad_alloc异常
  • 不返回,调用abort

类专属的new-handlers:只需每个class提供自己的set_new_handler和operator new即可(都是static),还要定义static的new_handler(类外还要再定义)。

额外定义一个NewHandlerHolder类,里面存一个new-handler成员。其析构函数是将new-handler传给set-new-handler。我们在operator new中创建了NewHandlerHolder成员,构造时传入的参数是set_new_handler(currentHandler) -> 一方面将当前的new-handler设置成currentHandler,另一方面将返回值存在NewHandlerHolder里,析构的时候重新设置。

进阶:将NewHandlerSupport定义成template类,并且在定义新的类的时候继承它。

class Widget: public NewHandlerSupport<Widget>{
  ...  
};

参数T确实没被使用到,实际上它确实也不需要被使用。只是希望继承自NewHandlerSupport的每一个class拥有互异的附件(明确地说是static的currentHandler)

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

重载operator new的目的:

  • 用来检测运用上的错误
  • 为了强化效能
  • 为了收集使用上的统计数据
  • 为了增加分配和归还的速度
  • 为了降低缺省内存管理器带来的空间额外开销

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

operator里会循环分配,分配失败就调用new_handler。直到异常或者分配成功(当然背后是:安装另一个new-handler、卸除new-handler、抛出bad_alloc异常、return)。

查看当前的new_handler是先调用set_new_handler(0),并且接收当前函数(返回值),再调用它。

  1. 当derived class里的operator new是继承自base的。它里面需要判断参数size != sizeof(Base),如果不相等的话就调用全局的::operator new(size)(不相等说明这个情况不是给你用的)

    (operator delete也要先判断size是否等于sizeof(Base),不等的话就::operator delete)

  2. 当operator delete收到null指针时不做任何事。

  3. 对于operator new,当申请大小size为0时,我们应该将他设置为1再申请

  4. 当base 欠缺virtural 析构函数,传给operator delete的size可能是错的

52. 写了placement new也要写placement delete

内存分配后,构造函数抛出异常,系统就会自动调用operator new对应的operator delete收回这块空间。

placement new是在operator new(size_t size)后再加一个参数,调用时放在new()里面

最常用的:

void operator new(std::size_t, void* pMemory) throw();
Widget* pw = new (std::cerr) Widget;

#include <new>里面就有这个placement new

delete pw; -> 调用正常的operator delete,placement delete只会在placement new调用后构造函数异常时才会调用。

在类中重载了operator new后,在创建这个类的对象时,可能会覆盖掉global operator new,解决办法:建立一个StandardNewDeleteForms类,将normal、placement、nothrow对应的new和delete都写在里面,让子类继承他们。

并且要在类中using StandardNewDeleteForms::operator new、StandardNewDeleteForms:: operator delete,让它们可见。

53. 不要轻忽编译器的警告

54. 让自己熟悉包括TR1在内的标准程序库

55. 让自己熟悉Boost

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值