C++11学习笔记 18.docx
- 文档编号:26323713
- 上传时间:2023-06-17
- 格式:DOCX
- 页数:37
- 大小:34.64KB
C++11学习笔记 18.docx
《C++11学习笔记 18.docx》由会员分享,可在线阅读,更多相关《C++11学习笔记 18.docx(37页珍藏版)》请在冰豆网上搜索。
C++11学习笔记18
c++未被广泛使用的特征,对某些特殊的应用非常重要,而另一些情况下什么用都没有
某些应用程序对内存分配有特殊的需求,因此我们无法将标准内存管理机制直接应用于这些程序。
它们常常需要自定义内存分配的细节,如使用关键字new将对象放置在特定的内存空间中。
为了实现这个目的,我们可以重载new运算符和delete运算符以控制内存分配的过程。
当我们使用一条new表达式时:
string*sp=newstring("avalue");
string*sp=newstring[10];
实际执行了三不操作。
第一步,new表达式调用一个名为operatornew(或者operatornew[])的标准库函数。
该函数分配一块足够大的、原始的、未命名的内存空间以便存储特定类型的对象(或者对象的数组)。
第二步,编译器允许相应的构造函数以构造这些对象,并为其传入初始值。
第三步,对象被分配了空间并构造完成,返回一个指向对象的指针。
当我们使用一个delete表达式删除一个动态分配的对象时:
deletesp;
delete[]arr;
实际上是执行了两步操作。
第一步,对sp所指的对象或arr所指的数组中的元素执行对象的析构函数。
第二步,编译器调用名为operatordelete(或者operatordelete[])的标准库函数释放内存空间。
如果应用程序希望控制内置分配过程,则它们需要定义自己的operatornew函数和operatordelete函数。
即使在标准库中已经存在这两个函数的定义,我们仍旧可以定义自己的版本。
编译器不会这种重复的定义提出异议,相反,编译器将使用我们定义的版本替代标准库定义的版本。
应用程序可以再全局作用域中定义operatornew函数和operatordelete函数,也可以将他们定义为成员函数。
当编译器发现一条new表达式或delete表达式后,将在程序中查找可供使用的operator函数。
如果被分配(释放)的对象是类类型,则编译器首先在类及其基类的作用域中查找。
此时如果该类含有operatornew成员或operatordelete成员,则相应的表达式将调用这些成员。
否则,编译器在全局作用域查找匹配的函数。
此时如果编译器找到了用户自定义的版本,则使用该版本执行new表达式或delete表达式;如果没找到,则使用标准库定义的版本。
我们可以使用作用域运算符令new表达式或delete表达式忽略定义在类中的函数,直接执行全局作用域中的版本。
如,:
:
new只在作用域中查找匹配的operatornew函数,:
:
delete与之类似。
标准库定义了operatornew函数和operatordelete函数的8个重载版本。
其中前4个版本可能抛出bad_alloc异常,后4个版本则不会抛出异常:
//这些版本可能抛出异常
void*operatornew(size_t); //分配一个对象
void*operatornew[](size); //分配一个数组
void*operatordelete(void*)noexcept; //释放一个对象
void*operatordelete[](void*)noexcept; //释放一个数组
//这些版本承诺不会抛出异常,
void*operatornew(size_t,nothrow_t&)noexcept;
void*operatornew[](size_t,nothrow_t&)noexcept;
void*operatordelete(void*,nothrow_t&)noexcept;
void*operatordelete[](void*,nothrow_t&)noexcept;
类型nothrow_t是定义在new头文件中的一个struct,在这个类型中不包含任何成员。
new头文件还定义了一个名为nothrow的const对象,用户可以通过这个对象请求new在非抛出版本。
与析构函数类似,operatordelete也不允许抛出异常。
当我们重载这些运算符时,必须使用noexcept异常说明符指定其不抛出异常。
应用程序可以自定义上面函数版本中的任意一个,前提是自定义的版本必须位于全局作用域或者类作用域中。
当我们将上述运算符定义成类的成员时,它们是隐式静态的。
我们无须显示地声明static,当然这么做也不会引发错误。
因为operatornew用在对象构造之前而operator用在对象销毁之后,所以正两个成员(new和delete)必须是静态的,而且它们不能操作类的任何数据成员。
对于operatornew函数或者operatornew[]函数来说,它的返回类型必须是void*,第一个形参必须是size_t且该形参不恩能够含有默认实参。
当我们为一个对象分配空间时使用operatornew;为一个数组分配空间时使用operatornew[]。
当编译器调用operatornew时,把存储指定类型对象所需的字节数传给size_t形参;当调用operatornew[]时,传入函数的则是存储数组中所有元素的空间。
如果我们想自定义operatornew函数,则可以为它提供额外的形参。
此时,用到这些自定义函数的new表达式必须使用new的定位形式将实参传给新增的形参。
尽管在一般情况下我们可以自定义具有任何形参的operatornew,但是下面这个函数却无论如何不能被用户重载:
void*operatornew(size_t,void*);
这种形式职工标准库使用,不能被用户重新定义。
对于operatordelete函数或operatordelete[]函数来说,它们的返回类型必须是void,第一个形参的类型必须是void*。
执行一条delete表达式将调用相应的operator函数,并用指向待释放内存的指针来初始化void*形参
当我们将operatordelete或operatordelete[]定义成类的成员时,该函数可以包含一个类型为size_t的形参。
此时,该形参的初始值是第一个形参所指对象的字节数。
size_t形参可用于删除继承体系中的对象。
如果基类有一个析构函数,则传递给operatordelete的字节数因待删除指针所指对象的动态类型不同而有所区别。
而且,实际运行的operatordelete函数版本也由对象的动态类型决定。
当自定义了全局operatornew和operatordelete后,这两个函数必须以某种方式指向分配内存释放内存的操作。
而且这两个函数还应该同时满足某些测试的目的,即检验其分配内存的方式是否与常规方式类似。
malloc和free在cstdlib头文件中。
malloc函数接受一个表示待分配字节数的size_t,返回指向分配空间的指针或返回0表示分配失败。
free函数接受一个void*,他是malloc返回的指针的副本,free将相关内存返回给系统。
调用free(0)没有任何意义
void*operatornew(size_tsize)
{
if(void*mem=malloc(size))
returnmem;
else
throwbad_alloc();
}
voidoperatordelete(void*mem)noexcept
{
free(mem);
}
尽管operatornew函数和operatordelete函数一般用于new表达式,然而他们毕竟是标准库的两个普通函数,因此普通代码也可以直接调用他们。
这两个函数的行为与allocator的allocate成员和deallocate成员非常类似,他们负责分配或释放内存空间,但是不会构造或销毁对象。
与allocator不同的是,对于operatornew分配的内存空间来说我们无法使用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]{brancedinitializerlist}
其中place_address必须是一个指针,同时在initializers中提供一个(可能为空的)以逗号分隔的初始值列表,该初始值列表将用于构造新的分配对象。
当仅通过一个地址值调用时,定位new使用operatornew(size_t,void*)“分配”它的内存。
这是一个我们无法自定义的operatornew版本。
该函数不分配任何内存,它只是简单地返回指针实参;然后由new表达式负责在指定的地址初始化对象以完成整个工作。
事实上,定位new允许我们在一个特定的、预先分配的内存上构造对象。
我们给allocator的construct的指针必须指向同一个allocator对象分配的空间,但是传给定位new的指针无序指向operatornew分配的内存。
事实上,传给定位new表达式的指针甚至不需要指向动态内存。
我们可以通过对象调用析构函数,也可以通过对象的指针或引用调用析构函数。
string*sp=newstring("avalue");
sp->~string();
和调用destroy类似,显示调用析构函数可以清楚给定对象但是不会释放对象所在的空间。
如果需要的话,我们可以重新使用该空间。
显示调用析构函数会销毁对象,但是不会释放内存。
1.经典例子
#include
#include
const intchunk = 16;
class Foo
{
public :
int val( ) { return _val; }
Foo( ) { _val = 0; }
private :
int_val;
};
//预分配内存,但没有Foo对象
char*buf = new char[ sizeof(Foo) * chunk ];
int
main( void )
{
//在buf中创建一个Foo对象
Foo*pb = new (buf) Foo;
//检查一个对象是否被放在buf中
if ( pb->val() == 0 )
{
cout <<"new expressio worked!
" < } //到这里不能再使用pb delete[] buf; return 0; } 但我不了解这种“定位new表达式”相对于一般的new的优点是什么? 它的用法比一般的new对象要复杂! 2.使用定位new表达式作用 1)placement new的作用就是: 创建对象但是不分配内存,而是在已有的内存块上面创建对象。 用于需要反复 创建并删除的对象上,可以降低分配释放内存的性能消耗。 2)定位new表达式(placementnewexpression,C++primerP347),允许程序员将对象创建在已经被分配好的内存中,new表的式的形式如下: new(place_address)type new(palce_address)type(initializer-list) place_address必须是个指针,指向已经分配好的内存。 为了使用这种形式的new表达式,必须包含头文件 定位new表达式不能调用delete删除 placementnew的对象,需要人为的调用对象的析构函数,并且人为的释放掉占用的内存。 #include #include using namespace std; const int chunk = 16; class Foo { public: int val(){return _val;} Foo(){_val=0;} private: int _val; }; int main() { //预分配内存buf char *buf = new char[sizeof(Foo) * chunk]; //在buf中创建一个Foo对象 Foo *pb=new (buf) Foo; //检查一个对象是否被放在buf中 if(pb->val()==0) cout<<"newexpressionworked! "< // 这里不存在与定位new表达式匹配的delete表达式,即: deletepb, 其实只是为了 //释放内存的话,我们不需要这样的表达式,因为定位new表达式并不分配内存。 //如果在析构函数中要做一些其他的操作呢? 就要显示的调用析构函数。 //当程序不再需要buf时,buf指向的内存被删除,它所包含的任何对象的生命期也就 //都结束了。 delete[] buf; return 0; } 定位new表达式的应用: 在我们的程序中,首先使用malloc为对象分配空间,然后通过定位new表达式将对象创建在已经被分配好的内存中,完成构造函数的调用。 3) “new (start) Screen;” 这是定位new操作 Screen *ps = new (start) Screen; 表示在已经开辟好的内存区start中为堆对象Screen申请一个内存 也就是说,如果start占100个字节 你现在从它的第1个字节开始,往里面写入Screen 这样做今后要释放内存的时候有麻烦,如果Screen里面有指针,并开辟了空间,那么delete ps会调用析构函 数,然而在释放指针变量所占的内存空间时也释放了为对象所分配的内存空间,所以start中的那块内存也丢失了,因为ps也指向了start.所以要避免这种情 运行时类型识别(run-timetypeidentification,RTTI)的功能有两个运算符实现: typeid运算符,用于返回表达式的类型。 dynamic_cast运算符,用于将基类的指针或引用安全地转换成派生类的指针或引用。 当我们将这两个运算符用于某种类型的指针或引用,并且该类型含有虚函数时,运算符将使用指针或引用所绑定对象的动态类型。 这两个运算符特别适用于以下情况: 我们想使用基类对象的指针或引用执行派生类操作并且该操作不是虚函数。 一般来说,只要有可能我们应该尽量使用虚函数。 当操作被定义成虚函数时,编译器将根据对象的动态类型自动地选择正确的函数版本。 然而,并非任何时候都能定义一个虚函数。 假设我们无法使用虚函数,则可以使用一个RTTI运算符。 另一方面,与虚函数成员函数相比,使用RTTI运算符蕴含着更多的潜在的风险: 程序员必须清楚地知道转换的目标类型并且必须检查类型转换是否被成功执行。 dynamic_cast dynamic_cast dynamic_cast 其中type必须是一个类类型,并且通常情况下该类型应该含有虚函数。 在第一种形式中,e必须是一个有效的指针;第二种形式中,e必须是一个左值,在第三种形式中,e不能使左值。 在上面的所有形式中,e的类型必须符合以下三个条件中的任意一个: e的类型是目标type的公有派生类、e的类型是目标type的公有基类或e的类型就是目标type的类型。 如果符合则类型可以转换成功。 否则,转换失败。 如果一条dynamic_cast语句的转换目标是指针类型并且失败了,则结果为0.如果转换目标是引用类型并且失败了,则dynamic_cast运算符将抛出一个bad_cast异常。 //Base类至少有一个虚函数,Derived是Base的共有派生类。 if(Derived*dp=dynamic_cast { //使用dp指向的Derived对象 } else { //bp指向一个Base对象 //使用bp指向的Base对象 } 我们可以对一个空指针执行dynamic_cast,结果是所需类型的空指针。 注意,我们在条件部分定义了dp,可以同时完成类型转换和条件检查两项任务。 而且,dp在if语句外部是不可访问的。 一旦转换失败,即使后续的代码忘了做相应判断,也不会接触到这个未绑定的指针,从而确保程序是安全的。 当引用的类型转换失败时,程序抛出一个名为std: : bad_cast的异常,该异常定义在typeinfo标准头文件中。 voidf(constBase&b) { try{ constDerived&d=dynamic_cast }catch(bad_cast){ //处理类型转换失败的情况 } } typeid操作的结果是一个常量对象的引用,该对象的类型是标准库类型type_info或type_info的公有派生类。 type_info类型定义在typeinfo头文件中。 typeid运算符可以作用于任意类型的表达式。 和往常一样顶层const被忽略,如果表达式是一个引用,则typeid返回该引用所引对象的类型。 不过当typeid作用于数组或函数时,并不会执行向指针的标准转换。 也就是说,typeid(a)所得的结果是数组类型而非指针类型。 当运算对象不属于类类型或者是一个不包含任何虚函数的类时,typeid运算符指示的是运算对象的静态类型。 而当运算对象是定义了一个至少一个虚函数的类的左值时,typeid的结果知道运行时才会求得。 通常我们使用typeid比较两天表达式的类型是否相同,或者比较一条表达式的类型是否与指定类型相同。 Derived*dp=newDerived; Base*bp=dp; if(typedid(*bp)==typeid(*dp)) { //bp和dp指向同一类型的对象 } if(typeid(*bp)==typeid(Derived) { //bp实际指向Derived对象 } 注意看,typeid应该作用域对象,因此我们使用*bp而非bp 当typeid作用于指针时(而非指针所值的对象),返回的结果是该指针的静态编译时类型。 typeid是否需要运行时检查决定了表达式是否会被求值。 只有当类型含有虚函数时,编译器才会对表达式求值。 反之,如果类型不含有虚函数,则typeid返回表达式的静态类型;编译器无序对表达式求值也能知道表达式的静态类型。 如果表达式的动态类型可能与静态类型不同,则必须在运行时对表达式求值以确定返回的类型。 这条规则使用于typeid(*p)的情况。 如果指针p所指的类型不含有虚函数,则p不必非得是一个有效的指针。 否则,*p将在运行时求值,此时p必须是一个有效的指针。 如果p是一个空指针,则typeid(*p)将抛出一个名为bad_typeid的异常。 在某些情况下RTTI非常有用,比如我们想为具有继承关系的类实现相等运算符是。 对于两个对象来说,如果他们的类型相同并且对应的数据成成员值相同,则我们说这两个对象是相等的。 在类的继承体系中,每个派生类负责添加自己的数据成员,因此派生类的相等运算符必须把派生类的新成员考虑进来。 一种容易理解的解决方案是定义一套虚函数,令其在继承体系的各个层次上分别执行相等性的判断。 遗憾的是,上述方案是很难实现的。 虚函数的基类版本必须和派生类版本具有相同的形参类型。 要想真正有效的相等比较操作,我们需要首先清除一个事实: 即如果参与比较的两个对象类型不同,则比较结果为false。 我们可以使用RTTI解决问题。 我们定义的相等运算符的形参是基类的引用,然后使用typeid检查两个运算对象的类型是否一样。 如果运算对象的类型不一致,则==返回false;类型一致才调用equal函数。 每个类定义的equal函数赋值比较类型自己的成员。 这些运算符接受Base&形参,但是在进行比较操作前先把运算对象转换成运算符所属的类类型。 classBase{ friendbooloperator==(constBase&,cosntBase&); public: //Base的接口成员 protected: virualboolequal(constBase&)const; //Base的数据成语和其他用于实现的成员 }; classDerived: publicBase{ public: //Derived的其他接口成员 protected: boolequal(constBase&)const; //Derived的数据成员和其他用于实现的成员 }; booloperator==(constBase&lhs,constBase&rhs) { returntypeid(lhs)==typeid(rhs)&&lhs.equal(rhs); } boolDerived: : equal(constBase&rhs)const { autor=dynamic_cast //执行比较Derived对象的操作并返回结果。 } //上述转换永远不会失败,因为毕竟我们只有在验证了运算对象的类型相同之后才会调用该函数。 然而这样的转换时必须的,执行了类型转换之后,当前函数才能访问右侧运算对象的派生类成员。 boolBase: : equal(constBase&rhs)const { //指向比较Base对象的操作 } type_info类的精确定义随着编译器的不同略有差异。 不过c++标准规定type_info类必须定义在typeinfo头文件中,并且至少提供下列操作: t1==t2 (! =) t.name()返回一个c风字符串,表示类型名字的可打印形式。 类型名字的生成凡是因系统而异,并且与在程序中使用的名字不一定一致。 t1.before(t2) 返回一个bool值,表示t1是否位于t2之前。 before所采用的顺序关系是依赖于编译器的 除此之外,因为type_info类一般是作为一个基类出现,所以它还应该提供一个公有的析构函数。 当编译器希望提供
- 配套讲稿:
如PPT文件的首页显示word图标,表示该PPT已包含配套word讲稿。双击word图标可打开word文档。
- 特殊限制:
部分文档作品中含有的国旗、国徽等图片,仅作为作品整体效果示例展示,禁止商用。设计者仅对作品中独创性部分享有著作权。
- 关 键 词:
- C+11学习笔记 18 C+ 11 学习 笔记
![提示](https://static.bdocx.com/images/bang_tan.gif)