--类和对象)
一、类的默认成员函数类的默认成员函数就是用户没有显式实现编译器会自动生成的成员函数。一般情况下一个类如果不写任何成员函数编译器会默认生成 6 个函数其中前四个比较重要。1. 构造函数和析构函数构造函数和析构函数的作用是完成对象的初始化以及清理对象申请的资源。类似于我们之前写的初始化栈和销毁栈。注意构造和析构虽然听起来像创建对象和销毁对象但并不是。构造函数的功能并不是分配空间创建对象而是完成对象实例化时的初始化析构函数也不是销毁对象而是释放对象申请的空间。我们常使用的局部对象在函数栈帧创建时空间就分配好了函数结束栈帧销毁对象也释放了不需要我们管。1构造函数构造函数的特点函数名与类名相同没有返回值连 void 也不需要写对象实例化时系统会自动调用构造函数如果类中没有显式定义构造函数那么 C 编译器会自动生成一个无参的默认构造函数。一旦用户显式定义编译器将不再自动生成。无参构造函数、全缺省构造函数、以及我们不写时编译器自动生成的构造函数都称为默认构造函数。但这三个函数只能存在一个不能同时存在。无参构造函数和全缺省构造函数虽然构成函数重载但在调用时会产生歧义。编译器自动生成的构造函数对内置类型没有初始化要求对自定义类型编译器会调用自定义类型的构造函数。如果自定义类型没有默认构造函数那么会报错我们需要用初始化列表来初始化这个对象以后会讲到。// 无参构造函数 Date() { _year 1; _month 1; _day 1; } // 带参构造函数 Date(int year, int month, int day) { _year year; _month month; _day day; } // 全缺省构造函数 Date(int year 1, int month 1, int day 1) { _year year; _month month; _day day; }编译器默认生成的构造函数对内置类型的处理2析构函数析构函数的特点函数名为~类名不需要返回值连 void 也不需要写对象生命周期结束时系统会自动调用析构函数如果类中没有显式定义析构函数那么 C 编译器会自动生成一个析构函数。一旦用户显式定义编译器将不再自动生成。析构函数对内置类型不做处理对自定义类型则会调用自定义类型的析构函数。我们显式写析构函数对于自定义类型编译器也会自动调用它的析构函数。当类中没有需要释放的资源时编译器自动生成的析构函数就够用不需要我们显式定义。一个局部域的多个对象C 规定后定义的先析构。3拷贝构造函数如果一个构造函数的第一个参数是自身类类型的引用且其他参数都有默认值那么这个构造函数被称为拷贝构造函数拷贝构造函数是一种特殊的构造函数。拷贝构造函数的特点拷贝构造函数是构造函数的重载拷贝构造函数的第一个参数必须是自身类类型对象的引用使用传值方式编译器直接报错因为语法逻辑上会引发无穷调用。C 规定自定义类型对象进行拷贝时必须调用拷贝构造所以自定义类型进行传值传参和传值返回时都要调用拷贝构造。若未显式定义拷贝构造编译器会自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝/浅拷贝一个字节一个字节的拷贝对自定义类型成员变量会调用它的拷贝构造。像 Date 这样的类成员变量全是内置类型且没有指向什么资源编译器自动生成的拷贝构造就可以完成需要的拷贝所以不需要我们显式实现拷贝构造。像 Stack 这样的类虽然也都是内置类型但是 _a 指向了资源编译器自动生成的拷贝构造完成的值拷贝/浅拷贝不符合我们的需求所以需要我们自己实现深拷贝对指向的资源也进行拷贝。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员编译器自动生成的拷贝构造会调用 Stack 的拷贝构造也不需要我们显式实现 MyQueue 的拷贝构造。这里还有一个小技巧如果一个类显式实现了析构并释放资源那么他就需要显式写拷贝构造否则就不需要。传值返回会产生一个临时对象调用拷贝构造传引用返回返回的是返回对象的别名引用没有产生拷贝。但是如果返回对象是一个当前函数局部域的局部对象函数结束就销毁了那么使用引用返回是有问题的这时的引用相当于一个悬垂引用类似于野指针一样。传引用返回可以减少拷贝但是一定要确保返回对象在当前函数结束后还在才能用引用返回。// 拷贝构造函数 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 Func1(Date d) { // Func1 传值传参会调用拷贝构造函数 cout d endl; d.Print(); } Date Func2() { // Func2 返回了一个局部对象 tmp 的引用作为返回值 // Func2 函数结束tmp 对象就销毁了相当于一个悬垂引用 Date tmp(2026, 7, 8); tmp.Print(); return tmp; }二、运算符重载1.运算符重载当运算符被用于类类型对象时可以通过运算符重载的形式指定新的含义类类型对象使用运算符时如果没有对应的运算符重载则会编译报错。运算符重载是具有特殊名字的函数他的名字由operator和后面要定义的运算符构成与普通函数一样它也有返回类型参数列表和函数体。重载运算符的函数的参数个数应该和运算符的作用对象一样多。一元运算符有一个参数二元运算符有两个参数二元运算符的左侧运算对象传给第一个参数右侧运算对象传给第二个参数。如果一个重载运算符函数是成员函数则它的第一个运算对象默认传给隐式的this指针因此运算符重载作为成员函数时参数比运算对象少一个。运算符重载以后其优先级和结合性与对应的内置类型运算符保持一致。不能通过连接语法中没有的符号来创建新的操作符比如operator。.* :: sizeof ?: .注意以上5个运算符不能重载。重载操作符至少有一个类类型参数不能通过运算符重载改变内置类型对象的含义如intoperator(int x, int y)一个类需要重载哪些运算符是看哪些运算符重载后有意义比如Date类重载operator-就有意义但是重载operator就没有意义。重载运算符时有前置和后置无法很好的区分C规定重载后置时增加一个int形参来方便区分。重载和时需要重载为全局函数因为重载为成员函数this指针默认抢占了第一个形参位置第一个形参位置是左侧运算对象调用时就变成了 对象 cout不符合使用习惯和可读性。重载为全局函数把ostream/istream放到第一个形参位置就可以了第二个形参位置当类类型对象。bool operator(const Date d) { return _year d._year _month d._month _day d._day; } //前置 Date operator() { return *this; } //后置 Date operator(int) { Date tmp this; return tmp; }2.赋值运算符重载赋值运算符重载是类的默认成员函数用于完成两个已经存在的对象的直接拷贝赋值这里与拷贝构造函数不同拷贝构造函数是用来在创建对象时初始化对象的。赋值运算符重载的特点赋值运算符重载是一个运算符重载规定必须重载为成员函数赋值运算符重载的参数建议写成 const 当前类类型的引用否则传值传参会有拷贝。有返回值且建议写成当前类类型的引用引用返回可以提高效率有返回值是为了实现连续赋值。没有显式实现时编译器会自动生成一个默认赋值运算符重载默认赋值运算符重载行为跟默认拷贝构造函数类似对内置类型成员变量会完成值拷贝/浅拷贝一个字节一个字节的拷贝对自定义类型成员变量会调用它的赋值重载函数。像 Date 这样的类成员变量全是内置类型且没有指向什么资源编译器自动生成的赋值运算符重载就可以完成需要的拷贝所以不需要我们显式实现赋值运算符重载。像 Stack 这样的类虽然也都是内置类型但是 _a 指向了资源编译器自动生成的赋值运算符重载完成的值拷贝/浅拷贝不符合我们的需求所以需要我们自己实现深拷贝对指向的资源也进行拷贝。像 MyQueue 这样的类型内部主要是自定义类型 Stack 成员编译器自动生成的赋值运算符重载会调用 Stack 的赋值运算符重载也不需要我们显式实现 MyQueue 的赋值运算符重载。这里还有一个小技巧如果一个类显式实现了析构并释放资源那么他就需要显式写赋值运算符重载否则就不需要。//赋值运算符重载 Date operator(const Date d) { if (this ! d) { _year d._year; _month d._month; _day d._day; } return *this; }三、取地址运算符重载1.const成员函数const修饰的成员函数被称为const成员函数const放在最后边。const实际上修饰的是成员函数隐藏的this指针修饰过后实际上是void Print(const Date* const this)但是由于不能在参数列表显示修改this指针所以只能将const放在最后边。非const对象也能调用const成员函数是一种权限的缩小。2.取地址运算符重载取地址运算符重载分为普通取地址运算符重载和 const 取地址运算符重载一般情况下编译器自动生成的版本就足够我们使用但如果不想让别人获取到真实地址也可以随意返回一个地址