导图社区 C类的封装思维导图
C 类的封装方法:类的定义,类的构造函数,成员函数,重载构造函数,析构函数,静态成员等详细案例解析和理论罗列。
编辑于2021-08-19 18:24:15C++类的封装
定义类
在C++编程中用类来表示对数据的封装和抽象
封装
对研究对象采用的一种信息频闭技术,从而使接口和实现分离
抽象
对象的最简化接口
抽象就是一个明确的接口
类
类不但提供了对数据的封装
还提供了对数据的处理函数
类就是一个包含有若干数据和函数的集合
此数据和函数称为类的成员
类是对现实事务的抽象,要声明一个类,使用关键字class
class是关键字,表示定义的是一个类
类名字表示该类的名字,这个名字需要符合C++中的命名规则;
花括号“{”和“}”是该类的定义域,所有类的内部成员(包括成员数据和成员函数)都要在其中定义,类一旦定义完成之后,没有任何方法可以增加类成员;
定义结束处的分号是必需的,这点和函数的定义不同。
在 C++中也可以用 struct 来定义类,这两种定义方式的默认访问限制是不同的。
类的数据成员
类是对事物的封装,因此类要封装必要的描述事物的数据。这些数据被包含在类中,称为类的数据成员或者成员变量,类的数据成员的定义和普通的变量定义一样。
类的数据成员,可以是任何的数据类型。
所以可以定义变量的数据类型都可以定义类的数据成员。
类的数据成员的定义顺序并没有特别的要求,这些数据成员可以以任何顺序出现,因此交换上面的数据成员定义的顺序对该类的使用并没有什么影响。
数据成员的定义顺序不同,可能会影响该类的 size,这是因为 C++的编译器中有一个对齐的概念,这里不考虑对齐。
但是在定义类的时候,是不可以给类的数据成员赋初始值的。这是因为类的定义仅仅是定义一个自定义类型,并没有声明对应的对象。
切记不可以在定义类的时候给数据成员赋初始值。要想给类的数据成员赋初始值,只能在构造函数中或者构造函数的初始化表中进行。
类的成员函数
类不但封装了数据,还抽象出了对数据的操作,通过这些操作,可以方便地修改类的数据成员。
这些操作可以完成类的数据成员之间的协调,可以保证数据的一致性。这些抽象出来的操作就是函数,还可以把函数封装到类中,成为类的成员函数,这些函数主要处理该类的数据成员,成为该类的专用函数。
以定义一个绩效考评的函数performance,该函数可以根据该雇员的表现修改grade以及salary。因为performance函数主要用于改变Employee类的grade和salary,所以performance函数可以作为该类的成员函数。
在上面的定义中,函数在前面,变量在后面,这是可以的。而在定义全局变量和全局函数的时候,这样的情况是不允许的。
成员函数定义方法
成员函数可以定义在类定义内部,在类定义中定义的成员函数把函数声明为内联的,即便没有使用 inline 标识符。
或者单独使用范围解析运算符 :: 来定义。
在 :: 运算符之前必须使用类名。
: 符号称为作用域解析运算符。它可以用来指示这些是类成员函数,并且告诉编译器它们属于哪个类。
类名和作用域解析运算符是函数名的扩展名。当一个函数定义在类声明之外时,这些必须存在,并且必须紧靠在函数头中的函数名之前。
规则
类的成员函数必须在类的声明中定义。
这个是不成立的
在类的成员函数中,可以直接使用类的成员变量。
类的成员变量和成员函数的定义,没有严格的顺序要求,可以以任何的顺序出现。可以先定义成员变量,再定义成员函数;也可以先定义成员函数,再定义成员变量;也可以变量和函数交替定义。
类的组织结构
类的长度只跟数据成员有关,而和成员函数无关。
类中的数据成员,按照定义的先后顺序排列,类的大小完全由数据成员决定。
示例
类对象
类里封装了成员变量和成员函数,这些成员都依附于类,在类外不可以直接操作成员变量和成员函数,必须通过类来调用。
但是类是一个类型,不占用存储空间,不可以直接对类进行操作,就像不可以直接给int赋值一样
因此要使用类,就要先定义一个类对象,定义类对象和定义普通的变量一样
也可以定义一个类的指针
类指针指向一个新生成的对象
类是个类型,因此类只需定义一次,一个类可以定义很多对象。类本身不占用内存,只有定义了类对象之后,对象才占用内存空间
访问类对象成员
一旦定义了一个实际的Employee类的对象,就可以使用点运算符“.”来访问类对象的成员。
通过“*”操作符可以得到指针指向的对象,因此也可以像上面那样操作类指针的成员。
在通过类指针调用类成员之前,同样要保证指针指向正确的地址。
对于上面对类指针的调用,在C++中通常用另外一个组合操作符“->”来代替,这个操作符可以直接操作类指针的成员
上面两种调用类指针成员的方法是等效的,不过通常用后面的方法。
综合示例
定义了职员类 Employee 的对象,给这个类对象的数据成员赋值并调用成员函数,然后使用指针来指向这个对象
通过指针来访问数据成员。
隐含的this指针
在类的成员函数内部,暗含着一个名字是this的指针,这个指针指向了调用该函数的类对象。
emp.performance()
那么在函数performance的内部,这个this指针指向的就是emp对象。
这个指针由编译器去维护,不需要显式地定义,可以直接使用,并且不需要担心这个指针的指向是否正确。
this指针的用途
那么,在定义函数的时候,就必须有一个返回值,返回一个该类对象的引用,因此上面的类定义应当修改为
这些函数的返回类型是Ball&,指明该成员函数返回对其自身类类型的对象的引用,每个函数都返回调用自己的那个对象。在这里,要想返回自身对象,只能使用 this 指针
使用this指针是比较便捷的得到当前对象的方法,在类的操作符重载中还有应用。
类成员的访问限制
类的特性是封装和抽象,也就是只向外部提供有限的功能接口,其他部分只有内部可见,外部不能访问。在定义类的时候,可以在成员的前面加上访问限制,来限制该成员是否可以被外部访问。
关键字
public
公有的,这个限制后面所定义的成员(包括数据和函数),可以被外部访问。一般抽象出来供外部使用的接口,都要定义为public限制的。
private
私有的,表示定义的成员只供本类内部使用,外部不可以使用。一般情况下受保护的数据成员和内部函数要定义为private限制的。
protected
受保护的,其特性和private一致,外部不可用,只能内部用。不过这个关键字主要用在类继承的时候基类的定义中,本节不做过多的讨论。
友元不受public,private和protected的限制。
语法
访问限制的使用格式是关键字加冒号“:”
当定义类的时候,其成员的访问限制默认都是private,也就是说如果不加任何访问限制,那么所有的成员都是private限制的。
另外不需要在每一个成员前面都加上访问限制,如果一个成员前面没有访问限制,则其访问限制和前面定义的成员相同。
私有与安全性
在前面设计的Employee类中,所有的数据都设计为public,这是非常不可取的。对于grade和salary,是不能随便修改的
只能通过performance的评定来改动
因此有必要修改为private限制。作为设计的一般准则,应当保持类的数据成员为private,限制外部随便访问。
限制外部随便访问。外部可以随便修改类的数据成员是非常不安全的,那就像一个外壳打开了的电视机,可以随便去更换元器件,这样谁也不能保证电视机能正常工作。
提供一个公有的函数来获取或者 设置私有成员变量
可以很容易地修改实现细节,把细节封装在函数中,对于外部的接口不变,修改了内部细节造成的影响很小。
可以统一对数据的有效性、完整性进行检查,而不用担心有什么遗漏。
可以提高对数据的保护,防止外部随便修改数据。
示例修改
综合示例
类成员函数 getArea 仅使用成员变量 radius 但不修改该成员变量。像这样的函数,它使用一个类变量的值,但不改变它,则称为访问器。
函数 setRadius 修改了 radius 变量的内容,像这样的成员函数,它将一个值存储在一个成员变量中或者改变它的值,则称为设置器。
友元
一般情况下应当把数据成员限制设置为private以限制外部的访问。不过这样也有局限性,必须要为所有外部可能用到的变量添加存取函数
因此可以适当地让某些外部函数可以访问私有变量。
在某些情况下,允许特定的非成员函数访问一个类的私有成员,同时仍然阻止一般的访问,这就是友元(friend)。友元机制允许一个类将其非公有成员的访问权限授予指定的函数或类。友元的声明以关键字friend开始,它只能出现在类定义的内部。友元声明可以出现在类中的任何地方,并且友元不受其声明出现部分的访问控制的影响。
通常情况下把友元的声明放在类的开始部分或者结尾部分,这样比较直观。
要声明友元,使用friend关键字。比如Employee_Mgr类专门用来管理Employee类,要想让Employee_Mgr的对象可以直接访问Employee的私有数据,可以为Employee的定义添加友元声明
类Employee_Mgr必须在上面的声明之前已经定义。
添加了上面的友元声明之后,Employee_Mgr类的所有函数都可以访问类Employee对象的所有成员变量和成员函数,无论这些成员是私有的还是公有的。
如果不希望望Employee_Mgr类的所有函数都具有这个特权,可以只为某几个函数设置特权。
假如只希望Employee_Mgr类的feedback函数有这样的权限,那么可以在 friend 后面跟上该函数的声明。也可以设置全局函数为友元,这时候这个全局函数就具备访问私有成员的权限。
示例
定义了一个简单的类A,A有一个私有的成员id,Manager类和运算符<<通过友元声明来访问A的私有成员。
类的构造函数
构造函数的定义
对于类对象,比如前面定义的类Employee,如果想要在定义类对象的时候赋初值
Employee emp = {"Jason, 8, 7000"}
其实这是不可以的,类对象不可以直接这么赋初值。要想让类对象在生成的时候有一个初值,就要使用构造函数。
构造函数
构造函数是特殊的成员函数,与其他成员函数不同,构造函数和类同名,而且没有返回类型。与其他成员函数相同的是,构造函数也有形参表和函数体。一个类可以有多个构造函数,每个构造函数必须有与其他构造函数不同数目或类型的形参。
构造函数也受访问限制的约束。
在上面的定义中增加了构造函数,本构造函数接收3个参数。有了这个定义之后,就可以在定义类对象的时候赋初值了
Employee emp = {"Jason, 8, 7000"}
在创建对象的时候,编译器会自动调用构造函数,不需要用户来调用。
构造函数的重载
可以为一个类声明的构造函数的数量没有限制,因此可以根据需要声明多个构造函数,不同的构造函数允许用户指定不同的方法来初始化类。不过同函数的重载一样,这些构造函数的形参必须不同。
当一个类定义了多个构造函数的时候,编译器会根据提供的实参的类型和数目来选择构造函数。
默认构造函数
如果没有为一个类定义任何构造函数,那么在初始化的时候会调用默认构造函数。
默认构造函数是由编译器生成的,为所有的数据成员提供初始化。默认构造函数没有任何参数。
在最初定义的Employee类中没有构造函数,这个时候使用的就是由编译器提供的默认构造函数。
编译器会为该类添加默认构造函数,这里就调用默认构造函数。
因为该类定义了构造函数,所以编译器不再为其添加默认构造函数,那么这个类就没有无参数的构造函数。因此构造这个类的对象的时候必须提供参数
如果为类提供了构造函数,同时也应该为其提供一个无参数的构造函数。
复制构造函数
前面介绍了类的构造函数,可以方便地为类指定初始值,下面考虑用一个现有的类对象来创建新的对象的过程。
编译器会用位复制来执行上面的过程,新生成的emp2所有的数据都和emp1一样。对于前面定义的Employee类,这个过程是可以接受的。但是对于一些包含了指针数据成员的类,位复制的时候只会复制这个指针,两个指针指向同样的地址,后面的对象依赖于前面的对象,因此仅有位复制是不行的。
定义
在C++中,可以用自己定义的函数来替代位复制,这个函数就是复制构造函数。
复制构造函数是一个特殊的构造函数,该构造函数接收一个本类类型的引用作为参数,参数数目只能是一个。
通过使用复制构造函数,可以用一个现有的类对象来创建一个新的类对象。
复制构造函数的参数必须使用引用。
有了上面的定义之后,用 emp1 来创建 emp2 的时候,就会调用这个复制构造函数,并且把emp1作为参数传入。
复制构造函数中要实现对象的复制,因此一般情况下要在复制构造函数中复制成员变量。下面是Employee类的复制构造函数的实现
同构造函数一样,如果一个类没有定义复制构造函数,编译器会自动添加一个复制构造函数。不过一个类只能有一个复制构造函数。前面介绍了复制构造函数的定义,下面介绍在什么情况下编译器会调用复制构造函数。简单地说就是在由类对象创建新的类对象的时候
在定义类对象的时候直接用其他的类对象初始化,比如前面的 Employee emp2 = emp1。
调用函数的时候。如果函数的参数为类类型,且没有定义为引用,这时函数是值传递的,编译器会自动为传入的参数创建一个复制,这个时候会调用复制构造函数。
在函数返回的时候。如果函数的返回类型为类类型,且没有定义为引用。那么在函数返回的时候编译器也会用一个复制返回,这个时候会调用复制构造函数
构造函数初始化列表
与任何其他函数一样,构造函数具有名字、形参表和函数体。与其他函数不同的是,构造函数也可以包含一个构造函数初始化列表,构造函数初始化列表紧跟在构造函数的后面,以一个冒号开始,接着是一个以逗号分隔的数据成员列表,每个数据成员后面跟一个放在圆括号中的初始化式。
为Employee的构造函数提供初始化列表
使用初始化列表的效率更高。
不是所有的数据成员都必须出现在初始化列表中。
初始化列表中每个成员只能出现一次,不可以重复初始化。
数据成员在初始化列表中的出现顺序与类中定义的顺序无关。
示例
为Point类定义了一个友元输出函数,所以程序可以像输出普通变量那样输出类的数据
根据提供参数的不同,编译器会选择不同的构造函数。在fun的调用过程中,会调用两次复制构造函数,一次是在参数传递过程中,一次是在函数返回时。
类的析构函数
在创建对象的时候会调用构造函数,相应地,在释放对象的时候会调用析构函数。
析构函数也是类中一个特殊的成员函数,同构造函数一样,析构函数也是由编译器自动调用的。
析构函数的定义
析构函数与构造函数相反,当对象脱离其作用域时(例如对象所在的函数已调用完毕),系统自动执行析构函数。析构函数往往用来做“清理善后”的工作,例如在建立对象时用new开辟了一片内存空间,应在退出程序前在析构函数中用delete释放。
析构函数的定义跟构造函数类似,使用波浪线“~”加类名作为函数名,没有返回类型,没有参数。
一般情况下,构造函数和析构函数都要定义为public限制的。
一个类可以定义多个构造函数,但是只能定义一个析构函数。这是因为构造函数可以定义参数,可以通过初始化形参列表的不同来区分,而析构函数不需要提供参数,并且是在释放时由编译器自动调用的,因此析构函数只能定义一个。
默认析构函数
如果没有为类定义析构函数,那么编译器也会自动添加一个析构函数,不过这个由编译器添加的析构函数不进行任何操作。
因此,如果有一些需要自己去释放的东西,比如指针或者打开的文件之类的,就一定需要定义析构函数;
如果没有这些需要自己释放的东西,可以不定义析构函数,由编译器来提供。下面为前面示例中的类添加一个析构函数
本例中共创建了 6 个对象,所以会调用 6 次析构函数。前两次析构函数是在函数返回时被调用的,
后面的4次析构函数是在main函数返回时被调用的。
类的static成员
类可以定义静态成员,达到所有对象公用的目的,但是又不破坏封装。
通常,非static数据成员存在于类类型的每个对象中。不像普通的数据成员,static数据成员独立于该类的任意对象而存在,每个static数据成员是与类关联的对象,并不与该类的对象关联。
正如类可以定义共享的static数据成员一样,类也可以定义static成员函数。static成员函数没有this形参,可以直接访问所属类的static成员,但不能直接使用非static成员。
好处
有利于类的封装,可以把static成员定义为私有成员,防止外部访问。
static成员是与特定的类相关联的,在外部使用时必须要用类名字作为前缀,可使程序更加清晰。
static成员的名字在类的作用域中,可以避免命名冲突。
定义static成员
在成员声明前加上关键字static可将成员定义为static成员。static成员遵循正常的公有、私有访问限制。
每一个要定义为static的成员前面都需要有static关键字,这点和访问限制不同。
示例
对于前面定义的Point类,加一个静态的数据和静态的函数
在上面的定义中,先定义了一个static函数。static函数的定义和普通的类成员函数的定义一样,可以直接在定义的后面实现,也可以在类的外面提供函数实现。当在类外面实现的时候,不需要static关键字。
在静态函数中,不可以使用 this 指针,因为静态函数是所有对象共用的。同理,在静态函数中也不可以使用非静态的成员变量,只可以使用类的静态变量。
后面是一个static数据成员,跟普通的数据成员不同,static数据成员必须在类定义体的外部定义,定义的时候必须要有类名作为前缀。
使用static成员
可以使用域操作符::从类直接调用static成员,或者通过对象、引用或指向类类型对象的指针间接调用。
在任何地方都可以用上面的方法通过域操作符直接访问static成员。不过这个访问也受访问限制的约束。外部不可以访问private的static成员。也可以通过类对象对静态成员进行访问
当在类的内部使用静态成员时,可以直接使用,不需要域操作符。
可以用使用非static成员的方法来使用static成员。
总结
可以把类的数据成员和成员函数定义为private的,使外部不可以访问;
对于类所抽象出的接口,应该定义为public的,以便外部访问。
通过友元可以让外部拥有访问private成员的能力。
类可以定义构造函数,这是特殊的成员函数,控制如何初始化类的对象。
可以重载构造函数,为类定义多个不同的构造函数。
复制构造函数是用原有对象生成新对象时调用的。
在释放类的时候会调用析构函数,一个类只能定义一个析构函数。
当没有定义构造函数、析构函数的时候,编译器会自动添加一个。
还可以在类中定义static成员,供该类所生成的对象共用。