C++从入门到实战----类和对象(中)
目录
正文开始
一、类和对象
1.1 类的默认成员函数
默认成员函数就是用户没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。我们要从两个⽅⾯去学习:
- 第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
- 第⼆:编译器默认⽣成的函数不满足我们的需求,我们需要⾃⼰实现,那么如何自己实现?

1.2 构造函数
构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要任务并不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),⽽是对象实例化时初始化对象。构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功能,构造函数⾃动调⽤的特点就完美的替代的了Init。
构造函数的特点:
- 函数名与类名相同。
- 无返回值 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
- 对象实例化时系统会⾃动调⽤对应的构造函数。
- 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个⽆参的默认构造函数,⼀旦⽤户显 式定义编译器将不再⽣成。
- 默认构造函数是指 不需要传参数就能调用的构造函数,它包括三种情况: 无参构造函数(没有参数)、全缺省构造函数(所有参数都有默认值),以及当你 没写任何构造函数时编译器自动生成的那个构造函数。这三者都属于 默认构造函数,但一个类中只能存在其中一个——不能同时定义无参和全缺省构造函数,否则调用时会产生歧义。需要注意的是,很多人误以为只有编译器生成的那个才叫默认构造函数,其实只要能无参调用的构造函数,都算默认构造函数。
- 当类中未定义构造函数时,编译器自动生成的默认构造函数不会初始化内置类型成员(值不确定),但会调用自定义类型成员的默认构造函数;若该自定义类型没有默认构造函数,则会导致编译错误,此时必须通过初始化列表显式初始化。
常见的错误:(1)
#define _CRT_SECURE_NO_WARNINGS 1 #include <iostream> using namespace std; class Data { private: int _year; int _month; int _day; public: //1.无参构造函数 //Data() //{ // _year = 1; // _month = 1; // _day = 1; //} //2.带参构造函数 Data(int year, int month, int day) { _year = year; _month = month; _day = day; } /*3.全缺省构造函数*/ //Data(int year = 1, int month = 1, int day = 1) //{ // _year = year; // _month = month; // _day = day; //} void Print() { cout << _year << "/" << _month << "/" << _day << endl; } }; int main() { Data d1; //这里想要调用"默认构造函数",但是我注释了1和3,写了带参的构造函数,所以编译器不再生成默认构造函数 d1.Print(); return 0; }
原因:你写了带参构造函数 → 编译器不再自动生成默认构造函数 → Date d1; 没有匹配的构造函数可用!
(2)
#define _CRT_SECURE_NO_WARNINGS 1 #include <iostream> using namespace std; class Data { private: int _year; int _month; int _day; public: //1.无参构造函数 //Data() //{ // _year = 1; // _month = 1; // _day = 1; //} //2.带参构造函数 //Data(int year, int month, int day) //{ // _year = year; // _month = month; // _day = day; //} /*3.全缺省构造函数*/ Data(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } void Print() { cout << _year << "/" << _month << "/" << _day << endl; } }; int main() { //Data d1; //这里想要调用"默认构造函数",但是我注释了1和3,写了带参的构造函数,所以编译器不再生成默认构造函数 //d1.Print(); Data d1; d1.Print(); Data d2(); return 0; }
Date d2(); 在 C++ 中不是创建对象!
它的意思是:声明一个名为 d2 的函数,该函数:
所以不要这样写:Date d2(),要么就别加括号,加括号就传参数
#include<iostream> using namespace std; typedef int STDataType; class Stack { public: Stack(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr == _a) { perror("malloc申请空间失败"); } _capacity = n; _top = 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; // 两个Stack实现队列 class MyQueue { public: //编译器默认⽣成MyQueue的构造函数调⽤了Stack的构造,完成了两个成员的初始化 private: Stack pushst; Stack popst; }; int main() { MyQueue mq; return 0; }
1.3 析构函数
析构函数与构造函数功能相反,析构函数不是完成对对象本⾝的销毁,⽐如局部对象是存在栈帧的, 函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,完成对 象中资源的清理释放⼯作。析构函数的功能类⽐我们之前Stack实现的Destroy功能,⽽像Date没有 Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。析构函数的特点:
- 析构函数名是在类名前加上字符 ~。
- 无参数⽆返回值。 (这⾥跟构造类似,也不需要加void)
- ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
- 对象⽣命周期结束时,系统会⾃动调用析构函数
- 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
- 如果类中没有申请资源时,析构函数可以不写,直接使⽤编译器⽣成的默认析构函数,如Date;如 果默认⽣成的析构就可以⽤,也就不需要显⽰写析构,如MyQueue;但是有资源申请时,⼀定要自己写析构,否则会造成资源泄漏,如Stack。
- ⼀个局部域的多个对象,C++规定后定义的对象先析构。
#include<iostream> using namespace std; typedef int STDataType; class Stack { public: Stack(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr == _a) { perror("malloc申请空间失败"); } _capacity = n; _top = 0; } ~Stack() { cout << "~Stack()" << endl; free(_a); _a = nullptr; _top = _capacity = 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; // 两个Stack实现队列 class MyQueue { public:// 显⽰写析构,也会⾃动调⽤Stack的析构 /*~MyQueue() {}*/ private: Stack pushst; Stack popst; }; int main() { Stack st1; Stack st2; return 0; }对比一下用 C++ 和 C 实现的 Stack 解决之前括号匹配问题 isValid,我们发现有了构造函数和析构函数确实方便了很多,不会再忘记调用 Init 和 Destroy 函数了,也方便了不少。
#define _CRT_SECURE_NO_WARNINGS 1 #include<iostream> using namespace std; typedef int STDataType; class Stack { public: Stack(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr == _a) { perror("malloc申请空间失败"); return; } _capacity = n; _top = 0; } ~Stack() { cout << "~Stack()" << endl; free(_a); _a = nullptr; _top = _capacity = 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; // ⽤最新加了构造和析构的C++版本Stack实现 bool isValid(const char* s) { Stack st; //之前这边需要初始化 while (*s) { if (*s == '[' || *s == '(' || *s == '{') { st.Push(*s); } else { // 右括号⽐左括号多,数量匹配问题 if (st.Empty()) { //这边需要销毁 return false; } // 栈⾥⾯取左括号 char top = st.Top(); st.Pop();// 顺序不匹配 if ((*s == ']' && top != '[') || (*s == '}' && top != '{') || (*s == ')' && top != '(')) { //这边也需要销毁 return false; } } ++s; } // 栈为空,返回真,说明数量都匹配 左括号多,右括号少匹配问题 //这边也需要销毁 return st.Empty(); }1.4 拷贝构造函数
作用:就是用一个已经存在的、同类型的对象(即已初始化的对象)来初始化一个正在被创建的新对象(即尚未初始化的对象)
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外的参数都有默认值,则此构造函数也叫做拷贝构造函数,也就是说拷贝构造是一个特殊的构造函数。
- 拷贝构造函数是构造函数的—个重载。
- 拷贝构造函数的第一个参数必须是 类类型对象的引用,使用 传值方式编译器直接报错, 也就是拷贝构造的参数如果不是引用会导致无穷递归(因为传值传参会调用拷贝构造)。拷贝构造函数也可以有多个参数,但是第一个参数必须是类类型对象的引用,后面的参数必须有缺省值。
- C++ 规定 自定义类型对象进行拷贝行为必须调用 拷贝构造,所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。
- 若 未显式定义拷贝构造,编译器会 自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成 值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用它的拷贝构造。
- 像 Date 这样的类成员变量全是 内置类型且 没有指向什么资源,编译器自动生成的拷贝构造就可以完成需要的拷贝,所以不需要我们显式实现拷贝构造。像 Stack 这样的类,虽然也都是内置类型,但是
_a指向了资源,编译器自动生成的拷贝构造完成的值拷贝/浅拷贝 不符合我们的需求,所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员,编译器自动生成的拷贝构造会调用 Stack 的拷贝构造,也不需要我们显式实现 MyQueue 的拷贝构造。这里还有一个小技巧:如果一个类显式实现了析构并释放资源,那么它就需要显式写拷贝构造,否则就不需要。 - 传值返回会产生一个临时对象,调用 拷贝构造; 传引用返回,返回的是返回对象的别名(引用), 没有产生拷贝。但是如果返回对象是一个当前 函数局部域的局部对象,函数结束就销毁了,那么使用 引用返回是有问题的,这时的引用相当于一个野引用,类似于一个野指针。传引用返回可以减少拷贝,但一定要确保返回对象在当前函数结束后仍然存在,才能使用引用返回。所以在正确性和性能选择上,优先选择正确性。其次才是性能。
#include<iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } // 编译报错:error C2652: “Date”: 非法的复制构造函数: 第一个参数不应是“Date” // Date(Date d) Date(const Date& d) { _year = d._year; _month = d._month; _day = d._day; } Date(Date* d) { _year = d->_year; _month = d->_month; _day = d->_day; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; void Func1(Date d) { cout << &d << endl; d.Print(); } // Date Func2() Date& Func2() { Date tmp(2024, 7, 5); tmp.Print(); return tmp; // 危险!返回局部变量的引用 } int main() { Date d1(2024, 7, 5); // C++规定自定义类型对象进行拷贝行为必须调用拷贝构造, // 所以这里的d1传值传参给d要调用拷贝构造完成拷贝, // 传引用传参可以减少这里的拷贝。 Func1(d1); cout << &d1 << endl; // 这里可以完成拷贝,但不是拷贝构造,只是一个普通的构造(接受指针) Date d2(&d1); d1.Print(); d2.Print(); // 这样写才是拷贝构造:通过同类型的对象初始化 Date d3(d1); d3.Print(); // 也可以这样写,这里也是拷贝构造 Date d4 = d1; d4.Print(); // Func2返回了一个局部对象tmp的引用作为返回值 // Func2函数结束,tmp对象就销毁了,相当于一个野引用 Date ret = Func2(); ret.Print(); return 0; }两个栈来实现一个队列
#include<iostream> using namespace std; typedef int STDataType; class Stack { public: Stack(int n = 4) { _a = (STDataType*)malloc(sizeof(STDataType) * n); if (nullptr == _a) { perror("malloc申请空间失败"); return; } _capacity = n; _top = 0; } Stack(const Stack& st) { // 需要对_a指向资源创建同样⼤的资源再拷贝值 _a = (STDataType*)malloc(sizeof(STDataType) * st._capacity); if (nullptr == _a) { perror("malloc申请空间失败!!!"); return; } memcpy(_a, st._a, sizeof(STDataType) * st._top); _top = st._top; _capacity = st._capacity; } void Push(STDataType x) { if (_top == _capacity) { int newcapacity = _capacity * 2; STDataType* tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType)); if (tmp == NULL) { perror("realloc fail"); return; } _a = tmp; _capacity = newcapacity; } _a[_top++] = x; } ~Stack() { cout << "~Stack()" << endl; free(_a); _a = nullptr; _top = _capacity = 0; } private: STDataType* _a; size_t _capacity; size_t _top; }; // 两个Stack实现队列 class MyQueue { public: private: Stack pushst; Stack popst; }; int main() { Stack st1; st1.Push(1); st1.Push(2); // Stack不显⽰实现拷贝构造,⽤⾃动⽣成的拷贝构造完成浅拷贝 // 会导致st1和st2⾥⾯的_a指针指向同⼀块资源,析构时会析构两次,程序崩溃 Stack st2 = st1; MyQueue mq1; // MyQueue⾃动⽣成的拷贝构造,会⾃动调⽤Stack拷贝构造完成pushst/popst // 的拷贝,只要Stack拷贝构造⾃⼰实现了深拷贝,他就没问题 MyQueue mq2 = mq1; return 0; }二、赋值运算符重载
2.1 运算符重载
- 当运算符被用于类类型的对象时,C++ 语言允许我们通过运算符重载的形式指定新的含义。C++ 规定类类型对象使用运算符时,必须转换成调用对应的运算符重载;若没有对应的运算符重载,则会编译报错。
- 运算符重载是具有特殊名字的函数,它的名字是由
operator和后面要定义的运算符共同构成。和其他函数一样,它也具有其返回类型、参数列表以及函数体。 - 重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数,二元运算符有两个参数;二元运算符的左侧运算对象传给第一个参数,右侧运算对象传给第二个参数。
- 如果一个重载运算符函数是成员函数,则它的第一个运算对象默认传给隐式的
this指针,因此运算符重载作为成员函数时,参数比运算对象少一个。 - 运算符重载以后,其优先级和结合性与对应的内置类型运算符保持一致。
- 不能通过连接语法中没有的符号来创建新的操作符:比如
operator@。 - 操作符至少有一个类类型参数,不能通过运算符重载改变内置类型对象的含义,如:
int operator+(int x, int y)。 - 一个类需要重载哪些运算符,是看哪些运算符重载后有意义。比如
Date类重载operator-就有意义(可以计算两个日期之间的天数差),但是重载operator+就没有意义(两个日期相加没有合理的语义)。 - 重载
++运算符时,有前置++和后置++,运算符重载函数名都是operator++,无法很好地区分。C++ 规定,后置++重载时,增加一个int形参,与前置++构成函数重载,方便区分。 - 重载
<<和>>时,需要重载为全局函数,因为如果重载为成员函数,this指针会默认抢占第一个形参位置,而第一个形参位置对应的是左侧运算对象,调用时就会变成对象 << cout,不符合使用习惯和可读性。重载为全局函数时,可以把ostream/istream放在第一个形参位置,第二个形参位置放类类型对象,从而实现如cout << 对象的自然语法。
Date.h
#pragma once #pragma once #include<iostream> using namespace std; #include<assert.h> class Date { //友元函数声明,使得类外面的函数可以使用类里面的private friend ostream& operator<<(ostream& out,const Date& d); friend istream& operator>>(istream& in, Date& d); public: bool CheckDate(); //默认构造——全缺省默认构造 Date(int year = 1900, int month = 1, int day = 1); //不改变成员变量的都可以把const加上 void Print()const; // const 修饰的是*this int GetMonthDay(int year, int month) { assert(_month > 0 && _month < 13); static int monthDayArray[13] = { -1,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 monthDayArray[month]; } 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); // d1 += 天数 Date& operator+=(int day); Date operator+(int day); // d1 -= 天数 Date& operator-=(int day); Date operator-(int day); //前置++和后置++ //d1++ = d1.operator++(0) Date operator++(int); //++d1 = d1.operator++(),后置++是正常的 Date operator++(); // d1 - d1 int operator-(const Date& d); ////流插入的运算符重载 //void operator<<(ostream& out); private: int _year; int _month; int _day; }; //流插入的运算符重载 ostream& operator<<(ostream& out,const Date& d); //流提取的运算符重载 istream& operator>>(istream& in, Date& d);Date.cpp
#define _CRT_SECURE_NO_WARNINGS 1 #include "Date.h" bool Date::CheckDate() { if (_month < 1 || _month > 12 || _day < 1 || _day > GetMonthDay(_year, _month)) { return false; } else { return true; } } //默认构造函数,用来初始化 Date::Date(int year, int month, int day) { _year = year; _month = month; _day = day; if (!CheckDate()) { cout << "非法日期:"; Print(); } } //打印日期 //void Date::Print(const Date* this)和下面那个等价 void Date::Print()const { cout << _year << "-" << _month << "-" << _day << endl; } //+= Date& Date::operator+=(int day) { //如果有些脑残这边day写了负数就炸缸了,所以这边还是要严谨一点 if (day < 0) { return *this -= (-day); } _day += day; while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); _month++; if (_month == 13) { _year++; _month = 1; } } return *this; } //+ Date Date::operator+(int day) { Date tmp = *this; tmp += day; return tmp; } // d1 -= 天数 Date& Date::operator-=(int day) { //这边和+=那边一样,所以测开的岗位也是很有必要的 if (day < 0) { return *this += (-day); } _day -= day; while (_day <= 0) { _month--; if (_month == 0) { _year--; _month = 12; } _day += GetMonthDay(_year, _month); } return *this; } // - Date Date::operator-(int day) { Date tmp = *this; tmp -= day; return tmp; } //< bool Date::operator<(const Date& d) { if (_year < d._year) { return true; } else if (_year == d._year) { if (_month < d._month) { return true; } else { if (_month == d._month) { return _day < d._day; } } } return false; } bool Date::operator==(const Date& d) { return _year == d._year && _month == d._month && _day == d._day; } bool Date::operator<=(const Date& d) { return *this < d || *this == d; } bool Date::operator>(const Date& d) { return !(*this <= d); } bool Date::operator>=(const Date& d) { return !(*this < d); } bool Date::operator!=(const Date& d) { return !(*this == d); } //前置++和后置++ //d1++ = d1.operator++(0) Date Date::operator++(int) { //前置返回++之前的值,所以需要拷贝一份出来 Date tmp = *this; *this += 1; return tmp; } //++d1 = d1.operator++(),后置++是正常的 Date Date::operator++() { *this += 1; return *this; } // d1 - d1 int Date::operator-(const Date& d) { int flag = 1; Date max = *this; Date min = d; if (max < min) { max = d; min = *this; flag = -1; } //到了这,就不用管谁正谁负了 int n = 0; while (max != min) { ++min; ++n; } return flag * n; } //流提取和流插入的运算符重载 //void Date::operator<<(ostream& out) //{ // out << _year << "年" << _month << "月" << _day << endl; //} ostream& operator<<(ostream& out,const Date& d) { out << d._year << "年" << d._month << "月" << d._day << "日" << endl; return out; } istream& operator>>(istream& in, Date& d) { while (1) { cout << "请依次输入年月日" << endl; in >> d._year >> d._month >> d._day; if (!d.CheckDate()) { cout << "输入错误!!" << endl; d.Print(); cout << "请重新输入!" << endl; } else { break; } } return in; }2.2 赋值运算符重载
赋值运算符重载是一个默认成员函数,用于完成两个已经存在的对象之间的拷贝赋值。这里要注意跟拷贝构造区分:拷贝构造用于用一个已存在的对象去初始化另一个正在创建的对象。
赋值运算符重载的特点:
- 赋值运算符重载是一个运算符重载,规定必须重载为成员函数。赋值运算符重载的参数建议写成
const当前类类型引用,否则会因传值传参而产生不必要的拷贝。 - 赋值运算符重载应有返回值,且建议返回当前类类型的引用。引用返回可以提高效率,而提供返回值的目的是为了支持连续赋值场景(如
a = b = c)。 - 没有显式实现时,编译器会自动生成一个默认赋值运算符重载。默认赋值运算符重载的行为跟默认拷贝构造函数类似:对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝),对自定义类型成员变量会调用其赋值运算符重载函数。
- 像
Date这样的类,成员变量全是内置类型且没有指向任何资源,编译器自动生成的赋值运算符重载就可以完成所需的拷贝,因此不需要我们显式实现赋值运算符重载。
像Stack这样的类,虽然成员也都是内置类型,但_a指向了动态分配的资源,编译器自动生成的赋值运算符重载执行的是值拷贝/浅拷贝,会导致多个对象共享同一块内存,析构时可能重复释放,因此不符合需求,需要我们自己实现深拷贝(即对指向的资源也进行拷贝)。
像MyQueue这样的类,内部主要包含自定义类型Stack成员,编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载,只要Stack的赋值行为正确,MyQueue就无需显式实现自己的赋值运算符重载。 - 这里还有一个小技巧:如果一个类显式实现了析构函数并释放资源,那么它通常也需要显式实现赋值运算符重载(以及拷贝构造函数);否则,可以依赖编译器生成的默认版本。
#include<iostream> using namespace std; class Date { public: Date(int year = 1, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } Date(const Date& d) { cout << " Date(const Date& d)" << endl; _year = d._year; _month = d._month; _day = d._day; } // 传引⽤返回减少拷贝 // d1 = d2; Date& operator=(const Date& d) { // 不要检查⾃⼰给⾃⼰赋值的情况 if (this != &d) { _year = d._year; _month = d._month; _day = d._day; } // d1 = d2表达式的返回对象应该为d1,也就是*this return *this; } void Print() { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { Date d1(2024, 7, 5); // 需要注意这⾥是拷贝构造,不是赋值重载 Date d2(d1); Date d3(2024, 7, 6); d1 = d3; // 请牢牢记住赋值重载完成两个已经存在的对象直接的拷贝赋值 // 拷贝构造⽤于⼀个对象拷贝初始化给另⼀个要创建的对象 Date d4 = d1; return 0; }三、取地址运算符重载
3.1 const成员函数
- 将
const修饰的成员函数称之为 const 成员函数,const修饰成员函数时应放在成员函数参数列表的后面。 const实际修饰的是该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员进行修改。当const修饰Date类的Print成员函数时,其隐含的this指针类型由Date* const this变为const Date* const this。
#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(const Date* const this) const void Print() const { cout << _year << "-" << _month << "-" << _day << endl; } private: int _year; int _month; int _day; }; int main() { // 这⾥⾮const对象也可以调⽤const成员函数是⼀种权限的缩⼩ Date d1(2024, 7, 5); d1.Print(); const Date d2(2024, 8, 5); d2.Print(); return 0; }3.2 取地址运算符重载
取地址运算符重载分为普通取地址运算符重载和 const 取地址运算符重载。一般情况下,这两个函数编译器自动生成的版本已经足够使用,不需要显式实现。除非在一些特殊场景下,例如我们不希望别人获取当前类对象的真实地址,就可以自行实现这两个运算符重载,并返回一个无效或伪装的地址。
class Date { public: Date* operator&() { return this; // return nullptr; } const Date* operator&() const { return this; // return nullptr; } private: int _year; // 年 int _month; // 月 int _day; // 日 };