C++学习笔记13:运算符重载(赋值操作符2)

移动语义

完成所有权的移交,当拷贝构造和赋值构造时,目标对象的所有权必须移交给我们的新的对象,原始对象将丧失所有权,_p指针将不再指向原来的那个数组;

左值与右值

C原始定义

  • 左值:可以出现在赋值号的左边或者右边
  • 右值:只能出现在赋值号的右边

C++的定义

  • 左值:用于标识非临时对象或者非成员函数的表达式
  • 右值:用于标识临时对象的表达式或与任何对象无关的值(纯右值),或用于标识即将失效的对象的表达式(失效值)

左值引用与右值引用

左值引用:&

右值引用:&&

  • 深拷贝需要频繁分配和释放内存,效率比较低
  • 移动语义的目的:所有权移交,不需要重新构造和析构
  • 为与构造函数兼容,移动语义必须为引用,而不能是指针或者普通量
  • 普通引用传递左值,以允许函数内部修改目标数据对象
  • 为区分左值引用,实现移动语义时必须传递右值引用
  • 为保证能够修改目标数据对象,在函数内部必须将右值引用作为左值引用对待
class A
{
public:
    A() :_n(0), _p(nullptr){}
    explicit A(int n):_n(n),_p(new int[n]){}
    A(int n, int *p) :_n(n), _p(p) {}
    A(A && that);
    A & operator=(A && that);
    virtual ~A() { if (_p) { delete[]_p, _p = nullptr; } }
public:
    int & operator[](int i);
    const int & operator[](int i)const;
private:
    int _n;
    int *_p;
};

A::A(A && that)
{
    //nullptr:C++11预定义的空指针类型nullptr_t的常对象
    //可隐式转换为任意指针类型和bool类型,但不能转化为整数类型,以取代NULL
    _n = that._n, _p = that._p, that._n = 0, that._p = nullptr;
    //*this = that;//此代码不会调用下面重载的赋值操作符函数
    //具名右值引用that在函数内部被当作左值,不是右值
    //匿名右值引用才会被当作右值,理论上如此.....
    //*this = static_cast<A &&>(that);//等价于*this = std::move(that);
    //上一行代码可以调用下面重载的移动赋值操作符,但是有可能导致程序崩溃
    //因为this指向的本对象可能刚刚分配内存,_p字段所指向的目标数据对象无定义
}

A & A::operator=(A && that)
{
    if (_p)//删除此行代码可能导致内存泄漏
        delete[]_p;
    //可以测试是否为同一个对象,以避免自身复制操作,但意义不大
    _n = that._n, _p = that._p, that._n = 0, that._p = nullptr;
    return *this;
}

移动语义重载

class A
{
public:
    A() :_n(0), _p(nullptr) {}
    explicit A(int n) :_n(n), _p(new int[n]) {}
    A(int n, int *p) :_n(n), _p(p) {}
    //可以同时提供拷贝语义与移动语义版本,前者使用常左值引用
    //不能修改目标数据对象的值,后者则可以修改
    A(const A & that);
    A(A && that);
    A & operator =(const A & that);//深拷贝版本
    A & operator =(A && that);//移动赋值版本
    virtual ~A() { if (_p) { delete[]_p, _p = nullptr; } }
public:
    int & operator[](int i);
    const int & operator[](int i)const;
private:
    int _n;
    int *_p;
};

int main()
{
    A a(4);
    for (int i = 0; i < 4; i++)
    {
        a[i] = i + 1;
    }
    A b(a);//调用拷贝构造函数
    b = a; //调用普通赋值版本
    //把左值引用转换为右值引用,否则会调用左值版本
    A c(static_cast<A &&>(a));//调用移动构造版本
    c = static_cast<A &&>(a);//调用移动赋值版本
    return 0;
}

左值引用同样可以实现移动语义

class A
{
public:
    A() :_n(0), _p(nullptr) {}
    explicit A(int n) :_n(n), _p(new int[n]) {}
    A(int n, int *p) :_n(n), _p(p) {}
    A(A & that);//重载非常量版本;移动构造语义
    A(const A & that);//重载常量版本;深拷贝构造语义
    A & operator=(A &that);//重载非常量版本;移动赋值语义
    A & operator=(const A & that);//重载常量版本;深拷贝赋值语义
    virtual ~A() { if (_p) { delete[]_p, _p = nullptr; } }
public:
    int & operator[](int i) throw(std::out_of_range);
    const int & operator[](int i) const throw(std::out_of_range);
private:
    int _n;
    int *_p;
};

A::A(A & that)
{
    _n = that._n, _p = that._p, that._n = 0, that._p = nullptr;
}

A::A(const A & that)
{
    this->_n = that._n;
    _p = new int[_n];
    for (int i = 0; i < _n; i++)
    {
        _p[i] = that._p[i];
    }
}

A & A::operator=(A & that)
{
    _n = that._n, _p = that._p, that._n = 0, that._p = nullptr;
    return *this;
}

A & A::operator=(const A & that)
{
    this->_n = that._n;
    if (_p)
    {
        delete[]_p;
    }
    _p = new int[_n];
    for (int i = 0; i < _n; i++)
    {
        _p[i] = that._p[i];
    }
    return *this;
}
//main.cpp
int main()
{
    A a1;//缺省构造
    const A a2;//缺省构造
    A a3(a1);//调用A::A(A &),移动构造
    A a4(a2);//调用A::A(const A &),深拷贝构造
    //对于非常量,必须转型为常量才能进行深拷贝
    A a5(const_cast<const A &>(a1));//调用A::A(const A &)
    A a6, a7, a8;//缺省构造
    a6 = a1;//调用A::operator=(A &),移动赋值
    a7 = a2;//调用A::operator=(const A &),深拷贝赋值
    a8 = const_cast<const A &>(a1);//调用A::operator=(const A &)
    return 0;
}

右值引用的意义

右值引用可以使用文字作为函数实际参数

//不接受文字作为实际参数,因无法获取文字的左值
int f(int &x) { return ++x; }
//接受文字作为实际参数,传递右值引用
//具名右值引用作为左值,匿名右值引用作为右值
//在函数内部理论如此,但实际上...
int f(int && x) { return ++x; }
int main()
{
    //错误代码,++操作符的操作数必须为左值
    //std::cout << ++10 << std::endl;
    //可能有问题,传递右值引用,但部分编译器可能将其作为左值
    std::cout << f(10) << std::endl;//11?
    return 0;
}

右值引用的意义

避免编写过多的构造与赋值函数

  • 不管是左值引用还是右值引用,若同时提供拷贝语义与移动语义,需要2对(4个)构造和赋值函数
  • 若通过单独提供成员值的方式构造对象,单成员至少需要4个构造函数和赋值函数,双成员至少需要8个构造和赋值函数
  • 使用右值引用,通过函数模板可以缩减代码编写量

实现完美转发

时间: 2024-11-06 11:59:00

C++学习笔记13:运算符重载(赋值操作符2)的相关文章

C++ Primer Plus学习笔记之运算符重载

C++ Primer Plus学习笔记之运算符重载 1,成员函数和友元函数选择的建议 下面我们先看两个例子: 成员函数重载 #include<iostream> using namespace std; class Complex { public: Complex(double r=0,double i=0) { re=r; im=i; } Complex operator+(const Complex& obj); Complex operator!(); void Display

C++学习笔记之运算符重载

一.运算符重载基本知识 在前面的一篇博文 C++学习笔记之模板(1)——从函数重载到函数模板 中,介绍了函数重载的概念,定义及用法,函数重载(也被称之为函数多态)就是使用户能够定义多个名称相同但特征标(参数列表)不同的函数,目的是在对不同类型的参数执行相同的操作时只用一个同名的函数. 运算符重载,就是使同一个运算符在面临不同类型的数据时作出不同的操作(函数重载是操作相同),就是让同一个运算符有多重功能.实际上我们经常用的许多运算符已被重载,例如,将*用于地址,将得到存储在这个地址中的值:但将它用

PKU C++程序设计实习 学习笔记4 运算符重载

第四章 运算符重载 4.1 运算符重载的基本概念 1. 运算符 2. 自定义数据类型与运算符重载 C++提供了数据抽象的手段:用户自己定义数据类型 -- 类 ? 调用类的成员函数->操作它的对象 类的成员函数->操作对象时,很不方便 ? 在数学上,两个复数可以直接进行+/-等运算 Vs. 在C++中,直接将+或-用于复数是不允许的 3. 运算符重载 对抽象数据类型也能够直接使用C++提供的运算符 ? 程序更简洁 ? 代码更容易理解 运算符重载 ? 对已有的运算符赋予多重的含义 ? 使同一运算符

Swift学习笔记(13)--属性 (Properties)

普通属性用var和let即可,本文不做详述 1.延迟存储属性 延迟存储属性是指当第一次被调用的时候才会计算其初始值的属性.在属性声明前使用@lazy来标示一个延迟存储属性. class DataImporter { /* DataImporter 是一个将外部文件中的数据导入的类. 这个类的初始化会消耗不少时间. */ var fileName = "data.txt" // 这是提供数据导入功能 } class DataManager { @lazy var importer = D

Java学习笔记之方法重载,动态方法调度和抽象类

一.方法重载 如果子类中的方法与它的超类中的方法有相同的方法名,则称子类中的方法重载超类中的方法,特别是当超类和子类中的方法名和参数类型都相同时,在子类中调用该方法时,超类中的方法会被隐藏.考虑下面程序: 1 class A 2 { 3 int i, j; 4 A(int a, int b) 5 { 6 i = a; 7 j = b; 8 } 9 10 // display i and j 11 void show() 12 { 13 System.out.println("i and j: &

Perl语言学习笔记 13 目标操作

1.改变目录 chdir "/etc" or die "can't chdir to '/etc'!\n"; 省略参数会回到用户主目录,与cd效果一样: 2.文件名通配 my @all_files = glob "*"; #不包括以点号开头的文件 my @pm_files = glob "*.pm"; 一次匹配多种模式,用空格隔开:my @files = ".* *"; #可以匹配所有的文件,包括以点号开头

HTML&CSS基础学习笔记13—无序列表

无序列表 有时我们的工作繁忙,杂事很多,怕忘记,就会把事情一件件列出来,防止忘记. 它们的排列顺序对于我们来说并不重要,可以随意调换,我们将它称为无序列表,HTML里用<ul>标签来表示无序列表,列表里的项目则用<li>标签来表示: 1 2 3 4 5 <ul>     <li></li>     <li></li>     ... </ul> 看一段实例代码: 对于的浏览器显示结果是这样的: 更多内容学习,请

C++ Primer笔记12_运算符重载_递增递减运算符_成员访问运算符

1.递增递减运算符 C++语言并不要求递增递减运算符必须是类的成员.但是因为他们改变的正好是所操作对象的状态,所以建议设定为成员函数. 对于递增与递减运算符来说,有前置与后置两个版本,因此,我们应该为类定义两个版本的递增与递减运算符. 问题来了,程序是如何区分前置和后置呢?因为都是++和-- 为了解决这个问题,后置版本的递增递减运算符接受一个额外的(不被使用)int类型的形参.当我们使用后置运算符时,编译器为这个形参提供一个值为0的实参.这个形参唯一的作用就是区分前置和后置运算符函数. 因为不会

PHP学习笔记——3.运算符

目录: PHP学习笔记——1.变量 PHP学习笔记——2.常量 PHP学习笔记——3.运算符 1.算术运算符 + - * / %(求模) %:余数的正负值是由被除数决定的 2.比较运算符 == != > < >= <= === !== ===与!==比较的是变量的值和类型 注:在不同类型进行比较时 PHP会将前一个变量先转换成和后一个变量同一类型,再进行比较 布尔->字符串 true->'1' false->'' 字符串->数字 '123abc456'-&g

赋值构造函数(重载赋值操作符)(c++常问问题二)

*什么是赋值构造函数(重载赋值操作符) 下面的代码演示了什么是赋值构造函数,如果不人为定义赋值构造函数,系统将默认给你分配一个浅拷贝的赋值构造函数(下面例子为深拷贝的赋值操作) class cat { public: //构造函数 cat():m_pMyName(NULL),m_unAge(0) { cout<<"cat defult ctor"<<endl; } //子类赋值构造函数(重载赋值操作符) cat& operator=(cat& o