默认拷贝函数和默认赋值操作符重载函数

c++ 类的中有两个特殊的构造函数,无参构造函数和拷贝构造函数。它们的特殊之处在于:

  • 当类中没有定义任何构造函数时,编译器会默认提供一个无参构造函数且其函数体为空;

  • 当类中没有定义拷贝构造函数时,编译器会默认提供一个拷贝构造函数,进行成员变量之间的拷贝。(这个拷贝操作是浅拷贝)

  1. int a = 1;
  2. int b;
  3. b = a;

上面的初始化及赋值操作是最正常不过的语法,所以在类的设计上,也兼容这种操作。

  1. class cls {}
  2. int main(void)
  3. {
  4. cls c1;
  5. cls c2 = c1; //初始化类,还可以 cls c2(c1);
  6. cls c3;
  7. c3 = c1; //赋值类
  8. return 0;
  9. }

如上的初始化类需要调用到 cls 类的默认实现的拷贝构造函数,为类赋值需要调用的是 cls 类的默认实现的赋值操作符重载函数,它们都是浅拷贝的。

  1. Student(const Student& student) :name(student.name) {
  2. }
  3. Student& operator = (const Student& student) {
  4. return *this;
  5. }

拷贝构造/赋值函数设置为private属性,其实现体可以什么都不写,那么这个类将变成一个不可被复制的类了。

赋值操作符重载函数

一般地,赋值运算符重载函数的参数是函数所在类的const类型的引用。加const是因为我们不希望在这个函数中对用来进行赋值的“原版”做任何修改。加上const,对于const的和非const的实参,函数就能接受,如果不加,就只能接受非const的实参。

规定都不是强制的,可以不加const,也可以没有引用,甚至参数可以不是函数所在的对象。

当程序没有显式地提供一个以本类或本类的引用为参数的赋值运算符重载函数时,编译器会自动生成这样一个赋值运算符重载函数。只有程序显式提供了以本类或本类的引用为参数的赋值运算符重载函数时,编译器才不会提供默认的版本。

右值引用

int&& a = 1; 右值引用 a 对应的汇编代码等价于一个左值引用引用了一个匿名变量,变量在汇编中等价于基址指针+地址偏移量为var_24 = dword ptr -24h

  • 函数 foo(int&) 和 foo(int&&) 函数体对应的汇编代码完全相同,即在函数内部访问左值引用和右值引用的参数是一样的。

  • 调用函数 foo(int&&) 时,会在主调函数的栈上面分配一个匿名变量的空间,传入的是匿名变量的地址。由于作用域只会将变量往下一级作用域传递,所以不用担心临时变量生命周期早于函数里释放的问题。

  • 右值引用与左值引用本质上是相同的,其实都只是指向一片内存空间的指针而已,它们都只是普通的引用变量。不同点在于,右值引用可以指向纯右值,它能将一个匿名对象(纯右值)持久化,之后就可以像使用一个有名对象一样使用它。

移动语义

当你需要在函数内 copy 参数 并且 要将 copy 的结果保存在非该函数的栈内时。这两个条件必须同时都满足时使用右值引用。(需要拷贝且被拷贝者之后不再被需要)

  1. vector::void push_back (const value_type& val);
  2. vector::void push_back (value_type&& val);

该函数将一个新的元素加到vector的最后面,位置为当前最后一个元素的下一个元素,新的元素的值是val的拷贝(或者是移动拷贝)。

  1. int main () {
  2. std::vector<int> myvector;
  3. int myint;
  4. myvector.push_back (myint); // push_back 的参数存储在了 myvector 中。
  5. return 0;
  6. }
  1. void func1(const T &t)
  2. {
  3. T local = t; // 左值引用代表变量在外部仍在使用
  4. // use local
  5. }
  6. void func2(T&& t)
  7. {
  8. // use t // 右值引用代表变量在外部不在使用
  9. }

本质上来说左值引用和右值引用在汇编层面上都是一致的,但是从句意上来说左值引用代表变量在外部仍在使用需要拷贝才可以去修改/存储。右值引用代表变量可以函数内可以随意使用。这称之为移动语义,将属于 main 函数块的临时对象的所有权,交给 push_back 函数中。

为什么使用左值引用不能叫做移动呢?

虽然 main 函数中的对象可以通过 data 的左值引用将对象本身(而不是拷贝的副本)传递给 push_back 函数,但 push_back 并不真正拥有对象的所有权,因为调用者可能在之后还会使用该对象,所以 push_back 不能修改该对象。

而使用右值引用传递对象,则代表调用者(有意或无意的)保证不会在之后继续使用该对象,所以 push_back 可以任意修改该右值。

移动构造函数

  1. class Person {
  2. private:
  3. int* data;
  4. public:
  5. Person() : data(new int[1000000]){}
  6. ~Person() { delete [] data; }
  7. // 拷贝构造函数,需要拷贝动态资源
  8. Person(const Person& other) : data(new int[1000000]) {
  9. std::copy(other.data,other.data+1000000,data);
  10. }
  11. // 移动构造函数,无需拷贝动态资源
  12. Person(Person&& other) : data(other.data) {
  13. other.data=nullptr; // 源对象的指针应该置空,以免源对象析构时影响本对象
  14. }
  15. };
  16. void func(Person p){
  17. // do_something
  18. }
  19. int main(){
  20. Person p;
  21. func(p); // 调用Person的拷贝构造函数来创建实参
  22. func(Person()); // 调用Person的移动构造函数来创建实参
  23. return 0;
  24. }

value 分类

纯右值 (prvalue)、亡值 (xvalue)、左值 (lvalue)

image.png

prvalue 字面量,this指针,lambda表达式,所有内建数值运算表达式:a + b, a % b, a & b, a << b,&a, 取址表达式

xvalue 就是临时变量(temporary object)。它是快要被销毁的值。有四种,返回右值引用的函数,转换为右值引用的变量,将亡值的属性,将亡值数组的元素。

lvalue就是左值,l代表left,指在内存中具有位置的值。

临时对象

  1. class A {
  2. public:
  3. A() {
  4. std::cout << "A" << std::endl;
  5. }
  6. A(const A& a) {
  7. std::cout << "A-&" << std::endl;
  8. }
  9. A(A&& a) {
  10. std::cout << "A-&&" << std::endl;
  11. }
  12. ~A() {
  13. std::cout << "destructor" << std::endl;
  14. }
  15. };
  16. int main() {
  17. A();
  18. std::cout << "return" << std::endl;
  19. return 0;
  20. }
  21. // A
  22. // destructor
  23. // return

生命周期在 A() 语句结束后就结束了,被称为临时对象。const int& a = A().c;为了保证我们使用 a 的时候依然存在,此时临时对象的生命周期会加长到作用域结束。

std::move

std::move 并不会真正地移动对象,真正的移动操作是在移动构造函数、移动赋值函数等完成的,std::move 只是将参数转换为右值引用而已(相当于一个 static_cast)。

std::move的意思是,被move的对象,不管是左值还是右值,我都要求编译器把它当做一个右值看待,意思就是这个对象的所有权被“交出去”了,原来的这个对象不在使用了,仅此而已。你可以理解为这就是给编译器的一个marker,其本身不会生成任何额外的代码。

右值引用是一个人为的概念,创建了一个新的类型叫右值引用,我们可以编写对应的重载函数,在以右值引用为参数的函数中,我们默认原来对象不在使用了,我们可以随意处理这个对象。

三种传参数方法

  1. class A {
  2. A() = default;
  3. A(std::string arg): s(std::move(arg)) {} // 根据参数 1.copy 2.move (A a1(a))
  4. // 1.move 2.move (A a1(std::move(a)))
  5. A(const std::string& arg): s(arg) {} // 1. 2.copy
  6. A(std::string&& arg): s(std::move(arg)) {} // 1. 2.move
  7. std::string s;
  8. }

将亡值

C++本来只有左值和右值,但是为了能充分利用右值,减少内存的分配,从而引入了将亡值。左值可以通过std::move()转换成将亡值,右值也可以通过std::move()或者隐式类型转换变为将亡值。将亡值仅仅是个标记,表示该表达式所持有的资源,可以被偷取。

纯右值会先隐式转换为将亡值,再选择重载的函数。所以,我们最后需要考虑的情况就分两种,传入的表达式为左值或者将亡值。

  • 函数参数接受的表达式是一个将亡值,对于这种表达式而言,我们可以偷取它所持有的资源。

  • 函数参数接受的表达式是一个左值,对于这种表达式而言,我们不能偷取它所持有的资源。

image.png