栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > C/C++/C#

【C++】深入运算符重载与拷贝构造

C/C++/C# 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

【C++】深入运算符重载与拷贝构造

深入运算符重载与拷贝构造

目录

深入运算符重载与拷贝构造

1.1 手动实现拷贝构造函数和赋值函数1.2 没有赋值函数并优化实现

1.2.1 没有赋值语句1.2.2 有赋值语句 1.3 完成缺省构造和赋值函数1.3 小结1.4 运算符重载示例练习与分析

1.1 手动实现拷贝构造函数和赋值函数

示例代码:

//需要实现拷贝构造函数和赋值函数

#include
using namespace std;

class Object
{
private:
    int num;
    int arr[5];
public:   
    Object(int n, int val = 0):num(n)
    {
        for(int i = 0; i < n; i++)
        {
            arr[i] = val;
        }
    }
};

int main()
{
    Object obja(5,23);
    Object objb(obja);
    Object objc(5);
    objc = obja;
    
    return 0;
}

当程序执行拷贝构造函数的时候,它的内存图的大概样子。

我们知道了一个类中至少有六个缺省的函数,分别是构造函数、拷贝构造函数、析构函数、赋值函数、普通对象的&的重载以及常对象取地址符的重在函数。

上述代码调用了缺省的拷贝构造函数、赋值函数。那么实际的函数实现是什么样子呢?

接下来我们将手动实现这两个函数:

    //拷贝构造函数
    Object(const Object& obj)
    {
        if (&obj == this)
        {
            return;
        }
        for (int i = 0; i < obj.num; i++)
        {
            this->arr[i] = obj.arr[i];
        }
        this->num = obj.num;

    }

    //也可使用初始化列表
    //Object(const Object& obj) :num(obj.num){}
    Object &operator=(const Object& obj)
    {
        for (int i = 0; i < obj.num; i++)
        {
            this->arr[i] = obj.arr[i];
        }
        this->num = obj.num;

        return *this;
    }

运行结果:

看似赋值函数可以使用,实际上在另一个场景中这样写是错误的。

Object(const Object& obj) :num(obj.num)
{
    memmove(this,&obj,sizeof(Object));
    //将obj的移植给this,大小为Object大小
    return *this;
}

如果我们的某些方法是虚函数,上述方法是不可通过的。

分析过程:在创建对象的时候,在对象最上方有一个虚表指针,指向虚函数表。我们在进行拷贝的时候,会将虚表指针也一起复制过去,而每个对象都有自己独一无二的虚函数表,这样显然不行。

看看下面一种更加糟糕的方案:

Object(int n,int val = 0) : num(n)
{
    memset(this,0,sizeof(Object));
    for(int i = 0; i < n; i++)
    {
        arr[i] = val;
    }
}

分析过程:如果我们存在虚函数,虚表在进入构造函数和拷贝构造函数之前(编译时期)会将被创建。虚指针是在运行时被创建的,当进行内存清空的时候,我们会将类中的虚表一块清理。

注意1:当看见内存拷贝函数的时候,一定要注意类的实际情况。

注意2:如果是一个简单的类型,没有虚函数和继承关系,谨慎使用memset。如果加了虚函数以及虚表,使用memset很容易崩溃,谨慎使用。

1.2 没有赋值函数并优化实现

编译器中默认的做法是什么样子的呢?

如果没有拷贝构造和赋值函数,会有语义方面的拷贝构造和赋值函数。

当类没有虚函数或没有继承关系(简单类),并且没有手动实现的拷贝构造函数和赋值函数它并不会产生相应的函数,将原始对象的首地址抓住,将目的对象的首地址抓住。将原对象中的数据值依次拷贝到目的对象(赋值)。

1.2.1 没有赋值语句

代码示例:

//没有赋值语句

#include
using namespace std;

class Object
{
private:
    int num;
    int arr[5];
public:
    Object(int n, int val = 0):num(n)
    {
        for(int i = 0; i < n; i++)
        {
            arr[i] = val;
        }
    }
    
    void Print() const
    {
        cout << num << endl;
        
        for(int i = 0; i < num; i++)
        {
            cout << arr[i] << " ";
        }
        cout << endl;
    }
};
Object &fun()
{
    Object objx(5,1);
	return objx;
}
int main()
{
    Object obja(5,23);
    Object objb(obja);
    Object objc(5);
    objc = fun();
    objc.Print();
    
    return 0;
}

1、运行结果:

如果有虚函数,会老老实实的将构造函数和赋值函数给出。

如果是继承关系,父类中有拷贝构造,子类中也会继承父类的拷贝构造。

2、分析

如果没有赋值语句,会缺省一个赋值语句,并且没有函数调用过程,不会形成现场保护。我们将返回的对象赋值给objc,会调用赋值语句,但是我们没有写,按理来说系统会生成缺省赋值语句。但是系统是一个简单的类(无虚函数、继承关系),就不会生成缺省赋值函数,而会将原来的对象的首地址进行抓取,将目的对象的首地址进行抓取,将数据值导入到目的对象。从而没有将fun函数释放的栈帧进行清理。

1.2.2 有赋值语句

如果有赋值语句,它会调用该赋值语句。

//有赋值语句,调用赋值语句

#include
using namespace std;

class Object
{
private:
    int num;
    int arr[5];
public:
    Object(int n, int val = 0):num(n)
    {
        for(int i = 0; i < n; i++)
        {
            arr[i] = val;
        }
    }
    
    void Print() const
    {
        cout << num << endl;
        
        for(int i = 0; i < num; i++)
        {
            cout << arr[i] << " ";
        }
        cout << endl;
    }
    
    Object& operator=(const Object& obj)
    {
        for(int i = 0; i < obj.num; i++)
        {
            this->arr[i] = obj.arr[i];
        }
        this->num = obj.num;
        
        return *this; 
	}
};
Object &fun()
{
    Object objx(5,100);
	return objx;
}
int main()
{
    Object obja(5,23);
    Object objb(obja);
    Object objc(5);
    objc = fun();
    objc.Print();
    
    return 0;
}

1、运行结果

fun函数返回的时临时对象的地址。

可以看出生成的是随机数。

2、过程详细情况

构造完obja、objb两个对象,构造objc对象。

初始化数组为 5个0。

调用fun函数,并在fun函数里构造临时对象。

临时对象为5个100。

接着调用赋值函数,可以清楚的看见arr数组中的随机值。

调用打印函数,num值都被覆盖了。

3、分析

如果写了赋值语句,我们会调用赋值语句。我们在主函数中为fun分配一个栈帧。在fun栈帧中,有一个objx对象。我们以引用返回它,返回的是objx对象的地址。随后我们将地址给赋值语句,我们调用赋值函数,obj对象所引用到已失效的栈帧成员,它的数据并没有被改变。但是我们原先的栈帧会给赋值语句使用。

调用赋值语句前:

调用赋值语句后,覆盖之前fun函数使用的栈帧:

所引用的对象已经失效。

注意:无论如何,不要将对象以引用返回。

了解了缺省赋值函数和缺省构造函数的实现和使用过程,剩下的问题也就迎刃而解了。

1.3 完成缺省构造和赋值函数

接着,我们看下一个程序:

//完成缺省赋值函数和缺省拷贝构造函数

#include
using namespace std;


class Object
{
private:
    int num;
    int *ip;
public:
    Object(int n ,int val = 0):num(n) 
    {
        ip = (int *)malloc (sizeof(int) * n);
        for(int i = 0; i < num; i++)
        {
            ip[i] = val;
        }
    }
    ~Object()
    {
        free(ip);
        ip = NULL;
    }
};

编写:

Object(const Object &obj):num(obj.num),ip(obj.ip) {}

Object & operator=(const Object Obj)
{
    if(this != &obj)
    {
        num = obj.num;
        ip = obj.ip;
    }
    return *this;
}

注意:默认的不够使用的原因

当我们从Obja赋值给objc对象的时候,将8替换成5.但是只有五个元素被改变为23,剩下的会内存丢失。

当我们析构的时候,会对同一个空间进行二次释放。

这时系统给的默认的拷贝构造函数和赋值函数会无法满足我们的使用,就需要自行书写。

1.3 小结

    运算符重载的函数名必须为关键字operator添加一个合法的运算符。在调用该函数时,将右操作数作为函数的实参。

    当用类的成员函数实现运算符的重载时,运算符的重载函数的参数(当为双目运算符时)为一个或(当为单目运算符时)没有。运算符的左操作数一定是对象,因为重载的运算符是该对象的成员函数,而右操作数是该函数的参数。

    单目运算符“++”和"–"存在前置和后置不同

    前置++:

    ​ 返回类型 类名::operator++() { }

    后置++:

    ​ 返回类型 类名::operator++(int) { }

    后置“++”中的参数int 仅仅作为区分使用,并无实际意义。

    少数的运算符不允许重载

重载运算符有以下的限制:

注意:一旦运算符重载之后,就不是运算符,它是一个函数。按照的是函数的结核性和语法结构法则进行。

1.4 运算符重载示例练习与分析

分为不同任务进行:

#include
using namespace std;


class Int
{
    int value;
public:
    Int(int x = 0) : value(x)
    {
        cout << "Int():" << this << endl;
    }
    Int(const Int& it) :value(it.value)
    {
        cout << "Copy Int:" << this << endl;
    }
    Int& operator = (const Int& it)
    {
        if (this != &it)
        {
            value = it.value;
        }
        cout << this << " = " << &it << endl;
        return *this;
    }
    ~Int()
    {
        cout << "Destroy Int :" << this << endl;
    }
};

int main()
{
    Int a(10), b(0), c(0);

    b = ++a;
    //b = a.operator++();
    //b = operator++(&a);

    c = a++;
    //c = a.operator++(0);      //带有一个参数
    //c = operator++(&a,0);

    return 0;
}

任务1:实现前置++与后置++函数

    前置++实现:

    //Int const *operator++(int *const this);
    Int &operator++()
    {
        value += 1;
        
        return *this;
    }
    //b = ++a;
    //b = a.operator++();
    //b = operator++(&a);
    
    

    过程分析:b = ++a。我们首先需要将a进行自加,然后将a的值赋 值给b。这中间的过程只需要将a的value值进行 +1,然后返回this 指针。中间没有任何临时对象生成,所以不需要以值类型返回,以引用返回即可。

    后置++的实现:

    Int operator++(int)
    {
        Int tmp = *this;
        ++*this;
        return tmp;
    }
    
    //c = a++;
    //c = a.operator++(int);
    //c = operator++(&c,int);
    

    c = a++。我们首先要将a赋值给c,然后再实现a的自增。中间需要先定义一个临时对象作为返回值返回给c,然后再实现a的value值增加。所以需要以值的类型返回,不可使用引用类型,防止栈帧空间销毁,地址析构返回随机值的情况。

    注意:此处++ * this的理解:

    星号* 和 ++ 的优先级同级this指针 和 * 结合是 a, ++*this也就是++a,调用了前置++,返回旧的对象。

任务2:实现+函数:

int main()
{
    Int a(10),b(0),c(0);
    
    c = a + b;		//1、对象和对象相加
    
    c = a + 10;		//2、对象和整型相加
    
    c = 10 + a;		//3、整型和对象相加
}

    对象和对象相加:

    Object &operator+(const Object& obj)
    {
        return Int(x->value + y->value);
    }
    

    报错,显示运算符的参数太多。因为以+运算符来说,参数只有两个,但是编译器隐藏了一个参数this指针,那么实际上这个函数有三个参数。

    那么,我们只能定义一个参数。

    对象和整型相加

    理解了上面错误的原因,这个也就迎刃而解了。

    Int operator+(const int x) const 
    {
        return Int(this->value + x);
    }
    

    整型和对象相加

    按照要求我们写出下面的函数:

    Int operator+(const int x, const Int &it)
    {
        return Int(x + it.value);
    }
    

    结果又显示报错:

    问题与之前相同,+只能又两个参数,而此时是三个参数。但是修改仅能修改成对象 + 整型的类型。

    如果转为全局变量,不可对私有成员进行操作。

    全局函数,那么直接对象和变量相加:

任务3:实现==和 != 运算符的重载

    实现==运算符的的重载

    bool operator==(const Int &it) const 
    {
    	return this->value == it.value;
    }
    
    

    实现 != 运算符的重载

    bool operator!=(const Int &it) const 
    {
        return !(*this == it);
    }
    

任务 4:实现 > 和 < 运算符的重载

    实现 > 运算符的重载

    bool operator> (const Int &it) const 
    {
        return this->value > it.value;
    }
    

    实现 < 运算符的重载

    bool operator< (const Int &it) const
    {
        return this->value < it.value;
    }
    

任务 5: 实现 >= 和 <= 运算符的重载

    实现 >= 运算符的重载

    bool operator >= (const Int &it) const 
    {
        return !(*this < it);
    }
    

    实现 <= 运算符的重载

bool operator <= (const Int &it)const 
{
    return !(*this > it);
}

问题6:内置类型和自定义类型

#include
using namespace std;

class Int
{
    int value;
public:
    Int(int x = 0) : value(x)
    {
        cout << "Int():" << this << endl;
    }
    Int(const Int& it) :value(it.value)
    {
        cout << "Copy Int:" << this << endl;
    }
    Int& operator = (const Int& it)
    {
        if (this != &it)
        {
            value = it.value;
        }
        cout << this << " = " << &it << endl;
        return *this;
    }
    ~Int()
    {
        cout << "Destroy Int :" << this << endl;
    }

    Int& operator++()
    {
        value += 1;

        return *this;
    }

    Int operator++(int)
    {
        Int tmp = *this;
        ++* this;
        return tmp;
    }

    Int operator+(const int x) const
    {
        return Int(this->value + x);
    }

    void Print()
    {
        cout <<"a的值为:"<value << endl;
    }
};

int main()
{
    int i = 0;
    i = i++ + 1;

    cout << "i的值为:" << i << endl;

    Int a = 0;
    a = a++ + 1;
    a.Print();

    return 0;
}

输出结果:

我们可以清除的看到,i的值(内置类型)为2,但是a的值(自定义类型)打印出来是1。

自我分析:

首先我们可以看出有一个后置++,先不用管,将i + 1赋值给i,此时,i的值为1。随后退出函数,我们的后置++启动,i自加。

注意:尽量使用前置++而减少使用后置++

一旦重载我们的所谓的++,+等运算符时,它们就不能理解为运算符了,它们是以函数的法则来进行的。

前置++:

int main()
{
    Int i = 0;
    Int n = 10;
    
    for(; i < n; ++i)
    {
        i.Print();
    }
    
    return 0;
}

输出结果:
可以发现前置++只是输出了a的值,并没有调用拷贝构造函数。

后置++:

int main()
{
    Int i = 0;
    Int n = 100;
    
    for(i; i < n; i++)
    {
        i.Print();
    }
}

输出结果:

后置++需要调用重载函数,需要进行对象的构造与返回以及对象的析构,开销大。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/717242.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号