这一章在干嘛?
前面九章我们一直在”面向过程”地写代码:先想清楚步骤,再考虑数据放哪。这一章开始换脑子——先想清楚”我要管理的是什么东西”,再决定它能做什么操作。C++ 用**类(class)**把数据和操作打包在一起,这就是面向对象编程(OOP)的核心工具。本章从零设计一个股票类 Stock,讲透类声明、访问控制、构造函数/析构函数、const 成员函数和 this 指针——这是全书 OOP 三部曲的第一章。
10.1 过程性编程和面向对象编程
先看一个思维实验:假设你要给垒球队写一个统计程序。
过程性思维(C 语言式)先问:“我要执行哪些步骤?“——输入数据、计算、更新、显示,于是设计一堆函数,再回头想数据怎么放:用几个数组?还是设计一个结构体装下每个球员的信息?
面向对象思维(OOP 式)先问:“我要管理的是什么东西?“——是”球员”这个整体。于是设计一个表示球员的对象,包含名字和所有统计数据,再为它配套方法:录入、计算(比如自动算打击率)、更新、显示。用户与数据的交互方式(初始化、更新、报表)就是这个对象的用户接口。
一句话:过程性编程先想”做什么步骤”,OOP 先想”数据是什么、怎么被使用”,接口定了之后才考虑实现和存储。OOP 强调的几个特性:抽象、封装与数据隐藏、多态、继承、代码可复用性。类正是实现这一切的 C++ 载体——本章讲前三者的地基,多态(运算符重载)在第 11 章,继承在第 13 章。
10.2 抽象和类
抽象是应对复杂度的手段:你是几万亿个原子的集合,但你更愿意把自己当作”一个人”来处理。计算中的抽象,就是只提取信息与用户交互的关键操作特征,忽略实现细节。
什么是类型?
别把类型只理解成”占多少内存”。指定一个基本类型其实做了三件事:
- 决定数据对象需要多少内存;
- 决定内存中的比特如何解释(long 和 float 可能都是 32 位,但解释方式完全不同);
- 决定能对它执行哪些操作(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() 里直接用 company、shares;如果你在类外的普通函数里这么干,编译器会直接拦下你。
完整的实现文件如下(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!三个关键观察:
stock2 = stock1;是赋值:默认把源对象的每个成员逐一拷贝给目标对象(stock2 原来的内容被覆盖)。stock1 = Stock("Nifty Foods", 10, 50.0);是对已存在的对象使用构造函数——先创建临时对象,把内容拷给 stock1,再立即析构临时对象(输出中两条 Nifty Foods 消息相邻出现正是这个原因)。而开头的Stock stock2 = Stock(...)是初始化,通常直接构造,不产生临时对象。- 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() const、topval() 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 可靠性的来源:数据被私有了,就不存在外部代码把它改乱的可能;如果改用”一个数组 + 一个独立的下标变量”来表示栈,每次写新代码都得自己小心别出错。
自测:类和结构体在 C++ 中唯一的区别是什么?
默认访问级别:class 默认 private,struct 默认 public。惯例上 class 做完整的类设计,struct 只装纯数据(POD)。
自测:
Stock second();声明的是什么?一个返回类型为 Stock、无参数的函数,不是对象。隐式调用默认构造函数不能加括号,正确写法是
Stock second;。
自测:什么时候编译器不再自动提供默认构造函数?
只要你为类定义了任意一个构造函数。此时
Stock a;要合法,必须自己补默认构造(无参版本,或全参数带默认值的版本,二选一)。
自测:
const Stock & topval(const Stock & s) const;中三个 const 分别是什么意思?①返回类型的 const:返回的引用指向 const 对象;②参数的 const:不修改显式传入的 s;③括号后的 const:const 成员函数,不修改隐式的调用对象(*this)。
自测:析构函数在哪些时机被自动调用?
①静态存储对象:程序结束时;②自动存储对象:离开其定义的代码块时;③new 创建的对象:执行 delete 时;④临时对象:用完立即。一般不要显式调用析构函数。