6.6 KiB
使用c++改进出程序性能
2.1右值引用
左值:表达式结束后依然存在的持久对象
右值:表达式结束时就不存在的临时对象
一个区分左值与右值的便捷方法:看能不能对表达式取地址,如果能则为左值,否则为右值。所有的具名变量或者对象都未左值,而右值不具名。
右值有两个概念构成,一个是将亡值,另一个是纯右值。
2.1.1 &&的特性
右值引用就是对一个右值进行引用的类型。因为右值不具名,所以我们只能通过引用的方式找到它。
template<typnename T>
void f(T&& param); //T的类型需要推断,所以是个universal references
template<typename T>
class Test{
Test(Test&& rhs); //已经定义类型,所以&&是个右值
};
void f(Test&& rhs); //同上
template<typnename T>
void f(const T&& params); //&&是个右值,因为universal references仅仅在T&&下发生。
T&&被左值或者右值初始化被称为引用折叠。
总结:
- 左值和右值是独立于他们的类型的,右值引用类型可能是左值也可以是右值。
- auto&&或是函数类型自动推导的T&&是一个未定的引用类型。
- 所有的右值引用叠加到右值引用上仍然是右值引用,其他都是左值引用。当T&&为函数模板时,输入左值,它会变成左值引用,而输入右值时则会变成具名的右值引用。
- 编译器会将已经命名的右值引用视为左值,而将未命名的右值引用视为右值。
2.1.2 右值引用优化性能,避免深拷贝
对于含有堆内存的类,我们需要提供深拷贝的拷贝构造函数,如果使用默认构造函数,会导致堆内存的重复删除:
class A{
public:
A():m_ptr(new int(0))
{
}
~A()
{
delete m_ptr;
}
private:
int* m_ptr;
};
A Get(bool flag)
{
A a;
A b;
if(flag)
return a;
else
return b;
}
int main()
{
A a=Get(false); //运行出错
}
在上面的代码中,默认构造函数是浅拷贝,a和b会指向同一个指针m_ptr,在析构时会导致重复删除该指针,正确的方法是提供深拷贝函数。
A(const A& a):m_ptr(new int(*a.m_ptr))
{
}
但是有时这种拷贝构造函数是没有必要的,使用移动构造函数会更好。
A(A&& a):m_ptr(a.m_ptr)
{
a.m_ptr=nullptr;
}
这里的A&&用来根据参数是左值还是右值来建立分支,如果是临时值,则会选择移动构造函数。
需要注意的是:一般在提供右值引用的构造函数同时,也会提供左值引用的拷贝构造函数,以保证移动不成还可以用拷贝;提供了移动也同时提供拷贝构造函数。
有关拷贝构造函数相关的一些笔记:
拷贝构造函数是C++独有的,它是一种特殊的构造函数,用基于同一类的一个对象构造和初始化另一个对象。
当没有重载拷贝构造函数时,通过默认拷贝构造函数来创建一个对象
A a;
A b(a);
A b=a; 都是拷贝构造函数来创建对象b
强调:这里b对象是不存在的,是用a 对象来构造和初始化b的!!
浅拷贝:如果复制的对象中引用了一个外部内容(例如分配在堆上的数据),那么在复制这个对象的时候,让新旧两个对象指向同一个外部内容,就是浅拷贝。(指针虽然复制了,但所指向的空间内容并没有复制,而是由两个对象共用,两个对象不独立,删除空间存在)
深拷贝:如果在复制这个对象的时候为新对象制作了外部对象的独立复制,就是深拷贝。
拷贝构造函数重载声明如下:
A (const A&other)
下面为拷贝构造函数的实现:
class A
{
int m_i
A(const A& other):m_i(other.m_i)
{
Cout<<”拷贝构造函数”<<endl;
}
}
赋值函数
当一个类的对象向该类的另一个对象赋值时,就会用到该类的赋值函数。
当没有重载赋值函数(赋值运算符)时,通过默认赋值函数来进行赋值操作
A a;
A b;
b=a;
强调:这里a,b对象是已经存在的,是用a 对象来赋值给b的!!
赋值运算的重载声明如下:
A& operator = (const A& other)
- 拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行赋值操作。
class A;
A a;
A b=a; //调用拷贝构造函数(b不存在)
A c(a) ; //调用拷贝构造函数
/****/
class A;
A a;
A b;
b = a ; //调用赋值函数(b存在)</span>
- 一般来说在数据成员包含指针对象的时候,需要考虑两种不同的处理需求:一种是复制指针对象,另一种是引用指针对象。拷贝构造函数大多数情况下是复制,而赋值函数是引用对象
- 实现不一样。拷贝构造函数首先是一个构造函数,它调用时候是通过参数的对象初始化产生一个对象。赋值函数则是把一个新的对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检察一下两个对象是不是同一个对象,如果是,不做任何操作,直接返回。
2.2 move语义
普通左值也可以借助移动语义进行优化
c++11提供std::move方法将左值转化为右值,从而方便使用移动语义。即强制将左值转化为右值引用。
{
std::list<std::string> tokens;
std::list<std::string> t=tokens;
}
std::list<std::string> tokens;
std::list<std::string> t=std::move(tokens);
事实上标准库容器都实现了move语义,而一些int之类的基础类型计算使用move,但仍然会发生拷贝。
2.3 forward和完美转发
右值引用类型独立于值,一个右值引用参数作为函数的形参,在函数内部再转发该参数时它已经变成一个左值了。
所以我们需要“完美转发”,是指在函数模板中,完全依照模板的参数的类型(即保持参数的左值、右值特征),讲参数传递给函数模板中调用另一个函数。c++11中提供了std::forward
2.4emplace_back减少内存拷贝与移动
emplace_back能通过参数构造对象,不需要拷贝或者移动内存,相比push_back能够更好得避免内存的拷贝与移动,使得容器性能得到进一步提升。大多数情况下应该优先使用emplace_back。
其原理是直接利用构造函数直接构造对象
当然在push_back还不能完全被代替,比如在没有提供构造函数的的struct。