引言
以C语言栈的实现为例,在实际开发中,我们可能会遇到以下两个问题:
1.初始化和销毁管理不当:C语言中的栈实现通常需要手动管理内存(如使用malloc和free),这导致初始化和销毁栈时容易出错或忘记。
2.代码重复:在栈的操作中,经常需要传递栈的指针或引用,这导致代码冗余和难以维护。
在C++编程中,类的实现引入了一些成员函数,这些函数即使在类中没有被显式定义,编译器也会自动生成它们。这些函数被称为类的默认成员函数。一共有6种默认成员函数:构造函数、析构函数、拷贝构造函数、赋值运算符重载、取地址运算符重载、const取地址运算符重载。接下来我们会一一讲解这些成员函数。
构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象,而是初始化对象。
特征
1.函数名与类名相同,无返回值。
class MyClass {
public:
MyClass() { // 构造函数
// 初始化代码
}
};
2.对象实例化时编译器自动调用对应的构造函数。
int main()
{
MyClass obj; // 自动调用 MyClass() 构造函数
return 0;
}
3.构造函数可以重载,是为了支持多种初始化的方式。
class MyClass {
public:
MyClass() {
// 没有参数的构造函数
cout << "Default Constructor" << endl;
}
MyClass(int a) {
// 带一个整型参数的构造函数
cout << "Constructor with int: " << a << endl;
}
MyClass(int a, int b) {
// 带两个整型参数的构造函数
cout << "Constructor with two ints: " << a << ", " << b << endl;
}
};
int main() {
MyClass obj1;
MyClass obj2(10);
MyClass obj3(20, 30);
return 0;
}
4. 编译器自动生成的默认构造函数。
如果类中没有显示定义构造函数,C++编译器会自动生成一个无参的默认构造函数。一旦用户定义了构造函数,编译器将不再生成默认构造函数。
注意:如果我们不写构造函数,在不同编译器中,类的成员变量的初始化情况也会有差异。
在vs2013中:
如果我们不写构造函数,在成员变量中,内置类型不会被初始化,而自定义类型会被初始化。
补充:内置类型和自定义类型是什么?
①内置类型(基本类型):语言本身定义的基础类型,如int,char,double,指针等等。
②自定义类型:用struct,class等等定义的类型。
在vs2019中:
如果我们不写构造函数,成员变量中有自定义类型的时候,内置类型和自定义类型就会都被初始化。
总结:
- 如果类中有内置类型的成员变量,通常需要自己写构造函数来初始化这些变量。
- 如果类中只有自定义类型的成员变量,可以考虑让编译器自动生成默认构造函数。
补充:C++11的缺省值
C++11允许在成员声明时给缺省值,这样编译器可以生成一个带有缺省参数的构造函数。
class MyClass {
public:
//...一些成员函数
public:
//C++11支持:
//这里不是初始化,是声明
//这里给的是默认的缺省值,编译器生成默认构造函数的时候会用到
int a = 10;
int b = 20;
};
5. 构造函数的调用与普通函数不同
class MyClass {
public:
MyClass() {
cout << "Constructor called" << endl;
}
void normalFunction1() {
cout << "Normal function2 called" << endl;
}
};
void normalFunction2() {
cout << "Normal function2 called" << endl;
}
int main() {
MyClass obj; // 自动调用构造函数
// MyClass obj(); 不可以这样写,会与函数声明有冲突,编译器不好识别
obj.normalFunction1(); // 显式调用普通成员函数
normalFunction2(); // 显式调用普通函数
return 0;
}
6. 默认构造函数
不传参数就可以调用的函数就是默认构造函数(不要与默认成员函数弄混淆)。无参构造和全缺省构造函数都是默认构造函数。一个类中默认构造函数只能有一个,否则无参调用时会存在歧义。
析构函数
析构函数与构造函数功能相反,它并不是用来销毁对象本身,因为局部对象的销毁工作是由编译器完成的。它的功能是在对象销毁时会自动调用析构函数,完成对象中资源的清理工作。
特征
1.析构函数名
在类名前加上字符~
,无参数无返回值类型。
class MyClass {
public:
~MyClass() {
// 清理工作
cout << "MyClass destructor called" << endl;
}
};
2.一个类只能有一个析构函数
若未显式定义,系统会自动生成默认的析构函数。
class MyClassWithoutDestructor {
// 没有显式定义析构函数
};
int main() {
MyClassWithoutDestructor obj;
// 系统自动调用默认析构函数,但不做任何操作
return 0;
}
3.对象生命周期结束时,C++编译系统会自动调用析构函数
#include <iostream>
class MyClass {
public:
~MyClass() {
std::cout << "MyClass destructor called" << std::endl;
}
};
int main() {
MyClass obj;
// 当 obj 超出作用域时,析构函数被调用
return 0;
}
注意:
1.析构函数不能重载。
2.内置类型成员在析构时不需要做处理,它们会自动被清理。
3.自定义类型会去调用它的析构函数:
#include<iostream>
using namespace std;
class MemberClass {
public:
~MemberClass() {
cout << "MemberClass destructor called" << endl;
}
};
class MyClass {
public:
~MyClass() {
cout << "MyClass destructor called" << endl;
// member 的析构函数会自动被调用
}
private:
MemberClass member;
};
int main() {
MyClass obj;
return 0;
}
总结什么情况需要我们写析构函数:
1.一般情况下,有动态申请资源就需要显示写析构函数来释放资源。
#include<iostream>
using namespace std;
class MyClass {
public:
MyClass() {
data = (int*)malloc(sizeof(int) * 10); // 动态分配内存
if (data == nullptr) {
return;
}
}
~MyClass() {
free(data);// 释放动态分配的内存
data = nullptr;
cout << "MyClass destructor called, memory freed" << endl;
}
private:
int* data;
};
int main() {
MyClass obj;
return 0;
}
2.没有动态申请的资源,不需要写析构。
#include <iostream>
using namespace std;
class MyClass {
public:
MyClass() {
cout << "MyClass constructor called" << endl;
}
// 不需要显式定义析构函数
// ~MyClass() {} // 可以省略
};
int main() {
MyClass obj;
return 0;
}
3.需要释放资源的成员都是自定义类型,不需要写析构。
#include <iostream>
class MemberClass {
public:
~MemberClass() {
std::cout << "MemberClass destructor called" << std::endl;
}
};
class MyClass {
public:
MyClass() {
std::cout << "MyClass constructor called" << std::endl;
}
// 不需要显式定义析构函数,MemberClass 的析构函数会自动被调用
// ~MyClass() {} // 可以省略
private:
MemberClass member;
};
int main() {
MyClass obj;
return 0;
}
补充:不同作用域下同类对象的构造和析构顺序
1.全局作用域
全局对象在程序开始执行之前按照声明的顺序进行构造,在程序结束时按照相反的顺序进行析构。
2.静态作用域
静态对象在它们所在的作用域第一次被访问时(对于函数内的静态对象,是第一次调用该函数时)进行构造,在程序结束时进行析构。静态对象的构造和析构顺序与它们在代码中的出现顺序相反(即后构造的先析构)。
3.局部作用域
局部对象(包括在函数体、代码块内声明的对象)在它们被创建时(即执行到声明它们的代码行时)进行构造,在它们离开作用域时(通常是函数返回或代码块结束时)进行析构。局部对象的构造和析构顺序与它们在代码中的出现顺序相反(即后构造的先析构)。
(代码例子)
拷贝构造
概念
拷贝构造函数是类的一个特殊成员函数,用于创建一个新对象作为现有对象的副本。它允许使用另一个同类型的对象来初始化新创建的对象。
特征
1.函数名与类名相同
拷贝构造函数是构造函数的一种重载形式,因此其函数名必须与类名完全相同。
2.参数为类类型对象的常量引用且参数只有一个
这样做有两个原因:一是避免无穷递归调用(如果参数是传值方式),二是防止在拷贝过程中修改被拷贝的对象。
错误示范:通过值传递的拷贝构造函数
如果拷贝构造函数通过值传递来接收参数,每次调用拷贝构造函数时都会再次触发拷贝构造函数的调用,形成无穷递归。
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
// 错误示范:通过值传递接收参数,会导致无穷递归
Date(Date d) // 这里应该使用 const Date& d 来避免无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
// 在 main 函数中调用拷贝构造函数
int main()
{
Date d1;
// 这一行会导致调用 Date(Date d) 拷贝构造函数,从而进入无穷递归
Date d2(d1); // 若按上述错误方式实现,这里会导致编译错误或运行时错误
return 0;
}
正确示范:通过引用传递的拷贝构造函数
为了避免无穷递归,拷贝构造函数应该通过常量引用来接收参数。
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;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
3.默认拷贝构造函数
如果类中没有显式定义拷贝构造函数,编译器会生成一个默认的拷贝构造函数。默认的拷贝构造函数按照内存存储的字节序进行拷贝,这种拷贝方式称为浅拷贝或值拷贝。
补充:C++规定,如果要拷贝一个内置类型的变量,值拷贝(也就是浅拷贝)就可以了;如果要拷贝一个自定义类型的变量,必须调用拷贝构造来完成拷贝。
浅拷贝的问题:
- 同一个空间析构两次:当一个类包含指向动态分配内存的指针,并且使用默认拷贝构造函数进行拷贝时,两个对象会共享同一块内存。这会导致在析构时,同一块内存被两个对象分别析构,引发未定义行为(如崩溃)。
- 修改一个对象会影响另一个对象:同样地,由于两个对象共享同一块内存,修改一个对象的成员变量(特别是包含指针的成员变量)也会影响另一个对象。
class Stack {
private:
int* arr;
int top;
int maxSize;
public:
// 构造函数
Stack(int size = 10) {
maxSize = size;
top = -1;
arr = (int*)malloc(maxSize * sizeof(int));
if (arr == nullptr) {
throw std::bad_alloc(); // 如果内存分配失败,抛出异常
}
// 默认拷贝构造函数(错误做法:浅拷贝)
Stack(const Stack& other);{
maxSize = other.maxSize;
top = other.top;
// 注意:这里只是简单地复制了指针,没有分配新的内存并复制数据
arr = other.arr;
}
// 析构函数
~Stack() {
free(arr);
}
// 其他成员函数(省略实现)
void push(int value) {
if (top < maxSize - 1) {
arr[++top] = value;
}
}
void pop() {
if (top >= 0) {
--top;
}
}
void print() const {
for (int i = 0; i <= top; ++i) {
cout << arr[i] << " ";
}
cout << endl;
}
};
int main() {
Stack s1;
s1.push(1);
s1.push(2);
Stack s2 = s1; // 使用默认拷贝构造函数(浅拷贝)
s2.pop();
s2.print(); // 输出: 1
s1.print(); // 输出: 1(因为 s1 和 s2 共享同一块内存,s2 的 pop 操作影响了 s1)
// 当 s1 和 s2 析构时,同一块内存会被析构两次,引发未定义行为
return 0;
}
在上面的代码中,Stack 类的拷贝构造函数是浅拷贝,只是复制了指针 arr,而没有分配新的内存并复制数据。因此,s1 和 s2 共享同一块内存,修改 s2 会影响 s1。当 s1 和 s2 析构时,同一块内存会被析构两次,这是未定义行为。
正确做法:深拷贝解决方案
为了避免上述问题,我们需要自己实现拷贝构造函数,进行深拷贝。
// 拷贝构造函数(深拷贝)
Stack(const Stack& other) {
maxSize = other.maxSize;
top = other.top;
// 使用malloc分配新的内存,并进行深拷贝
arr = (int*)malloc(maxSize * sizeof(int));
if (arr == nullptr) {
throw std::bad_alloc(); // 如果内存分配失败,抛出异常
}
memcpy(arr, other.arr, (top + 1) * sizeof(int)); // 复制数据
}
总结需要自己写拷贝构造函数的情况
1.成员变量都是内置类型不用写:内置类型(如 int、float 等)的拷贝是值拷贝,不会引发浅拷贝问题。
2.成员对象都是自定义类型不用写:如果自定义类型的成员对象已经正确实现了拷贝构造函数(特别是深拷贝),那么你的类也可以依赖这些成员对象的拷贝构造函数,而不需要自己实现。
3.需要写拷贝构造函数的情况:当类中包含指向动态分配内存的指针或其他需要深拷贝的资源时,必须自己实现拷贝构造函数进行深拷贝。例如,上面的 Stack 类就需要自己实现拷贝构造函数来避免浅拷贝导致的问题。
运算符重载之赋值重载
前置知识:运算符重载
运算符重载是一种使类能够使用特殊函数名来重新定义或扩展内置运算符行为的方法。这些特殊函数的名字由关键字 operator 后跟要重载的运算符符号组成。通过运算符重载,可以使类的实例像内置类型一样使用某些运算符。
函数原型
返回值类型 operator 操作符(参数列表);
注意事项
-
至少有一个类类型参数:运算符重载函数至少有一个参数必须是类类型,这样才能确保重载是针对自定义类型而不是内置类型。
-
操作数数量与参数数量匹配:如果操作符涉及多个操作数,重载函数就有多个参数。例如,二元操作符(如加法+)需要两个参数,一元操作符(如取负-)只需要一个参数。
-
不能创建新的操作符:不能通过连接其他符号来创建新的操作符,例如: operator@ 是不允许的。
-
不能重载的运算符:某些运算符不能重载,包括 *、::、sizeof、?: (条件运算符)和 .(成员访问运算符)。
-
成员函数重载时的隐藏参数:当运算符作为类的成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的 this 指针。
赋值重载
为自定义类型(如类)重载赋值运算符=的行为。
特征
1.格式
- 参数类型:const T&,传递引用可以提高传参效率。值传递会创建参数的副本,这在大型对象或复杂对象时会导致不必要的开销。
// 假设有一个大型对象LargeObject
LargeObject foo = bar; // 如果bar是通过值传递,这里会创建bar的副本
LargeObject& fooRef = bar; // 如果bar是通过引用传递,这里不会创建副本
- 返回值类型:T&,返回引用可以提高返回效率,并支持连续赋值。
//以上面的Date类为例
Date d1, d2, d3;
d1 = d2 = d3; // 如果没有返回引用,则无法支持这种连续赋值
- 检测是否自己给自己赋值:通过比较this指针和传入参数的地址来实现。
if (this == &d) {
return *this;
}
- 返回*this:符合连续赋值的含义,并允许链式操作。
Date& Date::operator=(const Date& d){
if (this == &d) {
return *this;
}
_year = d._year;
_month = d._month;
_day = d._day;
return *this;
}
2.默认赋值运算符重载:
- 如果用户没有显式实现赋值运算符重载,编译器会生成一个默认的赋值运算符重载,以值的方式逐字节拷贝(浅拷贝)。
- 默认生成的赋值重载与拷贝构造行为一样,内置类型成员是值拷贝(浅拷贝),自定义类型成员会去调用它的赋值重载函数。
总结:
- 不需要自己实现赋值拷贝的数据类型:内置类型(如int、float等)。
- 需要自己实现赋值拷贝的数据类型:管理动态内存的类。
3.赋值重载只能重载成类的成员函数,不能重载成全局函数。
- 除了赋值重载,其他运算符重载函数可以写成全局的。
#include <iostream>
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;
}
// 为了展示结果,添加一个输出方法
void print() const {
std::cout << _year << "-" << _month << "-" << _day << std::endl;
}
private:
int _year;
int _month;
int _day;
};
// 全局函数:重载加法运算符
Date operator+(const Date& date, int days) {
Date tmp = date;
tmp._day += day;
while (tmp._day > GetMonthDay(tmp._year, tmp._month)) {
++tmp._month;
if (tmp._month > 12) {
++tmp._year;
tmp._month = 1;
}
int lastMonth = tmp._month - 1, lastYear = tmp._year;
if (lastMonth < 1) {
lastMonth = 12;
lastYear -= 1;
}
tmp._day -= GetMonthDay(lastYear, lastMonth);
}
return tmp;
}
// 全局函数:重载相等运算符
bool operator==(const Date& date1, const Date& date2) {
return date1._year == date2._year && date1._month == date2._month && date1._day == date2._day;
}
int main() {
Date d1(2023, 10, 1);
Date d2 = d1 + 10; // 使用重载的加法运算符
d1.print(); // 输出:2023-10-1
d2.print(); // 输出:2023-10-11(注意:这里的日期计算是简化的)
if (d1 == d2) {
std::cout << "d1 和 d2 相等" << std::endl;
} else {
std::cout << "d1 和 d2 不相等" << std::endl;
}
// 测试赋值运算符
Date d3;
d3 = d1;
d3.print(); // 输出:2023-10-1
return 0;
}
前置++和后置++重载
前置++
class Date{
public:
Date(int year = 1900, int month = 1, int day = 1){
_year = year;
_month = month;
_day = day;
}
// 前置++:返回+1之后的结果
// 以引用方式返回提高效率
Date& operator++(){
_day += 1;
return *this;
}
private:
int _year;
int _month;
int _day;
};
我们前面说过,函数中的局部变量中的引用是不能作为返回值返回的,因为函数结束后引用的变量在函数结束后会销毁,为什么这里可以返回(*this)的引用呢?
这里存在一个误解。this
指针并不是指向一个局部变量的指针,而是指向调用成员函数的对象实例的指针。因此,*this
返回的是对象本身的引用,而不是一个局部变量的引用。所以this指向的对象函数结束后不会销毁。
后置++
class Date{
public:
Date(int year = 1900, int month = 1, int day = 1){
_year = year;
_month = month;
_day = day;
}
Date& operator++(){
_day += 1;
return *this;
}
// 后置++:
Date operator++(int){
Date temp(*this);
_day += 1;
return temp;
}
private:
int _year;
int _month;
int _day;
};
前置++和后置++都是一元运算符,为了让前置++与后置++形成能正确重载,C++规定:后置++重载时多增加一个int类型的参数,但调用函数时该参数不用传递,编译器自动传递
注意:后置++是先使用后+1,因此需要返回+1之前的旧值,故在实现时需要先将this保存一份,然后给this+1,而temp是临时对象,因此只能以值的方式返回,不能返回引用。
日期类的实现
Date.h
#pragma once
#include <iostream>
#include<cassert>
#include<cmath>
using namespace std;
class Date
{
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month);
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
// d2(d1)
Date(const Date& d);
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 析构函数
~Date();
//打印
void print();
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day);
// 日期-天数
Date operator-(int day);
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// >运算符重载
bool operator>(const Date& d);
// ==运算符重载
bool operator==(const Date& d);
// >=运算符重载
bool operator >= (const Date& d);
// <运算符重载
bool operator < (const Date& d);
// <=运算符重载
bool operator <= (const Date& d);
// !=运算符重载
bool operator != (const Date& d);
// 日期-日期 返回天数
int operator-(const Date& d);
private:
int _year;
int _month;
int _day;
};
Date.cpp
#include "Date.h"
Date::Date(int year, int month, int day) {
if (month > 0 && month <= 12 && day >= 0 && GetMonthDay(year, month)) {
_year = year;
_month = month;
_day = day;
}
else {
cout << "非法日期" << endl;
}
}
int Date::GetMonthDay(int year, int month) {
if (month > 0 && month <= 12) {
int Month[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))) {
Month[2] = 29;
}
return Month[month];
}
else {
cout << "非法月份" << endl;
assert(false);
}
}
Date::Date(const Date& d) {
_year = d._year;
_month = d._month;
_day = d._day;
}
Date::~Date() {
}
Date Date::operator+(int day) {
Date tmp = *this;
tmp += 100;
return tmp;
}
Date Date::operator-(int day)
{
Date tmp = *this;
tmp -= day;
return tmp;
}
Date& Date::operator-=(int day)
{
if (day < 0) {
*this += abs(day);
return *this;
}
int curDay = _day;
_day -= day;
if (_day <= 0) {
--_month;
if (_month <= 0) {
_month = 12;
--_year;
}
_day += curDay;
}
while (_day <= 0) {
--_month;
if (_month <= 0) {
_month = 12;
--_year;
}
_day += GetMonthDay(_year, _month);
}
return *this;
}
Date& Date::operator++()
{
*this += 1;
return *this;
}
Date Date::operator++(int)
{
Date tmp = *this;
*this += 1;
return tmp;
}
Date Date::operator--(int)
{
Date tmp = *this;
*this -= 1;
return tmp;
}
Date& Date::operator+=(int day) {
if (day < 0) {
*this -= abs(day);
return *this;
}
_day += day;
while (_day > GetMonthDay(_year, _month)) {
++_month;
if (_month > 12) {
++_year;
_month = 1;
}
int lastMonth = _month - 1, lastYear = _year;
if (lastMonth < 1) {
lastMonth = 12;
lastYear -= 1;
}
_day -= GetMonthDay(lastYear, lastMonth);
}
return *this;
}
void Date::print() {
cout << _year << "-" << _month << "-" << _day << endl;
}
Date& Date::operator--()
{
(*this) -= 1;
return *this;
}
bool Date::operator>(const Date& d)
{
if (_year > d._year) {
return true;
}else if (_year < d._year) {
return false;
}
//年份相同的情况
if (_month > d._month) {
return true;
}
else if (_month < d._month) {
return false;
}
//年份和月份都相同的情况
if (_day > d._day) {
return true;
}
else if (_day < d._day) {
return false;
}
//日期相同的情况
return false;
}
bool Date::operator==(const Date& d)
{
return (*this)._year == d._year && (*this)._month == d._month && (*this)._day == d._day;
}
bool Date::operator>=(const Date& d)
{
if ((*this) > d || (*this) == d) {
return true;
}
return false;
}
bool Date::operator<(const Date& d)
{
if (_year < d._year) {
return true;
}
else if (_year > d._year) {
return false;
}
//年份相同的情况
if (_month < d._month) {
return true;
}
else if (_month > d._month) {
return false;
}
//年份和月份都相同的情况
if (_day < d._day) {
return true;
}
else if (_day > d._day) {
return false;
}
//日期相同的情况
return false;
}
bool Date::operator<=(const Date& d)
{
if ((*this) > d) {
return false;
}
else {
return true;
}
}
bool Date::operator!=(const Date& d)
{
if ((*this) == d) {
return false;
}
else {
return true;
}
}
int Date::operator-(const Date& d)
{
Date maxDate, minDate;
int flag = 1, result = 0;
if ((*this) > d) {
maxDate = *this, minDate = d;
}
else {
maxDate = d, minDate = *this;
flag = -1;
}
Date minTmp = minDate, helper = minDate;
helper._month = maxDate._month;
helper._day = maxDate._day;
if (minTmp < helper) {
for (int i = minTmp._month + 1; i < helper._month; i++) {
result += GetMonthDay(minTmp._year, i);
}
result += GetMonthDay(minTmp._year, minTmp._month) - minTmp._day;
result += helper._day;
}
else if (minTmp > helper) {
for (int i = helper._month + 1; i < minTmp._month; i++) {
result -= GetMonthDay(minTmp._year, i);
}
result -= GetMonthDay(helper._year, helper._month) - helper._day;
result -= minTmp._day;
}
while (helper != maxDate) {
int curMonth = helper._month;
int curYear = helper._year;
for (int i = 0; i < 12; i++) {
if (curMonth > 12) {
curMonth = 1;
++curYear;
}
result += GetMonthDay(curYear, curMonth);
++curMonth;
}
++helper._year;
}
return flag * result;
}
类的流插入和流提取重载
在C++中,cin
和cout
分别用于从标准输入和向标准输出流数据。它们分别属于istream
和ostream
类:
这些类通过重载流运算符(如<<
和>>
)来支持内置类型的数据的自动识别和处理:
为了让我们自定义的类(如Date
类)也能使用这些流运算符,我们需要对它们进行重载。
流插入运算符重载(<<
)
讲解
所以,如果我们想支持cout<<d1;该怎么办呢?
void Date::operator<<(ostream& out, const Date& d) {
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
但是这种成员函数形式的重载,只能通过d1<<cout;和d1.operator<<(cout)的方式调用到流插入重载函数,并不符合我们平时的使用习惯,不能通过cout<<d1;的方式调用。
那我们该怎么才能让它支持cout<<d1;这种写法呢?
首先,我们要知道,插入重载不能写成员函数,因为Date对象默认占用第一个参数(做了左操作数),写出来就一定d1<<cout;和d1.operator<<(cout);这两种写法,不符合使用习惯。所以我们要想办法让cout成为第一个参数, 我们让他成为全局函数形式就可以了。但是这样我们就不放访问私有的成员变量了。这里有两种办法:
①我们先在类中获取写一个成员变量的函数,把这个函数的属性设置公有的,在全局的流插入重载函数中调用这个函数来获取成员变量的值。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1);
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//省略一些重载函数的声明......
// 流插入不能写成成员函数?
// 因为Date对象默认占用第一个参数,就是做了左操作数
// 写出来就一定是下面这样子,不符合使用习惯
//d1 << cout; // d1.operator<<(cout);
//void operator<<(ostream& out);
int GetYear()
{
return _year;
}
int GetMonth()
{
return _month;
}
int GetDay()
{
return _day;
}
private:
int _year;
int _month;
int _day;
};
void operator<<(ostream& out, const Date& d)
{
out << d.GetYear() << "年" << d.GetMonth() << "月" << d.GetDay() << "日" << endl;
}
②友元函数:在类中对要操作的全局函数加一个友元函数声明,可以让该全局函数突破私有权限访问成员函数。
class Date
{
// 友元函数声明
friend void operator<<(ostream& out, const Date& d);
public:
Date(int year = 1, int month = 1, int day = 1);
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//省略一些重载函数的声明......
// 流插入不能写成成员函数?
// 因为Date对象默认占用第一个参数,就是做了左操作数
// 写出来就一定是下面这样子,不符合使用习惯
//d1 << cout; // d1.operator<<(cout);
//void operator<<(ostream& out);
private:
int _year;
int _month;
int _day;
};
void operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
到目前为止,流插入重载函数还有一个功能无法完成,那就是连续的流插入: cout<<d1<<d2<<d3;
我们需要知道,cout其实是我们的终端控制台,<<是流插入运算符,cout<<d1;指的是d1对象流向终端控制台。对于cout<<d1<<d2<<d3;这条语句,我们需要清楚,它会先输出d1再输出d2最后再输出d3。所以会先让d1流向cout(调用了流插入重载函数),然后函数返回流入了d1的cout;再让d2流入cout(调用了流插入重载函数),返回流入了d2的cout;最后d3流入cout(再次调用流插入重载函数),返回流入了d3的cout。所以这是从左往右执行的。
实现
所以该这样写:
class Date
{
// 友元函数声明
friend ostream& operator<<(ostream& out, const Date& d);
public:
Date(int year = 1, int month = 1, int day = 1);
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
Date(const Date& d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//省略一些重载函数的声明......
// 流插入不能写成成员函数?
// 因为Date对象默认占用第一个参数,就是做了左操作数
// 写出来就一定是下面这样子,不符合使用习惯
//d1 << cout; // d1.operator<<(cout);
//void operator<<(ostream& out);
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
}
流提取运算符重载(>>
)
实现
class Date
{
// 友元函数声明
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1);
void Print() const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
Date(const Date& d) // 错误写法:编译报错,会引发无穷递归
{
_year = d._year;
_month = d._month;
_day = d._day;
}
//省略一些重载函数的声明...
private:
int _year;
int _month;
int _day;
};
ostream& operator<<(ostream& out, const Date& d)
{
out << d._year << "年" << d._month << "月" << d._day << "日" << endl;
return out;
}
istream& operator>>(istream& in, Date& d)
{
int year, month, day;
in >> year >> month >> day;
if (month > 0 && month <= 12 && day >= 0 && day <= d.GetMonthDay(year, month)) {
d._year = year;
d._month = month;
d._day = day;
}
else {
cout << "非法日期" << endl;
assert(false);
}
return in;
}
const修饰对象时,需要注意的问题
我们发现对象被const修饰后就调用不了了。因为这里的权限被放大了。
具体来说:d1.print()会转换成d1.print(&d1),print()
函数会隐式地通过this
指针接收到d1
的指针,d1的指针是Date*类型,没有被const修饰的;d2.print()会转换成d2.print(&d2),print()
函数会隐式地通过this
指针接收到d2
的指针,d2的指针是const Date*类型,是有被const修饰的。
通过前面的学习,我们知道,这里的print()隐藏了一个Date* this的参数,d2调用print()时,print()
函数会隐式地通过this
指针接收到d2
的指针,d2的指针是const Date*类型,权限被放大了(因为print()函数中可能会有修改this指针指向的对象(在这里是d2)的操作)。
如何解决?把传入Print()函数的Date* this参数用const修饰就可以了,这样就把权限缩小了。但是它是隐藏的指针,我们不能用改显示指针的方式去改它,正确的改法如下:
这样普通对象和const对象都可以调用print()函数了。
注意:但是不一定所有的成员函数都可以加const,要修改成员变量的函数就不能加const。
这里还有一个问题:
d1<d2和 d1 + 100不会报错,但是d2<d1和d2 + 100会报错。其实与刚刚讲的问题一样,d2调用<重载函数和+重载函数的时候权限被放大了。保险起见,对于不会修改成员变量的函数,我们都加上const修饰this指针:
#pragma once
#include <iostream>
#include<cassert>
#include<cmath>
using namespace std;
class Date
{
friend ostream& operator<<(ostream& out, const Date& d);
friend istream& operator>>(istream& in, Date& d);
public:
// 获取某年某月的天数
int GetMonthDay(int year, int month)const;
// 全缺省的构造函数
Date(int year = 1900, int month = 1, int day = 1);
// 拷贝构造函数
// d2(d1)
Date(const Date& d);
// 赋值运算符重载
// d2 = d3 -> d2.operator=(&d2, d3)
Date& operator=(const Date& d);
// 析构函数
~Date();
//打印
void print()const;
// 日期+=天数
Date& operator+=(int day);
// 日期+天数
Date operator+(int day)const;
// 日期-天数
Date operator-(int day)const;
// 日期-=天数
Date& operator-=(int day);
// 前置++
Date& operator++();
// 后置++
Date operator++(int);
// 后置--
Date operator--(int);
// 前置--
Date& operator--();
// >运算符重载
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;
// !=运算符重载
bool operator != (const Date& d)const;
// 日期-日期 返回天数
int operator-(const Date& d)const;
private:
int _year;
int _month;
int _day;
};
成员函数的声明加上了const,那定义处也要加上const。
取地址及const取地址操作符重载
主要是普通对象和const对象取地址,这两个默认成员函数一般不用重新定义 ,编译器默认会生成的就够我们使用了。
class Date{
public :
Date* operator&(){
return this;
}
const Date* operator&()const{
return this;
}
private:
int _year ; // 年
int _month ; // 月
int _day ; // 日
};
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需