【C++】类与对象(三) 运算符重载 赋值重载 取地址及const取地址操作符重载(2)

简介: 【C++】类与对象(三) 运算符重载 赋值重载 取地址及const取地址操作符重载(1)

四、赋值运算符重载(默认成员函数)

1、引入

我们首先来看一个使用场景,我们想要把一个已经初始化的自定义类型的数据赋值给另一个已经初始化的自定义类型(不是对象初始化时赋值,对象初始化时赋值用的是拷贝构造)该怎么办?

看看下面的代码:

//赋值重载
#include<iostream>
using namespace std;
class Date
{
public:
  Date(int year = 1, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "年" << _month << "月" << _month << "日" << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  Date d2 = d1;//或者Date d2(d1)  会调用默认生成的拷贝构造,对象初始化时赋值用的是拷贝构造
  Date d3;
  d3 = d1;//我们没有实现Date类的运算符 = 的赋值重载,所以会调用默认生成的赋值重载
          //最后d3里面的数据与d1一样
}

2、特性

1. 用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

注意:内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值(=)运算符重载完成赋值。

实例代码:

如上面的代码

2. 赋值运算符重载格式:

参数类型:const T&,传递引用可以提高传参效率

返回值类型:T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值检测是否自己给自己赋值

返回*this :要符合连续赋值的含义

#include<iostream>
using namespace std;
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;
};
int main()
{
  Date d1(2023,2,12);
  Date d2;
  d2 = d1;
  return 0;
}

3. 赋值运算符只能重载成类的成员函数不能重载成全局函数

#include<iostream>
using namespace std;
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 =”必须是非静态成员



原因:赋值运算符如果不显式实现,编译器会生成一个默认的。

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

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

和拷贝构造函数一样我们继续思考:既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,对于内置类型还需要自己实现吗?

和拷贝构造一样,特性4也是我们写与不写复制重载函数的判断条件!

例如:

// 这里会发现下面的程序会崩溃掉,编译器生成的是浅拷贝,导致我们析构了两次空间,
//这里就需要我们以后讲的深拷贝去解决。
#include<iostream>
using namespace std;
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;
}

到这里我们就把六个默认成员函数中的第四个:复制重载给讲完了。

复制重载其实是运算符重载的一部分!

五、取地址及const取地址操作符重载

1、取地址操作符重载(默认成员函数)

我们还是先看代码再思考:

#include<iostream>
using namespace std;
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;
};
int main()
{
  Date d1;
  cout << &d1 << endl;
}

结果符合我们的预期,你可能觉得没有什么值得思考的点。

但是我们说过:对于自定义类型我们不能对他们像对内置类型那样使用运算符,但是我们对Date类的对象 d1 使用了取地址运算符&,而我们并没有实现&的运算符重载,结果我们却可以使用&,而且结果很对。为什么呢?

这是因为第五个默认成员函数:取地址操作符重载,即我们不写,编译器会帮我们自动生成,它的作用就是帮我们实现自定义类型对象的取地址。

取地址重载的手动实现

通常情况下我们一般自己不写此函数,让编译器自动生成。那假设我们自己实现此函数该怎么办呢?

实现代码如下:

//取地址重载函数
#include<iostream>
using namespace std;
class Date
{
public:
  Date(int year = 1900, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  //取地址重载
  Date* operator&()
  {
    return this;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main()
{
  Date d1;
  cout << &d1 << endl;
}

2、const取地址操作符重载(默认成员函数)

我们定义对象时一般都不会加const,那我们如果给对象加const会发生什么?

那我们再看一段代码:

#include<iostream>
using namespace std;
class Date
{
public:
  Date(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << "Print()" << endl;
    cout << "year:" << _year << endl;
    cout << "month:" << _month << endl;
    cout << "day:" << _day << endl << endl;
  }
private:
  int _year; // 年
  int _month; // 月
  int _day; // 日
};
int main()
{
  Date d1(2022, 1, 13);
  d1.Print();
  const Date d2(2022, 1, 13);
  d2.Print();
  return 0;
}

我们发现无法编译通过

为什么给对象加了const后我们调用函数就失败了呢?按照加const报错的常见原因,不难想应该是权限被放大了。

还记得this指针的类型是什么吗?答案是:* const类型。这里应该是Date * const

我们用const修饰的对象取地址后应该是什么类型?答案是:const *。这里应该是const Date*

两个类型不匹配,const修饰对象后内容不能被更改,所以我们的this指针要改变类型,在*前加一个const

但是呢 this指针是编译器传递的,我们无法加const ,这该怎么办呢?

这里C++编译器又做了特殊化处理我们需要加const在函数括号后面,才能对this指针进行修饰

正确代码:

#include<iostream>
using namespace std;
class Date
{
public:
  Date(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print() const
  {
    cout << "Print()const" << endl;
    cout << "year:" << _year << endl;
    cout << "month:" << _month << endl;
    cout << "day:" << _day << endl << endl;
  }
private:
  int _year; // 年
  int _month; // 月
  int _day; // 日
};
int main()
{
  Date d1(2022, 1, 13);
  d1.Print();
  const Date d2(2022, 1, 13);
  d2.Print();
  return 0;
}

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。

请思考下面的几个问题:

  1. const对象可以调用非const成员函数吗?
    答案:不可以,传递this指针时权限会放大
  2. 非const对象可以调用const成员函数吗?
    答案:可以,传递this指针时权限缩小
  3. const成员函数内可以调用其它的非const成员函数吗?
    答案:不可以,传递this指针时权限会放大
  4. 非const成员函数内可以调用其它的const成员函数吗?
    答案:可以,传递this指针时权限缩小

const取地址重载手动实现

同理在前面的代码中我们取const类型的地址时没有对&进行重载,但我们却可以使用,同样是因为编译器自动帮我们实现了const取地址重载

注意两个不太一样,两个函数构成函数重载!

取地址操作符重载

Date* operator&()             //对非 const 对象取地址

const取地址重载

const Date* operator&()const  //对 const 对象取地址

手动实现:

//const取地址重载函数
#include<iostream>
using namespace std;
class Date
{
public:
  Date(int year=0, int month=0, int day=0)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print() const
  {
    cout << "Print()const" << endl;
    cout << "year:" << _year << endl;
    cout << "month:" << _month << endl;
    cout << "day:" << _day << endl << endl;
  }
  const Date* operator&()const //返回值const Date * 是为了与this 指针保持一致
  {
    return this;
  }
private:
  int _year; // 年
  int _month; // 月
  int _day; // 日
};
int main()
{
  const Date d1;
  cout << &d1 << endl;
  return 0;
}

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容

相关文章
|
10天前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
39 4
|
12天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
35 4
|
1月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
27 4
|
1月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
21 1
|
1月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
23 4
|
1月前
|
存储 编译器 C++
【C++类和对象(下)】——我与C++的不解之缘(五)
【C++类和对象(下)】——我与C++的不解之缘(五)
|
1月前
|
编译器 C++
【C++类和对象(中)】—— 我与C++的不解之缘(四)
【C++类和对象(中)】—— 我与C++的不解之缘(四)
|
1月前
|
编译器 C语言 C++
C++入门3——类与对象2-2(类的6个默认成员函数)
C++入门3——类与对象2-2(类的6个默认成员函数)
24 3
|
1月前
|
C++
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
C++番外篇——对于继承中子类与父类对象同时定义其析构顺序的探究
53 1
|
1月前
|
编译器 C语言 C++
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
C++入门4——类与对象3-1(构造函数的类型转换和友元详解)
20 1