文章目录
二、封装篇(下)
4.1 对象数组与对象成员
(1)对象数组
很多时候我们不止需要一个对象,而是一组对象,如一个班50个学生,那就可以使用对象数组。
【对象数组代码实践】
题目:定义一个坐标(Coordinate
)类,其数据成员包含横坐标和纵坐标,分别从栈和堆中实例化长度为3的对象数组,给数组中的元素分别赋值,最后遍历两个数组。
头文件:
源程序:
结果 :
从运行结果来看,首先看到的是打印出六行“Coordinatre()
”,这是因为分别从栈实例化了长度为3的对象数组和从堆实例化了长度为3的对象数组,每实例化一个对象就要调用一次默认构造函数。
最后只打印出三行“~Coordinate()
”,那是不是只是从堆上实例化的对象销毁时调用了析构函数,而从栈实例化的对象销毁时,也有调用函数
——从栈实例化的对象在销毁时,系统自动回收内存,即自动调用析构函数,只是当我们按照提示“请按任意键结束”,按下任何键后,屏幕会闪一下,就在这闪的过程中,会出现三行“~Coordinate()
”的字样,只是我们不容易看到而已。
(2)对象成员
上面说的类的数据成员都是基本的数据类型,比如汽车类,我们只声明了汽车轮子个数,显然还不够,因为轮子本身就是一个对象,汽车上还有发动机、座椅等等对象。
如下的直角坐标,起点A和终点B,要定义这样的线段类和点坐标类
【对象成员代码实践 】
定义两个类:
(1)坐标类:Coordinate
数据成员:横坐标m_iX
,纵坐标m_iY
成员函数:构造函数、析构函数,数据成员的封装函数
(2)线段类:Line
数据成员:点A m_coorA
,点B m_coorB
成员函数:构造函数,析构函数,数据成员的封装函数,信息打印函数
头文件Coordinate.h
:
头文件Line.h
:
源程序Coordinate.cpp
:
源程序Line.h
:
源程序demo.cpp
:
从运行结果来看,先连续调用了两次坐标类的构造函数,再调用了一次线段类的构造函数,这就意味着先创建了两个坐标类的对象,这两个坐标类的对象就是A点和B点,然后才调用线段这个对象,线段这个对象是在A点和B点初始化完成之后才被创建。
而在销毁时,先调用的是线段类的析构函数,然后连续调用两次坐标类的析构函数。可见,对象成员的创建与销毁的过程正好相反,也验证了我们之前给出的结论。
作为一条线段来说,我们希望的是,在这条线段创建的时候就已经将线段的起点和终点确定下来。为了达到这个目的,我们往往希望线段这个类的构造函数是带有参数的,并且这个参数将来能够传给这两个点,所以可以进一步完善这个程序。
4.2 深拷贝与浅拷贝
在封装(上)中学习了拷贝构造函数的声明方法和自动调用的时间,但是如何实现拷贝构造函数呢?分为深拷贝和浅拷贝。
【栗子1】成员变量没有指针
- 上面栗子中:定义的一个数组的类(Array)中定义了一个数据成员(
m_iCount
),并且定义了构造函数,在其中对数据成员赋了初值5。 - 另外还定义了一个拷贝构造函数。在这个拷贝构造函数是这样实现的:传入的参数是
arr
,这个参数的数据类型也是Array
类对象,所以其肯定也含有数据成员m_iCount
,这里将arr
的数据成员m_iCount
赋值给本身的m_icount
。 - 当我们使用时,先用
Array arr1
来实例化一个arr1
的时候,就会调用到arr1的构造函数,也就是说将arr1中的数据成员m_icount
赋了初值5。 - 而我们使用
Array arr2 = arr1
的时候,也就是用arr1
去初始化arr2
——这时实例化arr2
的时候就会调用到它的拷贝构造函数,拷贝构造函数中的参数arr
其实就是arr1
,里面代码实现的时候,就相当于将arr1
的数据成员m_icount
赋值给arr2
的数据成员m_icount
。
【栗子2】成员变量多了一个指针
在这个例子中,我们新加了一个数据成员,它是int
型的指针m_pArr
,其在构造函数中,从堆中申请了一段内存,并且指向了申请的这段内存,内存的大小就是m_icount
。
(1)当我们使用时,先用Array arr1
来实例化一个arr1
的时候,就会调用到arr1的构造函数,也就是说将arr1中的数据成员m_icount
赋了初值5。
(2)而我们使用Array arr2 = arr1
的时候,也就是用arr1去初始化arr2,这时实例化arr2的时候就会调用到它的拷贝构造函数,于是就将arr1的数据成员m_icount
赋值给arr2的数据成员m_icount
,将arr1的数据成员m_pArr
赋值给arr2的数据成员m_pArr
。
- 在这两个例子中,有共同的特点,那就是,只是将数据成员的值作了简单的拷贝,我们就把这种拷贝模式称为浅拷贝。
- 但是对于第一个例子来说,使用浅拷贝的方式来实现拷贝构造函数并没有任何问题,而对于第二个例子来说,肯定是有问题的——经过浅拷贝之后,对象arr1中的指针和对象arr2中的指针势必会指向同一块内存(因为我们将arr1的数据成员
m_pArr
赋值给arr2的数据成员m_pArr
),这里假设指向的地址是0x00FF00(如下图所示)。
- 在这个时候,如果我们先给arr1的
m_pArr
赋了一些值,也就是说在这段内存中就写了一些值,然后我们再给arr1的m_pArr
去赋值的时候,这段内存就会被重写,而覆盖掉了之前给arr1的m_pArr
所赋的一些值。 - 更严重的问题是,当我们去销毁arr1这个对象的时候,我们为了避免内存泄漏,肯定会释放掉
m_pArr
所指向的这段内存。如果我们已经释放掉了这段内存,我们再去销毁arr2这个对象时,我们肯定也会以同样的方式去释放掉arr2中m_pArr
这个指针所指向的这段内存,那么就相当于,同一块内存被释放了两次,导致报错。 - 所以我们希望这里的拷贝构造函数所完成的工作,两个对象的指针所指向的应该是两个不同的内存,拷贝的时候不是将指针的地址简单的拷贝过来,而是将指针所指向的内存当中的每一个元素依次的拷贝过来。(如下图)
为了实现刚才的效果,如下修改:
这段代码与之前的代码的区别在于其拷贝构造函数,其中的m_pArr
不是直接赋值arr中的m_pArr
,而是先分配一段内存(PS:这段内存分配成功与否,这里没有判断,因为这个不是这里要讲的重点),重点是后面的一段for循环语句。我们应该将arr中的m_pArr
的每一个元素都拷贝到当前的m_pArr
所指向的相应的内存当中去。
总结:当进行对象拷贝时,不是简单的做值的拷贝,而是将堆中内存的数据也进行了拷贝(深拷贝)。
【深浅拷贝代码实践】
- 定义一个
Array
类。
- 数据成员:
m_iCount
- 成员函数:构造函数、拷贝构造函数,析构函数
- 数据成员的封装函数
- 要求通过这个例子体会浅拷贝原理
头文件Array.h
而该Array.cpp
源程序为:
main.cpp
源程序为:
- 在1的基础上增加一个数据成员:
m_pArr
- 并增加
m_pArr
地址查看函数 - 同时改造构造函数、拷贝构造函数和析构函数
- 要求通过这个例子体会深拷贝的原理和必要性
修改后的栗子如下:
Array.h
增加数据成员m_iCount
:
源程序Array.cpp
,注意这里我们还是先让拷贝构造函数是用浅拷贝试试:
源程序main.cpp
,这里还是通过Arr1来实例化arr2:
显然,可以发现拷贝构造函数执行后,arr1
和arr2
的m_pArr
值相同,即两个指针指向同一个地址(内存),而在析构时就会执行两次对同一块内存的释放操作,导致报错。
之所以上面木有报错,是因为程序最后加上了system("pause");
如果继续按任意键,就会如下的报错,即只执行了一次析构函数~Array()
。
深拷贝的方式则需要在拷贝构造函数中给当前的这个指针先分配一段内存,然后将传入的对象的对应位置的内存拷贝到新申请的这段内存中区。改为深拷贝后的代码(只需要修改构造函数,和拷贝构造函数):
并且这次按任意键后,程序也没奔溃:
4.3 对象指针、对象成员指针
(1)对象指针
定义了一个坐标的类(Coordinate
),其有两个数据成员(一个表示横坐标,一个表示纵坐标)。当我们定义了这个类之后,我们就可以去实例化它了。如果我们想在堆中去实例化这个对象呢,就要如下所示:
通过new运算符实例化一个对象后(这个对象就会执行它的构造函数),而对象指针p就会指向这个对象。我们的重点是要说明p与这个对象在内存中的相关位置以及它们之间的对应关系。当我们通过这样的方式实例化一个对象后,它的本质就是在内存中分配出一块空间,在这块空间中存储了横坐标(m_iX
)和纵坐标(m_iY
),此时m_iX的地址与p所保存的地址应该是一致的,也就是说p所指向的就是这个对象的第一个元素(m_iX
)。如果想用p去访问这个元素,很简单,就可以这样来访问(p -> m_iX
或者p -> m_iY
),也可以在p前加上*,使这个指针变成一个对象,然后通过点号(.)来访问相关的数据成员(如(*p).m_iY
)。
注意:这里的new
运算符可以自动调用对象的构造函数,而C语言中的malloc
则只是单纯的分配内存而不会自动调用构造函数。
(2)对象指针代码实践
定义Coordinate
类:
- 数据成员:
m_iX
和m_iY
- 声明对象指针,并通过指针操控对象
- 计算两个点,横、纵坐标的和
(3)对象成员指针
对象成员,就是作为一个对象来说,它成为了另外一个类的数据成员。而对象成员指针呢,则是对象的指针成为了另外一个类的数据成员了。
(4)内存中的对象成员指针
当实例化line这个对象的时候,那么两个指针(m_pCoorA
和m_pCoorB
)也会被定义出来,由于两个指针都是指针类型,那么都会占4个基本内存单元。如果我们在构造函数当中,通过new
这样的运算符从堆中来申请内存,实例化两个Coordinate
这样的对象的话呢,这两个Coordinate
对象都是在堆中的,而不在line这个对象当中,所以刚才我们使用sizeof的时候呢,也只能得到8,这是因为m_pCoorA
占4个基本内存单元,m_pCoorB
占4个基本内存单元,而右边的两个Coordinate
对象并不在line这个对象的内存当中。当我们销毁line
对象的时候呢,我们也应该先释放掉堆中的内存,然后再释放掉line
这个对象。
(5)对象成员指针代码实践
定义两个类:
坐标类:Coordinate
数据成员:m_iX
和m_iY
成员函数:构造函数、西沟函数、数据成员封装函数
线段类:Line
数据成员:点A指针 m_pCoorA
,点B指针m_pCoorB
成员函数:构造函数、析构函数、信息打印函数
这里和以前的变化,即将Line
类中的成员之前是Coordinate
类对象,这里是Coordinate
类对象指针。
4.4 this指针
this指针就是指向其自身数据的指针。
C++中的每一个对象都能通过this
指针访问自己的地址,this
指针是所有成员函数的隐含参数,所以在成员函数内部,可以用来指向调用对象。
PS:友元函数没有this
指针,因为友元函数不是类的成员,只有成员函数才有this
指针。
【this指针代码实践】
定义一个Array类:
数据成员:m_iLen
表示数组长度
成员函数:构造函数 析构函数 m_iLen
的封装函数
信息打印函数printInfo
4.5 const进阶
const的部分可以回顾 【C++基础】引用的用法、const常量引用。
4.6 常指针和常引用
(1)对象的引用和对象的指针
先看一个下面的栗子,类中三个成员函数,其中printInfo()函数是一个常成员函数。那么在实现的时候,也需要在printInfo函数后面加上const关键字来修饰:
在成员函数的定义后面加上const
:
1.c++的一个机制,让该函数的权限为只读,也就是说它没法去改变成员变量的值。
2.同时,如果一个对象为const,它只有权利调用const函数,因为成员变量不能改变。
比如定义了一个类FooClass
:
对A调用成员函数Foo()将会出错!
A是一个const的对象,但是Foo()只能用于非const的对象。定义成员函数Foo()时,显然不能把调用它的对象写到形参列表里面去声明为一个const,比如Foo(const FooClass* this)。怎么让Foo()能用于const对象呢?就是给Foo()加上一个const声明,如下:
这个时候,对前面创建的const的A就可以调用这个成员函数了。
实际上,成员函数Foo()有一个隐式的形参this,它是自身对象的一个指针,但是不能显式地使用在Foo()的形参列表里。加上const就说明,this指向的对象是const对象。
当然,加上了const声明的成员函数,不能对调用它的对象内的成员进行修改(声明为mutable的成员例外)。
(2)对象的常引用和常指针
Reference
[1] https://www.zhihu.com/question/27860418