【C++】类和对象(中)

一、类的6个默认成员函数

如果一个类中什么成员都没有,简称为空类。但是空类中并不是真的什么都没有,任何类在什么都不写的时候,编译器会自动生成以下 6 个默认成员函数。
默认成员函数用户没有显式实现,编译器会生成的成员函数称为默认成员函数。
class Date{// 空类
};

二、构造函数

1、概念

class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << "-" << _month << "-" << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date d1;d1.Init(2023, 1, 26);d1.Print();Date d2;d2.Init(2023, 8, 9);d2.Print();return 0;
}
对于 Date 类,可以通过 Init 公有方法给对象设置日期,但如果每次创建对象时都调用该方法设置信息,未免有点麻烦,或者有时候忘记初始化,那能否在对象创建时,就将信息设置进去呢?
构造函数是一个特殊的成员函数 名字与类名相同 ,创建类类型对象时由编译器自动调用,以保证每个数据成员都有 一个合适的初始值,并且在对象整个生命周期内 只调用一次

2、特性

构造函数是特殊的成员函数,不能以普通函数的定义和调用规则去理解,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是 初始化对象

特征:

  1. 函数名类名相同
  2. 无返回值不用写 void
  3. 对象实例化时编译器自动调用对应的构造函数。
  4. 构造函数可以重载
class Date
{
public:// 1、无参构造函数Date(){_year = 1;_month = 1;_day = 1;}// 写法相同/*Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}*/// 2、带参构造函数Date(int year, int month, int day){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};void TestDate()
{Date d1; // 调用无参构造函数Date d2(2023, 9, 12); // 调用带参的构造函数Date d3(); // 声明了d3函数,该函数无参,返回一个日期类型的对象// warning C4930: “Date d3(void)”: 未调用原型函数(是否是有意用变量定义呢?)
}

注意:如果通过无参构造函数创建对象时对象后面不用跟括号,否则就成了函数声明。


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

class Date
{
public:
/*// 如果用户显式定义了构造函数,编译器将不再生成Date(int year, int month, int day){_year = year;_month = month;_day = day;}
*/void Print(){cout << _year << "-" << _month << "-" << _day << endl;}private:int _year;int _month;int _day;
};int main()
{ Date d1;// 无参构造函数,放开后报错:error C2512: “Date”: 没有合适的默认构造函数可用return 0;
}

将 Date 类中构造函数屏蔽后,代码可以通过编译,因为编译器生成了一个无参的默认构造函数。

将 Date 类中构造函数放开,代码编译失败,因为一旦显式定义任何构造函数,编译器将不再生成。


6、关于编译器生成的默认成员函数,在不实现构造函数的情况下,编译器会生成默认的构造函数。但是看起来默认构造函数又没什么用?d 对象调用了编译器生成的默认构造函数,但是 d 对象 _year / _month / _day,依旧是随机值。也就说在这里编译器生成的默认构造函数并没有什么用?

C++ 把类型分成内置类型(基本类型)和自定义类型
  • 内置类型就是语言提供的数据类型,如:int / double / char / 指针等。
  • 自定义类型就是我们使用 class / struct / union 等自己定义的类型。

从下面的代码中可以发现编译器生成默认的构造函数会对自定类型成员 _t 调用的它的默认成员函数。也就是说,默认生成的构造函数对内置类型成员不作处理,对自定义类型成员去调用它的默认构造函数这个设计是 C++ 早期设计的一个缺陷,本来应该内置类型也一并处理。

  1. 对于类中的内置类型成员 —> 不处理(为随机值,除非声明时给了缺省值 - C++11)
  2. 对于类中的自定义类型成员 —> 自动调用它的默认构造函数(不要参数就可以调用的,比如 无参构造函数 或 全缺省构造函数
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}
private:int _hour;int _minute;int _second;
};class Date
{
private:// 基本类型(内置类型)int _year;int _month;int _day;// 自定义类型Time _t;
};int main()
{Date d;return 0;
}
注意 :C++11 中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在
类中声明时可以给默认值,注意这里不是初始化,是声明,给缺省值。
class Time
{
public:Time(){cout << "Time()" << endl;_hour = 0;_minute = 0;_second = 0;}
private:int _hour;int _minute;int _second;
};class Date
{
private:// 基本类型(内置类型)// 给缺省值int _year = 1; // 声明int _month = 1;int _day = 1;// 自定义类型Time _t;
};int main()
{Date d;return 0;
}

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

注意 :默认构造函数:(不传参数就可以调用的)
  1. 无参构造函数。
  2. 全缺省构造函数。
  3. 没写,编译器默认生成的构造函数
class Date
{
public:// 1、无参构造函数Date(){_year = 1;_month = 1;_day = 1;}// 2、带参构造函数Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}
private:int _year;int _month;int _day;
};int main()
{Date d1; // 无法通过编译 - 调用存在二义性return 0;
}

构造函数特点不用传参数就可以调用。

  1. 一般的类都不会让编译器默认生成构造函数,大部分都会自己去写。显示写一个全缺省,很好用。
  2. 特殊情况才会默认生成构造函数(例如 Myqueue 这样的类)。
  3. 每个类最好都要提供默认构造函数。

三、析构函数

1、概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没的呢?
析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作

构造函数是为了替代 Init,析构函数是为了替代 Destroy。 


2、特性 

析构函数是特殊的成员函数,其特征如下:

  1. 析构函数名是在类名前加上字符 ~
  2. 无参数无返回值类型
  3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意析构函数不能重载
  4. 对象生命周期结束时,C++ 编译系统系统自动调用析构函数。
  5. 后定义的先析构,跟数据结构中的栈性质相同。
typedef int DataType;
class Stack
{
public:Stack(size_t capacity = 4){_array = (DataType*)malloc(sizeof(DataType) * capacity);if (NULL == _array){perror("malloc申请空间失败!!!");return;}_capacity = capacity;_size = 0;}void Push(DataType data){// CheckCapacity();_array[_size] = data;_size++;}// 其他方法...~Stack(){if (_array){free(_array);_array = NULL;_capacity = 0;_size = 0;}}
private:DataType* _array;int _capacity;int _size;
};void TestStack()
{Stack s;s.Push(1);s.Push(2);
}

6、关于编译器自动生成的析构函数,从下面的程序可以看到,编译器生成的默认析构函数,对自定义类型成员调用它的析构函数。

  1. 对于类中的内置类型成员 —> 不处理。

  2. 对于类中的自定义类型成员 —> 调用它的析构函数完成清理工作。

class Time
{
public:~Time(){cout << "~Time()" << endl;}
private:int _hour;int _minute;int _second;
};class Date
{
private:// 基本类型(内置类型)int _year = 1970;int _month = 1;int _day = 1;// 自定义类型Time _t;
};int main()
{Date d;return 0;
}
// 程序运行结束后输出:~Time()
在 main 方法中根本没有直接创建 Time 类的对象,为什么最后会调用Time类的析构函数?


因为 main 方法中创建了 Date 对象 d,而 d 中包含 4 个成员变量,其中 _year,  _month, _day三个是内置类型成员,销毁时不需要资源清理,最后系统直接将其内存回收即可;而 _t 是 Time 类对象,所以在 d 销毁时,要将其内部包含的 Time 类的 _t 对象销毁,所以要调用 Time 类的析构函数。但是 main 函数中不能直接调用 Time 类的析构函数,实际要释放的是 Date 类对象,所以编译器会调用 Date 类的析构函数,而 Date 没有显式提供,则编译器会给 Date 类生成一个默认的析构函数,目的是在其内部调用 Time 类的析构函数,即当 Date 对象销毁时,要保证其内部每个自定义对象都可以正确销毁。main 函数中并没有直接调用 Time 类析构函数,而是显式调用编译器为 Date 类生成的默认析构函数。


注意:创建哪个类的对象则调用该类的析构函数,销毁那个类的对象则调用该类的析构函数。


7、如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如 Date 类;有资源申请时,一定要写,否则会造成资源泄漏,比如 Stack 类。

默认生成析构函数的特点(跟构造函数类似):

  1. 一些内置类型的类不作处理。比如 Date这样的类,没有资源需要清理;比如 MyQueue 也可以不写,默认生成的就可以。
  2. 自定义类型成员回去调用它的析构函数,比如:Stack、Queue...

四、拷贝构造函数

1、概念

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

2、特征

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

  1. 拷贝构造函数构造函数的一个重载形式
  2. 拷贝构造函数的参数只有一个必须是类类型对象的引用,使用传值方式编译器会直接报错,因为会引发无穷递归调用。
class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date(const Date& d) // 正确写法//Date(const Date d)    // 错误写法:编译报错,会引发无穷递归{_year = d._year;_month = d._month;_day = d._day;}private:int _year;int _month;int _day;
};int main()
{Date d1;Date d2(d1);return 0;
}

使用引用作为拷贝构造函数的参数可以有效避免无穷递归调用的问题。因为引用在初始化时不会调用拷贝构造函数,而是直接将引用绑定到已经存在的对象上。

tips:建议拷贝构造函数的参数类型前加上 const,防止其值误被修改。


3、若未显示定义,编译器会生成默认的拷贝构造函数。默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。

浅拷贝:

  1. 一个对象修改会影响另一个对象。
  2. 会析构两次,程序崩溃。

解决方法:自己实现深拷贝。 

如果没有显式定义,编译器自动生成的拷贝构造函数,它会做哪些事情呢?

  1. 对于类中的内置类型成员 —> 值拷贝
  2. 对于类中的自定义类型成员 —> 自动调用它的拷贝构造函数来完成拷贝初始化。
class Time
{
public:Time(){_hour = 1;_minute = 1;_second = 1}Time(const Time& t){_hour = t._hour;_minute = t._minute;_second = t._second;cout << "Time::Time(const Time&)" << endl;}
private:int _hour;int _minute;int _second;
};class Date
{
private:// 基本类型(内置类型)int _year = 1;int _month = 1;int _day = 1;// 自定义类型Time _t;
};int main()
{Date d1;Date d2(d1);return 0;
}

用已经存在的 d1 拷贝构造 d2,此处会调用 Date 类的拷贝构造函数。但 Date 类并没有显式定义拷贝构造函数,则编译器会给 Date 类生成一个默认的拷贝构造函数。

注意 :在编译器生成的默认拷贝构造函数中,内置类型 按照字节方式直接拷贝 的,而自定
义类型 调用其拷贝构造函数 完成拷贝的。

4、编译器生成的默认拷贝构造函数已经可以完成字节序的值拷贝了,还需要自己显式实现吗?当然像日期类这样的类是没必要的。那么下面的类呢?

像日期类这样的类是没必要的。但有些需要深拷贝的类,其内部往往是很复杂的,是需要用户显式定义拷贝构造函数来完成深拷贝的。

// 下面这个程序会崩溃掉,因为这里就需要深拷贝去解决。
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(s1); // 会造成指向的同一块区域的内容被两次释放return 0;
}
注意 :类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请 时,则拷贝构造函数是一定要写的,否则就是浅拷贝。

如图:指向了同一块空间。

image-20220417221717913

那么会引发什么问题呢?会导致 _str 指向的空间被释放两次,引发程序崩溃

image-20220417221809176


5、拷贝构造函数典型调用场景:

  1. 使用已存在对象创建新对象。
  2. 函数参数类型为类类型对象。
  3. 函数返回值类型为类类型对象。
class Date
{
public:Date(int year, int minute, int day){cout << "Date(int, int, int):" << this << endl;}Date(const Date& d){cout << "Date(const Date& d):" << this << endl;}~Date(){cout << "~Date():" << this << endl;}
private:int _year;int _month;int _day;
};Date Test(Date d)
{Date temp(d);return temp;
}int main()
{Date d1(2023 ,9, 13);Test(d1);return 0;
}

为了提高程序效率,一般对象传参时,尽量使用引用类型,返回时根据实际场景,能用引用
尽量使用引用。

一些类需要显示写拷贝和赋值,比如:Stack、Queue...

一些类不需要显示写拷贝和赋值。比如 Date 这样的类,默认生成就会完成值拷贝 / 浅拷贝,比如 MyQueue 这样的类,默认生成就会调用它的自定义类型成员 Stack 的拷贝和赋值。


五、赋值运算符重载

1、运算符重载

C++ 为了增强代码的可读性引入了 运算符重载 运算符重载是具有特殊函数名的函数,也具有其返回值类型函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
  1. 函数名字:关键字 operator 后面接需要重载的运算符符号
  2. 函数原型:返回值类型 operator 操作符(参数列表)

内置类型可以直接使用运算符运算,编译器知道要如何运算。

自定义类型无法直接使用运算符,编译器不知道要如何运算。

注意
  • 不能通过连接其他符号来创建新的操作符:比如 operator@。
  • 重载操作符必须有一个类类型参数。
  • 用于内置类型的运算符,其含义不能改变,例如:内置的整型 +,不能改变其含义。
  • 作为类成员函数重载时,其形参看起来比操作数数目少 1,因为成员函数的第一个参数为隐藏的 this
  • .*   ::   sizeof   ?:   .  注意以上 5 个运算符不能重载 
// 全局的operator==
class Date
{ 
public:Date(int year = 1, 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(2023, 9, 13);Date d2(2023, 9, 14);cout << (d1 == d2) << endl;
}

运算符重载,一般有两种方式

  • 重载成类的成员函数(形参数目看起来比该运算符需要的参数少一个,因为成员函数有隐含的 this 指针,且函数的第一个形参就是 this 指针)。
  • 重载成类的友元函数(必须有一个参数要是类的对象)(一般不这样做,而是重载成成员函数)。

下面这种写法更好: 

class Date
{ 
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}// bool operator==(Date* this, const Date& d2)bool operator==(const Date& d2) // 注意左操作数是隐藏的this,指向调用函数的对象{return _year == d2._year && _month == d2._month && _day == d2._day;}private:int _year;int _month;int _day;
};void Test ()
{Date d1(2023, 9, 13);Date d2(2023, 9, 14);cout << d1.operator==(d2) << endl; // d1.operator==(&d1, d2);
}

2、赋值运算符重载

(1)赋值运算符重载格式
  • 参数类型const T&传递引用可以提高传参效率。
  • 返回值类型T&,返回引用可以提高返回的效率,有返回值目的是为了支持连续赋值
  • 检测是否自己给自己赋值
  • 返回 *this :要复合连续赋值的含义。
class Date
{ 
public :Date(int year = 1, 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;return *this; // 支持连续赋值}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 ;
};void TestDate()
{Date d1(2023, 9, 13);Date d2(d1);Date d3(2023, 11, 28);d2 = d1 = d3;
}

(2)赋值运算符只能重载成类的成员函数不能重载成全局函数
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 =”必须是非静态成员

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

(3)用户没有显式实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝
注意 :内置类型成员变量是直接赋值的,而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。
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 = 1;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;
}

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


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

class Date
{
public:Date(int year = 1, int month = 1, int day = 1){_year = year;_month = month;_day = day;}Date& operator++() // 前置++:返回+1之后的结果。{_day += 1;return *this; // this 指向的对象函数结束后不会销毁,故以引用的方式返回提高效率。}Date operator++(int) // 后置++是先使用后+1,因此需要返回+1之前的旧值{Date temp(*this); // 在实现时需要先将this保存一份,然后给this+1_day += 1;return temp; // 因为temp是临时对象,因此只能以值的方式返回,不能返回引用}private:int _year;int _month;int _day;
};int main()
{Date d;Date d1(2023, 9, 13);d = d1++; // d: 2023,9,13  d1:2023,9,14d = ++d1; // d: 2023,9,14  d1:2023,9,14return 0;
}

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


六、日期类的实现

1、Date.h

// Date.h
#pragma once#include<iostream>
#include<assert>
#include<stdbool.h>
using namespace std;class Date
{
public:// 获取某年某月的天数int GetMonthDay(int year, int month){assert(month >= 1 && month <= 12);// 每月的天数(这个函数会被频繁调用,每次进来都要重新定义数组,所以将其定义为静态的)// 默认是平年static int days[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};int day = days[month];if (month == 2 &&((year % 4 == 0 && year % 100 != 0) || (year%400 == 0)))// 把month == 2写在前面可以直接筛选出更少的内容{day += 1; // 闰年的二月是29天}return day;}Date(int year = 1, int month = 1, int day = 1) // 全缺省的构造函数{_year = year;_month = month;_day = day;//判断日期是否合法if (_year < 0 || _month <= 0 || _month >= 13 || _day <= 0 || _day > GetMonthDay(_year, _month)){cout << _year << "/" << _month << "/" << _day << "->";cout << "非法日期" << endl;}}// 打印日期void Print(){cout << _year << "/" << _month << "/" << _day << endl;}// 拷贝构造、赋值运算符、析构函数用编译器自动生成的就可以了(因为Date类是浅拷贝)// 日期 += 天数 --> d1 += 100Date& operator+=(int day);// 日期 + 天数 --> d1 + 100Date operator+(int day);// 日期 -= 天数 --> d1 -= 100Date& operator-=(int day);// 日期 - 天数 --> d1 - 100Date operator-(int day);// 前置++Date& operator++(); // 编译器会解释为:Date& operator++(Date* const this);// 后置++Date operator++(int); // 编译器会解释为:Date& operator++(Date* const this, int);// 前置--Date& operator--();// 后置--Date operator--(int);// >运算符重载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;elsereturn false;}// ==运算符重载bool operator==(const Date& d){return _year == d._year && _month == d._month && _day == d._day;}// 这里我们只需要把>和==运算符重载了,下面的运算符都可以复用其代码了// >=运算符重载bool operator>=(const Date& d){return *this > d || *this == d; // 复用operator>、operator==}// <运算符重载bool operator<(const Date& d){return !(*this > d); // 复用operator>=,再取反}// <=运算符重载bool operator<=(const Date& d){return !(*this > d); // 复用operator>,再取反}// !=运算符重载bool operator!=(const Date& d){return !(*this == d); // 复用operator==,再取反}// 日期 - 日期(返回相差天数) --> d1 - d2int operator-(const Date& d);private:int _year;int _month;int _day;
};

2、Date.cpp

(1)日期 += 天数(返回累加天数后的日期)

比如:d1 += 100

注意:d1本身要被更改,天数累加到 d1 上面去。

Date& Date::operator+=(int day)
{if (day < 0) // 如果day是负数,就向前计算,相当于 -={return *this -= -day; // 调用-=运算符重载函数}_day += day; // 累加天数// 日期不合法,需要进位while (_day > GetDays(_year, _month)) // 表示当前月的天数已经过完了{_day -= GetDays(_year, _month);   // 减去当前月的天数_month++; // 月进位if (_month == 13) // 判断当前月份是否合法{_year++; // 年进位_month = 1; // 更新为1月}}return *this;/* 写法二:复用+运算符重载函数的代码*this = *this + day; // d1等价于*this,对d1进行+天数操作,再赋值给d1return *this;        // 返回d1*/
}

 (2)日期 + 天数(返回累加天数后的日期)

比如 :d1 + 100

注意:d1本身不能被更改,天数累加到一个临时对象上面去。

// 写法一:
Date Date::operator+(int day)
{Date tmp(*this); // 拷贝构造一份临时对象,防止调用本函数的对象被更改tmp._day += day; // 累加天数while (tmp._day > GetDays(tmp._year, tmp._month)) // 表示当前月的天数已经过完了{tmp._day -= GetDays(tmp._year, tmp._month);   // 减去当前月的天数tmp._month++; // 月进位if (tmp._month == 13) // 判断当前月份是否合法{tmp._year++;      // 年进位tmp._month = 1;   // 更新为1月}}return tmp; // 返回临时对象
}// 写法二:
Date Date::operator+(int day)
{/* 复用 += 运算符重载函数的代码 */Date tmp(*this); // 拷贝构造一份临时对象tmp += day;      // 对临时对象进行 += 天数操作return tmp;      // 返回临时对象
}

(3)日期 -= 天数(返回累减天数后的日期)

比如:d1 -= 100

Date& Date::operator-=(int day)
{if (day < 0) // 如果day小于0,就往后计算,相当于 +={return *this += -day; // 调用+=运算符重载函数}_day -= day; // 累减天数while (_day <= 0) // 说明天数不够减了,需要向上一个月去借{_month--; // 月份-1if (_month == 0){_year--;_month = 12;}_day += GetDays(_year, _month); // 借上一个月的天数}return *this;
}

 (4)日期 - 天数(返回累减天数后的日期)

比如:d1 - 100

Date Date::operator-(int day)
{// 复用 -= 运算符重载函数的代码Date tmp(*this); // 拷贝构造一份临时对象tmp -= day;      // 对临时对象进行 -= 天数操作return tmp;      // 返回临时对象
}

 (5)前置++ 和 后置++

注意:按正常的运算符重载规则,无法区分 前置++ 和 后置++,为了区分,这里做了一个特殊处理,给 后置++ 增加了一个 int 参数,这个参数仅仅是为了区分,使 前置++ 和 后置++ 构成重载。

// 前置++
// ++d1
Date& Date::operator++()
{// 复用 += 运算符重载函数的代码*this += 1;return *this;
}// 后置++
// d1++
Date Date::operator++(int)
{Date tmp(*this); // 保存当前对象自减前的值*this += 1; // 复用 += 运算符重载函数的代码return tmp; // 返回当前对象自减前的值
}

(6)前置-- 和 后置– 
// 前置--
// --d1
Date& Date::operator--()
{// 复用 -= 运算符重载函数的代码 *this -= 1;return *this;
}// 后置--
// d1--
Date Date::operator--(int)
{Date tmp(*this); // 保存当前对象自减前的值*this -= 1; // 复用 -= 运算符重载函数的代码return tmp; // 返回当前对象自减前的值
}

(7)日期 - 日期(返回相差的天数,有正负之分)

比如:d1 - d2 

思路:让小的日期不断往后++,直到等于大的日期,统计加了多少次,就相差多少天。

  • 大的日期 - 小的日期 = 正的天数
  • 小的日期 - 大的日期 = 负的天数
int Date::operator-(const Date& d)
{// 判断出大的日期和小的日期Date max = *this;Date min = d;int flag = 1; // 加一个flag变量来控制天数的正负if (max < min){max = d;min = *this;flag = -1;}// 让小的日期累加天数,加了多少次,说明就相差了多少天int count = 0;while (min != max){++min;++count;}return flag * count;
}

【总结】 
// Date.cpp
#include "Date.h"// 日期 += 天数
Date& Date::operator+=(int day)
{if (day < 0) // 如果day是负数,就向前计算,相当于 -={return *this -= -day; // 调用-=运算符重载函数}_day += day; // 累加天数// 日期不合法,需要进位while (_day > GetDays(_year, _month)) // 表示当前月的天数已经过完了{_day -= GetDays(_year, _month);   // 减去当前月的天数_month++; // 月进位if (_month == 13) // 判断当前月份是否合法{_year++; // 年进位_month = 1; // 更新为1月}}return *this;
}// 日期 + 天数
Date Date::operator+(int day)
{Date tmp(*this); // 拷贝构造一份临时对象,防止调用本函数的对象被更改tmp._day += day; // 累加天数while (tmp._day > GetDays(tmp._year, tmp._month)) // 表示当前月的天数已经过完了{tmp._day -= GetDays(tmp._year, tmp._month);   // 减去当前月的天数tmp._month++; // 月进位if (tmp._month == 13) // 判断当前月份是否合法{tmp._year++;      // 年进位tmp._month = 1;   // 更新为1月}}return tmp; // 返回临时对象
}// 日期 -= 天数
Date& Date::operator-=(int day)
{if (day < 0) // 如果day小于0,就往后计算,相当于 +={return *this += -day; // 调用+=运算符重载函数}_day -= day; // 累减天数while (_day <= 0) // 说明天数不够减了,需要向上一个月去借{_month--; // 月份-1if (_month == 0){_year--;_month = 12;}_day += GetDays(_year, _month); // 借上一个月的天数}return *this;
}// 日期 -= 天数
Date Date::operator-(int day)
{// 复用 -= 运算符重载函数的代码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; // 返回当前对象自减前的值
}// 日期 - 日期
int Date::operator-(const Date& d)
{// 判断出大的日期和小的日期Date max = *this;Date min = d;int flag = 1; // 加一个flag变量来控制天数的正负if (max < min){max = d;min = *this;flag = -1;}// 让小的日期累加天数,加了多少次,说明就相差了多少天int count = 0;while (min != max){++min;++count;}return flag * count;
}

七、const 成员

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

class Date
{
public:Date(int year, int month, int day){_year = year;_month = month;_day = day;}void Print() // void Print(Date* const this){cout << "Print()" << endl;cout << "year:" << _year << endl;cout << "month:" << _month << endl;cout << "day:" << _day << endl << endl;}void Print() const // void Print(const Date* const this){cout << "Print() const" << endl;cout << "year:" << _year << endl;cout << "month:" << _month << endl;cout << "day:" << _day << endl << endl;}
private:int _year;  // 年int _month; // 月int _day;   // 日
};void Test()
{Date d1(2022,1,1);d1.Print();const Date d2(2023,1,1); // const修饰d2.Print(); // d2.Print(&d2); // &d2的类型是const Date*,只能读不能写// 传给第一个Print会导致权限放大为可读可写
}
  1. const 修饰成员函数是有好处的,这样 const 对象可以调用,非 const 对象也可以调用。
  2. 但并不是说所有的成员函数都要加 const ,具体得看成员函数的功能,如果成员函数是修改型(比如:operrato+=、Push),那就不能加;如果是只读型(比如:Print、operator+),那就最好加上 const。
  3. const 成员(只读)函数内不可以调用其它的非 const 成员(可读可写)函数(权限放大),非 const 成员(可读可写)函数内可以调用其它的 const 成员(只读)函数(权限缩小)。

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

下面这两个是默认成员函数,一般不用重新定义,不写编译器默认会自动生成。
class Date
{ 
public:Date* operator&()// Date* operator&(Date* const this){return this;}const Date* operator&()const // const Date* operator&(const Date* const this){return this;}
private:int _year;  // 年int _month; // 月int _day;   // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如不想让别人获取到这个类型对象的地址
class Date {
public:Date* operator&(){return nullptr;}const Date* operator&() const{return nullptr;}  
private:int _year;int _month;int _day;
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/91882.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

5G消息发展的前景与挑战

随着5G技术的快速发展和普及&#xff0c;5G消息正逐渐成为全球通信领域的新焦点。 随着5G技术的快速发展和普及&#xff0c;5G消息正逐渐成为全球通信领域的新焦点。 5G消息发展呈现规模化、产业化趋势 自2020年4月国内三大运营商联合发布5G消息白皮书以来&#xff0c;已经过…

【C语言】通讯录系统实现 (保姆级教程,附源码)

目录 1、通讯录系统介绍 2、代码分装 3、代码实现步骤 3.1、制作菜单menu函数以及游戏运行逻辑流程 3.2、封装人的信息PeoInfo以及通讯录Contact结构体类型 3.3、初始化通讯录InitContact函数 3.4、增加联系人AddContact函数 3.5、显示所有联系人ShowContact函数 3.6、…

【Idea】idea、datagrip设置输入法

https://github.com/RikudouPatrickstar/JetBrainsRuntime-for-Linux-x64/releases/tag/jbr-release-17.0.6b829.5https://github.com/RikudouPatrickstar/JetBrainsRuntime-for-Linux-x64/releases/tag/jbr-release-17.0.6b829.5 下载后解压并重命名为 jbr, 然后替换对应 ide…

CTF-XSS

知识 例子 。2022 CNSS夏令营 To_be_Admin_Again_and_Again XSS攻击. 与admin有关, 可以尝试http://1.117.6.207:65005/admin, 显示Get out, HACKER! Only admin can see the flag!, 说明需要一个admin的验证, 这里想到获取admin的cookie. 每条留言会自动查看, 可以使用XS…

Python-Flask:编写自动化连接demo脚本:v1.0.0

主函数&#xff1a; # _*_ Coding : UTF-8 _*_ # Time : 13:14 # Author : YYZ # File : Flask # Project : Python_Project_爬虫 import jsonfrom flask import Flask,request,jsonify import sshapi Flask(__name__)# methods: 指定请求方式 接口解析参数host host_info[…

TSM动作识别模型【详解】

文章目录 本文使用的是somethingv2数据集&#xff0c;解压后是如下形式&#xff1b; 由于该压缩数据进行了分卷操作&#xff0c;需要合并后才能进行解压。首先我们将下面4个json文件剪贴到其他文件夹&#xff0c;只保留00-19的文件&#xff0c;然后在该文件夹下打开cmd&#xf…

模块化CSS

1、什么是模块化CSS 模块化CSS是一种将CSS样式表的规则和样式定义封装到模块或组件级别的方法&#xff0c;以便于更好地管理、维护和组织样式代码。这种方法通过将样式与特定的HTML元素或组件相关联&#xff0c;提供了一种更具可维护性、可复用性和隔离性的方式来处理样式。简单…

机器人制作开源方案 | 四轴飞行器

1. 概述 基于探索者搭建的模块化四轴飞行器研究平台&#xff0c;采用独特的设计方式&#xff0c;可实现在室内完成对四轴飞行器、无人机等运动控制的原理研究&#xff0c;以及学习飞行控制的原理知识。 2. 组装 请按照下图进行机架的组装。 整体图 请解压文末资料中的 /软件/Mi…

【LeetCode热题100】--226.翻转二叉树

226.翻转二叉树 /*** Definition for a binary tree node.* public class TreeNode {* int val;* TreeNode left;* TreeNode right;* TreeNode() {}* TreeNode(int val) { this.val val; }* TreeNode(int val, TreeNode left, TreeNode right) {* …

【聊天系统的优化】RPC方式的优化

RPC方式的优化 聊天系统的中RPC的选择Jsonprotobufmsgpack 聊天系统的中RPC的选择 在RPC方式中&#xff0c;常用的三种方式&#xff1a;Json&#xff0c;protobuf&#xff0c;Msgback 设定一个简单的加和服务&#xff0c;客户端发送一个list给服务端&#xff0c;需要将list的…

QT的ui设计中改变样式表的用法

在QT的ui设计中,我们右键会弹出一个改变样式表的选项,很多人不知道这个是干什么的。 首先我们来看下具体的界面 首先我们说一下这个功能具体是干嘛的, 我们在设置很多控件在界面上之后,常常都是使用系统默认的样式,但是当有些时候为了美化界面我们需要对一些控件进行美化…

Kafka-Kerberos票据刷新问题

线上kafka使用了 kerberos 认证&#xff0c;每隔24小时&#xff0c;票据过期&#xff0c;无法自动续期&#xff0c;出现消息发送失败问题。 从日志可以发现会有如下报错&#xff1a; 2023-09-14 17:48:47,144 [kafka-kerberos-refresh-thread-kafka/hdp-1HADOOP.COM] [] WARN …

Kafka(一)使用Docker Compose安装单机Kafka以及Kafka UI

文章目录 Kafka中涉及到的术语Kafka镜像选择Kafka UI镜像选择Docker Compose文件Kafka配置项说明KRaft vs Zookeeper和KRaft有关的配置关于Controller和Broker的概念解释Listener的各种配置 Kafka UI配置项说明 测试Kafka集群Docker Compose示例配置 Kafka中涉及到的术语 对于…

面试必考精华版Leetcode236. 二叉树的最近公共祖先

题目&#xff1a; 代码(首刷看解析 10.1&#xff09;&#xff1a; class Solution { public:TreeNode* ansnullptr;bool FindSon(TreeNode* root,TreeNode* p,TreeNode* q){if(root nullptr) return false;bool lson FindSon(root->left,p,q);bool rson FindSon(root-&…

基于SpringBoot的课程答疑系统

目录 前言 一、技术栈 二、系统功能介绍 学生信息管理 科目类型管理 老师回答管理 我的收藏管理 学生问题 留言反馈 交流区 三、核心代码 1、登录模块 2、文件上传模块 3、代码封装 前言 随着信息互联网信息的飞速发展&#xff0c;无纸化作业变成了一种趋势&#x…

vSAN7.0更换硬盘步骤

更换容量盘 预先检查 查看故障硬盘 清单->集群->监控->vsan->skyline运行->物理磁盘->运维运行状况 检查数据同步状态 清单->集群->监控->vsan->重新同步对象&#xff0c;数值全为0表示未重建。 数据迁移检查 清单->集群->监控->…

Java实现word excel ppt模板渲染与导出及预览 LibreOffice jodconverter

Java Office 一、文档格式转换 文档格式转换是office操作中经常需要进行一个操作&#xff0c;例如将docx文档转换成pdf格式。 java在这方面有许多的操作方式&#xff0c;大致可以分为内部调用&#xff08;无需要安装额外软件&#xff09;&#xff0c;外部调用&#xff08;需…

数据结构--Trie字符串统计

1、“Trie树” 作用&#xff1a; 高效地存储和查找字符串集合的数据结构。 2、“Trie树” 存储字符串的形式如下&#xff1a; 用 “0” 来表示 “根节点&#xff08;root&#xff09;”。存入一个字符串时&#xff0c;会在字符串最后结尾的那个字符节点打上标记。比如&#x…

babel.config.js配置文件详解

文章目录 一、前言三、babel 详解四、拓展阅读 一、前言 项目开发阶段&#xff0c;使用可选链操作符 ?. 出现以下编译报错问题&#xff1a; 分析&#xff1a;由于可选链操作符 ?. 是ES2020&#xff08;即ES11&#xff09;中推出的新语法&#xff0c;允许我们不需要校验当前属…

FreeRTOS(以STM32F1系列为例子)

目录 任务管理任务函数任务控制块顶层任务状态创建任务xTaskCreatexTaskCreateStaticxTaskCreateRestricted 任务优先级和心跳设置心跳设置优先级概述vTaskPrioritySetuxTaskPriorityGet 非运行态扩充阻塞态vTaskDelay挂起状态vTaskSuspend就绪状态完整的状态转换图延迟函数vTa…