类与对象(上)

1.面向过程和面向对象初步认识

    其实祖师爷最开始是在C的基础上想扩展的是类和对象,最开始C++是以此角度去扩展的。前面一些新增的语法是发现C语言中有些地方不好用于是改出的一些东西。这里初次认识一下什么是面向过程什么是面向对象。

    C语是面向过程的,关注的是过程,分析出求解问题的步骤,通过函数调用逐步解决问题。拿洗衣服举例子看看面向过程的关注:

    C++是基于面向对象的,关注的是对象,将一件事情拆分成不同的对象,靠对象之间的交互完 成。同样拿洗衣服举例看看面向对象的关注:

    其实面向对象是在更好的描绘这个世界,更像现实中的样子,它在虚拟的计算机中描绘了现实的样子。也可以说面向对象是比面向过程更高级的开发方式。先简单这样了解,后期慢慢深入理解。

2.类的引入

    其实很早就已经接触过类了,在C++中struct首先升级成了类。因为C++是兼容C语言的,struct以前的用法都可以继续用,所以按照原来结构体的方式定义也是可以的,如定义一个栈:

struct Stack
{
	int* a;
	int top;
	int capacity;
};

int main()
{
	struct Stack st1;
	return 0;
}

    但C++同时把struct升级成了类,所以可以理解为用struct定义了一个类,Stack就是类名,类名就可以做类型,所以main函数中可以这样定义变量:

	Stack st2;

    C语言结构体中只能定义变量,在C++中,类里面不仅可以定义变量,也可以定义函数。如:

struct Stack
{
	void Init(int defaultcapacity = 4)
	{
		a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == a)
		{
			perror("malloc申请空间失败");
			return;
		}
		capacity = defaultcapacity;
		top = 0;
			
	}

	int* a;
	int top;
	int capacity;
};

    把类中定义的变量称为成员变量,类中定义的函数称为成员函数,类就是把数据和方法都放在了一起。这样做的第一个好处是:以前C语言写栈/队列/链表等结构时都要为不同的结构写不同的初始化函数,所以一般Init前面加个前缀名称方便用于不同结构的初始化。现在C++中就不需要加前缀名称了,因为把不同的结构都定义成了不同的类,各自的数据和方法都在一起,因此各自玩各自的就可以了。下面再看这样的例子:

    为什么同一个.cpp中定义两个类里面的Init可以同名?因为类定义的也是一个域叫类域,C/C++中只要{}花括号括起来的都是一个域,并且它们是不同的域中定义的。有了这些以后写起来会舒服一些,并且C++就可以慢慢玩起来了。现在补充一些函数:

struct Stack
{
	void Init(int defaultcapacity = 4)
	{
		a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == a)
		{
			perror("malloc申请空间失败");
			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;
	Stack st2;
	st2.Init();
	st2.Push(1);
	st2.Push(2);
	st2.Push(3);
	st2.Destroy();
	return 0;
}

    就算完全用C的语法也会识别为类。

3.类的定义

    可能C++祖师爷升级了struct后用着感觉不太好,就又弄了一个class。

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

    虽然struct可以定义类,但C++更喜欢用class,class和struct用法一样,class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面分号不能省略。类体中内容称为类的成员:类中的变量称为类的属性或成员变量; 类中的函数称为类的方法或者成员函数。现在体验一下class:

class Stack
{
	void Init(int defaultcapacity = 4)
	{
		a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == a)
		{
			perror("malloc申请空间失败");
			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()
{
	Stack st2;
	st2.Init();
	st2.Push(1);
	st2.Push(2);
	st2.Push(3);
	st2.Destroy();
	return 0;
}

    当编译的时候发现编译不过,报了这样的错误:

    struct可以编过,class不能编过,这里报错说涉及到关于权限的问题,说无法访问private。因为C++对成员给了一些限定,这个限定是什么呢?C++给了三种访问限定符:

    有共有、保护、私有,这三个访问限定符被分为两类:公有分为一类,就是说在类外面可以直接访问;保护和私有暂且归为一类,就是说在类外面不可以直接访问。那访问限定符怎样用呢,下面来看这样的例子:

class Stack
{
public:
	void Init(int defaultcapacity = 4)
	{
		a = (int*)malloc(sizeof(int) * capacity);
		if (nullptr == a)
		{
			perror("malloc申请空间失败");
			return;
		}
		capacity = defaultcapacity;
		top = 0;

	}
	void Push(int x)
	{
		//...
		a[top++] = x;
	}

	void Destroy()
	{
		free(a);
		a = nullptr;
		top = capacity;
	}
private:
	int* a;
	int top;
	int capacity;
};

    比如Stack中的函数期望被别人访问,就在前面加一个共有。成员变量不期望被访问,就加一个私有。访问权限作用域从该访问限定符出现的位置开始直到下一个访问限定符出现时为止,如果后面没有访问限定符,作用域就到 } 即类结束。也就是从public到private都是共有的,private直到结束都是私有的。所以想给别人用的就公有,不想给别人用的就私有,一般定义类数据是私有/保护的,方法是公有的。

    如果是struct可以用访问限定符吗?当然可以,因为只要是类按理来说都是可以用的。用class和struct唯一的区别就是:class不写访问限定符默认是私有的,所以前面报错,struct不写默认是共有的。平时不建议用默认概念,最好显示的指定。

    下面来看这样一些点:首先类中函数的声明和定义是可以分离的。拿前面的栈举例子,如果某个函数太长就可以将其分离:

    单纯分离出来定义函数是不知道是全局函数还是成员函数的,所以要加stack::来指定类域,编译器一看就知道Init不是一个普通的全局函数而是类里面的一个成员函数的定义。

    其次在类中定义的函数编译器默认可能会将其当成内联函数处理,同时想让上图Init变内联是不能直接加inline的,因为内联声明和定义不能分离。也可以直接在类中加inline,但类中定义的函数默认内联,所以C++中长的函数定义和声明建议分离,短的直接在类中定义。就算很长的函数在类中定义也不会成为内联,因为是否内联决定权在编译器。

    类里面定义时喜欢有些地方加"_",下面看这样一个例子:

class Date
{
public:
	void Init(int year)
	{
		// 这里的year到底是成员变量,还是函数形参?
		year = year;
	}
private:
	int year;
};

    定义了一个日期类,这里的year对我们而言就不太好区分,但编译器还是认识的,因为这里局部域和类域都有year,这里先用局部域,再用类域,所以是局部域赋值给类域。平时还是区分开方便看,所以做这样的改进:

    左右改变都可以,只要可以区分就可以。

4.类的封装

    面向对象的三大特性:封装、继承、多态。在类和对象阶段,主要是研究类的封装特性,那什么是封装呢?封装就是将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互,也可以理解为封装就是把数据和方法放在一起,不想对外的变私有,想对外的变共有。

    封装本质上是一种更好的管理,让用户更方便使用类。比如:对于电脑这样一个复杂的设备,提供给用户的就只有开关机键、通过键盘输入,显示器,USB插孔等,让用户和计算机进行交互,完成日常事务。但实际上电脑真正工作的却是CPU、显卡、内存等一些硬件元件。

    对于计算机使用者而言,不用关心内部核心部件,比如主板上线路是如何布局的,CPU内部是如何设计的等,用户只需要知道,怎么开机、怎么通过键盘和鼠标与计算机进行交互即可。因此计 算机厂商在出厂时,在外部套上壳子,将内部实现细节隐藏起来,仅仅对外提供开关机、鼠标以 及键盘插孔等,让用户可以与计算机进行交互即可。

    在C++语言中实现封装,可以通过类将数据以及操作数据的方法进行有机结合,通过访问权限来 隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。

    C语言数据和方法是分离的,C++没有分离,有时候不规则使用C语言,比如一会通过直接访问初始化,一会调用函数初始化,或者随意用top和函数不符越界:

这些都不太好,因此C++中进行了封装,这样直接用方法就行,会更加规范。

5.类的作用域

    类定义了一个新的作用域叫类域,类的所有成员都在类的作用域中。在类体外定义成员时,需要使用 :: 作用域操作符指明成员属于哪个类域。目前位置就已经知道了四个域:局部域、全局域、命名空间域、类域。域的特点是不同域中可以定义同名变量,域会限制访问。编译器的搜索规则一般是先去局部域,如果有类域就先局部域再类域,最后去全局/命名空间域,命名空间域要看有没有展开/指定。局部域和全局域会影响生命周期,类域和命名空间域不影响生命周期。

6.类的实例化

    再来看看这副图,Func.h中成员变量是声明还是定义?对于成员函数来说,有函数名,参数,返回类型就是声明,函数的实现是定义;对于成员变量来说不开空间只是说了类型和名字是什么是声明,开了空间是定义,但变量不是一个一个定义,而是整体定义:Stack st2。所以把用类类型创建对象的过程,称为类的实例化。

    类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个类并没有分配实际的内存空间来存储它;比如:入学时填写的学生信息表,表格就可以看成是一个类,来描述具体学生信息。类就像谜语一样,对谜底来进行描述,谜底就是谜语的一个实例。一个类可以实例化出多个对象,实例化出的对象占用实际的物理空间,存储类成员变量。所以不可以这样:

int main()
{
	Stack::top = 1;
	return 0;
}

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

7.类对象模型

class A
{
public:
void PrintA()
{
   cout<<_a<<endl;
}
private:
char _a;
};

    这个类的大小是多少呢?首先明确C++和C语言一样都是要遵循内存对齐的,不内存对齐访问数据会有效率的损失。那上图计算类的大小时要不要考虑成员函数呢?这里是不要的,为什么呢?

这里实例化了三个对象,它们三个的成员变量都是不一样的,每个人都有自己独立的空间。类的实例化相当于用图纸建造出了很多房子,成员变量像卧室,厨房等这样的空间,每个房子都要有这样独立的空间。成员函数类似于球场这样的空间,每家每户可以都有,但太浪费了,况且用球场是把大家叫来一起用,还不如放在小区大家一起玩,因此不算成员函数。sizeof(类)和sizeof(对象)是一样的,可以类比为一个看着设计图算大小,另一个拿着尺子在房子里面算大小,都是一个意思。

    类对象存储模型有这样几种猜测:

第一种就是函数和数据都在对象里面,每个对象中成员变量是不同的,但是调用同一份函数,如果按照此种方式存储,当一个类创建多个对象时,每个对象中都会保存一份代码,相同代码保存多次,浪费空间。第二种是代码只保存一份,在对象中保存存放代码的地址,指针指向表,这种暂时不管。第三种是只保存成员变量,成员函数存放在公共的代码段,这种符合类对象存储模型:

比如对上图中,aa1._a = 1指的是在对象指向的空间找到a去赋值,aa1._a = PrintfA()指的是不去对象中找函数地址,而是去公共区域去找。

    下面复习一下内存对齐规则:

为什么要内存对齐呢?因为计算机读取数据时按倍数去访问,这和硬件有关系。内存读数据和数据总线有关,读数据一次性读过来,假设32根线就读32个位,4个字节,不能想读几个就读几个。比如一次访问4字节:

假设成员变量公开,现在对两个成员变量都++,对齐访问1次就可以,不对齐ch还好说访问1次,但a就要访问2次。复习了对齐规则后,下面来看看这两个类的大小是多少:

看到结果都是1,为什么不是0呢?先可以这样简单理解一下:假设小区里面其他房都建好了,现在有个房,现阶段没有卧室,厕所等的规划,那也需要留一个地出来,否则怎么知道这里有房子。

如果是0字节,那怎么区分aa1和aa2?取地址又怎么办呢?如果没开空间都没有地址。根据结果显然不是0字节,所以没有成员变量的类对象,需要1字节,是为了占位,表示对象存在,不存储有效对象。所以空类和没有成员函数的类它们大小都是1字节。结论:一个类的大小,实际就是该类中”成员变量”之和,当然要注意内存对齐注意空类的大小,空类比较特殊,编译器给了空类一个字节来唯一标识这个类的对象。

8.this指针

    上图中,定义了两个日期类的对象,初始化后又打印一下。拿打印函数举例,这里调用的是不是同一个打印函数呢?是的,那既然调用的同一个函数为什么打印的结果是不一样的,不都是call同一个地址。再说和对象没关系,打印函数不在对象中,其实这里有个隐藏的我们直接没有看到的东西叫隐含的this指针。虽然不在对象中找这个函数,但是要传参数,这个参数叫隐含的this指针。可以这样理解:

编译器编译后会把这个函数改成左边的形式,this->year也不是凭空访问的,它不是访问类中的,因为类中声明没有开空间,所以访问是实实在在定义的,访问this指向的。调用的地方也会处理为右边的形式,这里不会在对象中找函数,但会把地址传过去。这其实和以前写的数据结构一样,只是这里悄悄的传了。它们调用的是同一个函数,但它们形参不同,d1.print调用时this是指向d1的就访问d1的年月日,d2.print调用时this是指向d2的就访问d2的年月日,Init也是同样的原理。

那能不能自己显示的去写出隐藏的内容?语法规定不能显示去写,不允许在实参和形参位置显示写,但允许在函数里面用。那能不能给this赋值呢?这是不可以的,因为this的类型是Data* const this的,修饰的是this本身,this的指向不能变。

    下面来看这样几个问题:首先可以Data :: _year这样访问吗?不可以,因为是声明,如同图纸中没有房间。那this指针存在哪里呢?肯定不在对象里面,因为前面计算对象大小的时候也没有包含this指针。this指针是个形参,所以this指针是跟普通参数一样存在函数调用的栈帧里面。

通过汇编可以看到,vs下面对this指针传递进行了优化,对象地址是放在ecx,ecx存储this指针的值。再看下一个问题:

这里分别选什么呢?左右两边肯定都不选a,因为就算有问题也是空指针问题,空指针访问是运行问题,不会报编译错误。这里先说结论,左边选c,右边选b。左右两边p调用print,不会发生解引用,因为print的地址不在对象中。编译时就直接call+地址了,p会作为实参传递给this指针,传递空指针不报错,对空指针解引用才报错。对于左边来说,this指针是空的,但是函数内没有对this指针解引用;对于右边来说,this指针是空的,但是函数内访问_a-,本质是this->_a。不要看到有->就一定是解引用,关键看底层是怎么实现的。

最后也就知道为什么不能直接A::print()了,这样this指针不知道传什么,里面的对象不知道访问什么。

9.C和C++实现栈的区别

    C实现的栈没有封装,数据和方法是分离的,可用过函数访问数据,也可通过变量(结构体定义变量)访问数据。C++数据和方法是封装在一起的,想给别人访问的定义成公,不想给别人访问的定义成私有,不用考虑top指向哪里,只有一个选择就是调用函数,其次调用简洁,但从底层看两者差不多。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值