C++ 知识点精华剖析

对于面向过程的语言学习结束之后 , 我们就要尝试去面向对象编程了 ,接下来跟着"吃不到猫的鱼"一起继续学习C++吧!
在这篇博客里面我将会讲述C++的知识点 , 并且使用图解的方式对知识点进行剖析 , 保证每一个小白都能学习了我这篇博客之后有一番自己的感悟和对C++的理解

友情提示:代码内部会有相应的解释 , 如果有需要的地方 , 我会在代码块后面进行知识点图文讲解 , 请大家具体实例 , 具体分析 , 边看代码边看图解 , 效果更佳!!!

4:类和对象篇

4.2.5:深拷贝和浅拷贝

浅拷贝:编译器上的赋值操作
深拷贝:在堆区申请一片自己的空间 , 来进行拷贝操作

下面我给大家演示一个实例,我们在实例中边学习边理解

#include<iostream>
using namespace std;
class Person {
public :    //公共权限public , 在哪都能访问下面的属性权限
    int M_Age;    //属性定义在前面
    int* Height;    //身高 , 但是使用指针来开辟 , *解引用之后就是eight具体的数字
    Person() {
        cout << "无参构造函数" << endl;
    }
    Person(int Age , int height) {
        M_Age = Age;       //将传入的Age赋值到M_Age中去(也就是将Age的数值传入到对象的M_Age中来),实现对象的创建
        Height = new int(height);   // 直接创建一个新的Height属性 ,而且是以对象的创建形式来进行接收 , 因为Height本来就是一个指针变量 , 就是用来接收地址的  ,此时创建对象的动作 , 其实就是在堆区创建出来的一个对象 , 此时已经脱离栈区 ,在堆区已经开始申请空间了 , 但是堆区是一个很特殊的地方, 堆区的内存申请不像栈区会在程序结束的时候编译器自动回收 , 堆区的内存必须是程序员主动创建 , 主动回收(当然!!! 在程序关闭的时候要是程序员没有进行内存回收的话 , OS会帮助程序员实现内存回收的动作)
        cout << "有参构造函数" << endl;
    }
    ~Person() {
        cout << "析构函数" << endl;   //析构函数就是和构造函数是同时出现的 , 构造函数负责初始化对象 , 析构函数负责销毁对象 ,为空间提高内存使用率
        //析构函数就是当我们的程序员手动在内存的堆区创建的空间为了存放数据 , 在程序结束的时候就要帮助程序员回收创建的内存 , 我们调用一个if语句来看一下此时创建的堆区的数据有没有被系统释放 , 如果被释放了的话,那么这个申请的数据应该都是NULL , 但是要是系统没有帮我们回收数据的时候 , 此时我们就应该主动地delete 创建的数据 ,并且把它们置空,避免出现内存泄漏的问题
        if (Height != NULL) {
            delete Height;
            Height = NULL;
        }
    }
};
void Test1() {   //是一个测试接口 , 用来测试这次学习的知识点的代码
    Person p1(18 , 180);  //此时创建了一个对象p1 , 并且此时调用的构造函数是有参构造函数
    cout << "P1的年龄:" <<  p1.M_Age << "P1的身高是;" << p1.Height <<  endl;
    Person p2(p1);   //此时由于创建出来的对象p2其实是通过拷贝构造函数生成的 , 但是我们发现在类创建的时候, 我们没有发现拷贝构造函数呀 , 是因为编译器在用户没有创建构造函数的时候 , 就已经帮用户创建了构造函数
    cout << "P2的年龄:" << p2.M_Age << "P2的身高是;" << p2.Height << endl; //由于我们调用了默认拷贝构造函数(浅拷贝) , 所以此时将对象p1的所有信息已经传递到p2对象的内部 ,但是由于是拷贝的构造函数其实就要用一个新的对象来接收 , 所以两个对象的内存地址就不一致

}
int main(){
    Test1();
    return 0;
}

此时我们运行上述的代码时候 , 就会出现这样的一个问题(如下图):

为什么呢?

现在我帮大家回忆一下析构函数的作用 , 析构函数就是为了将这个创建的对象在堆区在程序结束的时候自动销毁 , 为内存空间节省空间的目的.

那么此时我给大家把这个代码的堆栈图画一下:

 此时我们可以看出 , 哎呀原来浅拷贝(系统会自动调用浅拷贝) 只能拷贝一份系统中的数据 , 并且实现共享 , 那这样我们创建了两个对象都需要析构函数回收内存的时候 , 最后一个析构函数就会去删除一个已经删除的堆数据,  此时就会出现"二次删除"的问题.

那怎么解决呢?

我们既然想要每一次都能够把堆区的数据都删除干净 , 而且不会报错,  那我们就在每一个对象的析构函数中主动在创建一个堆数据 , 每次在调用的时候 , 都去多删一个堆数据 , 就类似于

原来我有一个苹果 , 现在我又有一个孪生兄弟了 , 是我的克隆体 , 他也有一个苹果 , 但是他的苹果和我的苹果是同一个苹果(两个人共享一个苹果) , 等到我吃完的时候 , 那他的苹果也就不见了(类似于堆 , 共享堆变量) , 此时他想要吃苹果的时候 , 没了(但是他还从我的手中要夺回属于他的苹果) , 但是此时啥都没有 , 他就会生气 , 为了解决这个问题 , 我们可以在我吃完苹果的时候(在delete堆变量的时候在创建一个对象) 给他再买一个 , 我吃完了(堆变量删除之后) , 他也能再继续吃一个美味的苹果啦(此时因为有了创建一个新的堆数据 , 我们就可以实现这个对象在调用析构函数的时候 , 也能实现对堆内存的释放,从而避免了内存泄漏(弥补了浅拷贝的缺陷))的问题.

下面是新的代码 , 也有了解决之后的图解:

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Person {
public :    //公共权限public , 在哪都能访问下面的属性权限
    int M_Age;    //属性定义在前面
    int* Height;    //身高 , 但是使用指针来开辟 , *解引用之后就是eight具体的数字
    Person() {
        cout << "无参构造函数" << endl;
    }
    Person(int Age , int height) {
        M_Age = Age;       //将传入的Age赋值到M_Age中去(也就是将Age的数值传入到对象的M_Age中来),实现对象的创建
        Height = new int(height);   // 直接创建一个新的Height属性 ,而且是以对象的创建形式来进行接收 , 因为Height本来就是一个指针变量 , 就是用来接收地址的  ,此时创建对象的动作 , 其实就是在堆区创建出来的一个对象 , 此时已经脱离栈区 ,在堆区已经开始申请空间了 , 但是堆区是一个很特殊的地方, 堆区的内存申请不像栈区会在程序结束的时候编译器自动回收 , 堆区的内存必须是程序员主动创建 , 主动回收(当然!!! 在程序关闭的时候要是程序员没有进行内存回收的话 , OS会帮助程序员实现内存回收的动作)
        cout << "有参构造函数" << endl;
    }
    ~Person() {
        cout << "析构函数" << endl;   //析构函数就是和构造函数是同时出现的 , 构造函数负责初始化对象 , 析构函数负责销毁对象 ,为空间提高内存使用率
        //析构函数就是当我们的程序员手动在内存的堆区创建的空间为了存放数据 , 在程序结束的时候就要帮助程序员回收创建的内存 , 我们调用一个if语句来看一下此时创建的堆区的数据有没有被系统释放 , 如果被释放了的话,那么这个申请的数据应该都是NULL , 但是要是系统没有帮我们回收数据的时候 , 此时我们就应该主动地delete 创建的数据 ,并且把它们置空,避免出现内存泄漏的问题
        if (Height != NULL) {
            delete Height;
            Height = NULL;
        }
    }
    //-----------------------下面就是解决浅拷贝缺陷的代码(也就是深拷贝的代码)----------------------------------------
    Person(const Person& p) {
        M_Age = p.M_Age;   //重新将Age的属性进行赋值
        //因为浅拷贝是用户没有写出拷贝构造函数的时候系统自动调用的一个构造函数
        //Height = p.Height;  //这个是系统自己写的拷贝构造函数的代码
        //但是就是因为系统提供的浅拷贝的方法只有一个堆内存变量,等到析构函数调用的时候就会出现"二次删除"的错误操作 , 所以我们每一次在调用构造函数(拷贝新对象)的时候 , 就自己再写一个新的堆变量 , 供自己再去调用析构函数来删除 
        Height = new int(*p.Height);    ///在创建新对象的时候 , 就应该传入数据 , 而不是地址 , 此时我们已经为Height变量创建了新的堆变量 , 此时就不会发生报错信息 , 因为此时已经创造出来一个新的对象来供析构函数来删除了
    }
};
void Test1() {   //是一个测试接口 , 用来测试这次学习的知识点的代码
    Person p1(18 , 180);  //此时创建了一个对象p1 , 并且此时调用的构造函数是有参构造函数
    cout << "P1的年龄:" <<  p1.M_Age << "   P1的身高是;" << *p1.Height <<  endl;
    Person p2(p1);   //此时由于创建出来的对象p2其实是通过拷贝构造函数生成的 , 但是我们发现在类创建的时候, 我们没有发现拷贝构造函数呀 , 是因为编译器在用户没有创建构造函数的时候 , 就已经帮用户创建了构造函数
    cout << "P2的年龄:" << p2.M_Age << "   P2的身高是;" << *p2.Height << endl; //由于我们调用了默认拷贝构造函数(浅拷贝) , 所以此时将对象p1的所有信息已经传递到p2对象的内部 ,但是由于是拷贝的构造函数其实就要用一个新的对象来接收 , 所以两个对象的内存地址就不一致

}
int main(){
    Test1();
    return 0;
}




 此时我们再次聊一聊"浅拷贝"和"深拷贝"的问题 , 此时我相信大家就都已经明白了我刚才上述刚开始简述的问题了

浅拷贝 : 其实"浅拷贝"就是编译器自动在构造函数中为不同的对象创建一个堆内存 , 将不同的堆内存都共同使用这一片堆数据

深拷贝: "深拷贝"其实就是为了避免析构函数每次都回收内存 , 而又因为不同的对象都对这片堆内存有着共享的操作 , 深拷贝直接实现了 , "一个对象 , 一个堆数据"的自由 ,让析构函数回收不再出现错误.


4.2.6 初始化列表

我们经常在创建对象的时候 , 会使用构造函数来初始化对象的属性, 然而初始化对象的属性的操作很多 , 下面我将给大家简述一下"传统初始化方式"和"初始化列表"的两种方式的不同之处.

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;
class Person1 {
public :
    int M_A;
    int M_B;
    int M_C;
    //传统初始化操作
    Person1(int a , int b , int c) {
        M_A = a;
        M_B = b;
        M_C = c;
    }

};

void Test1() {   //此时测试的是类的传统初始化操作
    Person1 p1(10, 20, 30);      //此时我们调用的时候 , 就必须将对象的属性传递过去
    cout << "P1的M_A : " << p1.M_A << endl;
    cout << "P1的M_B : " << p1.M_B << endl;
    cout << "P1的M_C : " << p1.M_C << endl;
}


//下面是初始化列表的测试类
class Person2 {
public:
    //初始化列表初始化属性
    int M_A;
    int M_B;
    int M_C;
    //传统初始化操作
    Person2() : M_A(20), M_B(30), M_C(50) {   //此时的  类名: 变量1(属性值)  变量2(属性值)  变量3(属性值) 
        
    }

};
void Test2() {
    Person2 p;
    cout << "P的M_A : " << p.M_A << endl;
    cout << "P的M_B : " << p.M_B << endl;
    cout << "P的M_C : " << p.M_C << endl;
}
int main(){
    Test1();
    Test2();
    return 0;
}

我们在上面其实就会发现我定义了两个类 , 和两个Test测试类 , 我们发现第一个Person1的初始化方式就是我们常见的"传统初始化" , 将对象外部的数据传入到对象内部 

 Person1(int a , int b , int c) {
        M_A = a;
        M_B = b;
        M_C = c;
    }

但是!!第二个就是C++独有的"列表初始化"方式 , 

Person2() : M_A(20), M_B(30), M_C(50) {  

//此时的  类名: 变量1(属性值)  变量2(属性值)  变量3(属性值) 
        
    }

在调用这个类的构造函数的时候 , 也就是说初始化对象的时候 , 我们的调用方式就是这样

    Person2 p()  ;    //此时由于已经通过属性传值自动在构造函数的时候实现了赋值 , 那么就不用传参了

有的同学就会问:这个20 , 30 , 50是咋传递进去对象的呢?

其实20 , 30 , 40 就是通过"列表初始化" , 将20赋值给了对象Person中的M_A属性 , 将30赋值给了对象Person中的M_B属性 , 将40赋值给了对象Person中的M_C属性

此时我们也会发现一个问题 , 这个"列表初始化"的方式 , 好像不能直接传参 , 这就限制了程序的拓展性 , 此时我们应该做些什么了 , 于是我们想到之前C++好像有一个特性叫做"缺省参数" , 此时我们就可以调用缺省参数的方法 , 来实现这个构造函数接口的可延展性

  Person2(int a  , int b , int c) : M_A(a), M_B(b), M_C(c) {  

//此时的  类名: 变量1(属性值)  变量2(属性值)  变量3(属性值) 
        
    }

此时就实现了"列表初始化"的可延展性 , 让程序变得更加灵活 , 我们又学习到了一个新的对象初始化的技巧.


4.2.7 类对象作为类成员

在我们C++ / Java  中有这样的一种特性 , 创建出来的一个类 , 可以作为另外一个类的子类 , 也同时可以成为另一个类的父类 , 下面我们给大家简述一下 一个类成为另一个类的子类这种情况

下面我给大家写一个实例 , 在实例中来理解类作为另一个类的子类的情况:

#include<iostream>
#include<string>
using namespace std;

//类作为另一个类的子类
class Phone {
public :
    //手机名称
    string P_name;
    Phone(string name) {         //此时1我们创建一个有参构造方法来对对象的属性进行赋值
        P_name = name;
        cout << "此时调用了Phone的构造函数" << endl;
    }
    ~Phone() {
        cout << "此时调用了Phone的析构函数" << endl;

    }
};
class Person {
public :
    //名字
    string My_name;
    //手机
    Phone My_Phone;  //此时我们抽象出来一个手机对象

    Person(string name, string P_name) : My_name(name) , My_Phone(P_name)  {     //此处的My_Phone的赋值其实就是My_Phone.P_name = P_name  直接初始化了对象 ,并且对初始化对象的属性进行了赋值
        cout << "此时调用了Person的构造函数" << endl;
    }
    ~Person() {
        cout << "此时调用了Person的析构函数" << endl;
    }
};
void Test() {
    Person p1("张三", "iPhone 15 Pro ");
    cout << p1.My_name << " 拿着一部 "  <<   p1.My_Phone.P_name  << "手机" << endl;
}
int main(){
    Test();
    return 0;
}

对于上述的代码我给大家一步步剖析:

首先我们先创建一个Person类 , Person类是需要几个属性 , 我给了两个属性 , 一个是Person的name , 另一个是这个Person使用的手机的P_Name ,

class Person {
public :
    //名字
    string My_name;
    //手机
    Phone My_Phone;  //此时我们抽象出来一个手机对象

    Person(string name, string P_name) : My_name(name) , My_Phone(P_name)  {     //此处的My_Phone的赋值其实就是My_Phone.P_name = P_name  直接初始化了对象 ,并且对初始化对象的属性进行了赋值
        cout << "此时调用了Person的构造函数" << endl;
    }
    ~Person() {
        cout << "此时调用了Person的析构函数" << endl;
    }
};

但是在初始化Phone的name的时候 , 发现没有Phone这个类 , 于是我们重新创建一个Phone类

class Phone {
public :
    //手机名称
    string P_name;
    Phone(string name) {         //此时1我们创建一个有参构造方法来对对象的属性进行赋值
        P_name = name;
        cout << "此时调用了Phone的构造函数" << endl;
    }
    ~Phone() {
        cout << "此时调用了Phone的析构函数" << endl;

    }
};

此时我们分别调用他们的构造函数和析构函数 , 因为此时Phone对象其实就是Person对象的子类 , 我们想要一探究竟Phone对象和Person对象的构造方法和析构方法是什么样的情况 , 于是我们专门写了一个测试接口

void Test() {
    Person p1("张三", "iPhone 15 Pro ");
    cout << p1.My_name << " 拿着一部 "  <<   p1.My_Phone.P_name  << "手机" << endl;
}

在这个接口里面我分别调用了Person对象  , 因为Phone对象在Person对象中存在 , Phone是erson对象的子类 , 于是我们发现他们的构造方法和析构方法有下面的出现情况:

 

因为Phone类是Person的子类 , 在我们初始化Person类的时候,其实我们已经调用了Phone类的对象初始化 , 于是就有了下面的思维图形:

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值