一、类的默认成员函数用户没有显式实现的就是成员默认函数编译器会自动生成的成员函数称为默认成员函数。 一个类不写的情况下会默认生成以下六个默认成员函数。二、构造函数构造函数是特殊的成员函数需要注意的是构造函数虽然名称叫构造但是构造函数的 主要任务并不开空间创建对象而是对象实例化时初始化对象。构造函数的本质是要替代 我们以前Stack和Date类中写的Init函数的功能构造函数自动调用的特点就完美取代了 Init。构造函数的特点 1.函数名与类名相同。 2.无返回值。(返回值啥都不需要给也不需要写void) 3.对象实例化时系统会自动调用对应的构造函数。 4.构造函数可以重载。 5.如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦用户显 式定义编译器将不再生成。 6.无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数都叫做默认构造函 数。但是这三个函数有且只有一个存在不能同时存在。无参构造函数和全缺省构造函数虽然构成 函数重载但是调用时会存在歧义。要注意很多人会认为默认构造函数是编译器默认生成那个叫 默认构造实际上无参构造函数、全缺省构造函数也是默认构造总结一下就是不传实参就可以调 用的构造就叫默认构造。 7.我们不写编译器默认生成的构造对内置类型成员变量的初始化没有要求也就是说是是否初始 化是不确定的看编译器。对于自定义类型成员变量要求调用这个成员变量的默认构造函数初始 化。如果这个成员变量没有默认构造函数那么就会报错我们要初始化这个成员变量需要用 初始化列表才能解决。#includeiostream using namespace std; class Date { public: // 1 .无参构造函数 Date() { _year 1; _month 1; _day 1; } // 2 .带参构造函数 Date(int year, int month, int day) { _year year; _month month; _day day; } // 3 .全缺省构造函数 /*Date(int year 1 , 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() { // 如果留下三个构造中的第二个带参构造第一个和第三个注释掉 // 编译报错error C2512 :“Date”: 没有合适的默认构造函数可用 Date d1; // 调用默认构造函数 Date d2(2025, 1, 1); // 调用带参的构造函数 // 注意如果通过无参构造函数创建对象时对象后面不用跟括号否则编译器无法 // 区分这里是函数声明还是实例化对象 //warning C4930 :“Date d3(void)”: 未调用原型函数(是否是有意用变量定义的?) Date d3(); d1.Print(); d2.Print(); return 0; }三、析构函数析构函数与构造函数功能相反析构函数不是完成对对象本身的销毁比如局部对象是存在栈帧的 函数结束栈帧销毁他就释放了不需要我们管C规定对象在销毁时会自动调用析构函数完成对 象中资源的清理释放工作。析构函数的功能类比我们之前Stack实现的Destroy功能而像Date没有 Destroy其实就是没有资源需要释放所以严格说Date是不需要析构函数的。析构函数的特点 1.析构函数名是在类名前加上字符~。 2.无参数无返回值。(这里跟构造类似也不需要加void) 3.一个类只能有一个析构函数。若未显式定义系统会自动生成默认的析构函数。 4.对象生命周期结束时系统会自动调用析构函数。 5.跟构造函数类似我们不写编译器自动生成的析构函数对内置类型成员不做处理自定类型成员会 调用他的析构函数。 6.还需要注意的是我们显示写析构函数对于自定义类型成员也会调用他的析构也就是说自定义类 型成员无论什么情况都会自动调用析构函数。 7.如果类中没有申请资源时析构函数可以不写直接使用编译器生成的默认析构函数如Date如 果默认生成的析构就可以用也就不需要显示写析构如MyQueue但是有资源申请时一定要 自己写析构否则会造成资源泄漏如Stack。 8.一个局部域的多个对象C规定后定义的先析构。对比一下用C和C实现的Stack解决之前括号匹配问题isValid。// 用之前C版本Stack实现 bool isValid(const char* s) { ST st; STInit(st); while (*s) { // 左括号入栈 if (*s ( || *s [ || *s {) { STPush(st, *s); } else // 右括号取栈顶左括号尝试匹配 { if (STEmpty(st)) { STDestroy(st); return false; } char top STTop(st); STPop(st); // 不匹配 if ((top ( *s ! )) || (top { *s ! }) || (top [ *s ! ])) { STDestroy(st); return false; } } s; } // 栈不为空说明左括号比右括号多数量不匹配 bool ret STEmpty(st); STDestroy(st); return ret; } int main() { cout isValid([()][]) endl; cout isValid([(])[]) endl; return 0; }//用最新加了构造和析构的C版本Stack实现 #includeiostream using namespace std; 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.拷贝构造函数是构造函数的一个重载。 2.拷贝构造函数的第一个参数必须是类类型对象的引用使用传值方式编译器直接报错因为语法逻 辑上会引发无穷递归调用。拷贝构造函数也可以多个参数但是第一个参数必须是类类型对象的引 用后面的参数必须有缺省值。 3.C规定自定义类型对象进行拷贝行为必须调用拷贝构造所以这里自定义类型传值传参和传值返 回都会调用拷贝构造完成。 4.若未显式定义拷贝构造编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成 员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝)对自定义类型成员变量会调用他的拷贝构 造。 5.像Date这样的类成员变量全是内置类型且没有指向什么资源编译器自动生成的拷贝构造就可以完 成需要的拷贝所以不需要我们显示实现拷贝构造。像Stack这样的类虽然也都是内置类型但 是_a指向了资源编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求所以需要 我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部主要是自定义类型 Stack成员编译器自动生成的拷贝构造会调用Stack的拷贝构造也不需要我们显示实现 MyQueue的拷贝构造。这里还有一个小技巧如果一个类显示实现了析构并释放资源那么他就 需要显示写拷贝构造否则就不需要。 6.传值返回会产生一个临时对象调用拷贝构造传值引用返回返回的是返回对象的别名(引用)没 有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象函数结束就销毁了那么使用 引用返回是有问题的这时的引用相当于一个野引用类似一个野指针一样。传引用返回可以减少 拷贝但是一定要确保返回对象在当前函数结束后还在才能用引用返回。#includeiostream 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); d2.Print(); // 也可以这样写这里也是拷贝构造 Date d4 d1; d2.Print(); // Func2返回了一个局部对象tmp的引用作为返回值 // Func2函数结束tmp对象就销毁了相当于了一个野引用 Date ret Func2(); ret.Print(); return 0; }五、赋值运算符重载5.1 运算符重载1、当运算符被用于类类型的对象时C语言允许我们通过运算符重载的形式指定新的含义。C规 定类类型对象使用运算符时必须转换成调用对应运算符重载若没有对应的运算符重载则会编 译报错。 2、运算符重载是具有特殊名字的函数他的名字是由operator和后面要定义的运算符共同构成。和其 他函数一样它也具有其返回类型和参数列表以及函数体。 3、重载运算符函数的参数个数和该运算符作用的运算对象数量一样多。一元运算符有一个参数二元 运算符有两个参数二元运算符的左侧运算对象传给第一个参数右侧运算对象传给第二个参数。 4、如果一个重载运算符函数是成员函数则它的第一个运算对象默认传给隐式的this指针因此运算 符重载作为成员函数时参数比运算对象少一个。 5、运算符重载以后其优先级和结合性与对应的内置类型运算符保持一致。 6、不能通过连接语法中没有的符号来创建新的操作符比如operator。 7、.* :: sizeof ?: . 注意以上5个运算符不能重载。 8、重载操作符至少有一个类类型参数不能通过运算符重载改变内置类型对象的含义如int operator(int x , int y) 9、一个类需要重载哪些运算符是看哪些运算符重载后有意义比如Date类重载operator-就有意 义但是重载operator就没有意义。 10、重载运算符时有前置和后置运算符重载函数名都是operator无法很好的区分。 C规定后置重载时增加一个int形参跟前置构成函数重载方便区分。 11、重载和时需要重载为全局函数因为重载为成员函数this指针默认抢占了第一个形参位 置第一个形参位置是左侧运算对象调用时就变成了对象cout不符合使用习惯和可读性。 重载为全局函数把ostream/istream放到第一个形参位置就可以了第二个形参位置当类类型对 象。class Date { public: Date(int year 1, int month 1, int day 1) { cout Date(int year 1, int month 1, int day 1) endl; _year year; _month month; _day day; } Date(Date d) { _year d._year; _month d._month; _day d._day; } bool operator(const Date x) { return _year x._year _month x._month _day x._day; } int operator-(const Date d) { } //语法上可以但是实践上没有意义 //int operator-(const Date d) //有意义 //Date operator(int day) private: int _year; int _month; int _day; }; //bool operator(const Date x1,const Date x2) //{ // return x1._year x2._year // x1._month x2._month // x1._day x2._day; //} int main() { //int i 1, j 2; ////bool ret1 i j; //cout (i j) endl; //i; Date d1(2026, 8, 28); Date d2(2026, 8, 30); cout (d1 d2) endl; return 0; }5.2 赋值运算符重载赋值运算符重载是一个默认成员函数用于完成两个已经存在的对象直接的拷贝赋值这里要注意跟 拷贝构造区分拷贝构造用于一个对象拷贝初始化给另一个要创建的对象。 赋值运算符重载的特点 1.赋值运算符重载是一个运算符重载规定必须重载为成员函数。赋值运算重载的参数建议写成 const 当前类类型引用否则会传值传参会有拷贝 2.有返回值且建议写成当前类类型引用引用返回可以提高效率有返回值目的是为了支持连续赋 值场景。 3.没有显式实现时编译器会自动生成一个默认赋值运算符重载默认赋值运算符重载行为跟默认拷 贝构造函数类似对内置类型成员变量会完成值拷贝/浅拷贝(一个字节一个字节的拷贝)对自定义 类型成员变量会调用他的赋值重载函数。 4.像Date这样的类成员变量全是内置类型且没有指向什么资源编译器自动生成的赋值运算符重载就 可以完成需要的拷贝所以不需要我们显示实现赋值运算符重载。像Stack这样的类虽然也都是 内置类型但是_a指向了资源编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我 们的需求所以需要我们自己实现深拷贝(对指向的资源也进行拷贝)。像MyQueue这样的类型内部 主要是自定义类型Stack成员编译器自动生成的赋值运算符重载会调用Stack的赋值运算符重载 也不需要我们显示实现MyQueue的赋值运算符重载。这里还有一个小技巧如果一个类显示实现 了析构并释放资源那么他就需要显示写赋值运算符重载否则就不需要。5.3 日期类的实现//Date.h #pragma once #includeassert.h #includeiostream 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) { 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 year % 100 ! 0) || (year % 400 0))) return 29; else return monthDayArray[month]; } void Print(); bool CheckDate(); Date(int year 1, int month 1, int day 1); Date operator(int day); Date operator(int day); Date operator-(int day); Date operator-(int day); //d1 Date operator(); //d1 Date operator(int); //--d1 Date operator--(); //d1-- Date operator--(int); 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; }; ostream operator(ostream out, const Date d); istream operator(istream in, Date d);//Date.cpp #define _CRT_SECURE_NO_WARNINGS #includeDate.h void Date::Print() { cout _year / _month / _day endl; } Date::Date(int year, int month, int day) { _yearyear; _monthmonth; _dayday; if (!CheckDate()) { cout 非法日期- *this; } } Date Date::operator(int day) { if (day 0) { return *this - -day; } _day day; //进位 while (_dayGetMonthDay(_year,_month)) { _day - GetMonthDay(_year, _month); _month; if (_month 13) { _month 1; _year; } } return *this; } Date Date::operator(int day) { Date tmp(*this); tmp._day day; //进位 while (tmp._day GetMonthDay(tmp._year, tmp._month)) { tmp._day - GetMonthDay(tmp._year, tmp._month); tmp._month; if (tmp._month 13) { tmp._month 1; tmp._year; } } return tmp; } Date Date::operator-(int day) { _day - day; while (_day 0) { --_month; if (_month 0) { _month 12; --_year; } _day GetMonthDay(_year, _month); } return *this; } Date Date::operator-(int day) { Date tmp(*this); tmp - day; return tmp; } //d1 Date Date::operator() { *this 1; return *this; } //d1 Date Date::operator(int) { Date tmp(*this); *this 1; return tmp; } //--d1 Date Date::operator--() { *this - 1; return *this; } //d1-- Date Date::operator--(int) { Date tmp(*this); *this - 1; return tmp; } 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); } bool Date::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; } 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); } int Date::operator-(const Date d) { Date max *this; Date min d; int flag 1; if (*this d) { max d; min *this; 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 日 endl; return out; } istream operator(istream in, Date d) { while (1) { cout 请依次输入年月日:; in d._year d._month d._day; if (d.CheckDate()) { break; } else { cout非法日期请重新输入endl; } } return in; } bool Date::CheckDate() { if (_month 1 || _month12 || _day1 || _dayGetMonthDay(_year, _month)) { return false; } else { return true; } }//test.cpp #includeDate.h void TestDate1() { Date d1(2026, 8, 30); Date d2 d1 1000; d1.Print(); d2.Print(); Date d3(2026, 8, 30); Date d4 d3 1000; d3.Print(); d4.Print(); } void TestDate2() { Date d1(2026, 8, 30); Date d2 d1 - 1000; d1.Print(); d2.Print(); Date d3(2026, 8, 30); Date d4 d3 - 1000; d3.Print(); d4.Print(); Date d5(2026, 8, 30); d5 -100; d5.Print(); } void TestDate3() { Date d1(2026, 8, 30); Date d2 d1; d1.Print(); d2.Print(); Date d3(2026, 8, 30); Date d4 d3; d3.Print(); d4.Print(); } void TestDate4() { Date d1(2025, 4, 26); Date d2(2025, 7, 28); cout (d1 d2) endl; cout (d2 d1) endl; cout (d1 d2) endl; cout d1 - d2 endl; } void TestDate5() { Date d1(2025, 4, 26); Date d2(2025, 7, 28); //流插入 coutd1;//operator operator(cout, d1); //d1.operator(cout); //int i 1; //double d 2.2; //cout i; //cout d; //cout i d endl; //cout.operator(i); //cout.operator(d); cin d1 d2; cout d1d2; } int main() { //TestDate1(); //TestDate2(); //TestDate3(); //TestDate4(); TestDate5(); return 0; }