C++从入门到精通——类的6个默认成员函数之赋值运算符重载

C/C++
76
0
0
2024-08-21
赋值运算符重载
  • 前言
  • 一、运算符重载
  • 定义
  • 实例
  • 注意要点
  • 函数重载与运算符重载的区别
  • 不同点
  • 相似点
  • 总结
  • 二、赋值运算符重载
  • 赋值运算符重载格式
  • 赋值运算符重载要点
  • 重载要点
  • 传值返回和传址返回要点
  • 三、前置++和后置++重载
  • 示例
  • 概念
  • 四、深挖operator
  • 友元函数
  • 模拟实现
  • 友元函数

前言

类的6个默认成员函数:如果一个类中什么成员都没有,简称为空类。

空类中真的什么都没有吗?并不是,任何类在什么都不写时,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显式实现,编译器会生成的成员函数称为默认成员函数。

class Date {};

在这里插入图片描述

一、运算符重载

定义

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型 operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:比如operator@
  • 重载操作符必须有一个类类型参数 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不 能改变其含义
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
  • .* :: sizeof ?: . 注意以上5个运算符不能重载。

实例

// 全局的operator==
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    //private:
    int _year;
    int _month;
    int _day;
};
// 这里会发现运算符重载成全局的就需要成员变量是公有的,那么问题来了,封装性如何保证?
// 这里其实可以用友元解决,或者干脆重载成成员函数。
bool operator==(const Date& d1, const Date& d2)
{
    return d1._year == d2._year
        && d1._month == d2._month
        && d1._day == d2._day;
}
void Test()
{
    Date d1(2018, 9, 26);
    Date d2(2018, 9, 27);
    cout << (d1 == d2) << endl;
}
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    // bool operator==(Date* this, const Date& d2)
    // 这里需要注意的是,左操作数是this,指向调用函数的对象
    bool operator==(const Date& d2)
    {
        return _year == d2._year;
        && _month == d2._month
            && _day == d2._day;
    }
private:
    int _year;
    int _month;
    int _day;
};

注意要点

同时存在全局运算符重载和类里的运算符重载,编译器会优先调用类里的运算符重载

bool operator==(const Date& d1, const Date& d2)
{
    return d1._year == d2._year
        && d1._month == d2._month
        && d1._day == d2._day;
}
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    // bool operator==(Date* this, const Date& d2)
    // 这里需要注意的是,左操作数是this,指向调用函数的对象
    bool operator==(const Date& d2)
    {
        return _year == d2._year;
        && _month == d2._month
            && _day == d2._day;
    }
private:
    int _year;
    int _month;
    int _day;
};

函数重载与运算符重载的区别

不同点

函数重载和运算符重载是C++中两个相关但不同的概念。

函数重载是指在同一个作用域中定义多个具有相同名称但参数列表不同的函数。这样做的目的是为了提供更灵活的函数调用方式,使得同一个函数名可以根据不同的参数类型或参数个数执行不同的操作。

运算符重载是指在C++中允许自定义类的成员函数或非成员函数来重新定义运算符的行为。通过运算符重载,可以为自定义的类创建与内置类型相似的运算符行为,使得自定义类的对象可以像内置类型一样进行运算。

总结:函数重载是针对函数进行的,通过改变参数列表来定义多个同名函数;而运算符重载是针对运算符进行的,通过重新定义运算符的行为来实现与内置类型相似的运算。

相似点

函数重载和运算符重载在某些方面是相似的,它们都是通过改变函数或运算符的行为来提供更灵活的功能。

  1. 名称相同:函数重载和运算符重载都是使用相同的名称来定义多个不同的行为。
  2. 参数列表变化:函数重载通过改变参数列表来定义多个同名函数,而运算符重载通过改变函数参数或者在类中定义成员函数重载运算符。
  3. 增加灵活性:无论是函数重载还是运算符重载,都可以根据需要定义不同的行为,使得代码更加灵活易用。
  4. 增加可读性:函数重载和运算符重载可以使代码更具可读性,因为可以根据函数名或运算符符号来推测其功能。

尽管函数重载和运算符重载在某些方面相似,但它们的目的和应用场景有所不同。函数重载用于定义同一功能的不同实现,而运算符重载用于为自定义类创建与内置类型相似的运算符行为。

总结
  • 函数重载:可以让函数名相同,参数不同的函数同时存在
  • 运算符重载:让自定义类型可以使用运算符,并且控制运算符的行为,增强可读性
  • 他们之间各论各的,没有关系
  • 多个同一运算符重载可以构成函数重载

二、赋值运算符重载

赋值运算符重载格式

  • 参数类型:const T&,传递引用可以提高传参效率
  • 返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回*this :要复合连续赋值的含义
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }

    Date(const Date& d)
    {
        _year = d._year;
        _month = d._month;
        _day = d._day;
    }

    Date& operator=(const Date& d)
    {
        if (this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }

        return *this;
    }
private:
    int _year;
    int _month;
    int _day;
};

赋值运算符重载要点

重载要点
  1. 赋值运算符只能重载成类的成员函数不能重载成全局函数
class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    int _year;
    int _month;
    int _day;
};
// 赋值运算符重载成全局函数,注意重载成全局函数时没有this指针了,需要给两个参数
Date& operator=(Date& left, const Date& right)
{
    if (&left != &right)
    {
        left._year = right._year;
        left._month = right._month;
        left._day = right._day;
    }
    return left;
}
// 编译失败:
// error C2801: “operator =”必须是非静态成员

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值运算符重载只能是类的成员函数。

在这里插入图片描述

  1. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。 注意:
  • 内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
  • 跟拷贝构造类似。
class Time
{
public:
    Time()
    {
        _hour = 1;
        _minute = 1;
        _second = 1;
    }
    Time& operator=(const Time& t)
    {
        if (this != &t)
        {
            _hour = t._hour;
            _minute = t._minute;
            _second = t._second;
        }
        return *this;
    }
private:
    int _hour;
    int _minute;
    int _second;
};
class Date
{
private:
    // 基本类型(内置类型)
    int _year = 1970;
    int _month = 1;
    int _day = 1;
    // 自定义类型
    Time _t;
};
int main()
{
    Date d1;
    Date d2;
    d1 = d2;
    return 0;
}

既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?验证一下试试?

// 这里会发现下面的程序会崩溃掉?这里就需要我们以后讲的深拷贝去解决。
typedef int DataType;
class Stack
{
public:
    Stack(size_t capacity = 10)
    {
        _array = (DataType*)malloc(capacity * sizeof(DataType));
        if (nullptr == _array)
        {
            perror("malloc申请空间失败");
            return;
        }
        _size = 0;
        _capacity = capacity;
    }
    void Push(const DataType& data)
    {
        // CheckCapacity();
        _array[_size] = data;
        _size++;
    }
    ~Stack()
    {
        if (_array)
        {
            free(_array);
            _array = nullptr;
            _capacity = 0;
            _size = 0;
        }
    }
private:
    DataType* _array;
    size_t _size;
    size_t _capacity;
};
int main()
{
    Stack s1;
    s1.Push(1);
    s1.Push(2);
    s1.Push(3);
    s1.Push(4);
    Stack s2;
    s2 = s1;
    return 0;
}

注意:如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

在这里插入图片描述

传值返回和传址返回要点

可以看到传值和传址在遇到不同问题时有不同的表现,如下,在运算符重载的问题下,传址调用比传值调用的效率更高,关于为什么要返回*this,见下面

在这里插入图片描述

正常的赋值表达式都是支持连续赋值的,如果我们使用Date作为返回值,可以见到效率太低了,相比指向返回Date的引用效率更高

d1 = d2 = d3
 Date& operator=(const Date& d)
    {
        if (this != &d)
        {
            _year = d._year;
            _month = d._month;
            _day = d._day;
        }

        return *this;
    }

根据上式,我们可以见到,我们是把d3的值赋给d2,而d2的地址存放在this指针里,我们需要对this指针进行解引用才能得到d2的地址,得到d2的地址后,我们便可以进行下一步的赋值

三、前置++和后置++重载

示例

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
    // 前置++:返回+1之后的结果
    // 注意:this指向的对象函数结束后不会销毁,故以引用方式返回提高效率
    Date& operator++()
    {
        _day += 1;
        return *this;
    }
    // 后置++:
    // 前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载
    // C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
        // 注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故需在实现时需要先将this保存一份,然后给this + 1
        //而temp是临时对象,因此只能以值的方式返回,不能返回引用
        Date operator++(int)
    {
        Date temp(*this);
        _day += 1;
        return temp;
    }
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d;
    Date d1(2022, 1, 13);
    d = d1++;    // d: 2022,1,13   d1:2022,1,14
    d = ++d1;    // d: 2022,1,15   d1:2022,1,15
    return 0;
}

概念

在C++中,++操作符可以被重载为前置++和后置++两种形式。

前置++表示在操作数之前增加1,并返回增加后的值。

后置++表示在操作数之后增加1,并返回增加前的值。

下面是前置++和后置++的重载函数:

class MyClass {
public:
    MyClass& operator++() {          // 前置++
        // 在此处增加1的逻辑
        return *this;
    }
    
    MyClass operator++(int) {       // 后置++
        MyClass temp(*this);        // 复制当前对象
        // 在此处增加1的逻辑
        return temp;                // 返回增加前的对象
    }
};

四、深挖operator

在C++中,输出流操作符 << 可以被重载用于自定义类型的对象,以便在流中输出该对象的内容。

友元函数

下面是重载流输出操作符的示例:

#include <iostream>

class MyClass {
public:
    int value;
    
    MyClass(int val) : value(val) {}
    
    friend std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
        os << obj.value;
        return os;
    }
};

int main() {
    MyClass obj(42);
    
    std::cout << obj << std::endl;  // 输出对象的内容
    
    return 0;
}

在上述示例中,我们定义了一个名为MyClass的类,该类具有一个整型成员变量value。我们将流输出操作符 << 声明为友元函数,并在函数中实现输出对象的内容。在主函数中,我们创建了一个名为objMyClass对象,并使用流输出操作符将其内容输出到标准输出流中。

输出结果将是 “42”。

注意,我们可以通过重载流输出操作符来控制输出对象的格式和内容。

模拟实现

class Date
{
public:
    Date(int year = 1900, int month = 1, int day = 1)
    {
        _year = year;
        _month = month;
        _day = day;
    }
   	void operator<<(ostream& out)
   	{
		out<<_year<<"年"<<_month<<"月"<<_day<<"日"<<endl;
	}
private:
    int _year;
    int _month;
    int _day;
};
int main()
{
    Date d1;
    d1.operator<<(cout);
    d1 << cout;
    return 0;
}

注意以下情况,因为在类里定义的函数,第一个对象永远是this指针,写成cout<<d1是错误的写法,即函数重载中,参数顺序和操作数顺序是一致的。

d1 << cout;

我们可以通过使用在类外面定义一个新函数,或者在类里使用上面的示例。下面定义的函数直接运行的话是会出错的,因为_year,_month,_day是私有的。

void operator<<(ostream& outconst Date& d)
{
	out<<d._year<<"年"<<d._month<<"月"<<d._day<<"日"<<endl;
}

以此类推,流输入也是同理

友元函数

友元函数是指在类的定义中,声明为友元的函数可以直接访问该类的私有成员和保护成员。友元函数可以是全局函数,也可以是其他类的成员函数。在C++中,使用关键字friend来声明友元函数。友元函数的定义通常在类的外部。

友元函数的特点是可以绕过类的访问权限,直接访问类的私有成员和保护成员。这在一些特殊情况下很有用,例如需要在其他类中对该类的私有成员进行操作或者需要在全局函数中操作该类的私有成员。

需要注意的是,友元函数并不是类的成员函数,因此在调用时不需要通过对象来访问,可以直接使用函数名进行调用。另外,由于友元函数不属于类的成员函数,因此不能使用this指针。

友元函数的具体用法可以分为两种情况:

  1. 全局函数作为友元函数:全局函数可以在类的外部定义,并通过friend关键字声明为友元函数。在全局函数的定义中,可以直接访问该类的私有成员和保护成员。
  2. 对象的成员函数作为友元函数:在另一个类的成员函数中通过友元关键字将该类的成员函数声明为友元函数。在友元函数的定义中,可以直接访问该类的私有成员和保护成员。

需要注意的是,友元函数并不是类的成员函数,因此不能直接访问类的成员变量和成员函数。如果需要访问类的成员变量和成员函数,可以通过对象来访问。

友元函数的使用应该谨慎,因为它破坏了封装性原则,导致代码可读性和可维护性降低。在设计类的时候,应该尽量避免使用友元函数,而是通过成员函数来操作类的私有成员和保护成员。只有在确实有必要的情况下,才考虑使用友元函数。