C++——类与对象(中)
目录1. 类的6个默认成员函数2. 构造函数定义作用特性3. 析构函数4. 拷贝构造函数5. 赋值运算符重载运算符重载赋值运算符重载6. const成员函数7. 取地址及const取地址操作符重载1. 类的6个默认成员函数如果一个类中什么成员都没有简称为空类。空类中真的什么都没有吗并不是任何类在什么都不写时编译器会自动生成以下6个默认成员函数构造函数主要完成初始化工作析构函数主要完成清理工作拷贝构造函数使用同类对象初始化创建对象赋值重载函数主要把一个对象赋值给另外一个对象普通对象取地址const对象取地址默认成员函数他是默认成员函数用户没有显式实现编译器会生成的成员函数称为默认成员函数。2. 构造函数定义构造函数是一个特殊的成员函数名字与类名相同,创建类类型对象时由编译器自动调用以保证每个数据成员都有 一个合适的初始值并且在对象整个生命周期内只调用一次。作用构造函数在对象实例化时被调用用来初始化对象需要注意的是构造函数虽然名称叫构造但是构造函数的主要任务并不是开空间创建对象而是初始化对象。特性函数名与类名相同。无返回值。(注意这里的无返回值不是代表void)对象实例化时编译器自动调用对应的构造函数。构造函数可以重载。构造函数分为无参的构造函数和有参的构造函数其中无参构造函数和全缺省构造函数又称为默认构造函数。接下来以一个例子为例来看看构造函数classData{public:Data()//无参构造函数{}Data(intyear,intmonth,intday)//有参构造函数{coutData()endl;_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};intmain(){Data d;// 调用无参构造函数Datad1(2026,1,26);// 调用带参的构造函数return0;}以上是构造函数有参和无参的定义这里需要区分调用无参构造函数的写法很多用在调用无参构造函数的时候习惯在后面加上空括号——Data d()这种写法是错误的因为这会让编译器无法判断这是对象函数函数名。5.如果类中没有显式定义构造函数则C编译器会自动生成一个无参的默认构造函数一旦用户显式定义编译器将不再生成。这里有几种情况1.如果成员变量是内置类型则不对其做其他处理该默认构造函数会对它进行初始化2.如果成员变量是自定义类型则调用它自己的默认构造函数3.C中在声明成员变量如果给了缺省值则会用缺省值进行初始化。在这里说明一下C把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型如int/char…自定义类型就是我们使用class/struct/union等自己定义的类型用例子来看看我们显示定义的构造函数和编译器定义的默认构造函数显式构造函数classData{public:Data()//无参构造函数{}Data(intyear,intmonth,intday)//有参构造函数{coutData()endl;_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};intmain(){Data d;// 调用无参构造函数Datad1(2026,1,26);// 调用带参的构造函数return0;}结果默认构造函数classData{private:int_year;int_month;int_day;};intmain(){Data d;return0;}结果总结一点构造函数一般情况下都是需要自己定义的不需要自己写的情况主要有内置类型成员有缺省值全是自定义类型的成员。3. 析构函数概念析构函数与构造函数功能相反析构函数不是完成对对象本身的销毁局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数完成对象中资源的清理工作。它与我们的destory()函数功能类似。特性析构函数名是在类名前加上字符 ~无参数无返回值类型。一个类只能有一个析构函数。若未显式定义系统会自动生成默认的析构函数。注意析构函数不能重载。对象生命周期结束时C编译系统系统自动调用析构函数。(也就是出了作用域)现在来举一个例子typedefintDataType;classStack{public:Stack(size_t capacity3){_array(DataType*)malloc(sizeof(DataType)*capacity);if(NULL_array){perror(malloc申请空间失败!!!);return;}_capacitycapacity;_size0;}voidPush(DataType data){// CheckCapacity();_array[_size]data;_size;}~Stack()//显式定义的析构函数{cout~Stack()endl;if(_array){free(_array);_arrayNULL;_capacity0;_size0;}}private:DataType*_array;int_capacity;int_size;};voidTestStack(){Stack s;s.Push(1);s.Push(2);}intmain(){TestStack();return0;}我们运行程序之后发现析构函数被调用在调试的时候我们也可以发现他是在生命周期结束后也就是出了TestStack这个函数作用域之后被调用。5.关于编译器自动生成的析构函数同样的也是主要涉及以下两种情况1.如果成员变量是内置类型则不对其做任何处理2.如果成员变量是自定义类型则调用它自己的默认析构函数下面举个例子classTime{public:~Time(){cout~Time()endl;}private:int_hour;int_minute;int_second;};classDate{private:// 基本类型(内置类型)int_year1970;int_month1;int_day1;// 自定义类型Time _t;};intmain(){Date d;return0;}这个代码的析构函数的调用情况是可以看到内置类型的成员变量在生命周期结束后也没有被默认的析构函数清理而自定义类型的成员变量则被自身的析构函数清理。4. 拷贝构造函数在创建对象时可否创建一个与已存在对象一某一样的新对象呢答案当然是可以的这个时候就要涉及到我们的拷贝构造函数这里先提前说一个C的规定内置类型的拷贝直接拷贝自定义类型的拷贝是传值的必须调用拷贝构造包括赋值(注意两点一是自定义类型而是传值传参。概念拷贝构造函数只有单个形参该形参是对本类类型对象的引用(一般常用const修饰)在用已存在的类类型对象创建新对象时由编译器自动调用。也就是自定义类型对象的传参要调用拷贝构造。接下来先解释一下这个是什么情况要调用拷贝构造voidfunc(inti){}voidfunc1(Date d){}intmain(){Date d1;func1(d1);func(10);return0;}这个代码里面func形参是内置类型所以直接传值就行但func1形参是自定义类型并且是传值那么在传参时要调用拷贝构造。特征拷贝构造函数是构造函数的一个重载形式拷贝构造函数的参数只有一个且必须是类类型对象的引用使用传值方式编译器直接报错因为会引发无穷递归调用。这里来解释一下第二点首先要知道C规定自定义类型对象的传参是传值的必须要调用拷贝构造(更深的说其实是传值传参和传值返回都会创建临时变量进行拷贝所以就必须调用拷贝构造接下来看这个例子分析classDate{public:Date(intyear1900,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}// Date(const Date d) // 正确写法——在调用拷贝构造之后拷贝构造是传引用不是传值就不会出现又调用拷贝构造的情况。Date(constDate d)// 错误写法编译报错会引发无穷递归{_yeard._year;_monthd._month;_dayd._day;}private:int_year;int_month;int_day;};intmain(){Date d1;Dated2(d1);//拷贝d1return0;}这里的无限递归是怎么回事呢原因是调用函数先要传参而我们自定义类型的传参就会调用一次拷贝构造然后拷贝构造又会接着传参又会接着调用拷贝构造图示展示是这样的我再用调试来更加清晰的展示这个无限递归是怎么回事当我们光标运行到func1函数时,我们按F11它首先会去调用构造函数那么上面无限递归的问题就是传参之后调用拷贝构造拷贝构造也要进行传参这样就反复传参反复调用拷贝构造就导致了无限循环。那么我们要如何解决这样的问题呢有两种放法1.用指针原因是任何指针都是内置类型只有自定义类型的才会进行拷贝构造。2.引用(常用):原因是引用相当于给变量取别名不涉及传值。这样我们就只有func1(d1)这一个是传值而拷贝构造就不是传值了。3.如果我们没有显示定义拷贝构造这里也有两种情况1.内置类型完成值拷贝/浅拷贝2.自定义类型会调用它自身的拷贝构造举例classDate{public:Date(intyear1900,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}private:int_year;int_month;int_day;};intmain(){Date d1;Dated2(d1);//拷贝d1return0;}像上述代码我们没有显示定义一个拷贝构造函数但是它依旧能够进行拷贝就是因为编译器自动生成了一个拷贝构造。注意看上面是不是觉得不用自己写拷贝构造也是可以的接下来看一个例子typedefintDataType;classStack{public:Stack(size_t capacity3){_array(DataType*)malloc(sizeof(DataType)*capacity);if(NULL_array){perror(malloc申请空间失败!!!);return;}_capacitycapacity;_size0;}voidPush(DataType data){// CheckCapacity();_array[_size]data;_size;}~Stack()//显式定义的析构函数{cout~Stack()endl;if(_array){free(_array);_arrayNULL;_capacity0;_size0;}}private:DataType*_array;int_capacity;int_size;};intmain(){Stack s;Stacks1(s);return0;}上述代码运行之后程序会崩溃这是什么原因呢我们在进行拷贝时由于没有显式的定义拷贝构造函数那么编译器的默认拷贝构造函数对象按内存存储按字节序完成值拷贝也就是说对象s中的_array的内容被拷贝到对象s1中那么这两对象中的指针指向的是同一片空间那么s和s1生命周期结束后析构函数要被调用两次这是什么意思呢也就是析构函数对一个空间清理了两次这就会导致程序的崩溃。在这里引出什么情况下使用引用返回什么情况下使用传值返回引用返回当函数返回的对象出了函数作用域之后还存在那么就最好使用引用返回传值返回当函数返回的对象出了函数作用域之后不存在了那么必须使用传值返回如果还用引用返回那么将出现上述一样的程序崩溃原理都是一样的。注意在编译器生成的默认拷贝构造函数中内置类型是按照字节方式直接拷贝的而自定义类型是调用其拷贝构造函数完成拷贝的。5. 赋值运算符重载运算符重载C为了增强代码的可读性引入了运算符重载运算符重载是具有特殊函数名的函数也具有其返回值类型函数名字以及参数列表其返回值类型与参数列表与普通的函数类似比如我们要比较日期类对象d1和d2的大小我们要通过一个比较大小的函数将两个对象进行传参来进行比较但是这样可读性太差。在C中我们可以将等运算符进行重载这样我们对d1和d2的比较就直接可以写成d1d2等形式增强可读性。函数概念函数名字为关键字operator后面接需要重载的运算符符号。函数原型返回值类型 operator操作符(参数列表)这里实现一个简单的运算符重载函数booloperator(constDated){if(_yeard._year)returntrue;elseif(_yeard._year_monthd._month)returntrue;elseif(_yeard._year_monthd._month_dayd._day)returntrue;elsereturnfalse;}返回值类型为bool操作符为参数为const Date d我们来测试一下//mainintmain(){Dated1(2026,2,1);Dated2(2026,1,30);cout(d1d2)endl;return0;}运行结果为1也就是d1大于d2。这里我们可以注意到我们要传递两个参数为什么我们写的运算符重载函数只有一个形参这里解释一下运算符重载函数里面的参数个数取决于操作符所需要的操作数个数比如比较大小他是需要d1和d2两个操作数所以形参必须是两个但是在这里为什么只有一个形参这是因为我将这个运算符重载函数定义在了Date类里面那么这个operator函数就是成员函数我们知道类的成员函数里面参数列表是会隐藏一个this指针的所以其实在这里这个operator函数已经有两个形参了。这里是完整的代码classDate{public:Date(intyear1900,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}Date(constDated){_yeard._year;_monthd._month;_dayd._day;}booloperator(constDated){if(_yeard._year)returntrue;elseif(_yeard._year_monthd._month)returntrue;elseif(_yeard._year_monthd._month_dayd._day)returntrue;elsereturnfalse;}private:int_year;int_month;int_day;};intmain(){Dated1(2026,2,1);Dated2(2026,1,30);cout(d1d2)endl;return0;}我还需要强调一下在输出d1d2这个返回值时要加上括号因为“的优先级比”高。运算符重载也可以在类外进行定义这时候就要写两个显示形参因为没有隐藏的this指针了。注意不能通过连接其他符号来创建新的操作符比如operator重载操作符必须有一个类类型参数用于内置类型的运算符其含义不能改变例如内置的整型不 能改变其含义作为类成员函数重载时其形参看起来比操作数数目少1因为成员函数的第一个参数为隐藏的this.* :: sizeof ?: . 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。赋值运算符重载赋值运算符重载是默认成员函数只能在类中进行定义赋值运算符重载的作用就是赋值拷贝是将两个已经存在的对象进行复制拷贝接下来我将一步步来写最终得到我们的赋值运算符重载第一运算符重载voidoperator(constDated){_yeard._year;_monthd._month;_dayd._day;}intmain(){Dated1(2026,2,1);Dated2(2026,1,30);d1d2;//将d2赋值给d1d1进行赋值拷贝d1.Print();return0;}这里可以运行也确实成功赋值拷贝了但是如果是多个赋值就会出现问题intmain(){Dated1(2026,1,30);Date d2,d3;d3d2d1;return0;}报错也就是d2赋值给d3的时候出现报错为什么呢因为d1赋值给d2后这个d2d1表达式的返回值是void所以并不能赋值给d3。第二赋值运算符重载Dateoperator(constDated){_yeard._year;_monthd._month;_dayd._day;return*this}intmain(){Dated1(2026,1,30);Date d2,d3;d3d2d1;return0;}增加返回值后就可以进行连续的赋值。如果按第二步的赋值运算符重载的话其实每次我们进行赋值都会调用一次拷贝构造原因是我们这个是传值返回传值返回每次都要创建临时变量来拷贝我们的返回值在这里我们的返回值是自定义类型那么拷贝返回值就要调用拷贝构造。举例将上述代码运行之后因为赋值了两次所以调用了两次拷贝构造。最后由于this指针出了赋值运算符构造函数后不销毁所以我们可以考虑使用引用返回这样就不会调用拷贝构造了。Dateoperator(constDated){_yeard._year;_monthd._month;_dayd._day;return*this;}这就是我们最终的赋值运算符重载。赋值运算符重载格式参数类型const T传递引用可以提高传参效率返回值类型T返回引用可以提高返回的效率有返回值目的是为了支持连续赋值检测是否自己给自己赋值返回*this要复合连续赋值的含义用户没有显式实现时编译器会生成一个默认赋值运算符重载以值的方式逐字节拷贝。内置类型成员变量是直接赋值的而自定义类型成员变量需要调用对应类的赋值运算符重载完成赋值。这里的情况和拷贝构造函数时一样的。同样的涉及到资源管理的问题我们要自己写赋值运算符重载不然也可能出现像拷贝构造那样的程序崩溃问题。补充说明赋值运算符重载的赋值拷贝和拷贝构造的拷贝的区别赋值运算符重载的赋值拷贝两个已经存在的对象进行复制拷贝拷贝构造的拷贝一个已经存在的对象去初始化另一个对象举个例子这里我就直接用编译器默认的赋值运算符重载classDate{public:Date(intyear1900,intmonth1,intday1){_yearyear;_monthmonth;_dayday;}Date(constDated){_yeard._year;_monthd._month;_dayd._day;}private:int_year;int_month;int_day;};intmain(){Dated1(2026,1,30);Date d2;//d2和d1都是已经存在了的对象d2d1;//赋值重载可以复制到VS中看赋值运算符的颜色//d1已经存在用来初始化d5Date d5d1;//拷贝构造也可以复制到VS中对比两者赋值运算符的颜色return0;}最后根据上面知识完成了一个日期类以及相关功能的实现。6. const成员函数概念:将const修饰的“成员函数”称之为const成员函数const修饰类成员函数实际修饰该成员函数隐含的this指针表明在该成员函数中不能对类的任何成员进行修改。代码示例#includeiostreamusingnamespacestd;classDate{public:Date(intyear,intmonth,intday){_yearyear;_monthmonth;_dayday;}voidPrint()const{cout_year-_month-_dayendl;}private:int_year;int_month;int_day;};对于成员函数Print在其后面添加关键字const就是const成员函数。对于为什么要有这个const修饰其实涉及一个权限的问题这里我们在main中写两个例子intmain(){Dated1(2026,2,8);d1.Print();constDated2(2026,2,16);d2.Print();//如果Print函数不加const修饰的话d2是无法调用Print的return0;}这里d2无法调用Print涉及到一个权限的问题我们知道类的成员函数的形参列表中隐藏了一个this指针这个this指针是Date*类型的而在上述中我们的d2是const Date所以d2.Print()将d2的地址传给this指针时由于d2被const修饰导致其为常性所以会出现权限被放大的情况这时候编译器就会报错。那么我们在Print函数后面加个const修饰其实就是修饰this指针的这样权限就不会发生放大的情况只会发生平移或者缩小。那么加了这个const修饰的好处是什么呢刚刚也讲到加了这个const修饰就不会出现权限放大的情况那么也就是说普通对象和const修饰的对象都可以使用一定程度上防止出现程序报错。注意如果成员函数要修改成员变量则不能加const修饰这个很容易理解加了const修饰this指针指向的那个对象不就不可以修改数据了嘛。7. 取地址及const取地址操作符重载这两个默认成员函数一般不用重新定义 编译器默认会生成。举个例子classDate{public:Date*operator(){returnthis;}constDate*operator()const{returnthis;}private:int_year;// 年int_month;// 月int_day;// 日};这两个运算符一般不需要重载使用编译器生成的默认取地址的重载即可只有特殊情况才需要重载比如想让别人获取到指定的内容如上述。