ZT C++关键字new学习
http://blog.csdn.net/waken_ma/article/details/4007914
C++关键字new学习
很多新手对C++关键字new可能不是很了解吧,今天我一起来学习一下。
“new”是C++的一个关键字,同时也是操作符。关于new的话题非常多,因为它确实比较复杂,也非常神秘,下面我将把我了解到的与new有关的内容做一个总结。
{
int i;
public:
A(int _i) :i(_i*_i) {}
void Say() { printf(/"i=%d//n/", i); }
};
//调用new:
A* pa = new A(3);
pa->A::A(3);
return pa;
operator就是我们平时所使用的new,其行为就是前面所说的三个步骤,我们不能更改它。但具体到某一步骤中的行为,如果它不满足我们的具体要求
时,我们是有可能更改它的。三个步骤中最后一步只是简单的做一个指针的类型转换,没什么可说的,并且在编译出的代码中也并不需要这种转换,只是人为的认识
罢了。但前两步就有些内容了。
new来完成的,这里的new实际上是像加减乘除一样的操作符,因此也是可以重载的。operator
new默认情况下首先调用分配内存的代码,尝试得到一段堆上的空间,如果成功就返回,如果失败,则转而去调用一个new_hander,然后继续重复前面
过程。如果我们对这个过程不满意,就可以重载operator new,来设置我们希望的行为。例如:
{
public:
void* operator new(size_t size)
{
printf(/"operator new called//n/");
return ::operator new(size);
}
};
A* a = new A();
{
printf(/"global new//n/");
return malloc(size);
}
operator三步操作中的第二步,也就是在取得了一块可以容纳指定类型对象的内存后,在这块内存上构造一个对象,这有点类似于前面代码中的
“p->A::A(3);”这句话,但这并不是一个标准的写法,正确的写法是使用placement new:
[Page]
void main()
{
char s[sizeof(A)];
A* p = (A*)s;
new(p) A(3); //p->A::A(3);
p->Say();
}
A(3)”这种奇怪的写法便是placement
new了,它实现了在指定内存地址上用指定类型的构造函数来构造一个对象的功能,后面A(3)就是对构造函数的显式调用。这里不难发现,这块指定的地址既
可以是栈,又可以是堆,placement对此不加区分。但是,除非特别必要,不要直接使用placement new
,这毕竟不是用来构造对象的正式写法,只不过是new operator的一个步骤而已。使用new
operator地编译器会自动生成对placement
new的调用的代码,因此也会相应的生成使用delete时调用析构函数的代码。如果是像上面那样在栈上使用了placement
new,则必须手工调用析构函数,这也是显式调用析构函数的唯一情况:
{
void* p = null
while(!(p = malloc(size)))
{
if(null == new_handler)
throw bad_alloc();
try
{
new_handler();
}
catch(bad_alloc e)
{
throw e;
}
catch(…)
{}
}
return p;
}
以自定义一个new_handler,并使用std::set_new_handler函数使其生效。在自定义的new_handler中,我们可以抛出
异常,可以结束程序,也可以运行一些代码使得有可能有内存被空闲出来,从而下一次分配时也许会成功,也可以通过set_new_handler来安装另一
个可能更有效的new_handler。例如:
[Page]
{
printf(“New handler called!//n”);
throw std::bad_alloc();
}
std::set_new_handler(MyNewHandler);
用,因为如果这里调用new再失败的话,可能会再导致对new_handler的调用,从而导致无限递归调用。——这是我猜的,并没有尝试过。
{
static int count;
SomeClass() {}
public:
static SomeClass* GetNewInstance()
{
count++;
return new SomeClass();
}
};
{
SomeClass* p = new SomeClass();
count++;
return p;
}
{
lock(someMutex); //加一个锁
delete p;
p = new SomeClass();
unlock(someMutex);
}
STL并不依赖C++默认的内存分配方式,而是使用一套自行实现的方案。首先SGI
STL将可用内存整块的分配,使之成为当前进程可用的内存,当程序中确实需要分配内存时,先从这些已请求好的大内存块中尝试取得内存,如果失败的话再尝试
整块的分配大内存。这种做法有效的避免了大量内存碎片的出现,提高了内存管理效率。
inline void construct(T1* p, const T2& value)
{
new(p) T1(value);
}
new语法中调用构造函数的写法,如果传入的对象value正是所要求的类型T1,那么这里就相当于调用拷贝构造函数。类似的,因使用了
placement new,编译器不会自动产生调用析构函数的代码,需要手工的实现:
inline void destory(T* pointer)
{
pointer->~T();
}
此范围内的对象逐一调用析构函数。如果所传入的对象是非简单类型,这样做是必要的,但如果传入的是简单类型,或者根本没有必要调用析构函数的自定义类型
(例如只包含数个int成员的结构体),那么再逐一调用析构函数是没有必要的,也浪费了时间。为此,STL使用了一种称为“type
traits”的技巧,在编译器就判断出所传入的类型是否需要调用析构函数:
inline void destory(ForwardIterator first, ForwardIterator last)
{
__destory(first, last, value_type(first));
}
inline void __destory(ForwardIterator first, ForwardIterator last, T*)
{
typedef typename __type_traits<T>::has_trivial_destructor trivial_destructor;
__destory_aux(first, last, trivial_destructor());
}
//如果需要调用析构函数:
template<class ForwardIterator>
inline void __destory_aux(ForwardIterator first, ForwardIterator last, __false_type)
{
for(; first < last; ++first)
destory(&*first); //因first是迭代器,*first取出其真正内容,然后再用&取地址
}
//如果不需要,就什么也不做:
tempalte<class ForwardIterator>
inline void __destory_aux(ForwardIterator first, ForwardIterator last, __true_type)
{}
有。这里的关键在于__type_traits<T>这个模板类上,它根据不同的T类型定义出不同的
has_trivial_destructor的结果,如果T是简单类型,就定义为__true_type类型,否则就定义为__false_type类
型。其中__true_type、__false_type只不过是两个没有任何内容的类,对程序的执行结果没有什么意义,但在编译器看来它对模板如何特
化就具有非常重要的指导意义了,正如上面代码所示的那样。__type_traits<T>也是特化了的一系列模板类:
struct __false_type {};
template <class T>
struct __type_traits
{
public:
typedef __false _type has_trivial_destructor;
……
};
template<>//模板特化
struct __type_traits<int> //int的特化版本
{
public:
typedef __true_type has_trivial_destructor;
……
};
…… //其他简单类型的特化版本
[Page]
struct __type_traits<MyClass>
{
public:
typedef __true_type has_trivial_destructor;
……
};
现了在程序编译期通过编译器来决定为每一处调用使用哪个特化版本,于是在不增加编程复杂性的前提下大大提高了程序的运行效率。更详细的内容可参考《STL
源码剖析》第二、三章中的相关内容。
……
delete s;
不过是这块内存被回收,此时使用delete[]与delete没有区别,但如果p指向的是复杂类型,delete[]会针对动态分配得到的每个对象调用
析构函数,然后再释放内存。因此,如果我们对上述分配得到的p指针直接使用delete来回收,虽然编译期不报什么错误(因为编译器根本看不出来这个指针
p是如何分配的),但在运行时(DEBUG情况下)会给出一个Debug assertion failed提示。
{
int a;
public:
MyClass() { printf(/"ctor//n/"); }
~MyClass() { printf(/"dtor//n/"); }
};
void* operator new[](size_t size)
{
void* p = operator new(size);
printf(/"calling new[] with size=%d address=%p//n/", size, p);
return p;
}
// 主函数
MyClass* mc = new MyClass[3];
printf(/"address of mc=%p//n/", mc);
delete[] mc;
为我们申请了16字节,并且在operator
new[]返后我们得到的内存地址是实际申请得到的内存地址值加4的结果。也就是说,当为复杂类型动态分配数组时,系统自动在最终得到的内存地址前空出了
4个字节,我们有理由相信这4个字节的内容与动态分配数组的长度有关。通过单步跟踪,很容易发现这4个字节对应的int值为0x00000003,也就是
说记录的是我们分配的对象的个数。改变一下分配的个数然后再次观察的结果证实了我的想法。于是,我们也有理由认为new[]
operator的行为相当于下面的伪代码:
[Page]
T* New[](int count)
{
int size = sizeof(T) * count + 4;
void* p = T::operator new[](size);
*(int*)p = count;
T* pt = (T*)((int)p + 4);
for(int i = 0; i < count; i++)
new(&pt[i]) T();
return pt;
}
了比预期多4个字节的内存并用它来保存对象的个数,然后对于后面每一块空间使用placement
new来调用无参构造函数,这也就解释了为什么这种情况下类必须有无参构造函数,最后再将首地址返回。类似的,我们很容易写出相应的delete[]的实
现代码:
void Delete[](T* pt)
{
int count = ((int*)pt)[-1];
for(int i = 0; i < count; i++)
pt[i].~T();
void* p = (void*)((int)pt – 4);
T::operator delete[](p);
}
delete[]与operator delete也是,不同的是new operator与new[] operator、delete
operator与delete[] operator。当然,我们可以根据不同的需要来选择重载带有和不带有“[]”的operator
new和delete,以满足不同的具体需求。
operator返回的结果也是相同的,看来,是否在前面添加4个字节,只取决于这个类有没有析构函数,当然,这么说并不确切,正确的说法是这个类是否需
要调用构造函数,因为如下两种情况下虽然这个类没声明析构函数,但还是多申请了4个字节:一是这个类中拥有需要调用析构函数的成员,二是这个类继承自需要
调用析构函数的类。于是,我们可以递归的定义“需要调用析构函数的类”为以下三种情况之一:
delete,或更直接的说free()是如何来回收这块内存的呢?这就要研究malloc()返回的内存的结构了。与new[]类似的是,实际上在
malloc()申请内存时也多申请了数个字节的内容,只不过这与所申请的变量的类型没有任何关系,我们从调用malloc时所传入的参数也可以理解这一
点——它只接收了要申请的内存的长度,并不关系这块内存用来保存什么类型。下面运行这样一段代码做个实验:
for(int i = 0; i < 40; i += 4)
{
char* s = new char[i];
printf(/"alloc %2d bytes, address=%p distance=%d//n/", i, s, s - p);
p = s;
}
内存已经被分配了,于是也忽略它。结果中最小的差值为16字节,直到我们申请16字节时,这个差值变成了24,后面也有类似的规律,那么我们可以认为申请
所得的内存结构是如下这样的:
配时的距离,经过试验一次性分配更大的长度可知,第二个字节也是这个意义,并且代表高8位,也就说前面空的这8个字节中的前两个字节记录了一次分配内存的
长度信息,后面的六个字节可能与空闲内存链表的信息有关,在翻译内存时用来提供必要的信息。这就解答了前面提出的问题,原来C/C++在分配内存时已经记
录了足够充分的信息用于回收内存,只不过我们平常不关心它罢了。
ZT C++关键字new学习的更多相关文章
- C++——volatile关键字的学习
首先声明一点,本文是关于volatile关键字的学习,学习内容主要是来自一些大牛的网络博客. 一篇是何登成先生的C/C++ Volatile关键词深度剖析(http://hedengcheng.com ...
- C++11 noexcept 关键字用法学习
最近学习和写了一个 mint 的板子 ,其中用到了 noexcept 关键字,对这个关键字不太熟悉,便学习一下刘毅学长的文章. C++98 中的异常规范(Exception Specification ...
- 【原创】对Java的synchronized关键字的学习
在Java中,每一个线程都有一个内部锁.当我们使用synchronized关键字时,就是利用这个内部锁来实现线程对某个对象的锁定控制. 那么,如果某个对象中有两个方法,方法一和方法二都使用了synch ...
- zt C++ list 类学习笔记
C++ list 类学习笔记 分类: C++ 2011-09-29 00:12 7819人阅读 评论(0) 收藏 举报 listc++iteratorvectorcconstructor 双向循环链表 ...
- synchronized关键字的学习与总结
- 我在阿里这仨月 前端开发流程 前端进阶的思考 延伸学习的方式很简单:google 一个关键词你能看到十几篇优秀的博文,再这些博文中寻找新的关键字,直到整个大知识点得到突破
我在阿里这仨月 Alibaba 试用期是三个月,转眼三个月过去了,也到了转正述职的时间.回想这三个月做过的事情,很多很杂,但还是有重点. 本文谈一谈工作中遇到的各种场景,需要用到的一些前端知识,以及我 ...
- .NET异步操作学习之一:Async/Await中异常的处理
以前的异常处理,习惯了过程式的把出现的异常全部捕捉一遍,然后再进行处理.Async/Await关键字出来之后的确简化了异步编程,但也带来了一些问题.接下来自己将对这对关键字进行学习.然后把研究结果放在 ...
- 201521123109《java程序设计》第四周学习总结
1. 本周学习总结 #1.1 尝试使用思维导图总结有关继承的知识点. #1.2 使用常规方法总结其他上课内容. - 了解了有关类的继承的知识 - 了解继承和多态的关系以及一些关键字内容 - 学习了O ...
- Java入门(四):运算符优先级、关键字与保留字
上次介绍了Java的运算符,今天来介绍下运算符的优先级,以及Java的关键字.保留字. 一.运算符优先级 序号 运算符 名称 目数 结合性 说明 1 [ ] 方括号 从左向右 优先级最高 . 点号 双 ...
随机推荐
- [转] 使用 JavaScript 创建并下载文件
[From] https://gaohaoyang.github.io/2016/11/22/js-create-file-and-download/ 本文将介绍如何使用 JavaScript 创建文 ...
- QWebView使用
问题: 开始编译的时候在pro文件中要加上 QT += core gui webkitwidgets 文件使用部分加上 #include<QtWebKitWidgets/QWebView&g ...
- python从字符串内取两个符号之间的内容
#取字符串中两个符号之间的东东 def txt_wrap_by(self,start_str, end, html): start = html.find(start_str) if start &g ...
- Iterator遍历 (遍历集合)
迭代器(Iterator) 迭代器是一种设计模式,它是一个对象,它可以遍历并选择序列中的对象,而开发人员不需要了解该序列的底层结构.迭代器通常被称为“轻量级”对象,因为创建它的代价小. Java中的I ...
- vsftpd配置文件解析
对vsftpd配置文件详细解答. 1.默认配置: 1>允许匿名用户和本地用户登陆. anonymous_enable=YES local_enable=YES 2>匿名用户使用的登陆名为f ...
- [Verilog] parameter
parameter和localparam的作用范围均为本模块,区别在于前者可用于在实例化模块的时候进行参数的传递. 用已定义的参数对变量赋值时,按照补码的方式处理,若出现溢出的情况,则截取低位.
- one-vs-all案例
使用one-vs-all初始手写字母识别 数据特点 每一个图片都是20 x 20的像素矩阵,但是在输入的样本中是一个1 x 400的向量,标签y在{0, 1, 2, ..., 9}之间取值 共有500 ...
- (四)selenium打开和关闭浏览器
一.Selenium简介 Selenium3.0主要变更特性: ①移除seleniumRC ②FireFox和Safari推出了自己的driver(geckodriver 和 Safaridriver ...
- 深入理解JavaScript系列(11):执行上下文(Execution Contexts)
简介 从本章开始,我将陆续(翻译.转载.整理)http://dmitrysoshnikov.com/网站关于ECMAScript标标准理解的好文. 本章我们要讲解的是ECMAScript标准里的执行上 ...
- Javaweb三大组件之一 servlet
1.servlet基本使用 注意:单例:init只会执行一次 线程不安全 1.1 创建方式 (1)实现servlet接口[不使用] package cn.getword.servlet; imp ...