Effetive C++读书笔记-第8、9章

8 定制new和delete & 9 杂项讨论

49 了解new-handler的行为

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

为了指定new-handler函数,需要调用std::set_new_handler(),其参数是无法分配足够内存时该被调用的函数,返回值是即将被替换的new-handler函数。

namespace std{
    typedef void(*new_handler)();   //函数指针
    new_handler set_new_handler(new_handler p) throw();
}

如果希望处理内存分配失败的情况和class相关,就令每一个class提供自己的set_new_handler和operator new即可。

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;
    reutrn oldHandler;
}

Widget的operator new做以下事情:

  1. 调用标准set_new_handler,告知Widget错误处理函数。但是由于currentHandler被赋值为0,故Widgetnew-handler函数为全局的new-handler
  2. 调用全局operator new,如果失败,全局operator new会调用Widgetnew-handler(全局new-handler),如果全局operator new最终无法分配足够内存,会抛出一个bad_alloc异常。这时Widgetoperator new要恢复原本的全局new-handler,之后传播异常。
  3. 如果全局operator new调用成功,Widgetoperator new会返回一个指针,指向分配的内存。Widget析构函数会管理全局new-handler,它会将Widget类中operator new被调用前的那个全局new-handler恢复回来。

使用方法如下:

void outOfMem();    //函数声明

Widget::set_new_handler(outOfMem);//设定outOfmem为Widget的new-handling函数
Widget* pw1=new Widget;//内存分配失败,则调用outOfMEM

std::string* ps=new std::string;//内存分配失败则调用global new-handling(如果有)

Widget::set_new_handler(0);//设定Widget专属new-handling为null
Widget* pw2=new Widget;//内存分配失败则立刻抛出异常

实现这个方案的class代码基本相同,用个基类加以复用是个好的方法。可以用个模板基类,如此以来每个派生类将获得实体互异的class data复件。这个基类让其派生类继承它获取set_new_handleroperator new,模板部分确保每一个派生类获得一个实体互异的currentHandler成员变量。

template<typename T>
class NewHandlerSupport{
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;
};

template<typename T> std::new_handler
NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw()
{
    std::new_handler oldHandler=currentHandler;
    currentHandler=p;
    return oldHandler;
}

template<typename T> void* NewHandlerSupport<T>::operator new(std::size_t size)
throw(std::bad_alloc)
{
    NewHandlerHolder h(std::set_new_handler(currentHandler);
    return ::operator new(size);
}
//将每一个currentHandler初始化为null
template<typename T>
std::new_handler NewHandlerSupport<T>::currentHandler=0;

有了这个class template,为Widget添加set_new_handler就容易了

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

在模板基类中,从未使用类型T。因为currentHandler是static类型,使用模板的话会是每个class都有自己的currentHandler。

根据C++标准,类模板的隐式实例化只会导致其static数据成员声明的实例化,不会促成其定义的实例化。

2 C++中operator new分配失败抛出异常bad_alloc,但是旧标准是返回null指针。旧标准这个形式为nothrow形式。但是使用nothrow new只能保证operator new不抛出异常,不能保证像new(std::nothrow) Widget这样的表达式不抛出异常。

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

这里的operator newoperator delete操作符,仅仅是分配内存(对应于标准库中的operator newoperator delete),对于构造和析构,是通过类的构造和析构函数完成的。

1 重载operator new操作符时,应当注意两点。一是operator new内部应当有一个无穷循环,并且在背部尝试分配内存,如果无法满足内存需求,就应当调用new-handler。或者是抛出bad_alloc异常。二是当收到大小为0的空间申请时,应当将内存改为1,并分配返回空间。

某个类内部重载了operator new后,应当在函数中考虑大小判断(判断要分配的空间大小是不是等于当前类的大小),这样这个operator new操作符只会被当前类所使用,可以保证当此类的派生类没有重写operator new函数后,不会默认调用基类的operator new操作符而产生错误的内存分配。

2 重载operator delete时,应当在收到null指针请求时不做任何事。

同样的,调用operator delete后,应当在函数中考虑大小判断(判断要分配的空间大小是不是等于当前类的大小),这样这个operator delete操作符只会被当前类所使用,可以保证当此类的派生类没有重写operator delete函数后,不会默认调用基类的operator delete操作符而产生错误的内存分配。

自行编写一个简单的示例(这个示例非常好,可以清晰的表明基类和派生类之间构造和析构的关系,也能表明自行定义的operator newoperator delete的工作过程):

#include <iostream>
using namespace std;

class Base
{
public:
    Base(int _b = 0) : base(_b){ cout << "base ctor\n"; }
    ~Base(){ cout << "base dtor\n"; }
    //重载new和delete
    void *operator new(size_t size) throw(bad_alloc);
    void operator delete(void *rawMemory, size_t size) throw();
    void *operator new[](std::size_t size) throw(bad_alloc);    //唯一需要做的就是提供一块未加工内存(不需要考虑有多少对象)
    void operator delete[](void *p) throw();
private:
    int base;
};
void *Base::operator new(size_t size) throw(bad_alloc)
{
    if(size != sizeof(Base)){   //包括了处理0字节和派生类等大小不同的情况
        cout << "std::operator new\n";
        return ::operator new(size);
    }
    new_handler handler;    //用来保存原始的new_handler函数
    void *ret = nullptr;
    while(true){
        cout << "Base::operator malloc\n";
        ret = malloc(size);
        if(ret) return ret;
#define NEW
#ifdef NEW
        handler = get_new_handler();    //使用此API
#else
        //这两步就是为了保存原始的new_handler
        handler = set_new_handler(nullptr); //传空进去,内存分配不够就会报bad_alloc
        set_new_handler(handler);
#endif

        //没找到足够的空间就用handler函数
        if(handler) handler();
        else throw(bad_alloc());
    }
}
void Base::operator delete(void *rawMemory, size_t size) throw()
{
    if(!rawMemory) return ;
    if(size != sizeof(Base)){
        cout << "std::operator delete\n";
        ::operator delete(rawMemory);
        return ;
    }
    cout << "Base::operator free\n";
    free(rawMemory);    //对应malloc内存分配
    return ;
}
void *Base::operator new[](size_t size){
    //调用operator new(或者自己使用malloc)
    //而operator new直接malloc返回一片raw memory(没有经过任何操作的一块内存)
    cout << "Base::operator new[]\n";
    return operator new(size);
    //return malloc(size);
}
void Base::operator delete[](void *rawMemeory){
    cout << "Base::operator delete[]\n";
    if(rawMemeory) free(rawMemeory);
}

class Derived : public Base
{
public:
    Derived(int _d = 1) : derived(_d){ cout << "derived ctor\n"; };
    ~Derived(){ cout << "derived dtor\n"; }
private:
    int derived;
};

void outofMem() {  //用来作为新的handler函数
    cout << "out of memory" << endl;
    abort();
}

int main()
{
    set_new_handler(outofMem);  //自行定义handler函数
    Base *b = new Base;
    cout << "--------------------------------------\n";
    Derived *d = new Derived;
    cout << "--------------------------------------\n";
    delete b;
    cout << "--------------------------------------\n";
    delete d;
    cout << "--------------------------------------\n";
    Base *barr = new Base[3];
    cout << "--------------------------------------\n";
    Derived *darr = new Derived[3];
    cout << "--------------------------------------\n";
    delete [] barr;
    cout << "--------------------------------------\n";
    delete [] darr;

    return 0;
}

输出如下:

Base::operator malloc
base ctor
--------------------------------------
std::operator new
base ctor
derived ctor
--------------------------------------
base dtor
Base::operator free
--------------------------------------
derived dtor
base dtor
std::operator delete
--------------------------------------
Base::operator new[]
std::operator new
base ctor
base ctor
base ctor
--------------------------------------
Base::operator new[]
std::operator new
base ctor
derived ctor
base ctor
derived ctor
base ctor
derived ctor
--------------------------------------
base dtor
base dtor
base dtor
Base::operator delete[]
--------------------------------------
derived dtor
base dtor
derived dtor
base dtor
derived dtor
base dtor
Base::operator delete[]

52 写了placement new也要写placement delete

1 当自定义operator new之后,在new的过程(有两个函数被调用,第一个函数就是operator new,用以分配内存,第二个是构造函数。)中如果构造环节抛出异常,那么通过operator new申请的空间将无法被归还(用户尚未获得内存地址,无法释放),会造成内存泄漏,编译器无法进行内存释放的原因是找不到和operator new对应的operator delete(如果我们没有自行定义对应的operator delete的话)。

如果使用正常的operator newoperator delete,运行期系统可以找到如何释放new开辟内存的delete函数。但是如果使用非正常形式的operator new,究竟使用那个delete就会有问题了。

如果在operator new中参数除了size_t size(一定会有)之外还有别的参数,那么这就是所谓的placement new

系统标准库中有一个接受长度和void*地址的placement new函数(#include <new>),这个placement new(名字直译为“一个特定位置上的new”)是“接受一个指针指向对象该被构造之处”,声明如下:

 void* operator new(std::size_t, void* pMemory) throw(); //placement new

关于这个placement new的使用方法,示例程序如下:

#include <iostream>
#include <new>
using namespace std;

int main()
{
    //operator new申请一块内存空间,用来存储string对象
    void *rawMemory = operator new(sizeof(string));
    //用string指针指向此块内存空间,使这块内存被视为一个string对象
    string *ps = static_cast<string *>(rawMemory);
    //使用placement new构造此块内存中的对象
    new (ps) string ();
    cout << ps->length() << endl;   //0
    ps->append("abcdefg");  //追加内容
    cout << ps->length() << endl;   //7
    ps->~string(); //析构
    cout << ps->length() << endl;   //0
    operator delete(ps);    //释放空间

    return 0;
}

编译器在寻找和operator new对应的operator delete的时候,是寻找“参数个数和类型”都和operator new相同的某个operator delete,如果找到,就是调用对象。

类似于placement newdelete如果接受额外参数,便称为placement delete。为了防止内存泄漏,placement new应该有一个对应的placement delete

为一个Widget类定义对应的placement newplacement delete。定义如下:

class Widget{
public:
    ...
    static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
    static void operator delete(void* pMemory) throw();
    static void operator delete(void* pMemory, std::ostream& logStream) throw();
};
Widget* pw=new (std:cerr) Widget;//调用operator new,并传递cerr作为ostream实参

这样如果Widget构造函数抛出异常,就会调用对应版本的placement delete。但是如果没有异常,调用delete pw;就会调用正常版本的operator deleteplacement delete只有在placement new调用构造函数抛出异常时才会被调用。

2 成员函数的名称会掩盖其外围作用域中相同名称的函数,所以要避免类专属的new掩盖客户希望调用的new。例如

class Base{
public:
    ...
    static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);//会掩盖global new
    ...
};

Base* pb=new Base;//错误,因为正常形式的operator new被掩盖
Base* pb1=new (std::cerr) Base;//调用Base的placement new

在派生类的operator new会掩盖继承而来的operator new和全局版本的new

class Derived: public Base{
public:
    ...
    static void* operator new(std::size_t size) throw(std::bad_alloc);//重新声明正常形式的new
};


Derived* pd=new (std::clog) Derived;//错误,因为Base的placement new被掩盖了
Derived* pd1=new Derived;//正确

在缺省情况下,C++在全局作用域内提供以下形式的operator new:

void* operator(std::size_t) throw(std::bad_alloc);//普通new
void* operator(std::size_t, void*) throw();//placement new
void* operator(std::size_t, const std::nothrow_t&) throw();//nothrow new

在类内声明任何形式的operator new都会掩盖上面这些标准形式。对于每一个可用的operator new,要确保提供了对应形式的operator delete

可以建立一个基类,内含所有正常形式的new和delete:

class StadardNewDeleteForms{
public:
    //normal
    static void* operator new(size_t size) throw(bad_alloc)
    {return ::operator new(size);}

    static void operator delete(void* pMemory) throw()
    {::operator delete(pMemory);}

    //placement
    static void* operator new(size_t size, void* ptr) throw(bad_alloc)
    {return ::operator new(size, ptr);}

    static void operator delete(void* pMemory, void* ptr) throw()
    {::operator delete(pMemory, ptr);}

    //nothrow
    static void* operator new(size_t size, const nothrow_t& nt) throw(bad_alloc)
    {return ::operator new(size,nt);}

    static void operator delete(void* pMemory,const nothrow_t&) throw()
    {::operator delete(pMemory);}
};

如果想以自定义方式扩充标准形式,可以使用继承机制和using声明:

class Widget: public StandardNewDeleteForms{
public:
    //让这些形式可见
    using StandardNewDeleteForms::operator new;
    using StandardNewDeleteForms::operator delete;
    //添加自己定义的
    static void* operator new(size_t size, ostream& logStream) throw(std:;bad_alloc);
    static void operator detele(size_t size, ostream& logStream) throw();
};

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

本节内容参考:

C++11 std::function用法

C++11 lambda表达式与函数对象

1 std::function(C++11标准库,包含在<functional>中)

可以看做是函数指针的扩充版本(兼容函数指针,更换接口不会引起代码的变化),std::function<>的这种多态能力确实很强,可以定义一个回调列表,而列表的元素可接受的可调用物类型并不相同。类型如下:

template< class R, class... Args >
class function<R(Args...)>;

R是返回值的参数类型,Args是参数类型(这里用到了C++11的变长参数模板)。

对于函数指针来说,一般定义如下:

typedef int(* func)(int, int);

那么func就是一个函数指针了,如果说我们想在map中存储这个函数指针的话:

map<char, func> binops_limit;

那么我们可以这样插入元素到map中:

//普通函数
int add(int i, int j) { return i + j; }
//lambda表达式
auto mod = [](int i, int j){return i % j; };

typedef int(* func)(int, int);
map<char, func> binops_limit;
binops_limit.insert(make_pair( '+', add ));
binops_limit.insert(make_pair( '%', mod ));

对于std::function来说,不仅支持以上两种类型,还支持函数对象:

struct divide
{
    int operator() (int denominator, int divisor)
    {
        return denominator / divisor;
    }
};

typedef function<int(int, int)> funcnew;
map<char, funcnew> binops;
binops.insert( make_pair('+', add) );
binops.insert( make_pair('-', minus<int>()) );
binops.insert( make_pair('*', [](int i, int j){return i - j; }) );
binops.insert( make_pair('/', divide()) );
binops.insert( make_pair('%', mod) );
cout << binops['+'](10, 5) << endl;
cout << binops['-'](10, 5) << endl;
cout << binops['*'](10, 5) << endl;
cout << binops['/'](10, 5) << endl;
cout << binops['%'](10, 5) << endl;

如上所示,function可以将普通函数,lambda表达式和函数对象类统一起来。它们并不是相同的类型,然而通过function模板类,可以转化为相同类型的对象(function对象),从而放入一个map里。

2 std::bind(C++11标准库,包含在<functional>中)

C++0x版本中,我们通常会使用bind1stbind2nd来实现函数对象的参数绑定;使用ptr_fun来实现普通函数到函数对象的转换;使用mem_funmem_fun_refmem_fun1mem_fun_ref1实现成员函数到函数对象的转换。

而在C++11中,可以使用std::bind实现参数转换。

std::bind是一个函数模板, 它就像一个函数适配器,可以把一个原本接收N个参数的函数fn,通过绑定一些参数,返回一个接收M个参数的函数ret,同时还可以实现参数顺序调整等操作。两种函数原型如下:

template <class Fn, class... Args>
bind (Fn&& fn, Args&&... args);

template <class Ret, class Fn, class... Args>
bind (Fn&& fn, Args&&... args);

fn是函数(函数对象/函数指针/成员函数指针),生成一个其有某一个或多个函数参数被“绑定”或重新组织的函数对象。剩下的是参数绑定值1,参数绑定值2,…,参数绑定值n。

bind的返回值是function<R(Args...)>类型的函数。

示例程序如下:

3 lambda表达式(C++11)

lambda表达式是C++11中引入的一项新技术,利用lambda表达式可以编写内嵌的匿名函数,用以替换独立函数或者函数对象,并且使代码更可读。但是从本质上来讲,lambda表达式只是一种语法糖,因为所有其能完成的工作都可以用其它稍微复杂的代码来实现。

几个简单的lambda表达式用法:

#include <iostream>
#include <functional>
using namespace std;

//返回值最好使用auto
//定义lambda表达式(无参无返回值)
function<void(void)> funclambda = []{ cout << "hello" << endl; };
//如果需要参数,那么就要像函数那样,放在圆括号里面,如果有返回值,返回类型要放在->后面,即拖尾返回类型
function<int(int, int)> add = [](int a, int b) -> int { return a + b; };
//也可以忽略返回类型,lambda会自动推断出返回类型
auto multiply = [](int a, int b){ return a * b; };

int main()
{
    funclambda();
    cout << add(1, 2) << endl;
    cout << multiply(3, 4) << endl;

    return 0;
}

lambda表达式最前面的方括号是lambda表达式一个很要的功能,就是闭包。

lambda表达式原理:每当定义一个lambda表达式后,编译器会自动生成一个匿名类(这个类当然重载了()运算符),我们称为闭包类型(closure type)。那么在运行时,这个lambda表达式就会返回一个匿名的闭包实例,其实一个右值(&&)。所以,我们上面的lambda表达式的结果就是一个个闭包。闭包的一个强大之处是其可以通过传值或者引用的方式捕捉其封装作用域内的变量,前面的方括号就是用来定义捕捉模式以及变量,我们又将其称为lambda捕捉块。例子如下:

#include <iostream>
#include <functional>
using namespace std;

int main()
{
    int x = 10;
    auto add_x = [x](int a){ return a + x; };   //按值传递捕捉x(不能给x赋值,按值传递的x不是左值)
    auto multiply_x = [&x](int a){ x = a * x; return x; };  //按引用传递捕捉x,所以这里可以给x赋值(因为按引用传递是左值)
    cout << add_x(7) << " " << multiply_x(3) << endl;

    return 0;
}

当lambda捕捉块为空时,表示没有捕捉任何变量。对于复制传值捕捉方式,类中会相应添加对应类型的非静态数据成员。在运行时,会用复制的值初始化这些成员变量,从而生成闭包。

这意味着lambda表达式无法修改通过复制形式捕捉的变量,因为函数调用运算符的重载方法是const属性的。如果想改动传值方式捕获的值,那么就要使用mutable,将lambda表达式标记为mutable,那么函数调用运算符是非const属性,例子如下:

int main()
{
    int x = 10;
    auto add_x2 = [x](int a) mutable { x *= 2; return a + x; };
    cout << add_x2(7) << endl;

    return 0;
}

lambda表达式是不能被赋值的(因为用delete关键字禁用了赋值操作符):

auto a = [] { cout << "A" << endl; };
auto b = [] { cout << "B" << endl; };

a = b;   // 非法,lambda无法赋值
auto c = a;   // 合法,生成一个副本

具体的捕获类型有:

  • []:默认不捕获任何变量;
  • [=]:默认以值捕获所有变量;
  • [&]:默认以引用捕获所有变量;
  • [x]:仅以值捕获x,其它变量不捕获;
  • [&x]:仅以引用捕获x,其它变量不捕获;
  • [=, &x]:默认以值捕获所有变量,但是x是例外,通过引用捕获;
  • [&, x]:默认以引用捕获所有变量,但是x是例外,通过值捕获;
  • [this]:通过引用捕获当前对象(其实是复制指针);
  • [*this]:通过传值方式捕获当前对象;

而lambda表达式一个更重要的应用是其可以用于函数的参数,通过这种方式可以实现回调函数。

4 随机数生成器(C++11)

此处不表,仅用做记录

猜你喜欢

转载自blog.csdn.net/u012630961/article/details/80363933