• 【C++】C++学习记录.DAY4-类的详解


    0.引子

    下面以一个例子来讲解类的简单用途

    Gun.h

    #pragma once
    #include <string>
    
    class Gun{
        public:
            Gun(std::string type){
                this->_bullet_count = 0;
                this->_type = type;
            }
    
            void addBullet(int bullet_num);
            bool shoot();
    
        private:
            int _bullet_count;
            std::string _type;
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    Gun.cpp

    #include "Gun.h"
    #include <iostream>
    using namespace std;
    
    void Gun::addBullet(int bullet_num){
        this->_bullet_count += bullet_num;
    } 
    bool Gun::shoot(){
        if(this->_bullet_count <= 0){
            cout << "There is no bullet!" << endl;
            return false;
        }
        this->_bullet_count -= 1;
        cout << "shoot successfully!" << endl;
        return true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    1.构造函数

    构造函数是一种特殊的成员函数,可以在创建对象的的时候自动调用构造函数的作用是初始化对象中的数据成员

    构造函数的名称必须与类名相同,其没有返回值类型。

    构造函数可以重载(无参构造函数、有参构造函数、拷贝构造函数)。

    在 Gun.h 中,Gun(std::string type) 为构造函数。

    Gun(std::string type){
        this->_bullet_count = 0;
        this->_type = type;
    }
    
    • 1
    • 2
    • 3
    • 4

    可以看出,这个构造函数是一个有参构造函数

    因此,我们在 new 的时候便可以直接向这个类里传入参数,例如new Gun("AK47")

    关于 new 的用法:((20220604194429-cozdgou ‘new’))

    在本文中,这里的new实际上是执行如下3个过程(这三个过程都在一个new operator中完成):

    1. 调用malloc/heap_alloc分配内存 ;2. 调用构造函数生成类对象;3. 返回相应指针

    所以,我们使用new Gun("AK47")的时候,实际上是返回了一个指向 Gun 类的指针,于是可以这样做:

    Gun *ptr_gun= new Gun("AK47")
    
    • 1

    同时,初始化这个新对象中的数据成员

    关于 this 关键字:

    在C++里面,每一个对象都能通过this指针来访问自己的地址,即它指向当前的对象,通过它可以访问当前对象的所有成员,是所有成员函数的隐藏参数(所以只能用在成员函数的内部)。

    通过 this 可以访问类的所有成员,包括 private、protected、public 属性。

    例如在上述构造函数中的语句中,将成员变量 _bullet_count 初始化为0,_type 初始化为传入对象的变量。

    this->_bullet_count = 0;
    this->_type = type;
    
    • 1
    • 2

    以上,便是通过构造函数来初始化对象的内容。

    2.构造函数的其它方法

    上述举例了括号法中有参构造函数的使用,括号法还包括无参和拷贝构造函数,下面介绍拷贝构造函数的使用。

    我们可以在Gun类中加入一个新的构造函数:

    // 拷贝构造函数
    Gun(const Gun &g){
        this->_bullet_count = 0;
        this->_type = g.type;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    这样,在main函数中我们便能复制之前创建的对象给新的对象使用:

    int main(){
    	Gun gun1("AK47");
    	Gun gun2(gun1);
    	Gun gun3 = gun1; // 等号法初始化调用拷贝构造函数
    	Gun gun4; //调用了无参构造函数
        	gun4 = gun1; //这里是等号赋值!!!和等号法初始化是两个概念
                 //不调用构造函数,而是执行赋值操作,把gun1的数据赋值给gun4 (默认=浅拷贝,将在后面介绍)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    注意,上面的 gun1 是一个类指针,指向内存中存放的类对象,所以在构造函数中我们才要取其地址。

    那么,在这里说一下类指针类对象,关于二者的应用我们已经在上文尝试过一遍了。

    类的指针: 他是一个内存地址值,他指向内存中存放的类对象(包括一些成员变量所赋的值).
    对象: 他是利用类的构造函数在内存中分配一块内存(包括一些成员变量所赋的值).

    指针变量是间接访问,但可实现多态通过父类指针可调用子类对象 )。
    直接声明可直接访问,但不能实现多态。

    类的对象:用的是 内存栈, 是个局部的临时变量.
    类的指针:用的是 内存堆, 是个永久变量,除非你释放它,所以在 new 之后一定要 delete

    在应用时,成员变量通过类的析构函数来释放空间,函数中的临时变量会在函数结束后自动释放,而指针需利用 delete 在相应的地方释放分配的内存块.

    C++的精髓之一就是 多态性,只有指针或者引用可以达到多态对象不行
    所以在函数调用时,我们通常传类指针。不管对象或结构参数多么庞大,用指针传过去的只有4个字节。如果用对象,参数传递占用的资源就太大了

    至此,我们已经大致理解了类指针和类变量的区别,接下来理解下面两个例子:

    参考博客:https://blog.csdn.net/qq_43471489/article/details/123018837

    void ParaFuncTest1(MyClassA A)
    {
        A.PrintData();
    }
    
    void ParaFuncTest2(MyClassA& A)
    {
    	A.PrintData();
    }
    
    void ParaFuncTest3(MyClassA* A)
    {
    	A->PrintData();
    }
    
    //第二种调用场景:类定义对象做函数参数
    void ClassTest2()
    {
        MyClassA A1(1, 2);
    
        ParaFuncTest1(A1); //实参A1初始化形参A对象元素的时候,会调用拷贝构造函数
        ParaFuncTest2(A1); //不会调用拷贝构造函数,因为引用是变量别名(A1的别名A,因为在同一个地址),引用传递并没有出现新对象,
        				   //只是给现有对象起个别名进行传递
        ParaFuncTest3(&A1); //不会调用拷贝构造函数,因为传递的是对象A1的地址,并没有新的对象元素出现
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25

    3.构造函数初始化列表

    如果我们想在一个类中初始化另一个类,则需要掌握构造函数初始化列表的使用方法。

    Soldier.h

    #include <string>
    #include "Gun.h"
    
    class Soldier{
        public:
            Soldier(std::string name);
            ~Soldier();
            void addBulletToGun(int num);
            bool fire();
        private:
            std::string _name;
            Gun _gun;
    
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    Soldier.cpp

    #include "Soldier.h"
    
    Soldier::Soldier(std::string name) : _gun("AK47"){
        this->_name = name;
    }
    
    void Soldier::addBulletToGun(int num){
        this->_gun.addBullet(num);
    }
    
    bool Soldier::fire(){
        return this->_gun.shoot();
    }
    
    Soldier::~Soldier(){}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    这样,我们在初始化Soldier对象的时候,会初始化该对象中的成员对象Gun,并为其传入初始值“AK47”

    更多初始化列表内容,参考博客https://blog.csdn.net/yili_xie/article/details/4803428

    以及https://blog.csdn.net/weicao1990/article/details/81536022

    4.析构函数

    在上文中可以看到一个函数~Soldier(),这个便是析构函数。

    析构函数是清理对象资源的一类特殊成员函数,析构函数的名称是类名前加~号,它在对象释放前自动调用,无参数(因为没有参数,所以无法重载—函数重载的判断依据是参数类型、参数顺序、参数个数)、无返回值类型且析构函数禁止使用return语句。

    了解了析构函数后,可以看学习下面的类函数思想:

    5.匿名对象

    详细参考:https://blog.csdn.net/wysnkyd/article/details/82709243

    以及https://blog.csdn.net/u014583317/article/details/108705360

    下面是构造函数的第三种调用场景,先看一遍下述代码,捋清逻辑,从而开始匿名对象的学习。

    MyClassA RetuFuncTest1()
    {
        MyClassA A(1, 2);
        return A; //执行 return A;会先产生一个匿名对象,执行拷贝构造函数,此对象作为返回值返回,
                  //然后释放临时对象A,A的生命周期到此结束(是否执行析构函数视情况而定)
    
    
    //MyClassA& RetuFuncTest2()
    //MyClassA* RetuFuncTest3()
    //{
    //	MyClassA A(1, 2);
    //	return &A; //A是局部变量,不能返回它的地址
    //}
    
    //第三种调用场景:函数返回类型为类定义的元素
    void ClassTest3()
    {
        RetuFuncTest1(); //如果不用变量来接这个函数,那么会在 RetuFuncTest1() 函数的 return A;
                         //语句处调用拷贝构造函数,并立即执行析构函数
                         //这是因为,函数返回一个对象元素,而局部变量A的生命周期只在函数体内,
                         //不能返回出来,所以会在return时创建一个匿名对象,主调函数种若没有
                         //对象元素来接,那么会立即调用析构函数把匿名对象析构
        //RetuFuncTest2();
        //RetuFuncTest3();
    
        MyClassA A1 = RetuFuncTest1(); //这里不会再次调用拷贝构造函数,
        			           //因为编译器会把函数RetuFuncTest1()
                                       //返回出来的匿名对象直接转化为A1,因此匿名对象不会被析构,
                                       //在RetuFuncTest1()函数结束时只调用一次析构函数来析构局部变量A
                                       //匿名对象已经分配好了资源,并直接转化为A1,
        			           //所以A1初始化不需要再次调用拷贝构造函数
        A1.PrintData();
    
        MyClassA A2; //调用无参构造函数
        A2 = RetuFuncTest1(); //这是等号赋值操作!!!此时匿名对象也不会立即析构,而是在执行完这句话,
                              //对A2赋值完之后,执行析构函数,析构匿名对象
        			  //(区别于匿名对象初始化A1,匿名对象转为A1,不会析构)
        A2.PrintData();
    } //生命周期结束,析构所有局部变量 A1(匿名对象) A2
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39

    在上述代码中,我们看到了一个新的名词——匿名对象,让我们来学习一下。

    产生匿名对象的三种情况:
      1)以值的方式给函数传参;
      MyClassA(1, 2); —> 生成了一个匿名对象,执行完其构造函数中的代码后,此匿名对象就此消失,执行析构函数。这就是匿名对象的生命周期。
      MyClassA A= MyClassA(1, 2); —>首先生成了一个匿名对象,然后将此匿名对象变为了A对象,其生命周期就变成了A对象的生命周期。

    2)类型转换;

    3)函数需要返回一个对象时:return A;

    注意,在上述代码中,如果在MyClassA A1 = RetuFuncTest1();的后面调用拷贝构造函数 MyClassA(A1);会报错,因为当MyClassA(A1)有变量来接的时候,编译器认为他是一个匿名对象。当没有变量来接的时候,编译器认为你MyClassA(A1)等价于MyClassA A1,所以就造成了重定义错误。

    6.拷贝构造函数中的深拷贝与浅拷贝

    浅拷贝:简单的赋值拷贝操作,在上文中已经用过(2.中的拷贝构造函数,以及5.中的等号赋值操作,都属于浅拷贝)。

    深拷贝:在堆区重新申请空间,进行拷贝操作,解决浅拷贝带来的堆区重复释放问题。如果堆区有内存,这时需要在析构代码里将堆区的内存释放掉。

    代码来源:https://blog.csdn.net/qq_40618919/article/details/118179069

    class Person
    {
    public:
    	Person()
    	{
    		cout << "Person的默认构造函数调用" << endl;
    	}
    	Person(int age , int height)
    	{
    		m_Age = age;
    		m_Height =  new int(height); 
    		cout << "Person的有参构造函数调用" << endl;
    	}
    
    
    	自己实现拷贝构造函数 解决浅拷贝带来的问题
    	Person(const Person &p)
    	{
    		cout << "Person 拷贝构造函数调用" << endl;
    		m_Age = p.m_Age;
    	
    		//编译器默认实现就是下面的这行代码,浅拷贝,会造成堆区重复释放问题
    		//m_Height = p.m_Height; 	
    		
    		//深拷贝操作,即在堆区创建一块内存
    		m_Height = new int(*p.m_Height);
    
    	}
    
    	~Person()
    	{
    		//析构代码,将堆区开辟数据做释放操作
    		if (m_Height != NULL)
    		{
    			delete m_Height;
    			m_Height = NULL;
    		}
    		cout << "Person的析构函数调用" << endl;
    	}
    
    	int m_Age; //年龄
    	int *m_Height; //身高 
    };
    
    void test1()
    {
    	Person p1(18 , 160);
    	cout << "p1的年龄为: " << p1.m_Age << " 身高为: " << *p1.m_Height <<endl;
    
    	Person p2(p1);
    	cout << "p2的年龄为: " << p2.m_Age << " 身高为: " << *p2.m_Height << endl;
    }
    
    int main() 
    {
    	test1();
    
    	system("pause");
    	return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61

    我们又会注意到,什么是堆区重复释放?

    参考:https://blog.csdn.net/weixin_46195203/article/details/114386453

    首先我们知道:浅拷贝Person p2(p1);)之后两个对象(P1,P2)指向同一个堆区地址;当P1执行析构函数时,释放掉了该堆区地址的内容。当P2执行析构函数时,也会释放掉该堆区地址的内容,但此时堆区的内容已经释放完了。就会报错了

    当深拷贝时,p2指向的新的堆区地址,这样在p1析构后,p2也能正常析构。

    在这里插入图片描述

    补充new的用法:m_Height = new int(height);

    参考:https://blog.csdn.net/h799710/article/details/107794434

    int *a = new int(10); //动态创建整型数,无参数是 * a=0,有参数则 * a = 参数
    int *p = new int[10]; //创建一个有10个元素的动态整型数组,没有赋值,元素为随机数
    int *p = new int[10] (); //创建一个有10个元素的动态整型数组,并都赋值为0

    注:当其他类对象作为本类成员,构造时候先构造类对象,再构造自身,析构的顺序与构造相反

    7.静态成员

    在成员变量和成员函数前加上关键字 static , 称为静态成员。

    静态成员变量:

    • 所有对象共享同一份数据
    • 在编译阶段分配内存
    • 类内声明,类外初始化
    class Person
    {
    public:
    	static int m_A;
    private:
    	static int m_B;
    };
    int Person::m_A = 100;
    int Person::m_B = 200;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    注:类外访问不到私有静态成员变量,例如本案例中的 m_B

    静态成员函数:

    • 所有对象共享同一个函数
    • 静态成员函数只能访问静态成员变量
    class Person
    {
    public:
    
       //静态成员函数
       static void func()
       {
       	m_A = 100; //静态成员函数可以访问 静态成员变量
       	//m_B = 200; //静态成员函数 不可以访问 非静态成员变量,无法区分到底是哪个对象的m_B属性
       	cout << "static void func调用 " << endl;
       }
    
       static int m_A; //静态成员变量
       int m_B; // 非静态成员变量
    
       //静态成员函数也是有访问权限的
    private:
       static void func2()
       {
       	cout << "static void func2调用" << endl;
       }
    };
    
    int Person::m_A =0;
    
    //有两种访问方式
    void test01()
    {
       //1、通过对象访问
       Person p;
       p.func();
    
       //2、通过类名访问
       Person::func();
    
       //Person::func2(); 类外访问不到私有静态成员函数
    }
    
    int main() 
    {
       test01();
       
       system("pause");
    
       return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
  • 相关阅读:
    如何开始着手一篇Meta分析 | Meta分析的流程及方法
    SeataAT模式如何达到读已提交的隔离级别
    模板多多的BI系统,绝不在可视化大屏制作上多花一秒
    基于数据库(MySQL)与缓存(Redis)实现分布式锁
    快速构建基本的SpringCloud微服务
    关于yarn安装和运行的错误
    629. K个逆序对数组
    Postman的高级用法—Runner的使用​
    二进制部署MySQL8.0
    java毕业设计球馆预约管理系统mybatis+源码+调试部署+系统+数据库+lw
  • 原文地址:https://blog.csdn.net/qq_43557907/article/details/125505783