详解C++三种new操作符
工作开发过程中,一般申请创建内存,使用的是new方法, 但是new存在三种操作符,其含义和应用的场景都不同, 这三种操作符分别是new operator, operator new, placement new。
那么new的三种操作符应该怎么使用,它们到底有什么区别呢,本文将针对这三种操作符结合例子来进行说明,最后再总结它们的特点。
new operator指的就是new操作符,我们平常使用的操作符,它经历两个阶段的操作:
- 调用::operator new申请内存(operator new后面将进行详细说明,这里理解为C语言中的malloc)
- 调用类的构造函数。
首先定义类Func, 用于后面的验证测试。
使用C++内置字符串对象,申请内存。
使用自定义对象FUNC来申请内存。
最后终端输出结果如下图所示, 可以看出调用new操作符会调用对象的构造函数,而调用delete操作符会调用对象的析构函数。
new operator操作符是不能被重载的,与接下来将要介绍的两种new操作符的一点不同之处。
operator new操作符单纯申请内存,并且是可以重载的函数。
(注意:::operator new 和 ::operator delete前面加上::表示全局)
调用operator new申请内存,内存申请的大小为自定义类Func的大小,经过调试发现,并没有输出类Func的构造函数,也没有调用Func的析构函数
1) 首先FUNC类中添加如下信息,重载operator new操作符,支持接受一个参数。重载operator delete操作符,支持接受一个参数,该参数是一个指针,指向将要释放内存的地址。
2) 主程序中调用new创建FUNC对象,然后调用delete释放对象
3) 运行调试之后的结果信息如下所示,new调用到重载的函数operator new, 同样的, delete也调用到重载的函数operator delete
1)首先FUNC类中添加如下信息,重载operator new操作符,但是支持两个参数,第一个参数是申请内存的大小,第二个参数则是一个字符串信息。
2)主程序中调用new创建FUNC对象,并且构造函数传入字符串信息,然后调用delete释放对象
3)运行调试之后的结果信息如下所示,new调用到重载的函数operator new的第二个版本
placement new操作符是重载operator new的一个版本,该函数的执行忽略了size_t参数,只返还第二个参数,该函数允许在已经构建好的内存中创建对象,这个是什么概念呢,后面将进行说明。下面是placement new操作符的声明以及调用方法。
1、placement new操作符的使用方法,首先提前申请好内存,然后在需要使用FUNC对象的时候,调用placement new来将对象指向已经创建好的内存地址,最后使用完成之后,需要手动调用析构函数,并且释放创建的内存。
2、 终端输出打印信息如下所示, 从中可以发现placement new会调用到对象的构造函数
最后我们总结下new三种操作方的特点,具体如下:
- new operator即new操作符,不能被重载,调用的时候,先申请内存,再调用构造函数,这是常用的调用方式。
- operator new操作符,能够被重载,单纯申请内存,相当于C语言中的malloc, 如果重载了operator new操作符,又需要调用原来的函数,那么需要在操作符前面加上::(即 ::operator new),重载该操作符通常是为了实现不同的内存分配方式。
- placement new操作符,仅仅返回已经申请好内存的指针,它通常应用在对效率要求高的场景下,提前申请好内存,能够节省申请内存过程中耗费的时间。
不再困惑!一文教你读懂C++右值引用和std::move
作者:rickonji 冀铭哲
C++11引入了右值引用,有一定的理解成本,工作中发现不少同事对右值引用理解不深,认为右值引用性能更高等等。本文从实用角度出发,用尽量通俗易懂的语言讲清左右值引用的原理,性能分析及其应用场景,帮助大家在日常编程中用好右值引用和std::move。
首先不考虑引用以减少干扰,可以从2个角度判断:左值可以取地址、位于等号左边;而右值没法取地址,位于等号右边。
- a可以通过 & 取地址,位于等号左边,所以a是左值。
- 5位于等号右边,5没法通过 & 取地址,所以5是个右值。
再举个例子:
- 同样的,a可以通过 & 取地址,位于等号左边,所以a是左值。
- A()是个临时值,没法通过 & 取地址,位于等号右边,所以A()是个右值。
可见左右值的概念很清晰,有地址的变量就是左值,没有地址的字面值、临时值就是右值。
引用本质是别名,可以通过引用修改变量的值,传参时传引用可以避免拷贝,其实现原理和指针类似。 个人认为,引用出现的本意是为了降低C语言指针的使用难度,但现在指针+左右值引用共同存在,反而大大增加了学习和理解成本。
左值引用大家都很熟悉,能指向左值,不能指向右值的就是左值引用:
引用是变量的别名,由于右值没有地址,没法被修改,所以左值引用无法指向右值。
但是,const左值引用是可以指向右值的:
const左值引用不会修改指向值,因此可以指向右值,这也是为什么要使用const &作为函数参数的原因之一,如std::vector的push_back:
如果没有const,vec.push_back(5)这样的代码就无法编译通过了。
再看下右值引用,右值引用的标志是&&,顾名思义,右值引用专门为右值而生,可以指向右值,不能指向左值:
下边的论述比较复杂,也是本文的核心,对理解这些概念非常重要。
有办法,std::move:
在上边的代码里,看上去是左值a通过std::move移动到了右值ref_a_right中,那是不是a里边就没有值了?并不是,打印出a的值仍然是5。
std::move是一个非常有迷惑性的函数,不理解左右值概念的人们往往以为它能把一个变量里的内容移动到另一个变量,但事实上std::move移动不了什么,唯一的功能是把左值强制转化为右值,让右值引用可以指向左值。其实现等同于一个类型转换:static_cast<T&&>(lvalue)。 所以,单纯的std::move(xxx)不会有性能提升,std::move的使用场景在第三章会讲。
同样的,右值引用能指向右值,本质上也是把右值提升为一个左值,并定义一个右值引用通过std::move指向该左值:
被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。仔细看下边代码:
看完后你可能有个问题,std::move会返回一个右值引用int &&,它是左值还是右值呢? 从表达式int &&ref = std::move(a)来看,右值引用ref指向的必须是右值,所以move返回的int &&是个右值。所以右值引用既可能是左值,又可能是右值吗? 确实如此:右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。
或者说:作为函数返回值的 && 是右值,直接声明出来的 && 是左值。 这同样也符合第一章对左值,右值的判定方式:其实引用和普通变量是一样的,int &&ref = std::move(a)和 int a = 5没有什么区别,等号左边就是左值,右边就是右值。
最后,从上述分析中我们得到如下结论:
- 从性能上讲,左右值引用没有区别,传参使用左右值引用都可以避免拷贝。
- 右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)。
- 作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
按上文分析,std::move只是类型转换工具,不会对性能有好处;右值引用在作为函数形参时更具灵活性,看上去还是挺鸡肋的。他们有什么实际应用场景吗?
在实际场景中,右值引用和std::move被广泛用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能。 在没有右值引用之前,一个简单的数组类通常实现如下,有构造函数、拷贝构造函数、赋值运算符重载、析构函数等。深拷贝/浅拷贝在此不做讲解。
该类的拷贝构造函数、赋值运算符重载函数已经通过使用左值引用传参来避免一次多余拷贝了,但是内部实现要深拷贝,无法避免。 这时,有人提出一个想法:是不是可以提供一个移动构造函数,把被拷贝者的数据移动过来,被拷贝者后边就不要了,这样就可以避免深拷贝了,如:
这么做有2个问题:
- 不优雅,表示移动语义还需要一个额外的参数(或者其他方式)。
- 无法实现!temp_array是个const左值引用,无法被修改,所以temp_array.data_ = nullptr;这行会编译不过。当然函数参数可以改成非const:Array(Array& temp_array, bool move){…},这样也有问题,由于左值引用不能接右值,Array a = Array(Array(), true);这种调用方式就没法用了。
可以发现左值引用真是用的很不爽,右值引用的出现解决了这个问题,在STL的很多容器中,都实现了以右值引用为参数的移动构造函数和移动赋值重载函数,或者其他函数,最常见的如std::vector的push_back和emplace_back。参数为左值引用意味着拷贝,为右值引用意味着移动。
如何使用:
在vector和string这个场景,加个std::move会调用到移动语义函数,避免了深拷贝。
除非设计不允许移动,STL类大都支持移动语义函数,即可移动的。 另外,编译器会默认在用户自定义的class和struct中生成移动语义函数,但前提是用户没有主动定义该类的拷贝构造等函数(具体规则自行百度哈)。 因此,可移动对象在<需要拷贝且被拷贝者之后不再被需要>的场景,建议使用std::move触发移动语义,提升性能。
还有些STL类是move-only的,比如unique_ptr,这种类只有移动构造函数,因此只能移动(转移内部对象所有权,或者叫浅拷贝),不能拷贝(深拷贝):
std::move本身只做类型转换,对性能无影响。 我们可以在自己的类中实现移动语义,避免深拷贝,充分利用右值引用和std::move的语言特性。
和std::move一样,它的兄弟std::forward也充满了迷惑性,虽然名字含义是转发,但他并不会做转发,同样也是做类型转换.
与move相比,forward更强大,move只能转出来右值,forward都可以。
std::forward<T>(u)有两个参数:T与 u。 a. 当T为左值引用类型时,u将被转换为T类型的左值; b. 否则u将被转换为T类型右值。
举个例子,有main,A,B三个函数,调用关系为:main->A->B,建议先看懂再看这里:
例2:
上边的示例在日常编程中基本不会用到,std::forward最主要运于模版编程的参数转发中,想深入了解需要学习万能引用(T &&)和引用折叠(eg:& && → ?)等知识,本文就不详细介绍这些了。
如有错误,请指正!
构造函数和析构函数
构造函数
概念:
构造函数是一种用于创建对象的特殊成员函数。
作用:
为对象分配空间
对数据成员赋初值
请求其他资源
特点:
当创建对象时,系统自动调用构造函数,不能在程序中直接调用。
构造函数名与类名相同。
构造函数允许为内联函数、重载函数、带默认形参值的函数。
构造函数可以有任意类型的参数,但不能具有返回类型。
如果程序中未声明,则系统自动产生出一个默认形式的构造函数。
例如:
Class A{
Public:
A(){}//不带参数的构造函数
A(int a=1,int b=2){}//带默认参数的构造函数
Private:
int a,b;
};
Void main()
{ A a1;//调用的是不带参数的构造函数
A a2();//调用带默认参数的构造函数,将a,b的值改为
A a3(3,7);//调用带默认参数的构造函数,将a,b的值改为3,7
}
拷贝构造函数
概念特点:
拷贝构造函数是一种特殊的构造函数,其形参为本类的对象引用,主要下面三种情况下被自动调用:
定义语句中用一个对象初始化另一个对象。
将一个对象作为参数按值调用方式传递给另一个对象时生成对象副本。
生成一个临时的对象作为函数的返回结果。
class 类名
{ public :
类名(形参);//构造函数
类名(类名 &对象名);//拷贝构造函数
…
};
类名::类名(类名 &对象名)//拷贝构造函数的实现
{ 函数体 }
例:
Class A
{private:
Int x,y;
Public:
A(int a=0,int b=0)
{x=a;y=b;}
A(A& aa)//拷贝构造函数
{x=aa.a;y=aa.b;}
}
默认的拷贝构造函数
如果程序员没有为类声明拷贝初始化构造函数,则编译器自己生成一个默认的拷贝构造函数。
这个构造函数执行的功能是:用作为初始值的对象的每个数据成员的值,初始化将要建立的对象的对应数据成员。
析构函数
概念:
析构函数名字为符号“~”加类名,析构函数没有参数和返回值。一个类中只可能定义一个析构函数,所以析构函数不能重载。
作用:
析构函数是用于取消对象的成员函数,当一个对象作用域结束时,系统自动调用析构函数。
特点:
如果一个对象被定义在一个函数体内,则当这个函数结束时,该对象的析构函数被自动调用。
若一个对象是使用new运算符动态创建的,在使用delete运算符释放它时,delete将会自动调用析构函数。
如果程序中未声明析构函数,编译器将自动产生一个默认的析构函数。
类组合的构造函数, 析构函数调用
构造函数调用顺序:先调用内嵌对象的构造函数(按内嵌时的声明顺序,先声明者先构造)。然后调用本类的构造函数。如果有虚函数,则先调用它。
析构函数的调用顺序正好相反。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。