类和对象三部曲(上)

Posted 做1个快乐的程序员

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了类和对象三部曲(上)相关的知识,希望对你有一定的参考价值。

1、面向过程和面向对象的区别

  众所周知,编程语言分为面向对象和面向过程两大类,那么什么是面向对象,什么又是面向过程呢???
在这里插入图片描述
  我们以外卖为例对此进行分析:
面向过程:订外卖、送外卖、取外卖。重点在这三个过程,每个过程什么人去做,怎么做。
面向对象:商家、骑手、客户。分三个对象,每个对象有目标和任务,该做什么是明确的。
  C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
  C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。
  我们在类的引入和定义中具体通过代码来观察面向过程和面向对象的区别。

2、类的引入和定义

2.1 类的引入

C语言中,结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。并且在C语言中,数据和方法是分离的
从下面可以看出对于面向过程而言,数据和方法是分开的,我们重点关注的是过程->函数!

typedef int STDataType;
//数据
struct MyStack
{
	STDataType* a;
	int size;
	int capacity;
};
//方法
void StackPush(struct Stack* ps, STDataType x);

对于C++而言,延申出一个新的定义–类有两部分组成:成员变量(属性)和成员函数(行为)。并且,一个类中同时存在成员变量和成员函数

struct Stack
{
	void Init(int initSize = 4)
	{
		a = (STDataType*)malloc(sizeof(STDataType)* initSize);
		size = 0;
		capacity = initSize;
	}
	//需要注意的是:成员变量在这只是声明,并没有定义,没有分配内存
	STDataType* a;
	int size;
	int capacity;
};

  值得注意的是,在C++中,不再使用struct,而是使用class定义一个类,当然保留了struct的使用,即在C++中struct代表类,可以同时存在成员变量和成员函数。对于struct和class有什么区别我们在访问限定符中再给大家详细介绍。

class Stack //使用class定义一个类
{
	void Init(int initSize = 4)
	{
		a = (STDataType*)malloc(sizeof(STDataType)* initSize);
		size = 0;
		capacity = initSize;
	}
	STDataType* a;
	int size;
	int capacity;
};

2.2类的定义

我们先对类的构成进行分析:
  class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号。
  类中的元素称为类的成员:类中的数据称为类的属性或者成员变量; 类中的函数称为类的方法或者成员函数。

class className
{
	// 类体:由成员函数和成员变量组成
}; // 一定要注意后面的分号

类的定义有两种方式:
  a:声明和定义全部放在类体中,需要注意:成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
  b:声明放在.h文件中,类的定义放在.cpp文件中。
🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋🍋

3、类的访问限定符及其封装

3.1访问限定符

C++实现封装的方式:用类将对象的属性与方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用。
      public(公有)
访问限定符:protected(保护)
      private(私有)
【访问限定符说明】
  1. public修饰的成员在类外可以直接被访问
  2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  4. class的默认访问权限为private,struct为public(因为struct要兼容C)
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别

  所以我们在设计类的过程中,一般将成员变量设计为私有,而对于想给别人访问的成员函数定义为公有,不想给别人访问的成员函数定义为私有或保护,至于私有和保护的区别,在后面继承中才加以区分。

typedef int STDataType;
class Stack
{
public:
	void Init(int initSize = 4);
	void Push(STDataType x);
private:
	STDataType* a;
	int size;
	int capacity;
};

3.2封装

  面向对象的三大特性:封装、继承、多态
  实际中,大家也要注意,面向对象不止三大特性。比如:抽象、反射(java)。
在这里插入图片描述
  封装实际上是一种管理,好比图中的遥控器,用户没有必要了解它的工作原理,怎么控制频道的切换,只需直到每个按钮的作用即可,按钮就是程序中的成员函数。
  数据和方法都封装到类中,管理起来,想给你访问的定义成公有,不想给你访问的定义成私有或保护。

  那么面向对象的封装好呢?还是面向过程的不封装好? -答案当然是:封装更好,封装更严格,不封装更自由

封装:将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。

4、类的作用域

  因为有类的出现,所以出现了新的作用域—类域。
  类的所有成员都在类的作用域中。在类体外定义成员,需要使用::作用域解析符指明成员属于哪个类域

class Person
{
public:
	void PrintPersonInfo();
private:
	char _name[20];
	char _gender[3];
	int _age;
};
// 这里需要指定PrintPersonInfo是属于Person这个类域
void Person::PrintPersonInfo()
{
	cout << _name << " "_gender << " " << _age << endl;
}

5、类的实例化

  用类的类型创建一个对象的过程就叫做–类的实例化。
    1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它。
    2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量。

  做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间。

typedef int STDataType;
class Stack
{
public:
	void Init(int initSize = 4);
private:
	STDataType* a;
	int size;
	int capacity;
};
void Stack::Init(int initSize)
{
	a = (STDataType*)malloc(sizeof(STDataType)* initSize);
	size = 0;
	capacity = initSize;
}
int main()
{
	Stack st;
	st.Init();
	//这就是实例化对象
	Stack s1;
	Stack s2;
	return 0;
}

6、 类对象模型

  在这里小编会向大家介绍一下类大小的计算以及类对象的存储方式。

  类对象的存储方式无非有两种:
    a:对象中包含类的各个成员,即成员函数和成员变量都包含
    b:只保存成员变量,成员函数在公共代码段
  经过分析,每个对象中成员变量是不同的,但是调用同一份函数,如果按照方式a存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。那么如何解决呢?
  所以最终结果是:只保存成员变量,成员函数存放在公共的代码段
  我们参照下面的代码,即可得出以上结论:而对于成员变量的大小计算,则遵顼结构体内存对齐规则,相信聪明的你们对这个已经很熟悉,如果还不了解的可以去百度一下,小编搬用的其它博主一篇文章,大家可以点击结构体内存对齐规则进行了解。

class A1 {
public:
	void f1(){}
private:
	int _a;
};
int main()
{
	A1 aa;
	cout << sizeof(A1) << endl;//4

这里还有一种特殊情况,对于没有成员变量的类,它的大小是多少呢?我们下面来研究。

//a:类中仅有成员函数
class A2 {
public:
	void f2() {}
};
//b:类中什么都没有 - 空类
class A3
{};
int main()
{
	A2 a2;
	cout << sizeof(a2) << endl;//1

	A3 a3;
	cout << sizeof(a3) << endl;//1
}

  经过结果的输入,发现结果为1,这是为什么呢?
  在这里,大小是1,并不是代表他们的大小为1,而是给1byte内存进行占位,表明这个对象存在过,如果大小为0,对上面的A2类,创建多个对象,a2,aa2,aaa2,那么怎么区分这三个对象呢?唯一的方法就是查看地址,地址不同就说明对象不同,如果大小为2,在内存中没有创建,就不能区分了。所以这里的作用是占位

7、 this指针

7.1this指针的引出

class Date
{
public:
	void Init(int year, int month, int day)
	{
		//这里面有两个year,但是参数的访问遵循就近原则,所以这两个year都是函数Init参数里的year,所以Date类中的year对象,并没有赋值
		//这其实是自己赋值给自己了,下面Date类中的year、month、day根本压根没作用
		year = year;
		month= month;
		day= day;
	}
private:
	int year; // 年
	int month; // 月
	int day; // 日
};
int main()
{
	Date d1;
	d1.Init(1998, 07, 18);
	return 0;
}

程序经过运行,我们发现Date类中的year、month、day没有改变,这是为什么呢?我们代码块中的注释已经给了答案,因为就近原则的限制,访问的两个year、month、day都是Init形参中的变量,所以没有进入到Date类中,那么针对这样的问题,我们应该怎么解决呢?

法一:改变变量的命名风格
经过命名风格改进以后:这样就没有冲突了,year就是参数里面的year,_year就是成员变量里的_year

class Date
{
public:
	//这个地方可以手动加this:this->year = year;但是编译器不会自动加this,因为函数名相同,他也不知道你想访问那个year,所以最后加域作用符:;或者改命名风格
	void Init(int year, int month, int day)
	{
		_year = year;
		_month= month;
		_day= day;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
	//其他风格:
	//int year_;//_放到变量后面
	//int m_year;//m代表成员变量(member)
	//int mYear;
};
int main()
{
	Date d1;
	d1.Init(1998, 07, 18);
	return 0;
}

法二:使用域作用限定符
加一个“::”,作为指定域的访问就可以了,但是这样去写太麻烦

class Date
{
public:
	void Init(int year, int month, int day)
	{
		Date::year = year;
		Date::month= month;
		Date::day= day;
	}
private:
	int year; // 年
	int month; // 月
	int day; // 日
};
int main()
{
	Date d1;
	d1.Init(1998, 07, 18);
	return 0;
}

法三:this指针
  a:因为这个this是一个隐含参数,所以我们不能越俎代庖在函数的形参以及函数调用对象初始化的时候,加上this,比如:void Init(Date* this, int year, int month, int day)、d1.Init(&d1, 1998, 07, 18);,不能自己写出来
  b:可以在成员函数中使用this指针
  c:this指针是存在栈里面的,因为他是一个形参;参数和局部变量都是在栈里面,因为this出了这个函数就不用了,所以在栈中。不同的编译器不同,VS是使用ecx寄存器存储,传参的

class Date
{
public:
	//这里的Init并不是只有3个参数,而是有4个参数,编译器遇到他会进行处理,增加一个隐含的参数,这是编译器编译时自己加的。
	//void Init(Date* this, int year, int month, int day)//这是固定死的,就叫this
	void Init(int year, int month, int day)
	{
		//经过编译器处理,加一个this关键字以后,类中的函数实际是这样的
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
//经过封装以后,在main函数里是不能随便更改成员变量的值的,只能初始化或者调用类中的函数,函数中可以检查日期的合法性,保证了成员变量的安全性。
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1;
	d1.Init(1998, 07, 18);//d1.Init(&d1, 1998, 07, 18);
	return 0;
}

那么问题又来了?
多个对象进行赋值时,是怎么保证每个对象进行精确进行对象的确定的?
在这里插入图片描述
答案是:通过this指针
我们将this打印出来

class Date
{
public:
	void Init(int year, int month, int day)
	{
		this->_year = year;
		this->_month = month;
		this->_day = day;
		//打印一下this
		cout << "this:" << this << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1;
	cout << "&d1:" << &d1 << endl;
	d1.Init(1998, 07, 18);//d1.Init(&d1, 1998, 07, 18);

	//哪个对象去调用成员函数,成员函数中访问的就是哪个对象中的成员变量,是通过this指针做到的。
	Date d2;
	cout << "&d2:" << &d2 << endl;
	d2.Init(1999, 01, 02);//d1.Init(&d2, 1999, 01, 02);
	return 0;
}

在这里插入图片描述
通过图中,我们可以观察到,当对对象d1进行赋值时,this此时就是d1,当对d2对象进行操作时,this就是d2。

7.2this指针的特性

   1. this指针的类型:类类型* const
   2. 只能在“成员函数”的内部使用
   3. this指针本质上其实是一个成员函数的形参,是对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针。
   4. this指针是成员函数第一个隐含的指针形参,一般情况由编译器通过ecx寄存器自动传递,不需要用户传递
   5. this指针是非静态成员函数的隐含形参,静态函数没有this指针。

关于类和对象还有好多内容需要我们进行探讨和学习,本篇文章的内容介绍到这里。如果大家还想对类和对象有进一步的了解,麻烦大家多多点赞,小编会继续更新。
在这里插入图片描述

以上是关于类和对象三部曲(上)的主要内容,如果未能解决你的问题,请参考以下文章

Peter Hessler和他的中国三部曲(上)

片段内部静态类和gradle问题

JavaScript基础总结三部曲之一

k8s自定义controller三部曲之二:自动生成代码

使用CSS伪类和伪对象实现dl;dt+dd在一行显示

使用CSS伪类和伪对象实现dl;dt+dd在一行显示