【C++】类和对象(上)

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

个人主页平行线也会相交
欢迎 点赞 收藏✨ 留言✉ 加关注本文由 平行线也会相交 原创
收录于专栏【C++之路
在这里插入图片描述

目录

一、面向过程和面向对象

C语言是面向过程的而C++是面向对象的那面向过程和面向对象到底是什么呢

我们拿一个非常典型的外卖系统来进行举例

面向过程
我们知道外面系统中主要有商品上架、用户选餐、商家派单、骑手送单这4个步骤而这4个步骤就是整个外卖系统的主要的4个过程。面向过程关心的就是整个过程中的过程步骤。
面向对象
面向对象的话就不再具体关注整个外卖系统中的各个过程而是关心整个过程中有谁参与到整个过程中来我们细想一下整个过程主要就有商家、用户、骑手这三个对象参与进来即面向对象。面向对象关心的是对象与对象之间的关系和交互可以将现实世界类和对象映射到虚拟到计算机系统中。

二、类的引入

之前在C语言的学习过程中我们知道在C语言结构体中只能定义变量在C++中结构体内不仅可以定义变量也可以定义函数
其次我们要知道C++是兼容C语言的C语言的struct用法在C++中依然是可以使用的只不过在C++中把struct升级成了

struct Queue
{
	//成员函数
	void Init(int defaultCapacity = 4)
	{

	}
};
struct Stack
{
	//成员函数
	void Init(int defaultCapacity = 4)
	{
		a = (int*)malloc(sizeof(int) * defaultCapacity);
		if (a == nullptr)
		{
			cout << "malloc申请空间失败" << endl;
			return;
		}
		capacity = defaultCapacity;
		top = 0;
	}

	void Push(int x)
	{
		//扩容......

		a[top++] = x;
	}

	void Destroy()
	{
		free(a);
		a = nullptr;
		top = capacity;
	}
	//成员变量
	int* a;
	int top;
	int capacity;
};
int main()
{
	struct Stack st1;//这里是C语言的语法即利用结构体类型创建了一个结构体变量
	st1.Init(20);

	Stack st2;//C++中把struct升级成了类C语言是不可以这么使用的
	st2.Init();
	st2.Push(1);
	st2.Push(2);
	st2.Push(3);
	st2.Push(4);
	st2.Destroy();
	return 0;
}

虽然C++把struct升级成了类即用struct也可以定义类但是在C++中更喜欢使用class来定义类

三、类的定义

类的定义的格式

class calssName
{
	//类体有成员函数和成员变量组成
};//分号不能丢

class为定义类的关键字ClassName为类的名字{}中是类的主体类体。
类体中内容称为类的成员类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。

类的定义有两种方式

方式1声明和定义全部放在类体中成员函数如果在类中定义编译器会根据需要把它当成内联函数处理。请看

在这里插入图片描述

方式2 类也是可以声明和定义分离的。当函数中的内容较长时我们就可以采用声明和定义分离的方式来定义类。类里进行声明类外进行定义

在这里插入图片描述

加上Stack::就是在告诉编译器Init函数不是普通的全局函数而是类里的一个成员函数的定义。
这里如果我们想让Init函数成为内联函数的话我们不可以直接在声明前面加上inline
在这里插入图片描述
这种写法是错误的因为内联函数的声明和定义是不可以分离的如果我们想让Init函数称为内联函数的话我们应该在类里面直接定义不要分离否则会链接不上。同时在C++中如果在类里面定义默认就是内联函数比如上述代码中如果我们想让Push函数成为内联函数的话没有必要加上inline可以加也可以不加因为在类里面进行定义的函数默认就是内联函数。所以在C++中长的函数就是声明和定义分离短的函数就会直接在类里进行定义。但是如果有一个很长的函数也在类里直接定义的话依然不会是内联函数因为一个函数是否成为内联函数的话取决于编译器。

下面来看看成员变量的命名规则
在这里插入图片描述
注意进行区分所以采用前加或者后加_进行区分。

四、访问限定符

在这里插入图片描述

访问限定符说明

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

在这里插入图片描述

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

现在有一个问题我们用class和用struct来定义类两者有什么区别呢
我们直到C++是兼容C语言的所以C++中的struct既可以当作结构体来进行使用struct也可以定义类。和class定义类一样区别就是struct定义的类默认访问权限是publicclass定义的类默认访问权限是private。同时我们一定要注意在继承和模板参数列表位置那里struct和class其实也是有区别的。当然无论是默认私有还是默认共有建议是我们直接使用显式的指令

五、类的作用域

类定义了一个新的作用域称为类域类的所用成员都在类的作用域中。在类体外定义成员时需要使用::作用域操作符指明成员属于哪个类域。
一般是按照局部域、其次类域、最后全局域其中命名空间域与全局域基本上处于同一个等级命名空间域不进行展开的话是没有办法访问的。

六、类的实例化

用类类型创建对象的过程称为类的实例化。

在这里插入图片描述

对于变量而言声明和定义的区别在于是否开辟内存空间如果开了就是定义如果没开就是声明。

那怎么样才算是定义呢请看
在这里插入图片描述

这一步叫做对象的实例化或者叫做对象定义

  1. 类是对对象进行描述的是一个模型一样的东西限定了类有哪些成员定义出一个类并没有分配实际的内存空间来存储它。
  2. 一个类可以实例化出多个对象实例化出的对象占用实际的物理空间存储类成员变量。
  3. 类实例化出对象就像现实中使用建筑设计图建造出房子类就像是设计图。我们通过设计图可以设计很多建筑物我们也可以通过类来实例化出多个对象。类和设计图并不会占据空间只有通过设计图创造出来的房子和类实例化出来的对象才占据内存空间

在来举个例子请看

class Date
{
public:
	void Init(int year)
	{
		_year = year;
	}
private:
	int _year;
};

上述代码中Date类是不占用具体的空间的只有经过对象的实例化之后才会占有一定的内存空间。比如
在这里插入图片描述
上图就是类的实例化。

但是我们不可以这样

int main()
{
	Date._year = 25;
	return 0;
}

上述代码就是错误的因为Date类是不占有内存空间的只有Date类实例化出来的对象才占有一定的内存空间。

我们也不可以这样

class Date
{
public:
	void Init(int year)
	{
		_year = year;
	}
//private:
	int _year;
};
int main()
{
	Date::_year = 25;
	return 0;
}

尽管上述代码中的_year变为了公有的上述代码依旧是错误的。原因还是没有经过类的实例化。这种行为相当于往声明里放数据当然是不可以的了因为没有经过类的实例化之前是不占据任何内存空间的。

七、计算类对象的大小

计算类对象的大小依然是和计算结构体大小的方式一样遵循内存对齐

#include<iostream>
using namespace std;
class A
{
public:
	void Print()
	{
		cout << _a << endl;
	}
private:
	char _a;
};

int main()
{
	cout << sizeof(A) << endl;
	return 0;
}

在这里插入图片描述

我们在计算类大小的时候是不需要考虑成员函数的即不需要计算成员函数的大小。sizeof类和sizeof对象计算出来的大小是一样的
在这里插入图片描述

下面是结构体内存对齐规则

1.第一个成员在结构体变量偏移量为0 的地址处
2.其他成员变量要对齐到某个数字对齐数的整数倍的地址处。对齐数 = 编译器默认的一个对齐数与该成员大小中的较小值。vs中默认值是8 Linux默认值为4。
3.结构体总大小为最大对齐数的整数倍。每个成员变量都有自己的对齐数。
4.如果嵌套结构体的话嵌套的结构体对齐到自己的最大对齐数的整数倍处结构体的整体大小就是所有最大对齐数包含嵌套结构体的对齐数的整数倍。

为什么要进行内存对齐

1.不是所有的硬件平台都能访问任意地址上的数据。
2.某些硬件平台只能只在某些地址访问某些特定类型的数据否则抛出硬件异常及遇到未对齐的边界直接就不进行读取数据了。
3.提升CPU访问内存的效率

内存对齐本质是空间换时间。

下面我们来计算比较特殊的计类对象的大小

当类中仅有成员函数或者类中什么都没有的时候即空类。请看

class A1
{
public:
	void f1()
	{

	}
};
class A2
{

};

在这里插入图片描述
这里大家或许有些许疑惑按照之前的说法即我们在计算类大小的时候是不需要考虑成员函数的即不需要计算成员函数的大小既然不需要考虑成员函数的大小的话那为什么这里的结果输出不是0而是1呢
我们首先来看一段代码
在这里插入图片描述
当计算没有成员变量的类对象时候需要1byte进行占位表示该对象存在并不会存储有效数据。

八、this指针

好了现在再来看一下这一端代码

#include<iostream>
using namespace std;

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 da1, da2;
	da1.Init(2023, 5, 20);
	da2.Init(2023, 5, 21);
	da1.Print();
	da2.Print();
	return 0;
}

运行结果如下
在这里插入图片描述

上述代码中我们调用了用一个Print函数来打印日期但是打印结果是不一样的为什么呢

错误有的友友说之所以打印结果不一样是因为da1和da2是两个对象所以结果不同这是错误的说法调用Print函数跟这里的对象da1和da2是没有关系的。因为都是call Print地址即都是call的同一个地址Print函数在公共的代码区域

之所以打印结果不一样是因为这里存在着隐含的this指针。编译器在进行编译的时候会对Print函数进行“暗箱操作”对调用的地方也会进行一些处理。

在这里插入图片描述
首先我们要知道编译器调用的的确是一个函数但是调用时候的形参是不同的一个this指针实指向da1的另一个this指针是指向da2的

虽然这里的确是this指针在起作用但是编译器不允许我们在形参和实参的位置去显式的写this指针而允许我们在成员函数内显式的写this指针。


在这里插入图片描述
在这里插入图片描述
还有一点需要我们注意虽然我们可以在函数内部去显式使用this但是我们不可以对this指针进行更改就比如说下面这种写法就是错误的请看

在这里插入图片描述
之所以不可以在函数内更改this指针是因为this指针的类型其实是这样的Data* const thisconst修饰的是this指针本身即this指针本身不可以进行修改。但是this指针指向的内容是可以更改的。
但是我们可以这样去写Data const * const this意思就是this指针本身不能修改的同时this指针指向的内容也不能进行修改。

这里插一点我们在访问_year _month _day的时候是不可以这样去访问的Data::_year或者da1::_year这两种都是错误的访问方式前一种是声明和定义的问题后面那种是域访问限定符的问题。正确的访问方式应该是这样的da1._year da1._month da1._day.

现在有一个问题this指针是存放在哪里呢

先来看第一个问题this指针是一个形参形参传给实参要进行压栈操作所以this指针是栈的一个变量所以this指针是和普通参数一样存在在栈里面即this指针作为栈帧的一部分存在在函数调用的栈帧里面随着函数调用的结束this指针会进行销毁。

这个x64环境下
在这里插入图片描述

这是x86环境下
在这里插入图片描述

vs下面对this指针传递进行优化对象地址是放在ecxecx存储this指针的值。x86

请问下面程序运行结果是什么

#include<iostream>
using namespace std;
class A
{
public:
	void Print()
	{
		cout << "Print()" << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

这里其实是正常运行的请看运行结果
在这里插入图片描述
对比着上面的代码请问下面代码运行结果是什么

#include<iostream>
using namespace std;
class A
{
public:
	void Print()
	{
		cout << _a << endl;
	}
private:
	int _a;
};

int main()
{
	A* p = nullptr;
	p->Print();
	return 0;
}

我们来看一下这段代码的运行结果
在这里插入图片描述
发现啥都没有其实这里是程序运行崩溃了。
第一段代码指针p调用Print不会发生解引用因为Print的地址并不在对象中Print函数的地址在公共的代码段中这里的指针p可以看到是空指针会作为实参会传递给形参this指针**传递一个空指针并不会报错对空指针进行解引用会放生报错。**所以第一段代码正常运行的原因就是因为指针p调用Print函数的时候并不会发生解引用。
第二段代码函数内部访问_a本质上是this->_a由于实参传给形参的时候传给this指针的是一个空指针第二段代码中函数内部对空指针进行了解引用操作所以第二段代码就直接程序运行崩溃了。
其实这里我们直接看汇编代码会更好的帮助我们理解因为代码底层终究还是要转化为指令。
在这里插入图片描述
我们发现指令中并没有解引用的行为即并不会通过指针p来寻找Print函数而是编译阶段在公共代码区域去找到Print函数这与访问成员变量有所区别访问成员变量的话要到指针p所指向对象的空间中去寻找。

既然成员函数并不在对象中那我们能不能这样去访问成员函数呢A::Print()
这中写法是坚决错误的因为这里成员函数中的this指针什么也没有接收到换句话说A::Print()什么也没有传给形参this哪怕传一个空指针nullptr也可以就怕什么都不传。我们要知道类创建的对象之所以可以调用成员函数就是因为this指针的作用我们通过指向这个对象的指针this指针接收到指向这个对象的指针之后就可以访问对象中的成员变量比如_year _month _day
这里还要注意一个问题我们是不可以这样去写的A::Print(nullptr)或者 A::Print(p);因为this指针不能在形参和实参进行显式传递

好了到这里就是C++中类和对象中第一部分的内容了。
就到这里了再见啦各位

在这里插入图片描述

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6
标签: c++