Effective C++学习笔记

文章目录

一 让自己习惯C++

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

c++总体来说有四个部分组成
1.C: C++由C而来,基本具有C的所有特性,C++在此基础上多了一些高级用法与语义。
2.Object-Orieted C++: 这部分是面向对象语言所具有的特征,像封装继承多态等。
3.Template C++: 这是C++泛型编程的部分,也就是使用模板。
4.STL: 标准模板库,由容器,算法,迭代器,仿函数,适配器,空间配置器组成。


  • C++是一个语言联邦

2:尽量用const,enum,inline替换#define

也可以称为“宁可以编译器替换预处理器”,因为#define不被视为语言的一部分,这可能会导致你写的宏定义名称未进入记号表从而导致问题的出现。

enum hack补偿法: 一个属于枚举类型的数值可以全当int被使用。取一个enum的地址是不合法的,所以当别人想用pointer或reference指向你的整数常量时是不合法的。


  • 对于单纯的常量,最好以const与enum替换#define
  • 对于形似函数的宏,最好以inline替换#define

3:尽可能的使用const

  • 指向常量的指针
    const char p = buf;//修饰,指针指向内存区域不能修改即内容不能改,指针指向可以变
  • 指针常量
    char * const p2 = buf;//修饰p2,指针指向不能变即地址不能改,指针指向的内存可以修改
  • 只读
    const char * const p3 = buf; //指向不能变,指向的内存也不能变

返回值加上const可以预防那个‘没有意思的赋值动作’。mutable(可变的)可以去除const的约束。
可以运用const成员函数实现出其non-const函数,经过const_case与static_case俩次转型。


  • 将某些东西声明为const可以帮助编译器找出错误用法
  • 当non-const与const函数等价出现时,令non-const调用const函数可以避免代码重复

4:确定对象使用前已先被初始化

初始化成员变量时,使用成员初始化列表。而初始化的顺序与变量声明的顺序相对应

编译单元:产出单一目标文件的那些源码

定义在不同编译单元的non-local-static对象初始化次序我们是无法决定的,这将会导致一些问题的产生。解决办法是将non-local-static对象搬到我们定义的函数内(返回引用),在该函数内被声明为local-static(单例模式的实现手法),这样我们调用的是函数,而不去直接操作对象。


  • 对于内置对象进行手工初始化,因为C++不保证初始化他们
  • 构造函数最好使用成员初值列,而不要在构造函数内体内使用赋值操作。
  • 为避免“跨单元初始化次序的问题”,要以local-static对象替换non-local-static

二 构造/析构/赋值运算

5:了解C++默默编写并调用哪些函数

//当你写了一个空类,相当于写了下面这个类
class Empty
{
public:
    Empty(){} //默认构造函数
    Empty(const Empty& rhs){}   //默认拷贝构造函数
    ~Empty(){}  //析构函数
    Empty& operator=(const Empty& rhs){}    //赋值操作符
};

当你声明了一个构造函数,系统将不会提供默认的构造函数。
如果你打算在一个含有引用成员的class内完成赋值操作,那么你必须重新定义赋值操作符,因为引用指向的对象是不能被修改的


  • 编译器暗自为class创建了default构造函数,copy构造函数,copy assignment操作符,以及析构函数

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

将其private化是一种解决办法,但这并不绝对安全,因为member函数和friend函数还是可以调用他们,你也可以聪明的不去定义他们。

在C++11标准后可以使用 ’ =delete禁止生成函数

当然你也可以去继承一个那些函数被私有化的类,这样本类将不会再声明这些函数


  • 驳回编译器默认的函数,可以将相应的成员函数声明为private并且不予以实现
  • 也可以继承其他被私有化的类

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

若多态基类析构不声明为virtual,经由父类指针删除子类的对象将会被释放不完全,会造成局部销毁资源泄露的问题
任何class只要带有虚函数都几乎确定应该也有一个虚析构函数,如果不带有虚函数,通常它并不意图被用作一个基类,当一个不被用作基类的类,令其析构函数为虚析构函数,往往是个不好的行为。因为,有虚函数就会有虚函数指针,虚函数表的生成,这会使对象的空间变大,浪费了资源。


  • 带有多态性质和virtual函数的类,它就应该拥有一个virtual析构函数
  • 如果该类不作为基类使用,或不具有多态性质,就不要声明虚析构函数

8:别让异常逃离析构函数

C++不喜欢析构函数吐出异常,因为在俩个同时作用的异常时,程序若是不结束就会导致不明确行为。
可以在析构函数捕获函数调用时可能出现的异常

~DBcon
{
    try
    {
        db.close();
    }
    catch(...)
    {
        /*制作运作记录,记下对close的调用失败*/
        abort();
    }
}

一个较佳的策略重新设计接口,使客户有机会对可能出现的问题作出反应,把调用close的职责从DBcon的析构函数转移到DBconnection手上。

class DBcon
{
public:
    ...
    void close()    //供客户使用的关闭函数
    {
        db.close();
        closed = true;
    }

    ~DBcon()
    {
        if(!closed)
        {
            try
            {
                db.close(); //关闭连接,如果客户没调用上面的函数话
            }
            catch(const std::exception& e)  //关闭失败的话,结束程序或吞下异常
            {
                /*制作运作记录,记下对close的调用失败*/
            }
            
        }
    }
private:
    DBconnection db;
    bool closed;
}

  • 析构函数绝对不要吐出异常,如果一个被析构函数调用的函数可能会出现异常,析构函数应该捕捉异常,然后吞下它或结束程序。
  • 如果客户需要对某个操作函数运行期间抛出的异常做出反应,那么class应该提供一个普通函数执行该操作。

9:绝对不要在构造和析构过程中调用virtual函数

base class构造期间virtual函数绝对不会下降到derived class阶层,因为调用构造函数时还未初始化成员变量。一旦析构函数执行,对象被销毁,而虚函数所需要的虚函数指针就存在对象中,这样会导致问题的出现。


  • 在构造和析构期间不要调用虚函数,因为这类调用从不下降到derived class

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

Widget & operator= (const Widget& rhs)
{
    ...
    return *this;
}

这个协议适用于+=,-=,*=等,这个协议不是强制性的。


  • 令赋值操作符返回一个reference to *this

11:在operator =中处理’自我赋值’

自我赋值中可能出现一个指针指向一个被删除的对象
解决办法

Widget & operator= (const Widget& rhs)
{
    if(this == &rhs)    //防止自我赋值
    {
        return *this;
    }
    delete pb;  //删除原有的对象
    pb = new Bitmap(*rhs.pb); //new一个新的空间
    return *this;
}

  • 确定任何函数如果操作一个以上的对象,而其中多个对象是同一个对象时,其行为依然是正确的

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

你应该让derived class的copying函数调用相应的base class函数复制,因为当你看似把子类的所有成员全部复制了,但其父类的成员(主要是私有成员)还没有被复制。
如果拷贝函数与复制操作符有相近的代码,我们往往新建一个init函数提供俩着调用,这样减少了代码重复


  • copying函数要确保复制对象内的所有成员变量所有的base成分
  • 不要尝试以某个copying函数实现另一个copying函数。应该将共同部分放到第三个函数中,供他们调用。

三 资源管理

13:以对象管理资源

俩个关键想法

  • 获得资源后立刻放进管理对象内,也称为‘资源取得时机便是初始化时机(RAII)’,比如获得指针放进智能指针内进行管理
  • 管理对象运用析构函数确保资源被释放,不论控制流如何离开区块,一旦对象被销毁其析构函数自然会被自动调用,于是资源会被释放。

智能指针内析构函数做的是delete而不是delete[],所以动态分配数组给智能指针并不是个好想法


  • 为了防止资源泄露,请使用RAII对象,他们在构造函数中获取资源,在析构函数中释放资源
  • 两个常用的RAII class分别是shared_ptr与auto_ptr,前者是较佳的选择。

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

当一个RAII对象被复制时,大多数时候你会选择以下俩种可能

  1. 禁止复制
    许多时候对RAII对象被复制并不合理,所以我们可以将copying操作声明为私有
  2. 对底层资源祭出’引用计数法‘
    智能指针share_ptr便是如此,当引用为0时就是被删除,且shared_ptr支持’删除器’
  • 复制底部资源:深度拷贝
  • 转移底部资源的拥有权:智能指针auto_ptr

  • 复制RAII对象时必须一并复制它所管理的资源,所以资源的copying行为绝对RAII对象的copying行为

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

智能指针都提供了一个get()成员函数,他会返回智能指针内部的原始指针(的复件)

用户定义转换:允许从类类型带其他类型的隐式转换显示转换,转换函数没有参数或显示返回类型

struct X {
    // 隐式转换
    operator int() const { return 7; }
 
    // 显式转换
    explicit operator int*() const { return nullptr; }
 
//   错误:转换类型标识中不允许出现数组运算符
//   operator int(*)[3]() const { return nullptr; }
    using arr_t = int[3];
    operator arr_t*() const { return nullptr; } // 若通过 typedef 进行则 OK
//  operator arr_t () const; // 错误:不允许任何情况下转换到数组
};
 
int main()
{
    X x;
 
    int n = static_cast<int>(x);   // OK:设 n 为 7
    int m = x;                     // OK:设 m 为 7
 
    int* p = static_cast<int*>(x);  // OK:设 p 为 null
//  int* q = x; // 错误:无隐式转换
 
    int (*pa)[3] = x;  // OK
}

  • 对原始资源的访问可能会有转换,其中显示转换比较安全感。但隐式转换对客户更方便
  • APIs往往要求访问原始资源,所有每个RAII class都应该提供一个去到该资源的方法

16:成对使用new和delete时要采取相同的形式

new会做俩件事,分配内存,调用一次(多次)构造函数
delete也会做俩件事,调用一次(多次)析构函数,释放内存


  • new中使用[ ],必须在相应的delete中也使用[ ],反之亦然。

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

func(shared_ptr<Widget>(new Widget),prioority()) 这可能会导致内存泄漏,因为它完成了三个动作

  1. 执行new Widget
  2. 调用prioority
  3. 调用shared_ptr的构造函数
    如果第二步失败了,那么就可能引发资源泄露
//将语句单独拿出来,这样可以防止问提的出现
shared_ptr<Widget> pw(new Widget);
func(pw,prioority());

  • 如果不这么做,一旦异常抛出,将难以察觉资源泄露

四 设计与声明

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


  • 阻止误用的办法包括建立新类型,限制类型上的操作,束缚对象值,以及消除客户的资源管理责任

19:设计class犹如设计type

当你设计了一个新的class,就相当于设计了一个新的type
要考虑的问题

  • 新对象应该如何创建和销毁
  • 对象的赋值和初始化该有什么差异
  • 新的对象如果被以值传递,意味着什么
  • 什么是新type的合法值
  • 新的type需要配合某个继承图系吗
  • 新的type需要什么样的转换
  • 什么样的操作符和函数对此新type才合理
  • 什么样的标准函数要被驳回
  • 谁该取用新type的成员
  • 什么是新type的为声明接口
  • 新的type有多么一般化
  • 你真的需要一个新的type吗

  • class的设计就是type的设计

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

缺省情况下C++以by value方式,传递对象到函数,都是以实际实参的副本,调用拷贝构造函数生成的新对象,调用析构进行销毁。还会产生对象切割问题:

class A
{
public:
    virtual void display()
    {
        cout << "wo shi A" << endl;
    }
};

class B: public A
{
public:
    void display()
    {
        cout << "wo shi B" << endl;
    }
};
void func(A a)  //a会被构造成A类型,而传B类型的对象,会被切割
{
    a.display();
}
/*func函数还是调用A的display函数*/
int main() 
{
    B b;
    func(b);//wo shi A
    return 0;
}

func(A a)函数改为func(A & a)问题将会解决

传引用通常意味着真正传递的是指针,因此当你有个对象是内置类型(int,double…),那么选择传值比传引用的效率更高。


  • 尽量以传引用替换传值,前者比较高效,并可避免切割问题
  • 以上规则并不适用于内置类型,以及STL的迭代器和函数对象(回调)。对他们而言,传值更适用。

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


  • 绝不要返回pointer或refere指向一个local stack对象与local static对象,或返回reference指向一个heap-allocated对象(内存泄露)。

22:将成员变量声明为private

其实只有俩种访问权限:private(提供封装)和其他(不提供封装)


  • 将成员变量声明为私有,这可赋予客户访问数据的一致性,可细微划分访问控制,并提供class作者以充分的实现弹性
  • protect并不比public更具有封装性

23:宁以non-memeber,non-friend 替换 member 函数

class A
{
public:
    void a();
    void b();
    void c();
}

当我们想调用这些成员函数时,有以下俩种做法

//直接声明一个新的member函数调用
class A
{
    void d()
    {
        a();
        b();
        c();
    }
}
//声明一个non-member函数调用
void d(A &a)
{
    a.a();
    a.b();
    a.c();
}

第二种非成员函数的做法更加的得当,因为愈少的函数可以访问它,数据的封装性就越高。
C++中,比较好的做法就是让,将类和非成员函数放到同一个namespace中。标准程序库中正是如此做法。将所有便利函数放在多个头文件内但隶属于同一个命名空间,意味着客户可以轻松扩展这一组便利函数。


  • 这样做可以增加封装性,包裹弹性与机能扩充性

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

class A
{
    cosnt A operator* (const A & rhs);
}

A a(1,2);
A b(3,4);
A c = a*b;  //通过
A d = a*2;  //通过(隐式转换)
A e = 2*a;  //错误
//应将其变成非成员函数
class A
{
    ....
}
cosnt A operator* (const A & rhs, const A & lhs);
A c = a*b;  //通过
A d = a*2;  //通过
A e = 2*a;  //通过

  • 如果你需要为某个函数的所有参数(包括被this指针所指的那个隐喻参数)进行类型转换,那么这个函数必须是个非成员函数

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

swap原本只是STL的一部分,而后成为异常安全编程的脊柱,以及用来处理自我赋值可能性的一种常见机制

pimpl手法:以指针指向一个对象,内含真正的数据

class B
{
    int a;
    int n;
    ....
}

class A
{
public:
    .....
private:
    B *b;
}
  1. 提供一个public swap成员函数,让他高效的置换你的类型的俩个对象值
  2. 在你的class或template所在的命名空间提供一个non-memeber swap,并令它调用上述的成员函数
  3. 确定包含一个using std::swap 声明式,在函数内曝光可见
class C
{
    ...
    void swap(C & a)
    {
        using std::swap;    //声明是必要的,让编译器选择最好的swap
        swap(b,a.b);    //若要置换就置换其b指针
    }

private:
    B *b;
}

  • 当std::swap对你的类型效率不高时,提供一个swap成员函数,并确定这个函数不抛出异常
  • 如果你提供了一个member swap,也该提供一个non-member swap 用来调用前者。

五 实现

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


  • 尽可能延后变量定义式的出现,这样做可增加程序的清晰度并改善程序效率

27:尽量少做转型动作

新式转型:(int)a
旧式转型:int(a)

C++提供四种转型方式:

  • const_cast:通常被用来将对象的常量性转除
  • dynamic_cast:主要用来执行"安全向下转型",也就是用来绝对某对象是否归属继承体系中的某个类型,可能会造成重大运行成本的转型动作
  • reinterpret_cast:意图执行低级转型,如将point to int 转型为一个int
  • static_cast:用来强迫隐式转换,如将non-const转为const,将int 转为double等,将基类指针转为派生类指针,但是不能将const转为non-const

单一的对象,可能拥有一个以上的地址,dervice的对象d,拥有俩个地址(偏移量)。

class base
{
    ...
}

class derived:public virtual base
{
    ...
}
derived d;
base *p = &d; //隐喻的将derived*转换为base*

在使用强制转换时,注意以下问题

class A
{
public:
    virtual void func()
    {
        cout << "wo shi A" << endl;
    }
    ~A()
    {
        cout << "bay" << endl;
    }
};

class B: public  A
{
public:
    virtual void func()
    {                                   //并不是当前对象执行操作
        static_cast<A>(*this).func();  //而是会生成一个临时对象A,
                                       //对func的调用也是调用该临时对象的
    }
};

int main() 
{
    B bb;           ///输出:wo shi A
    bb.func();      //      bay
    return 0;       //      bay
}

  • 尽量避免转型,特别是少用dynamic_cast
  • 如果必须转型,那么尝试将他隐藏与某个函数背后,随后客户可以调用它,而不是在自己的代码内写出
  • 宁可以写新式转型,也不要写旧式转型,前者更容易分辨

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

如果const成员函数传出一个reference,及有可能修改private变量(编译会报错,若加上返回类型为const,则通过)
引用,指针与迭代器统统都是所谓的handles(号码牌,用来取得某个对象),而返回一个"代表对象内部数据"的handle,带来降低封装性的风险。

dangling handles(空悬号码牌):这种handle所指东西的所属对象不复存在,这也是一种危险的行为。


  • 避免返回handle(包括引用,指针,迭代器)指向对象内部

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

copy-and-swap策略:修改对象数据的副本,然后在一个不抛出异常的函数中将修改后的数据和原件置换


  • 异常安全函数即使发生异常也不会泄露资源或允许任何数据结构被破坏。这样的函数分为三种可能性的保证:基本型,强烈型,不抛异常型
  • “强烈保证”一般由copy and swap 实现,并非对所有函数都可以实现

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

整体观念:对此函数的每一次调用,都用函数本体替换之inline只是对编译器的一种申请,而不是一种命令,inline函数一定被置于头文件中,因为inline是编译期行为,而virtual是运行期行为。

如果你认为template具现出来的函数都应该是inline,请将此template声明为inline,反之不需要,因为inline是需要成本的。

构造函数和析构函数往往是inlining的糟糕候选人。因为编译器在编译期间会给你的构造函数和析构函数额外加入很多的代码,像成员函数的构造析构等代码,所以通常构造析构函数比表面上看起来的要多,并不适合作为内联函数。

inline造成代码膨胀会导致额外的换页行为,降低指令高速缓存装置的命中率,以及伴随这些而来的效率降低

80-20法则:平均一个程序往往将80%的执行时间花费在20%的代码上。


  • 大多数inline限制在小型,频繁调用的函数上。这可使日后调试过程和二进制升级更容易。
  • 不要只因为function template在头文件中就把它作为inline

31:将文件的编译存在关系降至最低

  • 如果使用object reference 或 object pointer可以完成任务就不要使用object
  • 尽量以class声明式替换class定义式
  • 声明式和定义式提供不同的头文件

handle classinterface class 解除了接口和实现之间的耦合性,从而降低了文件之间编译性的关系。


  • 相依于声明式,不要相依与定义式,基于此构想的俩个手段就是handle classinterface class
  • 程序库头文件应该以“完全且仅有声明式”的形式存在

六 继承与面向对象设计

32:确定你的public继承塑模出Is-a关系

公开继承意味着"is-a"(是一种)的关系

is-a并非唯一存在与class之间的关系,还有has-a(有一个)和is-implemented-in-terms-of(根据某物实现出)

is-a 关系也叫继承或泛化,比如学生和人的关系、手机和电子产品的关系都属于继承关系。
复合有”has-a“或”根据某物实现出(is-implemented-in-terms-of)“的意思——当复合发生在应用域内的对象之间,表现出has-a关系;当它发生于实现域内则是表示“根据某物实现出”的关系。


  • public继承意味is-a。适用于base class 身上的每一件事情一定也适用于derived class身上,因为每一个dervied class对象也都是base class 对象

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

子会类继承父类的东西,但有些函数你并不想继承,但这违背了public的is-a关系,可以如下做

class base
{
public:
    virtual void m1() = 0;
    virtual void m1(int);
    virtual void m2();
    void m3();
    void m3(int);
}

class derived:base
{
public:
/*当派生类不想实现m1时*/
    using base::m1();   //让base的东西在derived可见,调用时调用的是base内的函数
    virtual void m1(int)    //或直接调用父类的函数皆可
    {
        base::m1(int);
    }
    virtual void m2();
    void m3();
    void m3(int);
}

  • 在derived class 内的名称会遮掩base class 内的名称,在public继承下从来没有人希望如此
  • 为了让遮掩的名称再见天日,可使用using声明式或转交函数(派生类函数调用基类函数)

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

  • 成员函数的接口总是会被继承
  • 声明一个纯虚函数就是为了让派生类只继承函数接口
  • 声明一个虚函数是让派生类继承该函数的接口和缺省实现(重写)
  • 声明普通函数的目的是为了令派生类继承函数的接口及一份强制性实现(重定义)

  • 在public继承下,derived class总是继承base class接口
  • 纯虚函数只具体指定接口继承
  • 虚函数具体指定接口继承及缺省实现继承
  • 普通函数具体指定接口继承及强制性继承

35:考虑virtual函数以为的其他选择

  • 使用NVI手法,那是模板方法設計模式的一种特殊形式。他以public non-virtual成员函数包裹较低访问性(private或protect)的virtual函数
  • 将virtual函数替换为"函数指针成员变量",这是策略模式的一种分解表现形式
  • 将继承体系内的virtual函数替换为另一个继承体系的virtual函数,这是策略模式传统的实现手法

  • 将机能从成员函数移到class外部函数,带来一个缺点是,非成员函数无法访问class的non-public成员
  • virtual函数的替换方案包括NVI手法及strategy设计模式的多种形式

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


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

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

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

class A
{
public:
    virtual void draw(int color = red) const = 0;
    ...
}
class B:public A
{
public:
    virtual void draw(int color = bule) const;  //错误,调用时还是会red
}

但当你原封不动的继承下来,父类要修改时,派生类也要做出修改。使用NVI手法设计,解决这代码重复的问题

class A
{
public:
    void dodraw(int color = red)
    {
        draw(color);
    }
private:
    virtual void draw(int color) const = 0;
    ...
}
class B:public A
{
    //由于条款36,所以red,不会被修改,而当你修改基类的red时,派生类也会被修改,这样就会十分的方便
    .....
private:
    virtual void draw(int color) const;  
}

  • 绝不要重新定义一个继承而来的缺省值数值,因为缺省数值都是静态绑定,而virtual函数–你唯一应该重写的东西却是动态绑定的

38:通过复合塑模出has-a或“根据某物实现出“

当复合发生在应用域的对象之间,表现出的时"has-a"(有一个)的关系;当它发生于实现域内则是表现"根据某物实现出"的关系

程序中的对象其实相当于你所塑型的世界中的某些事物,例如人,汽车,一张张视频画面等,这样的对象都是属于应用域;其他对象则纯粹是实现细节上的人工制品,像是缓存区,互斥器,查找树等,这些对象相当于你的软件的实现域

class Address{}
class PhoneNumber{}
class Person
{
    ...
privae:
    Address address;
    PhoneNumber phonenumber;
    string name;
}
//上述是一种has-a(有一个)的关系,而不是is-a(是一个)的关系,因为你会说"人有一个地址,一个电话";而不是说"人是一个地址,一个电话"

当即不是has-a,又不是is-a的关系时,例如当你在set中调用list时,这就是is-implemented-in-terms-of(根据某物实现出)


  • 复合的意义完全和public继承完全不同

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

private继承意味着is-implemented-in-terms-of(根据某物实现出),但最好使用复合。

当你面对俩个不是"is-a"关系的class时,其中一个需要访问另一个的protect成员,或需要重新定义其一或多个virtual函数,private继承会是很好的设计


  • private继承意味is-implemented-in-terms-of(根据某物实现出),它通常比复合级别低。但是当派生类需要访问proteced base class的成员,或重新定义继承而来的virtual函数时,这么设计是合理的。
  • 和复合不同,私有继承可以造成empty base(复用会占一个字节)最优化。

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

非必要使用virtual base时就不要用,virtual base的初始化责任是由继承体系中的最底层class负责。当无法避免时,我们也要在其中放置数据。


  • 多重继承比单一继承复杂,他可能导致新的歧义性,以及对virtual继承的需要
  • virtual继承会增加大小,速度等成本。如果virtual base class 不带任何数据,僵尸最具有实用价值的情况

七 模板与泛型编程

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

面向对象编程总是以显示接口(在源码中明确可见的)和运行期多态(virtual)解决问题,在泛型编程中还有隐式接口与编译期多态。

显式接口有函数的签名式(函数名称,参数类型,返回类型)构成。
隐式接口,T便是一个隐私接口

template <typename T>
void func(T &w)
{
    if(w.size() > 10 && w != adv)   //有效表达式
    ....
}

  • class和template都支持接口和多态
  • 对class而言接口是显式的,以函数签名为中心。多态则是通过virtual函数发生于运行期
  • 对template参数而言,接口是隐式的,基于有效表达式。多态则是通过template具显化和函数重载解析发生于编译期

42:了解typename的双重意义

template内出现的名称如果相依与某个template参数,称之为从属名称。如果从属名称在class内呈现嵌套状,我们称之为嵌套从属名称

template <typename T>
void func(T &w)
{
   typename C::const_iterator iter(w.begin()) //嵌套从属名称要加上typename,防止C::const_iterator有歧义
}

  • 声明template参数时,前缀关键字class和typename可互换
  • 请使用关键字typename标识嵌套从属类型名称,但不得在base class lists(基类列)和成员初始化列表内使用

43:学习处理模块化基类内的名称

模板为什么要特化,因为编译器认为,对于特定的类型,如果你能对某一功能更好的实现,那么就该听你的。

模板泛化

template <typename T,class T1>
class Mess
{
    ...
}

模板全特化:针对某个类型特化,所有的类型都是模板,会失去Tempalte的属性

tempalte<>
class Mess<int,double>
{
    ...
}

偏特化:介于泛化和全特化之间,有部分被明确化,有部分没有(普通函数没有偏特化)

tempalte<class T>
class<int ,T>
{
    ...
}

全特化与偏特化是要有泛化的基础上才能声明的,且参数的数量与泛化的一致
模板基类的任何特化版本都将支持其一般(泛化)版本所提供的接口。

template<class T,class T1>
class Ab
{
public:
	Ab(T a)
	{
		cout << "泛化" << endl;
	}
};

template<>
class Ab<int,float>
{
public:
	Ab(int a,float b)
	{
		cout << "全特化" << endl;
	}
};

template<class T>
class Ab<string,T>
{
public:
	Ab(string a,T b)
	{
		cout << "偏特化" << endl;
	}
};

int main()
{
	Ab<int, char> c(5.0);			//泛化
	Ab<string,char> a("你好",1);		//偏特化
	Ab<int,float> b(1,2);			//全特化

}

  • 可在派生类模板内通过"this->"指涉基类模板内的成员名称,或给予一个明白写出"base class资格修饰符"完成

44:将于参数无关的代码抽离templates


  • template生成多个class和多个函数,所以任何tempalte代码都不应该添加一些无关代码
  • 因非类型模板参数而造成的代码膨胀,往往可消除,做法是以函数参数或class成员变量替换template
  • 因类型参数而造成的代码膨胀,往往是让带有完全相同二进制表述的具现类型共享实现码

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

智能指针:行为像指针的的对象

template<typename T2>
class Test
{
public:
	template<typename T1>   //成员函数模板
	Test(T1 a, T2 b)
	{
		cout << a << endl;
		cout << b << endl;
	}
};

int main()
{
	//参数模板中不需要写成员模板参数,系统自动匹配
	Test<string> d(1, "nihao");	
}

  • 如果你声明成员模板用于泛化copy构造或泛化assignment操作符,你还需要声明正常的拷贝构造函数与赋值操作符

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

template实参推导过程中从不将隐式类型转换函数纳入考虑


  • 当我们编写一个class template,而她所提供于此tempalte相关的函数支持所有参数隐式类型转换时,请将那些函数定义为class template内部的friend函数

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

STL主要由"用以表现容器,迭代器和算法"的template构成

	/*五类迭代器*/
	struct input_iterator_tag{};	//单向 只能一次走一步,且只读取一次	适用于一次性操作算法
	struct output_iterator_tag{};	//单向 一次走一步,且只能写一次		适用于一次性操作算法
	struct forward_iterator_tag{};	//单向 读写都行,且能多次
	struct bidirectional_iterator_tag{};	//set,list,map使用这种迭代器
	struct random_access_iterator_tag{};	//vector,deque,string使用这种迭代器

这些迭代器都是is-a关系,所有的forward迭代器都是Input迭代器,以此类推

在 STL 中,容器与算法是分开的,彼此独立设计,容器与算法之间通过迭代器联系在一起。那么,算法是如何从迭代器类中萃取出容器元素的类型的?没错,这正是我们要说的 traits classes 的功能。

//迭代器所指对象的类型,称为该迭代器的 value_type。我们来简单模拟一个迭代器 traits classes 的实现
//my_iterator_traits 其实就是个类模板,其中包含一个类型的声明
template<class IterT>
struct my_iterator_traits {
    typedef typename IterT::value_type value_type;
};

//对于my_iterator_traits,我们再声明一个偏特化版本。
//即如果 my_iterator_traits 的实参为指针类型时,直接使用指针所指元素类型作为 value_type。也能正确的萃取出类型
template<class IterT>
struct my_iterator_traits<IterT*> {
    typedef IterT value_type;
};

//为了测试 my_iterator_traits 能否正确萃取迭代器元素的类型,我们先编写以下的测试函数。
void fun(int a) {
    cout << "fun(int) is called" << endl;
}

void fun(double a) {
    cout << "fun(double) is called" << endl;
}

void fun(char a) {
    cout << "fun(char) is called" << endl;
}

//测试代码如下:
my_iterator_traits<vector<int>::iterator>::value_type a;
fun(a);  // 输出 fun(int) is called
my_iterator_traits<vector<double>::iterator>::value_type b;
fun(b);  // 输出 fun(double) is called
my_iterator_traits<char*>::value_type c;
fun(c);  // 输出 fun(char) is called

设计并实现一个 traits class 的过程:

  1. 确认若干我们希望将来可取得的类型相关信息,例如,对于上面的迭代器,我们希望取得迭代器所指元素的类型;
  2. 为该信息选择一个名称,例如,上面我们起名为 value_type;
  3. 提供一个 template 和一组特化版本(例如,我们上面的 my_iterator_traits),内容包含我们希望支持的类型相关信息。

  • traits classes使得类型相关信息在编译期可用,它们以template和template 特化完成实现
  • 整合重载技术后,traits classes有可能在编译期对类型执行if…else测试

48:认识template元编程

元编程则是图灵完备的借助语言提供模板机制,通过编译器推导(deduce),在编译时生成程序。元编程经过编译器推导得到的程序,再进一步通过编译器编译,产生最终的目标代码。在使用 if 进行编译时测试中,用一个例子说明了两者的区别。

模板编程的应用主要有两种:泛型编程和元编程。前者注重于通用概念的抽象,设计通用的类型或算法,不需要过于关心编译器如何生成具体的代码;而后者注重于设计模板推导时的选择和迭代,通过模板技巧设计程序。


  • TMP可将运行期移往编译期,因而得以实现早期错误侦测和更高的执行效率
  • TMP可被用来生成“基于政策选择组合”的客户定制代码,也可以用来避免生成对某些特殊类型并不合适的代码

八 定制 new 和 delete

49:了解new-handler的行为


  • set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用
  • Nothrow new是一个颇为局限的工具,因为它只适用于内存分配,后继的构造函数调用还是可能抛出异常

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


-有许多理由写个自定义的new 和delete,包括改善较能,对heap运用错误进行调试,收集heap信息

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


  • operator new 应该内含一个无穷循环,并在其中尝试分配内存,如果它无法满足内存要求,就该调用new-handler。她也应该有能力处理0字节申请。class专属版本还应该处理"比正确大小更大的错误申请"
  • operator delete应该在收到null指针时不做任何事情。class专属版本则还应该处理"比正确大小更大的错误事情"

52:写了placement new 也要写placement delete


  • 当你写了一个placement New ,请确定也写出了一个placement delete,如果不这么做那么则可能发生内存泄露
  • 当你声明placement new 与delete ,请不要非故意的遮掩了它们正常版本

九 杂项讨论

53:不要轻视编译器的警告


  • 严肃对待编译器发出的警告信息
  • 不要过度依赖编译器的报警能力,在不同的编译器上对待事情的态度并不相同

54:让自己熟悉TR1在内的标准程序库

14个新TR1组件(程序库机能单位),都在std命名空间下

  • 智能指针:std::tr1::shared_ptr和std::tr1::weak_ptr
  • tr1::function:它是一个类模板,类的成员变量是一个函数指针。可以把它看作一个智能函数指针.
    1. 可以作为函数和成员函数。
    2. 可做回调函数,取代函数指针。
    3. 可作为函数的参数,从外部控制函数内部的行为。
#include <functional>
#include <iostream>
 
struct Foo {
    Foo(int num) : num_(num) {}
    void print_add(int i) const { std::cout << num_+i << '\n'; }
    int num_;
};
 
void print_num(int i)
{
    std::cout << i << '\n';
}
 
struct PrintNum {
    void operator()(int i) const
    {
        std::cout << i << '\n';
    }
};
 
int main()
{
    // 存储 a free function
    std::function<void(int)> f_display = print_num;
    f_display(-9);
 
    // store a lambda
    std::function<void()> f_display_42 = []() { print_num(42); };
    f_display_42();
 
    // store the result of a call to std::bind
    std::function<void()> f_display_31337 = std::bind(print_num, 31337);
    f_display_31337();
 
    // store a call to a member function
    std::function<void(const Foo&, int)> f_add_display = &Foo::print_add;
    const Foo foo(314159);
    f_add_display(foo, 1);
    f_add_display(314159, 1);
 
    // store a call to a data member accessor
    std::function<int(Foo const&)> f_num = &Foo::num_;
    std::cout << "num_: " << f_num(foo) << '\n';
 
    // store a call to a member function and object
    using std::placeholders::_1;
    std::function<void(int)> f_add_display2 = std::bind( &Foo::print_add, foo, _1 );
    f_add_display2(2);
 
    // store a call to a member function and object ptr
    std::function<void(int)> f_add_display3 = std::bind( &Foo::print_add, &foo, _1 );
    f_add_display3(3);
 
    // store a call to a function object
    std::function<void(int)> f_display_obj = PrintNum();
    f_display_obj(18);
}
  • tr1::bind:STL绑定器,参数绑定在占位符(std::placeholders::_1,…)上
#include <iostream>
#include <functional>
 
void fn(int n1, int n2, int n3) {
	std::cout << n1 << " " << n2 << " " << n3 << std::endl;
}
 
int fn2() {
	std::cout << "fn2 has called.\n";
	return -1;
}
 
int main()
{
	using namespace std::placeholders;
	auto bind_test1 = std::bind(fn, 1, 2, 3);
	auto bind_test2 = std::bind(fn, _1, _2, _3);
	auto bind_test3 = std::bind(fn, 0, _1, _2);
	auto bind_test4 = std::bind(fn, _2, 0, _1);
 
	bind_test1();//输出1 2 3
	bind_test2(3, 8, 24);//输出3 8 24
	bind_test2(1, 2, 3, 4, 5);//输出1 2 3,4和5会被丢弃
	bind_test3(10, 24);//输出0 10 24
	bind_test3(10, fn2());//输出0 10 -1
	bind_test3(10, 24, fn2());//输出0 10 24,fn2会被调用,但其返回值会被丢弃
	bind_test4(10, 24);//输出24 0 10
	return 0;
}

我们把其他的TR1组件划为俩组

  • Hash tables:用来实现sets,multisets与multi-maps
  • 正则表达式
  • Tuples(变量组):pair的升级版,pair只支持俩个对象,而他支持多个
  • tr1::array:一个支持成员函数的大小固定数组
  • tr1::mem_fn
  • tr1::reference_wrapper:一个让引用更像对象的设施
  • Type traits:一组traits class用以提供类型的编译期信息。
  • tr1_result_of:用来推导函数的返回类型

  • TR1自身只是一份规范,为获得TR1提供的好处,你需要一份实物,一个好的实物来源就是Boost

55:让自己熟悉Boost


  • Boost是一个社群
  • Boost提供许多TR1组件实现品,以及其他许多程序库

2022/4/28 pass one

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值