C 11学习笔记 18

发布 2022-06-17 16:02:28 阅读 2095

c++未被广泛使用的特征,对某些特殊的应用非常重要,而另一些情况下什么用都没有。

某些应用程序对内存分配有特殊的需求,因此我们无法将标准内存管理机制直接应用于这些程序。它们常常需要自定义内存分配的细节,如使用关键字new将对象放置在特定的内存空间中。为了实现这个目的,我们可以重载new运算符和delete运算符以控制内存分配的过程。

当我们使用一条new表达式时:

string *sp = new string("a value");

string *sp = new string[10];

实际执行了三不操作。第一步,new表达式调用一个名为operator new(或者 operator new[ ]的标准库函数。该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或者对象的数组)。

第二步,编译器允许相应的构造函数以构造这些对象,并为其传入初始值。第三步,对象被分配了空间并构造完成,返回一个指向对象的指针。

当我们使用一个delete表达式删除一个动态分配的对象时:

delete sp;

delete [ arr;

实际上是执行了两步操作。第一步,对sp所指的对象或arr所指的数组中的元素执行对象的析构函数。第二步,编译器调用名为operator delete(或者 operator delete[ ]的标准库函数释放内存空间。

如果应用程序希望控制内置分配过程,则它们需要定义自己的operator new函数和operator delete函数。即使在标准库中已经存在这两个函数的定义,我们仍旧可以定义自己的版本。编译器不会这种重复的定义提出异议,相反,编译器将使用我们定义的版本替代标准库定义的版本。

应用程序可以再全局作用域中定义operator new函数和operator delete函数,也可以将他们定义为成员函数。当编译器发现一条new表达式或delete表达式后,将在程序中查找可供使用的operator函数。如果被分配(释放)的对象是类类型,则编译器首先在类及其基类的作用域中查找。

此时如果该类含有operator new成员或operator delete 成员,则相应的表达式将调用这些成员。否则,编译器在全局作用域查找匹配的函数。此时如果编译器找到了用户自定义的版本,则使用该版本执行new表达式或delete表达式;如果没找到,则使用标准库定义的版本。

我们可以使用作用域运算符令new表达式或delete表达式忽略定义在类中的函数,直接执行全局作用域中的版本。如, :new只在作用域中查找匹配的operator new函数, :

delete与之类似。

标准库定义了operator new函数和operator delete函数的8个重载版本。其中前4个版本可能抛出bad_alloc异常,后4个版本则不会抛出异常:

/这些版本可能抛出异常。

void *operator new(size_t分配一个对象。

void *operator new[ ]size分配一个数组。

void *operator delete(void* )noexcept释放一个对象。

void *operator delete[ ]void* )noexcept释放一个数组。

/这些版本承诺不会抛出异常,void *operator new(size_t, nothrow_t& )noexcept;

void *operator new[ ]size_t, nothrow_t& )noexcept;

void *operator delete(void*, nothrow_t& )noexcept;

void *operator delete[ ]void*, nothrow_t& )noexcept;

类型nothrow_t是定义在new头文件中的一个struct,在这个类型中不包含任何成员。new头文件还定义了一个名为nothrow的const对象,用户可以通过这个对象请求new在非抛出版本。与析构函数类似,operator delete也不允许抛出异常。

当我们重载这些运算符时,必须使用noexcept异常说明符指定其不抛出异常。

应用程序可以自定义上面函数版本中的任意一个,前提是自定义的版本必须位于全局作用域或者类作用域中。当我们将上述运算符定义成类的成员时,它们是隐式静态的。我们无须显示地声明static,当然这么做也不会引发错误。

因为operator new用在对象构造之前而operator用在对象销毁之后,所以正两个成员(new和delete)必须是静态的,而且它们不能操作类的任何数据成员。

对于operator new函数或者operator new[ ]函数来说,它的返回类型必须是void*,第一个形参必须是size_t且该形参不恩能够含有默认实参。当我们为一个对象分配空间时使用operator new;为一个数组分配空间时使用operator new [ 当编译器调用operator new时,把存储指定类型对象所需的字节数传给size_t形参;当调用operator new [ 时,传入函数的则是存储数组中所有元素的空间。

如果我们想自定义operator new函数,则可以为它提供额外的形参。此时,用到这些自定义函数的new表达式必须使用new的定位形式将实参传给新增的形参。尽管在一般情况下我们可以自定义具有任何形参的operator new,但是下面这个函数却无论如何不能被用户重载:

void *operator new(size_t, void*);

这种形式职工标准库使用,不能被用户重新定义。

对于operator delete函数或operator delete函数来说,它们的返回类型必须是void,第一个形参的类型必须是void*。执行一条delete表达式将调用相应的operator函数,并用指向待释放内存的指针来初始化void*形参。

当我们将operator delete或operator delete[ ]定义成类的成员时,该函数可以包含一个类型为size_t的形参。此时,该形参的初始值是第一个形参所指对象的字节数。size_t形参可用于删除继承体系中的对象。

如果基类有一个析构函数,则传递给operator delete的字节数因待删除指针所指对象的动态类型不同而有所区别。而且,实际运行的operator delete函数版本也由对象的动态类型决定。

当自定义了全局operator new和operator delete后,这两个函数必须以某种方式指向分配内存释放内存的操作。而且这两个函数还应该同时满足某些测试的目的,即检验其分配内存的方式是否与常规方式类似。

malloc和free在cstdlib头文件中。malloc函数接受一个表示待分配字节数的size_t,返回指向分配空间的指针或返回0表示分配失败。free函数接受一个void*,他是malloc返回的指针的副本,free将相关内存返回给系统。

调用free(0)没有任何意义。

void *operator new(size_t size)

if(void *mem= malloc(size))

return mem;

elsethrow bad_alloc( )

void operator delete(void *mem) noexcept

free(mem);

尽管operator new函数和operator delete函数一般用于new表达式,然而他们毕竟是标准库的两个普通函数,因此普通**也可以直接调用他们。

这两个函数的行为与allocator的allocate成员和deallocate成员非常类似,他们负责分配或释放内存空间,但是不会构造或销毁对象。

与allocator不同的是,对于operator new分配的内存空间来说我们无法使用construct函数构造对象。相反,我们应该使用new的定位new形式构造对象。new的这种形式为分配函数提供了额外的信息。

我们可以使用定位地址new传递一个地址,此时定位new的形式如下:

new (place_address) type

new (place_address) type (initializers)

new (place_address) type [size]

new (place_address) type [size]

其中place_address必须是一个指针,同时在initializers中提供一个(可能为空的)以逗号分隔的初始值列表,该初始值列表将用于构造新的分配对象。

当仅通过一个地址值调用时,定位new使用operator new(size_t,void*)“分配”它的内存。这是一个我们无法自定义的operator new版本。该函数不分配任何内存,它只是简单地返回指针实参;然后由new表达式负责在指定的地址初始化对象以完成整个工作。

事实上,定位new允许我们在一个特定的、预先分配的内存上构造对象。

我们给allocator的construct的指针必须指向同一个allocator对象分配的空间,但是传给定位new的指针无序指向operator new分配的内存。事实上,传给定位new表达式的指针甚至不需要指向动态内存。

我们可以通过对象调用析构函数,也可以通过对象的指针或引用调用析构函数。

string *sp = new string("a value");

sp->~string( )

和调用destroy类似,显示调用析构函数可以清楚给定对象但是不会释放对象所在的空间。如果需要的话,我们可以重新使用该空间。

显示调用析构函数会销毁对象,但是不会释放内存。

1.经典例子。

#include

#include

constintchunk=16;

classfoo

public:

intval()

foo()private:

int_val;

/预分配内存,但没有foo对象。

char*buf=newchar[sizeof(foo)*chunk];

intmain(void)

/在buf中创建一个foo对象。

foo*pb=new(buf)foo;

/检查一个对象是否被放在buf中。

if(pb->val()=0)

cout<<"newexpressioworked!"

/到这里不能再使用pb

deletebuf;

return0;

但我不了解这种“定位new表达式”相对于一般的new的优点是什么?它的用法比一般的new对象要复杂!

C11学习笔记 5

参数传递时,如果是引用类型,它将绑定到对应的实参上,否者,将实参的值拷贝后赋给形参。引用可避免拷贝,如果不需要改变参数,最好定义成常量引用。和其他初始化一样,当用实参初始化时会忽略顶层const,当形参有顶层const时,传给它常量对象或非常量对象都可以。我们可以用非常量初始化一个底层const对象...

C 11学习笔记 11

动态分配的内存,只有在显式释放是,这些对象才会销毁。但是标准库中的两智能指针可以确保自动释放。除了静态内存和栈内存,每个程序还拥有一个内存池。这部分内存被称作自由空间或堆。静态内存用来保存局部static对象 类static数据成员以及定义在任何函数之外的变量。栈内存用来保存定义在函数没得非stat...

C 11学习笔记 16

tuple是类型pair的模板。不同tuple类型的成员类型也不同,但一个tuple可以有任意数量的成员。每个确定的tuple类型的成员数目是固定的。当我们希望将一些数据组合成单一对象,但有不想麻烦地定义一个新数据结构来表示时,tuple是非常有用的。快速而随意 的数据结构 tuple类型及其伴随类...