第10章-cpp对象和类

本章内容包括:
• 过程性编程和面向对象编程。
• 类概念。
• 如何定义和实现类。
• 公有类访问和私有类访问。
• 类的数据成员。
• 类方法(类函数成员)。
• 创建和使用类对象。
• 类的构造函数和析构函数。
• const成员函数。
• this指针。
• 创建对象数组。
• 类作用域。
• 抽象数据类型。

采用过程性编程方法时,首先考虑要遵循的步骤,然后考虑如何表示这些数据(并不需要程序一直运行,用户可能希望能够将数据存储在一个文件中,然后从这个文件中读取数据)。

采用OOP方法时,首先从用户的角度考虑对象——描述对象所需的数据以及描述用户与数据交互所需的操作。完成对接口的描述后,需要确定如何实现接口和数据存储。最后,使用新的设计方案创建出程序。

二、抽象和类

在计算中,为了根据信息与用户之间的接口来表示它,抽象是至关重要的。也就是说,将问题的本质特征抽象出来,并根据特征来描述解决方案。在垒球统计数据示例中,接口描述了用户如何初始化、更新和显示数据。抽象是通往用户定义类型的捷径,在C++中,用户定义类型指的是实现抽象接口的类设计。

类型是什么

首先,倾向于根据数据的外观(在内存中如何存储)来考虑数据类型。例如,char占用1个字节的内存,而double通常占用8个字节的内存。但是稍加思索就会发现,也可以根据要对它执行的操作来定义数据类型。例如,int类型可以使用所有的算术运算,可对整数执行加、减、乘、除运算,还可以对它们使用求模运算符(%)。

而指针需要的内存数量很可能与int相同,甚至可能在内部被表示为整数。但不能对指针执行与整数相同的运算。例如,不能将两个指针相乘,这种运算没有意义的,因此C++没有实现这种运算。因此,将变量声明为int或float指针时,不仅仅是分配内存,还规定了可对变量执行的操作。总之,指定基本类型完成了三项工作:

  • 决定数据对象需要的内存数量;
  • 决定如何解释内存中的位(long和float在内存中占用的位数相同,但将它们转换为数值的方法不同);
  • 决定可使用数据对象执行的操作或方法。

C++中的类

类是一种将抽象转换为用户定义类型的C++工具,它将数据表示和操纵数据的方法组合成一个整洁的包。

一般来说,类规范由两个部分组成:

  • 类声明:以数据成员的方式描述数据部分,以成员函数(被称为方法)的方式描述公有接口。
  • 类方法定义:描述如何实现类成员函数。

简单地说,类声明提供了类的蓝图,而方法定义则提供了细节。

接口是一个共享框架,供两个系统(如在计算机和打印机之间或者用户或计算机程序之间)交互时使用;例如,用户可能是您,而程序可能是字处理器。使用字处理器时,您不能直接将脑子中想到的词传输到计算机内存中,而必须同程序提供的接口交互。您敲打键盘时,计算机将字符显示到屏幕上;您移动鼠标时,计算机移动屏幕上的光标;您无意间单击鼠标时,计算机对您输入的段落进行奇怪的处理。程序接口将您的意图转换为存储在计算机中的具体信息。

对于类,我们说公共接口。在这里,公众(public)是使用类的程序,交互系统由类对象组成,而接口由编写类的人提供的方法组成。接口让程序员能够编写与类对象交互的代码,从而让程序能够使用类对象。例如,要计算string对象中包含多少个字符,您无需打开对象,而只需使用string类提供的size()方法。类设计禁止公共用户直接访问类,但公众可以使用方法size()。方法size()是用户和string类对象之间的公共接口的组成部分。通常,方法getline()是istream类的公共接口的组成部分,使用cin的程序不是直接与cin对象内部交互来读取一行输入,而是使用getline()。

如果希望更人性化,不要将使用类的程序视为公共用户,而将编写程序的人视为公共用户。然而,要使用某个类,必须了解其公共接口;要编写类,必须创建其公共接口。

为开发一个类并编写一个使用它的程序,需要完成多个步骤。这里将开发过程分成多个阶段,而不是一次性完成。通常,C++程序员将接口(类定义)放在头文件中,并将实现(类方法的代码)放在源代码文件中。这里采用这种典型做法。下面程序清单是第一个阶段的代码,它是Stock类的类声明。这个文件按第9章介绍的那样,使用了#ifndef等来访问多次包含同一个文件。

为帮助识别类,本书遵循一种常见但不通用的约定——将类名首字母大写。您将发现,下面程序清单看起来就像一个结构声明,只是还包括成员函数、公有部分和私有部分等内容。稍后将对该声明进行改进(所以不要将它用作模型),但先来看一看该定义的工作方式。

关于cpp之类与JAVA中类的相似

// stock00.h -- Stock class interface
#ifndef STOCK00_H_
#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();
};
#endif ←

首先,C++关键字class指出这些代码定义了一个类设计。这种语法指出,Stock是这个新类的类型名。该声明让我们能够声明Stock类型的变量——称为对象或实例。每个对象都表示一支股票。例如,下面的声明创建两个Stock对象,它们分别名为sally和solly:

Stock sally;
Stock solly;

例如,sally对象可以表示Sally持有的某公司股票。

istream和ostream类有成员函数,如get()和getline(),而Stock类声明中的函数原型说明了成员函数是如何建立的。例如,头文件iostream将getline()的原型放在istream类的声明中。

①访问控制

关键字private和public也是新的,它们描述了对类成员的访问控制。使用类对象的程序都可以直接访问公有部分,但只能通过公有成员函数(或友元函数,参见第11章)来访问对象的私有成员。例如,要修改Stock类的shares成员,只能通过Stock的成员函数。因此,公有成员函数是程序和对象的私有成员之间的桥梁,提供了对象和程序之间的接口。防止程序直接访问数据被称为数据隐藏(参见图10.1)。C++还提供了第三个访问控制关键字protected,第13章介绍类继承时将讨论该关键字。

类设计尽可能将公有接口与实现细节分开。公有接口表示设计的抽象组件。将实现细节放在一起并将它们与抽象分开被称为封装。数据隐藏(将数据放在类的私有部分中)是一种封装,将实现的细节隐藏在私有部分中,就像Stock类对set_tot()所做的那样,也是一种封装。封装的另一个例子是,将类函数定义和类声明放在不同的文件中。

数据隐藏不仅可以防止直接访问数据,还让开发者(类的用户)无需了解数据是如何被表示的。例如,show()成员将显示某支股票的总价格(还有其他内容),这个值可以存储在对象中(上述代码正是这样做的),也可以在需要时通过计算得到。从使用类的角度看,使用哪种方法没有什么区别。所需要知道的只是各种成员函数的功能;也就是说,需要知道成员函数接受什么样的参数以及返回什么类型的值。原则是将实现细节从接口设计中分离出来。如果以后找到了更好的、实现数据表示或成员函数细节的方法,可以对这些细节进行修改,而无需修改程序接口,这使程序维护起来更容易。

②控制对成员的访问

无论类成员是数据成员还是成员函数,都可以在类的公有部分或私有部分中声明它。但由于隐藏数据是OOP主要的目标之一,因此数据项通常放在私有部分,组成类接口的成员函数放在公有部分;否则,就无法从程序中调用这些函数。正如Stock声明所表明的,也可以把成员函数放在私有部分中。不能直接从程序中调用这种函数,但公有方法却可以使用它们。通常,程序员使用私有成员函数来处理不属于公有接口的实现细节。

class World {
    float mass; // private by default
    char name[20]; // private by default
public:
    void tellall(void);
    ...
};

然而,为强调数据隐藏的概念,本书显式地使用了private。

类描述看上去很像是包含成员函数以及public和private可见性标签的结构声明。实际上,C++对结构进行了扩展,使之具有与类相同的特性。它们之间唯一的区别是,结构的默认访问类型是public,而类为private。C++程序员通常使用类来实现类描述,而把结构限制为只表示纯粹的数据对象(常被称为普通老式数据(POD,Plain Old Data)结构)。

实现类成员函数

还需要创建类描述的第二部分:为那些由类声明中的原型表示的成员函数提供代码。成员函数定义与常规函数定义非常相似,它们有函数头和函数体,也可以有返回类型和参数。但是它们还有两个特殊的特征:

  • 定义成员函数时,使用作用域解析运算符(::)来标识函数所属的类;
  • 类方法可以访问类的private组件。
// stock00.cpp -- implementing the Stock class
#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';
}

①内联方法

定义位于类声明中的函数都将自动成为内联函数,因此Stock::set_tot( )是一个内联函数。类声明常将短小的成员函数作为内联函数,set_tot( )符合这样的要求。

如果愿意,也可以在类声明之外定义成员函数,并使其成为内联函数。为此,只需在类实现部分中定义函数时使用inline限定符即可:

class Stock {
private:
    ...
    void set_tot(); // 默认为内联函数
public:
    ...
};
inline void Stock::set_tot() { // 使用inline定义
    total_val = shares * shares_val;
}

内联函数的特殊规则要求在每个使用它们的文件中都对其进行定义。确保内联定义对多文件程序中的所有文件都可用的、最简便的方法是:将内联定义放在定义类的头文件(有些开发系统包含智能链接程序,允许将内联定义放在一个独立的实现文件)。

顺便说一句,根据改写规则(rewrite rule),在类声明中定义方法等同于用原型替换方法定义,然后在类声明的后面将定义改写为内联函数。也就是说,程序清单stock00.h中set_tot( )的内联定义与上述代码(定义紧跟在类声明之后)是等价的

②方法使用哪个对象

所创建的每个新对象都有自己的存储空间,用于存储其内部变量和类成员;但同一个类的所有对象共享同一组类方法,即每种方法只有一个副本。例如,假设kate和joe都是Stock对象,则kate.shares将占据一个内存块,而joe.shares占用另一个内存块,但kate.show()和joe.show()都调用同一个方法,也就是说,它们将执行同一个代码块,只是将这些代码用于不同的数据。在OOP中,调用成员函数被称为发送消息,因此将同样的消息发送给两个不同的对象将调用同一个方法,但该方法被用于两个不同的对象(参见下图)。

对象、数据和成员函数

使用类

// usestok0.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;
}

OOP程序员常依照客户/服务器模型来讨论程序设计。在这个概念中,客户是使用类的程序。类声明(包括类方法)构成了服务器,它是程序可以使用的资源。客户只能通过以公有方式定义的接口使用服务器,这意味着客户(客户程序员)唯一的责任是了解该接口。服务器(服务器设计人员)的责任是确保服务器根据该接口可靠并准确地执行。服务器设计人员只能修改类设计的实现细节,而不能修改接口。这样程序员独立地对客户和服务器进行改进,对服务器的修改不会对客户的行为造成意外的影响。

修改实现

在前面的程序输出中,可能有一个方面让您恼火——数字的格式不一致。现在可以改进实现,但保持接口不变。ostream类包含一些可用于控制格式的成员函数。这里不做太详细的探索,只需修改方法setf(),便可避免科学计数法

std::cout.setf(std::ios_base::fixed, std::ios_base::floatfield);

这设置了cout对象的一个标记,命令cout使用定点表示法。同样,下面的语句导致cout在使用定点表示法时,显示三位小数:

std::cout.precision(3);

可在方法show()中使用这些工具来控制格式,但还有一点需要考虑。修改方法的实现时,不应影响客户程序的其他部分。上述格式修改将一直有效,直到您再次修改,因此它们可能影响客户程序中的后续输出。因此,show()应重置格式信息,使其恢复到自己被调用前的状态。为此,可以像下面这样,使用返回的值:

std::streamsize precision = std::cout.precision(3); // 保存先前的精度值
...
std::cout.presion(precision); // 重置为旧值

// 保存原始标记
std::ios_base::fmtflags originflag = std::cout.set(std::ios_base::fixed);
...
// 重置为保存的值
std::cout.set(originflag, std::ios_base::floatfield);

fmtflags是在ios_base类中定义的一种类型,而ios_base类又是在名称空间std中定义的,因此originflag的类型名非常长。其次,originflag存储了所有的标记,而重置语句使用这些信息来重置floatfield,而floatfield包含定点表示法标记和科学表示法标记。第三,请不要过多考虑这里的细节。这里的要旨是,将修改限定在实现文件中,以免影响程序的其他方面。

根据上面的介绍,可在实现文件中将方法show()的定义修改成如下所示:

void Stock::show() {
	using std::cout;
	using std::ios_base;
	// 设置格式为 #.###
	ios_base::fmtflags orig = cout.setf(ios_base::fixed, ios_base::floatfield);
	std::streamsize prec = cout.precision(3);
	cout << "Company: " << company << " Shares: " << shares << '\n';
	cout << " Share Price:$" << share_val;
	// 设置格式为 #.##
	cout.precision(2);
	cout << " Total Worth:$" << total_val << '\n';
	// 重新存储原始格式
	cout.setf(orig, ios_base::floatfield);
	cout.precision(prec);
}

类的构造函数和析构函数

C++的目标之一是让使用类对象就像使用标准类型一样,然而,到现在为止,本章提供的代码还不能让您像初始化int或结构那样来初始化Stock对象。也就是说,常规的初始化语法不适用于类型Stock:

int year = 2001; // 无效的初始化(因为year是private的)
struct thing {
    char * pn;
    int m;
};
thing amabob = {"wodget", -23}; // 有效的初始化
Stock bot = {"Sukie's Autos, Inc", 200, 50.25}; // NO!编译错误

不能像上面这样初始化Stock对象的原因在于,数据部分的访问状态是私有的,这意味着程序不能直接访问数据成员。您已经看到,程序只能通过成员函数来访问数据成员,因此需要设计合适的成员函数,才能成功地将对象初始化(如果使数据成员成为公有,而不是私有,就可以按刚才介绍的方法初始化类对象,但使数据成为公有的违背了类的一个主要初衷:数据隐藏)。

一般来说,最好是在创建对象时对它进行初始化。例如,请看下面的代码:

Stock gift;
gift.buy(10, 24.75);

就Stock类当前的实现而言,gift对象的company成员是没有值的。类设计假设用户在调用任何其他成员函数之前调用acquire( ),但无法强加这种假设。避开这种问题的方法之一是在创建对象时,自动对它进行初始化。为此,C++提供了一个特殊的成员函数——类构造函数,专门用于构造新对象、将值赋给它们的数据成员。更准确地说,C++为这些成员函数提供了名称和使用语法,而程序员需要提供方法定义。名称与类名相同。例如,Stock类一个可能的构造函数是名为Stock( )的成员函数。构造函数的原型和函数头有一个有趣的特征——虽然没有返回值,但没有被声明为void类型。实际上,构造函数没有声明类型。

①声明和定义构造函数

// 构造函数原型,使用了默认的参数值
Stock(const string & co, long n = 0, double pr = 0.0);

下面是构造函数的一种可能定义:

// 构造函数定义
Stock::Stock(const string & co, long n, double pr) {
    company = co;
    if ( n < 0 ) {
        std::cerr << "Number of shares can't be negative;"
                    << company << " shares set to 0.\n";
        shares = 0;
    } else
        shares = n;
    share_val = pr;
    set_tot();
}

上述代码和本章前面的函数acquire()相同。区别在于,程序声明对象时,将自动调用构造函数。

不熟悉构造函数的您会试图将类成员名称用作构造函数的参数名,如下所示:

// NO!
Stock::Stock(const string & company, long shares, double share_val) {
    ...
}

这是错误的。构造函数的参数表示的不是类成员,而是赋给类成员的值。因此,参数名不能与类成员相同,否则最终的代码将是这样的:

shares = shares;

为避免这种混乱,一种常见的做法是在数据成员名中使用m_前缀:

class Stock {
private:
    string m_company;
    long m_shares;
    ...

另一种常见的做法是,在成员名中使用后缀_:

class Stock {
private:
    string company_;
    long shares_;
    ...

无论采用哪种做法,都可在公有接口中在参数名中包含company和shares。

②使用构造函数

C++提供了两种使用构造函数来初始化对象的方式。第一种方式是显式地调用构造函数:

Stock food = Stock("World", 250, 1.25);

这将food对象的company成员设置为字符串“World Cabbage”,将shares成员设置为250,依此类推。

另一种方式是隐式地调用构造函数:

Stock garment("Furry Mason", 50, 2.5); // 隐式调用,格式更紧凑

<=> Stock garment = Stock("Furry Mason", 50, 2.5); // 等价的显示调用

如果有个类StringBad,其有构造函数StringBad::StringBad(const char* s) {...},则看到如下代码:

StringBad headline1("Celery Stalks at Midnight");

就不要有迷惑了,其中headline1是一个StringBad实例。

每次创建类对象(甚至使用new动态分配内存)时,C++都使用类构造函数。下面是将构造函数与new一起使用的方法:

Stock *pstock = new Stock("Electroshock Games" , 18, 19.0);

这条语句创建一个Stock对象,将其初始化为参数提供的值,并将该对象的地址赋给pstock指针。在这种情况下,对象没有名称,但可以使用指针来管理该对象。我们将在第11章进一步讨论对象指针。

构造函数的使用方式不同于其他类方法。一般来说,使用对象来调用方法:

stock1.show(); // stock1对象调用show()方法

但无法使用对象来调用构造函数,因为在构造函数构造出对象之前,对象是不存在的。因此构造函数被用来创建对象,而不能通过对象来调用。

③默认构造函数

默认构造函数是在未提供显式初始值时,用来创建对象的构造函数。也就是说,它是用于下面这种声明的构造函数:

Stock fluffy_the_cat; // 使用默认构造器

这条语句管用的原因在于,如果没有提供任何构造函数,则C++将自动提供默认构造函数。它是默认构造函数的隐式版本,不做任何工作。对于Stock类来说,默认构造函数可能如下:

Stock::Stock() {};

当且仅当没有定义任何构造函数时,编译器才会提供默认构造函数。为类定义了构造函数后,程序员就必须为它提供默认构造函数。如果提供了非默认构造函数(如Stock(const char * co, int n, double pr)),但没有提供默认构造函数,则下面的声明将出错:

Stock stock1; ← 因为已定义构造函数,没法与当前的构造函数匹配!!!

这样做的原因可能是想禁止创建未初始化的对象。然而,如果要创建对象,而不显式地初始化,则必须定义一个不接受任何参数的默认构造函数。定义默认构造函数的方式有两种。一种是给已有构造函数的所有参数提供默认值:

Stock(const string & co = "Error", int n = 0, double pr = 0.0);

另一种方式是通过函数重载来定义另一个构造函数——一个没有参数的构造函数:

Stock();

由于只能有一个默认构造函数,因此不要同时采用这两种方式。实际上,通常应初始化所有的对象,以确保所有成员一开始就有已知的合理值。因此,用户定义的默认构造函数通常给所有成员提供隐式初始值。例如,下面是为Stock类定义的一个默认构造函数:

Stock::Stock() { // 默认构造函数
    company = "no name";
    shares = 0;
    share_val = 0.0;
    total_val = 0.0;
}

在设计类时,通常应提供对所有类成员做隐式初始化的默认构造函数。

使用上述任何一种方式(没有参数或所有参数都有默认值)创建了默认构造函数后,便可以声明对象变量,而不对它们进行显式初始化:

Stock first; // 隐式地调用默认构造函数
Stock first = Stock(); // 显示地调用
Stock *prelief = new Stock; // 隐式地调用

然而,不要被非默认构造函数的隐式形式所误导:

Stock first("Concrete Comglomerate"); // 调用构造函数
Stock second(); // 声明一个方法
Stock third; // 调用默认构造函数

第一个声明调用非默认构造函数,即接受参数的构造函数;第二个声明指出,second()是一个返回Stock对象的函数。隐式地调用默认构造函数时,不要使用圆括号。

④析构函数

用构造函数创建对象后,程序负责跟踪该对象,直到其过期为止。对象过期时,程序将自动调用一个特殊的成员函数,该函数的名称令人生畏——析构函数。析构函数完成清理工作,因此实际上很有用。例如,如果构造函数使用new来分配内存,则析构函数将使用delete来释放这些内存。Stock的构造函数没有使用new,因此析构函数实际上没有需要完成的任务。在这种情况下,只需让编译器生成一个什么要不做的隐式析构函数即可,Stock类第一版正是这样做的。然而,了解如何声明和定义析构函数是绝对必要的,下面为Stock类提供一个析构函数。

和构造函数一样,析构函数的名称也很特殊:在类名前加上~。因此,Stock类的析构函数为~Stock( )。另外,和构造函数一样,析构函数也可以没有返回值和声明类型。与构造函数不同的是,析构函数没有参数,因此Stock析构函数的原型必须是这样的:

~Stock();

由于Stock的析构函数不承担任何重要的工作,因此可以将它编写为不执行任何操作的函数:

Stock::~Stock() {}

然而,为让您能看出析构函数何时被调用,这样编写其代码:

Stock::~Stock() { // 类析构函数
    cout << "Bye, " << company << "!\n";
}

什么时候应调用析构函数呢?这由编译器决定,通常不应在代码中显式地调用析构函数(有关例外情形,请参阅第12章的“再谈定位new运算符”)。如果创建的是静态存储类对象,则其析构函数将在程序结束时自动被调用。如果创建的是自动存储类对象(就像前面的示例中那样),则其析构函数将在程序执行完代码块时(该对象是在其中定义的)自动被调用。如果对象是通过new创建的,则它将驻留在栈内存或自由存储区中,当使用delete来释放内存时,析构函数将自动被调用。最后,程序可以创建临时对象来完成特定的操作,在这种情况下,程序将在结束对该对象的使用时自动调用析构函数

由于在类对象过期时析构函数将自动被调用,因此必须有一个析构函数。如果程序员没有提供析构函数,编译器将隐式地声明一个默认析构函数,并在发现导致对象被删除的代码后,提供默认析构函数的定义。

⑤改进Stock类

// stock10.h ?Stock class declaration with constructors, destructor added
#ifndef STOCK1_H_
#define STOCK1_H_
#include <string>
class Stock {
private:
	std::string company;
	long shares;
	double share_val;
	double total_val;
	void set_tot() {
		total_val = shares * share_val;
	}
public:
	Stock();        // default constructor
	Stock(const std::string& co, long n = 0, double pr = 0.0);
	~Stock();       // noisy destructor
	void buy(long num, double price);
	void sell(long num, double price);
	void update(double price);
	void show();
};
#endif
// stock1.cpp ?Stock class implementation with constructors, destructor added
#include <iostream>
#include "stock10.h"
// constructors (verbose versions)
Stock::Stock() {      // default constructor
	std::cout << "Default constructor called\n";
	company = "no name";
	shares = 0;
	share_val = 0.0;
	total_val = 0.0;
}
Stock::Stock(const std::string& co, long n, double pr) {
	std::cout << "Constructor using " << co << " called\n";
	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();
}
// class destructor
Stock::~Stock() {     // verbose class destructor
	std::cout << "Bye, " << company << "!\n";
}
// other methods
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() {
	using std::cout;
	using std::ios_base;
	// set format to #.###
	ios_base::fmtflags orig =
		cout.setf(ios_base::fixed, ios_base::floatfield);
	std::streamsize prec = cout.precision(3);
	cout << "Company: " << company
		<< "  Shares: " << shares << '\n';
	cout << "  Share Price: $" << share_val;
	// set format to #.##
	cout.precision(2);
	cout << "  Total Worth: $" << total_val << '\n';
	// restore original format
	cout.setf(orig, ios_base::floatfield);
	cout.precision(prec);
}
// usestok1.cpp -- using the Stock class
// compile with stock10.cpp
#include <iostream>
#include "stock10.h"
int main() {
	{
		using std::cout;
		cout << "Using constructors to create new objects\n";
		Stock stock1("NanoSmart", 12, 20.0);            // syntax 1
		stock1.show();
		Stock stock2 = Stock("Boffo Objects", 2, 2.0); // syntax 2
		stock2.show();

		cout << "Assigning stock1 to stock2:\n";
		stock2 = stock1;
		cout << "Listing stock1 and stock2:\n";
		stock1.show();
		stock2.show();

		cout << "Using a constructor to reset an object\n";
		stock1 = Stock("Nifty Foods", 10, 50.0);    // temp object
		cout << "Revised stock1:\n";
		stock1.show();
		cout << "Done\n";
	}
	return 0;
}
Using constructors to create new objects
Constructor using NanoSmart called
Company: NanoSmart  Shares: 12
  Share Price: $20.000  Total Worth: $240.00
Constructor using Boffo Objects called
Company: Boffo Objects  Shares: 2
  Share Price: $2.000  Total Worth: $4.00
Assigning stock1 to stock2:
Listing stock1 and stock2:
Company: NanoSmart  Shares: 12
  Share Price: $20.000  Total Worth: $240.00
Company: NanoSmart  Shares: 12
  Share Price: $20.000  Total Worth: $240.00
Using a constructor to reset an object
Constructor using Nifty Foods called
Bye, Nifty Foods!
Revised stock1:
Company: Nifty Foods  Shares: 10
  Share Price: $50.000  Total Worth: $500.00
Done
Bye, NanoSmart!
Bye, Nifty Foods!

 在程序清单中,main()的开头和末尾多了一个大括号。诸如stock1和stock2等自动变量将在程序退出其定义所属代码块时消失。如果没有这些大括号,代码块将为整个main(),因此仅当main()执行完毕后,才会调用析构函数。在窗口环境中,这意味着将在两个析构函数调用前关闭,导致您无法看到最后两条消息。但添加这些大括号后,最后两个析构函数调用将在到达返回语句前执行,从而显示相应的消息。

下面的语句表明可以将一个对象赋给同类型的另一个对象:

stock2 = stock1;

与给结构赋值一样,在默认情况下,给类对象赋值时,将把一个对象的成员复制给另一个。在这个例子中,stock2原来的内容将被覆盖。

允许调用构造函数来创建一个临时对象,然后将该临时对象复制到stock2中,并丢弃它。如果编译器使用的是这种方式,则将为临时对象调用析构函数,因此生成下面的输出:

可以看到“Bye,Nifty Foods!”还未等{}中程序结束就调用了析构函数,即表明stock1对象已过期,解构函数将其解构了。但stock1.show()在调用析构函数后仍然正常调用了。为何stock2没有提前被解构呢?

构造函数不仅仅可用于初始化新对象。例如,该程序的main()中包含下面的语句:

stock1 = Stock("Nifty Foods", 10, 50.0); 

stock1对象已经存在,因此这条语句不是对stock1进行初始化,而是将新值赋给它。这是通过让构造程序创建一个新的、临时的对象,然后将其内容复制给stock1来实现的。随后程序调用析构函数,以删除该临时对象,如下面经过注释后的输出所示:

Using a constructor to reset an object
Constructor using Nifty Foods called
Bye, Nifty Foods!
Revised stock1:
Company: Nifty Foods  Shares: 10
  Share Price: $50.000  Total Worth: $500.00

有些编译器可能要过一段时间才删除临时对象,因此析构函数的调用将延迟。

最后,程序显示了下面的内容:

Done
Bye, NanoSmart!
Bye, Nifty Foods!

函数main( )结束时,其局部变量(stock1和stock2)将消失。由于这种自动变量被放在栈中,因此最后创建的对象将最先被删除,最先创建的对象将最后被删除(“NanoSmart”最初位于stock1中,但随后被传输到stock2中,然后stock1被重置为“Nifty Food”)。

输出表明,下面两条语句有根本性的差别

Stock stock2 = Stock("Boffo Object", 2, 2.0); ←← 直接创建对象。
stock1 = Stock("Niffy Foods", 10, 50.0); ←← Stock("Niffy"...);是个临时对象!!!

第一条语句是初始化,它创建有指定值的对象,可能会创建临时对象(也可能不会);第二条语句是赋值。像这样在赋值语句中使用构造函数总会导致在赋值前创建一个临时对象临时对象其实也很好理解,stock1被赋值(不是初始化)一个匿名的对象,这个匿名对象被赋值给stock1后,当然就没用了,就解构了。

如果既可以通过初始化,也可以通过赋值来设置对象的值,则应采用初始化方式。通常这种方式的效率更高。

⅟  C++11列表初始化  

在C++11中,可将列表初始化语法用于类吗?可以,只要提供与某个构造函数的参数列表匹配的内容,并用大括号将它们括起:

Stock hot_tip = {"Derivatives Plus Plus", 100, 45.0};
Stock jock {"Sport Age Storage, Inc"};
Stock temp {};←有创建临时对象(默认构造函数)temp

在前两个声明中,用大括号括起的列表与下面的构造函数匹配:

Stock::Stock(const std::string & co, long n = 0, double pr = 0.0);

因此,将使用该构造函数来创建这两个对象。创建对象jock时,第二和第三个参数将为默认值0和0.0。第三个声明与默认构造函数匹配,因此将使用该构造函数创建对象temp。

另外,C++11还提供了名为std::initialize_list的类,可将其用作函数参数或方法参数的类型。这个类可表示任意长度的列表,只要所有列表项的类型都相同或可转换为相同的类型,这将在第16章介绍。

 ½ const成员函数

const Stock land = Stock("Kludgehorn Properties");
land.show();←编译器拒绝

对于当前的C++来说,编译器将拒绝第二行。这是什么原因呢?因为show( )的代码无法确保调用对象不被修改——调用对象和const一样,不应被修改。我们以前通过将函数参数声明为const引用或指向const的指针来解决这种问题。但这里存在语法问题:show( )方法没有任何参数。相反,它所使用的对象是由方法调用隐式地提供的。需要一种新的语法——保证函数不会修改调用对象。C++的解决方法是将const关键字放在函数的括号后面。也就是说,show()声明应像这样:

void show() const; // 不允许改变【调用对象】

同样,函数定义的开头应像这样:

void Stock::show() const {...}; // 不允许修改调用对象

以这种方式声明和定义的类函数被称为const成员函数。就像应尽可能将const引用和指针用作函数形参一样,只要类方法不修改调用对象,就应将其声明为const从现在开始,我们将遵守这一规则。

❼构造函数和析构函数小结

对于未被初始化的对象,程序将使用默认构造函数来创建:

Bozo bubi; // 使用默认构造函数
Bozo *pd = new Bozo; // 使用默认构造函数

就像对象被创建时程序将调用构造函数一样,当对象被删除时,程序将调用析构函数。每个类都只能有一个析构函数。析构函数没有返回类型(连void都没有),也没有参数,其名称为类名称前加上~。例如,Bozo类的析构函数的原型如下:

~Bozo(); // 类析构函数

 如果构造函数使用了new,则必须提供使用delete的析构函数。

this指针

在头文件包含如下函数原型:

const Stock& topval(const Stock& s) const;

假设要对Stock对象stock1和stock2进行比较,并将其中股价总值较高的那一个赋给top对象,则可以使用下面两条语句之一:

top = stock1.topval(stock2);
top = stock2.topval(stock1);

第一种格式隐式地访问stock1,而显式地访问stock2;第二种格式显式地访问stock1,而隐式地访问stock2(参见下图)。无论使用哪一种方式,都将对这两个对象进行比较,并返回股价总值较高的那一个对象。

要注意的是topval()的实现,它将引发一个小问题。下面的部分实现强调了这个问题:

const Stock & Stock::topval(const Stock & s) const {
    if (s.total_val > total_val)
        return s; // 参数对象
    else
        return ?????; // 调用的对象
}

其中,s.total_val是作为参数传递的对象的总值,total_val是用来调用该方法的对象的总值。如果s.total_val大于toatl_val,则函数将返回指向s的引用;否则,将返回用来调用该方法的对象(在OOP中,是topval消息要发送给的对象)。问题在于,如何称呼这个对象?如果调用stock1.topval(stock2),则s是stock2的引用(即stock2的别名),但stock1没有别名。

 C++解决这种问题的方法是:使用被称为this的特殊指针。this指针指向用来调用成员函数的对象(this被作为隐藏参数传递给方法)。这样,函数调用stock1.topval(stock2)将this设置为stock1对象的地址,使得这个指针可用于topval( )方法。同样,函数调用stock2.topval(stock1)将this设置为stock2对象的地址。一般来说,所有的类方法都将this指针设置为调用它的对象的地址。确实,topval()中的total_val只不过是this->total_val的简写。

每个成员函数(包括构造函数和析构函数)都有一个this指针。this指针指向调用对象。如果方法需要引用整个调用对象,则可以使用表达式*this。在函数的括号后面使用const限定符将this限定为const,这样将不能使用this来修改对象的值。

然而,要返回的并不是this,因为this是对象的地址,而是对象本身,即*this(将解除引用运算符*用于指针,将得到指针指向的值)。现在,可以将*this作为调用对象的别名来完成前面的方法定义。

const Stock & Stock::topval(const Stock & s) const {
    if (s.total_val > total_val)
        return s; // 参数对象
    else
        return *this; // 调用的对象
}

返回类型为引用意味着返回的是调用对象本身,而不是其副本

对象数组

Stock mystuff[4]; // 创建一个包含4个元素的Stock对象数组
mystuff[0].update(); // 第1个元素调用update()
mystuff[3].update(); // 第4个元素调用show()
const Stock * tops = mystuff[2].topval(mystuff[1]);
// 比较第2和第3个元素,并设置tops指向有更大total_val值的对象

可以用构造函数来初始化数组元素。在这种情况下,必须为每个元素调用构造函数:

const int STKS = 4;
Stock stocks[STKS] = {
    Stock("NanoSmart", 12.5, 20),
    Stock("Boffo Objects", 200, 2.0),
    Stock("Monolithic Obelisks", 130, 3.25),
    Stock("Fleep Enterprise", 60, 6.5)
};

如果类包含多个构造函数,则可以对不同的元素使用不同的构造函数:

const int STKS = 10;
Stock stocks[STKS] = {
    Stock("NanoSmart", 12.5, 10),
    Stock();
    Stock("Monolithic Obelisks", 130, 3.25)
};

上述代码使用Stock(const string & co, long n, double pr)初始化stock[0]和stock[2],使用构造函数Stock()初始化stock[1]。由于该声明只初始化了数组的部分元素,因此余下的7个元素将使用默认构造函数进行初始化

初始化对象数组的方案是,首先使用默认构造函数创建数组元素,然后花括号中的构造函数将创建临时对象,然后将临时对象的内容复制到相应的元素中。因此,要创建类对象数组,则这个类必须有默认构造函数。

// stock20.h -- augmented version
#ifndef STOCK20_H_
#define STOCK20_H_
#include <string>
class Stock {
private:
	std::string company;
	int shares;
	double share_val;
	double total_val;
	void set_tot() {
		total_val = shares * share_val;
	}
public:
	Stock();        // default constructor
	Stock(const std::string& co, long n = 0, double pr = 0.0);
	~Stock();       // do-nothing destructor
	void buy(long num, double price);
	void sell(long num, double price);
	void update(double price);
	void show()const;
	const Stock& topval(const Stock& s) const;
};
#endif
// stock20.cpp -- augmented version
#include <iostream>
#include "stock20.h"
using namespace std;
// constructors
Stock::Stock() {      // default constructor
	shares = 0;
	share_val = 0.0;
	total_val = 0.0;
}
Stock::Stock(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();
}
// class destructor
Stock::~Stock() {}        // quiet class destructor
// other methods
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() const {
	using std::cout;
	using std::ios_base;
	// set format to #.###
	ios_base::fmtflags orig =
		cout.setf(ios_base::fixed, ios_base::floatfield);
	std::streamsize prec = cout.precision(3);
	cout << "Company: " << company
		<< "  Shares: " << shares << '\n';
	cout << "  Share Price: $" << share_val;
	// set format to #.##
	cout.precision(2);
	cout << "  Total Worth: $" << total_val << '\n';
	// restore original format
	cout.setf(orig, ios_base::floatfield);
	cout.precision(prec);
}
const Stock& Stock::topval(const Stock& s) const {
	if (s.total_val > total_val)
		return s;
	else
		return *this;
}
// usestok2.cpp -- using the Stock class
// compile with stock20.cpp
#include <iostream>
#include "stock20.h"
const int STKS = 4;
int main() {
	{
		// create an array of initialized objects
		Stock stocks[STKS] = {
			Stock("NanoSmart", 12, 20.0),
			Stock("Boffo Objects", 200, 2.0),
			Stock("Monolithic Obelisks", 130, 3.25),
			Stock("Fleep Enterprises", 60, 6.5)
		};
		std::cout << "Stock holdings:\n";
		int st;
		for (st = 0; st < STKS; st++)
			stocks[st].show();
		// set pointer to first element
		const Stock* top = &stocks[0];
		for (st = 1; st < STKS; st++)
			top = &top->topval(stocks[st]);
		// now top points to the most valuable holding
		std::cout << "\nMost valuable holding:\n";
		top->show();
	}
	return 0;
}
Stock holdings:
Company: NanoSmart  Shares: 12
  Share Price: $20.000  Total Worth: $240.00
Company: Boffo Objects  Shares: 200
  Share Price: $2.000  Total Worth: $400.00
Company: Monolithic Obelisks  Shares: 130
  Share Price: $3.250  Total Worth: $422.50
Company: Fleep Enterprises  Shares: 60
  Share Price: $6.500  Total Worth: $390.00

Most valuable holding:
Company: Monolithic Obelisks  Shares: 130
  Share Price: $3.250  Total Worth: $422.50

注意:top = &top->topval(stocks[st]); <=> top = &(top->topval(stocks[st]); 因为运算符优先级如下:

知道this指针就可以更深入了解C++的工作方式。例如,最初的UNIX实现使用C++前端cfront将C++程序转换为C程序。处理方法的定义时,只需将下面这样的C++方法定义:

void Stock::show() const {
	cout << "Company: " << company
            << "  Shares: " << shares << '\n';
	    << "  Share Price: $" << share_val;
	    << "  Total Worth: $" << total_val << '\n';
}

转换为下面这样的C-风格定义:

void show(const Stock * this) const {
	cout << "Company: " << this->company
            << "  Shares: " << this->shares << '\n';
	    << "  Share Price: $" << this->share_val;
	    << "  Total Worth: $" << this->total_val << '\n';
}

即将Stock::限定符转换为函数参数(指向Stock的指针),然后用这个指针来访问类成员。

同样,该前端将下面的函数调用转换:

top.show();
转换为:
show(&top);
这样,将调用对象的地址赋给了this指针(实际情况可能更复杂些)。

类作用域

C++类引入了一种新的作用域:类作用域。

在类中定义的名称(如类数据成员名和类成员函数名)的作用域都为整个类,作用域为整个类的名称只在该类中是已知的,在类外是不可知的。因此,可以在不同类中使用相同的类成员名而不会引起冲突。例如,Stock类的shares成员不同于JobRide类的shares成员。另外,类作用域意味着不能从外部直接访问类的成员,公有成员函数也是如此。也就是说,要调用公有成员函数,必须通过对象:

Stock sleeper("Exclusive Ore", 100, 0.25); //创建一个对象
sleeper.show(); // 使用对象调用成员方法
show(); // 无效!不能直接调用方法

同样,在定义成员函数时,必须使用作用域解析运算符:

void Stock::update(double price) {
    ...
}

总之,在类声明或成员函数定义中,可以使用未修饰的成员名称(未限定的名称),就像sell()调用set_tot()成员函数时那样。构造函数名称在被调用时,才能被识别,因为它的名称与类名相同。在其他情况下,使用类成员名时,必须根据上下文使用直接成员运算符(.)、间接成员运算符(->)或作用域解析运算符(::)。下面的代码片段演示了如何访问具有类作用域的标识符:

class Ik {
private:
	int fuss; // fuss 有类作用域
public:
	Ik(int f = 9) {
		fuss = f; // fuss 在此作用域
	}
	void ViewTk() const; // ViewTk 有类作用域
};
void Ik::ViewTk() const { // Ik:: 将ViewIk放到Ik所在类作用域
	cout << fuss << endl;
}
int main() {
	Ik* pik = new Ik;
	Ik ee = Ik(8); // 构造函数在类作用域因为有类名
	ee.viewIk(); // 类对象将ViewIk放进类作用域
	pik->ViewIk(); // 指向Ik的指针将ViewIk带入类作用域
}

❶作用域为类常量

有时候,使符号常量的作用域为类很有用。例如,类声明可能使用字面值30来指定数组的长度,由于该常量对于所有对象来说都是相同的,因此创建一个由所有对象共享的常量是个不错的主意。您可能以为这样做可行:

class Bakery {
private:
    const int Months = 12; ← 声明一个常量?错误!
    double costs[Months];
    ...

但这是行不通的,因为声明类只是描述了对象的形式,并没有创建对象。因此,在创建对象前,将没有用于存储值的空间(实际上,C++11提供了成员初始化,但不适用于前述数组声明,第12章将介绍该主题)。然而,有两种方式可以实现这个目标,并且效果相同。

第一种方式是在类中声明一个枚举。在类声明中声明的枚举的作用域为整个类,因此可以用枚举为整型常量提供作用域为整个类的符号名称。

class Bakery {
private:
    emun {Months = 12};
    double costs[Months];
    ...

注意,用这种方式声明枚举并不会创建类数据成员。也就是说,所有对象中都不包含枚举。另外,Months只是一个符号名称,在作用域为整个类的代码中遇到它时,编译器将用30来替换它。

由于这里使用枚举只是为了创建符号常量,并不打算创建枚举类型的变量,因此不需要提供枚举名。顺便说一句,在很多实现中,ios_base类在其公有部分中完成了类似的工作,诸如ios_base::fixed等标识符就来自这里。其中,fixed是ios_base类中定义的典型的枚举量。

C++提供了另一种在类中定义常量的方式——使用关键字static:

class Bakery {
private:
    static const int Months = 12;
    double costs[Months];
    ...

这将创建一个名为Months的常量,该常量将与其他静态变量存储在一起,而不是存储在对象中。因此,只有一个Months常量,被所有Bakery对象共享。第12章将深入介绍静态类成员。在C++98中,只能使用这种技术声明值为整数或枚举的静态常量,而不能存储double常量。C++11消除了这种限制。

❷作用域内枚举(C++11)

enum egg {Small, Medium, Large, Jumbo};
enum t_shirt {Small, Medium, Large, Xlarge};

这将无法通过编译,因为egg Small和t_shirt Small位于相同的作用域内,它们将发生冲突。为避免这种问题,C++11提供了一种新枚举,其枚举量的作用域为类。这种枚举的声明类似于下面这样:

enum class egg {Small, Medium, Large, Jumbo};
enum class t_shirt {Small, Medium, Large, Xlarge};

也可使用关键字struct代替class。无论使用哪种方式,都需要使用枚举名来限定枚举量:

egg choice = egg::Large;
t_shirt Floyd = t_shirt::Large;

枚举量的作用域为类后,不同枚举定义中的枚举量就不会发生名称冲突了,而您可继续编写处理鸡蛋和T恤的项目。

C++11还提高了作用域内枚举的类型安全。在有些情况下,常规枚举将自动转换为整型,如将其赋给int变量或用于比较表达式时,但作用域内枚举不能隐式地转换为整型

enum egg_old {Small, Medium, Large, Jumbo}; // 无作用域
enum class t_shirt {Small, Medium, Large, XLarge}; // 类作用域
egg_old one = Medium; // 无作用域
t_shirt rolf = t_shirt::Large; // 类作用域
int king = one; // 隐式地对于无作用域的类型转换
int ring = rolf;// 不允许!不能隐式类型转换
if (king < Jumbo) // 允许
    cout << "Jumbo converted to int before comparision.\n";
if (king < t_shirt::Medium) // 不允许!
    cout << "Not allowed: < not defined for scoped enum.\n";

 但在必要时,可进行显式类型转换(强转)

int Frodo = int(t_shirt::Small); // Frodo设置为0

枚举用某种底层整型类型表示,在C++98中,如何选择取决于实现,因此包含枚举的结构的长度可能随系统而异。对于作用域内枚举,C++11消除了这种依赖性。默认情况下,C++11作用域内枚举的底层类型为int。另外,还提供了一种语法,可用于做出不同的选择:

enum class : short pizza {Small, Medium, Large, XLarge}; // 把底层类型置为short类型应用于pizza

:short将底层类型指定为short。底层类型必须为整型。在C++11中,也可使用这种语法来指定常规枚举的底层类型,但如果没有指定,编译器选择的底层类型将随实现而异。

抽象数据类型

Stock类非常具体。然而,程序员常常通过定义类来表示更通用的概念。例如,就实现计算机专家们所说的抽象数据类型(abstract data type,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
// 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;
}
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
|A
Enter a PO number to add: |17885
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
|P
PO #17885 popped
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
|A
Enter a PO number to add: |17965
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
|A
Enter a PO number to add: |18002
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
|P
PO #18002 popped
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
|P
PO #17965 popped
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
|P
stack already empty
Please enter A to add a purchase order,
P to process a PO, or Q to quit.
|Q
Bye

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

itzyjr

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值