⭐博客主页:️CS semi主页
⭐欢迎关注:点赞收藏+留言
⭐系列专栏:C++初阶
⭐代码仓库:C++初阶
家人们更新不易,你们的点赞和关注对我而言十分重要,友友们麻烦多多点赞+关注,你们的支持是我创作最大的动力,欢迎友友们私信提问,家人们不要忘记点赞收藏+关注哦!!!
前言
类和对象的概念大家肯定不了解,下面的一些文案可以供大家学习一下。
一、面向过程和面向对象初步认识
我们拿人手洗衣服和洗衣机自动洗衣服作为例子对于面向过程和面向对象的初步认识的理解。
C语言(面向过程) —— 手洗衣服
C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
洗衣服流程图:
C++(面向对象) —— 洗衣机洗衣服
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
洗衣机洗衣服流程图:
二、类的引入
我们知道,C语言在结构体中只能定义变量,而C++的结构体中既可以定义变量也可以定义函数。我们用struct结构体演示一下Stack的书写过程:
#include<iostream>
#include<stdio.h>
#include<stdlib.h>
using namespace std;
struct Stack
{
//成员函数
void Init(int DefaultCapacity = 4)
{
a = (int*)malloc(sizeof(int) * DefaultCapacity);
if (a == nullptr)
{
perror("malloc fail\n");
return;
}
capacity = DefaultCapacity;
top = 0;
}
void Push(int x)
{
// 判断是否需要扩容
if (capacity == top) {
//扩容
int* temp = (int*)realloc(a, sizeof(int) * capacity * 2);
if (temp == NULL) {
perror("malloc failed");
return;
}
a = temp;
capacity *= 2;
}
a[top++] = x;
}
void Destroy()
{
free(a);
a = nullptr;
capacity = top = 0;
}
//成员变量
int* a;
int capacity;
int top;
};
int main()
{
struct Stack s1;
s1.Init();
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
s1.Push(5);
cout << s1.top << endl;
s1.Destroy();
return 0;
}
三、类的定义
我们引入class关键字的概念:
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。
类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。
因为下面的演示需要用到public、private和protected,所以我们先进行讲解类的访问限定符。
1、类的访问限定符
(1)访问限定符说明
- public修饰的成员在类外可以直接被访问
- protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
- 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
- 如果后面没有访问限定符,作用域就到 } 即类结束。
- class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
如下演示:
//struct Stack - 默认是公有
class Stack //-默认是私有
{
public://公有
//成员函数
void Init(int DefaultCapacity = 4)
{
a = (int*)malloc(sizeof(int) * DefaultCapacity);
if (a == nullptr)
{
perror("malloc fail\n");
return;
}
capacity = DefaultCapacity;
top = 0;
}
void Push(int x)
{
// 判断是否需要扩容
if (capacity == top) {
//扩容
int* temp = (int*)realloc(a, sizeof(int) * capacity * 2);
if (temp == NULL) {
perror("malloc failed");
return;
}
a = temp;
capacity *= 2;
}
a[top++] = x;
}
void Destroy()
{
free(a);
a = nullptr;
capacity = top = 0;
}
private://私有
//成员变量
int* a;
int capacity;
int top;
};
int main()
{
class Stack s1;
s1.Init();
s1.Push(1);
s1.Push(2);
s1.Push(3);
s1.Push(4);
s1.Push(5);
//cout << s1.top << endl;
s1.Destroy();
return 0;
}
2、类的两种定义方式:
(1) 声明和定义全部放在类体中
成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
当然了,最终是否是内联函数取决于编译器,倘若在类体中定义的代码过于长那么编译器也不会将其定义为内联函数。
如图:少量的代码直接在定义处展开,变成内联函数。
(2)定义与声明分离
注意的有两点:
1、在.cpp中的函数调用的时候要加上类限定域,能够根据这个域找到这个class。
2、在定义的时候默认缺省值不能加,这个在C++入门的时候讲过。
stack.h:
class Stack //-默认是私有
{
public://公有
//成员函数
void Init(int DefaultCapacity = 4);
void Push(int x);
void Destroy()
{
free(a);
a = nullptr;
capacity = top = 0;
}
//private://私有
//成员变量
int* a;
int capacity;
int top;
};
stack.cpp:
#include"Stack.h"
void Stack::Init(int DefaultCapacity)
{
a = (int*)malloc(sizeof(int) * DefaultCapacity);
if (a == nullptr)
{
perror("malloc fail\n");
return;
}
capacity = DefaultCapacity;
top = 0;
}
void Stack::Push(int x)
{
// 判断是否需要扩容
if (capacity == top) {
//扩容
int* temp = (int*)realloc(a, sizeof(int) * capacity * 2);
if (temp == NULL) {
perror("malloc failed");
return;
}
a = temp;
capacity *= 2;
}
a[top++] = x;
}
(3)成员变量命名规则
在定义成员名的时候,在前面加一个"_",这样是为了更好地区分形参与局部域。
(4)struct和class的区别
C++中struct默认是private(私有),class默认是public(公有)。
四、封装
**封装:**将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。封装本质上是一种管理,让用户更方便使用类。
在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。
封装本质上是一种更好的严格管理,不封装是自由的管理
1、把数据和方法都放到类里面。
2、访问限定符 —— 可以给你访问的定义成公有,不想给你访问的定义成私有/保护。
例如:我们在之前写的寻找栈的top的值的时候,我们分了两种情况,第一种情况是top从0开始,第二种情况是从-1开始,可是我们没有给其他读者进行说明,这样调用会导致出错,而进行封装了以后,可以规范使用函数来访问数据。
五、类的作用域
类定义了一个新的作用域,为类域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。
和我们上面将定义和声明分开以后需要加一个类的作用域。
void Stack::Push(int x)
{
// 判断是否需要扩容
if (capacity == top) {
//扩容
int* temp = (int*)realloc(a, sizeof(int) * capacity * 2);
if (temp == NULL) {
perror("malloc failed");
return;
}
a = temp;
capacity *= 2;
}
a[top++] = x;
}
六、类的实例化
用类类型创建对象的过程,称为类的实例化。对于变量而言,开空间就是定义,不开空间就是声明。
1、类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
2、一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
解释:
我们可以形象地理解类为一个图纸,而由类实例化出来的对象就为各个建筑,这个图纸没有空间不能存放东西,而只能由类实例化出来的对象的建筑才有空间,才可以存放东西,所以:类不能存放东西,只有类实例化出来的对象才能存放东西(改变)。
七、类对象模型
1、计算类对象的大小
大家猜一猜下面这串代码会输出什么?
class Stack
{
public:
void Init()
{
int b = 0;
int c = 2;
cout << "void Init()" << endl;
}
void Print()
{
cout << _a << endl;
}
private:
int _a;
};
int main()
{
Stack A;
printf("%d ", sizeof(A));
return 0;
}
我们以下有三种猜测和想法:
(1)对象中包含类的各个成员
如果当时候本贾尼老爷子这么定义一个类,那么实在是太乱了,这个类的字节大小也太大了,而且很难算。因为每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。所以这种想法pass掉。
(2)代码只保存一份,在对象中保存存放代码的地址
这种做法有点意思,仅仅存一个函数指针加上成员变量,可是函数指针是不是逻辑很复杂呀,需要用指针去访问那么多的成员函数。
(3)只保存成员变量,成员函数存放在公共的代码段
只保存成员变量,将这些类成员函数放到一个公共代码区,不需要函数指针,并不复杂,仅需编译中的call到函数定义中即可,对不同的对象存储不同的成员变量。
(4)计算大小
我们上面的计算字节大小为4,也就是单纯算成员变量的字节大小。
当然,一个不足以支撑全局,我们用三种举例来支持我们的想法。
i、 类中既有成员变量,又有成员函数
ii、类中仅有成员函数
iii、类中什么都没有—空类
占个位置,占1byte,表示对象存在,但不存储有效数据。
iv、结论
1、一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐。
2、空类和只有成员函数的类,编译器自动给个默认值为1个字节来唯一标识这个类的对象(表示这个类是存在的)。
2、结构体内存对齐规则
- 第一个成员在与结构体偏移量为0的地址处。
- 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。
注意:对齐数 = 编译器默认的一个对齐数 与 该成员大小的较小值。VS中默认的对齐数为8。 - 结构体总大小为:最大对齐数(所有变量类型最大者与默认对齐参数取最小)的整数倍。
- 如果嵌套了结构体的情况,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含嵌套结构体的对齐数)的整数倍。
我们回顾一下之前学习过的结构体内存对齐的大小,大家看一下下面输出的结果:
结果为:8 和 8。
画图解释:
int - 4个字节, char - 1个字节,最终结果要是对齐数的整数倍。
如下图,32位机器一次就读4个字节,因为是32根线也就是32个比特位,4个字节,如果不进行内存对齐,访问int是分块访问的,是不是有点太奇怪了,要访问两次,而对齐以后只需要访问一次即可。
八、this指针
1、this指针的引出
class A
{
public:
void Init(int year, int month, int date)
{
_year = year;
_month = month;
_date = date;
}
void Print()
{
cout << _year << "-" << _month << "-" << _date << endl;
}
private:
int _year;
int _month;
int _date;
};
int main()
{
A a1;
A a2;
a1.Init(2023, 4, 24);
a2.Init(2023, 4, 25);
a1.Print();
a2.Print();
return 0;
}
输出结果:
我们思考一个问题,我们既然之前说a1和a2调用的是同一个Print函数,输出的值却是各不相同,那么是什么原因导致输出的结果不相同呢?这里我们引入this指针,this指针顾名思义就是this指向d1或者d2。
C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参
数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成。
我们可以这么理解,a1调用的Print函数为a1.Print(&d1),也就是和数据结构一样传的是地址过去,然后在函数那边接收的是:void Print(A* this)。这个this指针悄咪咪地接收传参并找相对应的对象已经赋的值进行输出,是不是很妙。
衍生出一个问题:this指针为什么不明着写出来?答案是:方便。这个this指针不能在传参的参数部分写出来,而可以在cout后面写出来 。如下所示:
2、 this指针的特性
- this指针的类型:类类型* const,即成员函数中,不能给this指针赋值。
- 只能在“成员函数”的内部使用
- this指针本质上是“成员函数”的形参,当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。this指针是普通参数一样存在函数调用的栈帧里面。
- this指针是“成员函数”第一个隐含的指针形参,一般情况由编译器通过rcx寄存器自动传递,不需要用户传递。对象地址是放在rcx寄存器中,rcx存储this指针的值。
this指针为空的情况的讨论:
总结
类和对象上篇讲解完以后大家肯定会初步了解了类和对象,对于类和对象的概念和理解有了一个初步的印象,在之后还有类和对象中篇和下篇。
家人们不要忘记==点赞收藏+关注哦!!!