2020秋招_C++笔记之左值和右值,拷贝构造和移动构造,类型自动推导

目录

  • 拷贝构造函数
    • 拷贝构造函数和赋值构造函数
    • 调用拷贝构造函数的场景
    • 拷贝构造函数的参数类型必须是引用
    • 深拷贝和浅拷贝
  • 左值(lvalue)和右值(rvalue)
    • 左值引用和右值引用
    • 移动构造和移动赋值
    • 移动语义和std::move()
    • 通用引用(universal references)
    • 完美转发和std::forward()
    • emplace_back减少内存拷贝和移动
    • 总结
  • 模板类型自动推导理解
    • 情形1:ParamType是指针或者引用类型
    • 情形2:ParamType是通用引用类型(&&)
    • 情形3:ParamType不是指针也不是引用类型
  • auto和decltype

拷贝构造函数

拷贝构造函数和赋值构造函数

拷贝构造函数是一个对象初始化一块内存区域,这块内存就是新对象的内存;而赋值函数是对于一个已经被初始化的对象进行赋值操作。

调用拷贝构造函数的场景

  1. 一个对象以值传递(包括指针传递)的方式传入函数;
  2. 一个对象以值传递的方式从函数返回;
  3. 一个对象需要通过另一个对象进行初始化。

PS:赋值构造函数是用“=”进行赋值操作的时候调用。(此外,cls c2 = c1; // 初始化的时候用“=”是调用拷贝构造函数)

拷贝构造函数的参数类型必须是引用

如果参数类型不是引用,则进行值传递,值传递又会调用拷贝构造函数自身,从而造成无穷递归地调用拷贝构造函数。

深拷贝和浅拷贝

浅拷贝和深拷贝的区别在于,浅拷贝在复制对象的时候没有为新对象复制对象中的指针所指向的外部内存,而深拷贝在复制这个对象的时候为新对象创建了外部内存的独立赋值。
PS:如果两个对象不独立,当对象快结束的时候,会调用两次析构函数,两个对象共用的外部内存会被释放两次,导致指针悬挂(野指针)现象,出现内存的二次释放异常。

类的默认拷贝构造函数只会用被拷贝类的成员的值为拷贝类简单初始化,也就是说如果类成员是指针,只是将指针拷贝了,二者的指针指向的内存空间并没有拷贝,是一致的。因此类的默认拷贝构造函数是浅拷贝。下面是测试代码:

#include 

using namespace std;

class cls{
public:
    int* data = new int[10];
    int d = 1;
};

int main(void)
{
    cls* c1 = new cls();
    cls c2 = *c1;    // 还可以 cls c2(*c1),编译器默认提供的拷贝构造函数
    cls c3;          // 对象默认初始化,分配内存
    c3 = *c1;        // 编译器默认提供的赋值构造函数

    // 对象内部内容的成员变量进行了拷贝
    c1->d =  3;
    cout << "c1: " << c1->d << endl; // 3
    cout << "c1: " << c2.d << endl; // 1
    cout << "c1: " << c3.d << endl; // 1

    // 对象内部的指针所指向的外部内存没有进行拷贝,指向同一块内存地址
    c1->data[0] =  3;
    cout << "c1: " << c1->data[0] << endl; // 3
    cout << "c1: " << c2.data[0] << endl; // 3
    cout << "c1: " << c3.data[0] << endl; // 3
    // 指针所指向的数组首地址一样
    cout << "c1: " << c1->data << endl; 
    cout << "c1: " << c2.data << endl; 
    cout << "c1: " << c3.data << endl; 
    return 0;
}

以下内容是精简版笔记,详细内容来源于[c++11]我理解的右值引用、移动语义和完美转发。
另外的参考:第13课 右值引用

左值(lvalue)和右值(rvalue)

左值是指表达式结束后依然存在的持久化对象,右值是指表达式结束时就不再存在的临时对象。所有的具名变量或者对象都是左值,而右值不具名。很难得到左值和右值的真正定义,但是有一个可以区分左值和右值的便捷方法:看能不能对表达式取地址,如果能,则为左值,否则为右值。

左值引用和右值引用

左值引用(T&)只能绑定左值,右值引用(T&&)只能绑定右值,如果绑定的不对,编译就会失败。但是,常量左值引用(const T&) 是一个“万能”的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是只能读不能改。

总结一下,其中T是一个具体类型:

  • 左值引用,使用 T&, 只能绑定左值
  • 右值引用,使用 T&&, 只能绑定右值
  • 常量左值,使用 const T&, 既可以绑定左值又可以绑定右值
  • 已命名的右值引用,编译器会认为是个左值
  • 编译器有返回值优化(RVO),但不要过于依赖

移动构造和移动赋值

移动构造函数与拷贝构造函数的区别是,拷贝构造的参数是const T& str,是常量左值引用,而移动构造的参数是T&& str,是右值引用。临时对象是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是"偷"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,"偷"也白偷了。下面这张图可以解释copy和move的区别。
2020秋招_C++笔记之左值和右值,拷贝构造和移动构造,类型自动推导_第1张图片

移动语义和std::move()

对于一个左值,肯定是调用拷贝构造函数了,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11为了解决这个问题,提供了std::move()方法来将左值转换为右值,从而方便应用移动语义。

注意点:

  • B= std::move(A),虽然将A的资源给了B,但是A并没有立刻析构,只有在A离开了自己的作用域的时候才会析构,所以,如果继续使用A的成员变量,可能会发生意想不到的错误。
  • 如果我们没有提供移动构造函数,只提供了拷贝构造函数,std::move()会失效但是不会发生错误,因为编译器找不到移动构造函数就去寻找拷贝构造函数,也这是拷贝构造函数的参数是const T&常量左值引用的原因!
  • c++11中的所有容器都实现了move语义,move只是转移了资源的控制权,本质上是将左值强制转化为右值使用,以用于移动拷贝或赋值,避免对含有资源的对象发生无谓的拷贝。move对于拥有如内存、文件句柄等资源的成员的对象有效,如果是一些基本类型,如int和char[10]数组等,如果使用move,仍会发生拷贝(因为没有对应的移动构造函数),所以说move对含有资源的对象说更有意义。

通用引用(universal references)

当右值引用和模板结合的时候,T&&并不一定表示右值引用,它可能是个左值引用又可能是个右值引用。这里的&&是一个未定义的引用类型,称为通用引用,它必须被初始化,它是左值引用还是右值引用却决于它的初始化,如果它被一个左值初始化,它就是一个左值引用;如果被一个右值初始化,它就是一个右值引用
注意:只有当发生自动类型推断时(如函数模板的类型自动推导,或auto关键字),&&才是一个通用引用。

完美转发和std::forward()

所谓转发,就是通过一个函数将参数继续转交给另一个函数进行处理,原参数可能是右值,可能是左值,如果还能继续保持参数的原有特征(左值/右值、const/non-const),那么它就是完美的。

目前唯一的实现完美转发的办法就是借助通用引用类型和std::forward()模板函数共同实现完美转发。(实现原理:第15课 完美转发(std::forward))

template<class T>
void g(T&& t){
	f(std::forward<T>(t));
}

下面测试的代码可以证明借助通用引用类型和std::forward()模板函数能实现完美转发。

#include 
using namespace std;

// 在模板定义语法中关键字class与typename的作用完全一样。 
template<typename T>
void f(T& t){
	cout << "lvalue" << endl;
}
 
template<typename T>
void f(T&& t){
	cout << "rvalue" << endl;
}

template<typename T>
void f(const T&& t){
	cout << "const rvalue" << endl;
}

template<typename T>
void f(const T& t){
	cout << "const lvalue" << endl;
}

// 完美转发实现
// 将函数g的参数原封不动传递到函数f中
template<class T>
void g(T&& t){
	f(std::forward<T>(t));
}
 
int main(){
	g(1); // 传入右值
	
	int x = 1;
	g(x); // 传入左值

	const int y = 1;
	g(move(y)); // 传入const右值

	g(y); // 传入const左值
}

输出结果:
rvalue
lvalue
const rvalue
const lvalue

emplace_back减少内存拷贝和移动

我们之前使用vector一般都喜欢用push_back(),由上文可知容易发生无谓的拷贝,解决办法是为自己的类增加移动拷贝和赋值函数,但其实还有更简单的办法!就是使用emplace_back()替换push_back()

#include 
#include 
#include 
using namespace std;

class A {
public:
    A(int i){
//        cout << "A()" << endl;
        str = to_string(i);
    }
    ~A(){}
    A(const A& other): str(other.str){
        cout << "A&" << endl;
    }

public:
    string str;
};

int main()
{
    vector<A> vec;
    vec.reserve(10);
    for(int i=0;i<10;i++){
        vec.push_back(A(i)); //调用了10次拷贝构造函数
//        vec.emplace_back(i);  //一次拷贝构造函数都没有调用过
    }
    for(int i=0;i<10;i++)
        cout << vec[i].str << endl;
}

emplace_back()可以直接通过构造函数的参数构造对象,但前提是要有对应的构造函数。
对于mapset,可以使用emplace()。基本上emplace_back()对应push_bakc(), emplce()对应insert()。 C++11容器中新增加的emplace相关函数的使用

移动语义对swap()函数的影响也很大,之前实现swap可能需要三次内存拷贝,而有了移动语义后,就可以实现高性能的交换函数了。

总结

  1. 由两种值类型,左值和右值。
  2. 有三种引用类型,左值引用、右值引用和通用引用。左值引用只能绑定左值,右值引用只能绑定右值,通用引用由初始化时绑定的值的类型确定。
  3. 左值和右值是独立于他们的类型的,右值引用可能是左值可能是右值,如果这个右值引用已经被命名了,他就是左值。
  4. 引用折叠规则:所有的右值引用叠加到右值引用上仍然是一个右值引用,其他引用折叠都为左值引用。当T&&为模板参数时,输入左值,它将变成左值引用,输入右值则变成具名的右值应用。
  5. 移动语义可以减少无谓的内存拷贝,要想实现移动语义,需要实现移动构造函数和移动赋值函数。
  6. std::move()将一个左值转换成一个右值,强制使用移动拷贝和赋值函数,这个函数本身并没有对这个左值什么特殊操作。
  7. std::forward()universal references通用引用共同实现完美转发。
  8. empalce_back()替换push_back()增加性能。

以下内容是精简版笔记,详细内容来源于C++类型自动推导

模板类型自动推导理解

下面是一个函数模板的通用例子:

template <typename T>
void f(ParamType param);

f(expr);   // 对函数进行调用

编译器要根据expr来推断出T与ParamType的类型。特别注意的是,这两个类型有可能并不相同,因为ParamType可能会包含修饰词,比如const和&。

情形1:ParamType是指针或者引用类型

最简单的情况ParamType是指针或者引用类型,但不是通用引用类型(&&)。此时,类型推断要点是: 1. 如果expr是引用类型,那就忽略引用部分; 2. 通过相减expr与ParamType的类型来决定T的类型。

情形2:ParamType是通用引用类型(&&)

这种情形有点复杂,因为通用引用类型参数与右值引用参数的形式是一样的,但是它们是有区别的,前者允许左值传入。类型推断的规则如下: 1. 如果expr是左值,T和ParamType都推导为左值引用,尽管其形式上是右值引用(此时仅把&&匹配符,一旦匹配是左值引用,那么&&可以忽略了)。 2. 如果expr是右值,可以看成情形1的右值引用。

情形3:ParamType不是指针也不是引用类型

如果ParamType既不是引用类型,也不是指针类型,那就意味着函数的参数是传值了。传值方式意味着param是传入对象的一个新副本,相应地,类型推断规则为: 1. 如果expr类型是引用,那么其引用属性被忽略; 2. 如果忽略了expr的引用特性后,其是const类型,那么也忽略掉。

因为param是一个新对象,不论其如何改变,都不会影响传入的参数,所以引用属性与const属性都被忽略了。但是有个特殊的情况,当你送入指针变量时,会有些变化。尽管还是传值方式,但是复制是指针,当然改变指针本身的值不会影响传入的指针值,所以指针的const属性可以被忽略。但是指针指向常量的属性却不能忽略,因为你可以通过指针的副本解引用,然后就修改了指针所指向的值,原来的指针指向的内容也会跟着变化,但是原来的指针指向的是const对象。矛盾会产生,所以这个属性无法忽略。

以下内容是精简版笔记,详细内容来源于[C++11新特性— auto 和 decltype 区别和联系](https://blog.csdn.net/y1196645376/article/details/51441503)

auto和decltype

我们希望从表达式中推断出要定义变量的类型,但却不想用表达式的值去初始化变量。还有可能是函数的返回类型为某表达式的的值类型。在这些时候auto显得就无力了,所以C++11又引入了第二种类型说明符decltype,它的作用是选择并返回操作数的数据类型。在此过程中,编译器只是分析表达式并得到它的类型,却不进行实际的计算表达式的值。

decltype(f()) sum = x; // sum的类型就是函数f的返回值类型。 

对于decltype 所用表达式来说,如果变量名加上一对括号,则得到的类型与不加上括号的时候可能不同。如果decltype使用的是一个不加括号的变量,那么得到的结果就是这个变量的类型。但是如果给这个变量加上一个或多层括号,那么编译器会把这个变量当作一个表达式看待,变量是一个可以作为左值的特殊表达式,所以这样的decltype就会返回引用类型:

int i = 42; 
//decltype(i)   int  类型 
//decltype((i)) int& 类型 

你可能感兴趣的:(C++,2020秋招,c++11)