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