【C++】深入理解类和对象(1)

自己打败自己是最可悲的失败,自己战胜自己是最可贵的胜利。💓💓💓 

目录

  ✨说在前面

🍋知识点一:类的定义

• 🌰1.类定义格式

• 🌰2.访问限定符

• 🌰3.类域

🍋知识点二:实例化

• 🌰1.什么是实例化?

• 🌰2.对象的大小

• 🌰3.this指针

🍋C与C++实现Stack对比

• 🌰1.封装

• 🌰2.Stack的实现对比

 • ✨SumUp结语


  ✨说在前面

亲爱的读者们大家好!💖💖💖,我们又见面了,上一篇文章中我带大家学习了C++最基本的基础语法和C++的发展历史如果大家没有掌握好,可以再回去看看,复习一下,再进入今天的内容。

今天我们将要学习C++中很重要的一部分,也是C++学习中的第一大关卡——类和对象。如果大家准备好了,那就接着往下看吧~

  👇👇👇
💘💘💘知识连线时刻(直接点击即可)

【C++】入门基础知识【C++】入门基础知识

  🎉🎉🎉复习回顾🎉🎉🎉

        

   博主主页传送门:愿天垂怜的博客

 

🍋知识点一:类的定义

• 🌰1.类定义格式

🔥class为定义类的关键字,Stack为类的名字,{}中为类的主体,注意类定义结束时后面的分号是不能省略的。类体中的内容称为类的成员:类中的变量称为类的属性或成员变量;类中的函数称为类的方法或成员函数,

🔥为了区分成员变量,一般习惯上成员变量会加一个特殊标识,如成员变量前面或者后面加_或者m开头,注意C++中这个并不是强制的,只是一些惯例,具体看公司的要求。

🔥C++中struct也可以定义类,C++兼容C中struct的语法,同时struct升级成了类,明显的变化就是struct中可以定义函数,一般情况下我们还是推荐用class定义类。

🔥定义在类里面的成员函数默认为inline。

class Stack
{
	void Push(int x)//入栈
	{
		//...
	}
	void Pop()//出栈
	{
		//...
	}
	int Top()//取栈顶数据
	{
		//...
	}
	int* a;
	int top;
	int capacity;
};

 

• 🌰2.访问限定符

🔥C++一种实现封装的方式,用类将对象的属性和方法结合在一块,让对象更加完善,通过的访问权限选择性的将其接口提供给外部的用户使用。

🔥public修饰的成员在类外可以直接被访问;protected和private修饰的成员在类外不能直接被访问,protected和private是一样的,在学习继承的时候才能体现出他们的区别。

🔥访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现为止,如果后面没有访问限定符,作用域就要到类结束。

🔥class定义成员没有被访问限定符修饰时默认为private,struct默认为public。

🔥一般成员变量都会被限制为private/protected,需要给别人使用的成员函数会放为public。

示例1:访问限定符的使用

class Stack
{
public:
	void Push(int x)//入栈
	{
		//...
	}
	void Pop()//出栈
	{
		//...
	}
	int Top()//取栈顶数据
	{
		//...
	}
private:
	int* a;
	int top;
	int capacity;
};

示例2:区分成员变量

class Date
{
public:
	void Init(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	//为了区分成员变量,一般习惯上成员变量会加一个特殊标识_或者m开头
	int _year;//year m_year
	int _month;//声明,没有开空间
	int _day;
};

int main()
{
	Date d;
	d.Init(2024, 7, 9);

	return 0;
}

 

• 🌰3.类域

🔥类定义了一个新的作用域,类的所有成员都在类的作用域中,在类外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。

🔥类域影响的是编译的查找规则,下面程序中Init如果不指定类域Date,那么编译器就会把Init当成全局函数,那么编译时,找不到成员的声明/定义在哪里,就会报错。指定类域Date,就是直到Init是成员函数,当前域找不到成员,就会到类域中区查找。

示例:声明与定义分离

class Date
{
public:
	void Init(int year, int month, int day);
private:
	int _year;
	int _month;
	int _day;
};

//类外定义成员函数
void Date::Init(int year, int month, int day)
{
	_year = year;
	_month = month;
	_day = day;
}

注意:声明与定义分离,Init函数就不属于内联函数。

 

🍋知识点二:实例化

• 🌰1.什么是实例化?

🔥用类类型在物理内存中创建对象的过程,称为类实例化对象。

🔥类是对象进行一种抽象描述,是一个模型一样的东西,限定了类有哪些成员变量,这些成员链表只是声明,没有分配空间,用类实例化出来对象时,才会分配空间。

🔥一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。打个比方:类实例化出的对象就像现实中使用建筑设计图建造出房子,类就是设计图,设计图规划了有多少个房间,房间大小功能等,但并没有实体的建筑存在,也不能住人,用设计图修建出房子,房子才能住人。同样类就像设计图一样,不能存储数据,实例化出的对象分配物理内存存储数据。

示例:类实例化出对象,才会分配空间。 

#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类实例化出对象d1和d2
	Date d1;
	Date d2;

	d1.Init(2024, 3, 31);
	d1.Print();

	d2.Init(2024, 7, 5);
	d2.Print();

	return 0;
}

 

• 🌰2.对象的大小

分析一下类对象中哪些成员呢?类实例化出的每个对象,都有独立的数据空间,所以对象中肯定包含成员变量,那么成员函数是否包含呢?首先函数被编译后是一端指令,对象中没办法存储,这些指令存储在一个单独的区域(代码段),那么对象中非要存储的话,只能是成员函数的指针。再分析一下,对象中是否有存储指针的必要呢?Date示例化出d1和d2的成员函数Init/Print指针确是一样的,存储在各自的数据,如果用Date示例化100个对象,那么成员函数指针就重复存储100次,太浪费了。这里需要在额外说一下,其实函数指针是不需要存储的,函数指针是一个地址,调用函数被编译成汇编指令[call地址],其实编译器在编译链接的时候,就要找到函数的地址,不是在运行时找,只有动态多态是在运行时找,这个以后我们会说。

上面我们分析了对象中只存储成员变量,C++规定类实例化的对象也要符合内存对齐的规则。

我们回顾一下结构体的内存对齐规则:

🔥第一个成员在与结构体偏移量为0的地址处。

🔥其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。

注意:对齐数 = min{ 编译器默认的一个对齐数, 该成员大小 },VS中默认对齐数为8。

🔥结构体总大小为最大对齐数(所有变量最大者与默认对齐参数取最小)的整数倍。

🔥如果嵌套了结构体的情况,嵌套的结构体对其到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(含有嵌套结构体的对齐数)的整数倍。

问:为什么结构体需要结构对齐?

CPU在读取内存中的数据时,一次读取的数据量是有限的,通常是按照字(word)或双字(dword)等单位进行读取,这些单位的大小通常是2的幂次方(如4字节或8字节)。如果结构体中的成员变量不满足对齐要求,CPU可能需要多次读取操作才能获取完整的数据,并且,CPU不能从任意位置开始读取(整数倍位置开始读取),如一个int类型的变量会被拆成两次取读取,这会降低读取效率。而对齐后的结构体成员变量地址是连续的,CPU可以通过一次读取操作将整个结构体读入,从而提高读取效率。

#include <iostream>
using namespace std;

class A
{
public:
	void Print()
	{
		cout << _ch << endl;
	}
private:
	char _ch;
	int _i;
};

class B
{
public:
	void Print()
	{
		//...
	}
};

class C {};

int main()
{
	A a;
	B b;
	C c;
	cout << sizeof(a) << endl;
	cout << sizeof(b) << endl;
	cout << sizeof(c) << endl;

	return 0;
}

我们以上面的代码为例,思考A、B、C三种类类型所实例化出的对象a,b,c大小分别是多少。

根据对齐规则,我们很容易知道a的大小是8个字节,那b和c呢?b好歹有一个成员函数,c什么都没有,难道大小是0吗,那如果是0,就是没有开空间,那怎么证明b和c存在?

然而,上面程序运行后,我们看到没有成员变量的B和C类对象的大小是1,这个1,纯粹是为了占位,标识对象存在。

 

• 🌰3.this指针

我们之前写的Date类中有Init与Print两个成员函数,函数体内没有关于不同对象的区分,那当d1调用Init和Print函数时,该函数是如何知道应该访问的是d1对象还是d2对象呢?那么这里就要看到C++给了一个隐含的this指针解决这里的问题。

🔥编译器编译后,类的成员函数默认都会在形参的第一个位置,增加一个当前类类型的指针,叫做this指针。如Date类的Init的真实原型为:void Init(Date* const this, int year, int month, int day)

🔥类的成员函数中访问成员变量,本质都是通过this指针访问的,如Init函数中给_year赋值,this->year = year

🔥C++规定不能再形参和实参的位置显式的写this指针(编译时编译器会处理),但是可以在函数体内显式使用this指针。

#include<iostream>
using namespace std;

class Date
{
public:
	//void Init(Data* const this, int year, int month, int day)
	void Init(int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
	//void Print(Data* const this);
	void Print()
	{
		cout << this->_year << "/" << this->_month << "/" << this->_day << endl;
	}
private:
	//这里只是声明,没有开空间
	int _year;
	int _month;
	int _day;
};

int main()
{
	//Date类实例化出对象d1和d2
	Date d1;
	Date d2;
	
	d1.Init(2024, 3, 31);//d1.Init(&d1, 2024, 3, 31);
	d1.Print();//d1.Print(&d1);

	d2.Init(2024, 7, 5);//d2.Init(&d2, 2024, 7, 5);
	d2.Print();//d2.Print(&d2);
	
	return 0;
}

下面通过两个选择题测试一下前面只是到底有没有真正理解。

1.下面程序编译运行的结果是( )

A.编译报错        B.运行崩溃        C.正常运行

#include <iostream>
using namespace std;

class A
{
public:
    void Print()
   {
        cout << "A::Print()" << endl;
   }
private:
   int _a;
};

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

正确答案为C.

那为什么是正常运行呢?很多人会认为p是一个空指针,那么p->Print就是空指针解引用。实际这个想法是错误的,因为编译后p->Print所对应的指令是[call + 地址],而后[mov ecx p],这个地址并不存在p对象中,所以不会造成空指针的解引用,代码正常运行。

2.下面程序编译运行的结果是( )

A.编译报错        B.运行崩溃        C.正常运行

#include < iostream>
using namespace std;

class A
{
public:	
	void Print()
	{
		cout << "A::Print()" << endl;
		cout << _a << endl;
	}
private:
	int _a;
};

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

	return 0;
}

正确答案是B. 

那为什么会崩溃呢?崩溃的点不在于p->Print,而在于Print函数中会打印出_a的值,而_a是确确实实存在类对象中的,相当于解引用,所以会崩溃。

3.this指针存在内存中的哪个区域( )

A.栈        B.堆        C.静态区        D.常量区        E.对象里面

正确答案是A.

首先我们再计算类实例化的对象的大小时,并没有计算this指针,所以肯定是不在对象里的;而由于this指针是形参,形参存放在函数栈帧,也就是在栈区,所以这道题选A更合适。

为什么说更合适而不是正确呢,因为这还得看编译器,如VS下其实是存放在寄存器里的。

 

🍋C与C++实现Stack对比

 

• 🌰1.封装

面向对象三大特性:封装、继承、多态,下面的对比我们可以初步了解一下封装。 

🔥C++中数据结构和函数都放到了类里面,通过访问限定符进行了限制,不能再随意通过对象直接修改数据,这是C++封装的一种体现,也是最重要的变化。这里封装的本质是一种更严格规范的管理,避免出现乱访问修改的问题。当然封装不仅仅是这样的,我们后面还需要不断的去学习。

🔥C++中有一些相对方便的语法,比如Init给的缺省参数会方便很多;成员函数不需要传对象地址,因为有this指针隐含地传递了,方便了很多;使用类型不再需要typedef用类名就很方便。

🔥我们再这个C++的入门阶段实现Stack看起来变了很多,实际上本质变化不大,如果用后面的STL中用适配器实现的Stack,大家就能体会到C++的魅力了。

 

• 🌰2.Stack的实现对比

C语言实现Stack代码如下:

#include<stdio.h>
#include<stdlib.h>
#include<stdbool.h>
#include<assert.h>

typedef int STDataType;
typedef struct Stack
{
	STDataType * a;
	int top;
	int capacity;
}ST;

//栈的初始化
void STInit(ST * ps)
{
	assert(ps);
	ps->a = NULL;
	ps->top = 0;
	ps->capacity = 0;
}

//栈的销毁
void STDestroy(ST * ps)
{
	assert(ps);
	free(ps->a);
	ps->a = NULL;
	ps->top = ps->capacity = 0;
}

//压栈
void STPush(ST * ps, STDataType x)
{
	assert(ps);
	if (ps->top == ps->capacity)
	{
		int newcapacity = ps->capacity == 0 ? 4 : ps->capacity * 2;
		STDataType * tmp = (STDataType*)realloc(ps->a, newcapacity * sizeof(STDataType));
		if (tmp == NULL)
		{
			perror("realloc fail");
			return;
		}
		ps->a = tmp;
		ps->capacity = newcapacity;
	}
	ps->a[ps->top] = x;
	ps->top++;
}

//判断栈是否为空
bool STEmpty(ST * ps)
{
	assert(ps);
	return ps->top == 0;
}

//弹栈
void STPop(ST * ps)
{
	assert(ps);
	assert(!STEmpty(ps));
	ps->top--;
}

//取栈顶元素
STDataType STTop(ST * ps)
{
	assert(ps);
	assert(!STEmpty(ps));
	return ps->a[ps->top - 1];
}

//栈的大小
int STSize(ST * ps)
{
	assert(ps);
	return ps->top;
}

int main()
{
	ST s;
	STInit(&s);
	
	STPush(&s, 1);
	STPush(&s, 2);
	STPush(&s, 3);
	STPush(&s, 4);
	while (!STEmpty(&s))
	{
		printf("%d\n", STTop(&s));
		STPop(&s);
	}
	
		STDestroy(&s);
		return 0;
}

C++实现Stack代码如下:

#include<iostream>
using namespace std;

typedef int STDataType;
class Stack
{
public:
	//栈的初始化
	void Init(int n = 4)
	{
		_a = (STDataType*)malloc(sizeof(STDataType) * n);
		if (nullptr == _a)
		{
			perror("malloc申请空间失败");
			return;
		}
		_capacity = n;
		_top = 0;
	}
	//压栈
	void Push(STDataType x)
	{
		if (_top == _capacity)
		{
			int newcapacity = _capacity * 2;
			STDataType * tmp = (STDataType*)realloc(_a, newcapacity * sizeof(STDataType));
			if (tmp == nullptr)
			{
				perror("realloc fail");
				return;
			}
			_a = tmp;
			_capacity = newcapacity;
		}
		_a[_top++] = x;
	}
	//弹栈
	void Pop()
	{
		assert(_top > 0);
		--_top;
	}
	//判断栈是否为空
	bool Empty()
	{
		return _top == 0;
	}
	//取栈顶元素
	int Top()
	{
		assert(_top > 0);
		return _a[_top - 1];
	}
	//栈的销毁
	void Destroy()
	{
		free(_a);
		_a = nullptr;
		_top = _capacity = 0;
	}
private:
	//成员变量
	STDataType * _a;
	size_t _capacity;
	size_t _top;
};

int main()
{
	Stack s;
	s.Init();
	s.Push(1);
	s.Push(2);
	s.Push(3);
	s.Push(4);
	while (!s.Empty())
	{
		printf("%d\n", s.Top());
		s.Pop();
	}

	s.Destroy();

	return 0;
}

 

 • ✨SumUp结语

到这里本篇文章的内容就结束了,本节初步带大家学习了类和对象的第一大内容。这是类和对象的基础,后面的内容会更加有难度,下一章节的内容也格外重要。希望大家能够认真学习,打好基础,迎接接下来的挑战,希望大家继续捧场~

  • 35
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值