【C++】初识类和对象

news2024/12/22 18:48:34

🏖️作者:@malloc不出对象
⛺专栏:C++的学习之路
👦个人简介:一名双非本科院校大二在读的科班编程菜鸟,努力编程只为赶上各位大佬的步伐🙈🙈
在这里插入图片描述

目录

    • 前言
      • 一、面向过程和面向对象初步认识
      • 二、类的引入
      • 三、类的定义
      • 四、类的访问限定符及封装
        • 4.1 访问限定符
        • 4.2 封装
      • 五、类的作用域
      • 六、类的实例化
      • 七、类对象模型
        • 7.1 如何计算类对象的大小
        • 7.2结构体内存对齐规则
      • 八、this指针的引出
        • 8.1 this指针的特性


前言

本篇文章我们将来初步认识一下C++中的类和对象,了解面向对象与面向过程之间有什么区别。

一、面向过程和面向对象初步认识

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

二、类的引入

C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。

struct Student
{
	void Func()
	{
		cout << "hello world" << endl;
	}
	
	char* _name;
	int _age;
	char _sex;
};

C++ 中的结构体里能定义成员变量(成员属性),也能定义成员函数(成员方法),所以 C++ 的结构体就升级成了类。而 C 语言的结构体只能定义成员变量,其成员函数不能在结构体中定义。

三、类的定义

class Student
{
	// 类体:由成员函数和成员变量组成
}; //这个分号一定要记得写

class为定义类的关键字,Student为类的名字,{}中为类的主体。注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数

类的两种定义方式:

  • 声明和定义全部放在类体中
    在这里插入图片描述

注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。

  • 类声明放在.h文件中,成员函数定义放在.cpp文件中。注意:成员函数名前需要加类名::表示成员函数在哪个类域中。

在这里插入图片描述

四、类的访问限定符及封装

4.1 访问限定符

C++ 实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。

在这里插入图片描述

访问限定符说明

  1. public修饰的成员在类外可以直接被访问
  2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  4. 如果后面没有访问限定符,作用域就到 } 即类结束。
  5. class的默认访问权限为private,struct为public(因为struct要兼容C)

在简单了解访问限定符之后,我想问大家一个问题既然C++中的结构体升级为类了,那么它与class之间有什么区别?

C++ 需要兼容 C 语言,所以 C++ 中struct可以当成结构体使用。另外 C++ 中 struct 还可以用来定义类。和 class 定义类是一样的,区别是 struct 定义的类默认访问权限是 public,class 定义的类默认访问权限是 private。注意:在继承和模板参数列表位置,struct 和 class 也有区别,后序给大家介绍。

注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别。

4.2 封装

面向对象的三大特性:封装、继承、多态。

在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

封装本质上是一种管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如 何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以 及键盘插孔等,让用户可以与计算机进行交互即可。

在 C++ 中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。

五、类的作用域

类定义了一个新的作用域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 ::作用域操作符指明成员属于哪个类域。

// Person.h
class Person
{
public:
	void PrintPersonInfo();
private:
	char _name[20];
	char _gender[3];
	int _age;
};

// Person.cpp
#include "Person.h"

// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
	cout << _name << " "<< _gender << " " << _age << endl;
}

六、类的实例化

1.类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。
2.一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。
3.做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。

在这里插入图片描述

用类类型创建对象的过程,称为类的实例化。简单来说,类的的实例化就是为对象实际开辟了空间。

下面来看几个常见的例子:

#include<iostream>
using namespace std;

class Person
{
public:
	void showInfo();

	// 以下在类中的成员变量都是声明,因为它们都没有开辟空间,只有当类实例化对象时它们才有存储空间
	const char* _name;
	const char* _sex;
	int _age;
};

int main()
{
	//Person 类是没有空间的,只有 Person 类实例化出的对象才有具体的年龄。
	// Person._age = 10;  不行
	//Person::_age = 10;  不行

	Person man;  // 实例化一个对象man,此时成员变量有实际的空间了
	man._age = 10;
	man._name = "张三";
	man._sex = "男";

	cout << man._age << " " << man._name << " " << man._sex << endl;

	return 0;
}

在这里插入图片描述

七、类对象模型

7.1 如何计算类对象的大小

大家可以试着先计算一下这个类对象的大小

class A
{
public:
	void PrintA()
	{
		cout<<_a<<endl;
	}
private:
	char _a;
};

问题:类中既可以有成员变量,又可以有成员函数,那么一个类的对象中包含了什么?如何计算一个类的大小?

在这里插入图片描述

计算类对象跟我们之前讲过的结构体大小一样都是要考虑内存对齐的,sizeof(A)等于 1 说明该类只存储了成员变量_a,成员函数没有计算进来。

为什么成员变量在对象中,成员函数不在对象中?

因为每个对象的成员变量是不一样的,它们都在各自对象的存储空间当中;而每个对象调用成员函数是一样的,所以我们不需要将成员函数加入到每个对象的存储空间当中,这样会极大的浪费我们的空间,因此我们的成员函数不放在对象中,而是放到共享公共区域(代码段)。

7.2结构体内存对齐规则

1.第一个成员在与结构体偏移量为0的地址处。
2.其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8.
3.结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
4.如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。

类对象的大小跟之前我们在C语言中讲的结构体内存对齐规则是一样的,这里我就不做过多的介绍了,下面我们一起来看看特殊的例子:

class A1 {};  //空类

class A2
{
	void f2(){}
};

int main()
{
	cout << sizeof(A1) << endl;
	cout << sizeof(A2) << endl;

	return 0;
}

我们来进行分析一下,A1类是一个空类,A2类中只有一个成员函数,在上面我们分析过成员函数是不被计算在内的,所以A1和A2类其实在大小上两者是相等的,所以它们的大小都为0?我们来看看结果:

在这里插入图片描述

我们发现A1和A2类的大小都为1,这是为何?我们来看看下面的例子:

在这里插入图片描述

从上图我们发现A1和A2类都实例化了一个对象,那么假设它们的大小为0的话,它们的对象怎么可能会有对应的地址?那么这个1又是怎么计算出来的呢?

这一个字节不存储有效数据,它是用来占位标识对象被实例化定义出来了!!

结论:没有成员变量的类占1个字节大小,这1个字节用来占位标识对象被实例化定义出来了。

八、this指针的引出

我们先来看一个例子:

#include <iostream>
using namespace std;

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}

private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date d1;
	Date d2;
	d1.Init(2022, 11, 11);
	d2.Init(2023, 2, 24);

	return 0;
}

在上述内容中我们已经得知成员函数它是被所有对象所共享的,那么在这个例子中我想请问我们是如何知道是哪个对象调用的成员函数,有的读者说在main函数中这不是清清楚楚的写着d1、d2调用Init函数吗?在main函数中调用Init函数传递相同的参数,那么在Init函数内部你知道year、month以及day是给哪个对象的成员变量赋值吗?

这里其实C++在这里做了一个隐式的处理,使用了this指针使得我们清楚的知道此时是对哪个对象做的操作:

在这里插入图片描述

编译器在编译之后隐式的帮我们进行了处理,在主函数中d1对象调用成员函数就传入d1对象的地址,d2对象调用成员函数就传入d2对象的地址,在Init函数内部用一个指针函数this接收,这样就能知道此时year、month以及day是为哪个对象赋值了。这是编译隐式的帮我们进行了处理,我们不可显式的进行这样的处理,因为这是编译干的活,但是我们可以在成员函数内部显式的使用this:

在这里插入图片描述

这里额外的给大家讲一个关于成员变量命名风格的问题,我们来看看下面这个例子:

#include <iostream>
using namespace std;

class Date
{
public:
	void Init(int year, int month, int day)
	{
		year = year;
		month = month;
		day = day;
	}

	int year;
	int month;
	int day;
};

int main()
{
	Date d1;
	Date d2;
	d1.Init(2022, 11, 11);
	d2.Init(2023, 2, 24);
	cout << d1.year << " " << d1.month << " " << d1.day << endl;
	cout << d2.year << " " << d2.month << " " << d2.day << endl;;

	return 0;
}

大家想一想这里的结果会是多少?

在这里插入图片描述

为什么这里成员变量赋值未成功呢?

这是因为我们在成员函数内部局部变量优先被使用,成员函数形参接收到主函数传递过来的实参又被赋给了形参,所以我们的成员变量根本没有被赋值,因此我们看到打印的都是随机值。

第一种解决方案:使用this指针显式说明此时的变量为对象的成员变量。

在这里插入图片描述

关于这点也许有些读者还有一个疑问,既然之前讲过编译器在编译之后会隐式的使用this指针对成员变量进行指示,那么这里为什么会赋值不成功呢?

这其实就是语法规定了我们的成员变量与形参原则上是不要同名的,所以这里我们需要显式的使用this指针指明对象。

第二种解决方案:重命名成员变量的名称。

在这里插入图片描述

相较于第一种解决方案,我更喜欢这一种解决方案,这样可以避免频繁的使用this显示的指明对象。
关于成员变量的命名有很多中,目前来说我更喜欢使用前缀_ + 单词的命名方式,当然了自己喜欢使用哪种就用哪种即可。

8.1 this指针的特性

1.this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
2.只能在“成员函数”的内部使用
3.this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给
this形参。所以对象中不存储this指针。
4.this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传
递,不需要用户传递

在这里插入图片描述


下面我们来看两个经典的面试题

Q1:this指针存在哪里?

this指针一般是存在栈上的,因为它本身就是一个隐含形参。在VS下编译器做了一定的优化处理,它认为this指针是频繁使用的,所以一般将它放在寄存器中进行传递。

我们可以在VS下简单的查看一下汇编代码:

在这里插入图片描述

Q2:this指针可以为空吗?

我们来看一个例子,下面程序编译运行结果是? A、编译报错 B、运行崩溃 C、正常运行

#include <iostream>
using namespace std;

class A
{
public:
	void Func1()
	{
		cout << "Func1" << endl;
	}

	void Func2()
	{
		cout << _a << endl;
	}

	int _a;
};

int main()
{
	A* ptr = nullptr;
	
	ptr->Func1();
	ptr->Func2();

	return 0;
}

我们先来看看结果:

在这里插入图片描述

在看到这个结果之后想必很多读者会露出不可思议的表情,ptr是一个空指针,那么它用来调用这个成员函数不应该直接会发生程序崩溃吗?那为什么调用第一个成员函数竟然还成功的得出了答案呢?而调用第二个成员函数时却发生了崩溃呢?

看到这种看上去很简单的题我们一定要保持警惕,说不定就有大坑等着你呢!!在这道题中我们一定要联系类的特性来做,我们知道成员函数不是在对象中的,它是共享的放在代码段的,此时调用成员函数利用不是ptr解引用找到这个函数的,而是利用call指令找到这个函数的地址。说的再通俗一点ptr此时没有进行解引用操作,它的作用仅仅是作为实参将地址隐式的传递给this指针,所以此时成员函数中this指针为空,但在Func1成员函数中我们并没有使用this指针进行解引用操作,所以此时运行正常;而对于Func2成员函数来说,它想要打印成员变量_a,实际上编译器隐式的进行了处理用this指针指明_a所在的对象,cout << _a << endl; ==> cout << this->_a << endl; 空指针进行解引用于是引发了程序崩溃。

同理,写成(*ptr).Func1();这样也是可以正常运行的,我们不要表面上看见*ptr就是对ptr进行解引用操作了,实际上成员函数根本不在对象中。我们通过汇编代码进一步验证一下:

在这里插入图片描述

在这里插入图片描述

结论:有没有解引用的行为取决于访问右边的东西在不在对象里面,而不是用没用*和->这个符号。


本篇文章的内容就到这儿了难度不是很大,要求重点掌握this指针的细节。如果文章有任何问题或者错处,欢迎大家评论区相互交流啊orz~🙈🙈

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/390135.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Spring核心模块—— BeanFactoryPostProcessorBeanPostProcessor(后处理器)

后置处理器前言Spring的后处理器BeanFactoryPostProcessor&#xff08;工厂后处理器&#xff09;执行节点作用基本信息经典场景子接口——BeanDefinitiRegistryPostProcessor基本介绍用途具体原理例子——注册BeanDefinition使用Spring的BeanFactoryPostProcessor扩展点完成自定…

Linux安装minio单机版

说明&#xff1a;因为前面记录了一下minio的使用&#xff0c;这里说一下minio的安装&#xff0c;只是单机版哦 环境准备&#xff1a;Linux系统 说明图&#xff1a; 1.创建文件夹命令 我的是安装在/usr/local 文件夹下面的创建文件夹命令 #进入目标文件夹 cd /usr/local#创建…

5 GateWay断言Predicate

每一个Predicate的使用&#xff0c;可以理解为&#xff1a;当满足条件后才会进行转发&#xff0c;如果十多个&#xff0c;那就是满足所有条件才会转发 断言种类 After&#xff1a;匹配在指定日期时间之后发生的请求。Before&#xff1a;匹配在指定日期之前发生的请求。Betwee…

常见摄像头接口USB、DVP、MIPI接口的对比

常见摄像头接口DVP、MIPI、USB的比较 引言 摄像头传感器已经广泛用于嵌入式设备了&#xff0c;现在的手机很多都支持多个摄像头。 在物联网领域&#xff0c;摄像头传感器也越来越被广泛使用。今天就来简单聊一聊几种常见的摄像头接口。 传感器与主控设备进行通信&#xff0…

基于S32K148快速调试TJA1101

文章目录1.前言2.TJA1101简介3.TJA1101调试3.1 硬件3.1.1 整体框图3.1.2 评估板3.1.2.1 参考原理图3.1.2.2 引脚说明3.1.3 转接板3.1.3.1 参考原理图3.1.3.2 模式配置3.1.3.3 原理介绍3.2 软件3.2.1 物理层&#xff08;TJA1101&#xff09;&#xff1a;3.2.2 数据链路层&#x…

05_Pulsar的主要组件介绍与命令使用、名称空间、Pulsar的topic相关操作、Pulsar Topic(主题)相关操作_高级操作、

1.5.Apache Pulsar的主要组件介绍与命令使用 1.5.1.多租户模式 1.5.1.1. 什么是多租户 1.5.1.2.Pulsar多租户的相关特征_安全性&#xff08;认证和授权&#xff09; 1.5.1.3.Pulsar多租户的相关特性_隔离性 1.5.1.4.Pulsar多租户的相关操作 1-获取租户列表 2-创建租户 3-获取配…

RocketMQ单机安装与启动

RocketMQ单机安装与启动系统要求下载地址安装步骤RocketMq启动NameServer查看是否启动成功启动BrokerProxy查看是否启动成功修改tool.sh测试消息产生消息的消费关闭服务器系统要求 下载地址 官网下载地址 二进制包是已经编译完成后可以直接运行的&#xff0c;源码包是需要编译…

Vant2 源码分析之 vant-sticky

前言 原打算借鉴 vant-sticky 源码&#xff0c;实现业务需求的某个功能&#xff0c;第一眼看以为看懂了&#xff0c;拿来用的时候&#xff0c;才发现一知半解。看第二遍时&#xff0c;对不起&#xff0c;是我肤浅了。这里侧重分析实现原理&#xff0c;其他部分不拓展开来&…

轮转数组(每日一题)

目录 一、题目描述 二、题目分析 2.1 方法一 2.1.1 思路 2.1.2 代码 2.2 方法二 2.2.1 思路 2.2.2 代码 2.3 方法三 2.3.1 思路 2.3.2 代码 一、题目描述 oj链接&#xff1a;https://leetcode.cn/problems/rotate-array 给定一个整数数组 nums&#xff0c;将数组中的…

GDScript 导出变量 (Godot4.0)

概述 导出变量的功能在3.x版本中也是有的&#xff0c;但是4.0版本对其进行了语法上的改进。 导出变量在日常的游戏制作中提供节点的自定义参数化调节功能时非常有用&#xff0c;除此之外还用于自定义资源。 本文是&#xff08;Bilibili巽星石&#xff09;在4.0官方文档《GDScr…

手把手创建flask项目

Flask 框架流程 什么是Flask&#xff1a; Flask诞生于2010年, 使用python语言基于Werkzeug工具箱编写的轻量级Web开发框架 Flask本身相当于一个内核, 其他几乎所有的功能都要用到扩展(邮件:Flask-Mail, 用户认证:Flask-Login, 数据库:Flask-SQLAlchemy). Flask的核心在于Werkz…

在线图书借阅网站( Python +Vue 实现)

功能介绍 平台采用B/S结构&#xff0c;后端采用主流的Python语言进行开发&#xff0c;前端采用主流的Vue.js进行开发。 整个平台包括前台和后台两个部分。 前台功能包括&#xff1a;首页、图书详情页、用户中心模块。后台功能包括&#xff1a;总览、借阅管理、图书管理、分类…

unity知识点小结02

虚拟轴 虚拟轴就是一个数值在-11内的轴&#xff0c;这个数轴上重要的数值就是-1,0和1。当使用按键模拟一个完整的虚拟轴时需要用到两个按键&#xff0c;即将按键1设置为负轴按键&#xff0c;按键2设置为正轴按键。在没有按下任何按键的时候&#xff0c;虚拟轴的数值为0&#xf…

【UEFI基础】UEFI事件介绍

简述 在【UEFI基础】System Table和Architecture Protocols介绍Boot Service时提到有一部分与事件相关的接口&#xff0c;它们创建、触发、等待和关闭事件&#xff0c;来完成某些功能&#xff0c;本文将进一步介绍事件。 需要注意&#xff0c;因为Boot Service需要在DXE阶段才…

用数组名作函数参数的详解,以及形参实参采用数组名,形参实参采用指针变量的几种情况解析

关于地址&#xff0c;指针&#xff0c;指针变量可以参考我的这篇文章&#xff1a; 地址&#xff0c;指针&#xff0c;指针变量是什么&#xff1f;他们的区别&#xff1f;符号&#xff08;*&#xff09;在不同位置的解释&#xff1f;_juechen333的博客-CSDN博客https://blog.csd…

Kali的安装与配置

虚拟机安装kali Kali下载 官网下载地址 注&#xff1a;下载VMware版本 百度网盘 提取码&#xff1a;Chen 创建虚拟机 将下载的压缩包放到合适的位置解压 双击运行虚拟机 登录 默认的账号密码都为kali 基本配置 修改root账户密码 打开命令行输入 sudo su root 输入kali 输…

【机器学习】验证集loss震荡(loss的其他问题)

训练过程中发现&#xff0c;train loss一直下降&#xff0c;train acc一直上升&#xff1b;但是val loss、val acc却一直震荡。loss一会上一会下&#xff0c;但是总体趋势是向下的。 “loss震荡但验证集准确率总体下降” 如何解决&#xff1f; 测试集准确率这样震荡是正常的吗…

python2.7/3.8版本安装教程

Wiondos-Python环境安装 Python2.7 下载地址 官网 速度比较慢 百度网盘 提取码:Chen 安装Python2.7 直接next 选择安装目录 注意这一步将最后一项勾选 安装完成 cmd中输入python 检查pip是否安装 cmd中输入pip --version Python3.8 下载地址 官网 速度比较慢 百度网…

蓝桥杯C/C++程序设计 往届真题汇总(进阶篇)

文章目录1. 最短路2. 数字三角形3. 递增序列4. 杨辉三角形5. 跳跃6. 路径7. 迷宫8. 装饰珠9. 明码10. 字串分值11. 作物杂交12. 承压计算13. 全球变暖14. 直线15. 平面切分1. 最短路 题目描述&#xff1a; 如下图所示&#xff0c;G是一个无向图&#xff0c;其中蓝色边的长度是…

线程池执行父子任务,导致线程死锁

前言&#xff0c; 一次线程池的不当使用&#xff0c;导致了现场出现了线程死锁&#xff0c;接口一直不返回。而且由于这是一个公共的线程池&#xff0c;其他使用了次线程池的业务也一直阻塞&#xff0c;系统出现了OOM&#xff0c;不过是幸好是线程同事测试出来的&#xff0c;没…