【抽象和类】
引言:生活中充满复杂性,处理复杂性的方法之一是简化和抽象。如果我们要用信息与用户之间的的接口来表示计算,那么抽象将是至关重要的。也就是说,将问题的本质特征抽象出来,并根据特征来描述解决方案。在上一讲的垒球统计数据示例中,接口描述了用户如何初始化、更新和显示数据。抽象是通往用户定义类型的捷径,在C++中,用户定义类型指的是实现抽奖接口的类设计。
1.1 类型是什么
当我们看到一个给定的基本数据类型,我们会想到:
- 它定义的数据对象需要的内存;
- 它定义的数据对象能执行的操作;
- 它决定如何解释内存中的位(如long和float在内存中占用的位数相同,但将它们转换为数值的方法不同)。
具体来说,我们可以根据数据在内存中如何存储来考虑其数据类型(例如,char占用1个字节的内存,而double占用8个字节的内存),也可以根据能对数据进行的操作来定义其数据类型(例如,int类型可以使用所有的算术运算,可对整数执行加、减、乘、除运算,而且还可以对它们使用求模运算符%)。而指针需要的内存数量很可能与int相同,甚至可能在内部被表示为整数,但不能对指针执行与整数相同的运算(例如,不能将两个指针相乘,这种运算将毫无意义)。因此,将变量声明为int或float指针时,不仅仅是分配内存,还规定了可对变量执行的操作。
对于内置类型来说,有关操作的信息被内置到编译器中。但在C++中定义用户自定义的类型时,必须自己提供这些信息。付出这些劳动换来了根据实际需要定制新数据类型的强大功能和灵活性。
1.2 C++中的类
我们来尝试定义一个类:
- 类声明:数据成员+成员函数(前者描述数据部分,后者描述公有接口)
- 类方法定义:描述如何实现类成员函数
我们仿佛感觉出:类声明提供了类的蓝图,而方法定义则提供了细节。
什么是接口?
接口是一个共享框架,供两个系统(如计算机和打印机之间或者用户和计算机程序之间)交互时使用。我们举一个例子:假设用户是我,程序是字处理器。我使用字处理器时,不能直接将脑子中想到的词传输到计算机内存中,而必须同程序提供的接口交互。我敲打键盘时,计算机将字符显示到屏幕上;我移动鼠标时,计算机移动屏幕上的光标,我无意间单击鼠标时,计算机对我输入的段落进行奇怪的处理。程序接口将我的意图转换为存储在计算机中的具体信息。
对于类,我们常谈公共接口。在这里,公众(public)是使用类的程序,交互系统由类对象组成,而接口由编写类的人提供的方法组成。接口让程序员能够编写与类对象交互的代码,从而让程序能够使用类对象。举个例子:要计算string对象中包含多少个字符,我们无需打开对象,而只需使用string类提供的size()方法。类设计禁止公共用户直接访问类,但公众可以使用方法size()。方法size()是用户和string类对象之间的公共接口的组成部分。通常,方法getline()是istream类的公共接口的组成部分,使用cin的程序不是直接与cin对象内部交互来读取一行输入,而是使用getline()。
如果希望更人性化,我们不要将使用类的程序视为公共用户,而将编写程序的人视为公共用户。然而,要使用某个类,必须了解其公共接口;要编写类,必须创建其公共接口。
我们编程实现类时,通常将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。在头文件中,对于数据成员,我们要进行定义,而对于成员函数,我们可以进行定义,也可以用原型表示(那这部分成员函数将在源代码文件中定义)。因此,对于描述函数接口而言,原型就够了。
下面我们看一个头文件,它是Stock类的类声明:
// stock00.h -- Stock class interface // version 00 #ifndef STOCK00_H_ //使用#ifndef访问多次包含同一个文件 #define STOCK00_H_ #include <string> class Stock // class declaration { private: std::string company; long shares; double share_val; double total_val; void set_tot() { total_val = shares * share_val; } public: void acquire(const std::string & co, long n, double pr); void buy(long num, double price); void sell(long num, double price); void update(double price); void show(); }; // note semicolon at the end #endif
类最吸引人的特性——将数据和方法组合成一个单元。
类成员的访问控制也是我们需要着重考虑的。
C++提供了private/public/protected关键字来描述类成员的属性。使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数(或友元函数)来访问对象的私有成员。因此,公有成员函数被称为是程序和对象的私有成员之间的桥梁,它提供了对象和程序之间的接口。防止程序直接访问数据被称为数据隐藏。
由于数据隐藏是OOP主要的目标之一,因此类的数据项通常放在私有部分,组成类接口的成员函数放在公有部分(否则就无法从程序中调用这些函数)。尽管如此,我们仍可以将成员函数放在私有部分,虽然程序无法调用,但是公有方法却可以使用它。通常,程序员使用私有成员函数来处理不属于公有接口的实现细节。
类和结构
类描述看上去很像是包含成员函数以及public和private可见性标签的结构声明。实际上,C++对结构进行了扩展,使之具有与类相同的特性。它们之间唯一的区别是——结构默认访问类型是public,而类为private。C++程序员通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象。
1.3 实现类成员函数
在类声明的文件中,有部分成员函数是用原型表示的。那么我们需要为这些成员函数提供代码,我们称之为成员函数定义,这个过程在实现文件中完成。
成员函数定义有两个重要特征:
- 使用作用域解析运算符(::)来标识函数所属的类;
- 类方法可以访问类的private组件。
对于第一个特征,我们举例如下:
void Stock::update(double price)
它意味着我们定义的update()函数是Stock类的成员。这不仅将update()标识为成员函数,还意味着我们可以将另一类的成员函数也命名为update()。
因此,作用域解析运算符确定了方法定义对应的类的身份。我们说,标识符update()具有类作用域。Stock类的其他成员函数不必使用作用域解析运算符就可以使用update()方法,这是因为它们属于同一个类,因此update()对它们是可见的。
类方法的完整名称中包括类名。我们说,Stock::update()是函数的限定名,而简单的update()是全名的缩写(非限定名),他只能在类作用域中使用。
对于第二个特征,我们要知道的是,如果使用非成员函数访问该类的私有数据成员,编译器将禁止这么做(友元函数例外)。
掌握了上面这两点,我们便可实现类方法了,我们将这个过程放在一个独立的实现文件中(因此需要包含头文件stock00.h,让编译器能够访问类定义):
// stock00.cpp -- implementing the Stock class // version 00 #include <iostream> #include "stock00.h" void Stock::acquire(const std::string & co, long n, double pr) { company = co; if (n < 0) { std::cout << "Number of shares can't be negative; " << company << " shares set to 0.\n"; shares = 0; } else shares = n; share_val = pr; set_tot(); } void Stock::buy(long num, double price) { if (num < 0) { std::cout << "Number of shares purchased can't be negative. " << "Transaction is aborted.\n"; } else { shares += num; share_val = price; set_tot(); } } void Stock::sell(long num, double price) { using std::cout; if (num < 0) { cout << "Number of shares sold can't be negative. " << "Transaction is aborted.\n"; } else if (num > shares) { cout << "You can't sell more than you have! " << "Transaction is aborted.\n"; } else { shares -= num; share_val = price; set_tot(); } } void Stock::update(double price) { share_val = price; set_tot(); } void Stock::show() { std::cout << "Company: " << company << " Shares: " << shares << '\n' << " Share Price: $" << share_val << " Total Worth: $" << total_val << '\n'; }
4个成员函数都设置或重新设置了total_val成员值。这个类并非将计算代码编写4次,而是让每个函数都调用set_tot()函数。我们知道,set_tot()函数是在头文件中定义的私有成员函数,所以它只是实现代码的一种方式,而不是公有接口的组成部分。这种方法的主要价值在于:通过使用函数调用(而不是每次重新输入计算代码)可以确保执行的计算完全相同。另外,如果必须修订计算代码,则只需在一个地方进行修改即可。
我们称在类声明中定义的函数为内联函数,因此Stock::set_tot()是一个内联函数。类声明常将短小的成员函数作为内联函数,set_tot()符合这样的要求。内联函数的特殊规则要求——在每个使用它们的文件中都对其进行定义。那么确保内联定义对多文件程序中的所有文件都是可用的、最简便的方法是:将内联定义放在定义类的头文件中。
1.4 使用类
知道如何定义类及其方法后,我们便可以创建一个这样的程序,它创建并使用类对象:
// usestock0.cpp -- the client program // compile with stock00.cpp #include <iostream> #include "stock00.h" int main() { Stock fluffy_the_cat; fluffy_the_cat.acquire("NanoSmart", 20, 12.50); fluffy_the_cat.show(); fluffy_the_cat.buy(15, 18.125); fluffy_the_cat.show(); fluffy_the_cat.sell(400, 20.00); fluffy_the_cat.show(); fluffy_the_cat.buy(300000,40.125); fluffy_the_cat.show(); fluffy_the_cat.sell(300000,0.125); fluffy_the_cat.show(); return 0; }
C++的目标是使得使用类与使用基本的内置类型(如char和int)尽可能相同。要创建类对象,可以声明类变量,也可以使用new为类对象分配存储空间。可以将对象作为函数的参数和返回值,也可以将一个对象赋给另一个。C++提供了一些工具,可用于初始化对象,让cin和cout识别对象,甚至在相似的类对象之间进行自动类型转换。
注意,main()只是用来测试Stock类的设计。当Stock()类的运行情况与预期的相同后,便可以在其他程序中将Stock类作为用户定义的类型使用。要使用新类型,最关键的是要了解成员函数的功能,而不必考虑其实现细节。
客户/服务器模型
OOP程序员常依照客户/服务器模型来讨论程序设计。在这个概念中,客户是使用类的程序,类声明(包括类方法)构成了服务器,它是程序可以使用的资源。客户只能通过以公有方式定义的接口使用服务器,这意味着客户(客户程序员)唯一的责任是了解该接口。服务器(服务器设计人员)的责任是确保服务器根据该接口可靠并准确地执行。服务器设计人员只能修改类设计的实现细节,而不能修改接口。这样程序员独立地对客户和服务器进行改进,对服务器的修改不会对客户的行为造成意外的影响。
【类的构造函数和析构函数】
C++的目标之一——让使用类对象就像使用标准类型一样,然而,我们学到这还是不能像初始化int或结构那样来初始化Stock对象。也就是说,常规的初始化语法不适用于类型Stock:
int year = 2001;
struct thing{
char *pn;
int m;
};
thing amabob = {"wodget", -23};
Stock hot = {"Sukie's Autos, Inc.", 200, 50.25}; //NO!编译错误
我们之所以不能向上面这样初始化Stock对象的原因在于,数据部分的访问状态是私有的,即程序不能直接访问数据成员。
前面我们已经学过,程序只能通过成员函数来访问数据成员,因此我们需要设计合适的成员函数,才能成功地将对象初始化。
C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值赋给它们的数据成员。
下面我们就讲讲构造函数。
- 构造函数与类同名;
- 没有返回值(声明类型);
- 原型位于类声明的公有部分;
- 程序声明对象时将自动调用构造函数。
譬如,我要创建Stock的构造函数,由于需要为Stock对象提供3个值,因此应为构造函数提供3个参数(第4个数据total_val,是根据类内部函数得到的)。
构造函数原型:
//构造函数原型(带默认参数版本)
Stock(const string &co, long n = 0, double pr = 0.0);
显然,我们设置了默认参数,原因是我们此时只想设置company成员,而将其他值设置为0。没有返回类型,原型位于类声明的公有部分。
构造函数定义:
//第一个参数是指向字符串的指针,该字符串用于初始化成员company。n和pr参数为shares和share_val成员提供值。
Stock::Stock(const string &co, long n, double pr)
{
company = co;
if(n>0) shares = n;
share_val = pr;
set_tot(); //用成员函数为第四个数据成员赋值
}
这个代码与之前的函数acquire()相同,区别在于,程序声明对象时,将自动调用构造函数。
接下来提两个注意点。
首先是数据成员名称为构造函数做出的改变。
我们初次接触构造函数,常常会试图将类成员名称用作构造函数的参数值,如下所示:
Stock::Stock(const string &company, long shares, double share_val)
{
...
}
这是错误的,因为构造函数的参数是要赋给类成员的值,这样的参数名将会造成:
shares = shares;
为了避免这种混乱,我们可以在类成员的名称中使用后缀_:
class Stock
{
private:
string company_;
long shares_;
...
}
其次是我们要搞清对象和构造函数的关系。
我们无法使用对象来调用构造函数,因为在构造函数构造出对象之前,对象是不存在的。因此构造函数被用来创建对象,而不能通过对象来调用它。
最后我们学习默认构造函数。
默认构造函数:在未提供显示初始值时,用来创建对象的构造函数。
定义默认构造函数的两种方式:
1、给已有构造函数的所有参数提供默认值:
Stock(const string &co = "Error", int n = 0, double pr = 0.0);
2、通过函数重载来定义另一个函数——一个没有参数的构造函数:
Stock();
由于只能有一个默认构造参数,因此不要同时采用这两种方式。第二种方式看上去与编译器提供的默认构造参数相同,但我们可以对我们自己定义的默认构造函数(第二种方式)进行想要的函数定义:
Stock::Stock()
{
company = "no name";
shares = 0;
share_val = 0.0;
total_val = 0.0;
}
而编译器提供的默认构造函数可能如下:
Stock::Stock() { }
我们要搞清的几点内容如下:
- 如果类没有提供任何构造函数,则C++将自动提供默认构造函数;
- 为类定义了构造函数(不是给所有参数赋默认值的那种)后,程序员就必须为类提供默认构造函数,否则这种声明(Stock stock1;)将出错;
- 上述两种的默认构造函数都没有参数(而给所有参数赋默认值的那种默认构造函数则有参数),因为声明中不包含值。
创建了默认构造函数后,我们便可以声明对象变量而不对它们进行显示初始化:
Stock first; //隐式调用默认构造函数
Stock first = Stock(); //显示调用默认构造函数
Stock *prelief = new Stock; //隐式调用默认构造函数
然而,不要被非默认构造函数的隐式形式所误导:
Stock first("Concrete Conglomerate"); //调用构造函数
Stock second(); //函数声明
Stock third; //调用默认构造函数
第一个声明调用接收参数的构造函数;第二个声明指出second()是一个返回Stock对象的函数,隐式地调用默认构造函数时,不要使用圆括号。
放轻松,我们构造函数已经全部讲完,接下来就只剩析构函数了。
用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用析构函数来完成清理工作。
析构函数的特征:
- 名称为在类名前加上~,例如Stock类的析构函数名为~Stock;
- 没有返回值(声明类型)和参数,即析构函数的原型必须是“~Stock();”;
- 不承担任何重要的工作,故可以在函数体内不进行任何操作;
- 类对象过期时将被自动调用,因此类必须有一个析构函数(如果没有,编译器将做此事)。
具体什么时候调用析构函数?这由编译器决定,我们不应在程序中显示地调用析构函数(某些情况除外)。正常情况都是析构函数被自动调用,我们只要在类中进行声明和定义即可。
下面列出Stock类的声明和定义代码:
~Stock(); //声明
Stock::~Stock() //定义
{
}
【this指针】
到目前为止,每个成员函数都只涉及一个对象(即调用它的对象),但有时候方法可能涉及到两个对象,在这种情况下需要使用C++中的this指针。
比如说,我们要比较两个对象并将需要的那个对象返回给调用程序。那么问题来了,如何将两个对象传给提供给成员函数呢?
例如我们将该成员函数命名为topval(),则函数调用stock1.topval()将访问stock1对象的数据,而stock2.topval()将访问stock2对象的数据。如果希望该方法将两个对象进行比较,则必须将第二个对象作为参数传递给它。此处,我们按引用来传递参数,也就是说,topval()方法使用一个类型为const Stock &的参数。
我们如何将方法的答案传回给调用程序呢?此处,我们让方法返回一个引用,该引用指向需要的那个对象。
于是,用于比较的方法的原型如下:
const Stock& topval(const Stock &s) const;
该函数隐式地访问一个对象,而显示地访问另一个对象,并返回其中一个对象的引用。括号中的const表明该函数不会修改被显示地访问的对象;而括号后的const表明该函数不会修改被隐式地访问的对象。由于该函数返回两个const对象之一的引用,因此返回类型也应为const引用。
假设要对Stock对象stock1和stock2进行比较,并将其中股价总值较高的那一个赋给top对象,则可以使用下面两条语句之一:
top = stock1.topval(stock2); //隐式地访问stock1,显示地访问stock2
top = stock2.topval(stock1); //隐式地访问stock2,显示地访问stock1
实际上,这种表示法有点混乱,在我们学完重载运算符后,我们便可以使用关系运算符>来比较这两个对象。
说到这,this指针呢??
别急,我们看看方法topval()的实现:
const Stock& topval(const Stock &s) const
{
if(s.total_val > total_val)
return s; //参数对象
else
return ????; //调用对象
}
我们知道s.total_val是作为参数传递的对象的总值,total_val是用来调用该方法的对象的总值。如果s.total_val >大于total_val,则函数将返回指向s的引用;否则将返回用来调用该方法的对象。问题在于,如何称呼该对象?如果调用stock1.topval(stock2),则s是stock2的引用(即stock2的别名),但stock1没有别名。
C++解决这种问题的方法是:使用this指针——指向用来调用成员函数的对象,即对于函数调用stock1.topval(stock2)来说,this被设置为stock1对象的地址。一般来说,所有的类方法都将this指针设置为调用它的对象的地址。所以上面的代码中,total_val只不过是this->total_val的简写。
this是对象的地址,*this是对象本身。故可以将*this作为调用对象的别名完成上述方法topval()的定义。
于是上面的代码改写为:
const Stock& topval(const Stock &s) const //返回类型为引用意味着返回的是调用对象本身,而不是其副本
{
if(s.total_val > total_val)
return s; //参数对象
else
return *this; //调用对象
}
【对象数组】
对象数组的声明和标准类型数组的声明相同,对象数组中每一个元素都是一个对象。
下面我们来看一下对象数组的使用:
Stock mystuff[4]; //创建一个包含4个Stock类对象的数组
这个对象数组声明要求:Stock类没有显式地定义任何构造函数(将使用不执行任何操作的隐式默认构造函数),或Stock类定义了一个显示默认构造函数(就像这个例子一样)。
此外,我们还可以用构造函数来初始化数组元素(必须为每个元素调用构造函数):
Stock stock[4] = {
Stock("NanoSmart", 12.5, 20),
Stock("Boffo Objects", 200, 2.0),
Stock("Monolithic", 130, 3.25),
Stock("Fleep Enterprises", 60, 6.5),
};
上面的代码使用标准格式对数组进行初始化:用括号括起的、以逗号分隔的值列表。其中每次构造函数调用表示一个值。
如果类包含多个构造函数,则可以对不同的元素使用不同的构造函数:
Stock stock[10] = {
Stock("NanoSmart", 12.5, 20),
Stock(),
Stock("Boffo Objects", 200, 2.0),
};
上述代码使用Stock(const string& co,long n, double pr)初始化stock[0]和stock[2],使用构造函数Stock()初始化stock[1]。特别注意:该声明只初始化了数组的部分元素,因此剩余的7个元素将使用默认构造函数进行初始化。
初始化对象数组的方案是:首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。
因此,要创建类对象数组,则这个类必须有默认构造函数。
【类作用域】
在类中定义的名称的作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。因此,可以在不同类中使用相同的类成员名而不会引起冲突。
类作用域意味着不能从外部直接访问类的成员(公有函数也是如此),即要访问类的公有成员,必须通过对象。
正因为有类作用域的存在,我们在定义成员函数时,编写其函数名时必须使用作用域解析运算符;而在类声明或成员函数定义中,我们可以使用未限定的名称。
下面我们来看一下作用域为类的常量。
有时候,我们让符号常量的作用域为类很有用。比如说,类声明可能使用30来指定数组的长度,由于该常量对于所有的对象来说都是相同的,因此创建一个由所有对象共享的常量是个不错的主意。
如果我们如此声明:
class Bakery
{
private:
const int Months = 12; //声明一个常量??错误!!
double costs[Months];
...
但这是行不通的,因为声明类只是描述了对象的形式,并没有创建对象。因此,在声明对象前,将没有用于存储值的空间。
于是我们想到了两种方式实现这个目标。
(1)C++提供了另一种在类中定义常量的方式——使用关键字static:
class Bakery
{
private:
static const int Months = 12;
double costs[Months];
...
这将创建一个名为Months的常量,该常量将与其他静态变量存储在一起,而不是存储在对象中。因此,只有一个Months常量,被所有Bakery对象共享。
(2)在类中声明一个枚举:
在类声明中声明的枚举的作用域为整个类,因此可以用枚举为整型常量提供作用域为整个类的符号名称:
class Bakery
{
private:
enum {Months = 12};
double costs[Months];
...
注意,用这种方式声明枚举并不会创建类数据成员。也就是所有对象都不包含枚举。另外,Months只是一个符号名称,在作用域为整个类的代码中遇到它时,编译器将用12来代替它。这里使用枚举只是为了创建符号常量,并不打算创建枚举类型的变量,因此不需要提供枚举名。
C++11提供了一种新枚举,其枚举的作用域为类。
下面我们来看作用域内枚举。
传统的枚举存在一些问题,包括两个枚举定义中的枚举量可能发生冲突。
假设有一个处理鸡蛋和T恤的项目,其中可能包含下面这样的代码:
enum t_shirt {Small, Medium, Large, Xlarge};
enum egg {Small, Medium, Large, Jumbo};
这样的代码将无法通过编译,因为egg Small和t_shirt Small位于相同的作用域内,它们将发生冲突。
为避免这种问题,C++11提供了一种新枚举,其枚举的作用域为类。这种枚举的声明类似于下面这样:
enum class t_shirt {Small, Medium, Large, Xlarge};
enum class egg {Small, Medium, Large, Jumbo};
这里使用了关键字class,除此,我们需要使用枚举名来限定枚举量:
t_shirt Floyd = t_shirt::Large; //枚举量Large是枚举t_shirt中的
egg choice = egg::Large; //枚举量Large是枚举egg中的
枚举量的作用域为类后,不同枚举定义的枚举量就不会发生名称冲突了。
C++还提高了作用域内枚举的类型安全。在有些情况下,常规枚举将自动转换为整型,如将其赋给int变量或用于比较表达式时,但作用域内枚举不能隐式地转换为整型:
enum egg {Small, Medium, Large, Jumbo};
enum class t_shirt {Small, Medium, Large, Xlarge}; //枚举量的作用域为类
egg one = medium;
t_shirt rolf = t_shirt::Large;
int king = one; //可以进行隐式类型转换
int king = rolf; //不允许,作用域内枚举不能隐式地转换为整型
int Frodo = int(t_shirt::small); //允许,显示类型转换,Frodo被设置为 0
if(king < Jumbo) //允许
if(king < t_shirt::Medium) //不允许
枚举用某种底层整型类型表示,在C++98中,如何选择取决于实现,因此包含枚举的结构的长度可能随系统而异。对于作用域内枚举,C++11消除了这种依赖性。
C++11作用域内枚举的底层类型默认为int,此外还提供了一种语法,用于做出不同的选择:
enum class : short pizza {Small, Medium, Large, Xlarge};
:short将底层类型指定为short。底层类型必须为整型。在C++11中,也可以使用这种语法来指定常规枚举的底层类型,但如果没有指定,编译器选择的底层类型将随实现而异。
【抽象数据类型】
程序员常常通过定义类来表示更通用的概念。例如,就实现ADT而言,使用类是一种非常好的方式。
ADT:以通用的方式描述数据类型,而没有引入语言或实现细节。
下面举例说明。
通过使用栈,可以以这样的方式存储数据,即总是从堆顶添加或删除数据。比如,C++程序使用栈来管理自动变量:当新的自动变量被生成后,它们被添加到堆顶;消亡时,从栈中删除它们。
下面我们简要地介绍一下栈的特征:首先栈存储了多个数据项(该特征使得栈成为一个容器——一种更为通用的抽象),其次栈由可对它执行的操作来描述。
- 可创建空战;
- 可将数据项添加到堆顶(压入);
- 可从栈顶删除数据项(弹出);
- 可查看栈是否填满;
- 可参看栈是否为空。
可以将上述描述转换为一个类声明,其中公有成员函数提供了表示栈操作的接口,而私有成员负责存储栈数据。类概念非常适合于ADT方法。
私有部分必须表明数据存储的方式。例如,可以使用常规数组、动态分配数组或更高级的数据结构(如链表)。然而,公有接口应隐藏数据表示,而以通用的术语来表达,如创建栈、压入等。
下面是一段程序:
// stack.h -- class definition for the stack ADT
#ifndef STACK_H_
#define STACK_H_
typedef unsigned long Item;
class Stack
{
private:
enum {MAX = 10}; // constant specific to class
Item items[MAX]; // holds stack items
int top; // index for top stack item
public:
Stack();
bool isempty() const;
bool isfull() const;
// push() returns false if stack already is full, true otherwise
bool push(const Item & item); // add item to stack
// pop() returns false if stack already is empty, true otherwise
bool pop(Item & item); // pop top into item
};
#endif
私有部分表明,栈是使用数据实现的;而公有部分隐藏了这一点。因此可以使用动态数组来代替数组,而不会改变类的接口。这意味着修改栈的实现后,不需要重新编写使用栈的程序,而只需重新编译栈代码,并将其与已有的程序代码链接起来即可。
接口是冗余的,因为pop()和push()返回有关栈状态的信息(满或空),而不是void类型。在如何处理超出栈限制或者清空栈方面,这为程序员提供了两种选择。他可以在修改栈前使用isempty()和isfull()来查看,也可以使用push()和pop()的返回值来确定操作是否成功。
这个类不是根据特定的类型来定义栈,而是根据通用的Item类型来描述。在这个例子中,头文件使用typedef用Item代替unsigned long。如果需要double栈或结构类型的栈,则只需要修改typedef语句,而类声明和方法定义保持不变。
接下来是类的实现方法:
// stack.cpp -- Stack member functions
#include "stack.h"
Stack::Stack() // create an empty stack
{
top = 0;
}
bool Stack::isempty() const
{
return top == 0;
}
bool Stack::isfull() const
{
return top == MAX;
}
bool Stack::push(const Item & item)
{
if (top < MAX)
{
items[top++] = item;
return true;
}
else
return false;
}
bool Stack::pop(Item & item)
{
if (top > 0)
{
item = items[--top];
return true;
}
else
return false;
}
最后是测试该栈的程序:
// stacker.cpp -- testing the Stack class
#include <iostream>
#include <cctype> // or ctype.h
#include "stack.h"
int main()
{
using namespace std;
Stack st; // create an empty stack
char ch;
unsigned long po;
cout << "Please enter A to add a purchase order,\n"
<< "P to process a PO, or Q to quit.\n";
while (cin >> ch && toupper(ch) != 'Q')
{
while (cin.get() != '\n')
continue;
if (!isalpha(ch))
{
cout << '\a';
continue;
}
switch(ch)
{
case 'A':
case 'a': cout << "Enter a PO number to add: ";
cin >> po;
if (st.isfull())
cout << "stack already full\n";
else
st.push(po);
break;
case 'P':
case 'p': if (st.isempty())
cout << "stack already empty\n";
else {
st.pop(po);
cout << "PO #" << po << " popped\n";
}
break;
}
cout << "Please enter A to add a purchase order,\n"
<< "P to process a PO, or Q to quit.\n";
}
cout << "Bye\n";
return 0;
}
温故而知新
1.什么是类?
答:
类是用户定义的类型的定义。类声明指定了数据将如何存储,同时指定了用来访问和操纵这些数据的方法(类成员函数)。
2.类如何实现抽象、封装和数据隐藏?
答:
类表示人们可以以类方法的公有接口对类对象执行的操作,这是抽象。
类数据成员可以是私有的(默认值),这意味着只能通过成员函数来访问这些数据,这是数据隐藏。
实现的具体细节(如数据表示和方法的代码)都是隐藏的,这是封装。
3.对象和类的关系是什么?
答:
类定义了一种类型,包括如何使用它。对象是一个变量或其他数据对象(如由new生成的),并根据类定义被创建和使用。类和对象之间的关系同标准类型与其变量之间的关系相同。
4.除了是函数外,类函数成员和类数据成员之间的区别是什么?
答:
如果创建给定类的多个对象,则每个对象都有其自己的的存储空间;但所有的对象都使用同一组成员函数(通常,方法是公有的,而数据是私有的,但这知识策略方面的问题,而不是对类的要求)。
5.定义一个类来表示银行账户,数据成员包括储户姓名、账号(使用字符串)和存款。成员函数执行如下操作:
①创建一个对象并将其初始化;
②显示储户姓名、账号和存款;
③存入参数指定的存款;
④取出参数指定的款项。
请提供类声明、而不用给出方法实现。
答:
#include<string>//这个我倒没忘,想到了不过没写,但这里还是和答案保持一致吧 class Bankaccount { std::string name; char id[20]; double money; //我用的long long答案用的double,想想还是答案中的好 public: Bankaccout(); Bankaccout(const std::string na, const char* id_, const double mo); void show()const; //这三行都忘了加void void savemoney(const double mo); void loadmoney(double mo); //这行在参数多加了一个const }
6.类构造函数在何时被调用?类析构函数呢?
答:在创建类对象或显式调用构造函数时,类的构造函数都将被调用。当对象过期时,类的析构函数将被调用。
7.给出复习题5中的银行账户类构造函数的代码。
答:
Bankaccout::Bankaccout() {}; Bankaccout::Bankaccout(const std::string na, const char* id_, const long long mo) { name=na; id=id_; money=mo; }
补充:前两个参数可加&表示引用
8.什么是默认构造函数,拥有默认构造函数有什么好处?
答:
默认构造函数是在声明一个类对象,但未使用构造函数对其赋值时调用的函数。
使用默认构造函数可以在声明一个新的类对象时,自动给类对象的私有成员赋值。
补充:无参数、或参数全是默认值的,为默认构造函数。
9. this和*this是什么?
答:
this是一个指针,他指向当前的类对象的地址。例如,声明一个类,然后创建一个类对象,那么这个类对象的成员函数定义里如果有this,那么指的就是当前这个类对象。如果有另一个类对象,那么另一个类对象里面的成员函数定义里this,指的便是另一个。
*this指的是当前类对象。例如有一个类对象a,那么*this(一般出现在类成员函数定义之中)就指的a。而this是a的内存地址。
标准答案:this指针是类方法可以使用的指针,它指向用于调用方法的对象。因此,this是对象的地址,*this是对象本身。