C++类和对象(中)(构造、析构、拷贝、赋值重载、const成员函数)

上一篇的C++类和对象(上)-CSDN博客已经带大家走进了类和对象,有需要的同志们,可以自行阅览,今天我们将继续对类和对象进行学习。

1.类的6个默认成员函数

一个空类中,编译器会自动生成以下6个默认成员函数。

默认成员函数:用户没有显示实现,编译器自动生成的成员函数称为默认成员函数

2.构造函数

2.1概念

我们先看下面的一段代码

在上面Date类中,可以通过 Init  的成员函数给成员变量赋值,设置日期但是如果每次创建对象时都调用该方法设置,相对来说还是比较麻烦的。要是我们可以直接在创建对象的时候,就给其进行初始化就好了。

2.2特性

构造函数是特殊的成员函数,虽然构造函数名字为构造,但其主要任务并不是开空间创建对象,而是初始化对象。

其特征如下:

1.函数名与类名相同

2.无返回值

3.对象实例化时编译器自动调用对应的构造函数,且构造函数可以重载

绿色框的是通过有参构造函数进行成员变量的初始化

4.如果类中没有显示定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显示定义编译器将不再生成(即只要我们手动打了构造函数,编译器就不会自动生成构造函数)

在来一个例子(如下图):

5.那默认的构造函数有什么用呢?不是说构造函数用于初始化吗?

(注意!无参构造函数、全缺省构造函数、我们没写编译器自动生成的构造函数都是默认构造函数)

我们看下面两段代码

可以发现无论是系统自动生成的默认构造函数,还是我们手动打上去的无参默认构造函数,出来的结果的对象的成员变量都是随机值

这是为什么?

其实只要在无参的默认构造函数里面,给相对应的成员变量进行初始化,就不是随机值了,正如上图所示,_h=2,_year=9,所以运行结果中_h 和_year 就不是随机值,而其余的成员变量还是随机值。

这个其实是C++11中的一个缺陷,没有直接在生成对象的时候,给对象对应的成员变量初始化。

若不使用有参的构造函数在创建对象的时候直接初始化成员变量的话,而是使用无参的默认构造函数初始化,就需要额外在进行初始化。

所以有一些编译器对这个缺陷进行了优化,在调用无参默认构造函数的或者系统默认生成的构造函数的时候会给成员变量初始化成0。(下图中的代码和上一张图片代码一样,但是运行结果一个是随机值,一个是0。C++中没有明确定义初始化成0,是编译器自身的行为)

C++中针对这个成员变量不初始化的缺陷,又打了补丁,即:成员变量在类中声明时可以给默认值(如下图所示)

 全缺省构造函数

但是需要注意的是,我们有了全缺省构造函数,我们同时有无参构造函数的时候,调用时要注意冲突问题

如果对缺省不理解的,可以看c++中的缺省参数-CSDN博客

 3.析构函数

3.1概念

通过前面构造函数的学习,我们知道一个对象是怎么来的,那一个对象又是怎么没的?

析构函数与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作。

(其实就是怕我们申请了空间,忘记释放了,导致造成内存泄漏,所以就有了析构函数通过自动调用,进行资源清理。)

3.2特性

1析构函数名是在类名前加上字符~。

2.无参数无返回值类型。

3.一个类只能有一个析构函数,若未显示定义(即没有手动打析构函数的话),系统会自动生成默认的析构函数。注意:析构函数不能重载。

4.对象声明周期结束时,C++编译系统自动调用析构函数。

什么时候需要我们手动打析构函数?

就是有资源需要清理的时候,就需要写析构函数。(如下图)

什么时候不需要我们打析构函数?

1.没有资源清理的时候

2.没有资源需要清理,且对象中的成员变量都是自定义类型成员

(因为对象中的成员变量都是自定义类型成员,若这个自定义成员内部需要资源清理,则会调用这个自定义类型中的析构函数)

 4.拷贝构造函数

4.1概念

在创建对象的时候,可否创建一个与已存在对象一模一样的新对象呢?可以!

拷贝构造函数:只有单个形参,该形参是对类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。

4.2特征

拷贝构造函数也是特殊的成员函数,其特征如下;

1.拷贝构造函数时构造函数的一个重载形式。

2.拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。

若拷贝构造函数的参数不是类类型对象的引用,而是传值的话,是会报错的,正如下图这样。

那为什么会报错呢?

因为在使用拷贝构造函数的时候,拷贝构造函数的形参也是一个类对象,那就会自动调用该类对象的拷贝构造函数,而这个新的拷贝构造函数里面的形参又是一个类对象,于是又会继续调用拷贝构造函数......于是形成递归,导致出错。

正如下图这样

但是我们的编译器知道这个问题,所以我们要是写错了,写成了传值的方式,编译器不会形成递归而是直接报错。

如果我们使用指针的方式传d1的地址,使这个d2拷贝d1的值,可以吗?(如下图)

根据上图,我们发现d2也得到了d1的值。那使用指针传地址的方式也是拷贝吗?

答案:不是!虽然可以达到这个拷贝的效果,但在C++规定,就是只有引用的方式才是拷贝。(简而言之就是,规定。而且用指针比较不方便诶)

3.若未显示定义(即我们没有手动打),编译器会生成默认的拷贝构造函数。自动生成的拷贝构造函数也会完成拷贝。(如下图)

但是自动生成的拷贝函数只能是浅拷贝。什么是浅拷贝?

浅拷贝是对象内存存储按字节序完成拷贝,也叫做值拷贝。

请看下面这段代码:

在上图中的Stack类的成员变量是有整形数组的。

我首先实例化了s1对象,打印出s1中_arr、_size、_capacity的结果然后实例化s2对象,让s2为s1的浅拷贝,打印出s2中_arr、_size、_capacity的结果。然后我让s2调用了push成员函数,将s2中_arr[0]=num。然后再打印s2中_arr、_size、_capacity的结果可以发现此时s2中的_arr[0]已经被更改成99,最后我在打印s1中_arr、_size、_capacity的结果,发现s1的_arr[0]的值也被更改成99。

因为浅拷贝为字节拷贝使s1和s2指向同一个_arr空间,所以当更改s2中的_arr时,s1中的_arr也发生了变动。

我不想让s1中的_arr受到影响怎么办呢?那么就需要深拷贝,深拷贝就必须我们手动完成,像下面这样。

深拷贝给s2中的_arr重新开了一个空间,使s2的_arr不与s1的_arr指向同一块空间。这样修改s2中_arr的值的时候,s1中的_arr不受到影响。

但是上面的代码还是有错误!大家思考一下!

错误就是

没有

释放

资源!

注意我们申请了空间!所以我们要析构函数!

以及

我们给s2的_arr开了空间之后,并没有将s1的_arr的值给s2的_arr。

正确代码应该如下:

对于拷贝构造函数有以下几点需要补充

1.为什么要加const

2.

3.什么时候写拷贝?什么时候浅拷贝?什么时候深拷贝?

①如果没有资源管理的时候,一般不需要写拷贝构造,直接用编译器生成的拷贝构造即可。

②如果类中的成员变量都是自定义成员,(如下图)直接用编译器生成的拷贝构造即可。但是如果自定义成员内部有指向资源的,当完成拷贝的时,自定义成员内部也需要注意深拷贝。

③如果有指针或者指向资源的,若需完成拷贝,就需要进行深拷贝,同时需要注意完成析构函数。

 5.赋值运算符重载

5.1运算符重载

C++为了增强代码的可读性引入了运算符重载,运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。

那运算符重载和函数重载有什么关系呢?

函数重载:可以让函数名相同,参数不同的函数存在。

运算符重载:让自定义类型可以用运算符,并且控制运算符的行为,增强可读性。

他们之间的关系就是,多个同一运算符重载可以构成函数重载(除了这个原因其实没什么关系)

函数名字为:关键字operator后面接需要重载的运算符符号。

函数原型:返回值类型operator操作符(参数列表)

注意:

  • 不能通过连接其他符号来创建新的操作符:如operator@(就是平时不用的符号就不要拿来用,如下图)
  • 重载操作符必须有一个类类型参数。(其实可以这样理解一下,就是如果两个数字进行加减乘除,因为我们知道他是数字,所以无非是整型,浮点型,所以直接计算就可得出结果。那如果我们想要类的对象也进行加减乘除呢?类对象的成员变量这么多,我们要用哪一个进行这个加减乘除呢?就需要定义标准,这个标准怎么定义呢?就可以通过这个  operator操作符    来定义。根据我们的需要选定一个成员变量进行加减乘除,即对象也可以完成加减乘除了。所以必须有一个类类型参数
  • 用于内置类型的运算符,其含义不能改变。(函数取名表示的意思为加法,结果函数内部实现为减法或其他,可以这样做,但是这样做了不道德~~~,如下图)
  • 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this(下面一点会讲到)
  • .*    ::    sizeof    ?:    .    注意以上5个运算符不能重载。(什么是  .*  运算符?这个运算符不常见,大概了解一下(如下图))

现在有一个问题,类具有封装性,成员变量基本为私有(private),当我们定义的函数在类外时,如何获得对象的成员变量呢,总不能更改成员变量的权限吧,若更改则影响类的封装性。

方法一:

提供get,set方法,细心的小伙伴应该已经发现,我在上面的例子中已经有使用get方法了。get、set方式,如果学过java的伙伴就比较熟悉了。

方法二:

友元(很下面会讲,马上就想知道的伙伴可以马上冲浪一下~~)。

方法三;

将这个定义在类外的函数重载成成员函数。

前面不是说必须有一个类类型参数吗?这边怎么没有呢?

在解释这个为什么之前我们讲一个别的东西。

调用这个类型的函数的方式有两种(如下图)。

我们用operator就是为了对象的加减乘除,②看起来更像是对对象的加减乘除,而①更像是在调函数。虽然两个本来就是在调函数,如果使用①写起来是很麻烦,那我们干嘛要用这个operator,不如直接写一个普通函数,也可以完成对象的加减乘除。

好,现在我们回归问题,前面不是说必须有一个类类型参数吗?这边怎么没有呢?

其实是有的,只是我们没有看见而已,其实第一个参数为隐藏的this。d1对象调用operator+成员函数,当执行_year+n的时候,其实是this->_year+n。这时候我们的①就出来了,因为①的方式更容易的看出是d1在调用函数,然后形参中有一个隐藏的this指针,利于我们理解。

 5.2赋值运算符重载

1.赋值运算符重载格式

*参数类型:const T&,传递引用可以提高传参效率。

其实可以发现这个赋值运算符重载跟拷贝构造很像。但不是一个东西。

赋值是左右两边都是已经存在的对象(如下图),实例化出d1之后,实例化出d2,两个对象都已经存在了之后,将d2赋值给d1.

而拷贝是一个已经存在的对象,拷贝给另一个正在创建初始化的对象(如下图)。

拷贝若用“=”的方式来完成拷贝的话,写法和赋值很像。同时,二者的行为也很像需要区分。

既然这么像写一个就好了?!存在即有它的意义,只不是现在我们还没有深刻接触到罢了。

*返回值类型:T&,返回引用可以提高返回效率,有返回值的目的是为了支持连续赋值。

(如下图)

可以发现通过赋值函数重载完成了连续的赋值,把d3的值给了d2和d1。

可是为什么返回值是Date&?不可以是Date吗?有什么区别呢?

下面我们来看一段代码。

若将上面的func函数的返回值更改成引用会有什么变化?

通过上面的例子,我们发现返回值为引用是有风险的。

所以我们得出下面的结论:

返回对象声明周期到了,会析构,传值(通过临时变量)返回。

返回对象声明周期没到,不会析构,引用返回(减少临时变量的拷贝,增加效率)。

在下面的代码中,d3,d2,d1的作用域都在main函数中,所以调用opeartor=返回不会析构,返回值可以用Date&,即引用,使用引用的好处就是减少临时变量再拷贝,增加效率。

*检测是否自己给自己赋值。

因为自己给自己赋值的时候容易白白消耗,避免损失,增加是否自己给自己赋值的判断。

*返回*this:要复合连续赋值的含义。

有返回值可以完成连续赋值的操作。

2. 赋值运算符只能重载成类的成员函数不能重载成全局函数

原因:赋值运算符如果不显式实现,编译器会生成一个默认的。此时用户再在类外自己实现 一个全局的赋值运算符重载,就和编译器在类中生成的默认赋值运算符重载冲突了,故赋值 运算符重载只能是类的成员函数。

3.用户没有现实实现时,编译器会生成一个默认赋值运算符重载,以值的方式逐字节拷贝。

既然编译器生成的默认赋值运算符重载函数已经可以完成字节序的值拷贝了,还需要自己实现吗?

请看下面一段代码。

通过上图,我们发现将s1的值赋值给s2之后,更改了s2中_array下标为0处的值后,s1中_array下标为0地方的值也被更改了。因为是按照字节拷贝的,所以s1赋值给s2之后,s1和s2的_array同时指向同一块空间,所以更改了s2的_array后,导致s1的_array也被更改。

那我们应该怎么办?其实这边解决方式和深拷贝是一样的。这边就不做过多的讲述。(解决后如下图)

所以,我们发现如果类中未涉及到资源管理,赋值运算符是否实现都可以;一旦涉及到资源管理则必须要实现。

5.3前置++和后置++重载

两个都是++,虽然一个是前置++,一个是后置++,那在完成前置++和后置++重载的时候,如何区分呢?

前置与后置的区别(如下图)

5.4对象的输出

如果我们想要不通过调用函数输出对象,而是cout<<d1<<endl;的方式输出,需要怎么做呢?

通过上图,我们知道cout可以自动识别对象,完成输出。

用这个方式直接输出一个对象,却不可以,为什么?

其实对于内置类型,cout不是自动识别的,是因为在库中已经完成了相关的函数重载,所以达到了自动识别的效果。如果我们也想让对象使用cout输出,需要我们自己写一个operator<<(如下图)

(运算符重载中,参数顺序和操作顺序是一致的)

想要变成正常的样子,这是需要我们将其重载为全局函数,这样形参就可以由我们自己掌握,不存在第一个隐藏this指针了。

什么是友元呢?(如下图)

友元通俗讲就是将这个函数变成了朋友,就可以去你家玩了。这个友元声明放在哪个权限下都可以。

7.const成员

将const修饰的“成员函数”称之为const成员函数,const修饰类成员函数,实际修饰该成员函数隐含的this指针,表明在该成员函数中不能对类的任何成员修改。

我们来看一段代码

在实例化对象的时候前面添加了一个const,为什么在用这个d1对象调用成员函数就产生了错误呢?

因为const修饰d1对象,让d1对象是只读的。而Print成员函数是,可读可写的,一个只读的对象调用对象内可读可写的函数,产生了权限放大的问题。

那我们如何解决这个问题?

在成员函数的后面添加一个const,将Print成员函数的权限进行缩小。

(上图)为了解决权限不匹配问题,我们在成员函数增加了const,使其匹配。

但是本质上是修改了成员函数形参内this指针的权限,this指针是隐藏的,接收的是对象的地址。只要控制了this指针,这个成员函数内就不可以对成员变量进行修改,就完成了权限的匹配,变成了只读。

所以成员函数在没加const之前,隐藏的this是这样的 Date *const  this;成员函数增加了const之后,隐藏的this完整的描述是变成了这样 const Date *const  this。

通过下面的代码我们就可以发现添加了const的成员函数内部,this指向的值不可以被修改。

而没加const的成员函数,里面不可以更改this的指向,但是可以更改this指向的值。

所以如果不用修改成员的,我们可以加上const,如果要修改成员的我们就不用加const。如果一个函数两种情况都遇到了,我们加const和不加const的成员函数都实现,然后利用函数重载来完成。

请思考下面的几个问题:

1. const对象可以调用非const成员函数吗?

不可以,权限放大。

2. 非const对象可以调用const成员函数吗?

可以,权限缩小。

3. const成员函数内可以调用其它的非const成员函数吗?

不可以,权限放大(如果这个非const成员函数更改了成员,不就违背了加const的意愿吗)

4. 非const成员函数内可以调用其它的const成员函数吗?

可以,权限缩小。

8.取地址及const取地址操作符重载

这两个默认成员函数一般不用重新定义,编译器默认会生成。

这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让被人获取到指定的内容!(如下图)

好了,本篇文章终于进入尾声~

如果有问题欢迎指正批评,我们下次见!

  • 25
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值