C++ | 类和对象(上)

目录

什么是类

类的介绍

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++的一个开端

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

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

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值