1.5 类的引入
解决书店问题前,剩下的唯一问题是弄明白如何编写数据结构来表示交易数据。C++中我们通过定义类来定义自己的数据结构。类机制是C++中最重要的特征之一。事实上,C++设计的主要焦点是使得定义的类类型的行为可以像内置类型一样自然。我们前面已看到的像istream和ostream这样的库类型,都定义为类——也就是说,它们严格说来不是语言的一部分。
完全理解类机制需要掌握很多内容。所幸我们可以使用他人写的类而无需掌握如何定义自己的类。在这一节,我们将描述一个用于解决书店问题的简单类。当我们学习了更多关于类型、表达式、语句和函数的知识(所有这些在类定义中都将用到)后,将会在后面的章节实现这个类。
使用类时我们需要知道三个问题:
(1) 类的名字是什么?
(2) 它在哪里定义?
(3) 它支持什么操作?
对于书店问题,我们假定类命名为Sales_item且类定义在命名为Sales_item.h的头文件中。
Sales_item类的目的是存储ISBN并保存该书的销售册数、销售收入和平均售价。我们不关心如何存储或计算这些数据。使用类时我们不需要知道这个类是怎样实现的,相反,我们需要知道的是该类提供什么操作。
正如我们所看到的,使用像IO一样的库工具,必须包含相关的头文件。类似地,对于自定义的类,必须使得编译器可以访问和类相关的定义。这几乎可以采用同样的方式。一般来说,我们将类定义放入一个文件中,要使用该类的任何程序都必须包含这个文件。
依据惯例,类类型存储在一个文件中,其文件名如同程序的源文件名一样,由两部分组成:文件名和文件后缀。通常文件名和定义在头文件中的类名是一样的。通常后缀是.h,但也有一些程序员用.H、.hpp或.hxx。编译器通常并不挑剔头文件名,但IDE有时会。假设我们的类定义在名为Sale_item.h的文件中。
1. Sales_item对象上的操作
每个类定义一种类型,类型名与类名相同。因此,我们的Sales_item类定义了一种命名为Sales_item的类型。像使用内置类型一样,可以定义类类型的变量。当写下
Sales_item item;
就表示item是类型Sales_item的一个对象。通常将“类型Sales_item的一个对象”简称为“一个Sales_item对象”,或者更简单地简称为“一个Sales_item”。
除了可以定义Sales_item类型的变量,我们还可以执行Sales_item对象的以下操作:
l 使用加法操作符,+,将两个Sales_item相加
l 使用输入操作符,>>,来读取一个Sales_item对象
l 使用输出操作符,<<,来输出一个Sales_item对象
l 使用赋值操作符,=,将一个Sales_item对象赋值给另一个Sales_item对象
l 调用same_isbn函数确定两个Sales_item是否指同一本书。
2. 读入和写出Sales_item对象
知道了类提供的操作,就可以编写一些简单的程序使用这个类。例如,下面的程序从标准输入读取数据,使用该数据建立一个Sales_item对象,并将该Sales_item对象写到标准输出:
#include <iostream>
#include "Sales_item.h"
int main()
{
Sales_item book;
// read ISBN, number of copies sold, and sales price
std::cin >> book;
// write ISBN, number of copies sold, total revenue, and average price
std::cout << book << std::endl;
return 0;
}
如果输入到程序的是
0-201-70353-X 4 24.99
则输出将是
0-201-70353-X 4 99.96 24.99
输入表明销售了4本书,每本价格是24.99美元。输出表明卖出书的总数是4本,总收入是99.96美元,每本书的平均价格是24.99美元。
这个程序以两个#include指令开始,其中之一使用了一种新格式。iostream头文件由标准库定义,而Sales_item头文件则不是。Sales_item是一种自定义类型。当使用自定义头文件时,我们采用双引号(" ")把头文件名括起来。
在main函数中,首先定义一个对象,命名为book,用它保存从标准输入读取的数据。下一条语句读入数据到此对象,第三条语句将它打印到标准输出,像平常一样紧接着打印endl来刷新缓冲区。
关键概念:类定义行为
在编写使用Sales_item的程序时,重要的是记住类Sales_item的创建者定义该类对象可以执行的所有操作。也就是说,Sales_item数据结构的创建者定义创建Sales_item对象时发生什么,以及加运算符或输入输出运算符应用到Sales_item对象时发生什么,等等。
通常,只有由类定义的操作可被用于该类类型的对象。此时,我们知道的可以在Sales_ item对象上执行的操作只是前面列出的那些。
我们将在
3. 将Sales_item对象相加
更有趣的例子是将两个Sales_item对象相加:
#include <iostream>
#include "Sales_item.h"
int main()
{
Sales_item item1, item2;
std::cin >> item1 >> item2; // read a pair of transactions
std::cout << item1 + item2 << std::endl; // print their sum
return 0;
}
如果我们给这个程序下面的输入:
0-201-78345-X 3 20.00
0-201-78345-X 2 25.00
则输出为
0-201-78345-x 5 110 22
程序首先包含两个头文件Sales_item和iostream。接下来定义两个Sales_item对象来存放要求和的两笔交易。输出表达式做加法运算并输出结果。从前面列出的操作,可以得知将两个Sales_item相加将创建一个新对象,新对象的ISBN是其操作数的ISBN,销售的数量和收入反映其操作数中相应值的和。我们也知道相加的项必须具有同样的ISBN。
值得注意的是这个程序是如何类似于
习题
习题1.21 网站(http://www.awprofessional.com/cpp_primer)的第1章的代码目录下有Sales_item.h的副本。复制该文件到你的工作目录。编写程序循环遍历一组书的销售交易,读入每笔交易并将交易写至标准输出。
习题1.22 编写程序读入两个具有相同ISBN的Sales_item对象并产生它们的和。
习题1.23 编写程序读入几个具有相同ISBN的交易,输出所有读入交易的和。
不幸的是,将Sales_item相加的程序有一个问题。如果输入指向了两个不同的ISBN将发生什么?将两个不同ISBN的数据相加没有意义。为解决这个问题,首先检查Sales_item操作数是否都具有相同的ISBN。
#include <iostream>
#include "Sales_item.h"
int main()
{
Sales_item item1, item2;
std::cin >> item1 >> item2;
// first check that item1 and item2 represent the same book
if (item1.same_isbn(item2)) {
std::cout << item1 + item2 << std::endl;
return 0; // indicate success
} else {
std::cerr << "Data must refer to same ISBN"
<< std::endl;
return -1; // indicate failure
}
}
这个程序和前一个程序不同之处在于if测试语句以及与它相关联的else分支。在解释if语句的条件之前,我们明白程序的行为取决于if语句中的条件。如果测试成功,那么产生与前一程序相同的输出,并返回0表示程序成功运行完毕。如果测试失败,执行else后面的语句块,输出信息并返回错误提示。
1. 什么是成员函数
上述if语句的条件
// first check that item1 and item2 represent the same book
if (item1.same_isbn(item2)) {
调用命名为item1的Sales_item对象的成员函数。成员函数是由类定义的函数,有时称为类方法。
成员函数只定义一次,但被视为每个对象的成员。我们将这些操作称为成员函数,是因为它们(通常)在特定对象上操作。在这个意义上,它们是对象的成员,即使同一类型的所有对象共享同一个定义也是如此。
当调用成员函数时,(通常)指定函数要操作的对象。语法是使用点运算符(.运算符):
item1.same_isbn
意思是“命名为item1的对象的same_isbn成员”。点运算符通过它的左操作数取得右操作数。点运算符仅应用于类类型的对象:左操作数必须是类类型的对象;右操作数必须指定该类型的成员。
通常使用成员函数作为点运算符的右操作数来调用成员函数。执行成员函数和执行其他函数相似:要调用函数,可将调用运算符[“()”运算符]放在函数名之后。调用运算符是一对圆括号,括住传递给函数的实参列表(可能为空)。
same_isbn函数接受单个参数,且该参数是另一个Sales_item对象。函数调用
item1.same_isbn(item2)
将item2作为参数传递给名为same_isbn的函数,该函数是名为item1的对象的成员。它将比较参数item2的ISBN与函数same_isbn要操作的对象item1的ISBN。其效果是测试两个对象是否具有相同的ISBN。
如果对象具有相同的ISBN,执行if后面的语句,输出两个Sales_item对象的和。否则,如果对象具有不同的ISBN,则执行else分支的语句块。该块输出适当的错误信息并退出程序,返回-1。回想main函数的返回值被视为状态指示器。本例中,返回一个非零值表示程序未能产生期望的结果。
习题
习题1.24 编写程序读入几笔不同的交易。对于每笔新读入的交易,要确定它的ISBN是否和以前的交易的ISBN一样,并且记下每一个ISBN的交易的总数。通过给定多笔不同的交易来测试程序。这些交易必须代表多个不同的ISBN,但是每个ISBN的记录应分在同一组。





