前言

hello,大家好,今天我们来继续分享关于C++的知识——类和对象。我们这个部分的知识将会分为三部分来介绍,今天这篇是第一部分。王国维言读书有三境界,今天我们就先来这第一重境界:昨夜西风凋碧树,独上高楼,望尽天涯路。闲言少叙,让我们开始吧。

1.面向对象和面向过程之辨

在这个专栏的第一篇文章中,我们就已经介绍过这个问题,我们可以点击链接回顾一下哦C++之坦白说:我与C语言不得不说的那些事
在这里我们就不过多介绍啦。

C语言是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。
C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完成。

2.类的引入——从struct到class

在C语言中,我们已经学习过了结构体。我们来用一个代码回顾一下:

#include<iostream>
using namespace std;
struct student
{
   
	int id;
	int test_score;
};
void fail(student st)
{
   
	if (st.test_score < 60)
		cout << "yes" << endl;
	else
		cout << "no" << endl;
}

int main()
{
   
	student sst;
	cin >> sst.test_score;
	fail(sst);
}

这是一个检验是否挂科的小程序,我们将一个学生的id和考试成绩定义在一个struct里,又在下面定义了一个检查是否挂科的函数。其实我们也可以将fail函数写在struct里面,这在C语言里面是不允许的,但是在C++里面是可以的,因为<mark>C语言中,结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。</mark>
所以我们可以将上面的函数修改如下

#include<iostream>
using namespace std;
struct student
{
   
	int id;
	int test_score;
	void fail()
	{
   
		if (test_score < 60)
			cout << "yes" << endl;
		else
			cout << "no" << endl;
	}
};

int main()
{
   
	student sst;
	cin >> sst.test_score;
	sst.fail();
}

那么,我们发现,在这个struct的结构体中,我们既定义了一个学生的id和成绩,还定义了一个检验是否挂科的fail的函数,也就是说,我们不仅定义了成员变量(id和score)我们还定义了成员变量的行为(fail),这在C++中,我们有一种更好地比表示方法——类。从此,struct暂时退隐,class正式登上舞台。

3.类的定义

定义一个类的关键字是class。记住它,在我们的C++学习过程中,这个关键字将无比关键。类的定义与struct相似,<mark>在花括号结尾也不要忘记加一个分号哦</mark>

class classname
{
   
   
}

类的定义方式有两种,一种是声明和定义全部放在类体中。还有一种是 声明放在.h文件中,类的定义放在.cpp文件中。
需要注意的是,在第一种方法中,成员函数如果在类中定义,编译器可能会将其当成内联函数处理。
下面我们将演示这两种方法:


一般情况下,我们更推荐第二种方式。这种方式更适合书写大型程序。

4.类的访问限定及封装

4.1访问限定符

认识过了类,我们再来认识三个新朋友,private(私有)、public(公有)、protected(保护)。这三个是C++中的访问限定符,决定访问权限。那么我们为什么要设置访问权限呢?

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

那么,我们先来看一三个限定符各自决定的访问权限吧。

  1. public修饰的成员在类外可以直接被访问
  2. protected和private修饰的成员在类外不能直接被访问(此处protected和private是类似的)
  3. 访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止
  4. class的默认访问权限为private,struct为public(因为struct要兼容C)

好的,明确以上概念我们来举例说明一下为什么要进行访问限定。
比如你有一座很漂亮的庄园,而你又是一位热情好客的主人,所以你经常邀请客人来你的家里做客。客人们可以随意观赏庄园内的花园啊,亭子啊,假山啊(public)但是他们却不能进入你的私人领地比如你的卧室啊你的浴室等地(private)更不能打开你藏在书房里的保险柜和日记(protected)。这就是我们要进行访问限定使用访问限定符的原因。
那么我们来演示一下public和private。由于protected我们在后面进行介绍,我们就先不演示。
首先,我们来观察这个程序:

我们将这个程序做一个小小的改动:

通过以上两个演示,你有没有get到一点二者的神奇?
<mark>通常,我们在定义数据成员的时候设为private,在定义函数成员的时候设为public。这样的设计会在类的使用中发挥许多妙用</mark>

4.2封装

我们在上一节中提到了封装,那么究竟什么是封装呢?我们先来看定义:

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

那么如何来理解呢?
<mark>其实封装的本质是一种管理</mark>举一个例子,期末考试到了,老师如何控制管理期末考试?首先,试卷是严格保密的,老师不会直接把试卷给当代大学生们,所以要放在试卷袋里密封,等到考试才能打开,但是,这么严格保密将会导致一大批人挂科,老师于心不忍啊,那怎么办?于是要划重点,小小地透透题,这样保证大多数学生还是能会几道题的,不至于集体挂科。老师用(private/protected)将试题封装起来确保试卷是保密的,但又通过划重点(public)等你懂的方式泄露了一部分天机,使大家不至于集体挂科。
所以,你get到了吗?

5.类的作用域

我们之前在讲命名空间的时候提到过作用域,也提到过作用域限制符,那么在这里,他们又要重出江湖啦。
类相当于定义了一个新的作用域,类的所有成员都在类的作用域中。那么在类体外定义成员,则需要使用类作用域解析符::来表明属于哪一个类。
上一个例子:

6.类的实例化

什么叫做类的实例化呢?

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

  1. 类只是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它
  2. 一个类可以实例化出多个对象,实例化出的对象 占用实际的物理空间,存储类成员变量
  3. 做个比方。类实例化出对象就像现实中使用建筑设计图建造出房子,类>就像是设计图,只设计出需要什么东西,但是并没有实体的建筑存在,同样类也只是一个设计,实例化出的对象才能实际存储数据,占用物理空间


7.类的对象模型

7.1如何计算类对象的大小

我们知道,类中既有成员变量,又有成员函数,那么,我们该如何计算类的大小呢?

7.2类对象存储方式的猜测

1.对象中包含类的各个成员。既包括成员变量,又包括成员函数。
这种猜测是有道理的,但是也是有缺陷的,我们来看看下面这段代码


#include<iostream>
using namespace std;
class student
{
   
	int id;
	int score;
	int age;
	void fail()
	{
   
	  if(score>60)
	     cout<<"no"<<endl;
	   else
	     cout<<"yes"<<endl;
	  } 
};
int main()
{
   
	student st1;
	student st2;
	return 0;
}

在这个代码中,我们创建了s1,s2,两个对象,但是,当我们试图通过s1,s2,来调用fail函数时,每一个对象大都会保存一份函数代码,这样会造成空间的浪费。貌似不妥。
2.只保留成员变量,成员函数存在公共的代码段。
这样似乎就可以解决上面我们提出的问题。那么事实的真相到底是什么呢?
我们来验证一下:

我们可以发现:

一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐。

那么,我们不得不要考虑一种特殊情况,空类怎么办呢?

#include<iostream>
using namespace std;
class A
{
   

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

输出结果会是什么呢?根据我们上面得出的结论,可能会是0?因为空类里面没有成员变量啊,真的是这样吗?我们来验证一下:

答案是1!这是为什么呢?我们来分析一下:
如果大小为0的话,当我们利用A创建了a1和a2两个对象时,我们该如何区分他们?所以,我们要用一个字节来“占位”,来表示对象存在过。
所以综上所述,我们可以总结出:

一个类的大小,实际就是该类中”成员变量”之和,当然也要进行内存对齐,注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类。

8.this指针

好的,接下来就来到了我们的this指针环节啦。

8.1 this指针的引出

首先,我们先来定义一个日期类。


#include<iostream>
using namespace std;
class Date
{
   
public:
	void Display()//输出
	{
   
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	void SetDate(int year, int month, int day)//初始化
	{
   
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
   
	Date d1, d2;
	d1.SetDate(2018, 5, 1);
	d2.SetDate(2018, 7, 1);
	d1.Display();
	d2.Display();
	return 0;
}

我们来看一下运行结果:

那么我们这里有一个疑惑哈,Date类中有SetDate与Display两个成员函数,函数体中没有关于不同对象的区分,那当d1调用SetDate函数时,该函数是如何知道应该设置d1对象,而不是设置d2对象呢?
其实在这个过程中,编译器会增加一个隐隐含的参数——this指针。所以,如果我们把this指针表现出来,程序就会变成这个样子:

#include<iostream>
using namespace std;
class Date
{
   
public:
	void Display()//输出
	{
   
		cout << _year << "-" << _month << "-" << _day << endl;
	}
	void SetDate(Date* this,int year, int month, int day)//设置
	{
   
		this->_year = year;
		this->_month = month;
		this->_day = day;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
   
	Date d1, d2;
	d1.SetDate(&d1,2018, 5, 1);
	d2.SetDate(&d2,2018, 7, 1);
	d1.Display();
	d2.Display();
	return 0;
}



所以,我们可以总结出:

C++中通过引入this指针解决该问题,即:C++编译器给每个“非静态的成员函数“增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有成员变量的操作,都是通过该指针去访问。只不过所有的操作对用户是透明的,即用户不需要来传递,编译器自动完成

注意:

总结

好的,今天我们的分享就先到这里,感谢大家支持,也欢迎大家批评指正。我们接下来会继续介绍类和对象的相关知识,希望对大家有所帮助。最后,我们分享文章标题中的那首诗晏殊的《蝶恋花》

槛菊愁烟兰泣露,
罗幕轻寒,燕子双飞去。
明月不谙离恨苦,斜光到晓穿朱户。

昨夜西风凋碧树,
独上高楼,望尽天涯路。
欲寄彩笺兼尺素,山长水阔知何处?