这一章在干嘛?

前面九章我们一直在”面向过程”地写代码:先想清楚步骤,再考虑数据放哪。这一章开始换脑子——先想清楚”我要管理的是什么东西”,再决定它能做什么操作。C++ 用**类(class)**把数据和操作打包在一起,这就是面向对象编程(OOP)的核心工具。本章从零设计一个股票类 Stock,讲透类声明、访问控制、构造函数/析构函数、const 成员函数和 this 指针——这是全书 OOP 三部曲的第一章。

10.1 过程性编程和面向对象编程

10.2 抽象和类

10.3 第一次类声明:stock00.h

10.4 实现类成员函数

10.5 使用类与客户/服务器模型

10.6 构造函数和析构函数

10.7 const 成员函数

10.8 this 指针

10.9 对象数组

10.10 类作用域与类内常量

10.11 抽象数据类型:以 Stack 为例

10.1 过程性编程和面向对象编程

先看一个思维实验:假设你要给垒球队写一个统计程序。

过程性思维(C 语言式)先问:“我要执行哪些步骤?“——输入数据、计算、更新、显示,于是设计一堆函数,再回头想数据怎么放:用几个数组?还是设计一个结构体装下每个球员的信息?

面向对象思维(OOP 式)先问:“我要管理的是什么东西?“——是”球员”这个整体。于是设计一个表示球员的对象,包含名字和所有统计数据,再为它配套方法:录入、计算(比如自动算打击率)、更新、显示。用户与数据的交互方式(初始化、更新、报表)就是这个对象的用户接口

一句话:过程性编程先想”做什么步骤”,OOP 先想”数据是什么、怎么被使用”,接口定了之后才考虑实现和存储。OOP 强调的几个特性:抽象封装与数据隐藏多态继承代码可复用性。类正是实现这一切的 C++ 载体——本章讲前三者的地基,多态(运算符重载)在第 11 章,继承在第 13 章。

10.2 抽象和类

抽象是应对复杂度的手段:你是几万亿个原子的集合,但你更愿意把自己当作”一个人”来处理。计算中的抽象,就是只提取信息与用户交互的关键操作特征,忽略实现细节。

什么是类型?

别把类型只理解成”占多少内存”。指定一个基本类型其实做了三件事:

  1. 决定数据对象需要多少内存;
  2. 决定内存中的比特如何解释(long 和 float 可能都是 32 位,但解释方式完全不同);
  3. 决定能对它执行哪些操作(int 能加减乘除取模,指针不行)。

所以定义自己的类型(类)时,你也必须提供同样的信息:数据表示 + 允许的操作。

类规约的两部分

一个类的设计通常分两部分,实践上放在两个文件里:

  • 类声明:把数据写成数据成员(data members),把操作写成成员函数(member functions,也叫方法 methods)——放在头文件中,构成公共接口
  • 类方法定义:描述这些成员函数具体怎么实现——放在源代码文件中。

打个比方:类声明像房子的设计图纸,方法定义是施工细节,对象是按图纸造出来的一间间房子。买房子的人只需要知道门在哪、开关在哪(接口),不需要懂水电布线(实现)。

10.3 第一次类声明:stock00.h

以股票为例,先做简化抽象:支持”买入、增持、卖出、更新股价、显示持仓”五种操作,数据上保存公司名、持股数、每股价格、总市值。

// stock00.h -- Stock class interface
// version 00
#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();
}; // note semicolon at the end
 
#endif

有了这份声明,就可以像使用内置类型一样声明对象(也叫实例):

Stock sally;
Stock solly;

访问控制:private 与 public

这是本章最重要的概念之一:

  • public 部分任何使用该类的程序都可以直接访问——它们构成公共接口;
  • private 部分只能通过公共成员函数(或第 11 章的友元函数)访问——这就是数据隐藏(data hiding)

比如外界想改 shares,唯一的途径是调用 buy()sell() 这样的方法,而这些方法里可以加检查(不许买负数股、不许卖超过持有量)。数据被”隔离”起来,完整性就有了保障。**封装(encapsulation)**指把实现细节与抽象分开:数据放 private 是封装,把 set_tot() 这样的辅助函数藏进 private 也是封装,把方法定义放进单独的源文件还是封装。


▲ 图 10.1 Stock 类:private 成员只能经由 public 接口访问(数据隐藏),public 方法构成抽象接口

两个实用细节:

  • class 默认访问级别就是 private,所以 private 关键字可省略,但显式写出来更能强调数据隐藏的意图;
  • 结构体(struct)在 C++ 中其实和 class 几乎一样,唯一区别是struct 默认 public,class 默认 private。惯例:class 用于完整的类设计,struct 用于纯数据对象(POD)。

常见坑:忘了类声明结尾的分号

类声明的大括号后面必须跟一个分号:};。漏掉它,编译器会在你意想不到的下一处代码报错,新手往往排查半天。养成写完类立刻补分号的习惯。

10.4 实现类成员函数

成员函数定义和普通函数类似,但有两个特殊之处:

第一,用作用域解析运算符 :: 标明归属。方法定义写成 void Stock::update(double price) { share_val = price; set_tot(); } 这样的形式,Stock::update()限定名(qualified name),光写 update() 是只能在类作用域里用的缩写。这样不同的类可以有同名方法互不冲突(Buffoon::update() 是另一回事)。

**第二,方法可以直接访问类的私有成员。**比如 show() 里直接用 companyshares;如果你在类外的普通函数里这么干,编译器会直接拦下你。

完整的实现文件如下(acquire() 负责初次建仓,逻辑与 10.6 的构造函数几乎相同,此处省略;重点是 buy/sell 里的安全检查):

// stock00.cpp -- implementing the Stock class
// version 00
#include <iostream>
#include "stock00.h"
 
// acquire() 负责初次建仓,逻辑与 10.6 的构造函数几乎相同,此处省略
 
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::show()   // 打印 company、shares、share_val、total_val(改进版见 10.5)
{ /* ... */ }

注意 set_tot() 被四个方法复用:计算逻辑只写一遍,要改只改一处。因为它只是实现细节、不属于接口,所以放进 private——私有成员函数是”给自己人用”的工具函数

内联方法

类声明内部直接给出定义的函数自动成为内联函数(如 set_tot())。想把定义放在类外又想要内联:类声明中只写原型 void set_tot();,实现处写 inline void Stock::set_tot() { total_val = shares * share_val; },且通常放进头文件——内联函数要求在每个使用它的文件中都有定义。

方法到底作用于哪个对象?

Stock kate, joe;
kate.show(); // kate 对象调用 show()
joe.show();  // joe 对象调用 show()

kate.show() 中,方法把 shares 解释为 kate.shares。每个对象拥有自己的一份数据成员,但同一个类的所有对象共享同一套方法的同一份代码,只是把代码套用在不同数据上。OOP 术语把这叫”向对象发送消息”。


▲ 图 10.2 对象、数据与成员函数:数据各存一份,方法全体共享

10.5 使用类与客户/服务器模型

写个客户端程序试试水:

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

程序输出中注意两点:卖 400 股时被拒绝(You can't sell more than you have! Transaction is aborted.),说明安全检查生效;数字格式有些不统一($1.20389e+007 这种科学计数法),10.5 马上修复它。

OOP 常用客户/服务器模型描述设计:使用类的程序是客户,类声明及其方法是服务器。客户只需了解公共接口,服务器保证按接口可靠工作;服务器改进实现时不改动接口,双方即可独立演进。一个例子:嫌输出格式难看,可在 show() 里用 cout.setf(ios_base::fixed, ...) 固定小数点格式、cout.precision(3) 控制精度,礼貌的做法是修改前先保存原状(fmtflags orig = cout.setf(...)streamsize prec = cout.precision(3))、输出完恢复原值,避免污染客户端后续输出。改完输出变成整齐的 Share Price: $12.500 Total Worth: $250.00——接口没变,实现升级了。

10.6 构造函数和析构函数

上面的版本有个尴尬:数据是 private 的,Stock hot = {..., 200, 50.25}; 这种初始化直接编译错误——外界没法触碰私有数据;而且 Stock gift; gift.buy(10, 24.75); 会在未初始化的 company 上操作。解法就是让对象诞生时自动被初始化:构造函数。

构造函数

  • 与类同名:Stock 类的构造函数叫 Stock()
  • 没有声明返回类型(连 void 都不是);
  • 参数用来给数据成员赋值,可以带默认参数;
  • 程序声明对象时自动调用,不能由对象来”调用”构造函数——因为对象还没造出来呢。
// constructor prototype with some default arguments
Stock(const string & co, long n = 0, double pr = 0.0);
 
// constructor definition
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();
}

使用方式有显式与隐式两种,还可以配合 new:

Stock food = Stock("World Cabbage", 250, 1.25); // 显式
Stock garment("Furry Mason", 50, 2.5);          // 隐式(等价写法)
Stock *pstock = new Stock("Electroshock Games", 18, 19.0); // 动态对象

常见坑:构造函数参数与成员同名

Stock::Stock(const string & company, long shares, double share_val) 这样写是错的——shares = shares; 是给自己赋值。参数代表”要赋给成员的值”,必须用不同名字。常见惯例是给成员加 m_ 前缀(m_shares)或下划线后缀(shares_)。

默认构造函数

Stock fluffy_the_cat; 这种不带初始值的声明需要默认构造函数。规则要记牢:

  • 一个构造函数都不写时,编译器自动提供一个什么都不做的隐式默认构造;
  • 一旦你写了任何一个构造函数,提供默认构造的责任就转到你头上。此时 Stock stock1; 会编译报错——这个设计是有意的,让你可以”禁止创建未初始化的对象”。

提供默认构造的两种方式(只能二选一):

Stock(const string & co = "Error", int n = 0, double pr = 0.0); // 全默认参数
Stock(); // 或者:重载一个无参版本

而且默认构造最好真的初始化所有成员:

Stock::Stock() // default constructor
{
    company = "no name";
    shares = 0;
    share_val = 0.0;
    total_val = 0.0;
}

另一个易踩的坑:

Stock first("Concrete Conglomerate"); // 调用构造函数,OK
Stock second();  // 注意!这是在声明一个"返回 Stock 的函数",不是对象!
Stock third;     // 隐式调用默认构造,这才是想要的

隐式调用默认构造不要加括号

析构函数

对象消亡时,编译器自动调用析构函数做收尾。规则:

  • 名字是类名前加波浪号:~Stock()
  • 无返回类型、不能有参数、每个类只能有一个析构函数;
  • 如果构造函数用 new 分配了内存,析构函数就必须用 delete 释放(第 12 章的主角)。

Stock 类没用到 new,析构函数本来可以什么都不做,但为了观察调用时机,我们让它”出声”:Stock::~Stock() { cout << "Bye, " << company << "!\n"; }

调用时机由编译器管理:静态存储对象在程序结束时析构;自动存储对象在离开其定义所在的代码块时析构;new 出来的对象在 delete 时析构;临时对象在使用完毕后立即析构。

整合升级版 Stock

把构造/析构加进去,类声明升级为 stock10.h。数据成员和 set_tot() 不变,去掉不再需要的 acquire(),public 部分新增两个构造函数与析构函数的原型:

// stock10.h -- Stock class declaration with constructors, destructor added
#ifndef STOCK10_H_
#define STOCK10_H_
#include <string>
 
class Stock
{
private:
    // ...数据成员与 stock00.h 相同...
    void set_tot() { total_val = shares * share_val; }
public:
    // two constructors
    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

实现文件中其余方法与 stock00.cpp 相同,只需加上两个构造函数与析构函数的定义(前文已给出)。测试程序:

// 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.00 Total Worth: $240.00
Constructor using Boffo Objects called
Company: Boffo Objects Shares: 2
Share Price: $2.00 Total Worth: $4.00
Assigning stock1 to stock2:
...
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.00 Total Worth: $500.00
Done
Bye, NanoSmart!
Bye, Nifty Foods!

三个关键观察:

  1. stock2 = stock1;赋值:默认把源对象的每个成员逐一拷贝给目标对象(stock2 原来的内容被覆盖)。
  2. stock1 = Stock("Nifty Foods", 10, 50.0); 是对已存在的对象使用构造函数——先创建临时对象,把内容拷给 stock1,再立即析构临时对象(输出中两条 Nifty Foods 消息相邻出现正是这个原因)。而开头的 Stock stock2 = Stock(...)初始化,通常直接构造,不产生临时对象。
  3. main 里多套了一层大括号:自动变量在离开代码块时析构,加了括号才能在 return 前看到最后两声 “Bye”。

初始化优先于赋值

如果既能用初始化又能用赋值设置对象值,选初始化,通常效率更高。

C++11 列表初始化

只要大括号里的内容能匹配某个构造函数的参数表,就可以用列表初始化:Stock hot_tip = {"Derivatives Plus Plus", 100, 45.0};Stock jock {"Sport Age Storage, Inc"};(第二、三个参数用默认值)、Stock temp {};(匹配默认构造函数)。

构造与析构小结

  • 构造函数:与类同名、可重载多个版本、无声明类型;对象创建时被调用。只有一个参数的构造函数允许 Classname object = value; 这种赋值式初始化(第 11 章讲如何用 explicit 关闭它)。
  • 析构函数:~类名(),无参无返回值,每个类只能有一个;对象消亡时自动调用。构造函数用了 new 的类,析构函数就必须用 delete。

常见坑:写了带参构造函数后默认构造"消失"

定义了 Stock(const std::string & co, long n = 0, double pr = 0.0); 之后,Stock a; 直接编译失败。要么给全部参数配默认值,要么再重载一个无参构造——但两者只能选其一,同时写会产生二义性。

10.7 const 成员函数

看这段:

const Stock land = Stock("Kludgehorn Properties");
land.show(); // 编译器报错!

show() 没有承诺自己不会修改调用对象,而 land 是 const 的,不该被改动。麻烦在于 show() 没有参数可加 const——它操作的对象是调用时隐式传入的。C++ 的解法:把 const 放在函数括号后面

void show() const;          // 声明
void Stock::show() const    // 定义
{ ... }

这叫 const 成员函数,等于承诺”我不会修改调用它的对象”。规则和函数参数一样:只要方法不需要修改调用对象,就应把它声明为 const。本章后面的 show() consttopval() const 都遵守这条铁律。

10.8 this 指针

到目前为止每个方法只面对一个对象。如果方法需要同时面对两个对象呢?比如给 Stock 加一个 topval():比较两只股票,返回总市值较高的那个的引用。

原型这样设计:

const Stock & topval(const Stock & s) const;
  • 括号里的 const:不修改显式传入的参数 s;
  • 括号后的 const:不修改隐式调用的对象;
  • 返回 const 引用:因为返回的是两个 const 对象之一的引用。

调用可以是 stock1.topval(stock2)(stock1 隐式、stock2 显式)或反过来。实现时问题来了:

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

显式那方有名字 s,隐式这方没有别名!C++ 的答案是 this 指针——每个成员函数(包括构造和析构)都有一个隐藏的 this 指针,指向调用该方法的对象stock1.topval(stock2) 会把 this 设为 &stock1。方法里写的 total_val 其实是 this->total_val 的缩写。


▲ 图 10.4 this 指向调用对象

this 存的是地址,要返回对象本身就得解引用:*this。于是:

const Stock & Stock::topval(const Stock & s) const
{
    if (s.total_val > total_val)
        return s;      // argument object
    else
        return *this;  // invoking object
}

返回类型是引用,意味着返回的是对象本身而不是副本。升级后的头文件 stock20.h:数据成员、两个构造函数、buy/sell/update 与 stock10.h 相同,变化是析构函数改为安静版(不再打印)、show()topval() 都加上了 const:

// 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

通关标准:

能独立说出:①类声明和方法定义分别在哪个文件、各管什么;②private/public 的意义与默认访问级别;③构造函数/析构函数的命名、参数、返回值、调用时机三条规则;④”定义了任何构造函数后默认构造消失”这条规则并会补救;⑤this 指针是什么、*this 何时用。做到这五点,才好进入第 11 章的运算符重载。

10.9 对象数组

对象数组和普通数组一样声明:

Stock mystuff[4]; // 4 个 Stock 对象,全部用默认构造函数初始化

也可以用构造函数逐个初始化元素,还能混用不同构造函数(若列表只给前 3 个元素,其余元素自动用默认构造函数):

const int STKS = 4;
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)
};

配合 topval() 找出最值钱的持仓:

// usestok2.cpp -- using the Stock class
// compile with stock20.cpp
#include <iostream>
#include "stock20.h"
 
const int STKS = 4;
int main()
{
    // stocks[STKS] 的初始化与上面相同,此处省略
    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

注意:绝大部分工作量花在设计类上,类设计好了,客户端程序反而很简单。

顺带一提 this 的”底层真相”:早期 Unix 的 cfront 编译器把 C++ 转成 C 时,void Stock::show() const 被翻译成 void show(const Stock * this)、成员改写为 this->company,调用 top.show() 翻译成 show(&top)。理解了 this,就理解了方法调用的底层机制。


▲ 图 10.3 用成员函数访问两个对象:一方隐式(this),一方显式(参数)

10.10 类作用域与类内常量

C++ 新增类作用域:类中定义的名字(数据成员、成员函数名)在整个类内可见,类外不可见。所以不同类可以用同名成员互不冲突;public 方法也不能在类外裸调,必须通过对象(.->)或作用域解析符 ::(定义方法时)来用。比如 Stock sleeper(...); sleeper.show(); 合法,直接写 show(); 非法。

类作用域常量

想让所有对象共享一个符号常量(比如数组大小)?直接写 const int Months = 12; 不行——类声明只是描述”对象长什么样”,并不创建对象,声明时没地方存值。两种替代方案:

class Bakery
{
private:
    enum {Months = 12};           // 方案一:类内枚举,不占对象内存,编译器直接替换
    double costs[Months];
    ...
};
 
class Bakery
{
private:
    static const int Months = 12; // 方案二:静态常量,存于静态区,全体对象共享
    double costs[Months];
    ...
};

C++98 中静态常量方式只支持整数和枚举值,C++11 起解除限制。ios_base::fixed 中的 fixed 就是类内枚举常量的实例。

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

传统枚举的枚举量暴露在所在作用域中,两个 enum 的枚举量会冲突(enum egg {Small, ...};enum t_shirt {Small, ...}; 无法共存)。C++11 的 enum class(或 enum struct)把枚举量限定在类作用域内,且禁止隐式转 int,类型更安全:

enum class egg {Small, Medium, Large, Jumbo};
enum class t_shirt {Small, Medium, Large, Xlarge};
 
egg choice = egg::Large;         // 必须用类名限定
int Frodo = int(t_shirt::Small); // 只能显式转换,Frodo 为 0

还可以指定底层类型:enum class : short pizza {Small, Medium, Large, XLarge};

10.11 抽象数据类型:以 Stack 为例

Stock 类很具体。类更强大的用法是表达抽象数据类型(ADT):以通用方式描述类型,不牵扯实现细节。栈(stack)是经典例子——只从栈顶存取(后进先出 LIFO)。抽象描述是:

  • 可创建空栈;
  • 可将数据压入栈顶(push);
  • 可从栈顶弹出数据(pop);
  • 可检查栈是否满、是否空。

用类表达 ADT:public 接口表达这些抽象操作,private 数据负责存储(用数组还是链表,接口一概不管):

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

接口设计上有两处巧思:私有部分用了数组实现,但 public 接口对此只字未提——换成动态数组或链表,客户端一行不改;push/pop 返回 bool 报告成败,调用者既可先查 isfull/isempty 再操作,也可直接用返回值判断。数据类型用 typedef unsigned long Item; 隔离,想要 double 栈改一行 typedef 即可(更通用的做法是第 14 章的类模板)。

测试程序模拟职员用 LIFO 方式处理采购单:读入命令 A(压入采购单号)或 P(弹出处理),Q 退出。示例运行:依次压入 17885、17965、18002,弹出顺序是 18002 → 17965 → 17885——最后进的最先出,这正是栈的脾气。构造函数保证栈创建时必为空,push/pop 保证 top 始终被正确维护——这种”保证”正是 OOP 可靠性的来源:数据被私有了,就不存在外部代码把它改乱的可能;如果改用”一个数组 + 一个独立的下标变量”来表示栈,每次写新代码都得自己小心别出错。