C++面向对象笔记(3):封装篇(下)

封装篇(下)

1.对象成员的小总结

  1. 实例化对象A时,如果对象A有对象成员B,那么先执行对象B的构造函数,再执行A的构造函数。

  2. 如果对象A中有对象成员B,那么销毁对象A时,先执行对象A的析构函数,再执行B的析构函数。

  3. 如果对象A中有对象成员B,对象B没有默认构造函数,那么对象A必须在初始化列表中初始化对象B。

2.拷贝构造函数-浅拷贝与深拷贝

浅拷贝1:

Array.h

class Array {
public:
    // 构造&拷贝构造&析构
    Array();
    Array(const Array&arr);
    ~Array();
	// 属性操作方法
    void setCount(int count);
    int getCount();
private:
    int m_iCount;
};

Array.c

// 构造&拷贝构造
Array::Array() {
    cout << "Array" << endl;
}
Array::Array(const Array &arr) {
    m_iCount = arr.m_iCount;
    cout << "Array &" << endl;
}
// 析构
Array::~Array(){
    cout << "~Array" << endl;
}

// 属性操作方法
void Array::setCount(int count) {m_iCount = count;}
int Array::getCount() {return m_iCount;}

main.c

int main(){

    Array arr1;
    arr1.setCount(5);

    Array arr2(arr1);
    cout << arr2.getCount() << endl;

    return 0;
}
/*输出:
Array
Array &
5
~Array
~Array
*/

浅拷贝2:

此次在类中加入指针属性,所以需要注意的是关于类中指针的拷贝。

Array.h

class Array {
public:
    Array(int count);
    Array(const Array&arr);
    ~Array();

    void setCount(int count);
    int getCount();

    void printAddr();
private:
    int m_iCount;   // 指针元素个数
    int *m_pArr;    // 用于指向数组的指针
};

Array.c

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 = arr.m_pArr;

    cout << "Array &" << endl;
}

Array::~Array(){
    // 释放类中的指针属性
    delete []m_pArr;
    m_pArr = nullptr;
    cout << "~Array" << endl;
}

// 属性操作方法
void Array::setCount(int count) {m_iCount = count;}
int Array::getCount() {return m_iCount;}

void Array::printAddr() {cout << "m_pArrd = " << m_pArr << endl;}

main.c

int main(){

    Array arr1(5);
    Array arr2(arr1);

    arr1.printAddr();
    arr2.printAddr();

    return 0;
}
/*输出
Array
Array &
m_pArrd = 0xb516c0
m_pArrd = 0xb516c0
~Array

Process finished with exit code -1073740940 (0xC0000374)
*/

可见最后析构函数只完整运行了1次(输出一个~Array),应为类中的指针都指向同一块内存地址,所以第二次析构函数进行指针资源释放会出现错误,导致程序崩溃。所以就需要用到深拷贝了。

深拷贝:

Array.h和main.c这两个文件的内容完全不变,只需要修改Array.c中拷贝构造函数即可。注意观察两个类中指针属性指向的地址。

Array.c

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 &" << endl;
}

Array::~Array(){

    delete []m_pArr;
    m_pArr = nullptr;
    cout << "~Array" << endl;
}

void Array::setCount(int count) {m_iCount = count;}
int Array::getCount() {return m_iCount;}
void Array::printAddr() {cout << "m_pArrd = " << m_pArr << endl;}

main.c文件输出

/*输出
Array
Array &
m_pArrd = 0xa816c0
m_pArrd = 0xa816e0
~Array
~Array
*/

可以观察到,这两个类中的指针,指向的地址并不一样;所以析构函数能完整的运行两次,将所有的类中的指针属性正常释放掉。

3.this指针、对象指针

this指针的使用:

  1. this指针无需用户定义,是编译器自动产生的。

  2. this指针也是指针类型,所以在32位编译器下也占用4个基本的内存单元,即sizeof(this)的结果为4。

  3. 当成员函数的参数或临时变量与数据成员同名时,可以使用this指针区分同名的数据成员。

对上面的第3项举个例子:

如果我们在类中写了2个属性,分别是int x;int y;,然后在这个类的构造函数中,括号里的形参名也是x和y,在这种情况下,我们就可以使用 this指针 代表 当前类的指针 ,this->x就代表 当前类指针的x,这样就可以将临时变量x和当前类的x属性进行区分了。

Coordinate::Coordinate(int x,y){
    this->x = x;
    this->y = y;
}

这里只展示其中一种 对象指针 比较少用到的写法:

Coordinate p1;
Coordinate *p2 = &p1;

这里指针的写法和一般的变量一样。例如:int x=1; int *p = &x;

很多情况下都是 指针和堆内存 结合在一起使用的,但是这里是 指针和栈内存 结合在一起使用。

实例1,当 对象 作为 返回值 时:

要注意此时会调用拷贝构造函数,故会多生成一个对象,和原来的对象无关。如果我们对这个对象进行操作,之前的对象是不会受到任何影响的。

Array.h

class Array {
public:
    Array(int len);
    Array(const Array& arr);
    ~Array();
    void setLen(int len);
    int getLen();
    Array printInfo();
private:
    int m_iLen;
};

Array.c

Array::Array(int len){
    m_iLen = len;
    cout << "Array(int len)" << endl;
}
Array::Array(const Array &arr) {
    cout << "Array(const Array &arr)" << endl;
}
Array::~Array(){
    cout << "~Array()" << endl;
}

void Array::setLen(int len){m_iLen = len;}
int Array::getLen(){return m_iLen;}

// 当返回值是对象时,会调用拷贝构造函数,返回一个新的类,
// 所以程序结束的时候,会发现多调用了一次析构函数。
Array Array::printInfo(){
    cout << "printInfo(),return Array" << endl;
    return *this; // this是指针,*this是对象
}

main.c

int main() {
    
    Array arr1(10);
    
    // 对printInfo()返回的 【对象】 进行操作
    //(调用getLen()将m_iLen变量设置为5),
    // 但是此对象是通过拷贝构造函数新生成的,
    // 所以通过arr1.getLen()输出的值是不会有变化的。
    arr1.printInfo().setLen(5); 
    
    // 输出arr1这个对象的将m_iLen属性的值
    cout << arr1.getLen() << endl;
    
    return 0;
}
/* 输出:
Array(int len)
printInfo(),return Array
Array(const Array &arr)
~Array()
10
~Array()
*/

输出了两次~Array(),说明在程序的运行过程中,确实出现了两个Array的对象。

通过printInfo()和下一行的Array(const Array &arr)可知,在进入到printInfo()函数中返回Array对象之后,确实调用了拷贝构造函数

提示:如何避免这种情况?那么就只有使用 指针 或 引用 了,之后的例子里会讲。

实例2,当 对象引用 作为 返回值 时:

修改一下Array类的声明和实现。

Array.h

//Array printInfo();

Array& printInfo();

Array.c

//Array Array::printInfo(){
//    cout << "printInfo(),return Array" << endl;
//    return *this; // this是指针,*this是对象
//}

// 当返回值是对象的引用时,就不会调用拷贝构造函数
Array& Array::printInfo(){
    cout << "printInfo(),return Array&" << endl;
    return *this; // this是指针,*this是对象
}

main.c

int main() {
    
    Array arr1(10);
    
    // 对返回的 【对象引用】 进行操作
    // 但是此对象是
    // 所以通过arr1.getLen()输出的值是不会有变化的。
    arr1.printInfo().setLen(5); 
    
    // 输出arr1这个对象的将m_iLen属性的值
    cout << arr1.getLen() << endl;
    
    return 0;
}

/* 输出:
Array(int len)
printInfo(),return Array&
5
~Array()
*/

当传递对象的引用的时候,就不会调用拷贝构造函数创建一个新的对象了。而且通过引用我们可以对原对象()进行操作,就和我们使用指针的时候一样。

**返回对象引用的修改方法及其调用方法:**到此肯定注意到了在main中使用了arr1.printInfo().setLen(5)这种调用方式。如果我们将其它函数改造成返回对象的引用return *this,我们就可以使用这种调用方式,相当于每次返回的都是当前对象的引用,我们通过.对这个对象引用中的函数进行调用。

举个例子,如果我们将setLen也改成返回对象的引用,那么我们就可以使用如下的调用方式:

arr1.printInfo().setLen(5).printInfo();

在这一连串的.成员调用中,我们实际上都是对arr1这个对象中的属性/方法进行调用。

实例3,当 对象指针 作为 返回值 时:

将前面例2的代码由引用形式改成指针形式即可。

Array.h

//Array& printInfo();
Array* printInfo();

Array.c

// 当返回值是对象的引用时,就不会调用拷贝构造函数
//Array& Array::printInfo(){
//    cout << "printInfo(),return Array&" << endl;
//    return *this; // this是指针,*this是对象
//}
Array* Array::printInfo(){
    cout << "printInfo(),return Array*" << endl;
    return this; // this是指针,*this是对象
}

main.c

int main() {
    
    Array arr1(10);
    
    //arr1.printInfo().setLen(5); 
    // 对返回的 【指针】 进行操作,得到的结果和使用引用的一样
    arr1.printInfo()->setLen(5);
    
    // 输出arr1这个对象的将m_iLen属性的值
    cout << arr1.getLen() << endl;
    
    return 0;
}

/* 输出:
Array(int len)
printInfo(),return Array*
5
~Array()
*/

输出的内容和使用引用输出的一样,都是对arr1进行操作。

4.常量对象和常量成员函数之间的调用关系

1.如果我们用const修饰对象成员函数,会发现如果我们对函数体内的成员进行修改,会出现错误,代码如下:

// 错误
void Coordinate::changeX() const{
    m_iX = 10;
}

// 如果不用const修饰则没有问题
void Coordinate::changeX(){
    m_iX = 10;
}

2.上面的问题,和this指针有关,这里就来说明一下:

对于用户来说,编译器会帮我们添加this指针。

// 当我们定义changeX()这个成员函数的时候,看起来没有任何的参数
void Coordinate::changeX(){
    m_iX = 10;
};
// 但是实际上却隐含着一个参数,就是this指针
void changeX(Coordinate *this){
    this->m_iX = 10;
};

3.所以实际上代码为1中的代码是这种表现:

void Coordinate::changeX() const{
    m_iX = 10;
}
// 实际表现如下:
void changeX(const Coordinate *this){
    this->m_iX = 10;// 会出现错误
}

可以看出,对于changeX()这个对象成员函数,使用 const 进行修饰,就相当于对 this指针 进行const修饰,原来的Coordinate *this就被修饰成const Coordinate *this,this是一个常量指针(既这个指针指向一个常量),所以我们不能对 this 指向的对象的内容进行修改,而m_iX就是对象中的属性,所以我们当然不能对其进行修改了。

互为重载与其调用:

// 互为重载的声明
class Coordinate{
public:
    Coordinate(int x, int y);
    void changeX() const;	// 和下一行的void changeX()互为重载
    void changeX();
private:
    int m_iX;
    int m_iY;  
}

// 调用void changeX()
int main(){
    Coordinate coordinate(3,5);
    coordinate.changeX();
    return 0;
}

// 调用void changeX() const
int main(){
    const Coordinate coordinate(3,5);// 在实例化对象的时候,加上const
    coordinate.changeX();
    return 0;
}

使用const实例化出来的对象,我们称为常量对象

通过常量对象const Coordinate coordinate(3,5)调用的就是常量对象函数void changeX() const

这里再说明一下常量对象与常量对象函数:

1.常量对象:

如果对象使用const修饰,那么就称为常量对象;常量对象和一般的常量(const int x之类)类似,其内容都是不可以修改的,也就是其中属性除了初始化的时候进行赋值,其它任何时候都不能对常量对象中的属性进行修改。

2.常量对象函数:

如果类中的函数采用const修饰,那么就称为常量对象函数;常量对像函数的隐含参数this指针,也会受到const的影响,导致this指针指向的对象的内容不可以修改(即通过这种方式,只有读权限,没有写权限)。

所以什么样的对象函数可以改成常量对象函数?

如果一个对象函数不需要对 对象的属性 进行写操作,那么就可以改为常量对象函数。

注意1:

对象改成常量对象,对应的 获取对象属性的对象函数 也需要改成const修饰的。(修改对象属性的对象函数没法改,原因看上面“所以什么样的对象函数可以改成常量对象函数?”这个问题)

这是因为这个对象函数如果不用const修饰,那么隐含的this指针参数权限就是可读、可写的。而我们的常对象因为有const限制,所以权限只有可读。所以 隐含的this指针常量对象 的类型其实不同,也就不能用this指向常对象,否则会导致编译出现错误。

对应的程序代码在“04const2”

注意2:

当然,如果对象不是常量对象,对象函数对象函数可以用普通的对象函数,也可以用常量对象函数。

5.常量指针和常量引用的使用

普通的 对象引用 和 对象指针:

int main(){
    
    Coordinate coor1(3,5);
    Coordinate &coor2 = coor1;	// 对象的引用
    Coordinate *pCoor = &coor1;	// 对象的指针
    
    // 打印的都是coor1这种对象中的内容
    coor1.printInfo();
    coor2.printInfo();
    pCoor->printInfo();
    
    return 0;
}

常量指针和常量引用:

Coordinate.cpp

int Coordinate::getX(){
    return m_iX;
}
int Coordinate::getY(){
    return m_iY;
}
void Coordinate::printInfo() const {
    cout << "(" << m_iX << "," << m_iY << ")" << endl;
}

main.c

int main(){

    Coordinate coor1(3,5);
    const Coordinate &coor2 = coor1;	// 常量对象的引用
    const Coordinate *pCoor = &coor1;	// 常量对象的指针

    coor1.printInfo();
    coor2.getX();	// 报错
    pCoor->getY();	// 报错,'this' argument to member function 'getY' has type 'const Coordinate', but function is not marked const

    return 0;
}

和之前说的同理,引用和指针加上const修饰,就只有可读权限,而getX()getY()都是普通对象函数,其中隐含的this指针为可读、可写权限。类型不同,当然会报错了。

上面报错的翻译:

‘this’ argument to member function ‘getY’ has type ‘const Coordinate’, but function is not marked const

成员函数“getY”的这个参数的类型是“const Coordinate”,但是函数没有标记const

故,如果函数也用const修饰,则可以正常运行。

6.对象指针常量的使用

Coordinate.cpp

int Coordinate::getY(){
    return m_iY;
}
void Coordinate::printInfo() const {
    cout << "(" << m_iX << "," << m_iY << ")" << endl;
}

main.c

int main(){
    
    Coordinate coor1(3,5);
    Coordinate coor2(7,9);
    Coordinate * const pCoor = &coor1;
    
    pCoor->getY();	// 正常调用
    pCoor = coor2;	// 错误
    pCoor->printInfo();	// 正常调用
    
    return 0;
}

pCoor是一个指针常量,本身不可以修改,但是指向的内存上的内容是可以修改的,也就是说具有可读、可写的权限。

  1. pCoor->getY():这里是符合getY()这个对象函数的要求的,所以可以正常调用。

  2. pCoor = coor2:这里很明显的是要改变pCoor的指向,但是pCoor是常量,无法改变其指向的地址,所以是错误的。

  3. pCoor->printInfo():这个在本文第4章末尾的 注意2 有提及,对象是可读、可写的,而printInfo()中this指针只要求是可读的,所以当然也是可以正常调用的。

7.第5、6章小总结

  1. 常量对象只能调用常量成员函数,不能调用普通成员函数。
  2. 普通对象能够调用常量成员函数,也能够调用普通成员函数。
  3. 常量指针和常引用都只能调用对象的常量成员函数。

本篇为视频教程笔记,视频如下:

C++远征之封装篇(下)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值