C++类和对象(中)

一.类的默认成员函数:

     默认成员函数就是⽤⼾没有显式实现,编译器会⾃动⽣成的成员函数称为默认成员函数。⼀个类,我们不写的情况下编译器会默认⽣成以下6个默认成员函数,需要注意的是这6个中最重要的是前4个,最后两个取地址重载不重要,我们稍微了解⼀下即可。其次就是C++11以后还会增加两个默认成员函数,移动构造和移动赋值,这个我们后⾯再讲解

 

   

   这四个比较重要

 默认成员函数很重要,也⽐较复杂,我们要从两个⽅⾯去学习:
   
     • 第⼀:我们不写时,编译器默认⽣成的函数⾏为是什么,是否满⾜我们的需求。
     
     • 第⼆:编译器默认⽣成的函数不满⾜我们的需求,我们需要⾃⼰实现,那么如何⾃⼰实现?

二.构造函数:

      构造函数是特殊的成员函数,需要注意的是,构造函数虽然名称叫构造,但是构造函数的主要

任务并不是开空间创建对象(我们常使⽤的局部对象是栈帧创建时,空间就开好了),⽽是对象实例

化时初始化对象。

其实就是完成初始化的工作,构造函数的本质是要替代我们以前Stack和Date类中写的Init函数的功

能,构造函数⾃动调⽤的特点就完美的替代的了Init。

构造函数的特点:

1. 函数名与类名相同。
2. ⽆返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
3. 对象实例化时系统会自动调用对应的构造函数。
4. 构造函数可以重载。
5. 如果类中没有显式定义构造函数,则C++编译器会⾃动⽣成⼀个无参的默认构造函数,⼀旦⽤⼾显 式定义编译器将不再⽣成。
为了更加直观看出构造函数的特点,通过代码来分析:
这上面的代码就可以看出构造函数的两个特点
1. 函数名与类名相同。
2. ⽆返回值。 (返回值啥都不需要给,也不需要写void,不要纠结,C++规定如此)
基础的代码看过了,接下来看如何调用 无参的构造函数了:
我们可以看到函数名和类名相同,在调用函数时,直接是函数名加上 对象(d1)就自动调用了,
需要再想调用函数那样加括号,直接就可以调用了。因为这样就与无参的函数调用给区分开了。
再来看如何调用 带参的构造函数
带参构造函数的调用要加括号,并且给参数无参的构造函数不需要加括号。
再来看 全缺省的构造函数如何调用
这里带参构造函数和全缺省构造函数构成函数重载,因为函数名相同,函数参数不同,所以是函数
重载,就因为如此,当调用时会构成调用歧义,就不知道调用谁,所以不能这样写。当你写了全缺
省时,可以看出带参的构造函数就没有什么用了,无参的,带参的,全缺省都可以实现。
6. ⽆参构造函数、全缺省构造函数、我们不写构造时编译器默认⽣成的构造函数,都叫做默认构造函数 。但是 这三个函数有且只有⼀个存在,不能同时存在 。⽆参构造函数和全缺省构造函数虽然构成函数重载,但是调⽤时会存在歧义。要注意很多同学会认为默认构造函数是编译器默认⽣成那个叫默认构造,实际上⽆参构造函数、全缺省构造函数也是默认构造, 总结⼀下就是不传实参就可以调⽤的构造就叫默认构造。(0实参构造)
首先我们来看一下什么是默认的构造函数:
这里我们提供一个带参的构造函数,但是编译器会报错因为带参的构造函数并不是默认构造函数,
所以编译器无法调用默认构造
当无参的构造函数出来时,编译器就不会报错,因为这就是默认的构造函数。
全缺省构造函数出来时,编译器也不会报错,因为这也是默认的构造函数。
也就是说不传参的构造就是默认的构造函数。
上面也说了,当我们不写构造,编译器会自动生成一个默认的构造函数,现在我们就来看看,这个
编译器默认的构造函数是什么样的。
我们不写,编译器默认⽣成的构造,对内置类型成员变量的初始化没有要求,也就是说是是否初始
化是不确定的,看编译器。
综上来看编译器自动生成的默认构造无法满足我们的要求,所以 大部分默认构造还是需要我们自己
来写。
我们来看一下不需要自己写的默认构造的例子:
在C语言我们写过用两个栈实现队列,现在用C++来实现:
在队列类中有两个栈,当定义这个队列对象时,会去调用默认构造,如果没有默认构造就会报错,
当去调用MyQueue的默认构造时,对于自定义类型Stack时,又会去调用Stack的默认构造。
当按下F11继续往下走时,就会跳转到Stack的默认构造函数里面去。
大多数情况下构造函数都需要我们自己去实现,少数情况下类似MyQueue且Stack有默认构造时,Myqueue自动生成就可以用。 所以构造函数还是应写尽写。
这里有个经典例题可以看出析构和构造的先后顺序问题:

三.析构函数:

析构函数与构造函数功能相反, 析构函数不是完成对对象本⾝的销毁 ,⽐如局部对象是存在栈帧的
函数结束栈帧销毁,他就释放了,不需要我们管,C++规定对象在销毁时会⾃动调⽤析构函数,
成对象中资源的清理释放工作 。析构函数的功能 类⽐我们之前Stack实现的Destroy功能 ,⽽像
Date没有Destroy,其实就是没有资源需要释放,所以严格说Date是不需要析构函数的。
析构函数的特点:
1. 析构函数名是在 类名前加上字符 ~。
2. ⽆参数⽆返回值 。 (这⾥跟构造类似,也不需要加void)
3. ⼀个类只能有⼀个析构函数。若未显式定义,系统会⾃动⽣成默认的析构函数。
4. 对象 ⽣命周期结束时 ,系统会 ⾃动调⽤析构函数
5. 跟构造函数类似,我们不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
6. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
对于有资源申请的类需要写析构函数,就拿Stack为例,来写析构函数:
这就是我们写的Stack的一个析构函数,接下来我们来调试一下:
当初始化完成之后就会调用析构函数
这时候类里面的变量就全部被释放清零,当我们想全部看类里面的成员变量时,直接在监视窗口输
入this即可看到全部成员变量。
要注意的是 当我们定义了很多类对象时,后定义的先析构,就相当于栈帧,后进先出:
这里显示了地址,我们可以看等会调试调用析构函数后,谁最先被销毁:
可以看出s2先被销毁。
5. 跟构造函数类似,我们 不写编译器⾃动⽣成的析构函数对内置类型成员不做处理,⾃定类型成员会调⽤他的析构函数。
6. 还需要注意的是我们显⽰写析构函数,对于⾃定义类型成员也会调⽤他的析构,也就是说 ⾃定义类型成员⽆论什么情况都会⾃动调⽤析构函数。
结合5,6两点,我们定义的MyQueue没有写析构函数,但他会调用自定义类型成员的析构函数
直接跳转到了Stack的析构函数来了。
如果我们自己写了析构函数,但是里面没有置空操作,他还会去调用自定义类型成员的析构函数
吗?
他打印完了但是还是调用了,自定义类型的成员(就是MyQueue中的Stack)的析构函数,所以就
算显示写了,还是会去调用,就怕出现内存泄露。

四.拷贝构造函数:

如果⼀个构造函数的 第⼀个参数是自身类类型的引用 ,且 任何额外的参数都有默认值 ,则此构造函
数也叫做拷⻉构造函数,也就是说拷⻉构造是⼀个特殊的构造函数。
拷贝构造的特点:
1. 拷⻉构造函数是构造函数的⼀个重载。
2. 拷⻉构造函数的 第⼀个参数必须是类类型对象的引⽤ 使⽤传值⽅式编译器直接报错,因为语法
逻辑上会引发⽆穷递归调⽤ 。 拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象
的引⽤,后⾯的参数必须有缺省值。
3. C++规定⾃定义类型对象进⾏拷⻉⾏为必须调⽤拷⻉构造,所以这⾥⾃定义类型传值传参和传值
返回都会调⽤拷⻉构造完成。
4. 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。 ⾃动⽣成的拷⻉构造对内置类型
成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉) 对⾃定义类型成员变量会调⽤他的拷⻉
构造
5. 像Date这样的类成员变量全是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可
以完成需要的拷⻉,所以不需要我们显⽰实现拷⻉构造。像Stack这样的类,虽然也都是内置类
型,但 是_a指向了资源,编译器⾃动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所
以需要 我们⾃⼰实现深拷⻉(对指向的资源也进⾏拷⻉)。像MyQueue这样的类型内部主要是⾃定
义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现
MyQueue的拷⻉构造。 这⾥还有⼀个⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就
需要显⽰写拷⻉构造,否则就不需要。
6. 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),
没有产⽣拷⻉。 但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使
⽤ 引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。 传引⽤返回可以
减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。
现在我们通过代码来感受一下拷贝构造函数是什么样的:
2. 拷⻉构造函数的 第⼀个参数必须是类类型对象的引⽤ 使⽤传值⽅式编译器直接报错,因为语法
逻辑上会引发⽆穷递归调⽤ 。 拷⻉构造函数也可以多个参数,但是第⼀个参数必须是类类型对象
的引⽤,后⾯的参数必须有缺省值。
这样就是一个拷贝构造函数,第一个参数必须是类类型对象的引用。如果后面还要加参数的话就必
须有初始值:
没有初始值的话就不是拷贝构造函数:
如果第一个参数不是 类类型对象的引用,编译器还会报错

那为什么要拷贝构造函数的参数必须是引用呢?
因为:
所以需要用取地址符和const,
C++规定 函数的传值传参必须调用拷贝构造:
这里我们来调试一下,以前学的C语言是直接一个个字节拷贝过去,而在C++里是调用拷贝构造函
数。
这里走到这一步了,当按F11会进入拷贝构造,然后在进入fun函数的内容:
当构造函数走完之后,再进行fun函数里的内容:
4. 若未显式定义拷⻉构造,编译器会⽣成⾃动⽣成拷⻉构造函数。 ⾃动⽣成的拷⻉构造对内置类型
成员变量会完成值拷⻉/浅拷⻉(⼀个字节⼀个字节的拷⻉) 对⾃定义类型成员变量会调⽤他的拷⻉
构造
那么这样的话其实我们写的日期类不要写拷贝构造,也能编译通过,因为编译器会生成自己的拷贝
构造函数,然后进行浅拷贝:
那么我们以后是不是就可以不用写拷贝构造函数了呢,不是这样的因为 像Date这样的类成员变量全
是内置类型且没有指向什么资源,编译器⾃动⽣成的拷⻉构造就可以完 成需要的拷⻉,所以不需要
我们显⽰实现拷⻉构造。像 Stack这样的类,虽然也都是内置类型,但是_a指向了资源,编译器⾃
动⽣成的拷⻉构造完成的值拷⻉/浅拷⻉不符合我们的需求,所以需要我们⾃⼰实现深拷⻉(对指向
的资源也进⾏拷⻉)。 像MyQueue这样的类型内部主要是⾃定义类型 Stack成员,编译器⾃动⽣成
的拷⻉构造会调⽤Stack的拷⻉构造,也不需要我们显⽰实现 MyQueue的拷⻉构造。这⾥还有⼀个
⼩技巧,如果⼀个类显⽰实现了析构并释放资源,那么他就 需要显⽰写拷⻉构造,否则就不需要。
这里我们来通过代码的调试来看看是什么样的:
在这里的栈并没有自己写拷贝构造函数,我们来用系统自带的拷贝构造函数,来看看是什么样的:
这里虽然拷贝成功了,但是报错了,我们再来调试看一下:
这里我们可以看到st1和st2指向同一位置的, 因为是一个字节一个字节(浅拷贝/值拷贝)拷贝过来
的,所以指向同一位置,所以析构时析构同一地方两次就会报错。所以这里我们就要自己写构造
函数了。   
那这里就展示一下,深拷贝的代码是什么样的:
这里就是开辟一样大的空间,把值拷贝过来,你指向你的空间,我指向我的空间。
总的来说自定义类型,用传值传参并不好,要调用拷贝构造 ,又要开辟新的空间,这样就浪费了
空间和时间,所以最最好用引用传参 ,并且如果不改变参数时,最好在加上const修饰。
除了日期类不用写拷贝构造,这里我们再来看一个例子,也不用写拷贝构造:
像MyQueue这样的类型内部主要是⾃定义类型Stack成员,编译器⾃动⽣成的拷⻉构造会调⽤
Stack的拷⻉构造,也不需要我们显⽰实现 MyQueue的拷⻉构造。
这里也可以看出是深拷贝,因为两个地址不一样:
并且用的也是Stack的构造函数和拷贝构造函数和析构函数。上面也写了另一种拷贝构造的写法
这里拷贝构造的写法有两种:
6. 传值返回会产⽣⼀个临时对象调⽤拷⻉构造,传值引⽤返回,返回的是返回对象的别名(引⽤),
没有产⽣拷⻉。 但是如果返回对象是⼀个当前函数局部域的局部对象,函数结束就销毁了,那么使
⽤ 引⽤返回是有问题的,这时的引⽤相当于⼀个野引⽤,类似⼀个野指针⼀样。 传引⽤返回可以
减少拷⻉,但是⼀定要确保返回对象,在当前函数结束后还在,才能⽤引⽤返回。
拷贝构造这个函数也就结束了。
这里我们再来看看第六点:
这时一个传值返回的函数:
不想拷贝那么就用传引用返回,这里就返回st的别名:
当函数结束后会销毁st,但是销毁之后,调用析构函数,里面就全是野指针了。
我们可以调试看出并没有完成拷贝:
这里并没有拷贝上。所以这个程序是错的。
那怎样调整才是正确的呢,这里错误的原因就算出函数了st已经被销毁了,那么我们出函数的时候
不让他销毁即可,那么就将他变成静态局部变量:
也可以传参然后push值:

五.运算符重载:

当运算符被⽤于类类型的对象时,C++语⾔允许我们通过运算符重载的形式指定新的含义。
C++规定类类型对象使⽤运算符时,必须转换成调⽤对应运算符重载,若没有对应的运算符重载,
则会编译报错。
运算符重载是具有特殊名字的函数,他的名字是由operator和后⾯要定义的运算符共同构成 。和
其他函数⼀样,它也 具有其返回类型和参数列表以及函数体
重载运算符函数的参数个数和该运算符作⽤的运算对象数量⼀样多。 ⼀元运算符有⼀个参数(例
如++,--) ,⼆元运算符有两个参数, ⼆元运算符的左侧运算对象传给第⼀个参数,右侧运算对象
传给第⼆个参数。
如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指针,因此运
算符重载作为成员函数时,参数⽐运算对象少⼀个。
运算符重载以后,其优先级和结合性与对应的内置类型运算符保持⼀致。
现在我们来写一下关于日期的比较大小的运算符重载:
因为是私有变量,没有访问的权限,我们有两种解决办法, 一种就算暴力的将成员变量由私有变成
公有,第二种就是在类中自己写Get函数,返回参数,调用即可,因为类中可以任意调用成员变量
第二种这样的好处是既可以获得参数又保证外面不会改变私有的成员变量
我们看看是如何调用运算符重载的:
这就是当运算符重载函数写在类外面时的两种调用办法,可以看出方法二比方法一更加简单且直观
方法一就像调用函数的那样,函数名+括号+调用的对象。
第三种就算直接将运算符函数重载函数写到类里面去,但是 它的 第⼀个运算对象默认传给隐式的
this指针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。
我们来看看第三种的代码写法,并且如何调用运算符重载函数:
  上面也介绍了 如果⼀个重载运算符函数是成员函数,则它的第⼀个运算对象默认传给隐式的this指
针,因此运算符重载作为成员函数时,参数⽐运算对象少⼀个。 所以这里相当于第一个参数已经
传给了this指针,二元变一元参数,一元参数变成无参。x1就传给了this,x2就 传给了d2.调用写
法还是跟上面差不多。
这里运算符重载还没有讲完在下一篇博客会接着详解。
这就是C++类和对象(中)的所有东西了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值