类与对象(二)
依旧先来一手xmind思维导图类的6个默认成员函数如果一个类中什么成员都没有我们简称其为空类。但是空类中真的什么都没有吗其实不然任何一个类即使我们什么都不写类中也会自动生成6个默认成员函数。class Date {}; //空类注意这里的“默认”和“缺省”的意思差不多也就是你不写这6个函数编译器会自动生成你若是写了则编译器就不生成了。构造函数构造函数的概念构造函数名字与类名相同,创建类类型对象时由编译器自动调用保证每个数据成员都有 一个合适的初始值并且在对象的生命周期内只调用一次。例如以下日期类中的成员函数Date就是一个构造函数。当你用该日期类创建一个对象时编译器会自动调用该构造函数对新创建的变量进行初始化。注意构造函数的主要任务并不是开空间创建对象而是初始化对象。class Date { public: Date(int year 0, 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; };构造函数的特性一、构造函数的函数名与类名相同二、构造函数无返回值这里所说的构造函数无返回值是真的无返回值而不是说返回值为void。三、对象实例化时编译器自动调用对应的构造函数当你用类创建一个对象时编译器会自动调用该类的构造函数对新创建的变量进行初始化。四、构造函数支持重载这意味着你可以有多种初始化对象的方式编译器会根据你所传递的参数去调用对应的构造函数。五、无参的构造函数、全缺省的构造函数以及我们不写编译器自动生成的构造函数都称为默认构造函数并且默认构造函数只能有一个初学C时你可能认为只有当我们不写编译器自动生成的构造函数才被称为默认构造函数。其实并不是这样的以下3种都叫做默认构造函数1、我们不写编译器自动生成的构造函数。2、我们自己写的无参的构造函数。3、我们自己写的全缺省的构造函数。总而言之无需传参就可以调用的构造函数就是默认构造函数。说到这里你可能会想既然在我们不写的情况下编译器会自动生成一个构造函数那我们就没有必要自己写构造函数了。这种想法是不对的。看看以下代码#include iostream using namespace std; class Date { public: void Print() { cout _year 年 _month 月 _day 日 endl; } private: int _year; int _month; int _day; }; int main() { Date d1; // 编译器将调用自动生成的默认构造函数对d1进行初始化 d1.Print(); return 0; }最终d1当中的年月日都是随机值。到这里你可能会产生疑问d1对象调用了编译器自动生成的构造函数后d1对象的_year/_month/_day依旧是随机值那这编译器自动生成的构造函数还有什么意义编译器自动生成的构造函数机制1、编译器自动生成的构造函数对内置类型不做处理。2、对于自定义类型编译器会再去调用它们自己的默认构造函数。总结一下虽然在我们不写的情况下编译器会自动生成构造函数但是编译器自动生成的构造函数可能达不到我们想要的效果所以大多数情况下都需要我们自己写构造函数。析构函数析构函数的概念析构函数与构造函数功能相反析构函数负责完成对象的销毁对象在销毁时会自动调用析构函数完成类的一些资源清理工作。我们知道当一个类对象销毁时其中的局部变量也会随着该对象的销毁而销毁例如我们用日期类创建了一个对象d1当d1被销毁时对象d1当中的局部变量_year/_month/_day也会被编译器销毁。但是这并不意味着析构函数没有什么意义。像栈(Stack)这样的类对象当该对象被销毁时其中动态开辟的栈并不会随之被销毁需要我们对其进行空间释放这时析构函数的意义就体现了。析构函数的特性一、析构函数的函数名是在类名前加上字符‘~’class Date { public: Date()// 构造函数 {} ~Date()// 析构函数 {} private: int _year; int _month; int _day; };二、析构函数无参数无返回值析构函数所谓的无返回值也是真的无返回值而不是返回值为void。三、对象生命周期结束时C编译器会自动调用析构函数这就大大降低了C语言中栈空间忘记释放问题的发生因为当栈对象生命周期结束时C编译器会自动调用析构函数对其栈空间进行释放。四、一个类有且只有一个析构函数。若未显示定义系统会自动生成默认的析构函数编译器自动生成的析构函数机制1、编译器自动生成的析构函数对内置类型不做处理。2、对于自定义类型编译器会再去调用它们自己的默认析构函数。五、先构造的后析构后构造的先析构因为对象是定义在函数中的函数调用会建立栈帧栈帧中的对象构造和析构也要符合先进后出的原则。拷贝构造函数拷贝构造函数的概念拷贝构造函数只有单个形参该形参是对本类类型对象的引用一般常用从const修饰在用已存在的类类型对象创建新对象时由编译器自动调用。#include iostream using namespace std; class Date { public: Date(int year 0, 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(2021, 5, 31); Date d2(d1); // 用已存在的对象d1创建对象d2 return 0; }拷贝构造函数的特性一、拷贝构造函数是构造函数的一个重载形式因为拷贝构造函数的函数名也与类名相同。二、拷贝构造函数的参数只有一个且必须使用引用传参使用传值方式会引发无穷递归调用要调用拷贝构造函数就需要先传参若传参使用传值传参那么在传参过程中又需要进行对象的拷贝构造如此循环往复最终引发无穷递归调用。自定义类型的对象进行函数传参时一般推荐使用引用传参。使用传值传参也可以但每次传参时都会调用拷贝构造函数。三、若未显示定义拷贝构造函数系统将生成默认的拷贝构造函数看看以下代码#include iostream using namespace std; class Date { public: Date(int year 0, 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() { Date d1(2021, 5, 30); Date d2(d1); // 用已存在的对象d1创建对象d2 d1.Print(); d2.Print(); return 0; }代码中我们自己并没有定义拷贝构造函数但编译器自动生成的拷贝构造函数最终还是完成了对象的拷贝构造。编译器自动生成的拷贝构造函数机制1、编译器自动生成的拷贝构造函数对内置类型会完成浅拷贝值拷贝。2、对于自定义类型编译器会再去调用它们自己的默认拷贝构造函数。四、编译器自动生成的拷贝构造函数不能实现深拷贝上面说到编译器自动生成的拷贝构造函数会对内置类型完成浅拷贝。对于以下这句代码浅拷贝实际上就是将d1的内容完完全全的复制了一份拷贝给d2所以说浅拷贝也叫做值拷贝。Date d2(d1);// 用已存在的对象d1创建对象d2但某些场景下浅拷贝并不能达到我们想要的效果。例如栈(Stack)这样的类编译器自动生成的拷贝构造函数就不能满足我们的需求了Stack s1; Stack s2(s1);// 用已存在的对象s1创建对象s2代码中我们的本意是用已存在的对象s1创建对象s2但编译器自动生成的拷贝构造函数完成的是浅拷贝拷贝出来的对象s2将不能满足我们的要求。举个例子现有以下栈(Stack)类class Stack { public: Stack(int capacity 4) { _ps (int*)malloc(sizeof(int)* capacity); _size 0; _capacity capacity; } void Print() { cout _ps endl;// 打印栈空间地址 } private: int* _ps; int _size; int _capacity; };我们可以看到类中没有自己定义拷贝构造函数那么当我们用已存在的对象来创建另一个对象时将调用编译器自动生成的拷贝构造函数。看看以下代码运行结果int main() { Stack s1; s1.Print();// 打印s1栈空间的地址 Stack s2(s1);// 用已存在的对象s1创建对象s2 s2.Print();// 打印s2栈空间的地址 return 0; }结果打印s1栈和s2栈空间的地址相同这就意味着就算在创建完s2栈后我们对s1栈做的任何操作都会直接影响到s2栈。这是我们想要的效果吗显然不是我们希望在创建时s2栈和s1栈中的数据是相同的且创建完s2栈后我们对s1栈和s2栈之间的任何操作能够互不影响。而且这种情况下还会出现对同一块空间释放多次的问题。若我们自己定义的析构函数是正确的情况下当程序运行结束s2栈将被析构此时那块栈空间被释放然后s1栈也要被析构再次对那一块空间进行释放。可以看到这种情况下编译器自动生成的拷贝构造函数就不能满足我们的要求了。总结一下1、像Date这样的类需要的就是浅拷贝那么编译器自动生成的拷贝构造函数就够用了我们不需要自己写。2、像Stack这样的类浅拷贝会导致析构两次、程序崩溃等问题需要我们自己写对应的拷贝构造函数。赋值运算符重载运算符重载C为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数其目的就是让自定义类型可以像内置类型一样可以直接使用运算符进行操作。d1 d2;// 可读性高书写简单IsSame(d1, d2);// 可读性差书写麻烦运算符重载函数也具有自己的返回值类型函数名字以及参数列表。其返回值类型和参数列表与普通函数类似。运算符重载函数名为关键字operator后面接需要重载的操作符符号。函数原型返回值 operator运算符(参数列表)注意1.不能通过连接其他符号来创建新的操作符比如operator。2.重载操作符必须有一个类类型或枚举类型的操作数。3.用于内置类型的操作符重载后其含义不能改变。4.作为类成员的重载函数时函数有一个默认的形参this限定为第一个形参。5.sizeof 、:: 、.* 、?: 、. 这5个运算符不能重载。这里以重载 运算符作为例子我们可以将该运算符重载函数作为类的一个成员函数此时该函数的第一个形参默认为this指针。class Date { public: Date(int year 0, int month 1, int day 1) { _year year; _month month; _day day; } void Print() { cout _year 年 _month 月 _day 日 endl; } bool operator(const Date d)// 运算符重载函数 { return _year d._year _month d._month _day d._day; } private: int _year; int _month; int _day; };也可以将该运算符重载函数放在类外面但此时外部无法访问类中的成员变量这时我们可以将类中的成员变量设置为共有(public)这样外部就可以访问该类的成员变量了也可以用友元函数解决该问题。并且在类外没有this指针所以此时函数的形参我们必须显示的设置两个。class Date { public: Date(int year 0, int month 1, int day 1) { _year year; _month month; _day day; } void Print() { cout _year 年 _month 月 _day 日 endl; } 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; }赋值运算符重载这里以重载 运算符作为例子class Date { public: Date(int year 0, int month 1, int day 1)// 构造函数 { _year year; _month month; _day day; } Date operator(const Date d)// 赋值运算符重载函数 { if (this ! d) { _year d._year; _month d._month; _day d._day; } return *this; } void Print()// 打印函数 { cout _year 年 _month 月 _day 日 endl; } private: int _year; int _month; int _day; };重载赋值运算符需要注意以下几点一、参数类型设置为引用并用const进行修饰赋值运算符重载函数的第一个形参默认是this指针第二个形参是我们赋值运算符的右操作数。由于是自定义类型传参我们若是使用传值传参会额外调用一次拷贝构造函数所以函数的第二个参数最好使用引用传参第一个参数是默认的this指针我们管不了。其次第二个参数即赋值运算符的右操作数我们在函数体内不会对其进行修改所以最好加上const进行修饰。二、函数的返回值使用引用返回实际上我们若是只以d2 d1这种方式使用赋值运算符赋值运算符重载函数就没必要有返回值因为在函数体内已经通过this指针对d2进行了修改。但是为了支持连续赋值即d3 d2 d1我们就需要为函数设置一个返回值了而且很明显返回值应该是赋值运算符的左操作数即this指针指向的对象。和使用引用传参的道理一样为了避免不必要的拷贝我们最好还是使用引用返回因为此时出了函数作用域this指针指向的对象并没有被销毁所以可以使用引用返回。三、赋值前检查是否是给自己赋值若是出现d1 d1我们不必进行赋值操作因为自己赋值给自己是没有必要进行的。所以在进行赋值操作前可以先判断是否是给自己赋值避免不必要的赋值操作。四、引用返回的是*this赋值操作进行完毕时我们应该返回赋值运算符的左操作数而在函数体内我们只能通过this指针访问到左操作数所以要返回左操作数就只能返回*this。五、一个类如果没有显示定义赋值运算符重载编译器也会自动生成一个完成对象按字节序的值拷贝没错赋值运算符重载编译器也可以自动生成并且也是支持连续赋值的。但是编译器自动生成的赋值运算符重载完成的是对象按字节序的值拷贝例如d2 d1编译器会将d1所占内存空间的值完完全全地拷贝到d2的内存空间中去类似于memcpy。对于日期类编译器自动生成的赋值运算符重载函数就可以满足我们的需求我们可以不用自己写。但是这也不意味着所有的类都不用我们自己写赋值运算符重载函数当遇到一些特殊的类我们还是得自己动手写赋值运算符函数的。注意区别以下代码所调用的函数Date d1(2021, 6, 1); Date d2(d1); Date d3 d1;这里一个三句代码我们现在都知道第二句代码调用的是拷贝构造函数那么第三句代码呢调用的是哪一个函数是赋值运算符重载函数吗其实第三句代码调用的也是拷贝构造函数注意区分拷贝构造函数和赋值运算符重载函数的使用场景拷贝构造函数用一个已经存在的对象去构造初始化另一个即将创建的对象。赋值运算符重载函数在两个对象都已经存在的情况下将一个对象赋值给另一个对象。const成员const修饰类的成员函数我们将const修饰的类成员函数称之为const成员函数const修饰类成员函数实际修饰的是类成员函数隐含的this指针表明在该成员函数中不能对this指针指向的对象进行修改。例如我们可以对类成员函数中的打印函数进行const修饰避免在函数体内不小心修改了对象void Print()const// cosnt修饰的打印函数 { cout _year 年 _month 月 _day 日 endl; }思考下面几个问题经典面试题1.const对象可以调用非const成员函数吗2.非const对象可以调用const成员函数吗3.const成员函数内可以调用其他的非const成员函数吗4.非cosnt成员函数内可以调用其他的cosnt成员函数吗答案是不可以、可以、不可以、可以解释如下1.非const成员函数即成员函数的this指针没有被const所修饰我们传入一个被const修饰的对象用没有被const修饰的this指针进行接收属于权限的放大函数调用失败。2.const成员函数即成员函数的this指针被const所修饰我们传入一个没有被const修饰的对象用被const修饰的this指针进行接收属于权限的缩小函数调用成功。3.在一个被const所修饰的成员函数中调用其他没有被const所修饰的成员函数也就是将一个被const修饰的this指针的值赋值给一个没有被const修饰的this指针属于权限的放大函数调用失败。4.在一个没有被const所修饰的成员函数中调用其他被const所修饰的成员函数也就是将一个没有被const修饰的this指针的值赋值给一个被const修饰的this指针属于权限的缩小函数调用成功。取地址及const取地址操作符重载取地址操作符重载和const取地址操作符重载这两个默认成员函数一般不用自己重新定义使用编译器自动生成的就行了class Date { public: Date* operator()// 取地址操作符重载 { return this; } const Date* operator()const// const取地址操作符重载 { return this; } private: int _year; int _month; int _day; };日期类的实现在学习了C的6个默认成员函数后我们现在动手实现一个完整的日期类来加强对这6个默认成员函数的认识。这是日期类中所包含的成员函数和成员变量class Date { public: // 构造函数 Date(int year 0, int month 1, int day 1); // 打印函数 void Print() const; // 日期天数 Date operator(int day); // 日期天数 Date operator(int day) const; // 日期-天数 Date operator-(int day); // 日期-天数 Date operator-(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; bool operator!(const Date d) const; // 日期-日期 int operator-(const Date d) const; // 析构拷贝构造赋值重载可以不写使用默认生成的即可 private: int _year; int _month; int _day; };构造函数进入构造函数体首先需要检查日期的合法性只有当日期合法时才能进行后续的构造操作。// 获取某年某月的天数 inline int GetMonthDay(int year, int month) { // 数组存储平年每个月的天数 static int dayArray[13] { 0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; int day dayArray[month]; if (month 2 ((year % 4 0 year % 100 ! 0) || (year % 400 0))) { //闰年2月的天数 day 29; } return day; } // 构造函数 Date::Date(int year, int month, int day) { // 检查日期的合法性 if (year 0 month 1 month 12 day 1 day GetMonthDay(year, month)) { _year year; _month month; _day day; } else { // 严格来说抛异常更好 cout 非法日期 endl; cout year 年 month 月 day 日 endl; } }GetMonthDay函数中的三个细节1.该函数可能被多次调用所以我们最好将其设置为内联函数。2.函数中存储每月天数的数组最好是用static修饰存储在静态区避免每次调用该函数都需要重新开辟数组。3.逻辑与应该先判断month 2是否为真因为当不是2月的时候我们不必判断是不是闰年。注意当函数声明和定义分开时在声明时注明缺省参数定义时不标出缺省参数。打印函数这个不用说了相当简单。// 打印函数 void Date::Print() const { cout _year 年 _month 月 _day 日 endl; }日期 天数对于运算符我们先将需要加的天数加到日上面然后判断日期是否合法若不合法则通过不断调整直到日期合法为止。调整日期的思路1.若日已满则日减去当前月的天数月加一。2.若月已满则将年加一月置为1。反复执行1和2直到日期合法为止。注当需要加的天数为负数时转而调用-运算符重载函数。// 日期天数 Date Date::operator(int day) { if (day0) { // 复用operator- *this - -day; } else { _day day; // 日期不合法通过不断调整直到最后日期合法为止 while (_day GetMonthDay(_year, _month)) { _day - GetMonthDay(_year, _month); _month; if (_month 12) { _year; _month 1; } } } return *this; }日期 天数运算符的重载我们可以复用上面已经实现的运算符的重载函数。但是要注意虽然我们返回的是加了之后的值但是对象本身的值并没有改变。就像a b 1b 1的返回值是b 1但是b的值并没有改变。所以我们还可以用const对该函数进行修饰防止函数内部改变了this指针指向的对象。// 日期天数 Date Date::operator(int day) const { Date tmp(*this);// 拷贝构造tmp用于返回 // 复用operator tmp day; return tmp; }注意运算符的重载函数采用的是引用返回因为出了函数作用域this指针指向的对象没有被销毁。但运算符的重载函数的返回值只能是传值返回因为出了函数作用域对象tmp就被销毁了不能使用引用返回。日期 - 天数对于-运算符我们先用日减去需要减的天数然后判断日期是否合法若不合法则通过不断调整直到日期合法为止。调整日期的思路1.若日为负数则月减一。2.若月为0则年减一月置为12。3.日加上当前月的天数。反复执行1、2和3直到日期合法为止。// 日期-天数 Date Date::operator-(int day) { if (day 0) { // 复用operator *this -day; } else { _day - day; // 日期不合法通过不断调整直到最后日期合法为止 while (_day 0) { _month--; if (_month 0) { _year--; _month 12; } _day GetMonthDay(_year, _month); } } return *this; }日期 - 天数和运算符的重载类似我们可以复用上面已经实现的-运算符的重载函数而且最好用const对该函数进行修饰防止函数内部改变了this指针指向的对象。// 日期-天数 Date Date::operator-(int day) const { Date tmp(*this);// 拷贝构造tmp用于返回 // 复用operator- tmp - day; return tmp; }注当需要减的天数为负数时转而调用运算符重载函数。日期 - 天数和运算符的重载类似我们可以复用上面已经实现的-运算符的重载函数而且最好用const对该函数进行修饰防止函数内部改变了this指针指向的对象。// 日期-天数 Date Date::operator-(int day) const { Date tmp(*this);// 拷贝构造tmp用于返回 // 复用operator- tmp - day; return tmp; }注意-运算符的重载函数采用的是引用返回但-运算符的重载函数的返回值只能是传值返回也是由于-运算符重载函数中的tmp对象出了函数作用域被销毁了所以不能使用引用返回。前置 前置我们可以复用运算符的重载函数。// 前置 Date Date::operator() { // 复用operator *this 1; return *this; }后置 由于前置和后置的运算符均为为了区分它们的运算符重载我们给后置的运算符重载的参数加上一个int型参数使用后置时不需要给这个int参数传入实参因为这里int参数的作用只是为了跟前置构成重载// 后置 Date Date::operator(int) { Date tmp(*this);// 拷贝构造tmp用于返回 // 复用operator *this 1; return tmp; }注意后置也是需要返回加了之前的值只能先用对象tmp保存之前的值然后再然对象加一最后返回tmp对象。由于tmp对象出了该函数作用域就被销毁了所以后置只能使用传值返回而前置可以使用引用返回。后置 由于前置和后置的运算符均为为了区分它们的运算符重载我们给后置的运算符重载的参数加上一个int型参数使用后置时不需要给这个int参数传入实参因为这里int参数的作用只是为了跟前置构成重载。// 后置 Date Date::operator(int) { Date tmp(*this);// 拷贝构造tmp用于返回 // 复用operator *this 1; return tmp; }注意后置也是需要返回加了之前的值只能先用对象tmp保存之前的值然后再然对象加一最后返回tmp对象。由于tmp对象出了该函数作用域就被销毁了所以后置只能使用传值返回而前置可以使用引用返回。前置 –前置–我们也是可以复用前面的-运算符的重载函数。// 前置-- Date Date::operator--() { // 复用operator- *this - 1; return *this; }后置–后置–需要注意的事项和后置是一样的我这里就不过多阐述了。// 后置-- Date Date::operator--(int) { Date tmp(*this);// 拷贝构造tmp用于返回 // 复用operator- *this - 1; return tmp; }日期类的大小关系比较日期类的大小关系比较需要重载的运算符看起来有6个实际上我们只用实现两个就可以了然后其他的通过复用这两个就可以实现。注意进行日期的大小比较我们并不会改变传入对象的值所以这6个运算符重载函数都应该被const所修饰。运算符的重载运算符的重载很简单先判断年是否大于再判断月是否大于最后判断日是否大于这其中有一者为真则函数返回true否则返回false。bool Date::operator(const Date d) const { if (_year d._year) { return true; } else if (_year d._year) { if (_month d._month) { return true; } else if (_month d._month) { if (_day d._day) { return true; } } } return false; }运算符的重载运算符的重载也是很简单年月日均相等则为真。bool Date::operator(const Date d) const { return _year d._year _month d._month _day d._day; }运算符的重载即大于或者等于满足其中之一即可。 bool Date::operator(const Date d) const { return *this d || *this d; }运算符的重载大于等于的反面即是小于。bool Date::operator(const Date d) const { return !(*this d); }运算符的重载大于的返回即是小于等于。bool Date::operator(const Date d) const { return !(*this d); }!运算符的重载!等于的反面即是不等于。bool Date::operator!(const Date d) const { return !(*this d); }日期 - 日期日期 - 日期即计算传入的两个日期相差的天数。我们只需要让较小的日期的天数一直加一直到最后和较大的日期相等即可这个过程中较小日期所加的总天数便是这两个日期之间差值的绝对值。若是第一个日期大于第二个日期则返回这个差值的正值若第一个日期小于第二个日期则返回这个差值的负值。// 日期-日期 int Date::operator-(const Date d) const { 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; }代码中使用flag变量标记返回值的正负flag为1代表返回的是正值flag为-1代表返回的是负值最后返回总天数与flag相乘之后的值即可。