• C++——类和对象(了解面向过程和面向对象、初步认识类和对象、类大小的计算、this指针)


    类和对象

    1. 面向过程和面向对象

    在学习C++类和对象之前,我们首先需要搞清楚什么是面向过程,什么是面向对象

    1.1 面向过程

    我们以前学的C语言就是典型的面向过程的语言

    面向过程编程是一种以过程为中心的编程方法。在这种范式下,程序被划分为一系列函数或过程,这些函数用于解决特定的问题

    例如:我们可以将用手洗衣服看作是面向过程的:

    • 通过”放水“”手搓“”拧干“等一系列过程来达到将衣服洗干净的目的。

    面向过程的语言有如下特点:

    • 数据和函数之间通常是分离的,函数对数据进行操作,数据可以是全局的或局部的。
    • 面向过程的编程语言常常使用顺序、条件语句和循环来执行任务。

    1.2 面向对象

    C++、Java、Python等语言都是面向对象的语言。

    面向对象编程是一种以对象为中心的编程方法。在这种范式下,程序被组织成一组对象,每个对象包含数据和与之相关的方法

    例如,我们可以将用洗衣机洗衣服看作是面向对象的:

    在这里插入图片描述

    • ”衣服“”洗衣粉“是我们要关注的对象,我们只需要将要处理的对象放入“洗衣机”中,让洗衣机处理即可。
    • 而不要关心”洗衣机“具体干了什么和它的工作原理。

    面向对象的语言有如下的特点:

    • 对象是类的实例,类是定义了对象的属性和方法的蓝图。
    • 面向对象编程强调数据封装、继承和多态,这些概念有助于组织和管理复杂的程序。

    2. 类和对象

    2.1 什么是类

    在C语言中,我们有struct类型,我们称之为结构体。例如:

    struct Stack
    {
    	int* st;
    	int top;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5

    但是,C语言的结构体有如下的局限性,这使得我们在使用时很不方便:

    1. 定义结构体变量时,类型名太长。例如我们要定义上面的结构体类型的变量st1
    struct Stack st1;
    
    • 1
    1. 结构体内只能声明变量,而不能声明和定义函数

    为了解决这些问题,C++规定:可以在struct里面声明和定义函数

    例如,在C++中,我们可以这样实现一个栈:

    struct Stack
    {
    	void Init(int capacity)
    	{
    		_capacity = capacity;
    		_st = (int*)malloc(sizeof(int) * _capacity);
    		_top = 0;
    	}
    
    	void Push(int val)
    	{
    		if (_top == _capacity)
    		{
    			_capacity *= 2;
    			int* tmp = (int*)realloc(_st, sizeof(int) * _capacity);
    			if (nullptr == tmp)
    				exit(-1);
    
    			_st = tmp;
    		}
    
    		_st[_top++] = val;
    	}
    
    	//仅为了展示C++struct里面可以声明和定义函数
    	//故其他功能不做展示
    
    	int* _st;
    	int _top;
    	int _capacity;
    };
    
    • 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
    • 在C++中,我们就称用struct关键字修饰的结构为
    • 同时,C++更喜欢用class来声明类,而不是struct

    C++的类由这样的特点:

    1. 类名就是类型名。例如:有一个类为class Stack,那么就可以用这个类名定义一个变量st1:Stack st1
    2. 类整体定义的是一个作用域(由一对花括号{}包裹起来的就是一个作用域)
    3. C++兼容C语言的绝大多数语法,可以说C++的类是C语言struct的升级

    2.2 类的定义

    类的定义方法为:

    //class也可以换为struct
    class className
    {
        //类体:由成员函数和成员变量组成
    };	//注意这个分号
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • class/struct为定义类要用到的关键字,className为类名
    • 类体中的变量称为类的属性或者成员变量,类体中的函数称为类的方法或者成员函数

    2.2.1 声明和定义类中函数的两种方法

    方法一——将声明和定义放在一起

    例如:

    class Date
    {
    public:
    	void Init(int year = 1, int month = 1, int day = 1)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    如果将成员函数的声明和定义都放到一起,那就需要注意:该函数可能被编译器认定为inline内联函数

    注:如果对inline内联函数不太了解,建议看看👉C++特性——inline内联函数

    方法二——将声明和定义分离

    如果采用”在类里面声明函数,在类外面定义函数“的方法,那就需要通过域作用限定符:,将类名和函数名连接起来,用来说明定义的函数是这个类里面的。

    例如:

    在这里插入图片描述


    在日常写代码中,我们可以将方法一和方法二结合来定义类:将复杂的、代码量大的函数定义在类外,将频繁使用的、代码简单的函数定义在类里面。这样不仅可以提高效率,而且可以提高代码的可阅读性。

    2.2.2 声明成员变量的小细节

    我们来看一个Date类的声明:

    class Date
    {
    	void Init(int year = 1, int month = 1, int day = 1)
        {
            year = year;
            month = month;
            day = day;
    	}
        
        void Print()
    	{
    		cout << "Date-> " << year << ':' << month << ':' << day << endl;
    	}
    
    	int year;
    	int month;
    	int day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    我们声明的类成员变量为year, month, day,成员函数Init的三个形参也同样为year, month, day,当我们进行赋值语句的时候,是否可以得到正确的结果呢?

    我们对其初始化,并打印:

    int main()
    {
    	Date d1;
    	d1.Init(2023, 10, 23);
    	d1.Print();
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    output:

    Date-> -858993460:-858993460:-858993460
    
    • 1

    可以看到,并没有得到我们想要的结果。

    • 因此,为了防止类似错误的出现,并提高代码的可阅读性

    • 在C++中,我们一般将内里面的成员变量的名字前加下划线_

    class Date
    {
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    2.3 访问限定符

    我们同样以stack类为例子:

    class Stack
    {
    	void Init(int capacity)
    	{
    		_capacity = capacity;
    		_st = (int*)malloc(sizeof(int) * _capacity);
    		_top = 0;
    	}
    
    	void Push(int val)
    	{
    		if (_top == _capacity)
    		{
    			_capacity *= 2;
    			int* tmp = (int*)realloc(_st, sizeof(int) * _capacity);
    			if (nullptr == tmp)
    				exit(-1);
    
    			_st = tmp;
    		}
    
    		_st[_top++] = val;
    	}
    
    	//仅为了展示C++struct里面可以声明和定义函数
    	//故其他功能不做展示
    
    	int* _st;
    	int _top;
    	int _capacity;
    };
    
    • 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

    我们将储存数据的数组st,栈顶指针top,栈的最大容量capacity及其相关方法(成员函数)放入stack类后,

    • 一般来说,我们并不希望用户能直接修改sttopcapacity的内容,
    • 而是希望用户能够调用内里面的方法(成员函数)来间接地改变sttopcapacity,来实现栈的功能
    • 就像C语言是通过调用函数来操作栈,而不是直接操作栈的相关参数。

    因此为了限制用户访问类成员的权限,C++有了关键字——访问限定符

    在这里插入图片描述

    访问限定符有以下三类:

    • public(公有):public修饰的成员可以在类外直接被访问
    • protected(保护)、private(私有):现阶段我们认为protectedprivate没有区别的。被他们修饰的类成员不能在内外访问

    2.3.1 访问限定符的作用范围

    访问限定符的作用范围:

    • 从该访问限定符出现开始
    • 到下一个访问限定符出现结束

    例如:

    class Date
    {
    public:
    	void Init(int year = 1, int month = 1, int day = 1);
    
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    Date类里面,成员函数Initpublic修饰,可以在类外被访问,成员变量_year_month_dayprivate修饰,不能在类外被访问。

    2.3.2 class类和struct类的默认访问权限

    需要清楚,如果不在类里面加访问限定符

    • class类的默认访问权限是private
    • struct类的默认访问权限是public

    例如:

    class Date
    {
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	Date d1;
    	d1._year = 1;
    
    	return 0;
    }
    
    //会报错:无法访问 private 成员(在“Date”类中声明)
    //这就说明了:class类的默认访问权限就是private
    //而如果将class改为struct,那么就可以正常运行
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    2.4 类的实例化

    用类定义一个对象的过程就叫做类的实例化

    需要注意:

    • 当我们只是声明一个类时,这个类是并不会占据任何空间的。因为类里面只是对各成员的声明,而没有开辟任何空间
    • 只有当我们用类实例化出一个对象,我们才可以对类成员进行引用等操作
    • 一个类可以实例化多个对象

    例如:

    struct Date
    {
    	int _year = 1;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	Date._year = 1;
        //会报错:error C2059: 语法错误:“.”
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    我们也可以将类比作是构造图,将对象比作是房子,来理清二者之间的关系:

    • 构造图只是一张图纸,不会占据土地空间——类只是对成员的声明,不会开辟空间
    • 由构造图建造出的房子会占用实际的土地空间——由类实例化出的对象会开辟空间来存储各成员
    • 一张构造图可以建造出许多房子——一个类可以实例化多个对象

    2.5 类大小的计算

    当类中没有成员函数时,类所占空间的大小遵循C语言结构体大小的计算规则

    • 结构体的第一个成员永远放在相较于结构体变量起始位置偏移量为0的位置

    • 从第二个成员开始,往后的每个成员都要对齐到某个对齐数的整数倍处
      - 对齐数:结构体成员自身大小和默认对齐数的较小值

    • 结构体的总大小必须是最大对齐数的整数倍
      - 最大对齐数:所有成员的对齐数中的最大值

    例如:

    class Grade
    {
    	int _number;
    	double _math;
    	float _chinese;
    };
    
    int main()
    {
    	cout << sizeof(Grade) << endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    output:

    24
    
    • 1

    注:如果对于结构体大小的计算不了解,建议看看👉C语言结构体详解

    但是,如果类里面有成员函数呢?这个类的大小又是多少呢?

    例如:

    struct Date
    {
    	void Init(int year = 1, int month = 1, int day = 1);
    
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	cout << "sizeof(Date) -> " <<  sizeof(Date) << endl;
    
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    要搞清楚C++类的大小到底怎么计算,我们首先就要搞清楚类对象的存储方式到底是怎么样的。

    2.5.1 类对象的存储方式

    让我们来思考一个问题:

    用一个类创建多个对象时,类中的成员变量需要多开辟一份吗?类中的成员函数需要多开辟一个吗?

    如果想不清楚,我们仍可以用建房子来类比

    • 将房子中的卧室、厕所、厨房等私用设施比作是成员变量;将房子外的公园、亭子等公用设施比作是成员函数
    • 显然,当我们用一份构造图建造多个房子时,房子的厕所、卧室肯定是要重新新建的,而房子外的公园、亭子用原来的就好

    用类实例化多个对象也是如此:不同的对象所包含的成员变量为各自所有,需要重新开辟,而成员函数是这些对象共有的,不要开辟

    因此,类对象的存储方式应该是这样的:

    类对象只存储成员变量,而成员函数放在公共代码区

    在这里插入图片描述


    既然类对象的成员变量并不和成员函数存放在一起,那么自然计算类的大小时,也就不需要考虑成员函数了。

    所以:

    struct Date
    {
    	void Init(int year = 1, int month = 1, int day = 1);
    
    	int _year;
    	int _month;
    	int _day;
    };
    
    //Date类所占大小就是12
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    那么,空类的大小又是多少呢?

    class Test
    {
        
    };
    
    • 1
    • 2
    • 3
    • 4

    在这里插入图片描述

    C++规定:空类的大小为1,这个字节不存储有效数据。用来标识定义的对象存在过


    可以总结:

    • 空类的大小为1个字节
    • 类的大小实际上是“成员变量的大小之和”(注意内存对齐)
    • 成员函数存放在公共代码区,不用计算

    2.6 this指针

    看下面的代码:

    class Date
    {
    public:
    	void Init(int year = 1, int month = 1, int day = 1)
    	{
    		_year = year;
    		_month = month;
    		_day = day;
    	}
    private:
    	int _year;
    	int _month;
    	int _day;
    };
    
    int main()
    {
    	Date d1, d2;
    
    	d1.Init(2023, 10, 23);
    	d2.Init();
    
    	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

    上面的代码中,我们定义了类Date,同时示例化了两个对象d1, d2

    现在就要问大家一个问题:既然这两个对象用的都是同一个Init()函数,那编译器是怎么知道他要处理的是d1的成员变量还是d2的成员变量?

    C++通过引入this指针来处理这个问题:

    C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成

    上面的Init()函数和d1.Init(2023, 10, 23);实际上等价于:

    void Init(Date* this, int year = 1, int month = 1, int day = 1)
    {
        this->_year = year;
        this->_month = month;
        this->_day = day;
    }
    
    d1.Init(&d1, 2023, 10, 23);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.6.1 this指针的特性

    1. this指针被*const修饰:* const this。表示:不能修改this指针的指向this至指向当前对象),但是可以修改this指针指向空间的值(可以通过this指针访问成员变量)
    2. 不能写this相关的形参或实参,但是可以在类的成员函数里面显示的使用
    3. this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
    4. this指针可以为空

    最后,我们用一道题来结束本篇文章:

    #include 
    using namespace std;
    
    class Test
    {
    public:
    	void Print1()
    	{	
    		cout << "Print()" << endl;
    	}
    
    	void Print2()
    	{
    		cout << _a << endl;
    	}
    
    private:
    	int _a;
    };
    
    int main()
    {
    	Test* t1 = nullptr;
        
        t1->Print1();	//Yes or Not ?
    	t1->Print2();	//Yes or Not ? 
    
    	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

    大家认为这个程序会得到什么结果呢?

    我们来进行调试:

    在这里插入图片描述

    为什么会出现这种情况呢?

    • 我们定义了一个指向Test类对象的指针t1,但将其赋为空指针nullptr

    • 函数Print1()并没有访问类成员变量,而且成员函数存放在公共代码区,因此尽管this指针为空,我们也可以正常使用Print1()

    • 函数Print2()实际上可以写为:

      void Print2(Test* this)
      {
          cout << this->_a << endl;
      }
      
      • 1
      • 2
      • 3
      • 4

      由于this指针为空,空指针并不指向任何有效数据,显然就会发生错误。


    3. 总结

    本次我们对类和对象进行了初步的了解和学习:知道了面向过程和面向对象的基本概念,知道了类的定义和类的实例化等相关概念和操作。
    但C++类和对象的知识远不止于此。后面我们将继续学习关于类和对象的构造函数、析构函数、拷贝函数、运算符重载的知识,感兴趣的小伙伴可以订阅此专栏。
    👉C++教程
    请添加图片描述

  • 相关阅读:
    Spring Boot 2.6.x整合Swagger启动失败报错问题解决(治标还治本)
    文娱行业搜索最佳实践
    ClickHouse 的 MergeTree 引擎读写流程
    【Git学习三】git中的分支管理:git branch,git checkout,解决git中的分支冲突的方法
    Leetcode刷题【hot100】盛最多水的容器
    【RK3588】YOLO V5在瑞芯微板子上部署问题记录汇总
    开源AI模型:未来发展的领航者!
    【算法】【递归与动态规划模块】逻辑表达式组成期望结果的所有种数计算
    SegNeXt Rethinking Convolutional Attention Design for Semantic Segmentation
    如何在Django中使用django-crontab启动定时任务、关闭任务以及关闭指定任务
  • 原文地址:https://blog.csdn.net/l315225/article/details/134019074