文章目录

二、封装篇(下)

4.1 对象数组与对象成员

(1)对象数组

很多时候我们不止需要一个对象,而是一组对象,如一个班50个学生,那就可以使用对象数组。

【C++】面向对象之封装篇(下)_数据


【对象数组代码实践】

题目:定义一个坐标(​​Coordinate​​)类,其数据成员包含横坐标和纵坐标,分别从栈和堆中实例化长度为3的对象数组,给数组中的元素分别赋值,最后遍历两个数组。

【C++】面向对象之封装篇(下)_实例化_02


头文件:

class Coordinate{
public:
Coordinate();
~Coordinate ();
public:
int m_iX;
int m_iY;
};

源程序:

#include<iostream>
#include<stdlib.h>
#include"Coordinate.h"

using namespace std;

/* 对象数组
/* 要求
1. 定义Coordiante类
2. 数据成员:m_iX、m_iY
3. 分别从栈和堆中实例化长度为3的对象数组
4. 给数组中的元素分别赋值
5. 遍历两个数组
/* *****************************************/


Coordinate::Coordinate(){
cout <<"Coordinate()"<<endl;
}

Coordinate::~Coordinate (){
cout <<"~Coordinate()"<< endl;
}


int main(){
Coordinate coor[3]; //从栈上实例化对象数组
coor[0].m_iX =3;
coor[0].m_iY =5;


Coordinate *p =new Coordinate[3];
p->m_iX = 7; //直接写p的话,就说明是第一个元素
p[0].m_iY =9; //等价于 p->m_iY = 9

p++; //将指针后移一个位置,指向第2个元素
p->m_iX = 11;
p[0].m_iY = 13; //这里p指向的是第二个元素,p[0]就是当前元素,等价于p->m_iY = 13

p[1].m_iX = 15;//第3个元素的横坐标
p++; //将指针后移一个位置,指向第3个元素
p[0].m_iY = 17;//这里p指向的是第三个元素,p[0]就是当前元素,等价于p->m_iY = 17


for(int i = 0; i < 3; i++){
cout <<"coor_X: "<< coor[i].m_iX <<endl;
cout <<"coor_Y: "<< coor[i].m_iY <<endl;
}
for(int j = 0; j < 3; j++){
//如果上面p没有经过++操作,就可以按下面来轮询
//cout <<"p_X: " << p[i].m_iX <<endl;
//cout <<"p_Y: " << p[i].m_iY <<endl;
//但是,上面我们对p做了两次++操作,实际p已经指向了第3个元素,应如下操作
cout <<"p_X: "<< p->m_iX <<endl;
cout <<"p_Y: "<< p->m_iY <<endl;
p--;
}

//经过了三次循环后,p指向了一个非法内存,不能直接就delete,而应该让p再指向我们申请的一个元素的,如下
p++; //这样p就指向了我们申请的内存
delete []p;
p = NULL;


system("pause");
return 0;
}

结果 :

Coordinate()
Coordinate()
Coordinate()
Coordinate()
Coordinate()
Coordinate()
coor_X: 3
coor_Y: 5
coor_X: -858993460
coor_Y: -858993460
coor_X: -858993460
coor_Y: -858993460
p_X: 15
p_Y: 17
p_X: 11
p_Y: 13
p_X: 7
p_Y: 9
~Coordinate()
~Coordinate()
~Coordinate()
请按任意键继续. . .

从运行结果来看,首先看到的是打印出六行“​​Coordinatre()​​”,这是因为分别从栈实例化了长度为3的对象数组和从堆实例化了长度为3的对象数组,每实例化一个对象就要调用一次默认构造函数。

​ 最后只打印出三行“​​~Coordinate()​​​”,那是不是只是从堆上实例化的对象销毁时调用了析构函数,而从栈实例化的对象销毁时,也有调用函数
——从栈实例化的对象在销毁时,系统自动回收内存,即自动调用析构函数,只是当我们按照提示“请按任意键结束”,按下任何键后,屏幕会闪一下,就在这闪的过程中,会出现三行“​​​~Coordinate()​​”的字样,只是我们不容易看到而已。

(2)对象成员

上面说的类的数据成员都是基本的数据类型,比如汽车类,我们只声明了汽车轮子个数,显然还不够,因为轮子本身就是一个对象,汽车上还有发动机、座椅等等对象。

如下的直角坐标,起点A和终点B,要定义这样的线段类和点坐标类

【对象成员代码实践 】

【C++】面向对象之封装篇(下)_ico_03


定义两个类:

​ (1)坐标类:​​Coordinate​​​ ​ 数据成员:横坐标​​m_iX​​,纵坐标​​m_iY​

成员函数:构造函数、析构函数,数据成员的封装函数

(2)​线段类:​​Line​​​ ​ 数据成员:点​​A m_coorA​​,点​​B m_coorB​

成员函数:构造函数,析构函数,数据成员的封装函数,信息打印函数

头文件​​Coordinate.h​​:

class Coordinate{
public:
Coordinate();
~Coordinate();
void setX(int x);
int getX();
void setY(int y);
int getY();
private:
int m_iX;
int m_iY;
};

头文件​​Line.h​​:

#include "Coordinate.h"

class Line{
public:
Line();
~Line();
void setA(int x, int y);
void setB(int x, int y);
void printInfo();
private:
Coordinate m_coorA;
Coordinate m_coorB;
};

源程序​​Coordinate.cpp​​:

#include <iostream>
#include "Coordinate.h"
using namespace std;

Coordinate::Coordinate (){
cout <<"Coordinate()"<<endl;
}

Coordinate::~Coordinate (){
cout <<"~Coordinate()"<<endl;
}
void Coordinate::setX(int x){
m_iX = x;
}
int Coordinate::getX(){
return m_iX;
}
void Coordinate::setY(int y){
m_iY = y;
}
int Coordinate::getY(){
return m_iY;
}

源程序​​Line.h​​:

#include<iostream>
#include "Line.h"
//#include "Coordinate.h"

using namespace std;

Line::Line(){
cout <<"Line()"<< endl;
}
Line::~Line(){
cout <<"~Line()"<< endl;
}
void Line::setA(int x, int y){
m_coorA.setX(x);
m_coorA.setY(y);
}
void Line::setB(int x, int y){
m_coorB.setX(x);
m_coorB.setY(y);
}
void Line::printInfo(){
cout << "(" << m_coorA.getX() <<","<< m_coorA.getY()<< ")" <<endl;
cout << "(" << m_coorB.getX() <<","<< m_coorB.getY()<< ")" <<endl;
}

源程序​​demo.cpp​​:

//我们首先来实例化一个线段类的对象,如下
#include <iostream>
#include "Line.h"

using namespace std;

int main(){
Line *p = new Line();
delete p;
p = NULL;
system("pause");
return 0;
}

从运行结果来看,先连续调用了两次坐标类的构造函数,再调用了一次线段类的构造函数,这就意味着先创建了两个坐标类的对象,这两个坐标类的对象就是A点和B点,然后才调用线段这个对象,线段这个对象是在A点和B点初始化完成之后才被创建

而在销毁时,先调用的是线段类的析构函数,然后连续调用两次坐标类的析构函数。可见,对象成员的创建与销毁的过程正好相反,也验证了我们之前给出的结论。

【C++】面向对象之封装篇(下)_数据_04

作为一条线段来说,我们希望的是,在这条线段创建的时候就已经将线段的起点和终点确定下来。为了达到这个目的,我们往往希望线段这个类的构造函数是带有参数的,并且这个参数将来能够传给这两个点,所以可以进一步完善这个程序。

4.2 深拷贝与浅拷贝

在封装(上)中学习了拷贝构造函数的声明方法和自动调用的时间,但是如何实现拷贝构造函数呢?分为深拷贝和浅拷贝

【栗子1】成员变量没有指针

【C++】面向对象之封装篇(下)_数据_05

  • 上面栗子中:定义的一个数组的类(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】成员变量多了一个指针

【C++】面向对象之封装篇(下)_ico_06


在这个例子中,我们新加了一个数据成员,它是​​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(如下图所示)。

【C++】面向对象之封装篇(下)_后端_07

  • 在这个时候,如果我们先给arr1的​​m_pArr​​​赋了一些值,也就是说在这段内存中就写了一些值,然后我们再给arr1的​​m_pArr​​​去赋值的时候,这段内存就会被重写,而覆盖掉了之前给arr1的​​m_pArr​​所赋的一些值。
  • 更严重的问题是,当我们去销毁arr1这个对象的时候,我们为了避免内存泄漏,肯定会释放掉​​m_pArr​​​所指向的这段内存。如果我们已经释放掉了这段内存,我们再去销毁arr2这个对象时,我们肯定也会以同样的方式去释放掉arr2中​​m_pArr​​这个指针所指向的这段内存,那么就相当于,同一块内存被释放了两次,导致报错。
  • 所以我们希望这里的拷贝构造函数所完成的工作,两个对象的指针所指向的应该是两个不同的内存,拷贝的时候不是将指针的地址简单的拷贝过来,而是将指针所指向的内存当中的每一个元素依次的拷贝过来。(如下图)

【C++】面向对象之封装篇(下)_数据_08


为了实现刚才的效果,如下修改:

【C++】面向对象之封装篇(下)_实例化_09


这段代码与之前的代码的区别在于其拷贝构造函数,其中的m_pArr不是直接赋值arr中的​m_pArr​,而是先分配一段内存(PS:这段内存分配成功与否,这里没有判断,因为这个不是这里要讲的重点),重点是后面的一段for循环语句。我们应该将arr中的m_pArr的每一个元素都拷贝到当前的​m_pArr​所指向的相应的内存当中去

总结:当进行对象拷贝时,不是简单的做值的拷贝,而是将堆中内存的数据也进行了拷贝(深拷贝)。

【深浅拷贝代码实践】

  • 定义一个​​Array​​类。
  • 数据成员:​​m_iCount​
  • 成员函数:构造函数、拷贝构造函数,析构函数
  • 数据成员的封装函数
  • 要求通过这个例子体会浅拷贝原理

头文件​​Array.h​

class Array{
public:
//构造函数
Array();
//拷贝构造函数
Array(const Array &arr);
~Array();
void setCount(int count);
int getCount();
private:
//唯一个成员变量
int m_iCount;
};

而该​​Array.cpp​​源程序为:

#include"Array.h"
#include<iostream>
using namespace std;

Array::Array(){
cout <<"Array()"<< endl;
}
//拷贝构造函数
Array::Array(const Array &arr){
m_iCount = arr.m_iCount;
cout <<"Array(const Array &arr)"<<endl;
}
Array::~Array(){
cout <<"~Array()"<< endl;
}
void Array::setCount(int count){
m_iCount = count;
}
int Array::getCount(){
return m_iCount;
}

​main.cpp​​源程序为:

#include<iostream>
#include<stdlib.h>
#include"Array.h"

using namespace std;

int main(){
Array arr1;
arr1.setCount(5);

//通过Arr1来实例化arr2
Array arr2(arr1);

cout <<"arr2.m_iCount"<<" "<< arr2.getCount() << endl;

system("pause");
return 0;
}

【C++】面向对象之封装篇(下)_c++_10

  • 在1的基础上增加一个数据成员:​​m_pArr​
  • 并增加​​m_pArr​​地址查看函数
  • 同时改造构造函数、拷贝构造函数和析构函数
  • 要求通过这个例子体会深拷贝的原理和必要性

修改后的栗子如下:
​​​Array.h​​​增加数据成员​​m_iCount​​:

class Array{
public:
//构造函数
Array(int count);
//拷贝构造函数
Array(const Array &arr);
~Array();
void setCount(int count);
int getCount();

//新增查看地址函数
void printAddr();
private:
//唯一个成员变量
int m_iCount;

//新增数据成员:m_pArr
int *m_pArr;
};

源程序​​Array.cpp​​,注意这里我们还是先让拷贝构造函数是用浅拷贝试试:

#include"Array.h"
#include<iostream>
using namespace std;

Array::Array(int count){
m_iCount = count;
//堆上分配内存
m_pArr = new int[m_iCount];
}
Array::Array(const Array &arr){
m_iCount = arr.m_iCount;
m_pArr = arr.m_pArr;//这里先用浅拷贝实现方式来看看会有什么后果?
cout <<"Array(const Array &arr)"<<endl;
}
//析构函数
Array::~Array(){
//delete掉数组指针
delete []m_pArr;
m_pArr = NULL;
cout <<"~Array()"<< endl;
}
void Array::setCount(int count){
m_iCount = count;
}
int Array::getCount(){
return m_iCount;
}
void Array::printAddr(){
cout <<"m_pArr的值是:"<< m_pArr << endl;
}

源程序​​main.cpp​​,这里还是通过Arr1来实例化arr2:

#include<iostream>
#include<stdlib.h>
#include"Array.h"

using namespace std;

int main(){
//Array arr1;
//修改后的栗子
Array arr1(5);

//这次删除这句
//arr1.setCount(5);

//通过Arr1来实例化arr2
Array arr2(arr1);

//cout <<"arr2.m_iCount"<<" "<< arr2.getCount() << endl;
cout << "arr1中";
arr1.printAddr();
cout << "arr2中";
arr2.printAddr();

system("pause");
return 0;
}

显然,可以发现拷贝构造函数执行后,​​arr1​​​和​​arr2​​​的​​m_pArr​​值相同,即两个指针指向同一个地址(内存),而在析构时就会执行两次对同一块内存的释放操作,导致报错。

【C++】面向对象之封装篇(下)_c++_11


之所以上面木有报错,是因为程序最后加上了​​system("pause");​​​如果继续按任意键,就会如下的报错,即只执行了一次析构函数​​~Array()​​。

【C++】面向对象之封装篇(下)_实例化_12


深拷贝的方式则需要在拷贝构造函数中给当前的这个指针先分配一段内存,然后将传入的对象的对应位置的内存拷贝到新申请的这段内存中区。改为深拷贝后的代码(只需要修改构造函数,和拷贝构造函数):

Array::Array(int count)
{
m_iCount = count;
m_pArr = new int[m_iCount];
for(int i =0; i < m_iCount; i++)
{
m_pArr[i] = i;
}
cout <<"Array()"<< endl;
}
Array::Array(const Array &arr)
{
m_iCount = arr.m_iCount;
m_pArr = new int[m_iCount];
for(int i = 0; i < m_iCount; i++)
{
m_pArr[i] = arr.m_pArr[i];
}
cout <<"Array(const Array &arr)"<< endl;
}

并且这次按任意键后,程序也没奔溃:

【C++】面向对象之封装篇(下)_实例化_13

4.3 对象指针、对象成员指针

(1)对象指针

【C++】面向对象之封装篇(下)_ico_14


定义了一个坐标的类(​​Coordinate​​),其有两个数据成员(一个表示横坐标,一个表示纵坐标)。当我们定义了这个类之后,我们就可以去实例化它了。如果我们想在堆中去实例化这个对象呢,就要如下所示:

【C++】面向对象之封装篇(下)_ico_15


通过new运算符实例化一个对象后(这个对象就会执行它的构造函数),而对象指针p就会指向这个对象。我们的重点是要说明p与这个对象在内存中的相关位置以及它们之间的对应关系。当我们通过这样的方式实例化一个对象后,它的本质就是在内存中分配出一块空间,在这块空间中存储了横坐标(​​m_iX​​​)和纵坐标(​​m_iY​​​),此时m_iX的地址与p所保存的地址应该是一致的,也就是说p所指向的就是这个对象的第一个元素(​​m_iX​​​)。如果想用p去访问这个元素,很简单,就可以这样来访问(​​p -> m_iX​​​或者​​p -> m_iY​​​),也可以在p前加上*,使这个指针变成一个对象,然后通过点号(.)来访问相关的数据成员(如​​(*p).m_iY​​)。

【C++】面向对象之封装篇(下)_ico_16


注意:这里的​​new​​​运算符可以自动调用对象的构造函数,而C语言中的​​malloc​​则只是单纯的分配内存而不会自动调用构造函数。

(2)对象指针代码实践

定义​​Coordinate​​类:

  • 数据成员:​​m_iX​​​和​​m_iY​
  • 声明对象指针,并通过指针操控对象
  • 计算两个点,横、纵坐标的和

(3)对象成员指针

对象成员,就是作为一个对象来说,它成为了另外一个类的数据成员。而对象成员指针呢,则是对象的指针成为了另外一个类的数据成员了。

(4)内存中的对象成员指针

【C++】面向对象之封装篇(下)_ico_17


当实例化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​​类对象指针。

#include"Coordinate.h"

class Line
{
public:
Line(int x1, int y1, int x2, int y2);
~Line();
void printInfo();
private:
Coordinate *m_pCoorA;
Coordinate *m_pCoorB;
};

4.4 this指针

this指针就是指向其自身数据的指针。

【C++】面向对象之封装篇(下)_实例化_18

C++中的每一个对象都能通过​​this​​​指针访问自己的地址,​​this​​指针是所有成员函数的隐含参数,所以在成员函数内部,可以用来指向调用对象。

PS:友元函数没有​​this​​​指针,因为友元函数不是类的成员,只有成员函数才有​​this​​指针。

#include <iostream>

using namespace std;

class Box{
public:
// 构造函数定义
Box(double l=2.0, double b=2.0, double h=2.0)
{
cout <<"Constructor called." << endl;
length = l;
breadth = b;
height = h;
}
double Volume(){
return length * breadth * height;
}
int compare(Box box){
return this->Volume() > box.Volume();
}
private:
double length; // Length of a box
double breadth; // Breadth of a box
double height; // Height of a box
};

int main(void){
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box Box2(8.5, 6.0, 2.0); // Declare box2

if(Box1.compare(Box2)){
cout << "Box2 is smaller than Box1" <<endl;
}
else{
cout << "Box2 is equal to or larger than Box1" <<endl;
}
return 0;
}

【C++】面向对象之封装篇(下)_c++_19




【this指针代码实践】

定义一个Array类:

数据成员:​​m_iLen​​ 表示数组长度

成员函数:构造函数​ 析构函数​ ​​m_iLen​​的封装函数

​信息打印函数​​printInfo​

【C++】面向对象之封装篇(下)_数据_20

4.5 const进阶

const的部分可以回顾 ​​【C++基础】引用的用法、const常量引用​​。

【C++】面向对象之封装篇(下)_后端_21

4.6 常指针和常引用

(1)对象的引用和对象的指针

先看一个下面的栗子,类中三个成员函数,其中printInfo()函数是一个常成员函数。那么在实现的时候,也需要在printInfo函数后面加上const关键字来修饰:

class Coordinate{
public:
Coordinate(int x, int y);
int getX();
int getY();
//常成员函数
void printInfo() const;
private:
int m_iX;
int m_iY;
};

在成员函数的定义后面加上​​const​​​:
1.c++的一个机制,让该函数的权限为只读,也就是说它没法去改变成员变量的值。

2.同时,如果一个对象为const,它只有权利调用const函数,因为成员变量不能改变。

比如定义了一个类​​FooClass​​:

class FooClass{
public:
void Foo(){ /*...*/}
private:
/*...*/
};

//在程序中创建了一个FooClass的常量对象A,并试图调用这个成员函数Foo()
const FooClass A;
A.Foo();

对A调用成员函数Foo()将会出错!

A是一个const的对象,但是Foo()只能用于非const的对象。定义成员函数Foo()时,显然不能把调用它的对象写到形参列表里面去声明为一个const,比如Foo(const FooClass* this)。怎么让Foo()能用于const对象呢?就是给Foo()加上一个const声明,如下:

class FooClass{
public:
void Foo() const{ /*...*/}
private:
/*...*/
};

这个时候,对前面创建的const的A就可以调用这个成员函数了。

实际上,成员函数Foo()有一个隐式的形参this,它是自身对象的一个指针,但是不能显式地使用在Foo()的形参列表里。加上const就说明,this指向的对象是const对象。

当然,加上了const声明的成员函数,不能对调用它的对象内的成员进行修改(声明为mutable的成员例外)。

(2)对象的常引用和常指针

【C++】面向对象之封装篇(下)_c++_22

Reference

[1] https://www.zhihu.com/question/27860418