序言:要想开发一款成功的应用程序,其开发者必须充分了解并实现用户的需求。作为一个设计良好的类,既要有直观且易于使用的接口,也必须具备高效的实现过程。
一、类与对象基本概念
面向对象程序设计的主要特点为抽象、封装、继承与多态,而类是面向对象程序设计方法的核心概念。类的基本思想是问题抽象和封装,问题抽象是一种依赖于接口和实现分离的编程(以及设计)技术,封装则实现类的接口和实现分离且隐藏实现细节[1-2]。
问题抽象,包括数据抽象和行为抽象。数据抽象是为了描述某类对象的属性或状态,行为抽象是为了描述某类对象的共同行为或功能特征。
封装,将抽象得到的数据和行为(或功能)相结合,形成一个有机的整体。换言之,将数据与操作数据的函数代码进行整合并形成“类”,其中的数据和函数都是类的成员。
注意事项与编码规范:
1. 类本身就是一个作用域,类的成员函数的定义嵌套在类的作用域之内;
2. 定义在类内部的函数是隐式的inline函数(内联函数较于普通函数可以减少调用的开销、提高执行效率,但会增加编译后代码的长度);
3. 一般来说,如果非成员函数是类接口的组成部分,则这些函数的声明应该与类在同一个头文件内。
二、类的基本构成
(一)构造函数
在定义对象的时候进行的数据成员设置,称为对象的初始化。构造函数是类的一个成员函数,其特点有:函数名与类名相同,没有返回值,公有访问,其作用就是在对象被创建时利用特定的值构造对象,将对象初始化为一个特定的状态。一个类可以包含多个构造函数(函数重载),不同的构造函数之间必须在参数数量或参数类型上有所差异。
如果类中没有写构造函数,编译器会自动生成一个隐含的默认构造函数,该构造函数的参数列表和函数体皆为空(在建立对象时自动调用构造函数是C++程序“例行公事”的必然行为)。对于默认构造函数而言,如果存在类内的初始值即用此初始化成员,否则默认初始化该成员(赋空值)。
形如 Sales_data(const std::string &s):bookNo(s){}; 的对象初始化方式,":"以及":"与”{}“之间的代码称为构造函数初始值列表。构造函数初始值列表的作用是为新创建的对象的一个或几个数据成员赋初值。构造函数初始值是成员名字的一个列表,每个名字后面紧跟括号括起来的(或者在花括号内的)成员初始值。不同成员的初始化通过逗号分隔开来[2-p238]。
注意事项:
1. 在C++11新标准中,如果需要显式默认构造函数,在类内可以通过 类名()=default; 方式实现;
2. 编译器只有在发现类不包含任何构造函数的情况下才会自动生成一个默认的构造函数。一旦定义了一些其他的构造函数,除非再次定义一个默认的构造函数,否则类将没有默认构造函数;
3. 如果类包含有内置类型或者复合类型(如数组或指针)的成员,则只有当这些成员全都被赋予了类内的初始值时,这个类才适合于使用合成的默认构造函数;
4. 有的时候编译器不能为某些类合成默认构造函数。例如,如果类内包含一个其他类类型的成员且这个成员的类型没有默认构造函数,那么编译器将无法初始化该成员。
(二)复制构造函数
复制构造函数是一种特殊的构造函数,具有一般构造函数的所有特性,其形参是本类的对象的引用。其作用是使用一个已经存在的对象(由复制构造函数的参数指定),去初始化同类的一个新对象。
复制构造函数会被调用的三种情况:
1. 当用类的一个对象去初始化该类的另一个对象时;
2. 如果函数的形参是类的对象,调用函数时,进行形参和实参结合时(只有把对象用值传递才会调用复制构造函数。引用传递则不会调用复制构造函数);
3. 如果函数的返回值是类的对象,函数执行完成返回调用者时。
注意事项:
1. 如果没有定义类的复制构造函数,系统就会在必要时自动生成一个默认的复制构造函数。这个隐含的复制构造函数会把初始值对象的每个数据成员的值都复制到新建立的对象中;
2. 当类的数据成员中有指针类型时,默认的复制构造函数实现的只能是浅复制。浅复制会带来数据安全方面的隐患,要实现正确的复制,也就是深复制,必须编写复制构造函数。
(三)析构函数
析构函数与构造函数的作用几乎正好相反,它用来完成对象被删除前的一些清理工作。析构函数是在对象的生存期即将结束的时刻被自动调用的,完成调用后,对象相应的内存空间也被释放。
析构函数是类的一个公有函数成员,它的名称是由类名前面加”~“构成,没有返回值。和构造函数不同的是析构函数不接收任何参数,但可以是虚函数。如果不进行显式说明,系统也会生成一个函数体为空的默认析构函数。
注意事项:
1. 很多需要动态内存的类应该使用vector对象或者string对象管理必要的存储空间。使用vector或者string的类能避免分配和释放内存带来的复杂性,因为如果类包含vector或者string成员,则其拷贝、赋值和销毁的编译器自动合成版本函数能够正常工作;
2. 如果希望程序在对象被删除之前的时刻自动完成某些事情,就可以把它们写到析构函数中。
(四)访问控制与封装
1、访问控制属性
对类成员访问权限的控制,是通过设置成员的访问控制属性而实现的。访问控制属性可以有以下3种:公有类型(public)、私有类型(private)和保护类型(protected)。
公有类型成员定义了类的外部接口,在类外只能访问类的公有成员。私有成员只能被本类的成员函数访问,来自类外部的任何访问都是非法的。保护类型成员的性质和私有成员的性质相似,其差别在于继承过程中对产生的新类影响不同。
2、友元
类可以允许其他类或者函数访问它的非公有成员,方法是令其他类或者函数成为它的友元(friend)。如果类想把一个函数作为它的友元,只需要增加一条以friend关键字开始的函数声明语句即可。
友元声明只能出现在类定义的内部,但是在类内出现的具体位置不限。友元不是类的成员,也不受它所在区域访问控制级别的约束。
3. 封装的益处
(1)确保用户代码不会无意间破坏封装对象的状态;
(2)被封装的类的具体实现细节可以随时改变,而无需调整用户级别的代码。
三、结构体(Struct)和联合体(Union)
(一)结构体
C++中引入结构体是为了保持和C程序的兼容性。C语言只有结构体而没有类,C语言的结构体中只允许定义数据成员,不允许定义函数成员,而且C语言没有访问控制属性的概念,结构体的全部成员是公有的。
结构体可以使用{实际参数}进行初始化,这是一种聚合初始化的方式(类必须构造函数)。编译器会按照成员声明的顺序,将{}中的值依次赋给结构体的成员变量。具体形式如下:
struct aClass {int aa;int bb;int cc;};
aClass aa = {1,2,3};
(二)联合体
联合体从C语言继承而来,默认访问控制属性也是公有类型。联合体的全部数据成员共享同一组内存单元,其成员同时至多只有一个是有意义的(即不能同时存储多个成员的值)。
在给Union中的某个成员赋值后,其他成员的值将被覆盖;Union的字节存储大小由其最大字节存储大小的成员决定;Union可以在定义时直接初始化,但只能对第一个定义成员进行初始化[3]。
联合体限制[1]:
①联合体的各个对象成员,不能有自定义的构造函数、自定义的析构函数和重载的复制赋值运算符,不仅联合体的对象成员不能有这些函数,这些对象成员的对象成员也不能有,以此类推。
②联合体不能继承,因而也不支持包含多态。
参考资料:
[1] C++语言程序设计 / 郑莉,董渊,何江舟编著.—4版.—北京:清华大学出版社,2010.7(清华大学计算机系列教材)
[2] C++ Primer中文版:第5版 /(美)李普曼(Lippman,S.B.),(美)拉乔伊(Lajoie,J.),(美)默(Moo,B.E.)著;王刚,杨巨峰译. —北京:电子工业出版社,2013.9.
[3] Union:联合体的使用与理解-百度开发者中心