BlueRoseNote/02-Note/读书笔记/深入应用C++11代码优化与工程级应用/深入应用c++11代码优化与工程级应用第二章.md
2023-06-29 11:55:02 +08:00

6.6 KiB
Raw Blame History

使用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&&被左值或者右值初始化被称为引用折叠。
总结:

  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,在析构时会导致重复删除该指针,正确的方法是提供深拷贝函数。

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)
  1. 拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存区,而赋值函数是对于一个已经被初始化的对象来进行赋值操作。
class  A;  
A a;  
A b=a;   //调用拷贝构造函数b不存在  
A c(a) ;   //调用拷贝构造函数  
  
/****/  
  
class  A;  
A a;  
A b;     
b = a ;   //调用赋值函数(b存在)</span>  
  1. 一般来说在数据成员包含指针对象的时候,需要考虑两种不同的处理需求:一种是复制指针对象,另一种是引用指针对象。拷贝构造函数大多数情况下是复制,而赋值函数是引用对象
  2. 实现不一样。拷贝构造函数首先是一个构造函数,它调用时候是通过参数的对象初始化产生一个对象。赋值函数则是把一个新的对象赋值给一个原有的对象,所以如果原来的对象中有内存分配要先把内存释放掉,而且还要检察一下两个对象是不是同一个对象,如果是,不做任何操作,直接返回。

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。