)
类和对象2难点1.类的默认成员函数默认成员函数就是用户没有显式实现编译器会自动生成的成员函数称为默认成员函数对于一个类我们不写的情况下编译器会默认生成以下6个成员函数最重要的是前四个C11以后还会增加两个默认成员函数分别是移动构造和移动赋值6个默认成员函数 一初始化和清理 构造函数主要完成初始化工作 析构函数主要完成清理工作 二拷贝复制 拷贝构造是使用同类对象初始化创建对象 赋值重载主要是把一个对象赋值成另一个对象 三取地址重载 主要是普通对象和const对象取地址很少自己实现学习时要思考问题1.我们不写的时候编译器默认生成的函数行为是什么是否满足我们的需求2.编译器默认生成的函数不满足我们的要求需要自己实现如何自己实现2.构造函数构造函数是特殊的成员函数虽然叫构造但是构造函数的主要任务并不是开空间创造对象(我们常使用的局部对象是栈帧创建的时候空间就开好了)而是对象实例化时初始化对象构造函数的本质是替代我们以前Date类中写的Init函数的功能构造函数自动调用的特点就完美的替代了Init构造函数的特点1.函数名和类名相同2.无返回值返回值啥都不需要给也不需要写void3.对象实例化时系统会自动调用相应的构造函数4.构造函数可以重载5.如果类中没有显示定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦用户显示定义编译器将不再生成6.无参构造函数、全缺省构造函数、我们不写构造时编译器默认生成的构造函数都叫做默认构造函数但是这三个函数有且只有一个存在不能同时存在。无参函数和全缺省函数虽然构成函数重载但是调用的时候会发生歧义其实就是不传参就可以调用的函数就叫做默认构造7.编译器默认生成的构造对内置类型成员变量的初始化没有要求也就是是否初始化是不确定的如果这个成员变量没有默认构造函数那么就会报错初始化成员变量需要用初始化列表才能解决补充C把类型分为内置类型基本类型和自定义类型内置类型就是语言提供的原生数据类型如int/指针等自定义类型就是使用class/struct等关键字自己定义的类型class Date { public: void Init(int year, int month, int day) { _year year; _month month; _day day; } Date() { //赋值构造 _year 1; _month 1; _day 1; } Date(int year, int month, int day) {//带参构造 _year year; _month month; _day day; } private: int _year; int _month; int _day; };对象实例化一定会调用构造函数确保实例化是正确的class Date { public: void Init(int year, int month, int day) { _year year; _month month; _day day; } Date(int year, int month, int day) {//带参构造 _year year; _month month; _day day; } private: int _year; int _month; int _day; }; int main() { Date d1; //这里会出现错误因为只有带参数的构造 //只有我们不进行自己构造的时候才默认构造不带参数的 }3.析构函数析构函数与构造函数功能相反析构函数不是完成对对象本身的销毁比如局部对象是存在栈帧的函数结束栈帧销毁他就释放了不需要我们管C 规定对象在销毁时会自动调用析构函数完成对对象中资源的清理释放工作。析构函数的功能类比我们之前 Stack 实现的 Destroy 功能而像 Date 没有 Destroy其实就是没有资源需要释放所以严格说 Date 是不需要析构函数的。析构函数的特点析构函数名是在类名前加上字符。无参数无返回值。(这里跟构造类似也不需要加 void)一个类只能有一个析构函数。若未显式定义系统会自动生成默认的析构函数。对象生命周期结束时系统会自动调用析构函数。跟构造函数类似我们不写编译器自动生成的析构函数对内置类型成员不做处理自定义类型成员会调用他的析构函数。还需要注意的是我们显式写析构函数对于自定义类型成员也会调用他的析构也就是说自定义类型成员无论什么情况都会自动调用析构函数。就拿手写栈来举例子手写栈的析构函数为~Stack() { free(a); _a nullptr; _capacity _top 0; }拷贝构造int main() { Date d1(2020, 2, 4);//普通构造 Date d2(d1);//拷贝构造 }如果一个构造函数的第一个参数是自身类类型的引用且任何额外的参数都有默认值则此构造函数也叫做拷贝构造函数也就是说拷贝构造是一个特殊的构造函数。拷贝构造的特点拷贝构造函数是构造函数的一个重载。普通构造用字面量 / 多参数创建全新空白对象拷贝构造用已存在的同类对象复制生成新对象 二者函数名都是类名参数列表不同构成重载编译器自动匹配调用。拷贝构造函数的第一个参数必须是类类型对象的引用使用传值方式编译器直接报错因为语法逻辑上会引发无穷递归调用。拷贝构造函数也可以多个参数但是第一个参数必须是类类型对象的引用后面的参数必须有缺省值。//Date d1(d2) Date(Date d) {//这里必须用引用不能用传值 _year d._year; _month d._month; _day d._day; } -------------------------- 假如使用了传值 Date(Date d) 调用该函数的时候值传递需要拷贝实参生成形参d 拷贝自定义对象必须调用拷贝构造函数第三条 于是再次执行Date(Date d)无限递归导致栈溢出编译器报错 ---------------------------------------- 多参拷贝构造规则 允许多个形参但是第一个必须是本类引用剩下的参数必须给默认值 否则不被认定为是拷贝构造 // 合法拷贝构造Date(const Date d, int addYear 0){} // 非法第二个参数无默认值不属于拷贝构造 Date(const Date d, int addYear){}C 规定自定义类型对象进行拷贝行为必须调用拷贝构造所以这里自定义类型传值传参和传值返回都会调用拷贝构造完成。若未显式定义拷贝构造编译器会生成自动生成拷贝构造函数。自动生成的拷贝构造对内置类型成员变量会完成值拷贝 / 浅拷贝 (一个字节一个字节的拷贝)对自定义类型成员变量会调用他的拷贝构造。显式定义析构函数并且释放资源一般是和自定义拷贝构造是绑定的传值返回会产生一个临时对象调用拷贝构造传值引用返回返回的是对象的别名没有产生拷贝。如果返回对象是一个当前函数局部域的局部对象函数结束就销毁了那么使用引用返回是有问题的这时的引用相当于一个类似于野指针的野引用传引用返回可以减少拷贝但是一定要确保返回对象在当前函数结束后还在才能引用返回补充浅拷贝和深拷贝的区别浅拷贝编译器默认拷贝构造的行为只复制对象内存表层的值对于普通内置变量int/double直接复制数值对于指针成员只复制指针本身的地址不会重新开辟堆内存。结果两个对象的指针指向同一块堆资源。深拷贝手动实现拷贝构造不仅复制表层变量当成员是堆指针时给新对象单独开辟一块全新堆内存将源对象堆里的数据完整复制到新堆空间 结果两个对象拥有独立的堆资源互不干扰。运算符重载1.当运算符被用于类类型的对象的时候C语言允许我们通过运算符重载的形式指定新的含义。C规定类类型对象使用运算符的时候必须转化成调用对应的运算符重载如果没有对应的运算符重载则会发生编译报错2.运算符重载是具有特殊名字的函数他的名字是operator和后面要定义的运算符共同构成和其他函数一样他也有返回类型和参数列表以及函数体3.重载运算符函数的参数个数和该运算符作用的运算对象数量一样多一元运算符有一个参数二元运算符有两个参数二元运算符的左侧运算对象传给第一个参数右侧传给第二个参数4.如果一个重载运算符函数是成员函数则他的第一个运算对象默认传给隐式的this指针因此运算符重载作为成员函数的时候参数比运算对象少一个5.运算符重载之后其优先级和结合性和对应的内置类型运算符保持一致还是以Date日期类举例子Date d1(2020, 2, 4);//普通构造 Date d3(2020, 2, 5); bool ret d3 d1;会产生报错错误原因二进制“”: 没有找到接受“Date”类型的左操作数的运算符(或没有可接受的转换)没有与这些操作数匹配的 “” 运算符这就用到了我们的运算符重载先用相等做例子bool operator(const Date d1,const Date d2){ return d1._yeard2._year d1._monthd2._month d1._day d2._day; }但是这里会发生报错原因是operator的参数过多说明是几元操作就是几个参数但是有一个隐含的this所以要发生改变this默认指向第一个参数bool operator(const Date d2) { if (this-_year d2._year) { return true; } else if (this-_month d2._month) { return true; } else if (this-_day d2._day) { return true; } else { return false; } }补充不能通过链接语法中没有的符号来创建新的操作符如operator.* :: sizeof ?: .以上五个运算符不能重载重载操作符至少要有一个类类型的参数不能通过运算符重载改变内置类型对象的含义一个类需要重载哪些运算符是看哪些运算符重载之后有意义比如Date类中的operator-有意义而没有意义重载运算符时有前置和后置运算符重载都是operator没办法很好的区分所以C规定后置重载的时候增加一个int实参和前置构成函数重载方便区分// 1. 前置d 无参数 Date operator(){ // 日期1简化逻辑只简单日1 _day;return *this; // 返回自增后的自身引用返回支持连续 } // 2. 后置d 多加一个int占位参数 Date operator(int){ Date temp *this; // 先保存原始值 _day; // 自身自增 return temp; // 返回修改前的旧对象值返回不能引用 }赋值运算符重载的特点1.赋值运算符重载是一个运算符重载规定必须重载为成员函数赋值运算符重载的参数建议写成 const 当前类类型引用否则传值传参会有拷贝2.有返回值且建议写成当前类型引用引用返回可以提高效率有返回值的目的是支持连续赋值操作d1 d2 d33.默认的赋值运算符重载也是浅拷贝//拷贝构造 Date d1(2002.2.2); Date d2(d1);//是创造一个新的 //赋值是存在的两个对象的拷贝 Date d3(2005.2.3); d1 d3; ///////////////////////////////////////////// Date operator (const Date d){ _year d._year; _monthd._month; _day d._day; return *this; }