Chapter10 类和对象
10.1 抽象和类
一般来说,类规范由两个部分组成:
(1)类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口。
(2)类方法定义:描述如何实现类成员函数。
(简单地说,类声明提供了类的蓝图,而方法定义则提供了细节)
接口是一个共享框架,供两个系统交互时使用。
使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数(或友元函数)来访问对象的私有成员。公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口,防止程序直接访问数据(数据隐藏)。
类设计应尽可能将公有接口和实现细节分开,公有接口表示设计的抽象组件,将实现细节放在一起并将它们与抽象分开被称为封装。
C++对结构进行了扩展,使它之具有与类相同的特性。它们之间唯一的区别是,结构的默认访问类型为public,而类为private。
成员函数定义与常规函数定义非常相似,它们有函数头和函数体,也可以有返回类型和参数,但是它们还有两个特殊的特征:(1)定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类(类方法的完整名称中包括类名)(2)类方法可以访问类的private组件。
定义位于类声明中的函数都将自动称为内联函数,此外也可以在类声明之外定义成员函数,并使其称为内联函数,为此,只需在类实现部分中定义函数时使用inline限定符即可。
内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。(根据改写规则,在类声明中定义方法,等同于用原型替换方法定义,然后在类声明的后面将定义改写为内联函数)。
创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法,即每种方法只有一个副本。在OOP中,调用成员函数被称为发送消息,因此将同样的消息发送给两个不同的对象将调用同一个方法,但该方法被用于两个不同的对象。
要创建对象,可以声明类变量,也可以使用new为类对象分配存储空间。可以将对象作为函数的参数和返回值,也可以将一个对象赋给另一个。
指定类设计的第一步是提供类声明。类声明类似结构声明,可以包括数据成员和函数成员。声明有四有部分,在其中生命的成员只能通过成员函数进行访问;声明还具有公有部分,在其中生命的成员可被使用类对象的程序直接访问。典型的类声明格式如下:
1 | class className |
10.2 类的构造函数和析构函数
10.2.1 构造函数
程序声明对象时,将自动调用构造函数。(*构造函数的参数表示的不是类成员,而是赋给类成员的值,因此,参数名不能与类成员相同)
C++提供了两种使用构造函数来初始化对象的方式,一种是显式地调用构造函数:
1 | Stock food = Stock("test", 230, 1.25); |
另一种是隐式地调用构造函数:
1 | Stock food("test", 230, 1.25); |
也可以使用new运算符创建对象:
1 | Stock * pt = new Stock("test", 230, 1.25); |
C++无法使用对象来调用构造函数,因为在构造函数构造出对象之前,对象是不存在的。
默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。默认构造函数没有参数,因为声明中不包含值。当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。
如果要创建对象而不显式地初始化,则必须定义一个不接受任何参数的默认构造函数。定义默认构造函数的方式有两种。一种是给已有构造函数的所有参数提供默认值:
1 | Stock(const String & co = "Error", int n = 0, double pr = 0.0); |
另一种方式是通过函数重载来定义另一个构造函数——没有参数的构造函数:
1 | Stock(); |
10.2.2 析构函数
和构造函数一样,析构函数的名称也很特殊:在类名前加上~,另外析构函数和构造函数一样,也没有返回值和声明类型。与构造函数不同的是,析构函数没有参数。
如果创建的是静态存储类对象,则其析构函数将在程序结束时自动被调用。如果创建的是自动存储类对象,则将其析构函数将在程序执行完代码块时自动被调用给。如果对象是通过new创建的,则它将驻留在占内存或自由存储区中,当使用delete来释放内存时,其析构函数将自动被调用。
由于在类对象过期时析构函数将被自动调用,因此必须有一个析构函数。如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供析构函数的定义。
在默认情况下,将一个对象赋给同类型的另一个对象时,C++将源对象的每个数据成员的内容复制到目标对象中相应的数据成员中。
如果既可以通过初始化,也可以通过复制来设置对象的值,则应采用初始化方式。通常这种方式的效率更高。(在赋值语句中使用构造函数总会导致在赋值前创建一个临时对象,而初始化语句则不一定)
10.2.3 构造函数和析构函数小结
构造函数是一种特殊的类成员函数,在创建类对象时被调用。构造函数的名称和类名相同,但通过函数重载,可以创建多个同名的构造函数,条件是每个函数的特征标(函数参数)都不同。另外,构造函数没有声明类型。
接受一个参数的构造函数允许使用赋值语法将一个对象初始化为一个值。但这种方法可能会导致问题。(可以使用关键字explicit关闭隐式转换)
默认构造函数没有参数,因此如果创建对象时候没有进行显示地初始化,则将调用默认构造函数。如果程序中没有提供任何构造函数,则编译器会为程序定义一个默认构造函数;否则,必须自己提供默认构造函数。默认构造函数可以没有任何参数;如果有,则必须给所有参数都提供默认值。
如果构造函数使用了new,则必须提供使用delete的析构函数。
10.3 this指针
每个成员函数(包括构造函数和析构函数)都有一个this指针。this指针指向调用对象,如果方法需要引用整个调用对象,则可以使用表达式*this。
初始化数组的方案式,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。因此,要创建类对象数组,则这个类必须有默认构造函数。
10.4 类作用域
在类中定义的名称的作用域都为整个类,作用域为整个类的名称只在该类种是已知的,在类外是不可知的。类作用域意味着不能从外部直接访问类的成员,公有成员函数也是如此,要调用公有成员函数,必须通过对象,同样,在定义成员函数时,必须使用作用域解析运算符。
在类声明种声明的枚举的作用域为整个类,因此可以用枚举为整型常量提供作用域为整个类的符号名称。例:
1 | class Bakery |
用这种方式声明枚举并不会创建类数据成员。也就是说,所有对象中都不包含枚举。
C++提供了另一种在类中定义常量的方法——使用关键字static:
1 | class Bakery |
作用域内枚举不能隐式地转换为整型。
10.5 总结
面向对象编程强调的是程序如何表示数据。使用OOP方法解决编程问题的第一步是根据它与程序之间的接口来描述数据,从而指定如何使用数据。然后,设计一个类来实现该接口,一般来说,私有数据成员存储信息,公有成员函数(又称为方法)提供访问数据的唯一途径。
类是用户定义的类型,对象是类的实例。
如果希望成员函数对多个对象进行操作,可以将额外的对象作为参数传递给它。如果方法需要显式地引用调用它的对象,则可以使用this指针,由于this指针被设置为调用对象的地址,因此*this是该对象的别名。