Effective C++ 学习

Effective C++浅浅学习,很多不太理解

      • 尽量用const, enum, inline 替换#define
      • 尽可能使用const
      • 确认对象在使用前就已经被初始化
      • 构造,析构,赋值运算,拷贝构造
      • 为多态基类声明virtual析构函数
      • 不要让析构函数抛出异常
      • 不在构造和析构过程中调用virtual函数
      • 令operator= 返回一个指向 *this的引用
      • 在operator= 中处理“自我赋值”
      • 以对象管理资源/COPY/提供对原始资源的访问
      • 用独立语句将new的对象置入智能指针
      • 写更加容易被正确使用的接口
      • 以pass-by-reference-to-const替换pass-by-value
      • 以non-member、non-friend替换member函数
      • 若所有参数皆需类型转换,请为此采用non-member函数
      • 尽量不要进行强制类型转换
      • 异常安全的考虑
      • 不要过度使用inline
      • 降低文件间编译的依存关系
      • 一些需要在继承时注意的
      • 考虑virtual函数以外的其他选择
      • 复合类
      • private继承和多重继承
      • 隐式接口和编译期多态
      • typename的双重意义
      • 学习处理模板化基类内的名称
      • 将与参数无关的代码抽离templates
      • 运用成员函数模板接受所有兼容类型
      • 需要类型转换时请为模板定义非成员函数
      • 认识template元编程
      • 了解new-handler的行为
      • new和delete


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

  1. define只在预处理阶段起作用,简单的文本替换,因为在预处理阶段就已经做了替换,发生错误的时候,报错信息不好找。
  2. 可以用const来代替宏定义,const在编译、链接过程中起作用;
  3. const定义的常量是变量带类型,而#define定义的只是个常数不带类型;
  4. define只是简单的字符串替换没有类型检查。而const是有数据类型的,是要进行判断的,可以避免一些低级错误;
  5. define预处理后,占用代码段空间,const占用数据段空间;
  6. const不能重定义,而define可以通过#undef取消某个符号的定义,进行重定义;
  7. define独特功能,比如可以用来防止文件重复引用。

C++里有一个特殊的:类中的静态数据,在类中声明,在类外定义,其中整数可以不定义就使用。

enum可以替换const和define,优雅的实现一个常量的设置。

class A{
private:
	enum{NumTurns =5};
	int scores[NumTurns]; //constant
public:
	int numTurns(){
		return NumTurns;
	}
};

使用inline函数,替换宏定义函数

  • #define是关键字,inline是函数;
  • 宏定义在预处理阶段进行文本替换,inline函数在编译阶段进行替换;
  • inline函数有类型检查,相比宏定义比较安全;
    #define 更容易出错。inline可以获得宏的所有效率,也可以获得类型安全。

尽可能使用const

const出现在* 左侧,指向的是常量,
const出现在* 右侧,指针本身是常量。
顶层const:指的是const修饰的变量本身是一个常量,星号右边。
底层const:指的是const修饰的变量所指向的对象是一个常量,星号左边。

int a = 10;
int* const b1 = &a;    //顶层const,b1本身是一个常量。指针常量
const int* b2 = &a;    //底层const,b2本身可变,所指的对象是常量。常量指针

STL中迭代器以指针为模型。
const可以使代码变得更加健壮,确保哪些东西可以修改,哪些东西不可以修改。const还可以定义不同的函数重载。

确认对象在使用前就已经被初始化

内置类型如int,string等在默认构造函数进入构造函数体之前就已经完成了初始化,构造函数体中是在进行赋值,一般采用列表初始化的方法,直接进行初始化,直接调用拷贝构造方法,效率更高一些。

class A{
public:
    A():a(0){}
private:
    int a;
};
  • C++中不同编译单元中定义的非局部静态对象的初始化顺序是不固定的。所以定义的时候,最好将非局部静态对象定义为局部静态对象。

构造,析构,赋值运算,拷贝构造

  • 如果不自己声明任何函数,编译器会生成默认构造,拷贝构造,赋值运算符和析构函数,也不是任何时候都生成,需要用到才会生成。
  • 如果定义了构造函数,编译器就不会再生成默认构造函数
  • 如果类中的数据成员为static或者引用,编译器就会拒绝自动生成赋值运算符操作。
  • 如果基类的拷贝赋值运算符为private,派生类将无法生成拷贝赋值运算符,无法处理基类部分。
  • 如果不想编译器自动生成函数,就该明确拒绝。
    • 解法一:将拷贝构造和拷贝赋值运算符都设置为private,只给声明,不写定义,编译的时候没问题,链接的时候会出问题。
    • 解法一更进一步:定义一个基类,以后定义的类继承自这个基类,就没有办法拷贝构造和赋值运算。
    • 解法二:C++11 直接调用delete。

拷贝构造函数:

  • 当编写一个copy或者拷贝构造函数,应该确保复制成员里面的所有变量,以及所有基类的成员
  • 不要尝试用一个拷贝构造函数调用另一个拷贝构造函数,如果想要精简代码的话,应该把所有的功能机能放到第三个函数里面,并且由两个拷贝构造函数共同调用
  • 当新增加一个变量或者继承一个类的时候,很容易出现忘记拷贝构造的情况,所以每增加一个变量都需要在拷贝构造里面修改对应的方法。

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

  • 类的多态性,基类指针可以指向派生类的对象,如果删除该基类的指针,就会调用该指针指向的派生类析构函数,而派生类的析构函数又自动调用基类的析构函数,这样整个派生类的对象完全被释放。
  • 如果析构函数不被声明成虚函数,则编译器实施静态绑定,在删除基类指针时,只会调用基类的析构函数而不调用派生类析构函数,这样就会造成派生类对象析构不完全,造成内存泄漏。

如果类不会成为基类,也不要刻意的去写上virtual的析构函数,会引入虚表指针,占据额外的空间。

标准库的string类型不含虚函数,将string作为基类的话,有可能会出现内存泄漏。

想创建一个抽象类,但是没有任何纯虚函数,怎么办?声明纯虚析构函数。

不要让析构函数抛出异常

  1. 再封装一层中间层,下层抛出异常的话,在这一层捕捉。
class DBConnection{
public:
    static DBConnection create();
    void close();
};

class DBConn{
public:
    ~DBConn(){
        // db.close();
        // 方案1
        try
        {
            db.close();
        }
        catch(const std::exception& e)
        {
            std::abort();
            // 或者记录下异常
        }
    }
private:
    DBConnection db;
};
  1. 重新设计接口,使其客户有机会对可能出现的问题作出反应。
class DBConn
{
public:
    void close() //给客户使用的
    {
        db.close();
        closed = true;
    }

    ~DBConn()
    {
        if (!closed)
        {
            try //如果客户没关闭,再关闭 双保险
            {
                db.close();
            }
            catch (const std::exception &e)
            {
                std::abort();
                // 或者记录下异常
            }
        }
    }

private:
    DBConnection db;
    bool closed;
};

如果某个操作可能在失败时抛出异常,又存在某种需要必须处理该异常,那么这个异常必须来自析构函数以外的某个函数
析构函数不要抛出异常,因该在内部捕捉异常。如果客户需要对某个操作抛出的异常做出反应,应该将这个操作放到普通函数(而不是析构函数)里面

不在构造和析构过程中调用virtual函数

如果在构造函数中执行virtual函数,派生类对象的基类部分,会首先构造,构造基类时,虚函数永远不会进入派生类。所以要避免这种情况。

  • 如果在父类的构造函数中,调用子类的虚函数,是没有办法做到的。
  • 如果在父类的析构函数中,调用子类的析构函数,也是没有办法的。

令operator= 返回一个指向 *this的引用

该约定主要是为了支持连读和连写,这个操作同样适用于-=,+=,*=等

class A{
public:
    A& operator=(const A&rhs){ //返回的是当前类的一个引用。
        //..
        return *this; 
    }
    A& operator+=(const A&rhs){ //返回的是当前类的一个引用。
        //..
        return *this; 
    }
};

在operator= 中处理“自我赋值”

主要是要处理 a[i] = a[j] 或者 *px = *py这样的自我赋值。有可能会出现一场安全性问题,或者在使用之前就销毁了原来的对象,例如

class Bitmap{...}
class Widget{
private:
    Bitmap *pb;
};
Widget& Widget::operator=(const Widget& rhs){
    delete pb; // 当this和rhs是同一个对象的时候,就相当于直接把rhs的bitmap也销毁掉了
    pb = new Bitmap(*rhs.pb);
    return *this;
}

改进处理:

class Widget{
    void swap(Widget& rhs);    //交换this和rhs的数据
};
Widget& Widget::operator=(const Widget& rhs){
    Widget temp(rhs)           //为rhs数据制作一个副本
    swap(temp);                //将this数据和上述副本数据交换
    return *this;
}//出了作用域,原来的副本销毁

以对象管理资源/COPY/提供对原始资源的访问

为了防止在delete语句执行前return,所以需要用对象来管理这些资源。这样当控制流离开f以后,该对象的析构函数会自动释放那些资源。

  • 将资源放进智能指针,如shared_ptr或unique_ptr对象包装中,函数体结束以后,资源就会自动释放,调用析构函数。尽量使用shared_ptr,行为更加符合逻辑一些。
  • 为了防止资源泄露,使用RAII对象,在构造函数中获得资源并在析构函数中释放资源。

在资源管理类里面,如果出现了拷贝复制行为的话,需要注意这个复制具体的含义,从而保证和我们想要的效果一样
下面的代码会对锁进行拷贝,但是并没有达到想要的独立类对象的需求。

class Lock{
public:
    explicit Lock(Mutex *pm):mutexPtr(pm){
        lock(mutexPtr);//获得资源锁
    }
    ~Lock(){unlock(mutexPtr);}//释放资源锁
private:
    Mutex *mutexPtr;
}
Lock m1(&m)//锁定m
Lock m2(m1);
//好像是锁定m1这个锁。。而我们想要的是除了复制资源管理对象以外,还想复制它所包括的资源(deep copy)。通过使用shared_ptr可以有效避免这种情况。
  • 要么禁止拷贝复制
  • 要么用引用计数,添加智能指针。可以作为删除器来使用
class Lock{
public: 
    explicit Lock(Mutex* pm):mutexPtr(pm,unlock){ //用互斥量和unlock函数初始化shared_ptr
        lock(mutexPtr.get());//作为删除器
    }
private:
    std::shared_ptr<Mutex> mutexPtr;//获取普通指针
};

用独立语句将new的对象置入智能指针

主要是为了防止内存泄漏
考虑以下场景:

processWidget(shared_ptr<Widget>(new Widget), priority()) // 可能会造成内存泄漏

内存泄漏的原因为:先执行new Widget,再调用priority, 最后执行shared_ptr构造函数,那么当priority的调用发生异常的时候,new Widget返回的指针就会丢失了。当然不同编译器对上面这个代码的执行顺序不一样。所以安全的做法是:

shared_ptr<Widget> pw(new Widget)
processWidget(pw, priority())

这就是用独立语句将new的对象置入智能指针。凡是有new语句的,尽量放在单独的语句当中,特别是当使用new出来的对象放到智能指针里面的时候

写更加容易被正确使用的接口

Date(int month, int day, int year);

这一段代码可以有很多问题,例如用户将day和month顺序写反(因为三个参数都是int类型的),可以修改成:
Date(const Month &m, const Day &d, const Year &y);//注意这里将每一个类型的数据单独设计成一个类,同时加上const限定符,为了让接口更加易用,可以对month加以限制,只有12个月份

class Month{
    public:
    static Month Jan(){return Month(1);}//这里用函数代替对象,主要是方式第四条:non-local static对象的初始化顺序问题
}

对于一些返回指针的问题函数,例如:Investment *createInvestment(); 可以设计成返回类型为智能指针类型:std::shared_ptr createInvestment();

以pass-by-reference-to-const替换pass-by-value

有些传递参数的时候用引用和cosnt结合的操作,可以降低很多的构造和析构函数调用。

bool validateStudent(const Student &s);//省了很多构造析构拷贝赋值操作
bool validateStudent(s);

subStudent s;
validateStudent(s);//调用后,则在validateStudent函数内部实际上是一个student类型,如果有重载操作的话会出现问题

对于内置类型和stl标准库中的迭代器和函数对象,pass-by-value更加合适

必须返回对象时,别妄想返回其reference,因为很容易返回一个被删除的局部变量,就算是没被删除,在外部也不好释放堆内存

以non-member、non-friend替换member函数

class WebBrowser{
    public:
    void clearCache();
    void clearHistory();
    void removeCookies();
}

member 函数:
class WebBrowser{
    public:
    ......
    void clearEverything(){ clearCache(); clearHistory();removeCookies();}
}

non-member non-friend函数:
void clearBrowser(WebBrowser& wb){
    wb.clearCache();
    wb.clearHistory();
    wb.removeCookies();
}
  • member可以访问class的private函数,enums,typedefs等,但是non-member函数则无法访问上面这些东西,所以non-member non-friend函数更好。
  • 熟悉namespace用法:namespace可以用来对某些便利函数进行分割,将同一个命名空间中的不同类型的方法放到不同文件中,C++标准库就是这么用的。

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

class Rational{
    public:
    const Rational operator* (const Rational& rhs)const;
}
Rational oneHalf;
result = oneHalf * 2;
result = 2 * oneHalf;//出错,因为没有int转Rational函数

non-member函数
class Rational{}
const Rational operator*(const Rational& lhs, const Rational& rhs){
	return Rational(lhs.numerator()*rhs.numerator(), lhs.denominator()* rhs.denominator());
}
result = 2 * oneHalf; //不会出错

member函数中,无法解析2导致编译出错,non-member函数重新定义,就可以对2进行编译。

尽量不要进行强制类型转换

  • 尽可能延后变量定义式的出现时间:主要是防止变量在定义以后没有使用,影响效率,应该在用到的时候再定义,同时通过default构造而不是赋值来初始化。

尽量不要进行强制类型转换:

  • double转int会丢失精度。
  • 将一个类转换成他的父类也容易出现问题

C++的四种新型类型转换:应优先使用 static_cast。如果需要处理多态性,使用 dynamic_cast。只有在特殊情况下,当其他转换不适用时,才考虑使用 const_cast 或 reinterpret_cast。

  1. static_cast:它可以在相关类型之间进行转换,例如将浮点数转换为整数,或将派生类的指针转换为基类的指针。
int i = 10;
float f = static_cast<float>(i); // 将int转换为float
  1. dynamic_cast :主要用于处理多态。它在运行时执行安全的向下转换(由基类指针转换为派生类指针)。如果转换失败(即如果指针不实际指向派生类对象),则返回 nullptr。
Base* b = new Derived();
Derived* d = dynamic_cast<Derived*>(b); // 安全的向下转换
  1. const_cast:用来移除或添加 const(或 volatile)属性。它通常用于指针或引用,允许修改被 const 修饰的变量。
const int ci = 10;
int* modifiable = const_cast<int*>(&ci); // 移除const属性
  1. reinterpret_cast:提供了低级别的强制转换,它可以将任意指针类型转换为另一个指针类型,甚至可以转换为足够大的整数类型。这种转换不做任何类型检查,因此可能导致不安全的行为。
void* p = new int(10);
int* ip = reinterpret_cast<int*>(p); // 将void*转换为int*

异常安全的考虑

  • 尽量不要返回指向private变量的指针引用等,如果真的要用,尽量使用const进行限制,同时尽量避免悬吊的可能性。
  • 函数提供的异常安全保证,通常最高只等于其所调用各个函数的“异常安全保证”中最弱的那个。即函数的异常安全保证具有连带性

不要过度使用inline

  • 关键字 inline 建议编译器使用函数定义中的代码替换对该函数的每次调用。
  • 理论上,使用内联函数可以加速程序运行,因为它们消除了与函数调用关联的开销。 调用函数需要将返回地址推送到堆栈、将参数推送到堆栈、跳转到函数体,然后在函数完成时执行返回指令。 通过内联函数可以消除此过程。 相对于未内联扩展的函数,编译器还有其他机会来优化内联扩展的函数。
  • 内联函数的一个缺点是程序的整体大小可能会增加。
  • 内联代码替换操作由编译器自行决定。 例如,如果某个函数的地址已被占用或编译器判定函数过大,则编译器不会内联该函数。
  • inline 函数的过度使用会让程序的体积变大,内存占用过高
  • 编译器是可以拒绝将函数inline的,不过当编译器不知道该调用哪个函数的时候,会报一个warning。
  • 尽量不要为template或者构造函数设置成inline的,因为template inline以后有可能为每一个模板都生成对应的函数,从而让代码过于臃肿 同样的道理,构造函数在实际的过程中也会产生很多的代码.

降低文件间编译的依存关系

当有代码:

#include"date.h"
#include"address.h"
class Person{
private
    Dates m_data;//Dates类
    Addresses m_addr;//Addresses类
}

这种代码情况下,Person和Dates以及Addresses文件之间,形成了编译依存关系。如果这些头文件有任何一个改变,或者这些头文件所依赖的其他头文件有所改变,那么含Person class的文件就得重新编译。程序头文件应该有且仅有声明
如何实现解耦?

  • 定义一个接口类Impl:接口类与具体细目结合,用“声明的依存性”替换“定义的依存性”。
  • 或者定义一个全虚函数:然后通过继承的子类来实现相关的方法。这种情况下这些virtual函数通常被成为factory工厂函数。
原代码:
class Person{
private
    Dates m_data;
    Addresses m_addr;
}

添加一个Person的实现类,定义为PersonImpl,修改后的代码:
class PersonImpl;
class Person{
    private:
    shared_ptr<PersonImpl> pImpl;
}
全虚函数
class Person{
    public:
    virtual ~Person();
    virtual std::string name() const = 0;
    virtual std::string birthDate() const = 0;
}

一些需要在继承时注意的

  • 保证继承逻辑正确性,企鹅不是鸟类。
  • 避免遮掩继承而来的名称:如果继承base类并加上重载函数,而且又希望重新定义或覆写其中的一部分,那么需要加载一个using声明,否则会有些继承函数被覆盖。
    Effective C++ 学习_第1张图片
    Effective C++ 学习_第2张图片
  • pure virtual 函数式提供了一个接口继承,当一个函数式pure virtual的时候,意味着所有的实现都在子类里面实现。不过pure virtual也是可以有实现的,调用他的方法是在调用前加上基类的名称ps->Shape::draw();
  • 接口继承和实现继承不同,在public继承下,derived classes总是继承base的接口。
  • pure virtual函数只具体指定接口继承。
  • (非纯)impure virtual函数具体指定接口继承以及缺省实现继承。
  • non-virtual函数具体指定接口继承以及强制性的实现继承

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

  • 模板方法模式:NVI手法:通过public non-virtual成员函数间接调用private virtual函数,即所谓的template method设计模式:这种方法的优点在于事前工作和事后工作,这些工作能够保证virtual函数在真正工作之前之后被单独调用
class GameCharacter{
public:
    int healthValue() const{
        //做一些事前工作
        int retVal = doHealthValue();
        //做一些事后工作
        return retVal;
    }
private:
    virtual int doHealthValue() const{
        ...                   //缺省算法,计算健康函数
    }
}
  • 函数指针(strategy设计模式:
class GameCharacter; // 前置声明
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
    typedef int (*HealthCalcFunc)(const GameCharacter&);//函数指针
    explicit GameCHaracter(HealthCalcFunc hcf = defaultHealthCalc):healthFunc(hcf){}//可以换一个函数的
    int healthValue()const{return healthFunc(*this);}
private:
    HealthCalcFunc healthFunc;
}
  • 不要重新定义继承而来的non-virtual函数
  • 绝不重新定义继承而来的缺省参数值:virtual函数是动态绑定,但是缺省参数值是静态绑定。
class Shape{
public:
    enum ShapeColor {Red, Green, Blue};
    virtual void draw(ShapeColor color=Red)const = 0;
};
class Rectangle : public Shape{
public:
    virtual void draw(ShapeColor color=Green)const;//和父类的默认参数不同
}
Shape* pr = new Rectangle; // 注意此时pr的静态类型是Shape,但是他的动态类型是Rectangle
pr->draw(); //virtual函数是动态绑定,而缺省参数值是静态绑定,所以会调用Red

复合类

  • 复合是一种类型之间的关系,当某种类型的对象内含有其他类型的对象时,就是复合关系。
  • 比如一个Person类,包含一个Address对象,一个PhoneNumber对象。
  • set并不是一个list,但是set可以has a list:
template<class T>
class Set{
public: 
    void insert();
    //.......
private:
    std::list<T> rep;
}

private继承和多重继承

  • 因为private继承只是一种实现上的继承,不包含接口继承,即有一部分父类的private成员是子类无法访问的,而且经过private继承以后,子类的所有成员都是private的。
  • private继承通常比复合的级别低,但是当derived class 需要访问protect base class 的成员,或者需要重新定义继承而来的virtual函数时,这么设计是合理的。

多重继承:

class BorrowableItem{
    public:
    void checkOut();
};
class ElectronicGadget{
    bool checkOut()const;
};
class MP3Player:public BorrowableItem, public ElectronicGadget{...};
MP3Player mp;
mp.checkOut();//歧义,到底是哪个类的函数
只能使用:
mp.BorrowableItem::checkOut();
  • 在实际应用中, 经常会出现两个类继承与同一个父类,然后再有一个类多继承这两个类:
class Parent{...};
class First : public Parent(...);
class Second : public Parent{...};
class last:public First, public Second{...};
  • 这个时候,last子类会包含两个Parent类中的成员数据,这属于数据冗余,是不能容忍的。可以采用virtual虚继承来避免这种情况。但是virtual继承会带来额外的代价。

隐式接口和编译期多态

  • 面向对象编程:以显式接口(explicit interfaces)和运行期多态(runtime polymorphism)解决问题。
class Widget {
public:
    Widget();
    virtual ~Widget();
    virtual std::size_t size() const;
    void swap(Widget& other); //第25条
}

void doProcessing(Widget& w){
    if(w.size()>10){...}
}
  • 在templete编程中:隐式接口(implicit interface)和编译器多态(compile-time polymorphism)更重要。
template<typename T>
void doProcessing(T& w)
{
    if(w.size()>10){...}
}
  • 在上面这段代码中,w必须支持哪一种接口,由template中执行于w身上的操作来决定,例如T必须支持size等函数。这叫做隐式接口。
  • w的任何函数调用,例如operator>,都有可能造成template具现化,使得调用成功,根据不同的T调用具现化出来不同的函数,这叫做编译期多态。

typename的双重意义

  • 声明template参数时,前缀关键字class和typename是可以互换的
// class和typename两者有什么不同? 没有不同
template<class T> class A;
template<typename T> class A; 
  • 需要使用typename标识嵌套从属类型名称,但不能在base class lists(基类列)或者member initialization list(成员初始列)内以它作为base class修饰符:
template<typename C>
void print2nd(const C& container){
    if(container.size() >=2)
        typename C::const_iterator iter(container.begin());
        //这里的typename表示C::const_iterator是一个类型名称,
        //因为有可能会出现C这个类型里面没有const_iterator这个类型
        //或者C这个类型里面有一个名为const_iterator的变量
}

template class Derived : public typename Base ::Nested{}//错误的!!!!!

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

  • class CompanyA和CompanyB都有发送明文的函数:sendCleartext
  • 信息类MsgInfo:
  • MsgSender:模板类,负责发送消息。根据不同的Company去调用sendCleartext。
  • LoggingMsgSender:在MsgSender的基础上又记录了一些日志。但是无法编译,因为找不到一个特例化的MsgSender< company>。
class CompanyA{
public:
    void sendCleartext(const std::string& msg);
    ....
};

class CompanyB{....};

class MsgInfo{....};

template <typename Company>
class MsgSender{
public:
    void sendClear(const MsgInfo& info){
        std::string msg;
        Company c;
        c.sendCleartext(msg);
    }
}
template<typename Company>//想要在发送消息的时候同时写入log,因此有了这个类
class LoggingMsgSender:public MsgSender<Company>{
    public:
    void sendClearMsg(const MsgInfo& info){
        //记录log
        sendClear(info);//无法通过编译,因为找不到一个特例化的MsgSender
        // 编译的时候,不会主动的调用基类中的函数,因为存在一个特化版本,不存在这个函数会出问题。
    }
}

解决方法:对编译器说:base class template的任何特例化版本都支持其一般版本所提供的接口。

  • 方法一:生成一个全特例化的模板
  • 方法二:使用this:假设sendClear会被继承,会从基类里面寻找
template<typename Company>
class LoggingMsgSender:public MsgSender<Company>{
    public:
    void sendClearMsg(const MsgInfo& info){
        //记录log
        this->sendClear(info);//假设sendClear将被继承
    }
}
  • 方法三:使用using:告诉编译器,请他假设sendClear位于base class里面
template<typename Company>
class LoggingMsgSender:public MsgSender<Company>{
    public:

    using MsgSender<Company>::sendClear; //告诉编译器,请他假设sendClear位于base class里面

    void sendClearMsg(const MsgInfo& info){
        //记录log
        sendClear(info);//假设sendClear将被继承
    }
}
  • 方法四:指明位置,和使用using差不多。
template<typename Company>
class LoggingMsgSender:public MsgSender<Company>{
    public:
    void sendClearMsg(const MsgInfo& info){
        //记录log
        MsgSender<Company>::sendClear(info);//假设sendClear将被继承
    }
}

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

  • 在下面的代码中,sm1和sm2分别属于两个类,就会产生invert()函数,造成代码冗余。
template<typename T, std::size_t n>
class SquareMatrix{
    public:
    void invert();    //求逆矩阵
}

SquareMatrix<double, 5> sm1;
SquareMatrix<double, 10> sm2;
  • 修改代码:SquareMatrix继承自base类,避免了遮掩base版的invert,n的不同大小,不会产生不同的类,代码冗余降低。
template<typename T>
class SquareMatrixBase{
    protected:
    void invert(std::size_t matrixSize);
}

template<typename T, std::size_t n>
class SquareMatrix:private SquareMatrixBase<T>{
    private:
    using SquareMatrixBase<T>::invert;  //避免遮掩base版的invert
    public:
    void invert(){ this->invert(n); }   //一个inline调用,调用base class版的invert
}
  • templates生成多个classes和多个函数,所以任何template代码都不该与某个造成膨胀的template参数产生依赖关系。
  • 因非类型模板参数(non-type template parameters)而造成的代码膨胀,往往可以消除,做法是以函数参数后者class成员变量替换template参数

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

利用多态性将基类指针指向派生类对象很简单,但是对于包含模板的类型转换是比较麻烦的。因为编译器对于派生类模板参数和基类模板参数,是没有连接关系的。

Top* pt2 = new Bottom; //将Bottom*转换为Top*是很容易的
template<typename T>
class SmartPtr{
    public:
    explicit SmartPtr(T* realPtr);
};
SmartPtr<Top> pt2 = SmartPtr<Bottom>(new Bottom);//将SmartPtr转换成SmartPtr是有些麻烦的

可运用模板成员函数,来进行变动:使用成员函数模板生成“可接受所有兼容类型”的函数

template<typename T>
class SmartPtr{
public:
    template<typename U>
    SmartPtr(const SmartPtr<U>& other)  //为了生成copy构造函数
        :heldPtr(other.get()){....}
    T* get() const { return heldPtr; }
private:
    T* heldPtr;                        //这个SmartPtr持有的内置原始指针
};

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

进行混合类型算术运算的时候,会出现编译通过不了的情况

template<typename T>
const Rational<T> operator* (const Rational<T>& lhs, const Rational<T>& rhs){....}

Rational<int> oneHalf(1, 2);
Rational<int> result = oneHalf * 2; //错误,无法通过编译
// no match for 'operator' (operand types are 'Ratinal' and 'int'

解决方法:使用friend声明一个函数,进行混合式调用
这里的friend和非friend函数是没有关联的。类外部的函数,并非为声明函数的实现。

template<typename T>
class Rational{
    public:
    friend const Rational operator*(const Rational& lhs, const Rational& rhs){
        return Rational(lhs.numerator()*rhs.numerator(), lhs.denominator() * rhs.denominator());
    }
};
template<typename T>
const Rational<T> operator*(const Rational<T>& lhs, const Rational<T>&rhs){....}

当我们编写一个class template, 而他所提供的“与此template相关的”函数支持所有参数隐形类型转换时,请将那些函数定义为classtemplate内部的friend函数

认识template元编程

Template metaprogramming是编写执行于编译期间的程序,因为这些代码运行于编译器而不是运行期,所以效率会很高,同时一些运行期容易出现的问题也容易暴露出来

template<unsigned n>
struct Factorial{
    enum{
        value = n * Factorial<n-1>::value
    };
};
// 类似递归出口
template<>
struct Factorial<0>{
    enum{ value = 1 };
};                       //这就是一个计算阶乘的元编程

了解new-handler的行为

当new无法申请到新的内存的时候,会不断的调用new-handler,直到找到足够的内存,new_handler是一个错误处理函数:

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

new_handler是一个函数指针,可以用set_new_handler把函数设置到系统中,返回一个new_handler:set_new_handler允许客户制定一个函数,在内存分配无法获得满足时被调用
一个设计良好的new-handler要做下面的事情:

  • 让更多内存可以被使用
  • 安装另一个new-handler,如果目前这个new-handler无法取得更多可用内存,或许他知道另外哪个new-handler有这个能力,然后用那个new-handler替换自己
  • 卸除new-handler
  • 抛出bad_alloc的异常
  • 不返回,调用abort或者exit

new-handler无法给每个class进行定制,但是可以重写new运算符,设计出自己的new-handler
此时这个new应该类似于下面的实现方式:

void* Widget::operator new(std::size_t size) throw(std::bad_alloc){
        NewHandlerHolder h(std::set_new_handler(currentHandler));  // 安装Widget的new-handler
        return ::operator new(size);                                   
        //分配内存或者抛出异常,恢复global new-handler
    }

new和delete

  • 了解new和delete的合理替换时机。
  • 重写delete的时候,要保证删除null指针永远是安全的
  • 如果operator new接受的参数除了一定会有的size_t之外还有其他的参数delete的时候,也要注意。

参考列表:
Effective C++
https://www.bilibili.com/video/BV1Vs4y1z7zo/

你可能感兴趣的:(C++,c++)