C++ | 类和对象(上)

news/2024/11/16 12:31:46/

目录

什么是类

类的介绍

struct在两种语言中的有何区别

私有变量命名注意点

类的作用域

类的声明定义分离

类的访问限定符

封装

类的实例化

类对象的存储

this指针

一道this指针相关的王炸题:

结语


什么是类

类的介绍

我们举一个日常生活中的例子:

手机,是一类产品,这姑且算是一个类,而手机里面又分了很多具体的品牌:华为,小米,iphone等等,这些就算是手机这个类面向的对象

而我们C++类的学习,需要用到C语言中的一个知识点:结构体

我们试想一下:假设struct是定义的一本书,那么这就是一个类,而我们在main函数中创建了多个关于书的变量,这些变量就是书这个结构体创建出来的对象,如下代码:

#include<iostream>
using namespace std;struct Book//类
{int _a;int _b;int _c;
};int main()
{//类创建出的两个对象struct Book s1;struct Book s2;return 0;
}

struct在两种语言中的有何区别

我们之前用C语言代码实现数据结构的种种的时候,总会发现,我们的类里面只有数据,比如int,double,char等等,我们的各种待实现的函数都是在头文件中的全局定义的

这会有一个很麻烦的点:命名

我们在写栈的时候,可能会在同一个头文件中还会写队列相关的类和声明,这时我们栈的名字只能带点特色:

Stackinit,因为除了栈之外还有一个QueueInit,如果单写一个Init的话,编译器会不知道这是谁的初始化

但是在C++中的类对此进行了升级

1. 我们的类中不仅可以声明变量,还能直接写函数!

2. 我们在main函数中创建对象的时候无需再写如struct Book作为变量名,只写类名即可

#include<iostream>
using namespace std;struct Stack
{void Init(int n = 4){_a = (int*)malloc(sizeof(int) * n);if (_a == nullptr){perror("malloc fail");return;}_capacity = 0;_top = 0;}int* _a;int _capacity;int _top;
};int main()
{Stack s1;s1.Init(10);return 0;
}

我们会看到,如上代码,我们直接使用了Stack作为变量类型的名字而非struct Stack

私有变量命名注意点

如上我们写的变量前面都加上了_,比如_capacity,_top......

至于为什么要这样子写,我们看一段代码就能明白了:

#include<iostream>
using namespace std;struct Date
{void Init(int year = 2024, int month = 3, int day = 31){year = year;month = month;day = day;}int year;int month;int day;
};int main()
{Date s1;s1.Init();return 0;
}

看上述代码,你会发现里面出现了year = year这样子的写法,那这里面哪个是形参,那个是实参,我们并不知道

而且,我们的代码不是只写给我们自己看的,写完了之后说不定未来还会被某个人维护,但是这样子的代码可读性极差,会被骂的

所以我们就将类里面变量的名字做一点修改,这样就不会出现上述的情况了

但是每个公司,每个地方或许会有不同的命名风格:_day,day_......

类的作用域

类的声明定义分离

如果我们将类定义在头文件里面了(类中的函数只是声明),而我们在.cpp文件中要实现类中的函数的话

我们就需要在.cpp文件中使用的函数的前面加上      类名::

如下:

//.h文件内
struct Date
{void Init();int _year;int _month;int _day;
};
//.cpp文件内
void Date::Init()
{;
}

我们在.h文件中定义了类之后,在.cpp文件上实现,但是.cpp文件上找不到这个函数的出处啊

如上,.cpp文件里面找不到.h文件里的类里面的函数,是因为类自成一个类域,在这个类域里面的内容都是给包装起来的,我们是没法使用的

我们目前一共学习了4种域:局部域、全局域,命名空间域、类域

我们可以用理解命名空间域的方式来理解类域

如果我们想访问类里面的内容的话,就需要告诉编译器我是在这个类域里面的,编译器认识了,代码就能跑得了了

类的访问限定符

在C++里,我们并不会像C语言一样一直用struct,更多的是使用class,如下:

class Date
{void Init();int _year;int _month;int _day;
};

除了名称的改变,其他什么都不变

那有人就会疑惑了,既然什么都不变,那又何必多此一举搞一个class呢?

这就涉及到了访问限定符的相关概念

我们再来看一组代码:

class Date
{void Init();int _year;int _month;int _day;
};int main()
{Date s1;s1.Init();return 0;
}

看着好像没什么不对的,但是:

报错了

这是因为我们使用的是class,而在C++里面,有公有和私有的概念

C++中有三个单词代表公私有:

  • public(公有)
  • private(私有)
  • protected(私有)

由于C++兼容C语言,所以C语言中的struct依然能使用,也能拿来定义类

但是与class不同的是,struct定义的类默认是public,也就是公有,意味着里面的变量都是可以访问的

但是class默认是私有的,所以我们上面的代码跑不了,就是因为class默认私有,而我们将Init定义在私有里面,不能使用

如果变量为私有的话,那么我们在类外面就不能访问,这样子设计,是为了更加的安全

我们试想一下:中国的高铁和火车,如果要乘坐就需要买票、排队、刷脸,之后有序入座,这样子仅仅有条的,也同样有助于管理

但是我们再看看印度阿三的火车:

两相比较之下,相信你会明白为什么会出现访问限定符这个东西的

那如果我们想在同一个类里面既有公有又有私有的话,那我们就需要使用访问限定符:

class Date
{
public:/公有void Init();
private:私有int _year;int _month;int _day;
};int main()
{Date s1;s1.Init();return 0;
}

通过访问限定符,我们就实现了公有和私有的分离

封装

无论是C语言,还是C++,抑或是Java等,都是面向对象的语言

而所有面向对象的语言都有三个特征:

  • 封装
  • 继承
  • 多态

后面两个继承和多态我们暂时无需理会,这些是我们在很后面才会学到的内容

我们今天要讲的就一个封装:

封装的本质是便于管理,我将我类里面的内容分开进行管理,公有和私有,我想让你用的你才能用,我不想让你用的我就隐藏起来

就好比我们坐的火车,买了坐票的人才有座位,买了卧铺的人才有床睡,不然没有票买谁想坐哪里就坐哪里那可太乱了

类的实例化

class Date
{int _year;int _month;int _day;
};

如上,这是我们声明出来的一个类,这个类里面有三个变量:year、month、day

但是仔细想一下,这三个变量是声明还是定义?开空间了吗?

答案是否定的,这里只是声明,并没有开空间

那这些变量在哪里开的空间?

class Date
{int _year;int _month;int _day;
};int main()
{Date s1;Date s2;return 0;
}

看这个main函数,我现在用这个Date类创建出了一个对象,开辟了空间,而开辟出来的空间,就是留给如上这三个变量的

也就是说:这些变量的空间是跟类一块儿定义出来的

举一个形象的例子:我们建房子之前都需要有一张设计图

而我们的设计图就可以理解为是类

我们通过这张设计图就能建出一栋又一栋的房子,这就是我们通过设计图这个类创建出来的对象

而我们的设计图是不占空间的,但是建出来的房子是多少平在图纸上是有规定的,房子是占空间的

我们通过设计图建出房子是实例化

我们通过类创建出变量是类的实例化

类对象的存储

我们可以对类进行sizeof操作看一下结果:

class Date
{
public:void Init(int year){_year = year;}
private:int _year;int _month;int _day;
};int main()
{Date s1;cout << sizeof(s1) << endl;return 0;
}

我们可以看到,结果是12

我们按照C语言中学到的内存对齐的规则来看一看的话,我们会发现:

三个int,大小是12,总大小是最大对齐数的整数倍,也就是int的整数倍,刚好是12

另外:类的大小计算规则就是C语言中内存对齐的规则

但是也许你会疑惑:难道类中的函数不用计算大小吗?

我们再加一个函数试试:

class Date
{
public:void Init(int year){_year = year;}int Add(int a, int b){return a + b;}
private:int _year;int _month;int _day;
};int main()
{Date s1;cout << sizeof(s1) << endl;return 0;
}

我们会发现,结果还是12,这就意味着函数的大小是不被包含在类里面的

或者我们换一个思路,再来看点有意思的:

int main()
{Date s1;cout << sizeof(s1) << endl;Date s2;cout << sizeof(s2) << endl;return 0;
}

我们现在创建出了两个对象,但是这两个的大小都是12

试想一下,这两个对象出自同一个类,如果这两个对象都要使用类里面的函数,那函数在类里面又没有开空间存进去,那我该怎么用呢?

两个对象里面都有空间存着变量

就像一个小区里面一栋一栋的房子,当然你也可以说是居民楼

那假如我们现在要建一个篮球场,建一个高尔夫球场,建一个体育馆

那我们如果在每家每户里面都建一个,是不是有点太浪费了呀

我们只需要在公共场地建一个,如果想要打篮球,打高尔夫什么的,直接到公共建好的场地里就可以了

我们再来看两段代码,看一下这两段代码的结果:

class Date
{};class Book
{
public:void func(){}
};int main()
{Date s1;Book s2;cout << sizeof(s1) << endl;cout << sizeof(s2) << endl;
}

可能有人会觉得:输出的结果应该是 0 0,因为没有变量,只有函数或者连函数都没有,就是一个空类

但其实:

我们试想一下:如果我说我创建出来了一个对象,但是没有开空间,那我这个对象到底创建了出来没有,地址是什么?空间都没有,哪来的地址?

所以,即使是空类,我们创建对象的时候也会开空间,最小为1

this指针

我们先来看一段代码:

class Date
{
public:void Init(int year, int month, int day){_year = year;_month = month;_day = day;}void Print(){cout << _year << " " << _month << " " << _day << endl;}
private:int _year;int _month;int _day;
};int main()
{Date s1, s2;s1.Init(2024, 1, 13);s2.Init(2023, 11, 18);s1.Print();s2.Print();return 0;
}

我们有了一个类,创建了两个对象

但是这两个对象都是使用的都是同一个类,我们在调用Print函数的时候,我们是这样调用的:

s1.Print();
s2.Print();

不知各位有没有发现什么猫腻

我们用的是同一个函数,我们也没有传参,甚至函数都是无参的

但是当我们调用的时候,却能打印出不同的各自的日期,这是为什么?

这是因为编译器会有一个隐含的this指针

这就相当于,你看似没有传参,但是编译器已经帮你把对象的地址传过去了,并且在函数那里用了一个隐含的this指针来接收对象的地址

我们将this指针显示写出来给大家对照这看一看:

//类内部
/*void Print(int* this)
{cout << this->_year << " " << this->_month << " " << this->_day << endl;
}*/
void Print()
{cout << _year << " " << _month << " " << _day << endl;
}//main函数内部
//s1.Print(&s1);
s1.Print();//s2.Print(&s2);
s2.Print();

这下子我们就明白了,为什么我们明明没有传参,用的同一个函数,但是却能调用,因为隐含的this指针已经把对象的地址传过去了

其实Java也有一个this指针,但是python不是,python的那个叫做self,但性质也八九不离十

那我们的this指针是存在哪里的呢?

首先肯定不在类里,因为我们类的大小就是由类中变量决定的

静态区是存储static,全局变量的,不是

堆区的使用甚至要我们自己开辟空间,也不是

所以,this指针大概率是存在栈上的

为什么说是大概率呢?因为这是看编译器的,有些编译器会将this指针存进寄存器之中,因为我们老是要使用this指针,所以编译器干脆直接将其存进寄存器里面了,相当于是一个优化

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 s1;s1.Init(2024, 1, 13);return 0;
}

我们看到这段代码:

我们在main函数中对s1进行初始化时,只传了三个参数,我们来看看反汇编代码

注:此处使用的是VS2022

我们可以看到,前三个是分别将按个参数传了过去

但是我用红色框框圈起来的哪个部分,这个的意思是将s1的地址传给rcx这个寄存器,而s1的地址就是由this指针维护的,也就是相当于把this指针的值存进rcx这个寄存器里面了

一道this指针相关的王炸题:

class A
{
public:void Print(){cout << "Print()" << endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->Print();return 0;
}

请问,这道题会报错还是崩溃还是正常运行?

答案是正常运行

这是因为,虽然指针p是空指针,但是我们将nullptr作为this指针的值传过去时,我们并没有要通过this指针找类A中的相关变量,并没有,所以即使我传了一个nullptr过去,也对程序毫无影响,因为根本就没有用到this指针

class A
{
public:void PrintA(){cout << _a << endl;}
private:int _a;
};
int main()
{A* p = nullptr;p->PrintA();return 0;
}

那如果是这种情况呢?

我们会看到,我们将p的值置为nullptr之后,又将其作为this指针的值传过去,但是不比上一题没用到this指针,这题需要使用this指针去寻找变量_a

但是找不到啊!拿一个nullptr怎么找得到呢?

综上,这题我们的程序会报错

结语

类和对象上篇算是C++的一个开端

这一章准确来说是为了类和对象(中)那六个默认构造函数做铺垫

如果觉得这篇文章对你有帮助的话,希望能够多多支持!!


http://www.ppmy.cn/news/1456101.html

相关文章

【开发技巧 | 第三篇】windows端口被占用及解决方法

文章目录 3.windows端口被占用及解决方法3.1查看指定端口被占用情况3.2根据PID查看对应的进程3.3根据PID杀死对应的进程3.4小结 3.windows端口被占用及解决方法 3.1查看指定端口被占用情况 netstat -aon|findstr 7090或 netstat -aon|findstr "7090"最后一列数字为…

优雅处理返回信息状态码:Result对象在Spring Boot中的应用

前言 在开发过程中&#xff0c;处理返回的信息状态码是一个重要的问题&#xff0c;尤其是在大型项目中。为了统一处理这些状态码&#xff0c;我在Spring Boot中创建了一个名为Result的Java对象&#xff0c;用于封装返回的信息和状态码。在本文中&#xff0c;我将分享如何实现这…

Agent AI智能体:我们的生活即将如何改变?

你有没有想过&#xff0c;那个帮你设置闹钟、提醒你朋友的生日&#xff0c;甚至帮你订外卖的智能助手&#xff0c;其实就是Agent AI智能体&#xff1f;它们已经在我们生活中扮演了越来越重要的角色。现在&#xff0c;让我们一起想象一下&#xff0c;随着这些AI智能体变得越来越…

基于Python的LSTM网络实现单特征预测回归任务

长短期记忆网络&#xff08;Long Short-Term Memory, LSTM&#xff09;是一种特殊的递归神经网络&#xff08;RNN&#xff09;&#xff0c;适用于处理时间序列数据和其他序列数据的预测问题。它特别适合处理具有时间依赖性和长期依赖关系的序列数据。 以下是基于Python和Keras…

Redis领航分布式:Java实现高效Session管理

在构建分布式系统时&#xff0c;用户的会话管理是一个至关重要的问题。传统的基于服务器的会话管理方案可能会面临单点故障和性能瓶颈等问题。 而基于 Redis 的分布式会话管理方案能够有效地解决这些问题&#xff0c;并提供高可用性和性能。 本文将深入探讨 Redis 实现分布式…

从简单逻辑到复杂计算:感知机的进化与其在现代深度学习和人工智能中的应用(上)

文章目录 引言第一章&#xff1a;感知机是什么第二章&#xff1a;简单逻辑电路第三章&#xff1a;感知机的实现3.1 简单的与门实现3.2 导入权重和偏置3.3 使用权重和偏置的实现实现与门实现与非门和或门 文章文上下两节 从简单逻辑到复杂计算&#xff1a;感知机的进化与其在现代…

【Java】Stream流、方法引用(Java8)

Stream流 中间方法 distinct() 使用HashSet去重 终结方法 toArray() value 表示 流中数据的个数&#xff0c;要跟数组的长度保持一致。 collect() 收集到map中&#xff0c;比较复杂。需要指定 键 和 值 的生成规则。 方法引用 01_引用静态方法 ​ 引用类方法&#xff0c;其实…

leetcode---岛屿数量

. - 力扣&#xff08;LeetCode&#xff09; 代码&#xff1a; //岛屿题目的思想&#xff1a;二维矩阵图的DFS就是&#xff0c;上下左右遍历如果是0或者出界的话就return //规定的是陆地上下左右是水的话它就是岛屿。当遍历矩阵图中每一个点&#xff0c; //在调用递归算法之前…