目录
什么是类
类的介绍
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++的一个开端
这一章准确来说是为了类和对象(中)那六个默认构造函数做铺垫
如果觉得这篇文章对你有帮助的话,希望能够多多支持!!