目录
一、类与对象的初步认知
1.面向过程和面向对象的初步认识
(1)面向过程
(2)面向对象
二、类的引入
1.C++中的struct
(1)C++中的struct与C中struct的不同
(2)C++中的struct特性
2.C++中的class
(1)类的定义
(2)类的访问限定符及封装
(3)类的两种定义方式
三、封装
1.面向对象的三大特性
2.封装的含义
(1)含义
(2)封装的本质
(3)C与C++的封装
三、类对象模型
1.类对象的大小
2.类对象的存储方式
(1)对象 中包含类的各个成员
(2)代码只保存一份,在对象中保存存放代码的地址
(3)只保存成员变量,成员函数存放在公共的代码段
3.特殊的类的大小
(1)类中只有成员函数,没有成员变量
(2)空类
四、类的实例化
(1)类的含义
五、this指针
1.this指针的含义
2.this指针的作用
3.this指针的特性
4.this指针的存储位置
5.this指针为空
在学习c语言时,想必很多人都听到过面向过程和面向对象这两个词。但是受限于当时对语言的认知,一般不会对其进行了解。但是到了C++学习类与对象时,我们就要对“面向过程”和“面向对象”这两个词有一个初步的认知。
我们常说c语言是面向过程的语言,那么面向过程是什么意思呢?简单来讲,就是指c语言在使用中关注的是过程,即分析出求解问题的步骤,通过函数调用逐步解决问题
以洗衣服为例子,如果用c语言解决洗衣服这一问题,那么我们就需要经过如下几个步骤:
这每一个步骤都可以看做是一次函数定义和函数调用。可以看到,用c语言来写就会非常的复杂,每一个步骤都需要自己去完成。
而C++是面向对象的,关注的是对象。这里的对象指的就是C++会将一个问题分解为不同的部分,每个部分交给不同的对象,靠对象之间的交互完成。
同样以洗衣服为例,在C++中,洗衣服就可以看成如下图所示:
C++要解决洗衣服问题,就会将其划分出四个对象,分别是“人、衣服、洗衣服和洗衣机”,将整个问题交给这四个对象来完成。
C语言结构体中只能定义变量,在C++中,结构体内不仅可以定义变量,也可以定义函数。比如:如果我们用C语言方式实现的栈,结构体中只能定义变量。但是在C++中,结构体struct中也可以定义函数。
struct Stack
{
void Init()
{
data = nullptr;
top = 0;
capacity = 0;
cout << "Init" << endl;
}
void Push()
{
cout << "Push" << endl;
}
int* data;
int top;
int capacity;
};
在上面的程序中我们可以看到,在C++中的struct既可以定义变量,也可以定义函数。
struct Stack
{
void Init()
{
data = nullptr;
top = 0;
capacity = 0;
cout << "Init" << endl;
}
void Push()
{
cout << "Push" << endl;
}
int* data;
int top;
int capacity;
};
1.同一个struct中的变量可在内部随意使用
同样以上面的程序为例,我们可以看到,在struct中定义的函数Init()中我们使用了在struct中定义的data、top等变量。这是为了能够更加方便的在结构体内定义函数
2.同一个结构体中的变量的位置没有固定要求
从上面的程序我们可以看到,定义在结构体中的变量并没有像以前那样放在函数的最上面,而是放在了最下面。但是这个结构体依然可以正常使用。原因是struct中的函数在调用变量时会先从其函数内部找,找不到时再在整个结构体中去寻找,而不是像以前那样只从函数的上面查找。这样就避免了struct中的变量一定要放在函数上面的问题。
class为定义类的关键字,ClassName为类的名字,{}中为类的主体,注意类定义结束时后面的分号不能省略。
类体中的内容为类的成员,类中的变量称为类的属性或成员变量;类中的函数称为类的方法或者成员函数
基本形式如图所示:
1.访问限定符
访问限定符,顾名思义就是用来限制访问的符号。而类的访问限定符就是用来限定对类的访问。这里的访问限定符就涉及到了C++的封装。
在C++中,实现封装的方式是用类将对象的属性和方法结合在一块,让对象更加完善,通过访问权限选择性的将其接口提供给外部的用户使用
类中提供了三种访问方式:public、protected、private。即如图所示
2.访问限定符说明
(1)public
public的英文释义为公有,在C++中,由public修饰的成员在类外可以直接被访问
(2)protected和private
protected和private的英文释义分别有保护和私有的含义。private所修饰的成员在类外不能直接被访问。而protected与private的作用是类似的,但目前阶段我们不能完全理解protected的作用,可以暂时理解为和private的作用相同
从上图中可以看到,当我们在类外调用由private修饰的类成员时,程序直接报错了,这也说明了这些由private修饰的成员是不能在类外被直接访问的
(3)访问权限作用域
在类中,一个访问权限的作用域是从该访问限定符出现的位置开始直到下一个访问限定符出现为止。如果后面没有访问限定符,作用域就到},即类结束
从上面的两张图中我们可以看到,第一幅中只有一个public修饰,因此public的作用域就是直到这个类的结尾;而第二幅中存在public和private两个访问权限,public的作用域是到private,即只包含Init()函数。而private的作用域则是从data直到类的结尾
(4)class的默认访问权限是private,struct为public
在C++中,如果class里面没有加上访问限定符,那么出于保护数据的目的,会默认class中的数据都是被private所修饰的;
而C++中的struct则相反,在struct的内部没有访问限定符时,其内的数据都被视为由public修饰。原因是在C中没有类这一概念,只有结构体struct,而结构体中的数据都是可以被随意访问的。C++为了兼容C的这一特性将struct的默认权限视为public
在C++中,大部分时候使用的都是class,struct使用的次数比较少
注意:访问限定符只在编译时有用,当数据映射到内存后,没有任何访问限定符上的区别
1.声明和定义全部放在类体中
需要注意的是,成员函数如果在类中定义,编译器可能会将其当成内联函数处理
其形式如下所示:
class Stack
{
public:
void Init()
{
data = nullptr;
top = 0;
capacity = 0;
cout << "Init" << endl;
}
void Push()
{
cout << "Push" << endl;
}
int* data;
int top;
int capacity;
};
2.类声明放在.h头文件中,成员函数定义放在.cpp文件中
在这里又要涉及另一个概念,“类的作用域”
类定义了一个新的作用域,类的所有成员都在类的作用域中,在类体外定义成员时,需要使用::作用域操作符指明成员使用哪个类域
函数定义时需要在成员函数前加“类名::”
在这里要注意两点:
(1)类的声明和定义分离与结构体相似,都要写在头文件里面
(2).C++文件里面的声明必须要在函数名前面,返回值后面写上类名。这种做法一方面是为了说明该函数属于哪个类,另一方面也是为了明确函数内部调用变量时应去哪个类域里面查找
这里可以看到,定义了Stack和Queue两个类,每个类里面的函数名相同。如果在声明时不明确类域,就会导致命名冲突,出现错误。注意,这两个类中的函数名虽然相同,但不构成重载函数,因为不在同一个域里面
(1)封装 (2)继承 (3)多态
在这里,我们主要要先了解封装,继承和多态暂时不用过多了解。
但要注意的是,面向对象虽然说是三大特性,但其特性不止三个。如我国有“五大名山”,但这并不是指我国境内只有五个名山,而是最出名的山有五个,其他名山相对而言知名度没有那么高。这里的“三大特性”也是如此,面向对象存在多个特性,而这三个特性则是最为重要的。
封装是指将数据和操作数据的方法进行有机结合,隐藏对象的属性和实现细节,仅对外公开接口来和对象进行交互。
封装在本质上是一种管理,让用户更方便的使用类。封装就如我们在日常生活中所使用的计算机,一台计算机会提供开关键,键盘输入、鼠标等硬件设施来为用户提供操作。用户只需对这些硬件进行的简单的交互就能使用计算机,而无需深入接触并操作计算机内部的CPU,显卡、晶体管等计算机构成元件。
1.C
在C语言中,是并不存在“封装”这一概念的,但是我们可以通过一些方法实现封装。这里我们要着重讲的是C++的封装,就不在此赘述。有兴趣的话可以自行查阅相关资料。
2.C++
在前面我们也讲过C++中是存在“类”这一概念的。C++的封装正是基于类来实现的。
在C++中实现封装,可以通过类将数据及操作数据的方法进行有机结合,通过访问权限来隐藏对象内部实现细节,控制哪些方法可以在类外部直接被使用。因为类中存在有public,protected和private这三个概念。通过这三个限定符,可以控制类中的数据是否能被直接访问,应该如何被访问。这就对类中的数据进行了保护和访问限制。
class Stack
{
public:
void Init();
void Push();
private:
int* data;
int top;
int capacity;
};
例如在以上的类中,其中的data,top和capacity是无法从外部直接访问的,必须通过Stack中的函数接口进行访问。
类与结构体一样,都是可以计算所占空间大小的。
在结构体中只存在变量由固定大小的数据,因此可以直接进行计算。但是在类中一般都存在函数和变量,那么在这种情况下我们要如何计算类的大小呢?
从上图中我们可以看到,Stack类的大小为24,而这个大小正好是这个类中存储的变量经过“内存对齐”后算出来的空间大小。也就是说,在类的空间计算中是不会计算函数大小的
在上图中可以看到,在计算Stack的大小时,无论是直接用Stack计算大小还是创建一个对象后再计算对象的大小,都是可行的。就好比我们有一个房子的工程图,虽然我们没有实际修建出来,但通过图纸我们可以了解到它总共需要多少空间,这里能直接使用Stack进行计算也是一个道理
如上图所示,在计算类时,存储变量和函数的地址。
不同对象的变量都是不同的,但是其函数都是相同的。这就会导致我们每定义一个对象,都要存储一遍函数的地址,再通过这个函数地址找到这个函数计算它的大小,这无疑会造成空间浪费。就好比一个小区里面有一个健身房,物业给这个健身房上了个很多个锁,每个住户对应的锁的钥匙,住户只有拿着这些钥匙把所有的锁打开才能使用健身房。
这种存储方式不仅麻烦,而且会造成空间浪费,无疑并不适合类的空间计算
这种方式是将类里面的函数地址单独存一份,每次创建对象时就去拿这份代码,找到里面的函数的地址并计算大小。这种方式在第一种方式的基础上进行了优化,不再需要频繁的存储类中的函数地址,只用存储一份即可。但这种方式依然不太好。就好比一个小区里面有一个健身房,物业给这个健身房上了一把锁,每个住户都有这把锁的钥匙,要去的时候就用这个钥匙把锁打开即可
这种方法是指保存成员变量,在创建对象时只计算成员变量的大小,无需再计算函数的大小。对象需要调用函数时直接去公共的代码段找就行了。
这种方式就在第二种方式的基础上进一步的优化了。同样以健身房举例,这种方法就好比在一个小区里面有一个健身房,这个健身房是公共场所,没有上锁,小区里面的住户要去健身房的话直接去就行了,无需开锁。
由此,类的大小最终被决定为只计算其中的成员变量大小,当然,在计算时依然需要遵循“内存对齐”规则。
在前面我们的类中都是既存在成员函数,也存在成员变量。同时我们也了解到了类的大小只计算成员变量的大小。那么有没有可能会出现两种特殊的情况:类中只有成员函数,没有成员变量;或者类就是一个空类,里面什么都没有。在这两种情况下,类的大小又是多少呢?
从上面我们可以看到,无论是类中只有成员函数还是类是空类,其所占内存都是1字节。这时大家可能就会感到奇怪,类中明明只计算成员变量的大小,那么在只有成员函数或空类时不应该是0么?
原因是为了“占位”。因为这个类要在存储空间中被标识出来以示存在,这里所占的1字节就是为了标识对象存在,并不存储有效数据
用类类型创建对象的过程,称为类的实例化
在使用类时,我们可能会出现如下情况:
可以看到,在这里,Stack中的成员data和top是由public修饰的,按道理来说我们是可以在类外进行直接访问和使用的。但是在这里,我们可以清楚的看到在使用时却报错了。其原因就在于“类的实例化”
1.类是对对象进行描述的,是一个模型一样的东西,限定了类有哪些成员,定义出一个并没有分配实际的内存空间的类来存储它。
也就是说,类在创建好时,其本身是没有开辟内存空间的。这一点就与结构体不同。结构体在创建好后会自动开辟内存空间进行存储,类则不然。在类中的成员都属于“声明状态”。
我们可以将类看做是一张修房子的图纸,而类中的成员则是图纸中房子的房间、基础设施等。房子的设计图是不会导致占用土地的,只有修好的房子才会占用土地。就如下图所示:
结构体则不然,结构体可以看做是一个已经修好的房子,会占用内存空间。
那到现在可能我们就会有一个疑问:虽然在类中的变量是声明,但是函数并不一定是声明,在类中我们同样是可以定义函数,那是不是说在类中定义的函数就可以被直接使用呢?就如下所示:
可以看到,就算我们在类中直接定义了函数,且处于public状态,但我们在类外依然无法直接调用函数。这就涉及到了另一个知识点:this指针
现在假设我们有如下一个日期类:
我们现在再创建两个的d1,d2两个对象并对其进行打印
从上图中我们可以看到,这里根据对象的初始化值不同,打印出了不同的值。但我们在上面也说过了,不同的类进行函数调用时都是用的同一个函数。那这里就有一个问题,这个函数并无法对不同的对象进行区分,那么它为什么能准确的打印出对应的类的值呢?这就是this指针在起作用
C++编译器中给每个“非静态的成员函数”增加了一个隐藏的指针参数,让该指针指向当前对象(函数运行时调用该函数的对象),在函数体中所有“成员变量”的操作,都是通过该指针去访问。只不过所有的操作的对用户是透明的,即用户不需要手动传递,编译器自动完成
即在编译器中,类中的函数都是以如下形式存在的:
可以看到在上图中,定义了this指针的地方都有报错,这就涉及了this指针的一个特性——this指针不能“显性定义” ,即我们在函数定义中可以直接将this如上图那样写好,但是this指针不能够直接在类的成员函数的参数中写出
在前面我们就提出了一个疑问:为什么类中的成员函数在无法区分不同对象的情况下能够准确的打印出对应的类的值。
this指针的作用就是用于区分不同的对象。以上面的日期类为例,在函数调用时就会是如下形式:
在函数调用时,这个this指针就会被传入对应的对象的地址,根据这个地址来对不同的对象进行区分。
当然,在平时写代码时就不要将this指针显性的写出来,就算我们不写,编译器也会自动帮我们加上,没有写的必要。非要写上去不仅会显得代码冗杂,也会增加非必要的代码编写量。
从上图中,我们将this的地址和d1,d2的地址分别打印出来,可以看到this的地址就是对应的对象的地址,这也证明的this指针的存在和作用。
(1)this指针的类型:类类型*const,即成员函数中,不能给this指针赋值
即在函数调用中,this指针之前会加上const限定符,上面的图中作者没有加是为了避免理解const时被干扰。实际上是如下图所示:
(2)只能在“成员函数”的内部使用
(3)this指针本质上是“成员函数”的形参。当对象调用成员函数时,将对象地址作为实参传递给this形参。所以对象中不存储this指针
(4)this指针是“成员函数”第一个隐含的指针形参。一般情况由编译器通过ecx寄存器自动传递,不需要用户传递(一般是存在栈帧中,vs中是通过ecx寄存器传递。因为寄存器传递几个字节的小数据速度较快)
(5)this指针不能在类的成员函数的参数中“显性定义”
这里的存储位置是指内存空间中的栈、堆、静态区和常量区等
有些人可能认为this指针存在对象里面。但是在前面我们也算过对象的大小,里面并没有加上this指针的大小,因此this指针并没有存在对象中
也有些人认为this指针存在常量区。因为类中的函数就是存在常量区的,这里就是将this指针与成员函数相混淆了,实际上this指针并没有存储在常量区中
实际上this指针存在栈帧中。因为this指针本质上是“成员函数”的形参,只要是形参,那么就是存储在栈帧中,this指针也不例外,是存储在栈帧中的
在类中是否可以传一个空指针给this呢?在回答这个问题前,我们可以先看看如下两个题:
(1)
在这个题里面,我们创建了一个A类指针p指向nullptr。然后再p->Print(),解引用调用了函数Print()。因此,许多人看到这里的空指针解引用就会直接选B,但这道题实际上应该选C
原因在于p->Print(),很多人认为这里发生了解引用,但是因为Print()函数并没有存在对象中,而是存在常量区中,这里不会发生解引用,会直接调用Print()函数。而Print()函数中并没有错误,因此可以正常运行
(2)
这个题与第一个题目相差无几,唯一的差异就在于PrintA()函数中打印的是“_a”。而正是这个打印的值的差异,导致了这个题的答案是B
可以看到,这个程序直接崩溃了。
原因在于,虽然函数调用时正常,但是在函数使用的过程中会隐性存在this指针,这个题目里面的cout << _a << endl;会被看做cout << this->_a << endl;而我们类指针p是指向空的,这就会导致空指针的解引用,进而导致程序崩溃
因此,可以传空给this指针,但最好不要这样做,因为很容易出现空指针的解引用问题