【C++】类的默认成员函数

简介: 【C++】类的默认成员函数

类的六个默认成员函数

当一个类中什么成员都没有时被称为空类。

空类:即任何类在什么都不写时,编译器会自动生成6个默认成员函数。

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

默认成员函数

初始化和清理

构造函数主要完成初始化工作

析构函数主要完成清理工作

拷贝赋值

拷贝构造是使用同类对象初始化创建对象

赋值重载主要是把一个对象赋值给另一个对象

取地址重载

主要是普通对象和const对象取地址,这俩个很少会自己实现

构造函数

构造函数的概念

class Data
{
public:
  void Init(int year = 2000, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  void Print()
  {
    cout << _year << "/" << _month << "/" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Data d1;
  d1.Init();
  d1.Print();
  Data d2;
  d2.Init(2024,6,8);
  d2.Print();
  return 0;
}

每次创建代码时,都需有初始化代码,使得工作量加大,引入构造函数可以进行初始化工作。

构造函数是一个特殊的成员函数,名字与类名相同,创建类类型对象时由编译器自动调用,以保证每个数据成员都有一个合适的初始值,并且在对象的整个生命周期内只使用一次。

构造函数的特性

构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。

其特征如下:

1.函数名与类名相同

2.无返回值(也不需要写void)

3.对象实例化时编译器会自动调用对应的构造函数

class Data
{
public:
  Data(int year = 2000, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Data d1();
  return 0;
}

4.构造函数可以重载

class Data
{
public:
  //带参数的构造函数
  Data(int year, int month, int day)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  //不带参数的构造函数
  Data()
  {
    _year = 2000;
    _month = 1;
    _day = 1;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  //调用带参数的构造函数
  Data d1(2024,6,8);
  //调用不带参数的构造函数
  Data d2;
  return 0;
}

构造函数支持重载的原因是:可以存在不同的初始化情况。

【注意】如果公共无参构造函数创建对象时,对象后面不用接括号,否则会变成函数声明。

5.如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户定义编译器将不再生成。

class Data
{
public:
  void Print()
  {
    cout << _year << "/" << _month << "/" << _day << endl;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Data d1;
  d1.Print();
  return 0;
}

编译器默认初始化成随机值:

这里需要说明一下:

C++里将类型分成俩类:

1.内置类型

内置类型属于基本类型,是语言本身定义的基础类型,例如int、char、double、指针等等

2.自定义类型

用struct、calss等定义的类型

编译器自动默认生成构造函数,内置类型不做处理,自定义类型会去调用其默认构造。(这里需要注意的是,不同的编译器有不同的处理方法,有些编译器也会处理,但是至少部分编译器个性化处理,不是所有的编译器都会处理)

总结:

1.一般情况下,有内置类型成员的,就需要自己写构造函数,不能让编译器自己生成

2.全部都是自定义类型成员,可以考虑让编译器自己生成。(一个经典的算法题:使用俩个栈实现队列就可以使用默认构造函数)

6.C++11中,在成员声明的时候可以给缺省值

class Data
{
public:
  void Print()
  {
    cout << _year << "/" << _month << "/" << _day << endl;
  }
private:
  int _year = 2000;
  int _month = 1;
  int _day = 1;
};

【注意】这里不是初始化

由于这里只能存在成员变量的声明,没有开空间,这里给的是默认的缺省值,给编译器生成默认构造函数用。

7.无参的构造函数和全缺省的构造函数都称为默认构造函数、并且默认构造函数只能有一个。

【注意】无参构造函数、全缺省默认构造函数、如果我们自己没写,编译器默认生成的构造函数都可以认为是默认构造函数。

class Data
{
public:
  //无参构造函数
  Data()
  {
    _year = 2000;
    _month = 1;
    _day = 1;
  }
  //全参构造函数
  Data(int year = 2000,int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Data d1;
  return 0;
}

不传参就可以调用的就是默认构造函数。

析构函数

析构函数的概念

析构函数是特殊的成员函数,析构函数与构造函数的功能相反,析构函数不是完全对对象本身的销毁,局部对象销毁工作时编译器完成的,而对象在销毁时会自动调用析构函数完成对象中资源的清理工作。

析构函数的特性

1.析构函数名实在类名前面加上字符~

2.无参数无返回值

class Data
{
public:
  Data(int year = 2000, int month = 1, int day = 1)
  {
    this->_year = year;
    this->_month = month;
    this->_day = day;
  }
  ~Data()
  {
    _year = 0;
    _month = 0;
    _day = 0;
  }
private:
  int _year;
  int _month;
  int _day;
};

3.一个类只能有一个析构函数,若未显式定义,系统会自动生成默认的析构函数。【注意】析构函数不能重载。

4。对象声明周期结束时,C++编译器系统自动调用析构函数。

#include<iostream>
using namespace std;
class Data
{
public:
  Data(int year = 2000, int month = 1, int day = 1)
  {
    cout << "Data" << endl;
    this->_year = year;
    this->_month = month;
    this->_day = day;
  }
  ~Data()
  {
    cout << "~Data" << endl;
    _year = 0;
    _month = 0;
    _day = 0;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Data d1;
  return 0;
}

5.系统自动生成的默认构造函数:

(1).内置类型成员不做处理。

(2).自定义类型会去调用他的析构函数。

下面这段代码是本人实现的:

class Stack
{
public:
  Stack(int capacity = 4)
  {
    _arr = (int*)malloc(sizeof(int) * capacity);
    if (_arr == nullptr)
    {
      perror("malloc fail");
      return;
    }
    _capacity = capacity;
    _size = 1;
  }
  ~Stack()
  {
    free(_arr);
    _arr = nullptr;
    _size = 1;
    _capacity = 0;
  }
  void Push(int x)
  {
    if (this->_capacity == this->_size)
    {
      int* arr = (int*)realloc(this->_arr, sizeof(int) * this->_capacity * 2);
      if (arr == nullptr)
      {
        perror("realloc fail");
        return;
      }
      this->_arr = arr;
      this->_capacity *= 2;
    }
    this->_arr[this->_size - 1] = x;
    this->_size++;
  }
  void Pop()
  {
    this->_arr[this->_size - 1] = 0;
    this->_size--;
  }
private:
  int* _arr;
  int _size;
  int _capacity;
};
int main(void)
{
  Stack s1;
  s1.Push(1);
  s1.Push(2);
  s1.Push(3);
  s1.Push(4);
  s1.Pop();
  return 0;
}

这里可以发现手动实现的析构函数可以根据自己的要求满足实现。

class Stack
{
public:
  Stack(int capacity = 4)
  {
    _arr = (int*)malloc(sizeof(int) * capacity);
    if (_arr == nullptr)
    {
      perror("malloc fail");
      return;
    }
    _capacity = capacity;
    _size = 1;
  }
  void Push(int x)
  {
    if (this->_capacity == this->_size)
    {
      int* arr = (int*)realloc(this->_arr, sizeof(int) * this->_capacity * 2);
      if (arr == nullptr)
      {
        perror("realloc fail");
        return;
      }
      this->_arr = arr;
      this->_capacity *= 2;
    }
    this->_arr[this->_size - 1] = x;
    this->_size++;
  }
  void Pop()
  {
    this->_arr[this->_size - 1] = 0;
    this->_size--;
  }
private:
  int* _arr;
  int _size;
  int _capacity;
};
int main(void)
{
  Stack s1;
  s1.Push(1);
  s1.Push(2);
  s1.Push(3);
  s1.Push(4);
  s1.Pop();
  return 0;
}

所以,一般情况下如果没有动态内存申请,析构函数可以不写,例如:Data类;但是如果有动态内存申请,就需要显式写析构函数释放资源,否则会造成内存泄漏。例如:栈的实现。

构造函数与析构函数的调用顺序

  • 类的析构函数调用一般按照构造函数调用的相反顺序调用,但是需要注意static对象的存在,因为static改变了对象的生存作用域,需要等待程序结束后才可析构释放对象。
  • 全局对象先于局部对象进行构造。
  • 局部对象按照顺序进行构造,无论是否为static对象。
  • static修饰的对象会在局部对象析构后进行析构。

拷贝构造

拷贝构造的概念

创建对象时,需要创建一个与已存在对象一模一样的新对象,就需要用到拷贝构造。

拷贝构造:只有单个形参,该形参是对本类型对象的引用(一般常用const修饰),在用已存在的类型对象创建新对象时由编译器自动调用。

拷贝构造的特性

拷贝构造函数也是特殊的成员函数,其特征如下:

1.拷贝构造函数时构造函数的一个重载形式,所有书写格式与构造函数类似,但是参数类型不同。

2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器之间报错,因为会引发无穷递归调用。

观察下面代码:

class Data
{
public:
  Data(int year = 2000, int month = 1, int day = 1)
  {
    cout << "Data" << endl;
    this->_year = year;
    this->_month = month;
    this->_day = day;
  }
  ~Data()
  {
    cout << "~Data" << endl;
    _year = 0;
    _month = 0;
    _day = 0;
  }
  //拷贝构造函数
  //传值
  Data(Data d) 
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Data d1;
  Data d2(d1);
  return 0;
}

首先编译器会自动报错,原因是:

当我们以传值方式传参时,需要先建立一个临时拷贝,而在建立临时拷贝也需要传参,再以传值方式传参时,又需要建立一个临时拷贝…

这种无穷递归,编译器会强制检查,解决的办法有俩种:

1.使用指针(内置类型)

2.使用引用(推荐)

【注意】使用引用时需要注意赋值方向(d._year = _year);就是错误代码。

由于传引用的值不用改变,所有可以使用const缩小权限。

正确代码如下:

class Data
{
public:
  Data(int year = 2000, int month = 1, int day = 1)
  {
    this->_year = year;
    this->_month = month;
    this->_day = day;
  }
  ~Data()
  {
    _year = 0;
    _month = 0;
    _day = 0;
  }
  //拷贝构造函数
  //传值
  Data(const Data& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Data d1;
  Data d2(d1);
  return 0;
}

3.如果没有显式定义,编译器会生成默认的拷贝构造函数,默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫浅拷贝,或者值拷贝。

(1)内置类型成员完成值拷贝、浅拷贝。

(2)自定义类型成员会调用他的拷贝构造。

观察代码:

class Stack
{
public:
  Stack(int capacity = 4)
  {
    _arr = (int*)malloc(sizeof(int) * capacity);
    if (_arr == nullptr)
    {
      perror("malloc fail");
      return;
    }
    _capacity = capacity;
    _size = 1;
  }
  void Push(int x)
  {
    if (this->_capacity == this->_size)
    {
      int* arr = (int*)realloc(this->_arr, sizeof(int) * this->_capacity * 2);
      if (arr == nullptr)
      {
        perror("realloc fail");
        return;
      }
      this->_arr = arr;
      this->_capacity *= 2;
    }
    this->_arr[this->_size - 1] = x;
    this->_size++;
  }
  void Pop()
  {
    this->_arr[this->_size - 1] = 0;
    this->_size--;
  }
private:
  int* _arr;
  int _size;
  int _capacity;
};
int main(void)
{
  Stack s1;
  s1.Push(1);
  s1.Push(2);
  s1.Push(3);
  s1.Push(4);
  s1.Pop();
  Stack s2(s1);
  return 0;
}

以栈为例,栈不可以使用默认拷贝构造,栈默认生成的拷贝构造会将俩个指针指向同一个栈,在析构时,后面拷贝的指针会先析构,而前面被拷贝的指针会后析构,一个堆区被析构俩次,编译器会报错,同时如果修改其中一个值,会影响另一个。

栈拷贝构造的正确代码是:

Stack(const Stack& s)
  {
    _arr = (int*)malloc(sizeof(int) * s._capacity);
    if (_arr == nullptr)
    {
      perror("malloc fail");
      return;
    }
    memcpy(_arr, s._arr, sizeof(int) * s._size);
    _size = s._size;
    _capacity = s._capacity;
  }

需要动态开辟的都需要自己实现深拷贝,而像日期类可以不写拷贝构造,默认生成的拷贝构造就可以用。

赋值运算符重载

运算符重载

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

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

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

  • 内置类型可以通过编译器计算,而自定义类型也可以向内置类型一样,需要通过运算符重载,进行加、减、比较等。

以Date日期类举例:是否使用重载运算符,是观察这个运算符对这个类是否有意义,例如:日期相减可以计算相差天数,而日期相加却没有意义。

下面演示日期比较大小:

class Date
{
public:
  //Date构造
  Date(int year = 2000,int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  //Date默认析构,默认拷贝
  //
//private:
  int _year;
  int _month;
  int _day;
};
bool operator>(const Date& d1, const Date& d2)
{
  if (d1._year > d2._year)
  {
    return true;
  }
  else if (d1._year == d2._year && d1._month > d2._month)
  {
    return true;
  }
  else if (d1._year == d2._year && d1._month == d2._month && d1._day > d2._day)
  {
    return true;
  }
  return false;
}
int main(void)
{
  Date d1(2024, 6, 10);
  Date d2(2024, 5, 20);
  if (d1 > d2)
  {
    cout << "d1 > d2" << endl;
  }
  else
  {
    cout << "d1 < d2" << endl;
  }
  return 0;
}

这里需要注意的是编译器在编译期间将d1>d2转换成operator>(d1,d2),所以将d1>d2写成operator>(d1,d1)是相同的,第二种方式相当于调用函数。

int main(void)
{
  Date d1(2024, 6, 10);
  Date d2(2024, 5, 20);
  //第一种方式
  if (d1 > d2)
  {
    cout << "d1 > d2" << endl;
  }
  else
  {
    cout << "d1 < d2" << endl;
  }
  //第二种方式
  if (operator>(d1,d2))
  {
    cout << "d1 > d2" << endl;
  }
  else
  {
    cout << "d1 < d2" << endl;
  }
  return 0;
}

在反汇编的角度观察:

本质上二者都是相同的,编译器在编译期间将第一种方式转换为第二种方式,然后进行call

但是观察刚才的代码,如果在类中将成员变量改为私有,那么编译器会报错,在类外的函数是无法访问私有(private)和保护(protected),解决的办法有俩种:1.使用友元(建议能不用就不用,友元会破坏封装);2放在类内,将其视为成员函数。

下面演示将运算符重载视为成员函数:

class Date
{
public:
  //Date构造
  Date(int year = 2000, int month = 1, int day = 1)
  {
    _year = year;
    _month = month;
    _day = day;
  }
  //Date默认析构,默认拷贝
  //比较大小的运算符重载
  bool operator>(const Date& d)
  {
    if (_year > d._year)
    {
      return true;
    }
    else if (_year == d._year && _month > d._month)
    {
      return true;
    }
    else if (_year == d._year && _month == d._month && _day > d._day)
    {
      return true;
    }
    return false;
  }
private:
  int _year;
  int _month;
  int _day;
};
int main(void)
{
  Date d1(2024, 6, 10);
  Date d2(2024, 5, 20);
  //第一种方式
  if (d1 > d2)
  {
    cout << "d1 > d2" << endl;
  }
  else
  {
    cout << "d1 < d2" << endl;
  }
  //第二种方式
  if (d1.operator>(d2))
  {
    cout << "d1 > d2" << endl;
  }
  else
  {
    cout << "d1 < d2" << endl;
  }
  return 0;
}

将运算符重载函数放入类内时,参数只有一个,另外一个时隐藏的this。同时d1>d2准换为函数为d1.operator(d2),和使用成员函数时相同。

值得注意的是,写出成员函数更加方便,也更加容易理解。

【注意】

1.不能通过连接其他符号来创建新的操作符,例如:operator@

2.重载操作符必须有一个类类型参数

3.用于内置类型的运算符,其含义不能改变,例如:内置的整数+,不能改变其含义

4.作为类成员函数重载时,其形参看起来比操作数少1,因为成员函数的第一个参数为隐藏的this

5. “.*” “::” “sizeof” “?:” "."注意以上五个运算符不能重载。

6.操作符时几个操作数,重载函数就有几个参数。

赋值运算符重载

如何在类中实现一个赋值运算符重载?

//实现赋值运算符重载
  void operator=(const Date& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }

在这里可以发现赋值运算符重载与拷贝构造类似,但是值得注意的是,二者有本质上的区别,我们需要理一理二者的概念:

拷贝构造函数:函数用一个已经存在的对象初始化另一个对象

运算符重载函数:已经存在的俩个对象之间的复制拷贝

通过理解二者的概念,那么通过哪种方式调用呢?

int main(void)
{
  //调用拷贝构造函数
  Date d1(2024, 6, 10);
  Date d2 = d1;
  //调用运算符重载函数
  Date d3(2024, 5, 20);
  Date d4(2003, 2, 3);
  d3 = d4;
  return 0;
}

初步认识赋值运算符函数,那么赋值运算符是如何使用的呢?

int i , j , k;
  i = j = k = 10;

赋值运算符可以进行连续赋值:

所以我们可以将代码进一步更改:

//实现赋值运算符重载
  Date operator=(const Date& d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
    return *this;
  }

this指针不可以再形参出现,但是可以在成员函数内部出现。

但是在使用Date作为返回值时,每次赋值都会调用拷贝构造,所以建议直接使用引用返回。

代码如下:

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

因为传值返回,值在函数结束后会销毁,返回的是值的拷贝,而传引用返回,返回的是别名,是*this这个指针在未被销毁时所在地址的值的别名。

this是参数,也是进行传参的,this的生命周期实在函数调用结束后销毁,相当于在函数建立时先进行push,压栈,然后函数在结束后销毁,this也会被pop,虽然this这个指针被销毁了,但是this这个指针所指向的值没有被销毁。

会不会存在一种情况,是自己给自己赋值:

d1 = d2;

所以为了防止这种情况,需要对代码进行更改:

//实现赋值运算符重载
  Date& operator=(const Date& d)
  {
    if (this != &d)
    {
      _year = d._year;
      _month = d._month;
      _day = d._day;
    }
    return *this;
  }

总结:

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

  • 参数类型:const T& ,传递引用可以提高传参效率
  • 返回值类型:T& ,返回引用可以提高返回的效率,有返回值的目的是为了支持连续赋值
  • 检查是否自己给自己赋值
  • 返回*this,要符合连续赋值的含义

2.赋值运算符只能重载成类的成员函数不能重载成全局函数,因为类中存在默认赋值运算符重载,会与之冲突。

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

【注意】默认生成赋值重载跟拷贝构造行为一样:内置类型成员仅值拷贝,浅拷贝;自定义类型成员会去调用其赋值重载。

例如:Date日期类、Myqueue不需要自己实现赋值重载,stack需要自己实现,因为默认生成的是浅拷贝。

前置++与后置++重载

//前置++
  Date& operator++()
  {
    _day = _day + 1;
    return *this;
  }

【注意】使用前置++时,需要注意前置++时返回++以后的对象。

//后置++
  Date operator++(int)
  {
    Date tmp(*this);
    _day = _day + 1;
    return tmp;
  }

前置++与后置++都是一元运算符为了让让前置++与后置++形参正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时,该参数不用传递,编译器会自动传递。

【注意】使用后置++时,需要注意后置++返回++以前的对象,所以需要tmp保存一份,tmp是临时变量,返回时必须需要进行拷贝构造。

输入输出流重载

int i = 10;
  printf("%d", i);

我们知道printf可以打印内置类型,但是不可以打印自定义类型。

如果不使用调用函数的方式,是如何实现打印自定义函数呢?

在C++中支持使用流插入流提取运算符重载的方式进行打印自定义类型。

在C++reference中介绍cout是ostream类型的对象,cin是istream类型的对象。

在C++中,内置类型是初创C++时使用流插入和流提取定义的成员函数。

运算符重载是为了让自定义类型支持运算符:

1.可以直接支持内置类型的是库里实现的

2.可以直接支持自定义识别类型是因为函数重载

实现流插入自定义:

//自定义流插入
void Date::operator<<(ostream& out)
{
  out << _year << "年" << _month << "月" << _day << "日" << endl;
}
void TestDate2() 
{
  int i = 10;
  cout << i << endl;
  Date d1(2024,6,12);
  d1 << cout;
}

从我们初步实现流插入可以发现我们所实现的运算符不符合运算重载。

cout << d1;

由于cout是终端、控制台,要实现只能在库里实现,流插入不能写成成员函数,因为Date类对象默认占用第一个参数,就是主操作数,这种写法写出来只能是d1<<cout,但是这不符合使用习惯。

而全局函数不会占用第一个参数,没有默认参数,但是访问不了私有问题,解决办法有俩个:

1.写成公有的成员函数

class Date
{
public:
  //公有的成员函数
  int GetYear()
  {
    return _year;
  }
  int GetMonth()
  {
    return _month;
  }
  int GetDay()
  {
    return _day;
  }
private:
  int _year;
  int _month;
  int _day;
};
//全局的流插入操作符
void operator<<(ostream& out, Date& d)
{
  out << d.GetYear() << "年" << d.GetMonth() << "月" << d.GetDay() << "日" << endl;
}

2.使用友元函数(后续讲解)

class Date
{
public:
  //友元函数声明
  friend void operator<<(ostream& out, Date& d);
private:
  int _year;
  int _month;
  int _day;
};
//全局的流插入操作符
void operator<<(ostream& out, Date& d)
{
  out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}

这里使用友元函数比调用公有的函数方便一些。

在实际使用流插入的时候可以进行连续插入

cout << d1 << d2 << d3;

则需要更换返回值来进行连续插入

//全局的流插入操作符
ostream& operator<<(ostream& out, Date& d)
{
  out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
  return out;
}

同时ostream对象可以使用const修饰,因为其在流入控制台过程中没有被修改。

最后整合一下流提取运算符重载函数:

class Date
{
public:
  //友元函数声明
  friend ostream& operator<<(ostream& out, const Date& d);
private:
  int _year;
  int _month;
  int _day;
};
//全局的流插入操作符
ostream& operator<<(ostream& out, const Date& d)
{
  out << d._year << "年" << d._month << "月" << d._day << "日" ;
  return out;
}

通过对流插入的了解,我们可以试着写出流提取运算符重载:

class Date
{
public:
  //友元函数声明
  friend istream& operator>>(istream& in,  Date& d);
private:
  int _year;
  int _month;
  int _day;
};
//全局的流提取操作符
istream& operator>>(istream& in, Date& d)
{
  in >> d._year >> d._month >> d._day;
  return in;
}

值得注意的是:流提取是的参数是不可以加const的。istream不使用const修饰的原因是:当我们从输入流中读取数据时,流的状态会发生变化,例如读取位置会向前移动;对象不使用const修饰的原因是:输入流是可变的、输入流应该允许被修改。

总的来讲,流是支持任何形式的输入与输出。

const修饰成员

Date d1(2022,2,2);
  d1.print();
  const Date.d2(2011,10,10);
  d2.print();

假设d1与d2俩个对象都调用print成员函数时

void print();

d1可以调用成功,而d2却调用不成功。

d1在调用print成员函数时,Date的权限平移;而d2在调用print成员函数时,Date的权限被放大,由起初的Date的不可被修改,变成可修改时不被允许的。

那么如何可以更改呢?

void print(const Date* this);

如果可以这样写就可以实现d1调用成员函数,但是this指针是不能作为形参出现的。

所以在C++中对此做了新的说明:

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

void print() const;

const修饰的是*this,如果成员函数加上const,普通与const对象都可以调用,但并不是所有的成员函数都可以加上const修饰,要修改成员变量的成员函数不可以加,只要成员函数内部不修改成员变量,都应该加const,这样const对象和普通对象都可以调用。

实现完整的日期系统

  • Date.h
#pragma once
#include<iostream>
#include<assert.h>
using namespace std;
class Date
{
public:
  //全缺省的构造函数
  Date(int year = 2000, int month = 1, int day = 1);
  //析构函数
  ~Date();
  //拷贝构造函数
  Date(const Date& d);
  //赋值运算符重载
  Date& operator=(const Date& d);
  //打印
  void Print() const
  {
    cout << _year << "/" << _month << "/" << _day << endl;
  }
  //获取某年某月的天数
  int GetMonthDay(int year, int month) const;
  //日期+=天数
  Date& operator+=(int day);
  //日期+天数
  Date operator+(const int day) const;
  //日期-=天数
  Date& operator-=(int day);
  //日期-天数
  Date operator-(const int day) const;
  //前置++
  Date& operator++();
  //后置++
  Date operator++(int);
  //前置--
  Date operator--();
  //后置--
  Date operator--(int);
  //>运算符重载
  bool operator>(const Date& d) const;
  //==运算符重载
  bool operator==(const Date& d) const;
  //>=运算符重载
  bool operator>=(const Date& d) const;
  //<运算符重载
  bool operator<(const Date& d) const;
  //<=运算符重载
  bool operator<=(const Date& d) const;
  //日期-日期
  int operator-(Date& d) const;
  //友元函数声明
  friend ostream& operator<<(ostream& out, const Date& d);
  //全局的流提取操作符
  friend istream& operator>>(istream& in,  Date& d);
private:
  int _year;
  int _month;
  int _day;
};
//全局的流插入操作符
ostream& operator<<(ostream& out, const Date& d);
//全局的流提取操作符
istream& operator>>(istream& in, Date& d);
  • Date.cpp
#include"Date.h"
//全缺省的构造函数
Date::Date(int year, int month, int day)
{
  if (month > 0 && month < 13
    && day > 0 && day <= GetMonthDay(year, month))
  {
    _year = year;
    _month = month;
    _day = day;
  }
  else
  {
    cout << "日期输入故障" << endl;
    assert(false);
  }
}
//析构函数
Date::~Date()
{
  _year = 0;
  _month = 0;
  _day = 0;
}
//拷贝构造函数
Date::Date(const Date& d)
{
  _year = d._year;
  _month = d._month;
  _day = d._day;
}
//赋值运算符重载
Date& Date::operator=(const Date& d)
{
  if(this != &d)
  {
    _year = d._year;
    _month = d._month;
    _day = d._day;
  }
  return *this;
}
//获取某年某月的天数
int Date::GetMonthDay(int year, int month) const
{
  int MonthArr[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 };
  if (month == 2 && ((year % 4 == 0) && (year % 100 != 0)) || (year % 400 == 0))
  {
    return 29;
  }
  return MonthArr[month];
}
//日期+=天数
Date& Date::operator+=(int day)
{
  if (day <= 0)
  {
    day = -day;
    *this -= day;
    day = 0;
  }
  _day += day;
  while (_day > GetMonthDay(_year, _month))
  {
    _day -= GetMonthDay(_year, _month);
    ++_month;
    if (_month > 12)
    {
      ++_year;
      _month = 1;
    }
  }
  return *this;
}
//日期+天数
Date Date::operator+(int day) const
{
  Date tmp = *this;
  tmp += day;
  return tmp;
}
//日期-=天数
Date& Date::operator-=(int day)
{
  if (day < 0)
  {
    day = -day;
    *this += day;
    day = 0;
  }
  _day -= day;
  while (_day <= 0)
  {
    --_month;
    if (_month < 1)
    {
      --_year;
      _month = 12;
    }
    _day += GetMonthDay(_year, _month);
  }
  return *this;
}
//日期-天数
Date Date::operator-(int day) const
{
  Date tmp = *this;
  tmp -= day;
  return tmp;
}
//前置++
Date& Date::operator++()
{
  *this += 1;
  return *this;
}
//后置++
Date Date::operator++(int)
{
  Date tmp = *this;
  *this += 1;
  return tmp;
}
//前置--
Date Date::operator--()
{
  *this -= 1;
  return *this;
}
//后置--
Date Date::operator--(int)
{
  Date tmp = *this;
  *this -= 1;
  return tmp;
}
//>运算符重载 
bool Date::operator>(const Date& d) const
{
  if (_year > d._year)
  {
    return true;
  }
  else if (_year == d._year && _month > d._month)
  {
    return true;
  }
  else if (_year == d._year && _month == d._month && _day > d._day)
  {
    return true;
  }
  return false;
}
//==运算符重载
bool Date::operator==(const Date& d) const
{
  if (_year == d._year && _month == d._month && _day == d._day)
  {
    return true;
  }
  return false;
}
//>=运算符重载
bool Date::operator>=(const Date& d) const
{
  if (*this > d || *this == d)
  {
    return true;
  }
  return false;
}
//<运算符重载
bool Date::operator<(const Date& d) const 
{
  if (!(*this >= d))
  {
    return true;
  }
  return false;
}
//<=运算符重载
bool Date::operator<=(const Date& d) const
{
  if (!(*this > d))
  {
    return true;
  }
  return false;
}
//日期-日期
int Date::operator-(Date& d) const
{
  Date max = *this;
  Date min = d;
  int flag = 1;
  if (*this < d)
  {
    max = d;
    min = 1;
    flag = -1;
  }
  int n = 0;
  while (min < max)
  {
    ++min;
    ++n;
  }
  return n * flag;
}
//全局的流插入操作符
ostream& operator<<(ostream& out, const Date& d)
{
  out << d._year << "年" << d._month << "月" << d._day << "日" ;
  return out;
}
//全局的流提取操作符
istream& operator>>(istream& in, Date& d)
{
  int year = d._year;
  int month = d._month;
  int day = d._day;
  if (month > 0 && month < 13
    && day > 0 && day <= d.GetMonthDay(year, month))
  {
    in >> year >> month >> day;
  }
  else
  {
    cout << "日期输入故障" << endl;
    assert(false);
  }
  return in;
}

取地址操作符重载

class Date
{
public:
  Date* operator&()
  {
    return this;
  }
private:
  int _year;
  int _month;
  int _day;
};

const取地址操作符重载

class Date
{
public:
  const Date* operator&() const
  {
    return this;
  }
private:
  int _year;
  int _month;
  int _day;
};

取地址与const取地址操作符一般不需要重载,使用编译器默认取地址重载即可,只有存在默认情况才才需要重载。


相关文章
|
1月前
|
存储 编译器 C语言
【c++丨STL】string类的使用
本文介绍了C++中`string`类的基本概念及其主要接口。`string`类在C++标准库中扮演着重要角色,它提供了比C语言中字符串处理函数更丰富、安全和便捷的功能。文章详细讲解了`string`类的构造函数、赋值运算符、容量管理接口、元素访问及遍历方法、字符串修改操作、字符串运算接口、常量成员和非成员函数等内容。通过实例演示了如何使用这些接口进行字符串的创建、修改、查找和比较等操作,帮助读者更好地理解和掌握`string`类的应用。
51 2
|
1月前
|
存储 编译器 C++
【c++】类和对象(下)(取地址运算符重载、深究构造函数、类型转换、static修饰成员、友元、内部类、匿名对象)
本文介绍了C++中类和对象的高级特性,包括取地址运算符重载、构造函数的初始化列表、类型转换、static修饰成员、友元、内部类及匿名对象等内容。文章详细解释了每个概念的使用方法和注意事项,帮助读者深入了解C++面向对象编程的核心机制。
106 5
|
1月前
|
存储 编译器 C++
【c++】类和对象(中)(构造函数、析构函数、拷贝构造、赋值重载)
本文深入探讨了C++类的默认成员函数,包括构造函数、析构函数、拷贝构造函数和赋值重载。构造函数用于对象的初始化,析构函数用于对象销毁时的资源清理,拷贝构造函数用于对象的拷贝,赋值重载用于已存在对象的赋值。文章详细介绍了每个函数的特点、使用方法及注意事项,并提供了代码示例。这些默认成员函数确保了资源的正确管理和对象状态的维护。
97 4
|
1月前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
114 4
|
2月前
|
程序员 C++ 容器
在 C++中,realloc 函数返回 NULL 时,需要手动释放原来的内存吗?
在 C++ 中,当 realloc 函数返回 NULL 时,表示内存重新分配失败,但原内存块仍然有效,因此需要手动释放原来的内存,以避免内存泄漏。
|
2月前
|
存储 编译器 对象存储
【C++打怪之路Lv5】-- 类和对象(下)
【C++打怪之路Lv5】-- 类和对象(下)
32 4
|
2月前
|
编译器 C语言 C++
【C++打怪之路Lv4】-- 类和对象(中)
【C++打怪之路Lv4】-- 类和对象(中)
32 4
|
2月前
|
存储 安全 C++
【C++打怪之路Lv8】-- string类
【C++打怪之路Lv8】-- string类
29 1
|
2月前
|
存储 编译器 C语言
【C++打怪之路Lv3】-- 类和对象(上)
【C++打怪之路Lv3】-- 类和对象(上)
21 0
|
2月前
|
存储 编译器 C语言
深入计算机语言之C++:类与对象(上)
深入计算机语言之C++:类与对象(上)