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

在深入学习C++的类之前,了解两种主要的编程范式(Programming Paradigms)——过程性编程和面向对象编程——之间的区别是很有帮助的。C++ 语言同时支持这两种范式,但其强大的面向对象特性是其核心优势之一。

过程性编程 (Procedural Programming)

过程性编程是最早期的编程范式之一,像C语言就是典型的过程性语言。它的核心思想是将程序看作是一系列要执行的过程函数

  • 关注点: 主要关注点在于算法执行步骤。程序被分解为一系列的函数调用。
  • 数据处理: 数据通常是独立于函数存在的(例如全局变量),或者作为参数在函数之间传递。数据和操作数据的函数是分离的。
  • 组织方式: 程序通过函数的层次结构来组织。一个主函数调用其他函数,这些函数又可能调用更底层的函数。
  • 示例语言: C, Pascal, Fortran。

过程性编程的思维方式: “程序需要执行哪些步骤?需要哪些函数来实现这些步骤?”

例子(概念性):
假设要管理一个银行账户。在过程性方法中,你可能会有:

  • 一个数据结构(比如 struct)来存储账户信息(账号、余额)。
  • 一系列函数来操作这个数据结构:deposit(account, amount), withdraw(account, amount), check_balance(account)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// C 语言示例 (过程性)
struct BankAccount {
int accountNumber;
double balance;
};

void deposit(struct BankAccount* acc, double amount) {
if (amount > 0) {
acc->balance += amount;
}
}

void withdraw(struct BankAccount* acc, double amount) {
if (amount > 0 && acc->balance >= amount) {
acc->balance -= amount;
} else {
// 处理错误
}
}

double check_balance(struct BankAccount* acc) {
return acc->balance;
}

int main() {
struct BankAccount myAccount = {12345, 1000.0};
deposit(&myAccount, 500.0);
withdraw(&myAccount, 200.0);
// ...
return 0;
}

在这种模式下,数据(myAccount)和操作(deposit, withdraw)是分开定义的。

面向对象编程 (Object-Oriented Programming - OOP)

面向对象编程(OOP)是一种不同的思考方式。它将程序看作是由相互交互的对象 (Objects) 组成的。

  • 关注点: 主要关注点在于数据以及与数据相关的操作。程序的核心是对象。
  • 对象: 对象是现实世界实体的抽象,它封装 (Encapsulates)数据(属性/状态)和可以对这些数据执行的操作(方法/行为)
  • 组织方式: 程序通过创建对象并让这些对象相互发送消息(调用方法)来组织。
  • 核心概念:
    • 封装 (Encapsulation): 将数据和操作数据的函数捆绑在一起(形成类),并对外部隐藏对象的内部实现细节(数据隐藏)。
    • 继承 (Inheritance): 允许创建一个新类(派生类),该类继承现有类(基类)的属性和方法,从而实现代码重用和层次结构。
    • 多态 (Polymorphism): 允许不同类的对象对相同的消息(方法调用)做出不同的响应。这通常通过虚函数实现。
    • 抽象 (Abstraction): 关注对象的本质特征,忽略不重要的细节。类就是一种抽象。
  • 示例语言: C++, Java, C#, Python, Smalltalk。

面向对象编程的思维方式: “程序涉及哪些‘事物’(对象)?每个‘事物’有哪些特征(数据)?它可以做什么(方法)?这些‘事物’之间如何交互?”

例子(概念性):
对于银行账户,OOP方法会创建一个 BankAccount

  • 数据成员(属性): accountNumber, balance (通常设为私有 private,以实现数据隐藏)。
  • 成员函数(方法): deposit(amount), withdraw(amount), check_balance() (这些函数直接操作对象内部的数据)。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// C++ 示例 (面向对象)
class BankAccount {
private: // 数据隐藏
int accountNumber;
double balance;

public: // 公共接口
// 构造函数 (用于创建对象)
BankAccount(int accNum, double initialBalance) {
accountNumber = accNum;
balance = (initialBalance >= 0) ? initialBalance : 0.0;
}

void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}

void withdraw(double amount) {
if (amount > 0 && balance >= amount) {
balance -= amount;
} else {
// 处理错误
}
}

double check_balance() const { // const 表示此方法不修改对象状态
return balance;
}

int getAccountNumber() const {
return accountNumber;
}
}; // 注意类定义末尾的分号

int main() {
// 创建 BankAccount 对象
BankAccount myAccount(12345, 1000.0);

// 通过对象调用方法
myAccount.deposit(500.0);
myAccount.withdraw(200.0);
std::cout << "Balance: " << myAccount.check_balance() << std::endl;

return 0;
}

在这里,数据 (accountNumber, balance) 和操作 (deposit, withdraw, check_balance) 被紧密地捆绑在 BankAccount 类中。你通过 myAccount 这个对象来调用它的方法。

总结对比

特性 过程性编程 面向对象编程 (OOP)
核心 函数/过程 对象 (数据 + 方法)
数据 通常与函数分离 封装在对象内部
访问控制 有限 (主要靠作用域) 强 (public, private, protected)
主要优势 简单直接,适合小型或中型项目 模块化、重用性、可维护性、扩展性好
设计方法 自顶向下 (Top-down) 自底向上 (Bottom-up) 或混合
代码重用 主要通过函数库 主要通过继承和组合
适合场景 算法密集型、顺序执行任务 大型复杂系统、模拟、GUI

C++ 最初是从 C 语言发展而来的,因此它完全兼容过程性编程。然而,C++ 的真正威力在于其强大的面向对象特性,它允许开发者构建更大型、更复杂、更易于维护和扩展的软件系统。接下来的章节将深入探讨 OOP 的核心——类和对象。

10.2 抽象和类

面向对象编程(OOP)的核心思想之一是**抽象 (Abstraction)**。在编程中,抽象意味着关注事物的本质特征和行为,而忽略其不重要的内部细节。我们每天都在使用抽象:当你开车时,你只需要知道如何使用方向盘、油门和刹车(接口),而不需要了解引擎内部复杂的机械原理(实现细节)。

10.2.1 类型是什么

在编程语言中,“类型”(Type)定义了一组可能的值以及可以对这些值执行的操作。

  • 内置类型 (Built-in Types): C++ 提供了像 int, float, char, bool 这样的基本类型。我们知道 int 可以存储整数,并且可以对它们执行加、减、乘、除等运算。编译器知道如何表示这些类型的数据以及如何执行这些操作。
  • 用户定义类型 (User-Defined Types - UDT): C++ 允许程序员创建自己的类型来模拟现实世界或特定问题域中的概念。这就是类 (Class) 发挥作用的地方。类是一种将数据(属性)和操作这些数据的函数(方法)捆绑在一起的机制,从而创建新的数据类型。

例如,如果你在编写一个股票交易程序,你可能需要一个表示“股票”的类型。这个类型应该包含哪些数据(如股票名称、持有数量、单价)?可以对它执行哪些操作(如购买、出售、更新价格、显示信息)?类允许你精确地定义这些。

10.2.2 C++中的类

类是创建对象的蓝图或模板。它定义了:

  1. 数据成员 (Data Members): 对象将存储的数据(也称为属性、状态)。
  2. 成员函数 (Member Functions): 可以对对象的数据执行的操作(也称为方法、行为)。

类声明的基本语法:

1
2
3
4
5
6
7
8
9
10
11
12
class ClassName {
private:
// 私有数据成员和成员函数
// 通常将数据成员放在这里,实现数据隐藏

public:
// 公有数据成员和成员函数
// 这是类的公共接口,外部代码通过它们与对象交互

protected:
// 保护成员 (将在继承中讨论)
}; // 注意末尾的分号
  • class 关键字: 表明你正在定义一个类。
  • ClassName: 你为新类型指定的名称(遵循变量命名规则,通常首字母大写)。
  • 访问说明符 (Access Specifiers):
    • private: 私有成员只能被类的内部成员函数访问。这是实现数据隐藏 (Data Hiding) 的关键,保护数据不被外部代码随意修改,是封装的重要体现。默认情况下,类成员是 private 的。
    • public: 公有成员可以被程序中的任何地方访问(通过类的对象)。它们构成了类的**公共接口 (Public Interface)**。
    • protected: 与继承相关,现在可以暂时将其视为与 private 类似。
  • 成员: 类定义的花括号 {} 内部声明的变量(数据成员)和函数(成员函数)。

示例:定义一个简单的 Stock

假设我们要创建一个表示股票持有的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 通常放在头文件 (e.g., stock.h) 中
#include <string> // 为了使用 std::string

class Stock {
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() const; // 显示股票信息 (const表明此函数不修改对象)
}; // 类定义结束

这个 Stock 类定义了一个新的数据类型。它封装了股票相关的数据 (company, shares, share_val, total_val) 和操作这些数据的函数 (acquire, buy, sell, update, show)。数据成员被设为 private,外部代码不能直接访问它们,只能通过 public 的成员函数来交互。set_tot() 是一个内部辅助函数,也被设为 private

10.2.3 实现类成员函数

类定义通常只包含成员函数的声明(原型)。函数的定义(实现)可以放在类声明的内部(如果函数很简单,可以作为内联函数),或者更常见地,放在类声明的外部(通常在对应的源文件 .cpp 中)。

当在类外部定义成员函数时,你需要使用作用域解析运算符 :: 来指明这个函数属于哪个类。

语法:

1
2
3
ReturnType ClassName::FunctionName(ParameterList) {
// 函数体
}

示例:实现 Stock 类的成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 通常放在源文件 (e.g., stock.cpp) 中
#include <iostream>
#include "stock.h" // 包含类定义的头文件

// 使用作用域解析运算符 :: 指明函数属于 Stock 类
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 aborted.\n";
} else {
shares += num;
share_val = price; // 假设按新价格计算
set_tot();
}
}

void Stock::sell(long num, double price) {
if (num < 0) {
std::cout << "Number of shares sold can't be negative. "
<< "Transaction aborted.\n";
} else if (num > shares) {
std::cout << "You can't sell more than you have! "
<< "Transaction aborted.\n";
} else {
shares -= num;
share_val = price; // 假设按新价格计算
set_tot();
}
}

void Stock::update(double price) {
share_val = price;
set_tot();
}

// 注意 const 关键字在函数定义和声明中都要有
void Stock::show() const {
// 设置输出格式
std::ios_base::fmtflags orig =
std::cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
std::streamsize prec = std::cout.precision(3);

std::cout << "Company: " << company
<< " Shares: " << shares << '\n';
std::cout << " Share Price: $" << share_val;
// 设置精度为2位小数显示总价
std::cout.precision(2);
std::cout << " Total Worth: $" << total_val << '\n';

// 恢复原始格式
std::cout.setf(orig, std::ios_base::floatfield);
std::cout.precision(prec);
}

// 注意:私有成员函数 set_tot() 也可以在类外部定义,
// 但因为它很简单,通常会直接在类定义内部实现(如上所示),
// 这样它就可能被编译器视为内联函数。
// 如果在外部定义:
// void Stock::set_tot() {
// total_val = shares * share_val;
// }

const 成员函数:
show() 函数声明和定义的末尾都有 const 关键字。这表明 show() 是一个常量成员函数,它承诺不会修改调用它的对象的数据成员。这是一个好习惯,可以提高代码的可读性和安全性,并允许对 const 对象调用此函数。

10.2.4 使用类

一旦定义了类(蓝图),你就可以创建该类的对象 (Objects) 或**实例 (Instances)**。创建对象就像声明一个基本类型的变量一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 在 main() 函数或其他函数中
#include "stock.h" // 需要包含类定义

int main() {
// 创建两个 Stock 对象 (实例)
Stock stock1;
Stock stock2;

// 使用点号 . (成员访问运算符) 调用对象的公有成员函数
stock1.acquire("NanoSmart", 20, 12.50);
stock1.show(); // 显示 stock1 的信息

stock2.acquire("Boffo Objects", 2, 180.0);
stock2.show(); // 显示 stock2 的信息

stock2.buy(5, 190.0); // 增持 stock2
stock2.show();

stock1.sell(10, 15.75); // 卖出部分 stock1
stock1.show();

// 错误!不能直接访问私有成员
// stock1.shares = 50; // 编译错误
// std::cout << stock1.company; // 编译错误

return 0;
}
  • Stock stock1; 创建了一个名为 stock1Stock 类型的对象。
  • 使用成员访问运算符(点号 .来调用对象的公有成员函数,例如 stock1.acquire(...)stock2.show()
  • 每个对象都有自己的一套数据成员。stock1sharesstock2shares 是相互独立的。
  • 你不能从对象外部直接访问 private 成员,这强制你必须通过类提供的公共接口(public 函数)来与对象交互。

10.2.5 修改实现

将类的接口(头文件中的声明)和实现(源文件中的定义)分开的一个主要好处是封装带来的灵活性。

  • 接口 (Interface): 头文件 (stock.h) 定义了如何使用这个类(公共成员函数)。使用类的代码(如 main() 函数)只需要包含头文件。
  • 实现 (Implementation): 源文件 (stock.cpp) 包含了成员函数具体如何工作。

只要类的公共接口保持不变(函数名、参数、返回类型不变),你就可以自由地修改源文件中的实现细节(例如,改进 set_tot 的计算方式,或者改变内部数据的存储方式),而不需要修改或重新编译使用该类的其他代码文件(如包含 main() 的文件)。只需要重新编译实现文件 (stock.cpp) 并重新链接即可。

这大大降低了维护成本,并使得代码库更容易更新和改进。用户只关心“能做什么”(接口),而不必关心“怎么做”(实现)。

10.2.6 小结

  • 抽象是关注本质、忽略细节的编程思想。
  • 是C++实现抽象和创建用户定义类型的主要机制。
  • 类将数据(成员变量)操作数据的函数(成员函数)捆绑在一起。
  • 访问说明符public, private, protected)控制对类成员的访问。
  • 数据隐藏(通常将数据设为 private)是封装的关键,保护数据并隐藏实现细节。
  • 公共接口public 成员函数)定义了如何与类的对象交互。
  • 成员函数通常在类外部使用作用域解析运算符 :: 来定义。
  • 使用点号 . 访问对象的公有成员。
  • 将接口和实现分离(头文件/源文件)可以提高代码的模块化和可维护性。

10.3 类的构造函数和析构函数

在上一节中,我们定义了一个 Stock 类,并通过 acquire() 成员函数来设置其初始状态。然而,C++ 提供了一种更自动化、更专门化的方式来处理对象的初始化和清理工作:构造函数 (Constructor) 和 **析构函数 (Destructor)**。

10.3.1 声明和定义构造函数

构造函数是一种特殊的成员函数,它的主要目的是在创建类的对象时初始化该对象的数据成员。

特点与规则:

  1. 名称与类名相同: 构造函数的名称必须与它所属的类的名称完全一样。
  2. 没有返回类型: 构造函数没有声明返回类型,连 void 也没有。
  3. 自动调用: 当创建类的对象时,程序会自动调用相应的构造函数。
  4. 可以重载: 一个类可以有多个构造函数,只要它们的参数列表不同(参数个数、类型或顺序不同)。这允许以不同的方式初始化对象。

声明语法 (在类定义内):

1
2
3
4
5
6
7
8
class ClassName {
public:
// 构造函数声明 (无参数)
ClassName();
// 构造函数声明 (带参数)
ClassName(ParameterList);
// ... 其他成员 ...
};

定义语法 (在类外部):

1
2
3
4
5
6
7
8
9
// 无参数构造函数定义
ClassName::ClassName() {
// 初始化代码
}

// 带参数构造函数定义
ClassName::ClassName(ParameterList) {
// 使用参数进行初始化
}

示例:为 Stock 类添加构造函数

我们可以为 Stock 类添加构造函数来替代之前的 acquire() 函数的部分功能,确保对象在创建时就被赋予有意义的初始值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 在 stock.h 的类定义中声明
class Stock {
private:
std::string company;
long shares;
double share_val;
double total_val;
void set_tot() { total_val = shares * share_val; }

public:
// 构造函数声明 (两个版本)
Stock(); // 默认构造函数 (无参数)
Stock(const std::string &co, long n = 0, double pr = 0.0); // 带参数的构造函数 (使用默认参数)

// 不再需要 acquire(),因为构造函数处理了初始设置
// 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() const;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
// 在 stock.cpp 中定义构造函数
#include <iostream>
#include "stock.h"

// 默认构造函数定义
Stock::Stock() { // 默认构造函数
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();
}

// ... buy(), sell(), update(), show() 的定义保持不变 ...
// (注意:buy, sell, update 内部的 set_tot() 调用仍然需要)
void Stock::buy(long num, double price) {
if (num < 0) {
std::cout << "Number of shares purchased can't be negative. "
<< "Transaction aborted.\n";
} else {
shares += num;
share_val = price;
set_tot();
}
}

void Stock::sell(long num, double price) {
if (num < 0) {
std::cout << "Number of shares sold can't be negative. "
<< "Transaction aborted.\n";
} else if (num > shares) {
std::cout << "You can't sell more than you have! "
<< "Transaction aborted.\n";
} else {
shares -= num;
share_val = price;
set_tot();
}
}

void Stock::update(double price) {
share_val = price;
set_tot();
}

void Stock::show() const {
std::ios_base::fmtflags orig =
std::cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
std::streamsize prec = std::cout.precision(3);

std::cout << "Company: " << company
<< " Shares: " << shares << '\n';
std::cout << " Share Price: $" << share_val;
std::cout.precision(2);
std::cout << " Total Worth: $" << total_val << '\n';

std::cout.setf(orig, std::ios_base::floatfield);
std::cout.precision(prec);
}

10.3.2 使用构造函数

当创建对象时,编译器会自动选择匹配的构造函数来执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "stock.h" // 包含 Stock 类定义

int main() {
// 调用带参数的构造函数 Stock(const std::string &co, long n, double pr)
Stock stock1("NanoSmart", 12, 20.0); // 显式提供所有参数
stock1.show();

// 调用带参数的构造函数,利用了 n 和 pr 的默认值 (n=0, pr=0.0)
Stock stock2("Boffo Objects"); // 等同于 Stock("Boffo Objects", 0, 0.0)
stock2.show();

// 调用默认构造函数 Stock()
Stock stock3; // 注意:这里不能写 stock3()
stock3.show();

// 也可以使用 C++11 的列表初始化
Stock stock4 = {"Fleep Enterprises", 100, 1.25}; // 调用带参数构造函数
stock4.show();

Stock stock5{"Dummy Corp"}; // 调用带参数构造函数 (利用默认值)
stock5.show();

Stock stock6{}; // 调用默认构造函数 Stock()
stock6.show();

// 动态分配对象时也会调用构造函数
Stock *p_stock = new Stock("Electroshock Games", 18, 19.0);
p_stock->show();
delete p_stock; // 稍后会看到 delete 会调用析构函数

return 0;
}

输出可能包含 (取决于编译器和优化):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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: 0
Share Price: $0.000 Total Worth: $0.00
Default constructor called
Company: no name Shares: 0
Share Price: $0.000 Total Worth: $0.00
Constructor using Fleep Enterprises called
Company: Fleep Enterprises Shares: 100
Share Price: $1.250 Total Worth: $125.00
Constructor using Dummy Corp called
Company: Dummy Corp Shares: 0
Share Price: $0.000 Total Worth: $0.00
Default constructor called
Company: no name Shares: 0
Share Price: $0.000 Total Worth: $0.00
Constructor using Electroshock Games called
Company: Electroshock Games Shares: 18
Share Price: $19.000 Total Worth: $342.00

10.3.3 默认构造函数

默认构造函数 (Default Constructor) 是指不接受任何参数的构造函数。

  • 编译器生成的默认构造函数: 如果你没有为类定义任何构造函数,编译器会自动为你生成一个默认构造函数。这个合成的构造函数什么也不做(对于内置类型成员不会初始化,对于类类型成员会调用其默认构造函数)。
  • 用户定义的默认构造函数: 如果你定义了一个无参数的构造函数(如上面 Stock::Stock()),那么它就是默认构造函数。
  • 重要规则: 如果你为类定义了任何构造函数(即使是带参数的),编译器就不会再自动生成默认构造函数了。如果你还需要一个无参数的构造函数(例如,为了能创建 Stock stock3; 这样的对象),你就必须显式地定义它。

在我们的 Stock 示例中,因为我们定义了 Stock(const std::string &co, ...),编译器就不会自动生成默认构造函数。因此,我们必须自己提供 Stock::Stock(),否则 Stock stock3; 这样的声明将导致编译错误。

C++11 = default: 如果你定义了其他构造函数,但仍希望编译器为你生成默认的、行为简单的默认构造函数,可以使用 = default

1
2
3
4
5
6
7
class Example {
public:
Example(int v) : value(v) {} // 用户定义的带参构造函数
Example() = default; // 显式要求编译器生成默认构造函数
private:
int value;
};

10.3.4 析构函数

析构函数 (Destructor) 是另一种特殊的成员函数,它的主要目的是在对象生命周期结束时执行清理工作。

用途:

  • 释放对象在生命周期内分配的资源(例如,通过 new 分配的内存)。
  • 执行任何必要的关闭操作(例如,关闭文件、断开网络连接)。

特点与规则:

  1. 名称: 析构函数的名称是在类名前加上波浪号 ~(例如 ~Stock)。
  2. 没有返回类型: 和构造函数一样,析构函数也没有返回类型,连 void 也没有。
  3. 没有参数: 析构函数不能接受任何参数,因此不能被重载。一个类最多只有一个析构函数。
  4. 自动调用: 当对象被销毁时,析构函数会自动被调用。这发生在:
    • 对象的作用域结束时(对于自动存储对象,如函数内的局部对象)。
    • 当对指向对象的指针调用 delete 时(对于动态存储对象)。
    • 当包含该对象的对象被销毁时。

声明语法 (在类定义内):

1
2
3
4
5
6
class ClassName {
public:
// ... 构造函数和其他成员 ...
// 析构函数声明
~ClassName();
};

定义语法 (在类外部):

1
2
3
ClassName::~ClassName() {
// 清理代码
}

示例:为 Stock 类添加析构函数

对于我们当前的 Stock 类,它并没有在内部使用 new 分配内存,所以析构函数不是严格必需的。但为了演示,我们可以添加一个简单的析构函数。

1
2
3
4
5
6
7
8
9
10
// 在 stock.h 的类定义中声明
class Stock {
// ... private members ...
public:
// ... constructors ...
// ... other public methods ...

// 析构函数声明
~Stock();
};
1
2
3
4
5
6
7
8
9
10
11
12
13
// 在 stock.cpp 中定义析构函数
#include <iostream>
#include "stock.h"

// ... constructor definitions ...
// ... other method definitions ...

// 析构函数定义
Stock::~Stock() {
// 对于这个简单的 Stock 类,没什么需要显式清理的
// 但我们可以加一条打印语句来观察它何时被调用
std::cout << "Bye, " << company << "!\n";
}

观察析构函数的调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include "stock.h"

int main() {
{ // 创建一个内部作用域
std::cout << "--- Entering inner block ---\n";
Stock stock1("Smart Comp", 50, 5.0);
Stock stock2("Great Gadgets", 10, 12.0);
std::cout << "--- Exiting inner block ---\n";
// 当离开这个作用域时,stock2 和 stock1 (按相反顺序创建) 会被销毁
} // stock2 的析构函数先调用,然后是 stock1 的

std::cout << "--- Creating dynamic stock ---\n";
Stock *p_stock = new Stock("Dynamic Duo", 25, 2.5);
p_stock->show();
std::cout << "--- Deleting dynamic stock ---\n";
delete p_stock; // 调用析构函数,然后释放内存

std::cout << "--- main() is ending ---\n";
return 0;
}

可能的输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
--- Entering inner block ---
Constructor using Smart Comp called
Constructor using Great Gadgets called
--- Exiting inner block ---
Bye, Great Gadgets!
Bye, Smart Comp!
--- Creating dynamic stock ---
Constructor using Dynamic Duo called
Company: Dynamic Duo Shares: 25
Share Price: $2.500 Total Worth: $62.50
--- Deleting dynamic stock ---
Bye, Dynamic Duo!
--- main() is ending ---

注意析构函数调用的时机和顺序(对于局部对象,与构造顺序相反)。

10.3.5 改进 Stock 类

现在,我们可以整合构造函数和析构函数,得到一个更完善(虽然在这个例子中析构函数作用不大)的 Stock 类。

stock.h (最终版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// filepath: d:\ProgramData\files_Cpp\250424\stock.h
#ifndef STOCK_H_
#define STOCK_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(); // 默认构造函数
Stock(const std::string &co, long n = 0, double pr = 0.0);
// 析构函数
~Stock();

// 其他成员函数
void buy(long num, double price);
void sell(long num, double price);
void update(double price);
void show() const;
};

#endif // STOCK_H_

stock.cpp (最终版本)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
// filepath: d:\ProgramData\files_Cpp\250424\stock.cpp
#include <iostream>
#include "stock.h"

// 构造函数定义
Stock::Stock() {
company = "no name";
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::cerr << "Error: Number of shares can't be negative. "
<< company << " shares set to 0.\n"; // 使用 cerr 输出错误
shares = 0;
} else {
shares = n;
}
share_val = pr;
set_tot();
}

// 析构函数定义
Stock::~Stock() {
// 在这个简单类中,析构函数体可以为空
// std::cout << "Destructor called for " << company << "\n"; // 可以取消注释来观察
}

// 其他成员函数定义
void Stock::buy(long num, double price) {
if (num < 0) {
std::cerr << "Error: Number of shares purchased can't be negative. "
<< "Transaction aborted.\n";
} else {
shares += num;
share_val = price;
set_tot();
}
}

void Stock::sell(long num, double price) {
if (num < 0) {
std::cerr << "Error: Number of shares sold can't be negative. "
<< "Transaction aborted.\n";
} else if (num > shares) {
std::cerr << "Error: You can't sell more than you have! "
<< "Transaction aborted.\n";
} else {
shares -= num;
share_val = price;
set_tot();
}
}

void Stock::update(double price) {
share_val = price;
set_tot();
}

void Stock::show() const {
// 使用 iomanip 来设置格式可能更清晰
std::ios_base::fmtflags orig =
std::cout.setf(std::ios_base::fixed, std::ios_base::floatfield);
std::streamsize prec = std::cout.precision(3);

std::cout << "Company: " << company
<< " Shares: " << shares << '\n';
std::cout << " Share Price: $" << share_val;
std::cout.precision(2);
std::cout << " Total Worth: $" << total_val << '\n';

std::cout.setf(orig, std::ios_base::floatfield);
std::cout.precision(prec);
}

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

  • 构造函数:
    • 与类同名,无返回类型。
    • 在创建对象时自动调用,用于初始化对象。
    • 可以重载(提供不同的参数列表)。
    • 如果没有定义任何构造函数,编译器会生成一个默认构造函数(无参数,什么也不做)。
    • 如果定义了任何构造函数,编译器不再生成默认构造函数;如果需要无参数构造,必须自己定义。
  • 析构函数:
    • 类名前加 ~,无返回类型,无参数。
    • 在对象销毁时自动调用,用于清理资源。
    • 不能重载,一个类只有一个析构函数。
    • 如果类中使用了 new 分配资源,通常需要在析构函数中使用 delete 来释放。

构造函数和析构函数是 C++ 类机制的重要组成部分,它们确保了对象的正确初始化和资源的安全释放,是实现资源获取即初始化 (RAII - Resource Acquisition Is Initialization) 这一重要 C++ 编程范式的基石。

10.4 this 指针

在 C++ 类的成员函数内部,你有时可能需要引用调用该函数的对象本身。例如,当你在 stock1.show() 的实现代码中,如何明确地指代 stock1 这个对象?C++ 为此提供了一个特殊的指针,称为 this 指针。

this 指针是什么?

this 是一个隐含的指针,它存在于每个非静态成员函数(non-static member function)内部。它指向调用该成员函数的那个对象

  • 当你调用 stock1.show() 时,在 show() 函数的内部,this 指针就指向 stock1 对象。
  • 当你调用 stock2.buy(..) 时,在 buy() 函数的内部,this 指针就指向 stock2 对象。

编译器在调用成员函数时,会隐式地将对象的地址传递给该函数,这个地址就被 this 指针所持有。

关键点:

  1. 隐含参数: this 指针是作为隐含参数传递给非静态成员函数的。你不需要在函数参数列表中显式声明它。
  2. 指向调用对象: 它总是指向当前正在执行其成员函数的那个对象。
  3. 类型: this 指针的类型是 ClassName * const(对于非 const 成员函数)或 const ClassName * const(对于 const 成员函数)。这意味着 this 指针本身是一个常量指针(不能让它指向其他对象),并且对于 const 成员函数,它指向一个 const 对象(不能通过 this 修改对象的数据成员)。
  4. 访问成员: 在成员函数内部,当你直接访问数据成员(如 shares)或调用其他成员函数(如 set_tot())时,实际上是编译器隐式地使用了 this 指针,等同于 this->sharesthis->set_tot()

使用 this 指针

大多数情况下,你不需要显式地使用 this 指针,因为编译器会自动处理。例如,在 Stock::buy 函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void Stock::buy(long num, double price) {
if (num < 0) {
// ...
} else {
// shares += num; // 隐式使用 this->shares
// share_val = price; // 隐式使用 this->share_val
// set_tot(); // 隐式调用 this->set_tot()

// 下面是显式使用 this 的等效写法:
this->shares += num;
this->share_val = price;
this->set_tot();
}
}

然而,在某些特定场景下,显式使用 this 指针是必要的或有用的:

  1. 区分同名参数和成员: 当成员函数的参数名与数据成员名相同时,需要使用 this-> 来明确指定访问的是数据成员。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    class Example {
    private:
    int value;
    public:
    // 参数名 value 与数据成员 value 相同
    Example(int value) {
    // 必须使用 this->value 来引用数据成员
    this->value = value;
    }
    void print() const { std::cout << value << std::endl; } // 这里访问的是成员 value
    };

    int main() {
    Example ex(10); // 调用构造函数
    ex.print(); // 输出 10
    return 0;
    }

    (虽然这种命名方式有时会用到,但一些编码规范建议避免参数名和成员名完全相同,例如使用 m_valuevalue_ 作为成员名)

  2. 返回对象自身的引用或指针: 当成员函数需要返回调用该函数的对象本身时(通常是为了支持**方法链式调用 (Method Chaining)**)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    class Counter {
    private:
    int count = 0;
    public:
    Counter& increment() { // 返回类型是 Counter&
    count++;
    return *this; // 返回调用对象自身的引用
    }

    Counter& add(int val) { // 返回类型是 Counter&
    count += val;
    return *this; // 返回调用对象自身的引用
    }

    void display() const {
    std::cout << "Count: " << count << std::endl;
    }
    };

    int main() {
    Counter c;
    c.display(); // Output: Count: 0

    // 方法链式调用
    c.increment().add(5).increment(); // 先调用 increment(), 返回 c 的引用;
    // 再对 c 调用 add(5), 返回 c 的引用;
    // 最后对 c 调用 increment()

    c.display(); // Output: Count: 7
    return 0;
    }

    increment()add() 中,return *this; 返回的是调用对象(c)的引用,使得可以在其后继续调用该对象的其他成员函数。

  3. 在友元函数或独立函数中传递对象: 虽然 this 本身只在成员函数内可用,但你可以将 *this(对象本身)或 this(对象地址)传递给需要操作该对象的其他函数。

thisStock

让我们看看 this 如何应用于之前的 Stock 类。假设我们想添加一个方法,用于比较两个 Stock 对象的总价值,并返回总价值较高的那个对象的引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 在 stock.h 的类定义中添加声明
class Stock {
// ... private members ...
public:
// ... constructors, destructor, buy, sell, update, show ...


// 新增方法:比较总价值,返回价值更高的对象的引用
// 第一个const(返回值前):表示返回的引用是常量,不能被修改
// 第二个const(参数中) :表示参数是常量引用,函数内不能修改参数
// 第三个const(函数末尾):表示这是一个常量成员函数,不能修改调用对象的成员变量
const Stock& topval(const Stock& s) const;
};

// 在 stock.cpp 中添加定义
#include "stock.h"

// ... 其他定义 ...

// 定义 topval 方法
const Stock& Stock::topval(const Stock& s) const {
// this->total_val 是调用该方法的对象 (e.g., stock1) 的总价值
// s.total_val 是传入的参数对象 (e.g., stock2) 的总价值
if (s.total_val > this->total_val) {
return s; // 返回传入的对象 s
} else {
return *this; // 返回调用该方法的对象自身 (*this)
}
// 注意:函数声明和定义末尾的 const 表示此函数不会修改任何 Stock 对象,
// 因此 this 的类型是 const Stock* const,*this 的类型是 const Stock&
}

// 使用示例
#include <iostream>
#include "stock.h"

int main() {
Stock stock1("Company A", 100, 10.0); // total_val = 1000.0
Stock stock2("Company B", 50, 25.0); // total_val = 1250.0

stock1.show();
stock2.show();

const Stock& top = stock1.topval(stock2); // 调用 stock1.topval,传入 stock2
// 内部比较 s(stock2).total_val 和 this(stock1)->total_val
// 因为 stock2 价值更高,返回 stock2 的引用

std::cout << "\nTop value stock:\n";
top.show(); // 显示的是 stock2 的信息

return 0;
}

在这个例子中,this->total_val 明确表示访问的是调用 topval 函数的那个对象(stock1)的 total_val 成员,而 s.total_val 访问的是作为参数传递进来的对象(stock2)的 total_valreturn *this; 则返回了调用对象 stock1 本身的引用。

总结

  • this 是一个指向调用对象的指针,在非静态成员函数内部可用。
  • 它使得成员函数能够访问和操作调用它的那个特定对象的数据成员和成员函数。
  • 大多数情况下,this 的使用是隐式的。
  • 显式使用 this 主要用于:
    • 区分同名的参数和数据成员。
    • 从成员函数中返回调用对象自身的引用或指针(常用于方法链)。
  • this 指针的类型取决于成员函数是否为 const

10.5 对象数组

就像可以创建 intdoublechar 的数组一样,你也可以创建类对象的数组。数组的每个元素都是一个该类的对象。

声明对象数组

声明对象数组的语法与声明基本类型数组类似:

1
ClassName arrayName[numberOfElements];

例如,要创建一个包含 4 个 Stock 对象的数组:

1
2
3
4
#include "stock.h" // 假设 Stock 类定义在此

const int STKS = 4;
Stock myStocks[STKS]; // 创建一个包含 4 个 Stock 对象的数组

构造函数的调用

当程序创建对象数组时,它需要为数组中的每个元素(对象)调用构造函数。

  • 默认构造函数: 如果在声明数组时没有为元素提供显式的初始化值,程序将为数组中的每个元素调用类的**默认构造函数 (Default Constructor)**。

    • 在上面的例子 Stock myStocks[STKS]; 中,Stock::Stock() 这个默认构造函数将被调用 4 次,为 myStocks[0], myStocks[1], myStocks[2], myStocks[3] 这四个对象进行初始化。
    • 重要: 如果类没有默认构造函数(例如,你只定义了带参数的构造函数,而没有定义无参数的构造函数或使用 = default),那么尝试创建像 Stock myStocks[STKS]; 这样的未初始化数组将导致编译错误
  • 带参数的构造函数: 你可以在声明数组时使用初始化列表来为数组元素指定不同的构造函数调用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include "stock.h"

    const int STKS = 2;
    Stock portfolio[STKS] = {
    Stock("NanoSmart", 12, 20.0), // 调用 Stock(const string&, long, double)
    Stock("Boffo Objects", 200, 2.0) // 调用 Stock(const string&, long, double)
    };

    // C++11 及以后版本可以使用更简洁的列表初始化
    Stock investments[] = { // 编译器会自动计算数组大小 (3)
    Stock("Fleep Co", 5, 15.5),
    Stock(), // 调用默认构造函数 Stock()
    Stock("MacroHard", 18, 75.0)
    };
    • portfolio 数组的例子中,portfolio[0] 使用提供的参数调用 Stock(const string&, long, double) 构造函数,portfolio[1] 也一样。

    • investments 数组的例子中,investments[0]investments[2] 调用带参数的构造函数,而 investments[1] 则显式调用了默认构造函数 Stock()

    • 如果初始化列表提供的初始值数量少于数组大小,则剩余的元素将使用默认构造函数进行初始化。如果类没有默认构造函数,这将导致编译错误。

      1
      2
      3
      4
      5
      Stock funds[4] = {
      Stock("A Corp", 10, 1.0),
      Stock("B Ltd", 20, 2.0)
      // funds[2] 和 funds[3] 将使用默认构造函数 Stock() 初始化
      };

访问对象数组成员

访问数组中对象的成员与访问基本类型数组元素类似,先用索引 [] 选择数组中的特定对象,然后使用点号 . 访问该对象的公有成员(数据或函数)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <iostream>
#include "stock.h"

const int STKS = 4;

int main() {
// 使用初始化列表创建并初始化数组
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(); // 调用数组中第 st 个对象的 show() 方法
}

// 找到价值最高的股票 (使用上一节的 topval 假设它已添加)
// 注意:topval 需要添加到 Stock 类中才能编译
/*
const Stock* top = &stocks[0]; // 假设第一个是最高的
for (st = 1; st < STKS; st++) {
top = &top->topval(stocks[st]); // 比较并更新 top 指针
}
std::cout << "\nMost valuable holding:\n";
top->show(); // 显示价值最高的股票信息
*/

// 修改数组中某个对象的状态
std::cout << "\nBuying more Boffo Objects...\n";
stocks[1].buy(50, 2.5); // 调用 stocks[1] 对象的 buy() 方法
stocks[1].show();

return 0;
}

输出示例 (假设 topval 部分被注释掉):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Constructor using NanoSmart called
Constructor using Boffo Objects called
Constructor using Monolithic Obelisks called
Constructor using Fleep Enterprises called
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

Buying more Boffo Objects...
Company: Boffo Objects Shares: 250
Share Price: $2.500 Total Worth: $625.00
Bye, Fleep Enterprises!
Bye, Monolithic Obelisks!
Bye, Boffo Objects!
Bye, NanoSmart!

注意:程序结束时,数组 stocks 中的每个对象的析构函数都会被调用(按与构造相反的顺序)。

动态对象数组

你也可以使用 new 来创建动态的对象数组。

1
2
int size = 5;
Stock *portfolio = new Stock[size]; // 创建包含 5 个 Stock 对象的动态数组
  • 默认构造函数要求: 使用 new ClassName[size] 这种形式时,必须要求类具有可访问的默认构造函数,因为它会为数组中的每个元素调用默认构造函数。

  • C++11 列表初始化 (可选): C++11 允许在使用 new 创建数组时提供初始化列表,这样可以调用特定的构造函数,并且如果提供了所有元素的初始化值,则不强制要求默认构造函数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // C++11 or later
    Stock *p_list = new Stock[3] {
    Stock("X Inc", 1, 1.0),
    Stock("Y Ltd", 2, 2.0),
    Stock("Z LLC", 3, 3.0)
    };
    Stock *p_partial = new Stock[4] {
    Stock("A", 1, 1.0),
    Stock("B", 2, 2.0)
    // p_partial[2] 和 p_partial[3] 需要默认构造函数
    };
  • 访问: 访问动态数组成员可以使用数组表示法 portfolio[i].member 或指针表示法 (portfolio + i)->member

  • 释放内存: 必须使用 delete [] 来释放动态分配的对象数组,以确保每个对象的析构函数都被正确调用。

    1
    2
    3
    delete [] portfolio; // 调用 5 次析构函数,然后释放内存
    delete [] p_list;
    delete [] p_partial;

    错误: 使用 delete portfolio; 只会调用第一个元素的析构函数,并可能导致内存泄漏或未定义行为。

总结

  • 可以像创建基本类型数组一样创建对象数组。
  • 创建对象数组时,会为每个元素调用构造函数。
  • 如果未提供显式初始化,则调用默认构造函数。因此,对于未初始化的数组或使用 new ClassName[size] 创建的动态数组,类必须有可访问的默认构造函数。
  • 可以使用初始化列表为数组成员指定不同的构造函数。
  • 通过 arrayName[index].member 的方式访问数组成员。
  • 动态对象数组使用 new ClassName[size] 创建,并必须使用 delete [] arrayPtr 释放。

10.6 类作用域

我们已经知道,在函数内部或代码块内部定义的变量具有局部作用域(块作用域)。类似地,在类中定义的名称(数据成员、成员函数、嵌套类型、枚举等)也有其特定的作用域,称为**类作用域 (Class Scope)**。

类作用域的规则:

  1. 内部可见性: 在类声明或成员函数定义内部,可以直接访问类的成员(数据成员、成员函数、枚举等),无需特殊限定。
  2. 外部访问限制: 在类的外部,不能直接访问类的成员。必须通过对象(使用点号 . 或箭头 ->)或者通过类名和作用域解析运算符 ::(对于静态成员、嵌套类型或枚举)来访问。
  3. 名称隔离: 类作用域意味着在一个类内部定义的名称不会与在另一个类或全局作用域中定义的同名名称冲突。例如,两个不同的类可以都有一个名为 count 的数据成员。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class ClassA {
public:
int count; // ClassA 的 count
void process() { value = 10; } // 可以直接访问 value
private:
int value; // ClassA 的 value
};

class ClassB {
public:
double count; // ClassB 的 count (与 ClassA::count 不冲突)
void calculate() { /* ... */ }
};

int global_count = 100; // 全局变量

int main() {
ClassA objA;
ClassB objB;

objA.count = 1; // 访问 ClassA 对象的 count 成员
objB.count = 2.5; // 访问 ClassB 对象的 count 成员

// 错误!不能直接访问类内部的名称
// count = 5; // 访问哪个 count?
// value = 20; // 错误!value 在类作用域内

objA.process(); // 通过对象调用成员函数

return 0;
}

作用域解析运算符 ::

当我们需要在类外部引用类作用域内的名称时(例如,在定义成员函数或访问静态成员时),就需要使用类名和作用域解析运算符 ::

1
2
3
4
5
// 在类外部定义成员函数
void ClassA::process() { // 使用 ClassA:: 指明 process 属于 ClassA
value = 10; // 在成员函数内部,可以直接访问其他成员
this->count = 5; // 也可以显式使用 this
}

10.6.1 作用域为类的常量

有时,我们希望在类中定义一个常量,这个常量对于该类的所有对象来说都是一样的,并且可能在编译时就需要知道它的值(例如,用于指定数组大小)。

有几种方法可以在类作用域内创建常量:

  1. static const 成员 (整型或枚举类型 - C++11 前常用):
    对于整型(int, char, bool 等)或枚举类型的常量,可以在类定义内部使用 static const 直接初始化。static 意味着这个常量属于类本身,而不是任何特定对象(所有对象共享同一个常量),const 意味着它的值不能被修改。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class Bakery {
    private:
    // 这个常量属于类,所有对象共享
    static const int Months = 12; // 声明并初始化静态常量成员
    double costs[Months];
    public:
    // ...
    };

    // 注意:如果需要在类外部获取该常量的地址,
    // 或者编译器要求(较旧的编译器可能需要),
    // 可能还需要在源文件中提供一个定义(不带初始值):
    // const int Bakery::Months; // 可选的定义

    这种方式不能用于初始化非整型或非枚举类型的静态常量。

  2. static constexpr 成员 (C++11 及以后):
    C++11 引入了 constexpr,它允许在编译时计算常量表达式。使用 static constexpr 可以定义各种类型的类作用域常量,只要初始化表达式是常量表达式即可。这是现代 C++ 中定义类常量的推荐方式。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Config {
    public:
    static constexpr int MaxUsers = 100;
    static constexpr double Rate = 1.5;
    static constexpr const char* DefaultMsg = "Welcome";
    // ...
    };

    int main() {
    int userLimit = Config::MaxUsers; // 直接使用类名访问
    double currentRate = Config::Rate;
    // ...
    return 0;
    }

    static constexpr 成员默认是内联的,通常不需要在类外部再次定义。

  3. 枚举技巧 (Enum Hack - C++11 前的变通方法):
    在 C++11 之前,如果想在类中定义一个非整型的常量(或者只是想避免 static const 可能需要的外部定义),有时会使用匿名枚举。

    1
    2
    3
    4
    5
    6
    7
    class LegacyBox {
    private:
    enum { MaxSize = 100 }; // 枚举技巧
    int items[MaxSize]; // 使用枚举量作为数组大小
    public:
    // ...
    };

    MaxSize 就像一个值为 100 的整型常量,但它是一个枚举量。这种方法现在已不常用,static constexpr 是更好的选择。

10.6.2 作用域内枚举 (Scoped Enumerations - C++11)

传统的 C++ 枚举(enum)存在一些问题:

  • 名称冲突: 枚举量(enumerators)被放置在与枚举定义相同的作用域中,容易与其他名称(包括其他枚举的枚举量)发生冲突。
  • 隐式转换: 枚举量可以隐式地转换为整型,有时这可能导致逻辑错误或降低类型安全性。
1
2
3
4
5
6
7
enum OldColor { RED, GREEN, BLUE };
enum StopLight { RED, YELLOW, GREEN }; // 错误!RED 和 GREEN 重定义

OldColor myColor = RED; // OK
int colorValue = myColor; // OK, 隐式转换为 int (值为 0)

if (myColor == 0) { /* ... */ } // 可以和整数比较

为了解决这些问题,C++11 引入了**作用域内枚举 (Scoped Enumerations)**,使用 enum classenum struct 关键字定义。

特点:

  1. 作用域限制: 枚举量的作用域被限制在枚举本身内部。访问枚举量必须使用枚举名称和作用域解析运算符 ::
  2. 无隐式转换: 作用域内枚举类型不能隐式地转换为整型或其他类型。如果需要转换,必须使用显式类型转换(如 static_cast)。
  3. 类型安全: 增强了类型安全性,不同作用域枚举类型的值不能直接比较(除非重载了比较运算符)。
  4. 可指定底层类型: 可以显式指定枚举使用的底层整数类型(默认为 int)。

语法:

1
2
3
4
enum class EnumName : UnderlyingType { Enumerator1, Enumerator2, ... };
// 或者 enum struct (功能相同)
enum struct EnumName : UnderlyingType { Enumerator1, Enumerator2, ... };
// : UnderlyingType 是可选的

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#include <iostream>

// 使用 enum class 定义作用域内枚举
enum class Color : unsigned char { // 指定底层类型为 unsigned char
RED, // Color::RED
GREEN, // Color::GREEN
BLUE // Color::BLUE
};

enum class TrafficLight {
RED, // TrafficLight::RED (与 Color::RED 不冲突)
YELLOW,
GREEN
};

int main() {
Color myColor = Color::RED; // 必须使用作用域解析符
TrafficLight light = TrafficLight::GREEN;

// 错误!枚举量不在当前作用域
// Color anotherColor = RED;

// 错误!不能隐式转换为 int
// int colorCode = myColor;

// 需要显式转换
int colorCode = static_cast<int>(myColor);
unsigned char underlyingValue = static_cast<unsigned char>(myColor);

std::cout << "Color code: " << colorCode << std::endl; // 输出 0
std::cout << "Underlying value: " << static_cast<int>(underlyingValue) << std::endl; // 输出 0 (需要再次转换才能打印为数字)

// 错误!不同枚举类型不能直接比较
// if (myColor == light) { /* ... */ }

// 可以与相同类型的枚举量比较
if (myColor == Color::RED) {
std::cout << "The color is red." << std::endl;
}

if (light == TrafficLight::GREEN) {
std::cout << "The light is green." << std::endl;
}

return 0;
}

作用域内枚举是现代 C++ 中定义枚举类型的首选方式,因为它更安全、更不容易出错,并且避免了名称污染。

总结

  • 类成员(数据、函数、类型、常量、枚举)具有类作用域
  • 在类外部访问类成员需要通过对象(.->)或类名(::)。
  • 类作用域可以隔离名称,防止与全局或其他类中的名称冲突。
  • 可以使用 static const (整型/枚举) 或 static constexpr (C++11, 推荐) 在类内部定义常量。
  • C++11 引入了**作用域内枚举 (enum classenum struct)**,其枚举量作用域限制在枚举内,且不能隐式转换为整型,提高了代码的安全性和清晰度。

10.7 抽象数据类型

我们在本章中学习的类是 C++ 实现抽象数据类型 (Abstract Data Type, ADT) 的一种方式。ADT 是一种计算机科学的概念,它是一种数学模型,用于描述具有特定行为(语义)的数据类型,重点在于可以对数据执行的操作,而不是这些操作的具体实现方式或数据的内部表示。

ADT 的核心思想:

  1. 数据封装: ADT 将数据以及对这些数据进行操作的函数捆绑在一起。
  2. 接口与实现分离: ADT 定义了一个公共接口 (Interface)**,即一组可以对数据执行的操作。用户通过这个接口与数据交互,而不需要知道数据是如何存储的,或者操作是如何实现的(实现细节隐藏**)。

可以把 ADT 想象成一个“黑盒子”。你知道你可以给这个盒子提供什么输入(通过接口调用操作),以及你会得到什么输出或结果,但你不需要(也不能)看到盒子内部的构造。

与内置类型的类比:

想想 C++ 的内置类型 int

  • 数据: 它可以表示整数。
  • 操作: 你可以对 int 执行加、减、乘、除、比较等操作。
  • 抽象: 你使用这些操作时,并不需要关心 int 在内存中是如何用二进制位表示的,或者加法操作在 CPU 层面是如何执行的。你只关心操作的效果

ADT 将这种思想扩展到了用户自定义的数据类型。

C++ 类如何实现 ADT:

C++ 类天然地支持 ADT 的概念:

  • 数据表示: 类的数据成员(通常是 private)用于存储 ADT 的数据。
  • 操作接口: 类的公有成员函数(public methods)定义了 ADT 的公共接口,即允许外部代码执行的操作。
  • 实现隐藏: 将数据成员设为 private,并将实现细节(如私有辅助函数、成员函数的具体代码)与接口(类定义中的公有声明)分离,实现了数据隐藏和封装。

Stock 类为例:

我们可以将 Stock 类视为一个“股票持有” ADT。

  • ADT 描述: 一个表示某公司股票持有情况的类型。
  • 数据 (概念上): 公司名称、持有股数、当前股价、总价值。
  • 操作 (接口):
    • 创建一个股票持有记录(构造函数)。
    • 买入指定数量的股票(buy 方法)。
    • 卖出指定数量的股票(sell 方法)。
    • 更新股票价格(update 方法)。
    • 显示股票持有信息(show 方法)。
    • (可能还有) 获取总价值、获取公司名称等。
  • 实现 (隐藏细节):
    • 数据成员 company, shares, share_val, total_val 的具体类型(std::string, long, double)。
    • 私有辅助函数 set_tot() 的存在及其实现。
    • buy, sell, update, show 等函数的具体代码逻辑。

使用 Stock 类的程序员(客户端代码)只需要了解其公共接口(public 方法)。他们可以创建 Stock 对象,调用 buy(), sell(), show() 等方法来完成任务,而无需关心 total_val 是如何计算和更新的,或者 company 是用 std::string 还是 C 风格字符串存储的。如果类的设计者决定改变内部实现(例如,优化 set_tot 的计算),只要公共接口保持不变,客户端代码就无需修改。

ADT 的好处:

  • 抽象: 简化复杂性,让用户关注“做什么”而非“怎么做”。
  • 封装: 保护数据不被意外破坏,隐藏实现细节。
  • 模块化: 将程序分解为独立的、功能明确的单元(类/ADT)。
  • 可维护性: 修改一个 ADT 的内部实现不会影响使用该 ADT 的其他代码(只要接口不变)。
  • 可重用性: 设计良好的 ADT 可以在不同的程序中重复使用。

因此,在设计 C++ 类时,以 ADT 的思维方式进行思考——明确这个类代表什么概念,它应该提供哪些操作(公共接口),以及需要隐藏哪些内部细节——是非常有益的。这有助于创建出结构清晰、易于使用和维护的代码。

10.8 总结

本章介绍了面向对象编程(OOP)的核心概念,并深入探讨了 C++ 实现 OOP 的主要机制——**类 (Class)**。类是用户定义类型的基础,它允许我们将数据和操作数据的函数封装在一起。

主要内容回顾:

  1. 过程性编程 vs. 面向对象编程: 过程性编程关注执行步骤和函数,而面向对象编程关注数据及其相关操作,将它们封装在对象 (Object) 中。OOP 的核心思想包括封装、抽象、继承和多态。

  2. 抽象和类:

    • 抽象是关注本质特征、忽略实现细节的过程。
    • 是创建对象的蓝图,定义了对象的数据成员(属性)成员函数(方法)
    • 访问说明符public, private, protected)控制对类成员的访问。public 成员构成类的公共接口,而 private 成员(通常是数据)实现了数据隐藏,是封装的关键。
    • 类的成员函数通常在类定义中声明,在单独的源文件中使用作用域解析运算符 :: 定义。
    • 通过类的对象使用**点号 .**(或指针使用箭头 ->)访问其公有成员。
  3. 构造函数和析构函数:

    • 构造函数是与类同名的特殊成员函数,在创建对象时自动调用,用于初始化对象。它可以被重载。如果用户未定义任何构造函数,编译器会生成一个默认构造函数(无参)。如果用户定义了任何构造函数,编译器就不再生成默认构造函数。
    • 析构函数是类名前加 ~ 的特殊成员函数,在对象生命周期结束时自动调用,用于执行清理工作(如释放 new 分配的内存)。它没有参数,不能重载。
  4. this 指针:

    • 每个非静态成员函数都有一个隐含的 this 指针,指向调用该函数的对象。
    • 通常隐式使用,但在需要区分同名参数和成员、或需要返回对象自身引用/指针(如链式调用)时显式使用 this*this
  5. 对象数组:

    • 可以创建类对象的数组。
    • 创建数组时,会为每个元素调用构造函数(通常是默认构造函数,除非使用初始化列表)。
    • 访问方式为 arrayName[index].member
    • 动态对象数组使用 new ClassName[size] 创建,需要默认构造函数,并用 delete [] ptr 释放。
  6. 类作用域:

    • 类成员(数据、函数、类型、常量、枚举)具有类作用域,在类外部访问需要限定。
    • 可以使用 static conststatic constexpr (C++11 推荐) 定义类范围内的常量。
    • C++11 引入了**作用域内枚举 (enum class)**,提高了枚举的类型安全性和作用域控制。
  7. 抽象数据类型 (ADT):

    • ADT 是一个侧重于操作接口而非内部实现的数学模型。
    • C++ 类是实现 ADT 的强大工具,通过公共接口提供操作,通过私有成员隐藏实现细节,体现了封装和抽象的原则。

通过使用类,我们可以创建模块化、可重用、易于维护的复杂程序,更好地模拟现实世界的问题。掌握类的设计和使用是精通 C++ 的关键一步。

评论