在c++中,类是一种数据类型。我们只能说设计了一个类,不能说定义了一个类。类型的设计来自对现实世界的认知。
我们来看看下面这个对象,实体与类的关系图:
我们拿盖房子来解释这张图的意义,比如说我要盖房子,我看到别人家的房子有客厅、卧室、游泳房等,我想着自己也要盖一个啥样的房子,我就在自己脑海中有一个房子的抽象,接下来我将房子设计成图纸,就相当于上图中的类,最后我按照这个图纸的样子盖出了我的房子,盖出来的这个房子相当于我们用类实例化的对象。这个例子可以用下图来说明:
封装是面向对象程序设计最基本的特性,把数据(属性)和函数(操作)合成一个整体,在计算机世界是用类和对象实现的。
接下来我们设计一个商品Cgoods这个类:
class Cgoods//Cgoods是一个类名
{
private://私有访问限定符
char Name[21];//商品名称
int Amount;//商品数量
float Price;//商品价格
float Value;//商品价值
public://公有访问限定符
//下面这几个函数都是类成员函数的声明
void RegisterGoods(char[],int,float);//输入数据
void CountTotal(void);//计算商品总价值
void GetName(char[]);//读取商品名
int GetAmount(void);//读取商品总数量
float GetPrice(void);//读取商品总价格
float GetValue(void);//读取商品总价值
};//最后这个;不能少
成员访问限定符共有三种(实际上是四种):private、public、protected、什么都不写就默认为private。每种说明符可在类体中访问多次,它们的作用域是从该说明符出现开始到下一个说明符之前或类体结束之前。
访问说明符protected和private体现了类具有封装性,也就是说将类中的成员属性和成员函数实现保护起来的特性就是封装性。这种封装性具体作用于对象,也就是说我们可以访问这个类所实例化出对象的公有成员,不能访问这个类所实例化出对象的私有和保护的成员,一旦提到访问属性和方法时,一定指的是对象。
下面这个代码块,Cgoods这个对象实例化出c1这个对象,c1去访问私有属性Amount时会出错。
int main()
{
Cgoods c1;
c1.Amount;//error,Amount这个属性是私有的
return 0;
}
c++中的类比c语言中的结构体(struct)多了成员方法,类中含有成员属性和成员方法(函数),而结构体只有成员属性。
我们可以直接在类中实现成员方法。一般情况下,为了使类体不会很庞大,我们都在类中进行方法的声明,在类外进行方法的实现。
接下来我们在类外实现成员函数。
void Cgoods::RegisterGoods(char name[],int amount,float price)
{
strcpy(Name,name);//字符串拷贝函数
Amount=amount;
Price=price;
}
void Cgoods::CountTotal(void)
{
Value=Price*Amount;
}
void Cgoods::GetName(char name[])
{
strcpy(name,Name);
}
“::”为作用域解析运算符,在成员函数名前面加上“类名::”,就是告诉编译器,这个函数不是一个全局函数,或者说不是一个野函数,而是这个类里面的成员函数。
接着我们来分析几行代码,主要是来帮助我们理解变量或对象的可见性和生存周期的问题。如下图所示:
凡是在函数之外定义的变量都存放在数据区,凡是在函数内定义的变量都在栈区。
可见性指的是编译和链接的过程,而生存期指的是我们所说的运行过程。可见性是从这个变量或者对象被定义或者是被声明之后向下可见,而生存期就要分多钟情况来说了。一个全局变量或者全局对象的生存期是程序开始到程序结束一直存在,而一个局部变量或者局部对象的生存期指的是这个函数结束,它里面的变量或者对象的生存期也就到了。
当我们需要实例化很多对象时,每个对象都有自己独立的成员属性和成员方法,这样的话会占用很大一部分内存空间,如下图所示:
为了节约空间我们只为每个对象分配他的属性区(数据区),而他们的成员方法被放在一个公用的区域中,这样就大大节省了空间。如下图所示:
虽然这样会大大节约空间,但是有一个特别严重的问题就是,当我们的对象调用成员方法的时候,我们如何去区分这是哪个对象在调动其成员方法,因为每个对象的各自属性值都是不同的,所以调动成员方法所要操作的具体的属性值也是不同的,那么c++的编译器是如何来解决这个问题的呢?在这里我们就涉及到了this指针这个概念了。
编译器对类的编译一共分为三步:
1.是只扫描类的成员属性,不扫描成员方法,无论这个类的成员属性在类中的哪个地方都先扫描其成员属性。
2.是只扫描类中成员函数的声明,不管成员函数在类中有没有实现,编译器在编译阶段只扫描成员函数的声明。
3.是对类外成员函数的实现进行扫描,编译器去对照函数返回值类型,函数名称,参数类型和参数个数是否和类中的相对应。扫描完成之后对类的成员函数进行了改写,给它的参数里面多加了一个this指针。
例如对下面的成员函数改写:
void Cgoods::RegisterGoods(char name[],int amount,float price)
//void Cgoods::RegisterGoods(Cgoods * const this,char name[],int amount,float price)
{
strcpy(this->Name,name);//字符串拷贝函数
this->Amount=amount;
this->Price=price;
}
void Cgoods::CountTotal(void)
//void Cgoods::CountTotal(Cgoods * const this)
{
this->Value=this->Price*this->Amount;
}
我们可以看到,第三步改写的时候给成员函数的参数里面加了一个this指针,拿const来修饰,说明这个指向是不可修改的。但是还有一点我们也需要知道,只有是类的成员函数的时候,编译器才会给它加上一个this指针,如果是一个普通的全局函数,编译器是不会加this指针的。
当加上this指针扫描函数体的时候,编译器发现了类的成员属性,就会给成员属性也加上this->指针,如果程序员没有加编译器就会自己加上去,所以程序员在自己写程序的时候加不加this指针都是无所谓的。
但是在写类成员函数参数的时候,程序员不需要去加上this指针这个参数,因为形参的传递是由实参压栈的传递的,而this指针是由一个特殊的寄存器ecx带进去的。对象里面是没有this指针的。
当进入主函数时,碰到了类的成员函数,编译器也会对它进行改写,下面是改写的两个例子:
int main()
{
Cgoods c1;
Cgoods c2;
c1.RegisterGoods("c++",10,20);
//RegisterGoods(&c1,"c++",10,20);
//将c1的地址传给this指针
c2.RegisterGoods("java",100,19);
//RegisterGoods(&c2,"java",100,19);
//将c2的地址传给this指针
return 0;
}
正是因为this指针的存在,让我们可以在节约内存的同时对每个对象成员函数操作的过程中不混淆。
inline关键字
在一个函数之前加上inline关键字,就称这个函数为内联函数,如果在类的类体里面直接实现成员函数,系统将默认这个成员函数为内联函数。
当对一个程序进行编译的时候,发现这个函数是一个非内联函数,编译完成要执行的时候,系统就去调用这个函数,调用完成之后系统又返回到到调用该函数的下一行代码继续执行;若当编译器编译的时候发现某个函数是一个内联函数,它就会把这个函数的执行体直接拷贝到函数的调用点上,这个时候系统就不需要再去调动这个函数,直接执行就可以了,系统不需要去调动函数,因此程序的执行时间会变快。
下面用这幅图来解释上面这段话:
但是内联函数有个缺点,就是当函数体非常庞大时,主函数体的代码量就会非常多,会占用很大空间,这就是一个典型的拿空间换时间的例子。
如果一个函数里面有循环或者递归,以及函数体中的代码超过五行,一般是不允许使用内联的。
今天的内容就写到这里了。